glimmerjs / glimmer.js

Central repository for the Glimmer.js project
https://glimmerjs.com
MIT License
752 stars 75 forks source link

Passing arguments into components while testing. #14

Closed lcpriest closed 5 years ago

lcpriest commented 7 years ago

Hi team!

Is it possible to pass arguments into components while rendering them for testing?

  test('it renders', async function(assert) {
    this.rating = 10;

    await this.render(hbs`<feedback-form rating={{rating}} />`);
}

I understand that this is a contrived example as I could just pass "10" directly, but I would also like to be able to pass closure actions (not in current documentation, I found an example here).

tomdale commented 7 years ago

There is not a super easy API for doing this yet, although it's being worked on and is high priority once all of the recent performance-related work stabilizes.

If you have the appetite for hacking on low-level VM APIs, there is theoretically a way to do this today.

The core Glimmer "initial render loop" happens when you create a template iterator and call .next() on it until it is done. (example)

The template iterator encapsulates a few different pieces of state required to perform the render. One of these things is the "main" or "root" template—the entry point where Glimmer starts rendering. Note that this is a very simple template only; there's no associated component. (example, glimmer.js main template)

So if there's no component, how does the template get data? If I type {{someVal}} in the root template, where does someVal come from?

Specifically for the root template, that comes from a special Reference we call "self."

In Glimmer.js apps, self is a reference that contains an array, _roots, which is a list of metadata for every component that you've rendered via renderComponent. (example)

If we combine these concepts, it should be possible to build a test helper that:

  1. Compiles a new template invoking the component under test, like <FeedbackForm @rating={{rating}} />.
  2. Creates a new self reference that wraps { rating: 10 } (or whatever args you might want to pass).
  3. Joins the compiled template and the self plus the other state/configuration in the example above into a new template iterator and renders it.

I wanted to write down how this stuff works in case someone is highly motivated to dig in themselves, but I'll be upfront that it's a non-trivial amount of work involving low-level parts of the VM. It might be a fun project, but if you're just wanting to get work done, the best answer might be to wait for the renderComponent API that can take arbitrary arguments to get finished. 😁

rondale-sc commented 6 years ago

Feature added here: https://github.com/glimmerjs/glimmer-test-helpers/pull/4

Bump @glimmer/test-helpers (major update)

rondale-sc commented 6 years ago

Small update here:

The update in the PR I linked unregistered the main ComponentManager which we needed. https://github.com/glimmerjs/glimmer-test-helpers/pull/5 Fixes that. I've confirmed in my own project that on "@glimmer/test-helpers": "0.31.1" You can write tests like this:

test('should call preloadImages on didInsertElement', async function(assert) {
  assert.expect(1);

  this.images = [{ retinaSource: 'foo', source: 'bar' }];
  await this.render(hbs`<PreloadImages @images={{this.images}} />`);

  // assertion occurs in a spy
});
givanse commented 5 years ago

This is currently working. The issue can be closed.