glimmerjs / glimmer-experimental

A set of experimental high-level APIs built on Glimmer.js
https://glimmerjs.github.io/glimmer-experimental/
59 stars 26 forks source link

@ember/test-helpers render() style integration tests currently not possible #86

Closed izelnakri closed 3 years ago

izelnakri commented 3 years ago

Hi there,

I've been experimenting with implementing emberx/router and emberx/test-helper library on top of glimmerx: https://github.com/izelnakri/emberx

Currently there is an interesting roadblock for implementing readable and maintanable ember.js like import { render } from 'emberx/test-helpers', it could be a limitation of the current API. I'll explain the situation below, feel free to let me know if there are ways to implement this behavior, currently the documentation links are down.

In case if it doesn't exist, I would appreciate if we could implement it. Because otherwise it is currently not possible for one to write maintainable integration tests for glimmerx when they npm their way to ember from glimmer:

Intention:

import { module, test } from 'qunit';
import { render } from 'emberx/test-helpers';
import { setupRenderingTest } from 'emberx/test-helpers/setup';

import LinkTo from '../../src/components/LinkTo'; // NOTE: imported component to test

module('Integration | <LinkTo />', function (hooks) {
  setupRenderingTest(hooks);

  test('it works for a basic route without params', async function (assert) {
    await render('<LinkTo @route="public.index">Go to homepage</LinkTo>', LinkTo); // NOTE: currently 2nd argument needed unfortunately, explained below

    assert.dom('.ember-testing a').hasText('Go to homepage');
  });
});

Implementation and the roadblock(s):

// in package: emberx/src/test-helpers/index.js:

import GlimmerComponent, { hbs } from '@glimmerx/component';
import { renderComponent } from '@glimmerx/core';
import { getContext } from './context';

export function render(
  template: string,
  component: GlimmerComponent, // NOTE: normally we shouldn't need it if we could build an existing Component registry
  services: object | undefined
): Promise<any> {
  const context = getContext(); // NOTE: this just returns 'this' context of the test where its used
  const targetServices = context.services || services; // NOTE: this render API should be usable without implicit resolvers, thus needs an explicit way to set services context

  // testing something out:
  window.hbs = hbs`<LinkTo @route="something">Another</LinkTo>`; // NOTE: hbs is just a token currently, a precompile transpiler token, a babel plugin/addon 
  // thus no way to get an analysis of the dynamically provided template string in the `render(template)` argument.
  // we need that/or another function to return an AST with passed arguments, 
  // yield references as JS object when an hbs string is provided. 
  // Imported components inside the strings should be tokenized/included in the AST as well, somehow...

  console.log('template is', template); // this only gets transpiled when provided as hbs`` in the source file,
  // so as a string it loses the ComponentDefinition reference when its passed from another file, 
  // also imported Components inside this string miss their references, arguments, yielded state.

  // TODO: in other words we need to figure out a way to serialize template strings with passed in internal components, arguments and yielded context.

  return renderComponent(component, {
    element: document.getElementById('ember-testing'),
    args: Object.assign({}, context), // NOTE: I cant read the provided arguments from the template argument/string at the moment: I should be able to read { route: 'public.index', yield: ... }
    services: Object.assign({}, context, targetServices), // NOTE: interesting approach to make it ember.js compatible but also problematic
  }); 
  // NOTE: I can't read the yielded content in this API: 'Go to homepage' yielded string in this case. 
  // It could also be imported components with arguments/yields
}

Issues:

interface ComponentRegistry {
   [componentName: string]: GlimmerComponent;
}

return renderComponent('<LinkTo @route={{this.targetRoute}}><Icon @name="arrow-right" /> Go to homepage</LinkTo>',
    element: document.getElementById('ember-testing'),
    properties: { targetRoute:  'public.index' },
    yieldContext: {}, // ComponentRegistry{'Icon': IconComponentDefinition, 'AnotherComponent': GlimmerComponentWithYieldedStateAndAttributesAndArguments }
    services: Object.assign({}, servicesInThisContext),
}); 

// or maybe some other function that has the same API, accepts similar arguments as renderComponent, 
// with additional properties, yieldContext option & keys provided above.
izelnakri commented 3 years ago

Thinking again we should probably have an in-browser compiler and and pre-compilation babel transpiler plugin for hbs template function so new glimmerx templates could be read by node.js and deno js runtimes/environments directly. Example:

in-browser compiler: https://riot.js.org/compiler/#in-browser-compilation-with-inline-templates precompiler: https://riot.js.org/compiler/#pre-compilation

izelnakri commented 3 years ago

As it turns on I missed to put the hbs template function to every integration test :facepalm:. So everything works with template imports etc which is awesome! : https://github.com/izelnakri/emberx/commit/7174badcfdd10f5363b53e4088eb35252e01a8be#diff-694dacb1c70161f6edd8614dff46205525d5f04bb3464ea2105d369bd5a447a4

Only one caveat: renderComponent() doest include properties/context so instead of:

await render(hbs`<LinkTo @route="public.index">{{this.linkText}}</LinkTo>`);

I had to do:

await render(hbs`<LinkTo @route="public.index">{{@linkText}}</LinkTo>`);

Closing this issue now, sorry for the noise!