aurelia/aurelia

View on GitHub
packages/__tests__/src/3-runtime-html/enhance.spec.ts

Summary

Maintainability
B
4 hrs
Test Coverage
// This is to test for some intrinsic properties of enhance which is otherwise difficult to test in Data-driven tests parallel to `.app`
import { BrowserPlatform } from '@aurelia/platform-browser';
import { Constructable, DI, IContainer, Registration, onResolve, resolve } from '@aurelia/kernel';
import {
  CustomElement,
  ICustomElementViewModel,
  IPlatform,
  Aurelia,
  customElement,
  bindable,
  StandardConfiguration,
  ValueConverter,
  IAppRoot,
  AppTask,
  BindingBehavior,
} from '@aurelia/runtime-html';
import { assert, TestContext } from '@aurelia/testing';
import { createSpecFunction, TestExecutionContext, TestFunction } from '../util.js';
import { delegateSyntax } from '@aurelia/compat-v1';

describe('3-runtime-html/enhance.spec.ts', function () {
  interface TestSetupContext {
    getComponent: () => Constructable | ICustomElementViewModel;
    template: string;
    childIndex: number;
    beforeHydration: (host: HTMLElement, child: ChildNode | null) => void;
  }
  class EnhanceTestExecutionContext implements TestExecutionContext<any> {
    public constructor(
      public ctx: TestContext,
      public container: IContainer,
      public host: HTMLElement,
      public app: any,
      public childNode: ChildNode | null,
      public platform = container.get(IPlatform),
    ) { }
  }
  async function testEnhance(
    testFunction: TestFunction<EnhanceTestExecutionContext>,
    {
      getComponent,
      template,
      childIndex,
      beforeHydration
    }: Partial<TestSetupContext> = {}
  ) {
    const ctx = TestContext.create();

    const host = ctx.doc.createElement('div');
    host.innerHTML = template;
    ctx.doc.body.appendChild(host);
    const child = childIndex !== void 0
      ? host.childNodes.item(childIndex)
      : null;

    if (typeof beforeHydration === 'function') {
      beforeHydration(host, child);
    }

    const container = ctx.container;
    const au = new Aurelia(container);
    const enhanceRoot = await au.enhance({ host, component: getComponent() });

    const app = enhanceRoot.controller.scope.bindingContext;
    await testFunction(new EnhanceTestExecutionContext(ctx, container, host, app, child));

    await au.stop();
    await enhanceRoot.deactivate();
    ctx.doc.body.removeChild(host);
    au.dispose();
  }

  const $it = createSpecFunction(testEnhance);

  class App1 {
    public foo: string = 'Bar';
  }
  for (const { text, getComponent } of [
    { text: 'class', getComponent: () => CustomElement.define("app", App1) },
    { text: 'instance', getComponent: () => new App1() },
    { text: 'raw object', getComponent: () => ({ foo: 'Bar' }) },
  ]) {
    $it(`hydrates the root - ${text}`, function ({ host }) {
      assert.html.textContent('span', 'Bar', 'span.text', host);
    }, { getComponent, template: `<span>\${foo}</span>` });

    let handled = false;
    $it(`preserves the element reference - ${text}`,
      function ({ host, platform }) {
        handled = false;
        host.querySelector('span').click();
        platform.domQueue.flush();
        assert.equal(handled, true);
      },
      {
        getComponent,
        template: `<span>\${foo}</span>`,
        childIndex: 0,
        beforeHydration(host, child) {
          child.addEventListener('click', function () { handled = true; });
        }
      });
  }

  for (const initialMethod of ['app', 'enhance']) {

    it(`can be applied on an unhydrated inner node after initial hydration - ${initialMethod} - new container`, async function () {
      const message = "Awesome Possum";
      const template = `
    <button click.delegate="enhance()"></button>
    <div ref="r1" innerhtml.bind="'<div>\${message}</div>'"></div>
    <div ref="r2" innerhtml.bind="'<div>\${message}</div>'"></div>
    `;

      class App2 {
        private readonly r1!: HTMLDivElement;
        private readonly r2!: HTMLDivElement;
        public container: IContainer = resolve(IContainer);

        public async attaching() {
          await this.enhance(this.r1);
        }

        private async enhance(host = this.r2) {
          await new Aurelia(TestContext.create().container)
            .enhance({ host: host.querySelector('div'), component: { message } });
        }
      }
      const ctx = TestContext.create();

      const host = ctx.doc.createElement('div');
      ctx.doc.body.appendChild(host);

      const container = ctx.container;
      const au = new Aurelia(container);

      container.register(delegateSyntax);
      let component;
      let dispose: () => void | Promise<void>;
      if (initialMethod === 'app') {
        component = CustomElement.define({ name: 'app', template }, App2);
        await au.app({ host, component }).start();
      } else {
        host.innerHTML = template;
        component = CustomElement.define('app', App2);
        const enhanceRoot = await au.enhance({ host, component });
        dispose = () => onResolve(enhanceRoot.deactivate(), () => enhanceRoot.dispose());
      }

      assert.html.textContent('div', message, 'div', host);

      host.querySelector('button').click();
      await Promise.resolve();
      ctx.platform.domQueue.flush();
      ctx.platform.domQueue.flush();

      assert.html.textContent('div:nth-of-type(2)', message, 'div:nth-of-type(2)', host);

      await au.stop();
      await dispose?.();
      ctx.doc.body.removeChild(host);
      au.dispose();
    });
  }

  it(`respects the hooks in raw object component`, async function () {
    const ctx = TestContext.create();

    const host = ctx.doc.createElement('div');
    host.innerHTML = '<span></span>';
    ctx.doc.body.appendChild(host);

    const component = {
      eventLog: [],
      hydrating() { this.eventLog.push('hydrating'); },
      hydrated() { this.eventLog.push('hydrated'); },
      created() { this.eventLog.push('created'); },
      binding() { this.eventLog.push('binding'); },
      bound() { this.eventLog.push('bound'); },
      attaching() { this.eventLog.push('attaching'); },
      attached() { this.eventLog.push('attached'); },
    };
    const container = ctx.container;
    const au = new Aurelia(container);
    const enhanceRoot = await au.enhance({ host, component });

    await au.stop();
    await enhanceRoot.deactivate();
    ctx.doc.body.removeChild(host);

    assert.deepStrictEqual(component.eventLog, [
      'hydrating',
      'hydrated',
      'created',
      'binding',
      'bound',
      'attaching',
      'attached',
    ]);
    au.dispose();
  });

  it('throws when enhancing a realmless node (without window connected document)', function () {
    const ctx = TestContext.create();
    assert.throws(() => new Aurelia().enhance({
      host: new ctx.DOMParser().parseFromString('<div></div>', 'text/html').body.firstElementChild as HTMLElement,
      component: {}
    }));
  });

  it(`enhance works on detached node`, async function () {
    @customElement({ name: 'my-element', template: '<span>${value}</span>' })
    class MyElement {
      @bindable public value: string;
    }
    @customElement({
      name: 'app',
      template: '<div ref="container" id="container"></div>'
    })
    class App {
      private enhancedHost: HTMLElement;
      private enhanceView: IAppRoot;
      private readonly container!: HTMLDivElement;

      public async bound() {
        const _host = this.enhancedHost = ctx.doc.adoptNode(new ctx.DOMParser().parseFromString('<div><my-element value.bind="42.toString()"></my-element></div>', 'text/html').body.firstElementChild as HTMLElement);
        // this.container.appendChild(this.enhancedHost);
        const _au = new Aurelia(
          DI.createContainer()
            .register(
              Registration.instance(IPlatform, BrowserPlatform.getOrCreate(globalThis)),
              StandardConfiguration,
            )
        );
        this.enhanceView = await _au
          .register(MyElement) // in real app, there should be more
          .enhance({ host: _host, component: CustomElement.define({ name: 'enhance' }, class EnhanceRoot { }) });

        assert.html.innerEqual(_host, '<my-element><span>42</span></my-element>', 'enhanced.innerHtml');
        assert.html.innerEqual(this.container, '', 'container.innerHtml - before attach');
      }

      public attaching() {
        this.container.appendChild(this.enhancedHost);
      }

      // The inverse order of the stop and detaching is intentional
      public async detaching() {
        await this.enhanceView.deactivate();
        assert.html.innerEqual(this.enhancedHost, '<my-element></my-element>', 'enhanced.innerHtml');
        assert.html.innerEqual(this.container, '<div><my-element></my-element></div>', 'enhanced.innerHtml');
      }

      public unbinding() {
        this.enhancedHost.remove();
      }
    }

    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');
    ctx.doc.body.appendChild(host);

    const container = ctx.container;
    const au = new Aurelia(container);
    await au
      .app({ host, component: App })
      .start();

    assert.html.innerEqual(host.querySelector('#container'), '<div><my-element><span>42</span></my-element></div>', 'container.innerHTML - after attach');

    await au.stop();

    assert.html.innerEqual(host, '', 'post-stop host.innerHTML');

    ctx.doc.body.removeChild(host);
    au.dispose();
  });

  it('can re-activate an enhancement result', async function () {
    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');

    const container = ctx.container;
    const au = new Aurelia(container);
    host.innerHTML = `<div repeat.for="i of 3">\${i}</div>`;
    const root = await au.enhance({ host, component: { message: 'hello world' } });
    assert.html.textContent(host, '012');
    assert.strictEqual(host.querySelectorAll('div').length, 3);

    await root.deactivate();
    assert.html.textContent(host, '');

    await root.activate();
    assert.html.textContent(host, '012');
    assert.strictEqual(host.querySelectorAll('div').length, 3);
  });

  // we dont need to support this since the activation/deactivation can be awaited in the life cycle of any component
  //
  // it('can connect with parent controller if any', async function () {
  //   let parentController: IController;
  //   const { appHost, component, start, tearDown } = createFixture(
  //     '<my-el html.bind="html" controller.ref="myElController">',
  //     class App {
  //       public html = `<div>\${message}</div>`;
  //       public myElController: ICustomElementController;
  //     },
  //     [
  //       CustomElement.define({
  //         name: 'my-el',
  //         template: '<div innerhtml.bind="html" ref="div">',
  //         bindables: ['html']
  //       }, class MyEl {
  //         public static inject = [IAurelia];
  //         public div: HTMLDivElement;
  //         public enhancedRoot: ICustomElementController;
  //         public constructor(private readonly au$: Aurelia) {}

  //         public async attaching() {
  //           this.div.removeAttribute('class');
  //           this.enhancedRoot = await this.au$.enhance(
  //             {
  //               host: this.div,
  //               component: {
  //                 message: 'Hello _div_',
  //                 attaching(_, parent) {
  //                   parentController = parent;
  //                 }
  //               }
  //             },
  //             (this as any).$controller
  //           );
  //         }

  //         public detaching() {
  //           void this.enhancedRoot.deactivate(this.enhancedRoot, null);
  //           parentController = void 0;
  //         }
  //       }),
  //     ],
  //     false,
  //   );

  //   await start();

  //   assert.notStrictEqual(parentController, void 0);
  //   assert.strictEqual(component.myElController === parentController, true);
  //   assert.html.innerEqual(appHost, '<my-el><div><div>Hello _div_</div></div></my-el>');

  //   await tearDown();
  //   assert.strictEqual(parentController, void 0);
  //   assert.strictEqual(component.myElController, null);
  //   assert.html.innerEqual(appHost, '');
  // });

  it('uses resources in existing root container', async function () {
    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');

    const container = ctx.container.register(ValueConverter.define('x2', class X2 {
      public toView(val: any) {
        return Number(val) * 2;
      }
    }));
    const au = new Aurelia(container);
    host.innerHTML = '<div data-id.bind="1 | x2 | plus10"></div>';
    const root = await au.enhance({
      host,
      component: {},
      container: container.createChild().register(ValueConverter.define('plus10', class Plus10 {
        public toView(val: any) {
          return Number(val) + 10;
        }
      }))
    });

    assert.strictEqual(host.innerHTML, '<div data-id="12"></div>');
    await root.deactivate();
  });

  it('uses resources given in the container', async function () {
    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');

    const container = ctx.container;
    const au = new Aurelia(container);
    host.innerHTML = '<div data-id.bind="i | plus10"></div>';
    const root = await au.enhance({
      host,
      component: { i: 1 },
      container: container.createChild().register(
        ValueConverter.define('plus10', class Plus10 {
          public toView(v: any) {
            return Number(v) + 10;
          }
        }),
        BindingBehavior.define('xoixoi', class Xoixoi {})
      )
    });

    assert.strictEqual(host.innerHTML, '<div data-id="11"></div>');
    assert.strictEqual(ValueConverter.find(container, 'plus10'), null, 'It should register resources with the given container only.');
    assert.strictEqual(BindingBehavior.find(container, 'xoixoi'), null, 'It should register resources with the given container only.');
    await root.deactivate();
  });

  it('calls app tasks', function () {
    const logs = [];
    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');
    const au = new Aurelia();
    host.innerHTML = '<div>${message}</div>';
    const root = au.enhance({ host, component: {
      message: 'hello world',
      created() { logs.push('created'); },
      hydrating() { logs.push('hydrating'); },
      hydrated() { logs.push('hydrated'); },
      binding() { logs.push('binding'); },
      bound() { logs.push('bound'); },
      attaching() { logs.push('attaching'); },
      attached() { logs.push('attached'); },
      detaching() { logs.push('detaching'); },
      unbinding() { logs.push('unbinding'); },
    }, container: ctx.container.createChild().register(
      AppTask.creating(() => logs.push('Task.creating')),
      AppTask.hydrating(() => logs.push('Task.hydrating')),
      AppTask.hydrated(() => logs.push('Task.hydrated')),
      AppTask.activating(() => logs.push('Task.activating')),
      AppTask.activated(() => logs.push('Task.activated')),
      AppTask.deactivating(() => logs.push('Task.deactivating')),
      AppTask.deactivated(() => logs.push('Task.deactivated')),
    )});
    assert.strictEqual(host.textContent, 'hello world');

    const activationLogs = [
      'Task.creating',
      'Task.hydrating',
      'hydrating',
      'hydrated',
      'Task.hydrated',
      'created',
      'Task.activating',
      'binding',
      'bound',
      'attaching',
      'attached',
      'Task.activated',
    ];
    assert.deepStrictEqual(logs, activationLogs);

    logs.length = 0;
    return onResolve(root, (root) => onResolve(root.deactivate(), () => {
      assert.deepStrictEqual(logs, [
        'Task.deactivating',
        'detaching',
        'unbinding',
        'Task.deactivated',
      ]);
    }));
  });

  it('does not call app task on the original container', function () {
    const logs = [];
    const ctx = TestContext.create();
    const host = ctx.doc.createElement('div');
    const au = new Aurelia(ctx.container.register(
      AppTask.creating(() => logs.push('Task.creating')),
      AppTask.deactivating(() => logs.push('Task.deactivating')),
    ));

    return onResolve(
      au.enhance({ host, component: {} }),
      (root) => {
        assert.deepStrictEqual(logs, []);
        return onResolve(root.deactivate(), () => {
          assert.deepStrictEqual(logs, []);
        });
      });
  });
});