aurelia/aurelia

View on GitHub
packages/__tests__/src/router/router.spec.ts

Summary

Maintainability
F
2 wks
Test Coverage
import { IContainer } from '@aurelia/kernel';
import { IRouter, RouterConfiguration, routes, Viewport } from '@aurelia/router';
import { CustomElement, customElement, IPlatform, Aurelia } from '@aurelia/runtime-html';
import { assert, MockBrowserHistoryLocation, TestContext } from '@aurelia/testing';

describe('router/router.spec.ts', function () {
  function getModifiedRouter(container: IContainer) {
    const router = container.get(IRouter) as IRouter;
    const mockBrowserHistoryLocation = new MockBrowserHistoryLocation();
    mockBrowserHistoryLocation.changeCallback = async (ev) => { router.viewer.handlePopStateEvent(ev); };
    router.viewer.history = mockBrowserHistoryLocation as any;
    router.viewer.location = mockBrowserHistoryLocation as any;
    return router;
  }

  type NavigationStateCallback = (type: 'push' | 'replace', data: any, title: string, path: string) => void;
  function spyNavigationStates(router: IRouter, spy: NavigationStateCallback) {
    let _pushState;
    let _replaceState;
    if (spy) {
      _pushState = router.viewer.location.pushState;
      router.viewer.location.pushState = function (data, title, path) {
        spy('push', data, title, path);
        _pushState.call(router.viewer.location, data, title, path);
      };
      _replaceState = router.viewer.location.replaceState;
      router.viewer.location.replaceState = function (data, title, path) {
        spy('replace', data, title, path);
        _replaceState.call(router.viewer.location, data, title, path);
      };
    }
    return { _pushState, _replaceState };
  }
  function unspyNavigationStates(router, _push, _replace) {
    if (_push) {
      router.viewer.location.pushState = _push;
      router.viewer.location.replaceState = _replace;
    }
  }

  async function createFixture(config?, App?, stateSpy?) {
    const ctx = TestContext.create();
    const { container, platform } = ctx;

    if (App === void 0) {
      App = CustomElement.define({ name: 'app', template: '<template>left<au-viewport name="left"></au-viewport>right<au-viewport name="right"></au-viewport></template>' });
    }
    const Foo = CustomElement.define({ name: 'foo', template: '<template>Viewport: foo <a href="baz@foo"><span>baz</span></a><au-viewport name="foo"></au-viewport></template>' });
    const Bar = CustomElement.define({ name: 'bar', template: `<template>Viewport: bar Parameter id: [\${id}] Parameter name: [\${name}] <au-viewport name="bar"></au-viewport></template>` }, class {
      public static parameters = ['id', 'name'];
      public id = 'no id';
      public name = 'no name';

      // public static inject = [IRouter];
      // public constructor(private readonly router: IRouter) { }
      // public created() {
      //   console.log('created', 'closest viewport', this.router.getClosestViewport(this));
      // }
      // public canLoad() {
      //   console.log('canLoad', 'closest viewport', this.router.getClosestViewport(this));
      //   return true;
      // }
      public loading(params) {
        // console.log('load', 'closest viewport', this.router.getClosestViewport(this));
        if (params.id) { this.id = params.id; }
        if (params.name) { this.name = params.name; }
      }
      // public binding() {
      //   console.log('binding', 'closest viewport', this.router.getClosestViewport(this));
      // }
    });
    const Baz = CustomElement.define({ name: 'baz', template: `<template>Viewport: baz Parameter id: [\${id}] <au-viewport name="baz"></au-viewport></template>` }, class {
      public static parameters = ['id'];
      public id = 'no id';
      public loading(params) { if (params.id) { this.id = params.id; } }
    });
    const Qux = CustomElement.define({ name: 'qux', template: '<template>Viewport: qux<au-viewport name="qux"></au-viewport></template>' }, class {
      public canLoad() { return true; }
      public canUnload() {
        if (quxCantUnload > 0) {
          quxCantUnload--;
          return false;
        } else {
          return true;
        }
      }
      public loading() { return true; }
      public unloading() { return true; }
    });
    const Quux = CustomElement.define({ name: 'quux', template: '<template>Viewport: quux<au-viewport name="quux" scope></au-viewport></template>' });
    const Corge = CustomElement.define({ name: 'corge', template: '<template>Viewport: corge<au-viewport name="corge" used-by="baz"></au-viewport>Viewport: dummy<au-viewport name="dummy"></au-viewport></template>' });

    const Uier = CustomElement.define({ name: 'uier', template: '<template>Viewport: uier</template>' }, class {
      public async canLoad() {
        await wait(500);
        return true;
      }
    });

    const Grault = CustomElement.define(
      {
        name: 'grault', template: '<template><input type="checkbox" checked.two-way="toggle">toggle<div if.bind="toggle">Viewport: grault<au-viewport name="grault" stateful used-by="garply,corge" default="garply"></au-viewport></div></template>'
      },
      class {
        public toggle = false;
      });
    const Garply = CustomElement.define(
      {
        name: 'garply', template: '<template>garply<input checked.two-way="text">text</template>'
      },
      class {
        public text;
      });
    const Waldo = CustomElement.define(
      {
        name: 'waldo', template: '<template>Viewport: waldo<au-viewport name="waldo" stateful used-by="grault,foo" default="grault"></au-viewport></div></template>'
      },
      class { });
    const Plugh = CustomElement.define(
      {
        name: 'plugh', template: `<template>Parameter: \${param} Entry: \${entry}</template>`
      },
      class {
        public param: number;
        public entry: number = 0;
        public reloadBehavior: string = plughReloadBehavior;
        public loading(params) {
          console.log('plugh.load', this.entry, this.reloadBehavior, plughReloadBehavior);
          this.param = +params[0];
          this.entry++;
          this.reloadBehavior = plughReloadBehavior;
        }
      });

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

    const au = ctx.wnd['au'] = new Aurelia(container)
      .register(
        RouterConfiguration.customize(config ?? {}),
        App)
      .app({ host: host, component: App });

    const router = getModifiedRouter(container);
    const { _pushState, _replaceState } = spyNavigationStates(router, stateSpy);

    container.register(Foo, Bar, Baz, Qux, Quux, Corge, Uier, Grault, Garply, Waldo, Plugh);

    await au.start();

    async function tearDown() {
      unspyNavigationStates(router, _pushState, _replaceState);
      await au.stop(true);
      ctx.doc.body.removeChild(host);
    }

    return { au, container, platform, host, router, ctx, tearDown };
  }

  it('can be created', async function () {
    this.timeout(5000);

    const { tearDown } = await createFixture();

    await tearDown();
  });

  it('loads viewports left and right', async function () {
    this.timeout(5000);

    const { host, tearDown } = await createFixture();

    assert.includes(host.textContent, 'left', `host.textContent`);
    assert.includes(host.textContent, 'right', `host.textContent`);

    await tearDown();
  });

  it('navigates to foo in left', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'foo', `host.textContent`);

    await tearDown();
  });

  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('queues navigations', async function () {
    this.timeout(40000);

    const { host, router, tearDown } = await createFixture();

    router.load('uier@left').catch((error) => { throw error; });
    const last = router.load('bar@left');
    // Depending on browser/node, there can be 1 or 2 in the queue here
    assert.notStrictEqual(router['navigator'].queued, 0, `router.navigator.queued`);
    await last;
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('clears viewport', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'foo', `host.textContent`);
    await $load('-@left', router, platform);
    assert.notIncludes(host.textContent, 'foo', `host.textContent`);

    await tearDown();
  });

  it('clears all viewports', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    await $load('bar@right', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    await $load('-', router, platform);
    assert.notIncludes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('replaces foo in left', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    const historyLength = router.viewer.history.length;
    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'foo', `host.textContent`);
    assert.strictEqual(router.viewer.history.length, historyLength + 1, `router.viewer.history.length, actual after foo: ${router.viewer.history.length}`);

    await router.load('bar@left', { replace: true });

    assert.includes(host.textContent, 'bar', `host.textContent`);
    assert.strictEqual(router.viewer.history.length, historyLength + 1, `router.viewer.history.length, actual after bar: ${router.viewer.history.length}`);

    await tearDown();
  });

  it('navigates to bar in right', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar@right', router, platform);
    assert.includes(host.textContent, 'bar', `host.textContent`);

    await tearDown();
  });

  it('navigates to foo in left then bar in right', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await $load('bar@right', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('reloads state when refresh method is called', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await $load('bar@right', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await router.refresh();
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('navigates back and forward with one viewport', async function () {
    this.timeout(40000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await $load('bar@left', router, platform);
    assert.notIncludes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await router.back();

    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await router.forward();

    assert.notIncludes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('navigates back and forward with two viewports', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await $load('bar@right', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await router.back();

    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await router.forward();

    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('navigates to foo/bar in left/right', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('foo@left+bar@right', router, platform);
    assert.includes(host.textContent, 'foo', `host.textContent`);
    assert.includes(host.textContent, 'bar', `host.textContent`);

    await tearDown();
  });

  it('cancels if not canUnload', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    quxCantUnload = 1;

    await $load('baz@left+qux@right', router, platform);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: qux', `host.textContent`);

    await $load('foo@left+bar@right', router, platform);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: qux', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);

    await tearDown();
  });

  it('cancels if not child canUnload', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    quxCantUnload = 1;

    await $load('foo@left/qux@foo+uier@right', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: qux', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: uier', `host.textContent`);

    await $load('bar@left+baz@right', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: qux', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: uier', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: baz', `host.textContent`);

    await tearDown();
  });

  it('navigates to foo/bar in left/right containing baz/qux respectively', async function () {
    this.timeout(15000);

    const { platform, host, router, tearDown } = await createFixture();

    // await $load('foo@left+bar@right+baz@foo+qux@bar', router, platform);
    await $load('foo@left/baz@foo+bar@right/qux@bar', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: qux', `host.textContent`);

    await tearDown();
  });

  it('handles anchor click', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture({ useHref: true });

    await $load('foo@left', router, platform);
    assert.includes(host.textContent, 'foo', `host.textContent`);

    (host.getElementsByTagName('SPAN')[0] as HTMLElement).parentElement.click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);

    await tearDown();
  });

  it('handles anchor click with load', async function () {
    this.timeout(5000);

    const tests = [
      { bind: false, value: 'id-name(1)', result: 1 },
      { bind: true, value: "'id-name(2)'", result: 2 },
      { bind: true, value: "{ component: 'id-name', parameters: '3' }", result: 3 },
      { bind: true, value: "{ component: IdName, parameters: '4' }", result: 4 },
    ];

    const IdName = CustomElement.define({ name: 'id-name', template: `|id-name| Parameter id: [\${id}] Parameter name: [\${name}]` }, class {
      public static parameters = ['id', 'name'];
      public id = 'no id';
      public name = 'no name';
      public loading(params) {
        if (params.id) { this.id = params.id; }
        if (params.name) { this.name = params.name; }
      }
    });
    @routes([{ path: 'a-route-decorator', component: 'my-decorated-component' }])
    @customElement({
      name: 'app',
      dependencies: [IdName],
      template: `
      ${tests.map(test => `<a load${test.bind ? '.bind' : ''}="${test.value}">${test.value}</a>`).join('<br>')}
      <br>
      <au-viewport></au-viewport>
      `}) class App {
      // Wish the following two lines weren't necessary
      public constructor() { this['IdName'] = IdName; }
    }

    const { host, router, container, tearDown, platform } = await createFixture({ useHref: false }, App);

    container.register(IdName);

    for (let i = 0; i < tests.length; i++) {
      const test = tests[i];

      (host.getElementsByTagName('A')[i] as HTMLElement).click();

      await platform.domQueue.yield();

      assert.includes(host.textContent, '|id-name|', `host.textContent`);
      assert.includes(host.textContent, `Parameter id: [${test.result}]`, `host.textContent`);

      await router.back();
      assert.notIncludes(host.textContent, '|id-name|', `host.textContent`);
    }

    await tearDown();
  });

  it('understands used-by', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('corge@left', router, platform);
    assert.includes(host.textContent, 'Viewport: corge', `host.textContent`);

    await $load('corge@left/baz', router, platform);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);

    await tearDown();
  });

  it('does not update fullStatePath on wrong history entry', async function () {
    this.timeout(40000);

    const { platform, router, tearDown } = await createFixture();

    await $load('foo@left', router, platform);
    await $load('bar@left', router, platform);
    await $load('baz@left', router, platform);

    await tearDown();
  });

  it('parses parameters after component', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar(123)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await $load('bar(456)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);

    await tearDown();
  });

  it('parses named parameters after component', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar(id=123)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await $load('bar(id=456)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);

    await tearDown();
  });

  it('parses parameters after component individually', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar(123)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await $load('bar(456)@right', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);

    await tearDown();
  });

  it('parses parameters without viewport', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('corge@left/baz(123)', router, platform);
    assert.includes(host.textContent, 'Viewport: corge', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await tearDown();
  });

  it('parses named parameters without viewport', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('corge@left/baz(id=123)', router, platform);

    assert.includes(host.textContent, 'Viewport: corge', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await tearDown();
  });

  it('parses multiple parameters after component', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar(123,OneTwoThree)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [OneTwoThree]', `host.textContent`);

    await $load('bar(456,FourFiveSix)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [FourFiveSix]', `host.textContent`);

    await tearDown();
  });

  it('parses multiple name parameters after component', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar(id=123,name=OneTwoThree)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [OneTwoThree]', `host.textContent`);

    await $load('bar(name=FourFiveSix,id=456)@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [FourFiveSix]', `host.textContent`);

    await tearDown();
  });

  it('parses querystring', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('bar@left?id=123', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [123]', `host.textContent`);

    await $load('bar@left?id=456&name=FourFiveSix', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [FourFiveSix]', `host.textContent`);

    await tearDown();
  });

  it('overrides querystring with parameter', async function () {
    this.timeout(5000);

    // let locationPath: string;
    // let browserTitle: string;
    // const locationCallback = (type, data, title, path) => {
    //   // console.log(type, data, title, path);
    //   locationPath = path;
    //   browserTitle = title;
    // };

    const { platform, host, router, tearDown } = await createFixture(void 0, void 0);

    let url = 'bar(456)@left?id=123';
    await $load(url, router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);

    url = 'bar(456,FourFiveSix)@left?id=123&name=OneTwoThree';
    await $load(url, router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [456]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [FourFiveSix]', `host.textContent`);

    url = 'bar(name=SevenEightNine,id=789)@left?id=123&name=OneTwoThree';
    await $load(url, router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.includes(host.textContent, 'Parameter id: [789]', `host.textContent`);
    assert.includes(host.textContent, 'Parameter name: [SevenEightNine]', `host.textContent`);

    await tearDown();
  });

  it('uses default reload behavior', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('plugh(123)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    await $load('plugh(123)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    await $load('plugh(456)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 456', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    await $load('plugh(456)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 456', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    await tearDown();
  });

  it('uses overriding reload behavior', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    plughReloadBehavior = 'default';
    // This should default
    await $load('plugh(123)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    let component = (router.getEndpoint('Viewport', 'left') as Viewport).getContent().componentInstance;
    component.reloadBehavior = 'reload';
    // This should reload
    await $load('plugh(123)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 2', `host.textContent`);

    component.reloadBehavior = 'refresh';
    // This should refresh
    await $load('plugh(456)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 456', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);
    component = (router.getEndpoint('Viewport', 'left') as Viewport).getContent().componentInstance;

    component.reloadBehavior = 'default';
    // This should default
    await $load('plugh(456)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 456', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 1', `host.textContent`);

    component.reloadBehavior = 'reload';
    // This should reload
    await $load('plugh(123)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 2', `host.textContent`);

    component.reloadBehavior = 'disallow';
    // This should disallow
    await $load('plugh(456)@left', router, platform);
    assert.includes(host.textContent, 'Parameter: 123', `host.textContent`);
    assert.includes(host.textContent, 'Entry: 2', `host.textContent`);

    await tearDown();
  });

  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('loads default when added by if condition becoming true', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('grault@left', router, platform);
    assert.includes(host.textContent, 'toggle', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.notIncludes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[0].click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[0].click();

    await platform.domQueue.yield();

    assert.notIncludes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.notIncludes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[0].click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    await tearDown();
  });

  // if (PLATFORM.isBrowserLike)
  // TODO: figure out why this works in nodejs locally but not in CI and fix it
  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('keeps input when stateful', async function () {
    this.timeout(15000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('grault@left', router, platform);
    assert.includes(host.textContent, 'toggle', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.notIncludes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[0].click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[1].value = 'asdf';

    await platform.domQueue.yield();

    // NOT going to work since it loads non-stateful parent grault
    await $load('grault@left/corge@grault', router, platform);

    assert.notIncludes(host.textContent, 'garply', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: corge', `host.textContent`);

    await $load('grault@left/garply@grault', router, platform);

    assert.notIncludes(host.textContent, 'Viewport: corge', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    assert.strictEqual((host as any).getElementsByTagName('INPUT')[1].value, 'asdf', `(host as any).getElementsByTagName('INPUT')[1].value`);

    await tearDown();
  });

  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('keeps input when grandparent stateful', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('waldo@left', router, platform);
    assert.includes(host.textContent, 'Viewport: waldo', `host.textContent`);
    assert.includes(host.textContent, 'toggle', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.notIncludes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[0].click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    (host as any).getElementsByTagName('INPUT')[1].value = 'asdf';

    await $load('waldo@left/foo@waldo', router, platform);

    assert.notIncludes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);

    await $load('waldo@left/grault@waldo', router, platform);

    assert.notIncludes(host.textContent, 'Viewport: corge', `host.textContent`);
    assert.includes(host.textContent, 'Viewport: grault', `host.textContent`);
    assert.includes(host.textContent, 'garply', `host.textContent`);

    assert.strictEqual((host as any).getElementsByTagName('INPUT')[1].value, 'asdf', `(host as any).getElementsByTagName('INPUT')[1].value`);

    await tearDown();
  });

  // todo
  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('keeps children\'s custom element\'s input when navigation history stateful', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown, container } = await createFixture({ statefulHistoryLength: 2 });

    const GrandGrandChild = CustomElement.define({ name: 'grandgrandchild', template: '|grandgrandchild|<input>' }, null);
    const GrandChild = CustomElement.define({ name: 'grandchild', template: '|grandchild|<input> <grandgrandchild></grandgrandchild>', dependencies: [GrandGrandChild] }, null);
    const Child = CustomElement.define({ name: 'child', template: '|child|<input> <input type="checkbox" checked.bind="toggle"> <div if.bind="toggle"><input> <au-viewport name="child"></au-viewport></div>', dependencies: [GrandChild] }, class { public toggle = true; });
    const ChildSibling = CustomElement.define({ name: 'sibling', template: '|sibling|' }, null);
    const Parent = CustomElement.define({ name: 'parent', template: '<br><br>|parent|<input> <au-viewport name="parent"></au-viewport>', dependencies: [Child, ChildSibling] }, null);
    container.register(Parent);

    const values = ['parent', 'child', false, 'child-hidden', 'grandchild', 'grandgrandchild'];

    await $load('parent@left/child@parent/grandchild@child', router, platform);

    assert.includes(host.textContent, '|parent|', `host.textContent`);
    assert.includes(host.textContent, '|child|', `host.textContent`);
    assert.includes(host.textContent, '|grandchild|', `host.textContent`);
    assert.includes(host.textContent, '|grandgrandchild|', `host.textContent`);

    let inputs = host.getElementsByTagName('INPUT') as HTMLCollectionOf<HTMLInputElement>;
    for (let i = 0; i < inputs.length; i++) {
      if (typeof values[i] === 'string') {
        inputs[i].value = values[i] as string;
      }
    }
    for (let i = 0; i < inputs.length; i++) {
      if (typeof values[i] === 'string') {
        assert.strictEqual(inputs[i].value, values[i], `host.getElementsByTagName('INPUT')[${i}].value`);
      }
    }

    await $load('parent@left/sibling@parent', router, platform);

    assert.includes(host.textContent, '|parent|', `host.textContent`);
    assert.includes(host.textContent, '|sibling|', `host.textContent`);
    assert.notIncludes(host.textContent, '|child|', `host.textContent`);
    assert.notIncludes(host.textContent, '|grandchild|', `host.textContent`);
    assert.notIncludes(host.textContent, '|grandgrandchild|', `host.textContent`);

    await router.back();

    assert.includes(host.textContent, '|parent|', `host.textContent`);
    assert.includes(host.textContent, '|child|', `host.textContent`);
    assert.includes(host.textContent, '|grandchild|', `host.textContent`);
    assert.includes(host.textContent, '|grandgrandchild|', `host.textContent`);
    assert.notIncludes(host.textContent, '|sibling|', `host.textContent`);

    inputs = inputs = host.getElementsByTagName('INPUT') as HTMLCollectionOf<HTMLInputElement>;
    for (let i = 0; i < inputs.length; i++) {
      if (typeof values[i] === 'string') {
        assert.strictEqual(inputs[i].value, values[i], `host.getElementsByTagName('INPUT')[${i}].value`);
      }
    }

    await tearDown();
  });

  // TODO: Fix scoped viewports!
  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('loads scoped viewport', async function () {
    this.timeout(5000);

    const { platform, host, router, tearDown } = await createFixture();

    await $load('quux@left', router, platform);
    assert.includes(host.textContent, 'Viewport: quux', `host.textContent`);

    await $load('quux@quux!', router, platform);
    assert.includes(host.textContent, 'Viewport: quux', `host.textContent`);

    await $load('quux@left/foo@quux!', router, platform);
    assert.includes(host.textContent, 'Viewport: foo', `host.textContent`);

    (host.getElementsByTagName('SPAN')[0] as HTMLElement).click();

    await platform.domQueue.yield();

    assert.includes(host.textContent, 'Viewport: baz', `host.textContent`);

    await $load('bar@left', router, platform);
    assert.includes(host.textContent, 'Viewport: bar', `host.textContent`);
    assert.notIncludes(host.textContent, 'Viewport: quux', `host.textContent`);

    await tearDown();
  });
});

let quxCantUnload = 0;
let plughReloadBehavior = 'default';

const $load = async (path: string, router: IRouter, platform: IPlatform) => {
  await router.load(path);
  platform.domQueue.flush();
};

const wait = async (time = 500) => {
  await new Promise((resolve) => {
    setTimeout(resolve, time);
  });
};