packages/__tests__/src/router-lite/smoke-tests.spec.ts
import { LogLevel, Constructable, kebabCase, ILogConfig, Registration, noop, IModule, inject, resolve } from '@aurelia/kernel';
import { assert, MockBrowserHistoryLocation, TestContext } from '@aurelia/testing';
import { RouterConfiguration, IRouter, NavigationInstruction, IRouteContext, RouteNode, Params, route, INavigationModel, IRouterOptions, IRouteViewModel, IRouteConfig, Router, HistoryStrategy, IRouterEvents, ITypedNavigationInstruction_string, IViewportInstruction, RouteConfig, Routeable, RouterOptions, RouteContext } from '@aurelia/router-lite';
import { Aurelia, valueConverter, customElement, CustomElement, ICustomElementViewModel, IHistory, IHydratedController, ILocation, INode, IPlatform, IWindow, watch } from '@aurelia/runtime-html';
import { getLocationChangeHandlerRegistration, TestRouterConfiguration } from './_shared/configuration.js';
import { start } from './_shared/create-fixture.js';
import { isNode } from '../util.js';
function vp(count: number): string {
return '<au-viewport></au-viewport>'.repeat(count);
}
type C = Constructable;
type CSpec = (C | CSpec)[];
function getText(spec: CSpec): string {
return spec.map(function (x) {
if (x instanceof Array) {
return getText(x);
}
return kebabCase(x.name);
}).join('');
}
function assertComponentsVisible(host: HTMLElement, spec: CSpec, msg: string = ''): void {
assert.strictEqual(host.textContent, getText(spec), msg);
}
function assertIsActive(
router: IRouter,
instruction: NavigationInstruction,
context: IRouteContext,
expected: boolean,
assertId: number,
): void {
const isActive = router.isActive(instruction, context);
assert.strictEqual(isActive, expected, `expected isActive to return ${expected} (assertId ${assertId})`);
}
async function createFixture<T extends Constructable>(
Component: T,
deps: Constructable[],
level: LogLevel = LogLevel.fatal,
) {
const ctx = TestContext.create();
const { container, platform } = ctx;
container.register(TestRouterConfiguration.for(level));
container.register(RouterConfiguration);
container.register(...deps);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component: Component, host });
await au.start();
assertComponentsVisible(host, [Component]);
const logConfig = container.get(ILogConfig);
return {
ctx,
au,
host,
component: au.root.controller.viewModel as InstanceType<T>,
platform,
container,
router: container.get(IRouter),
startTracing() {
logConfig.level = LogLevel.trace;
},
stopTracing() {
logConfig.level = level;
},
async tearDown() {
assert.areTaskQueuesEmpty();
await au.stop(true);
}
};
}
describe('router-lite/smoke-tests.spec.ts', function () {
@customElement({ name: 'a01', template: `a01${vp(0)}` })
class A01 { }
@customElement({ name: 'a02', template: `a02${vp(0)}` })
class A02 { }
const A0 = [A01, A02];
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'a11', template: `a11${vp(1)}` })
class A11 { }
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
{ path: 'a11', component: A11, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'a12', template: `a12${vp(1)}` })
class A12 { }
const A1 = [A11, A12];
@customElement({ name: 'a21', template: `a21${vp(2)}` })
class A21 { }
@customElement({ name: 'a22', template: `a22${vp(2)}` })
class A22 { }
const A2 = [A21, A22];
const A = [...A0, ...A1, ...A2];
@customElement({ name: 'b01', template: `b01${vp(0)}` })
class B01 {
public async canUnload(
_next: RouteNode | null,
_current: RouteNode,
): Promise<true> {
await new Promise(function (resolve) { setTimeout(resolve, 0); });
return true;
}
}
@customElement({ name: 'b02', template: `b02${vp(0)}` })
class B02 {
public async canUnload(
_next: RouteNode | null,
_current: RouteNode,
): Promise<false> {
await new Promise(function (resolve) { setTimeout(resolve, 0); });
return false;
}
}
const B0 = [B01, B02];
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
{ path: 'b01', component: B01, transitionPlan: 'invoke-lifecycles' },
{ path: 'b02', component: B02, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'b11', template: `b11${vp(1)}` })
class B11 {
public async canUnload(
_next: RouteNode | null,
_current: RouteNode,
): Promise<true> {
await new Promise(function (resolve) { setTimeout(resolve, 0); });
return true;
}
}
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
{ path: 'b01', component: B01, transitionPlan: 'invoke-lifecycles' },
{ path: 'b02', component: B02, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'b12', template: `b12${vp(1)}` })
class B12 {
public async canUnload(
_next: RouteNode | null,
_current: RouteNode,
): Promise<false> {
await new Promise(function (resolve) { setTimeout(resolve, 0); });
return false;
}
}
const B1 = [B11, B12];
const B = [...B0, ...B1];
const Z = [...A, ...B];
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
{ path: 'a11', component: A11, transitionPlan: 'invoke-lifecycles' },
{ path: 'a12', component: A12, transitionPlan: 'invoke-lifecycles' },
{ path: 'b11', component: B11, transitionPlan: 'invoke-lifecycles' },
{ path: 'b12', component: B12, },
]
})
@customElement({ name: 'root1', template: `root1${vp(1)}` })
class Root1 { }
@route({
routes: [
{ path: 'a01', component: A01, transitionPlan: 'invoke-lifecycles' },
{ path: 'a02', component: A02, transitionPlan: 'invoke-lifecycles' },
{ path: 'a11', component: A11, transitionPlan: 'invoke-lifecycles' },
{ path: 'a12', component: A12, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'root2', template: `root2${vp(2)}` })
class Root2 { }
it('injecting Router and IRouter should yield the same instance', async function () {
@customElement({ name: 'app', template: 'app' })
class App {
public readonly router1: Router = resolve(Router);
public readonly router2: IRouter = resolve(IRouter);
}
const { component, tearDown } = await createFixture(App, []);
assert.strictEqual(component.router1, component.router2, 'router mismatch');
await tearDown();
});
// Start with a broad sample of non-generated tests that are easy to debug and mess around with.
it(`root1 can load a01 as a string and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load('a01');
assertComponentsVisible(host, [Root1, A01]);
assertIsActive(router, 'a01', router.routeTree.root.context, true, 1);
await tearDown();
});
it(`root1 can load a01 as a type and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(A01);
assertComponentsVisible(host, [Root1, A01]);
assertIsActive(router, A01, router.routeTree.root.context, true, 1);
await tearDown();
});
it(`root1 can load a01 as a ViewportInstruction and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load({ component: A01 });
assertComponentsVisible(host, [Root1, A01]);
assertIsActive(router, { component: A01 }, router.routeTree.root.context, true, 1);
await tearDown();
});
it(`root1 can load a01 as a CustomElementDefinition and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(CustomElement.getDefinition(A01));
assertComponentsVisible(host, [Root1, A01]);
assertIsActive(router, CustomElement.getDefinition(A01), router.routeTree.root.context, true, 1);
await tearDown();
});
it(`root1 can load a01,a02 in order and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load('a01');
assertComponentsVisible(host, [Root1, A01]);
assertIsActive(router, 'a01', router.routeTree.root.context, true, 1);
await router.load('a02');
assertComponentsVisible(host, [Root1, A02]);
assertIsActive(router, 'a02', router.routeTree.root.context, true, 2);
await tearDown();
});
it(`root1 can load a11,a11/a02 in order with context and can determine if it's active`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(A11);
assertComponentsVisible(host, [Root1, A11]);
assertIsActive(router, A11, router.routeTree.root.context, true, 1);
const context = router.routeTree.root.children[0].context;
await router.load(A02, { context });
assertComponentsVisible(host, [Root1, A11, A02]);
assertIsActive(router, A02, context, true, 2);
assertIsActive(router, A02, router.routeTree.root.context, false, 3);
assertIsActive(router, A11, router.routeTree.root.context, true, 3);
await tearDown();
});
it('root can load sibling components and can determine if it\'s active', async function () {
@route('c1')
@customElement({ name: 'c-1', template: 'c1' })
class C1 { }
@route('c2')
@customElement({ name: 'c-2', template: 'c2' })
class C2 { }
@route({ routes: [C1, C2] })
@customElement({ name: 'ro-ot', template: '<au-viewport name="$1"></au-viewport> <au-viewport name="$2"></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, '', 'init');
await router.load('c1+c2');
assert.html.textContent(host, 'c1 c2', 'init');
const ctx = router.routeTree.root.context;
assertIsActive(router, 'c1+c2', ctx, true, 1);
assertIsActive(router, 'c1@$1+c2@$2', ctx, true, 2);
assertIsActive(router, 'c2@$1+c1@$2', ctx, false, 3);
assertIsActive(router, 'c1@$2+c2@$1', ctx, false, 4);
assertIsActive(router, 'c2+c1', ctx, false, 5);
assertIsActive(router, 'c2$2+c1$1', ctx, false, 6); // the contains check is not order-invariant.
await au.stop(true);
});
it(`root1 can load a11/a01,a11/a02 in order with context`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load({ component: A11, children: [A01] });
assertComponentsVisible(host, [Root1, A11, A01]);
const context = router.routeTree.root.children[0].context;
await router.load(A02, { context });
assertComponentsVisible(host, [Root1, A11, A02]);
await tearDown();
});
it(`root1 correctly handles canUnload with load b11/b01,a01 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load({ component: B11, children: [B01] });
assertComponentsVisible(host, [Root1, B11, B01]);
assert.strictEqual(result, true, '#1 result===true');
result = await router.load({ component: B11, children: [A01] });
assertComponentsVisible(host, [Root1, B11, A01]);
assert.strictEqual(result, true, '#2 result===true');
await tearDown();
});
it(`root1 correctly handles canUnload with load b11/b02,a01 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load({ component: B11, children: [B02] });
assertComponentsVisible(host, [Root1, B11, B02]);
assert.strictEqual(result, true, '#1 result===true');
result = await router.load(A01);
assertComponentsVisible(host, [Root1, B11, B02]);
assert.strictEqual(result, false, '#2 result===false');
await tearDown();
});
it(`root1 correctly handles canUnload with load b11/b02,a01,a02 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load({ component: B11, children: [B02] });
assertComponentsVisible(host, [Root1, B11, B02], '#1');
assert.strictEqual(result, true, '#1 result===true');
result = await router.load(A01);
assertComponentsVisible(host, [Root1, B11, B02], '#2');
assert.strictEqual(result, false, '#2 result===false');
result = await router.load(A02);
assertComponentsVisible(host, [Root1, B11, B02], '#3');
assert.strictEqual(result, false, '#3 result===false');
await tearDown();
});
it(`root1 correctly handles canUnload with load b11/b02,b11/a02 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load(`b11/b02`);
assertComponentsVisible(host, [Root1, B11, [B02]]);
assert.strictEqual(result, true, '#1 result===true');
result = await router.load(`b11/a02`);
assertComponentsVisible(host, [Root1, B11, [B02]]);
assert.strictEqual(result, false, '#2 result===false');
await tearDown();
});
it(`root1 correctly handles canUnload with load b12/b01,b11/b01 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load(`b12/b01`);
assertComponentsVisible(host, [Root1, B12, [B01]]);
assert.strictEqual(result, true, '#1 result===true');
result = await router.load(`b11/b01`);
assertComponentsVisible(host, [Root1, B12, [B01]]);
assert.strictEqual(result, false, '#2 result===false');
await tearDown();
});
it(`root1 correctly handles canUnload with load b12/b01,b12/a01 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
let result = await router.load(`b12/b01`);
assertComponentsVisible(host, [Root1, B12, [B01]], '#1 text');
assert.strictEqual(result, true, '#1 result===true');
result = await router.load(`b12/a01`);
assertComponentsVisible(host, [Root1, B12, [A01]], '#2 text');
assert.strictEqual(result, true, '#2 result===true');
await tearDown();
});
it(`root1 can load a11/a01 as a string`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(`a11/a01`);
assertComponentsVisible(host, [Root1, A11, A01]);
await tearDown();
});
it(`root1 can load a11/a01 as a ViewportInstruction`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load({ component: A11, children: [A01] });
assertComponentsVisible(host, [Root1, A11, A01]);
await tearDown();
});
it(`root1 can load a11/a01,a11/a02 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(`a11/a01`);
assertComponentsVisible(host, [Root1, A11, A01]);
await router.load(`a11/a02`);
assertComponentsVisible(host, [Root1, A11, A02]);
await tearDown();
});
it(`root2 can load a01+a02 as a string`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(`a01+a02`);
assertComponentsVisible(host, [Root2, A01, A02]);
await tearDown();
});
it(`root2 can load a01+a02 as an array of strings`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(['a01', 'a02']);
assertComponentsVisible(host, [Root2, A01, A02]);
await tearDown();
});
it(`root2 can load a01+a02 as an array of types`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load([A01, A02]);
assertComponentsVisible(host, [Root2, A01, A02]);
await tearDown();
});
it(`root2 can load a01+a02 as a mixed array type and string`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load([A01, 'a02']);
assertComponentsVisible(host, [Root2, A01, A02]);
await tearDown();
});
it(`root2 can load a01+a02,a02+a01 in order`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(`a01+a02`);
assertComponentsVisible(host, [Root2, A01, A02]);
await router.load(`a02+a01`);
assertComponentsVisible(host, [Root2, A02, A01]);
await tearDown();
});
it(`root2 can load a12/a11/a01+a12/a01,a11/a12/a01+a12/a11/a01,a11/a12/a02+a12/a11/a01 in order with context`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(`a12/a11/a01+a12/a01`);
assertComponentsVisible(host, [Root2, [A12, [A11, [A01]]], [A12, [A01]]], '#1');
let context = router.routeTree.root.children[1].context;
await router.load(`a11/a01`, { context });
assertComponentsVisible(host, [Root2, [A12, [A11, [A01]]], [A12, [A11, [A01]]]], '#2');
context = router.routeTree.root.children[0].children[0].context;
await router.load(`a02`, { context });
assertComponentsVisible(host, [Root2, [A12, [A11, [A02]]], [A12, [A11, [A01]]]], '#3');
await tearDown();
});
// Now generate stuff
const $1vp: Record<string, CSpec> = {
// [x]
[`a01`]: [A01],
[`a02`]: [A02],
// [x/x]
[`a11/a01`]: [A11, [A01]],
[`a11/a02`]: [A11, [A02]],
[`a12/a01`]: [A12, [A01]],
[`a12/a02`]: [A12, [A02]],
// [x/x/x]
[`a12/a11/a01`]: [A12, [A11, [A01]]],
[`a12/a11/a02`]: [A12, [A11, [A02]]],
};
const $1vpKeys = Object.keys($1vp);
for (let i = 0, ii = $1vpKeys.length; i < ii; ++i) {
const key11 = $1vpKeys[i];
const value11 = $1vp[key11];
it(`root1 can load ${key11}`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(key11);
assertComponentsVisible(host, [Root1, value11]);
await tearDown();
});
if (i >= 1) {
const key11prev = $1vpKeys[i - 1];
const value11prev = $1vp[key11prev];
it(`root1 can load ${key11prev},${key11} in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(key11prev);
assertComponentsVisible(host, [Root1, value11prev]);
await router.load(key11);
assertComponentsVisible(host, [Root1, value11]);
await tearDown();
});
it(`root1 can load ${key11},${key11prev} in order`, async function () {
const { router, host, tearDown } = await createFixture(Root1, Z);
await router.load(key11);
assertComponentsVisible(host, [Root1, value11]);
await router.load(key11prev);
assertComponentsVisible(host, [Root1, value11prev]);
await tearDown();
});
}
}
const $2vps: Record<string, CSpec> = {
// [x+x]
[`a01+a02`]: [[A01], [A02]],
[`a02+a01`]: [[A02], [A01]],
// [x/x+x]
[`a11/a01+a02`]: [[A11, [A01]], [A02]],
[`a11/a02+a01`]: [[A11, [A02]], [A01]],
[`a12/a01+a02`]: [[A12, [A01]], [A02]],
[`a12/a02+a01`]: [[A12, [A02]], [A01]],
// [x+x/x]
[`a01+a11/a02`]: [[A01], [A11, [A02]]],
[`a02+a11/a01`]: [[A02], [A11, [A01]]],
[`a01+a12/a02`]: [[A01], [A12, [A02]]],
[`a02+a12/a01`]: [[A02], [A12, [A01]]],
// [x/x+x/x]
[`a11/a01+a12/a02`]: [[A11, [A01]], [A12, [A02]]],
[`a11/a02+a12/a01`]: [[A11, [A02]], [A12, [A01]]],
[`a12/a01+a11/a02`]: [[A12, [A01]], [A11, [A02]]],
[`a12/a02+a11/a01`]: [[A12, [A02]], [A11, [A01]]],
};
const $2vpsKeys = Object.keys($2vps);
for (let i = 0, ii = $2vpsKeys.length; i < ii; ++i) {
const key21 = $2vpsKeys[i];
const value21 = $2vps[key21];
it(`root2 can load ${key21}`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(key21);
assertComponentsVisible(host, [Root2, value21]);
await tearDown();
});
if (i >= 1) {
const key21prev = $2vpsKeys[i - 1];
const value21prev = $2vps[key21prev];
it(`root2 can load ${key21prev},${key21} in order`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(key21prev);
assertComponentsVisible(host, [Root2, value21prev]);
await router.load(key21);
assertComponentsVisible(host, [Root2, value21]);
await tearDown();
});
it(`root2 can load ${key21},${key21prev} in order`, async function () {
const { router, host, tearDown } = await createFixture(Root2, Z);
await router.load(key21);
assertComponentsVisible(host, [Root2, value21]);
await router.load(key21prev);
assertComponentsVisible(host, [Root2, value21prev]);
await tearDown();
});
}
}
it('can load single anonymous default at the root', async function () {
@customElement({ name: 'a', template: 'a' })
class A { }
@customElement({ name: 'b', template: 'b' })
class B { }
@route({
routes: [
{ path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
{ path: 'b', component: B, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'root',
template: `root<au-viewport default="a"></au-viewport>`,
dependencies: [A, B],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A]]);
await router.load('b');
assertComponentsVisible(host, [Root, [B]]);
await router.load('');
assertComponentsVisible(host, [Root, [A]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('can load a named default with one sibling at the root', async function () {
@customElement({ name: 'a', template: 'a' })
class A { }
@customElement({ name: 'b', template: 'b' })
class B { }
@route({
routes: [
{ path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
{ path: 'b', component: B, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'root',
template: `root<au-viewport name="a" default="a"></au-viewport><au-viewport name="b"></au-viewport>`,
dependencies: [A, B],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A]], '1');
await router.load('b@b');
assertComponentsVisible(host, [Root, [A, B]], '2');
await router.load('');
assertComponentsVisible(host, [Root, [A]], '3');
await router.load('a@a+b@b');
assertComponentsVisible(host, [Root, [A, B]], '4');
await router.load('b@a');
assertComponentsVisible(host, [Root, [B]], '5');
await router.load('');
assertComponentsVisible(host, [Root, [A]], '6');
await router.load('b@a+a@b');
assertComponentsVisible(host, [Root, [B, A]], '7');
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('can load a named default with one sibling at a child', async function () {
@customElement({ name: 'b', template: 'b' })
class B { }
@customElement({ name: 'c', template: 'c' })
class C { }
@route({
routes: [
{ path: 'b', component: B, transitionPlan: 'invoke-lifecycles' },
{ path: 'c', component: C, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'a',
template: 'a<au-viewport name="b" default="b"></au-viewport><au-viewport name="c"></au-viewport>',
dependencies: [B, C],
})
class A { }
@route({
routes: [
{ path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'root',
template: `root<au-viewport default="a">`,
dependencies: [A],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A, [B]]], '1');
await router.load('a/c@c');
assertComponentsVisible(host, [Root, [A, [B, C]]], '2');
await router.load('');
assertComponentsVisible(host, [Root, [A, [B]]], '3');
await router.load('a/(b@b+c@c)');
assertComponentsVisible(host, [Root, [A, [B, C]]], '4');
await router.load('a/c@b');
assertComponentsVisible(host, [Root, [A, [C]]], '5');
await router.load('');
assertComponentsVisible(host, [Root, [A, [B]]], '6');
await router.load('a/(c@b+b@c)');
assertComponentsVisible(host, [Root, [A, [C, B]]], '7');
await au.stop(true);
assert.areTaskQueuesEmpty();
});
for (const [name, fallback] of [['ce name', 'ce-a'], ['route', 'a'], ['route-id', 'r1']]) {
it(`will load the fallback when navigating to a non-existing route - with ${name} - viewport`, async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'root',
template: `root<au-viewport fallback="${fallback}">`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
A,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('b');
assertComponentsVisible(host, [Root, [A]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it(`will load the global-fallback when navigating to a non-existing route - with ${name}`, async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
],
fallback,
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
A,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('b');
assertComponentsVisible(host, [Root, [A]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it(`will load the global-fallback when navigating to a non-existing route - sibling - with ${name}`, async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
],
fallback,
})
@customElement({
name: 'root',
template: `root<au-viewport></au-viewport><au-viewport></au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
A,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('b+c');
assertComponentsVisible(host, [Root, [A, A]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
}
it('will load the global-fallback when navigating to a non-existing route - with ce-name - with empty route', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A, transitionPlan: 'invoke-lifecycles' },
{ id: 'r2', path: ['nf'], component: NF, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'n-f',
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
NF,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A]]);
await router.load('b');
assertComponentsVisible(host, [Root, [NF]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('will load the global-fallback when navigating to a non-existing route - parent-child', async function () {
@customElement({ name: 'ce-a01', template: 'ac01' })
class Ac01 { }
@customElement({ name: 'ce-a02', template: 'ac02' })
class Ac02 { }
@route({
routes: [
{ id: 'rc1', path: 'ac01', component: Ac01, transitionPlan: 'invoke-lifecycles' },
{ id: 'rc2', path: 'ac02', component: Ac02, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'rc1',
})
@customElement({ name: 'ce-a', template: 'a<au-viewport>', dependencies: [Ac01, Ac02] })
class A { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'r1',
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
dependencies: [A],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('a/b');
assertComponentsVisible(host, [Root, [A, [Ac01]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('will load the global-fallback when navigating to a non-existing route - sibling + parent-child', async function () {
@customElement({ name: 'ce-a01', template: 'ac01' })
class Ac01 { }
@customElement({ name: 'ce-a02', template: 'ac02' })
class Ac02 { }
@route({
routes: [
{ id: 'rc1', path: 'ac01', component: Ac01, transitionPlan: 'invoke-lifecycles' },
{ id: 'rc2', path: 'ac02', component: Ac02, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'rc1',
})
@customElement({ name: 'ce-a', template: 'a<au-viewport>', dependencies: [Ac01, Ac02] })
class A { }
@customElement({ name: 'ce-b01', template: 'bc01' })
class Bc01 { }
@customElement({ name: 'ce-b02', template: 'bc02' })
class Bc02 { }
@route({
routes: [
{ id: 'rc1', path: 'bc01', component: Bc01, transitionPlan: 'invoke-lifecycles' },
{ id: 'rc2', path: 'bc02', component: Bc02, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'rc2',
})
@customElement({ name: 'ce-b', template: 'b<au-viewport>', dependencies: [Bc01, Bc02] })
class B { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
{ id: 'r2', path: 'b', component: B, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'r1',
})
@customElement({
name: 'root',
template: `root<au-viewport></au-viewport><au-viewport></au-viewport>`,
dependencies: [A, B],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('a/ac02+b/u');
assertComponentsVisible(host, [Root, [A, [Ac02]], [B, [Bc02]]]);
await router.load('a/u+b/bc01');
assertComponentsVisible(host, [Root, [A, [Ac01]], [B, [Bc01]]]);
await router.load('a/u+b/u');
assertComponentsVisible(host, [Root, [A, [Ac01]], [B, [Bc02]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('au-viewport#fallback precedes global fallback', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'ce-b', template: 'b' })
class B { }
@route({
routes: [
{ id: 'r1', path: 'a', component: A, transitionPlan: 'invoke-lifecycles' },
{ id: 'r2', path: 'b', component: B, transitionPlan: 'invoke-lifecycles' },
],
fallback: 'r1',
})
@customElement({
name: 'root',
template: `root<au-viewport name="1"></au-viewport><au-viewport name="2" fallback="r2"></au-viewport>`,
dependencies: [A, B],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root]);
await router.load('u1@1+u2@2');
assertComponentsVisible(host, [Root, [A, B]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function as fallback is supported - route configuration', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): string {
return (vi.component as ITypedNavigationInstruction_string).value === 'foo' ? 'r2' : 'r3';
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
NF1,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function as fallback is supported - route configuration - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): string {
return rn.component.Type === P1 ? 'n-f-1' : 'n-f-2';
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
NF1,
NF2,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function as fallback is supported - viewport', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport fallback.bind>`,
})
class Root {
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): string {
return (vi.component as ITypedNavigationInstruction_string).value === 'foo' ? 'r2' : 'r3';
}
}
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
NF1,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function as fallback is supported - viewport - hierarchical', async function () {
function fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): string {
return rn.component.Type === P1 ? 'n-f-1' : 'n-f-2';
}
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport fallback.bind></au-viewport>' })
class P1 {
private readonly fallback = fallback;
}
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport fallback.bind></au-viewport>' })
class P2 {
private readonly fallback = fallback;
}
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
NF1,
NF2,
);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('class as fallback is supported - route configuration', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF },
],
fallback: NF,
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('class as fallback is supported - route configuration - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF },
],
fallback: NF,
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning class as fallback is supported - route configuration', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): Routeable {
return (vi.component as ITypedNavigationInstruction_string).value === 'foo' ? NF1 : NF2;
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning class as fallback is supported - route configuration - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): Routeable {
return rn.component.Type === P1 ? NF1 : NF2;
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('promise resolving to class as fallback is supported - route configuration', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF },
],
fallback: Promise.resolve(NF),
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('promise resolving to class as fallback is supported - route configuration - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF },
],
fallback: Promise.resolve(NF),
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning a promise resolving to class as fallback is supported - route configuration', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): Routeable {
return Promise.resolve((vi.component as ITypedNavigationInstruction_string).value === 'foo' ? NF1 : NF2);
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning a promise resolving to class as fallback is supported - route configuration - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): Routeable {
return Promise.resolve(rn.component.Type === P1 ? NF1 : NF2);
},
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('class as fallback is supported - viewport', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF },
],
})
@customElement({
name: 'root',
template: `root<au-viewport fallback.bind>`,
})
class Root {
fallback = NF;
}
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('class as fallback is supported - viewport - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport fallback.bind></au-viewport>' })
class P1 {
private readonly fallback = NF1;
}
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport fallback.bind></au-viewport>' })
class P2 {
private readonly fallback = NF2;
}
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning class as fallback is supported - viewport', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport fallback.bind>`,
})
class Root {
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): Routeable {
return (vi.component as ITypedNavigationInstruction_string).value === 'foo' ? NF1 : NF2;
}
}
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning class as fallback is supported - viewport - hierarchical', async function () {
function fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): Routeable {
return rn.component.Type === P1 ? NF1 : NF2;
}
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport fallback.bind></au-viewport>' })
class P1 {
fallback = fallback;
}
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport fallback.bind></au-viewport>' })
class P2 {
fallback = fallback;
}
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('promise resolving to class as fallback is supported - viewport', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f', template: 'nf' })
class NF { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF },
],
})
@customElement({
name: 'root',
template: `root<au-viewport fallback.bind>`,
})
class Root {
fallback = Promise.resolve(NF);
}
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('promise resolving to class as fallback is supported - viewport - hierarchical', async function () {
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport fallback.bind></au-viewport>' })
class P1 {
private readonly fallback = Promise.resolve(NF1);
}
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport fallback.bind></au-viewport>' })
class P2 {
private readonly fallback = Promise.resolve(NF2);
}
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning a promise resolving to class as fallback is supported - viewport', async function () {
@customElement({ name: 'ce-a', template: 'a' })
class A { }
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'a'], component: A },
{ id: 'r2', path: ['nf1'], component: NF1 },
{ id: 'r3', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport fallback.bind>`,
})
class Root {
fallback(vi: IViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): Routeable {
return Promise.resolve((vi.component as ITypedNavigationInstruction_string).value === 'foo' ? NF1 : NF2);
}
}
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [A]]);
await router.load('foo');
assertComponentsVisible(host, [Root, [NF1]]);
await router.load('bar');
assertComponentsVisible(host, [Root, [NF2]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('function returning a promise resolving to class as fallback is supported - viewport - hierarchical', async function () {
function fallback(vi: IViewportInstruction, rn: RouteNode, _ctx: IRouteContext): Routeable {
return Promise.resolve(rn.component.Type === P1 ? NF1 : NF2);
}
@customElement({ name: 'ce-c1', template: 'c1' })
class C1 { }
@customElement({ name: 'ce-c2', template: 'c2' })
class C2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C1 },
]
})
@customElement({ name: 'ce-p1', template: 'p1<au-viewport fallback.bind></au-viewport>' })
class P1 {
fallback = fallback;
}
@route({
routes: [
{ id: 'r1', path: ['', 'c'], component: C2 },
]
})
@customElement({ name: 'ce-p2', template: 'p2<au-viewport fallback.bind></au-viewport>' })
class P2 {
fallback = fallback;
}
@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 { }
@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 { }
@route({
routes: [
{ id: 'r1', path: ['', 'p1'], component: P1 },
{ id: 'r2', path: ['p2'], component: P2 },
{ id: 'r3', path: ['nf1'], component: NF1 },
{ id: 'r4', path: ['nf2'], component: NF2 },
],
})
@customElement({
name: 'root',
template: `root<au-viewport>`,
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assertComponentsVisible(host, [Root, [P1, [C1]]]);
await router.load('p2/foo');
assertComponentsVisible(host, [Root, [P2, [NF2]]]);
await router.load('p1/foo');
assertComponentsVisible(host, [Root, [P1, [NF1]]]);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
for (const attr of ['href', 'load']) {
it(`will load the root-level fallback when navigating to a non-existing route - parent-child - children without fallback - attr: ${attr}`, async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc12', component: GrandChildOneTwo },
],
})
@customElement({
name: 'c-one',
template: `c1 <br>
<nav>
<a ${attr}="gc11">gc11</a>
<a ${attr}="gc12">gc12</a>
<a ${attr}="c2">c2 (doesn't work)</a>
<a ${attr}="../c2">../c2 (works)</a>
</nav>
<br>
<au-viewport></au-viewport>`,
})
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22' })
class GrandChildTwoTwo { }
@route({
routes: [
{ id: 'gc21', path: ['', 'gc21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc22', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 <br>
<nav>
<a ${attr}="gc21">gc21</a>
<a ${attr}="gc22">gc22</a>
<a ${attr}="c1">c1 (doesn't work)</a>
<a ${attr}="../c1">../c1 (works)</a>
</nav>
<br>
<au-viewport></au-viewport>`,
})
class ChildTwo { }
@customElement({
name: 'not-found',
template: 'nf',
})
class NotFound { }
@route({
routes: [
{
path: ['', 'c1'],
component: ChildOne,
},
{
path: 'c2',
component: ChildTwo,
},
{
path: 'not-found',
component: NotFound,
},
],
fallback: 'not-found',
})
@customElement({
name: 'my-app',
template: `<nav>
<a ${attr}="c1">C1</a>
<a ${attr}="c2">C2</a>
</nav>
<au-viewport></au-viewport>` })
class Root { }
const { au, container, host } = await start({
appRoot: Root,
registrations: [
NotFound,
]
});
const queue = container.get(IPlatform).domQueue;
const rootVp = host.querySelector('au-viewport');
let childVp = rootVp.querySelector('au-viewport');
assert.html.textContent(childVp, 'gc11');
let [, a2, nf, f] = Array.from(rootVp.querySelectorAll('a'));
a2.click();
queue.flush();
await queue.yield();
assert.html.textContent(childVp, 'gc12');
nf.click();
queue.flush();
await queue.yield();
assert.html.textContent(childVp, 'nf');
f.click();
queue.flush();
await queue.yield();
childVp = rootVp.querySelector('au-viewport');
assert.html.textContent(childVp, 'gc21', host.textContent);
[, a2, nf, f] = Array.from(rootVp.querySelectorAll('a'));
a2.click();
queue.flush();
await queue.yield();
assert.html.textContent(childVp, 'gc22');
nf.click();
queue.flush();
await queue.yield();
assert.html.textContent(childVp, 'nf');
f.click();
queue.flush();
await queue.yield();
childVp = rootVp.querySelector('au-viewport');
assert.html.textContent(childVp, 'gc11');
await au.stop(true);
assert.areTaskQueuesEmpty();
});
}
it(`correctly parses parameters`, async function () {
const a1Params: Params[] = [];
const a2Params: Params[] = [];
const b1Params: Params[] = [];
const b2Params: Params[] = [];
@customElement({ name: 'b1', template: null })
class B1 {
public loading(params: Params) {
b1Params.push(params);
}
}
@customElement({ name: 'b2', template: null })
class B2 {
public loading(params: Params) {
b2Params.push(params);
}
}
@route({
routes: [
{ path: 'b1/:b', component: B1, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'a1',
template: `<au-viewport></au-viewport>`,
dependencies: [B1],
})
class A1 {
public loading(params: Params) {
a1Params.push(params);
}
}
@route({
routes: [
{ path: 'b2/:d', component: B2, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'a2',
template: `<au-viewport></au-viewport>`,
dependencies: [B2],
})
class A2 {
public loading(params: Params) {
a2Params.push(params);
}
}
@route({
routes: [
{ path: 'a1/:a', component: A1, transitionPlan: 'invoke-lifecycles' },
{ path: 'a2/:c', component: A2, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({
name: 'root',
template: `<au-viewport name="a1"></au-viewport><au-viewport name="a2"></au-viewport>`,
dependencies: [A1, A2],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
await router.load('a1/a/b1/b+a2/c/b2/d');
await router.load('a1/1/b1/2+a2/3/b2/4');
assert.deepStrictEqual(
[
a1Params,
b1Params,
a2Params,
b2Params,
],
[
[
{ a: 'a' },
{ a: '1' },
],
[
{ b: 'b' },
{ b: '2' },
],
[
{ c: 'c' },
{ c: '3' },
],
[
{ d: 'd' },
{ d: '4' },
],
],
);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('Router#load accepts route-id and params', async function () {
const a1Params: Params[] = [];
const a2Params: Params[] = [];
const a1Query: [string, string][][] = [];
const a2Query: [string, string][][] = [];
@customElement({
name: 'a1',
template: '',
})
class A1 implements IRouteViewModel {
public loading(params: Params, next: RouteNode) {
a1Params.push(params);
a1Query.push(Array.from(next.queryParams.entries()));
}
}
@customElement({
name: 'a2',
template: '',
})
class A2 implements IRouteViewModel {
public loading(params: Params, next: RouteNode) {
a2Params.push(params);
a2Query.push(Array.from(next.queryParams.entries()));
}
}
@route({
routes: [
{ id: 'a1', path: 'a1/:a', component: A1 },
{ id: 'a2', path: 'a2/:c', component: A2 },
]
})
@customElement({
name: 'root',
template: `<au-viewport></au-viewport>`,
dependencies: [A1, A2],
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const pushedUrls: string[] = [];
container.register(Registration.instance(
IHistory,
{
pushState(_: {} | null, __: string, url: string) {
pushedUrls.push(url);
},
replaceState: noop,
}
));
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(Root);
const router = container.get(IRouter);
const au = new Aurelia(container);
const host = ctx.createElement('div');
au.app({ component, host });
await au.start();
await router.load({ component: 'a1', params: { a: '12' } });
let url = pushedUrls.pop();
assert.match(url, /a1\/12$/, 'url1');
await router.load({ component: 'a2', params: { c: '45' } });
url = pushedUrls.pop();
assert.match(url, /a2\/45$/, 'url1');
await router.load({ component: 'a1', params: { a: '21', b: '34' } });
url = pushedUrls.pop();
assert.match(url, /a1\/21\?b=34$/, 'url1');
await router.load({ component: 'a2', params: { a: '67', c: '54' } });
url = pushedUrls.pop();
assert.match(url, /a2\/54\?a=67$/, 'url1');
assert.deepStrictEqual(
[
a1Params,
a2Params,
],
[
[
{ a: '12' },
{ a: '21' },
],
[
{ c: '45' },
{ c: '54' },
],
],
);
assert.deepStrictEqual(
[
a1Query,
a2Query
],
[
[
[],
[['b', '34']],
],
[
[],
[['a', '67']],
],
]
);
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('Router#load accepts viewport instructions with specific viewport name - component: class', async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc12', component: GrandChildOneTwo },
],
})
@customElement({ name: 'c-one', template: `c1 <au-viewport></au-viewport>`, })
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22' })
class GrandChildTwoTwo { }
@route({
routes: [
{ id: 'gc21', path: ['', 'gc21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc22', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 \${id} <au-viewport></au-viewport>`,
})
class ChildTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{
path: ['', 'c1'],
component: ChildOne,
},
{
path: 'c2/:id?',
component: ChildTwo,
},
],
})
@customElement({ name: 'my-app', template: '<au-viewport name="vp1"></au-viewport><au-viewport name="vp2" default.bind="null"></au-viewport>' })
class MyApp { }
const { au, container, host } = await start({ appRoot: MyApp });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const vps = Array.from(host.querySelectorAll(':scope>au-viewport'));
assert.html.textContent(vps[0], 'c1 gc11', 'round#1 vp1');
assert.html.textContent(vps[1], '', 'round#1 vp2');
await router.load([
{
component: ChildOne,
children: [{ component: GrandChildOneTwo }],
viewport: 'vp2',
},
{
component: ChildTwo,
params: { id: 21 },
children: [{ component: GrandChildTwoTwo }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 21 gc22', 'round#2 vp1');
assert.html.textContent(vps[1], 'c1 gc12', 'round#2 vp2');
await router.load([
{
component: ChildTwo,
viewport: 'vp2',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#3 vp1');
assert.html.textContent(vps[1], 'c2 NA gc21', 'round#3 vp2');
await au.stop(true);
});
it('Router#load accepts hierarchical viewport instructions with route-id', async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc-11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc-12', component: GrandChildOneTwo },
],
})
@customElement({ name: 'c-one', template: `c1 <au-viewport></au-viewport>`, })
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22 ${id}' })
class GrandChildTwoTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{ id: 'gc21', path: ['', 'gc-21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc-22/:id?', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 \${id} <au-viewport></au-viewport>`,
})
class ChildTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{
id: 'c1',
path: ['', 'c-1'],
component: ChildOne,
},
{
id: 'c2',
path: 'c-2/:id?',
component: ChildTwo,
},
],
})
@customElement({ name: 'my-app', template: '<au-viewport name="vp1"></au-viewport><au-viewport name="vp2" default.bind="null"></au-viewport>' })
class MyApp { }
const { au, container, host } = await start({ appRoot: MyApp });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const vps = Array.from(host.querySelectorAll(':scope>au-viewport'));
assert.html.textContent(vps[0], 'c1 gc11', 'round#1 vp1');
assert.html.textContent(vps[1], '', 'round#1 vp2');
await router.load([
{
component: 'c1',
children: [{ component: 'gc12' }],
viewport: 'vp2',
},
{
component: 'c2',
params: { id: 21 },
children: [{ component: 'gc22' }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 21 gc22 NA', 'round#2 vp1');
assert.html.textContent(vps[1], 'c1 gc12', 'round#2 vp2');
await router.load([
{
component: 'c2',
viewport: 'vp2',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#3 vp1');
assert.html.textContent(vps[1], 'c2 NA gc21', 'round#3 vp2');
await router.load([
{
component: 'c1',
children: [{ component: 'gc12' }],
viewport: 'vp2',
},
{
component: 'c2',
params: { id: 21 },
children: [{ component: 'gc22', params: { id: 42 } }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 21 gc22 42', 'round#4 vp1');
assert.html.textContent(vps[1], 'c1 gc12', 'round#4 vp2');
await router.load([
{
component: 'c1',
children: [{ component: 'gc12' }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc12', 'round#5 vp1');
assert.html.textContent(vps[1], '', 'round#5 vp2');
await au.stop(true);
});
it('Router#load supports class-returning-function as component', async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc-11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc-12', component: GrandChildOneTwo },
],
})
@customElement({ name: 'c-one', template: `c1 <au-viewport></au-viewport>`, })
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22 ${id}' })
class GrandChildTwoTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{ id: 'gc21', path: ['', 'gc-21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc-22/:id?', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 \${id} <au-viewport></au-viewport>`,
})
class ChildTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{
id: 'c1',
path: ['', 'c-1'],
component: ChildOne,
},
{
id: 'c2',
path: 'c-2/:id?',
component: ChildTwo,
},
],
})
@customElement({ name: 'my-app', template: '<au-viewport name="vp1"></au-viewport><au-viewport name="vp2" default.bind="null"></au-viewport>' })
class MyApp { }
const { au, container, host } = await start({ appRoot: MyApp });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const vps = Array.from(host.querySelectorAll(':scope>au-viewport'));
assert.html.textContent(vps[0], 'c1 gc11', 'round#1 vp1');
assert.html.textContent(vps[1], '', 'round#1 vp2');
// single
await router.load(() => ChildTwo);
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc21', 'round#2 vp1');
assert.html.textContent(vps[1], '', 'round#2 vp2');
// sibling
await router.load([() => ChildTwo, () => ChildOne]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc21', 'round#3 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#3 vp2');
// viewport instruction
await router.load([
{ component: () => ChildTwo, viewport: 'vp2' },
{ component: () => ChildOne, viewport: 'vp1' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#4 vp1');
assert.html.textContent(vps[1], 'c2 NA gc21', 'round#4 vp2');
// viewport instruction - params
await router.load([
{ component: () => ChildTwo, viewport: 'vp1', params: { id: 42 } },
{ component: () => ChildOne, viewport: 'vp2' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 42 gc21', 'round#5 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#5 vp2');
// viewport instruction - children
await router.load([
{ component: () => ChildTwo, viewport: 'vp2', children: [() => GrandChildTwoTwo] },
{ component: () => ChildOne, viewport: 'vp1', children: [() => GrandChildOneTwo] },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc12', 'round#6 vp1');
assert.html.textContent(vps[1], 'c2 NA gc22 NA', 'round#6 vp2');
// viewport instruction - parent-params - children
await router.load([
{ component: () => ChildTwo, viewport: 'vp1', params: { id: 42 }, children: [() => GrandChildTwoTwo] },
{ component: () => ChildOne, viewport: 'vp2' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 42 gc22 NA', 'round#7 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#7 vp2');
// viewport instruction - parent-params - children-params
await router.load([
{ component: () => ChildTwo, viewport: 'vp2', params: { id: 42 }, children: [{ component: () => GrandChildTwoTwo, params: { id: 21 } }] },
{ component: () => ChildOne, viewport: 'vp1', children: [() => GrandChildOneTwo] },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc12', 'round#8 vp1');
assert.html.textContent(vps[1], 'c2 42 gc22 21', 'round#8 vp2');
await au.stop(true);
});
// Use-case: router.load(import('./class'))
it('Router#load supports promise as component', async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc-11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc-12', component: GrandChildOneTwo },
],
})
@customElement({ name: 'c-one', template: `c1 <au-viewport></au-viewport>`, })
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22 ${id}' })
class GrandChildTwoTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{ id: 'gc21', path: ['', 'gc-21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc-22/:id?', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 \${id} <au-viewport></au-viewport>`,
})
class ChildTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{
id: 'c1',
path: ['', 'c-1'],
component: ChildOne,
},
{
id: 'c2',
path: 'c-2/:id?',
component: ChildTwo,
},
],
})
@customElement({ name: 'my-app', template: '<au-viewport name="vp1"></au-viewport><au-viewport name="vp2" default.bind="null"></au-viewport>' })
class MyApp { }
const { au, container, host } = await start({ appRoot: MyApp });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const vps = Array.from(host.querySelectorAll(':scope>au-viewport'));
assert.html.textContent(vps[0], 'c1 gc11', 'round#1 vp1');
assert.html.textContent(vps[1], '', 'round#1 vp2');
// single - default
await router.load(Promise.resolve({ default: ChildTwo }));
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc21', 'round#2 vp1');
assert.html.textContent(vps[1], '', 'round#2 vp2');
// single - non-default
await router.load(Promise.resolve({ ChildOne }));
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#3 vp1');
assert.html.textContent(vps[1], '', 'round#3 vp2');
// single - chained
await router.load(Promise.resolve({ ChildOne, ChildTwo }).then(x => x.ChildTwo));
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc21', 'round#4 vp1');
assert.html.textContent(vps[1], '', 'round#4 vp2');
// sibling
await router.load([Promise.resolve({ ChildTwo }), Promise.resolve({ ChildOne })]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc21', 'round#5 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#5 vp2');
// viewport instruction
await router.load([
{ component: Promise.resolve({ ChildTwo }), viewport: 'vp2' },
{ component: Promise.resolve({ ChildOne }), viewport: 'vp1' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#6 vp1');
assert.html.textContent(vps[1], 'c2 NA gc21', 'round#6 vp2');
// viewport instruction - params
await router.load([
{ component: Promise.resolve({ ChildTwo }), viewport: 'vp1', params: { id: 42 } },
{ component: Promise.resolve({ ChildOne }), viewport: 'vp2' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 42 gc21', 'round#7 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#7 vp2');
// viewport instruction - children
await router.load([
{ component: Promise.resolve({ ChildTwo }), viewport: 'vp2', children: [() => GrandChildTwoTwo] },
{ component: Promise.resolve({ ChildOne }), viewport: 'vp1', children: [() => GrandChildOneTwo] },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc12', 'round#8 vp1');
assert.html.textContent(vps[1], 'c2 NA gc22 NA', 'round#8 vp2');
// viewport instruction - parent-params - children
await router.load([
{ component: Promise.resolve({ ChildTwo }), viewport: 'vp1', params: { id: 42 }, children: [() => GrandChildTwoTwo] },
{ component: Promise.resolve({ ChildOne }), viewport: 'vp2' },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 42 gc22 NA', 'round#9 vp1');
assert.html.textContent(vps[1], 'c1 gc11', 'round#9 vp2');
// viewport instruction - parent-params - children-params
await router.load([
{ component: Promise.resolve({ ChildTwo }), viewport: 'vp2', params: { id: 42 }, children: [{ component: () => GrandChildTwoTwo, params: { id: 21 } }] },
{ component: Promise.resolve({ ChildOne }), viewport: 'vp1', children: [() => GrandChildOneTwo] },
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc12', 'round#10 vp1');
assert.html.textContent(vps[1], 'c2 42 gc22 21', 'round#10 vp2');
await au.stop(true);
});
it('Router#load accepts viewport instructions with specific viewport name - component: mixed', async function () {
@customElement({ name: 'gc-11', template: 'gc11' })
class GrandChildOneOne { }
@customElement({ name: 'gc-12', template: 'gc12' })
class GrandChildOneTwo { }
@route({
routes: [
{ id: 'gc11', path: ['', 'gc-11'], component: GrandChildOneOne },
{ id: 'gc12', path: 'gc-12', component: GrandChildOneTwo },
],
})
@customElement({ name: 'c-one', template: `c1 <au-viewport></au-viewport>`, })
class ChildOne { }
@customElement({ name: 'gc-21', template: 'gc21' })
class GrandChildTwoOne { }
@customElement({ name: 'gc-22', template: 'gc22 ${id}' })
class GrandChildTwoTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{ id: 'gc21', path: ['', 'gc-21'], component: GrandChildTwoOne },
{ id: 'gc22', path: 'gc-22/:id?', component: GrandChildTwoTwo },
],
})
@customElement({
name: 'c-two',
template: `c2 \${id} <au-viewport></au-viewport>`,
})
class ChildTwo {
private id: string;
public loading(params: Params) {
this.id = params.id ?? 'NA';
}
}
@route({
routes: [
{
id: 'c1',
path: ['', 'c-1'],
component: ChildOne,
},
{
id: 'c2',
path: 'c-2/:id?',
component: ChildTwo,
},
],
})
@customElement({ name: 'my-app', template: '<au-viewport name="vp1"></au-viewport><au-viewport name="vp2" default.bind="null"></au-viewport>' })
class MyApp { }
const { au, container, host } = await start({ appRoot: MyApp });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const vps = Array.from(host.querySelectorAll(':scope>au-viewport'));
assert.html.textContent(vps[0], 'c1 gc11', 'round#1 vp1');
assert.html.textContent(vps[1], '', 'round#1 vp2');
await router.load([
{
component: 'c1', /* route-id */
children: [{ component: 'gc-12' /* path */ }],
viewport: 'vp2',
},
{
component: ChildTwo, /* class */
params: { id: 21 },
children: [{ component: 'gc22' /* route-id */ }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 21 gc22 NA', 'round#2 vp1');
assert.html.textContent(vps[1], 'c1 gc12', 'round#2 vp2');
await router.load([
{
component: CustomElement.getDefinition(ChildTwo), /* definition */
viewport: 'vp2',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#3 vp1');
assert.html.textContent(vps[1], 'c2 NA gc21', 'round#3 vp2');
await router.load([
{
component: CustomElement.getDefinition(ChildTwo), /* definition */
params: { id: 42 },
children: [{ component: GrandChildTwoTwo /* class */, params: { id: 21 } }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 42 gc22 21', 'round#4 vp1');
assert.html.textContent(vps[1], '', 'round#4 vp2');
await router.load([
{
component: CustomElement.getDefinition(ChildTwo), /* definition */
children: [{ component: GrandChildTwoTwo /* class */ }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 NA gc22 NA', 'round#5 vp1');
assert.html.textContent(vps[1], '', 'round#5 vp2');
await router.load([
{
component: () => ChildTwo,
params: { id: 42 },
children: [{ component: Promise.resolve({ GrandChildTwoTwo }), params: { id: 21 } }],
viewport: 'vp2',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c1 gc11', 'round#6 vp1');
assert.html.textContent(vps[1], 'c2 42 gc22 21', 'round#6 vp2');
await router.load([
{
component: Promise.resolve({ ChildTwo }),
params: { id: 21 },
children: [{ component: CustomElement.getDefinition(GrandChildTwoTwo), params: { id: 42 } }],
viewport: 'vp1',
},
]);
await queue.yield();
assert.html.textContent(vps[0], 'c2 21 gc22 42', 'round#7 vp1');
assert.html.textContent(vps[1], '', 'round#7 vp2');
await au.stop(true);
});
it('children are supported as residue as well as structured children array', async function () {
@route('c1')
@customElement({ name: 'c-1', template: 'c1' })
class C1 { }
@route('c2')
@customElement({ name: 'c-2', template: 'c2' })
class C2 { }
@route({ path: 'p1', routes: [C1, C2] })
@customElement({ name: 'p-1', template: 'p1 <au-viewport name="$1"></au-viewport> <au-viewport name="$2"></au-viewport>' })
class P1 { }
@route(['', 'p2'])
@customElement({ name: 'p-2', template: 'p2' })
class P2 { }
@route({ routes: [P1, P2] })
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
const queue = container.get(IPlatform).taskQueue;
assert.html.textContent(host, 'p2');
await router.load({
component: 'p1/c1',
children: [{ component: C2, viewport: '$2' }]
});
await queue.yield();
assert.html.textContent(host, 'p1 c1 c2');
await au.stop(true);
});
// TODO(sayan): add more tests for parameter parsing with multiple route parameters including optional parameter.
it('does not interfere with standard "href" attribute', async function () {
const ctx = TestContext.create();
const { container } = ctx;
container.register(TestRouterConfiguration.for(LogLevel.warn));
container.register(RouterConfiguration);
const component = container.get(CustomElement.define(
{ name: 'app', template: '<a href.bind="href">' },
class App { public href = 'abc'; }
));
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component, host }).start();
assert.strictEqual(host.querySelector('a').getAttribute('href'), 'abc');
component.href = null;
ctx.platform.domQueue.flush();
assert.strictEqual(host.querySelector('a').getAttribute('href'), null);
await au.stop(true);
});
// #region location URL generation
{
@customElement({ name: 'vm-a', template: `view-a foo: \${params.foo} | query: \${query.toString()} | fragment: \${fragment}` })
class VmA {
public params!: Params;
public query!: Readonly<URLSearchParams>;
public fragment: string;
public loading(params: Params, next: RouteNode) {
this.params = params;
this.query = next.queryParams;
this.fragment = next.fragment;
}
}
@customElement({ name: 'vm-b', template: 'view-b' })
class VmB {
public readonly router: IRouter = resolve(IRouter);
public async redirectToPath() {
await this.router.load('a?foo=bar');
}
public async redirectWithQueryObj() {
await this.router.load('a', { queryParams: { foo: 'bar' } });
}
public async redirectWithMultivaluedQuery() {
await this.router.load('a?foo=fizz', { queryParams: { foo: 'bar' } });
}
public async redirectWithRouteParamAndQueryObj() {
await this.router.load('a/fizz', { queryParams: { foo: 'bar' } });
}
public async redirectWithClassAndQueryObj() {
await this.router.load(VmA, { queryParams: { foo: 'bar' } });
}
public async redirectVpInstrcAndQueryObj() {
await this.router.load({ component: VmA, params: { foo: '42' } }, { queryParams: { foo: 'bar' } });
}
public async redirectVpInstrcRouteIdAndQueryObj() {
await this.router.load({ component: 'a' /** route-id */, params: { foo: '42', bar: 'foo' } }, { queryParams: { bar: 'fizz' } });
}
public async redirectFragment() {
await this.router.load('a#foobar');
}
public async redirectFragmentInNavOpt() {
await this.router.load('a', { fragment: 'foobar' });
}
public async redirectFragmentInPathAndNavOpt() {
await this.router.load('a#foobar', { fragment: 'fizzbuzz' });
}
public async redirectFragmentWithVpInstrc() {
await this.router.load({ component: 'a', params: { foo: '42' } }, { fragment: 'foobar' });
}
public async redirectFragmentWithVpInstrcRawUrl() {
await this.router.load({ component: 'a/42' }, { fragment: 'foobar' });
}
public async redirectFragmentSiblingViewport() {
await this.router.load([{ component: 'a/42' }, { component: 'a' }], { fragment: 'foobar' });
}
public async redirectSiblingViewport() {
await this.router.load([{ component: 'a/42' }, { component: 'a' }], { queryParams: { foo: 'bar' } });
}
public async redirectWithQueryAndFragment() {
await this.router.load({ component: 'a', params: { foo: '42' } }, { queryParams: { foo: 'bar' }, fragment: 'foobar' });
}
public async redirectWithQueryAndFragmentSiblingViewport() {
await this.router.load([{ component: 'a', params: { foo: '42' } }, { component: 'a', params: { foo: '84' } }], { queryParams: { foo: 'bar' }, fragment: 'foobar' });
}
}
@route({
title: 'base',
routes: [
{ path: ['a', 'a/:foo'], component: VmA, title: 'A', transitionPlan: 'invoke-lifecycles', },
{ path: ['', 'b'], component: VmB, title: 'B', transitionPlan: 'invoke-lifecycles' },
],
})
@customElement({ name: 'app-root', template: '<au-viewport></au-viewport> <au-viewport default.bind="null"></au-viewport>' })
class AppRoot { }
async function start(buildTitle: IRouterOptions['buildTitle'] | null = null) {
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration.customize({ buildTitle }),
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: AppRoot, host }).start();
return { host, au, container };
}
it('queryString - #1 - query string in string routing instruction', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectToPath();
assert.html.textContent(host, 'view-a foo: | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\?foo=bar$/);
await au.stop(true);
});
it('queryString - #2 - structured query string object', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithQueryObj();
assert.html.textContent(host, 'view-a foo: | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\?foo=bar$/);
await au.stop(true);
});
it('queryString - #3 - multi-valued query string - value from both string path and structured query params', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithMultivaluedQuery();
assert.html.textContent(host, 'view-a foo: | query: foo=fizz&foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\?foo=fizz&foo=bar$/);
await au.stop(true);
});
it('queryString - #4 - structured query string along with path parameter', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithRouteParamAndQueryObj();
assert.html.textContent(host, 'view-a foo: fizz | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/fizz\?foo=bar$/);
await au.stop(true);
});
it('queryString - #5 - structured query string with class as routing instruction', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithClassAndQueryObj();
assert.html.textContent(host, 'view-a foo: | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\?foo=bar$/);
await au.stop(true);
});
it('queryString - #6 - structured query string with viewport instruction', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectVpInstrcAndQueryObj();
assert.html.textContent(host, 'view-a foo: 42 | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\?foo=bar$/);
await au.stop(true);
});
it('queryString - #7 - structured query string with viewport instruction - route-id and multi-valued key', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectVpInstrcRouteIdAndQueryObj();
assert.html.textContent(host, 'view-a foo: 42 | query: bar=fizz&bar=foo | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\?bar=fizz&bar=foo$/);
await au.stop(true);
});
it('queryString - #8 - sibling viewports', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectSiblingViewport();
assert.html.textContent(host, 'view-a foo: 42 | query: foo=bar | fragment: view-a foo: | query: foo=bar | fragment:');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\+a\?foo=bar$/);
await au.stop(true);
});
it('fragment - #1 - raw fragment in path', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragment();
assert.html.textContent(host, 'view-a foo: | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a#foobar$/);
await au.stop(true);
});
it('fragment - #2 - fragment in navigation options', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragmentInNavOpt();
assert.html.textContent(host, 'view-a foo: | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a#foobar$/);
await au.stop(true);
});
it('fragment - #3 - fragment in path always wins over the fragment in navigation options', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragmentInPathAndNavOpt();
assert.html.textContent(host, 'view-a foo: | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a#foobar$/);
await au.stop(true);
});
it('fragment - #4 - with viewport instruction', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragmentWithVpInstrc();
assert.html.textContent(host, 'view-a foo: 42 | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42#foobar$/);
await au.stop(true);
});
it('fragment - #5 - with viewport instruction - raw url', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragmentWithVpInstrcRawUrl();
assert.html.textContent(host, 'view-a foo: 42 | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42#foobar$/);
await au.stop(true);
});
it('fragment - #6 - sibling viewport', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectFragmentSiblingViewport();
assert.html.textContent(host, 'view-a foo: 42 | query: | fragment: foobar view-a foo: | query: | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\+a#foobar$/);
await au.stop(true);
});
it('query and fragment', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithQueryAndFragment();
assert.html.textContent(host, 'view-a foo: 42 | query: foo=bar | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\?foo=bar#foobar$/);
await au.stop(true);
});
it('query and fragment - sibling viewport', async function () {
const { host, au, container } = await start();
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectWithQueryAndFragmentSiblingViewport();
assert.html.textContent(host, 'view-a foo: 42 | query: foo=bar | fragment: foobar view-a foo: 84 | query: foo=bar | fragment: foobar');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /a\/42\+a\/84\?foo=bar#foobar$/);
await au.stop(true);
});
it('shows title correctly', async function () {
const { host, au, container } = await start();
assert.strictEqual(container.get(IPlatform).document.title, 'B | base');
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectToPath();
assert.strictEqual(container.get(IPlatform).document.title, 'A | base');
await au.stop(true);
});
it('respects custom buildTitle', async function () {
const { host, au, container } = await start((tr) => {
const root = tr.routeTree.root;
return `${root.context.config.title} - ${root.children.map(c => c.title).join(' - ')}`;
});
assert.strictEqual(container.get(IPlatform).document.title, 'base - B');
const vmb = CustomElement.for<VmB>(host.querySelector('vm-b')).viewModel;
await vmb.redirectToPath();
assert.strictEqual(container.get(IPlatform).document.title, 'base - A');
await au.stop(true);
});
}
it('querystring is added to the fragment when hash-based routing is used', async function () {
@customElement({ name: 'c-1', template: `c1 params: \${id} query: \${query} fragment: \${fragment}` })
class C1 implements IRouteViewModel {
private id: string;
private query: string;
private fragment: string;
public loading(params: Params, node: RouteNode): void | Promise<void> {
this.id = params.id;
this.query = node.queryParams.toString();
this.fragment = node.fragment;
}
}
@route({ routes: [{ id: 'c1', path: 'c1/:id', component: C1 }] })
@customElement({ name: 'app', template: '<a load="route: c1; params.bind: {id: 42, foo: \'bar\'}"></a><au-viewport></au-viewport>' })
class App { }
const { host, container } = await start({ appRoot: App, useHash: true });
const anchor = host.querySelector('a');
assert.match(anchor.href, /#\/c1\/42\?foo=bar$/);
anchor.click();
await container.get(IPlatform).taskQueue.yield();
assert.html.textContent(host, 'c1 params: 42 query: foo=bar fragment:');
const path = (container.get(ILocation) as unknown as MockBrowserHistoryLocation).path;
assert.match(path, /#\/c1\/42\?foo=bar$/);
// assert the different parts of the url
const url = new URL(path);
assert.match(url.pathname, /\/$/);
assert.strictEqual(url.search, '');
assert.strictEqual(url.hash, '#/c1/42?foo=bar');
});
it('querystring, added to the fragment, can be parsed correctly, when hash-based routing is used', async function () {
@customElement({ name: 'c-1', template: `c1 params: \${id} query: \${query} fragment: \${fragment}` })
class C1 implements IRouteViewModel {
private id: string;
private query: string;
private fragment: string;
public loading(params: Params, node: RouteNode): void | Promise<void> {
this.id = params.id;
this.query = node.queryParams.toString();
this.fragment = node.fragment;
}
}
@route({ routes: [{ id: 'c1', path: 'c1/:id', component: C1 }] })
@customElement({ name: 'app', template: '<a href="#/c1/42?foo=bar"></a><au-viewport></au-viewport>' })
class App { }
const { host, container } = await start({ appRoot: App, useHash: true });
host.querySelector('a').click();
await container.get(IPlatform).taskQueue.yield();
assert.html.textContent(host, 'c1 params: 42 query: foo=bar fragment:');
const path = (container.get(ILocation) as unknown as MockBrowserHistoryLocation).path;
assert.match(path, /#\/c1\/42\?foo=bar$/);
// assert the different parts of the url
const url = new URL(path);
assert.match(url.pathname, /\/$/);
assert.strictEqual(url.search, '');
assert.strictEqual(url.hash, '#/c1/42?foo=bar');
});
it('querystring, added to the fragment, can be parsed correctly, when hash-based routing is used - with fragment (nested fragment JFF)', async function () {
@customElement({ name: 'c-1', template: `c1 params: \${id} query: \${query} fragment: \${fragment}` })
class C1 implements IRouteViewModel {
private id: string;
private query: string;
private fragment: string;
public loading(params: Params, node: RouteNode): void | Promise<void> {
this.id = params.id;
this.query = node.queryParams.toString();
this.fragment = node.fragment;
}
}
@route({ routes: [{ id: 'c1', path: 'c1/:id', component: C1 }] })
@customElement({ name: 'app', template: '<a href="#/c1/42?foo=bar#for-whatever-reason"></a><au-viewport></au-viewport>' })
class App { }
const { host, container } = await start({ appRoot: App, useHash: true });
host.querySelector('a').click();
await container.get(IPlatform).taskQueue.yield();
assert.html.textContent(host, 'c1 params: 42 query: foo=bar fragment: for-whatever-reason');
const path = (container.get(ILocation) as unknown as MockBrowserHistoryLocation).path;
assert.match(path, /#\/c1\/42\?foo=bar#for-whatever-reason$/);
// assert the different parts of the url
const url = new URL(path);
assert.match(url.pathname, /\/$/);
assert.strictEqual(url.search, '');
assert.strictEqual(url.hash, '#/c1/42?foo=bar#for-whatever-reason');
});
it('fragment is added to fragment (nested fragment JFF) when using hash-based routing', async function () {
@customElement({ name: 'c-1', template: `c1 params: \${id} query: \${query} fragment: \${fragment}` })
class C1 implements IRouteViewModel {
private id: string;
private query: string;
private fragment: string;
public loading(params: Params, node: RouteNode): void | Promise<void> {
this.id = params.id;
this.query = node.queryParams.toString();
this.fragment = node.fragment;
}
}
@route({ routes: [{ id: 'c1', path: 'c1/:id', component: C1 }] })
@customElement({ name: 'app', template: '<au-viewport></au-viewport>' })
class App { }
const { host, container } = await start({ appRoot: App, useHash: true });
await container.get(IRouter)
.load(
{ component: 'c1', params: { id: '42' } },
{ queryParams: { foo: 'bar' }, fragment: 'for-whatever-reason' }
);
assert.html.textContent(host, 'c1 params: 42 query: foo=bar fragment: for-whatever-reason');
const path = (container.get(ILocation) as unknown as MockBrowserHistoryLocation).path;
assert.match(path, /#\/c1\/42\?foo=bar#for-whatever-reason$/);
// assert the different parts of the url
const url = new URL(path);
assert.match(url.pathname, /\/$/);
assert.strictEqual(url.search, '');
assert.strictEqual(url.hash, '#/c1/42?foo=bar#for-whatever-reason');
});
// TODO(sayan): add more tests for title involving children and sibling routes
it('root/child/grandchild/great-grandchild', async function () {
@customElement({ name: 'gcc-1', template: `gcc1` })
class GGC1 { }
@route({
routes: [
{ path: '', component: GGC1 },
],
})
@customElement({ name: 'gc-1', template: `<au-viewport></au-viewport>`, })
class GC1 { }
@route({
routes: [
{ path: '', redirectTo: 'gc1' },
{ id: 'gc1', path: 'gc1', component: GC1 },
],
})
@customElement({ name: 'c-1', template: `<au-viewport></au-viewport>`, })
class C1 { }
@route({
routes: [
{ path: '', redirectTo: 'c1' },
{ path: 'c1', component: C1, },
],
})
@customElement({ name: 'ro-ot', template: `<au-viewport></au-viewport>`, })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
assert.html.textContent(host, 'gcc1');
assert.match((container.get(ILocation) as unknown as MockBrowserHistoryLocation).path, /c1\/gc1$/);
await au.stop(true);
});
// #endregion
// TODO(sayan): add tests here for the location URL building in relation for viewport name
describe('navigation model', function () {
function getNavBarCe(
hasAsyncRouteConfig: boolean = false,
) {
@valueConverter('firstNonEmpty')
class FirstNonEmpty {
public toView(paths: string[]): string {
for (const path of paths) {
if (path) return path;
}
}
}
@customElement({
name: 'nav-bar',
template: `<nav if.bind="navModel">
<ul>
<li repeat.for="route of navModel.routes"><a href.bind="route.path | firstNonEmpty" active.class="route.isActive">\${route.title}</a></li>
</ul>
</nav><template else>no nav model</template>`,
dependencies: [FirstNonEmpty]
})
class NavBar implements ICustomElementViewModel {
private readonly navModel: INavigationModel | null;
private readonly node: INode = resolve(INode);
public constructor() {
this.navModel = resolve(IRouteContext).navigationModel;
}
public binding(_initiator: IHydratedController, _parent: IHydratedController): void | Promise<void> {
if (hasAsyncRouteConfig) return this.navModel?.resolve();
}
public assert(expected: { href: string; text: string; active?: boolean }[], message: string = ''): void {
const anchors = Array.from((this.node as HTMLElement).querySelector('nav').querySelectorAll<HTMLAnchorElement>('a'));
const len = anchors.length;
assert.strictEqual(len, expected.length, `${message} length`);
for (let i = 0; i < len; i++) {
const anchor = anchors[i];
const item = expected[i];
assert.strictEqual(anchor.href.endsWith(item.href), true, `${message} - #${i} href - actual: ${anchor.href} - expected: ${item.href}`);
assert.html.textContent(anchor, item.text, `${message} - #${i} text`);
assert.strictEqual(anchor.classList.contains('active'), !!item.active, `${message} - #${i} active`);
}
}
}
return NavBar;
}
it('route deco', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@route({
routes: [
{ path: ['', 'c11'], component: C11, title: 'C11' },
{ path: 'c12', component: C12, title: 'C12' },
]
})
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: 'c21', component: C21, title: 'C21' },
{ path: ['', 'c22'], component: C22, title: 'C22' },
]
})
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'ce-p3', template: 'p3' })
class P3 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
{ path: 'p3', component: P3, title: 'P3', nav: false },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
C11,
C12,
C21,
C22,
P1,
P2,
P3,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12', text: 'C12', active: false }], 'start child navbar');
// Round#1
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12', text: 'C12', active: true }], 'round#2 navbar');
// Round#3
await router.load('p2/c21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#3 navbar');
// Round#4 - nav:false, but routeable
await router.load('p3');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: false }], 'round#4 root');
assert.notEqual(host.querySelector('ce-p3'), null);
await au.stop(true);
});
it('getRouteConfig hook', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: ['', 'c11'], component: C11, title: 'C11' },
{ path: 'c12', component: C12, title: 'C12' },
]
};
}
}
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: 'c21', component: C21, title: 'C21' },
{ path: ['', 'c22'], component: C22, title: 'C22' },
]
};
}
}
@customElement({ name: 'ce-p3', template: 'p3' })
class P3 { }
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root implements IRouteViewModel {
public async getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): Promise<IRouteConfig> {
await new Promise((resolve) => setTimeout(resolve, 10));
return {
routes: [
{ path: ['', 'p1'], component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
{ path: 'p3', component: P3, title: 'P3', nav: false },
]
};
}
}
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
C11,
C12,
C21,
C22,
P1,
P2,
P3,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12', text: 'C12', active: false }], 'start child navbar');
// Round#1
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12', text: 'C12', active: true }], 'round#2 navbar');
// Round#3
await router.load('p2/c21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#3 navbar');
// Round#4 - nav:false, but routeable
await router.load('p3');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: false }], 'round#4 root');
assert.notEqual(host.querySelector('ce-p3'), null);
await au.stop(true);
});
it('async configuration', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: ['', 'c11'], component: Promise.resolve({ C11 }), title: 'C11' },
{ path: 'c12', component: C12, title: 'C12' },
]
};
}
}
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: 'c21', component: Promise.resolve({ 'default': C21 }), title: 'C21' },
{ path: ['', 'c22'], component: C22, title: 'C22' },
]
};
}
}
@route({ path: 'p3', title: 'P3', nav: false })
@customElement({ name: 'ce-p3', template: 'p3' })
class P3 { }
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: ['', 'p1'], component: Promise.resolve({ P1, 'default': { foo: 'bar' }, 'fizz': 'buzz' }).then(x => x.P1), title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
Promise.resolve({ P3 }),
]
};
}
}
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe(true);
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
C11,
C12,
C21,
C22,
P1,
P2,
P3,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12', text: 'C12', active: false }], 'start child navbar');
// Round#1
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12', text: 'C12', active: true }], 'round#2 navbar');
// Round#3
await router.load('p2/c21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#3 navbar');
// Round#4 - nav:false, but routeable
await router.load('p3');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: false }], 'round#4 root');
assert.notEqual(host.querySelector('ce-p3'), null);
await au.stop(true);
});
it('parameterized route', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@route({
routes: [
{ path: ['', 'c11'], component: C11, title: 'C11' },
{ path: ['c12/:id', 'c12'], component: C12, title: 'C12' },
]
})
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: ['c21', 'c21/:id'], component: C21, title: 'C21' },
{ path: ['', 'c22'], component: C22, title: 'C22' },
]
})
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 implements IRouteViewModel { }
@route({
routes: [
{ path: ['', 'p1'], component: P1, title: 'P1' },
{ path: ['p2/:id', 'p2'], component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
navBarCe,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2/:id', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12/:id', text: 'C12', active: false }], 'start child navbar');
// Round#2
await router.load('p2/42');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2/:id', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2/:id', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12/:id', text: 'C12', active: true }], 'round#2 child navbar');
// Round#3
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2/:id', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#3 child navbar');
// Round#4
await router.load('p1/c12/42');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2/:id', text: 'P2', active: false }], 'round#4 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12/:id', text: 'C12', active: true }], 'round#4 child navbar');
// Round#5
await router.load('p2/42/C21/21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2/:id', text: 'P2', active: true }], 'round#5 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#5 child navbar');
await au.stop(true);
});
it('with redirection', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@route({
routes: [
{ path: '', redirectTo: 'c11' },
{ path: 'c11', component: C11, title: 'C11' },
{ path: 'c12', component: C12, title: 'C12' },
]
})
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: '', redirectTo: 'c22' },
{ path: 'c21', component: C21, title: 'C21' },
{ path: 'c22', component: C22, title: 'C22' },
]
})
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'ce-p3', template: 'p3' })
class P3 { }
@route({
routes: [
{ path: '', redirectTo: 'p1' },
{ path: 'p1', component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
{ path: 'p3', component: P3, title: 'P3', nav: false },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12', text: 'C12', active: false }], 'start child navbar');
// Round#1
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12', text: 'C12', active: true }], 'round#2 navbar');
// Round#3
await router.load('p2/c21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#3 navbar');
// Round#4 - nav:false, but routeable
await router.load('p3');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: false }], 'round#4 root');
assert.notEqual(host.querySelector('ce-p3'), null);
await au.stop(true);
});
it('with redirection - path with redirection is not shown', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@customElement({ name: 'ce-c12', template: 'c12' })
class C12 { }
@customElement({ name: 'ce-c21', template: 'c21' })
class C21 { }
@customElement({ name: 'ce-c22', template: 'c22' })
class C22 { }
@route({
routes: [
{ path: '', redirectTo: 'c11' },
{ path: 'c11', component: C11, title: 'C11' },
{ path: 'c12', component: C12, title: 'C12' },
]
})
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: '', redirectTo: 'c22' },
{ path: 'c21', component: C21, title: 'C21' },
{ path: 'c22', component: C22, title: 'C22' },
]
})
@customElement({ name: 'ce-p2', template: '<nav-bar></nav-bar> p2 <au-viewport></au-viewport>' })
class P2 { }
@customElement({ name: 'ce-p3', template: 'p3' })
class P3 { }
@route({
routes: [
{ path: '', redirectTo: 'p1' },
{ path: 'p1', component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
{ path: 'p3', component: P3, title: 'P3', nav: false },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe(false);
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'start root');
let childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: true }, { href: 'c12', text: 'C12', active: false }], 'start child navbar');
// Round#1
await router.load('p2');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#1 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: false }, { href: 'c22', text: 'C22', active: true }], 'round#1 child navbar');
// Round#2
await router.load('p1/c12');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: true }, { href: 'p2', text: 'P2', active: false }], 'round#2 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p1>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c11', text: 'C11', active: false }, { href: 'c12', text: 'C12', active: true }], 'round#2 navbar');
// Round#3
await router.load('p2/c21');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: true }], 'round#3 root');
childNavBar = CustomElement.for<NavBar>(host.querySelector('ce-p2>nav-bar')).viewModel;
childNavBar.assert([{ href: 'c21', text: 'C21', active: true }, { href: 'c22', text: 'C22', active: false }], 'round#3 navbar');
// Round#4 - nav:false, but routeable
await router.load('p3');
await queue.yield();
rootNavbar.assert([{ href: 'p1', text: 'P1', active: false }, { href: 'p2', text: 'P2', active: false }], 'round#4 root');
assert.notEqual(host.querySelector('ce-p3'), null);
await au.stop(true);
});
it('parameterized redirection', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: 'p2' })
class P2 { }
@route({
routes: [
{ path: 'foo/:id', redirectTo: 'p1/:id' },
{ path: 'bar/:id', redirectTo: 'p2/:id' },
{ path: 'p1/:id', component: P1, title: 'P1' },
{ path: 'p2/:id', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
const router = container.get(IRouter);
// Start
await queue.yield();
type NavBar = InstanceType<typeof navBarCe>;
const rootNavbar = CustomElement.for<NavBar>(host.querySelector('nav-bar')).viewModel;
rootNavbar.assert([{ href: 'p1/:id', text: 'P1', active: false }, { href: 'p2/:id', text: 'P2', active: false }], 'start root');
// Round#1
await router.load('bar/42');
await queue.yield();
rootNavbar.assert([{ href: 'p1/:id', text: 'P1', active: false }, { href: 'p2/:id', text: 'P2', active: true }], 'round#1 root');
// Round#2
await router.load('foo/42');
await queue.yield();
rootNavbar.assert([{ href: 'p1/:id', text: 'P1', active: true }, { href: 'p2/:id', text: 'P2', active: false }], 'round#2 root');
await au.stop(true);
});
it('can be deactivated', async function () {
@customElement({ name: 'ce-c11', template: 'c11' })
class C11 { }
@route({
routes: [
{ path: ['', 'c11'], component: C11, title: 'C11' },
]
})
@customElement({ name: 'ce-p1', template: '<nav-bar></nav-bar> p1 <au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1, title: 'P1' },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
const navBarCe = getNavBarCe();
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration.customize({ useNavigationModel: false }),
navBarCe
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const queue = container.get(IPlatform).domQueue;
await queue.yield();
assert.html.textContent(host, 'no nav model root no nav model p1 c11');
await au.stop(true);
});
class InvalidAsyncComponentTestData {
public constructor(
public readonly name: string,
public readonly component: Promise<IModule>
) { }
}
function* getInvalidAsyncComponentTestData() {
yield new InvalidAsyncComponentTestData('empty', Promise.resolve({}));
yield new InvalidAsyncComponentTestData('no-CE in module', Promise.resolve({ foo() { /** noop */ } }));
}
for (const { name, component } of getInvalidAsyncComponentTestData()) {
it(`async configuration - invalid module - ${name}`, async function () {
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root implements IRouteViewModel {
public getRouteConfig(_parentDefinition: RouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
{ path: '', component, title: 'P1' },
]
};
}
}
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
try {
await au.app({ component: Root, host }).start();
assert.fail('expected error');
} catch (er) {
assert.match((er as Error).message, /AUR3175/);
}
await au.stop(true);
});
}
});
it('isNavigating indicates router\'s navigation status', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: 'p2' })
class P2 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<nav-bar></nav-bar> root <au-viewport></au-viewport>' })
class Root {
public isNavigatingLog: boolean[] = [];
public readonly router: IRouter = resolve(IRouter);
@watch<Root>(root => root['router'].isNavigating)
public logIsNavigating(isNavigating: boolean) {
this.isNavigatingLog.push(isNavigating);
}
}
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
P1,
P2,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const log = (au.root.controller.viewModel as Root).isNavigatingLog;
assert.deepStrictEqual(log, [true, false]);
log.length = 0;
await container.get(IRouter).load('p2');
assert.deepStrictEqual(log, [true, false]);
await au.stop(true);
});
it('custom base path can be configured', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: 'p2' })
class P2 { }
@route({
routes: [
{ path: 'p1', component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<a load="p1"></a><a load="p2"></a><au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
// mocked window
container.register(Registration.instance(IWindow, {
document: {
baseURI: 'https://portal.example.com/',
},
removeEventListener() { /** noop */ },
addEventListener() { /** noop */ },
}));
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration.customize({ basePath: '/mega-dodo/guide1/' }),
P1,
P2,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const anchors = Array.from(host.querySelectorAll('a'));
assert.deepStrictEqual(anchors.map(a => a.href), ['https://portal.example.com/mega-dodo/guide1/p1', 'https://portal.example.com/mega-dodo/guide1/p2']);
assert.strictEqual(host.querySelector('ce-p1'), null);
assert.strictEqual(host.querySelector('ce-p2'), null);
anchors[0].click();
const queue = container.get(IPlatform).domQueue;
await queue.yield();
assert.notEqual(host.querySelector('ce-p1'), null);
assert.strictEqual(host.querySelector('ce-p2'), null);
anchors[1].click();
await queue.yield();
assert.strictEqual(host.querySelector('ce-p1'), null);
assert.notEqual(host.querySelector('ce-p2'), null);
const router = container.get(IRouter);
await router.load('/mega-dodo/guide1/p1');
assert.notEqual(host.querySelector('ce-p1'), null);
assert.strictEqual(host.querySelector('ce-p2'), null);
await router.load('/mega-dodo/guide1/p2');
assert.strictEqual(host.querySelector('ce-p1'), null);
assert.notEqual(host.querySelector('ce-p2'), null);
await au.stop(true);
});
it('multiple paths can redirect to same path', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: 'p2' })
class P2 { }
@route({
routes: [
{ path: ['', 'foo'], redirectTo: 'p2' },
{ path: 'p1', component: P1, title: 'P1' },
{ path: 'p2', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
P1,
P2,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
const router = container.get(IRouter);
await au.app({ component: Root, host }).start();
assert.html.textContent(host, 'p2');
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
assert.match(location.path, /p2$/);
await router.load('p1');
assert.html.textContent(host, 'p1');
assert.match(location.path, /p1$/);
await router.load('foo');
assert.html.textContent(host, 'p2');
assert.match(location.path, /p2$/);
await au.stop(true);
});
it('parameterized redirect', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: `p2 \${id}` })
class P2 implements IRouteViewModel {
private id: string;
public loading(params: Params, _next: RouteNode, _current: RouteNode): void | Promise<void> {
this.id = params.id;
}
}
@route({
routes: [
{ path: '', redirectTo: 'p2' },
{ path: 'foo', redirectTo: 'p2/42' },
{ path: 'fizz/:bar', redirectTo: 'p2/:bar' },
{ path: 'bar', redirectTo: 'p2/43+p2/44' },
{ path: 'p1', component: P1, title: 'P1' },
{ path: 'p2/:id?', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport><au-viewport default.bind="null"></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
P1,
P2,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
const router = container.get(IRouter);
await au.app({ component: Root, host }).start();
assert.html.textContent(host, 'p2');
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
assert.match(location.path, /p2$/);
await router.load('p1');
assert.html.textContent(host, 'p1');
assert.match(location.path, /p1$/);
await router.load('foo');
assert.html.textContent(host, 'p2 42');
assert.match(location.path, /p2\/42$/);
await router.load('fizz/21');
assert.html.textContent(host, 'p2 21');
assert.match(location.path, /p2\/21$/);
try {
await router.load('bar');
assert.fail('Expected error for non-simple redirect.');
} catch (e) {
assert.match((e as Error).message, /AUR3502/, 'Expected error due to unexpected path segment.');
}
await au.stop(true);
});
it('parameterized redirect - parameter rearrange', async function () {
@customElement({ name: 'ce-p1', template: 'p1' })
class P1 { }
@customElement({ name: 'ce-p2', template: `p2 \${p1} \${p2}` })
class P2 implements IRouteViewModel {
private p1: string;
private p2: string;
public loading(params: Params, _next: RouteNode, _current: RouteNode): void | Promise<void> {
this.p1 = params.p1;
this.p2 = params.p2;
}
}
@route({
routes: [
{ path: 'fizz/:foo/:bar', redirectTo: 'p2/:bar/:foo' },
{ path: 'p2/:p1?/:p2?', component: P2, title: 'P2' },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport><au-viewport default.bind="null"></au-viewport>' })
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
P1,
P2,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
const router = container.get(IRouter);
await au.app({ component: Root, host }).start();
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
await router.load('fizz/1/2');
assert.html.textContent(host, 'p2 2 1');
assert.match(location.path, /p2\/2\/1$/);
await au.stop(true);
});
describe('path generation', function () {
it('at root', async function () {
abstract class BaseRouteViewModel implements IRouteViewModel {
public static paramsLog: Map<string, [Params, URLSearchParams]> = new Map<string, [Params, URLSearchParams]>();
public static assertAndClear(key: string, expected: [Params, URLSearchParams], message: string) {
assert.deepStrictEqual(this.paramsLog.get(key), expected, message);
this.paramsLog.clear();
}
public loading(params: Params, next: RouteNode, _: RouteNode): void | Promise<void> {
BaseRouteViewModel.paramsLog.set(this.constructor.name.toLowerCase(), [params, next.queryParams]);
}
}
@customElement({ name: 'fo-o', template: '' })
class Foo extends BaseRouteViewModel { }
@customElement({ name: 'ba-r', template: '' })
class Bar extends BaseRouteViewModel { }
@customElement({ name: 'fi-zz', template: '' })
class Fizz extends BaseRouteViewModel { }
@route({
routes: [
{ id: 'foo', path: ['foo/:id', 'foo/:id/bar/:a', 'foo/:id/:bar?/*b'], component: Foo },
{ id: 'bar', path: ['bar/:id'], component: Bar },
{ id: 'fizz', path: ['fizz/:x', 'fizz/:y/:x'], component: Fizz },
]
})
@customElement({
name: 'ro-ot',
template: `<au-viewport></au-viewport>`
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
Foo,
Bar,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
const router = container.get(IRouter);
// using route-id
assert.strictEqual(await router.load({ component: 'foo', params: { id: '1', a: '3' } }), true);
assert.match(location.path, /foo\/1\/bar\/3$/);
BaseRouteViewModel.assertAndClear('foo', [{ id: '1', a: '3' }, new URLSearchParams()], 'params1');
assert.strictEqual(await router.load({ component: 'foo', params: { id: '1', c: '3' } }), true);
assert.match(location.path, /foo\/1\?c=3$/);
BaseRouteViewModel.assertAndClear('foo', [{ id: '1' }, new URLSearchParams({ c: '3' })], 'params2');
assert.strictEqual(await router.load({ component: 'bar', params: { id: '1', c: '4' } }), true);
assert.match(location.path, /bar\/1\?c=4$/);
BaseRouteViewModel.assertAndClear('bar', [{ id: '1' }, new URLSearchParams({ c: '4' })], 'params3');
assert.strictEqual(await router.load({ component: 'foo', params: { id: '1', b: 'awesome/possum' } }), true);
assert.match(location.path, /foo\/1\/awesome%2Fpossum$/);
BaseRouteViewModel.assertAndClear('foo', [{ id: '1', b: 'awesome/possum' }, new URLSearchParams()], 'params4');
try {
await router.load({ component: 'bar', params: { x: '1' } });
assert.fail('expected error1');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+bar/);
}
try {
await router.load({ component: 'fizz', params: { id: '1' } });
assert.fail('expected error2');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+fizz/);
}
// using component
assert.strictEqual(await router.load({ component: Foo, params: { id: '1', a: '3' } }), true);
assert.match(location.path, /foo\/1\/bar\/3$/);
BaseRouteViewModel.assertAndClear('foo', [{ id: '1', a: '3' }, new URLSearchParams()], 'params5');
assert.strictEqual(await router.load({ component: Foo, params: { id: '1', c: '3' } }), true);
assert.match(location.path, /foo\/1\?c=3$/);
BaseRouteViewModel.assertAndClear('foo', [{ id: '1' }, new URLSearchParams({ c: '3' })], 'params6');
try {
await router.load({ component: Bar, params: { x: '1' } });
assert.fail('expected error1');
} catch (er) {
assert.match((er as Error).message, /No value for the required parameter 'id'/);
}
try {
await router.load({ component: Fizz, params: { id: '1' } });
assert.fail('expected error2');
} catch (er) {
assert.match(
(er as Error).message,
/required parameter 'x'.+path: 'fizz\/:x'.+required parameter 'y'.+path: 'fizz\/:y\/:x'/
);
}
// use path (non-eager resolution)
assert.strictEqual(await router.load('bar/1?b=3'), true);
BaseRouteViewModel.assertAndClear('bar', [{ id: '1' }, new URLSearchParams({ b: '3' })], 'params7');
await au.stop(true);
});
it('at root - with siblings', async function () {
abstract class BaseRouteViewModel implements IRouteViewModel {
public static paramsLog: Map<string, [Params, URLSearchParams]> = new Map<string, [Params, URLSearchParams]>();
public static assertAndClear(message: string, ...expected: [key: string, value: [Params, URLSearchParams]][]) {
const paramsLog = this.paramsLog;
assert.deepStrictEqual(paramsLog, new Map(expected), message);
paramsLog.clear();
}
public loading(params: Params, next: RouteNode, _: RouteNode): void | Promise<void> {
BaseRouteViewModel.paramsLog.set(this.constructor.name.toLowerCase(), [params, next.queryParams]);
}
}
@customElement({ name: 'fo-o', template: '' })
class Foo extends BaseRouteViewModel { }
@customElement({ name: 'ba-r', template: '' })
class Bar extends BaseRouteViewModel { }
@customElement({ name: 'fi-zz', template: '' })
class Fizz extends BaseRouteViewModel { }
@route({
routes: [
{ id: 'foo', path: ['foo/:id', 'foo/:id/faa/:a'], component: Foo },
{ id: 'bar', path: ['bar/:id'], component: Bar },
{ id: 'fizz', path: ['fizz/:x', 'fizz/:y/:x'], component: Fizz },
]
})
@customElement({
name: 'ro-ot',
template: `<au-viewport></au-viewport><au-viewport></au-viewport>`
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
Foo,
Bar,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
const router = container.get(IRouter);
// using route-id
assert.strictEqual(await router.load([{ component: 'foo', params: { id: '1', a: '3' } }, { component: 'bar', params: { id: '1', b: '3' } }]), true);
assert.match(location.path, /foo\/1\/faa\/3\+bar\/1\?b=3$/);
BaseRouteViewModel.assertAndClear('params1', ['foo', [{ id: '1', a: '3' }, new URLSearchParams({ b: '3' })]], ['bar', [{ id: '1' }, new URLSearchParams({ b: '3' })]]);
assert.strictEqual(await router.load([{ component: 'bar', params: { id: '2' } }, { component: 'foo', params: { id: '3' } }]), true);
assert.match(location.path, /bar\/2\+foo\/3$/);
BaseRouteViewModel.assertAndClear('params1', ['bar', [{ id: '2' }, new URLSearchParams()]], ['foo', [{ id: '3' }, new URLSearchParams()]]);
try {
await router.load([{ component: 'foo', params: { id: '3' } }, { component: 'bar', params: { x: '1' } }]);
assert.fail('expected error1');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+bar/);
}
try {
await router.load([{ component: 'foo', params: { id: '3' } }, { component: 'fizz', params: { id: '1' } }]);
assert.fail('expected error2');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+fizz/);
}
// using component
assert.strictEqual(await router.load([{ component: Foo, params: { id: '1', a: '3' } }, { component: Bar, params: { id: '1', b: '3' } }]), true);
assert.match(location.path, /foo\/1\/faa\/3\+bar\/1\?b=3$/);
BaseRouteViewModel.assertAndClear('params3', ['foo', [{ id: '1', a: '3' }, new URLSearchParams({ b: '3' })]], ['bar', [{ id: '1' }, new URLSearchParams({ b: '3' })]]);
assert.strictEqual(await router.load([{ component: Bar, params: { id: '2' } }, { component: Foo, params: { id: '3' } }]), true);
assert.match(location.path, /bar\/2\+foo\/3$/);
BaseRouteViewModel.assertAndClear('params4', ['bar', [{ id: '2' }, new URLSearchParams()]], ['foo', [{ id: '3' }, new URLSearchParams()]]);
try {
await router.load([{ component: Foo, params: { id: '3' } }, { component: Bar, params: { x: '1' } }]);
assert.fail('expected error1');
} catch (er) {
assert.match((er as Error).message, /No value for the required parameter 'id'/);
}
try {
await router.load([{ component: Foo, params: { id: '3' } }, { component: Fizz, params: { id: '1' } }]);
assert.fail('expected error2');
} catch (er) {
assert.match(
(er as Error).message,
/required parameter 'x'.+path: 'fizz\/:x'.+required parameter 'y'.+path: 'fizz\/:y\/:x'/
);
}
// path that cannot be eagerly resolved
assert.strictEqual(await router.load('foo/11+bar/21?b=3'), true);
BaseRouteViewModel.assertAndClear('params5', ['foo', [{ id: '11' }, new URLSearchParams({ b: '3' })]], ['bar', [{ id: '21' }, new URLSearchParams({ b: '3' })]]);
await au.stop(true);
});
it('with parent-child hierarchy', async function () {
abstract class BaseRouteViewModel implements IRouteViewModel {
public static paramsLog: Map<string, [Params, URLSearchParams]> = new Map<string, [Params, URLSearchParams]>();
public static assertAndClear(message: string, ...expected: [key: string, value: [Params, URLSearchParams]][]) {
const paramsLog = this.paramsLog;
assert.deepStrictEqual(paramsLog, new Map(expected), message);
paramsLog.clear();
}
public loading(params: Params, next: RouteNode, _: RouteNode): void | Promise<void> {
BaseRouteViewModel.paramsLog.set(this.constructor.name.toLowerCase(), [params, next.queryParams]);
}
}
@customElement({ name: 'ce-l21', template: '' })
class CeL21 extends BaseRouteViewModel { }
@customElement({ name: 'ce-l22', template: '' })
class CeL22 extends BaseRouteViewModel { }
@customElement({ name: 'ce-l23', template: '' })
class CeL23 extends BaseRouteViewModel { }
@customElement({ name: 'ce-l24', template: '' })
class CeL24 extends BaseRouteViewModel { }
@route({
routes: [
{ id: '21', path: ['21/:id', '21/:id/to/:a'], component: CeL21 },
{ id: '22', path: ['22/:id'], component: CeL22 },
]
})
@customElement({ name: 'ce-l11', template: '<au-viewport></au-viewport>' })
class CeL11 extends BaseRouteViewModel { }
@route({
routes: [
{ id: '23', path: ['23/:id', '23/:id/tt/:a'], component: CeL23 },
{ id: '24', path: ['24/:id'], component: CeL24 },
]
})
@customElement({ name: 'ce-l12', template: '<au-viewport></au-viewport>' })
class CeL12 extends BaseRouteViewModel { }
@route({
routes: [
{ id: '11', path: ['11/:id', '11/:id/oo/:a'], component: CeL11 },
{ id: '12', path: ['12/:id'], component: CeL12 },
]
})
@customElement({
name: 'ro-ot',
template: `<au-viewport></au-viewport>`
})
class Root { }
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
CeL11,
CeL21,
CeL22,
CeL12,
CeL23,
CeL24,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
const router = container.get(IRouter);
// using route-id
assert.strictEqual(
await router.load({
component: '11',
params: { id: '1', a: '3' },
children: [{ component: '21', params: { id: '2', a: '4' } }]
}),
true);
assert.match(location.path, /11\/1\/oo\/3\/21\/2\/to\/4$/);
BaseRouteViewModel.assertAndClear('params1', ['cel11', [{ id: '1', a: '3' }, new URLSearchParams()]], ['cel21', [{ id: '2', a: '4' }, new URLSearchParams()]]);
assert.strictEqual(
await router.load({
component: '12',
params: { id: '1', a: '3' },
children: [{ component: '24', params: { id: '2', a: '4' } }]
}),
true);
assert.match(location.path, /12\/1\/24\/2\?a=3&a=4$/);
BaseRouteViewModel.assertAndClear('params2', ['cel12', [{ id: '1' }, new URLSearchParams([['a', '3']])]], ['cel24', [{ id: '2' }, new URLSearchParams([['a', '3'], ['a', '4']])]]);
// using CE class
assert.strictEqual(
await router.load({
component: CeL11,
params: { id: '1', a: '3' },
children: [{ component: CeL21, params: { id: '2', a: '4' } }]
}),
true);
assert.match(location.path, /11\/1\/oo\/3\/21\/2\/to\/4$/);
BaseRouteViewModel.assertAndClear('params3', ['cel11', [{ id: '1', a: '3' }, new URLSearchParams()]], ['cel21', [{ id: '2', a: '4' }, new URLSearchParams()]]);
assert.strictEqual(
await router.load({
component: CeL12,
params: { id: '1', a: '3' },
children: [{ component: CeL24, params: { id: '2', a: '4' } }]
}),
true);
assert.match(location.path, /12\/1\/24\/2\?a=3&a=4$/);
BaseRouteViewModel.assertAndClear('params4', ['cel12', [{ id: '1' }, new URLSearchParams([['a', '3']])]], ['cel24', [{ id: '2' }, new URLSearchParams([['a', '3'], ['a', '4']])]]);
const el12 = host.querySelector('ce-l12');
const ce12 = CustomElement.for<CeL12>(el12).viewModel;
assert.strictEqual(await router.load({ component: CeL23, params: { id: '5', a: '6' } }, { context: ce12 }), true);
assert.match(location.path, /12\/1\/23\/5\/tt\/6$/);
BaseRouteViewModel.assertAndClear('params5', ['cel23', [{ id: '5', a: '6' }, new URLSearchParams()]]);
await au.stop(true);
});
});
describe('transition plan', function () {
it('replace - inherited', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan: 'replace',
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce1/1"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2', 'round#2');
// no change
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2', 'round#3');
await au.stop(true);
});
it('replace - inherited - sibling', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeTwo.id2;
return true;
}
}
@route({
transitionPlan: 'replace',
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2', 'ce2/:id'],
component: CeTwo,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1@$1+ce2@$2"></a><a load="ce1/2@$1+ce2/1@$2"></a><au-viewport name="$1"></au-viewport> <au-viewport name="$2"></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 ce2 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2 ce2 2 2', 'round#2');
// no change
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2 ce2 2 2', 'round#3');
await au.stop(true);
});
it('invoke-lifecycles - inherited', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public loading() {
this.id2 = ++CeOne.id2;
}
}
@route({
transitionPlan: 'invoke-lifecycles',
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce1/1"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 2', 'round#2');
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 3', 'round#3');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 4', 'round#4');
await au.stop(true);
});
it('invoke-lifecycles - inherited - sibling', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public loading() {
this.id2 = ++CeOne.id2;
}
}
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public loading() {
this.id2 = ++CeTwo.id2;
}
}
@route({
transitionPlan: 'invoke-lifecycles',
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2', 'ce2/:id'],
component: CeTwo,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1@$1+ce2@$2"></a><a load="ce1/2@$1+ce2/1@$2"></a><a load="ce1/2@$2+ce2/1@$1"></a><a load="ce1/3@$2+ce2/1@$1"></a><au-viewport name="$1"></au-viewport> <au-viewport name="$2"></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 ce2 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 2 ce2 1 2', 'round#2');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(3)').click();
await queue.yield();
assert.html.textContent(host, 'ce2 2 3 ce1 2 3', 'round#3');
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 3 4 ce2 3 4', 'round#4');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 3 5 ce2 3 5', 'round#5');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(3)').click();
await queue.yield();
assert.html.textContent(host, 'ce2 4 6 ce1 4 6', 'round#6');
// no change
host.querySelector<HTMLAnchorElement>('a:nth-of-type(3)').click();
await queue.yield();
assert.html.textContent(host, 'ce2 4 6 ce1 4 6', 'round#7');
// change only one vp
host.querySelector<HTMLAnchorElement>('a:nth-of-type(4)').click();
await queue.yield();
assert.html.textContent(host, 'ce2 4 6 ce1 4 7', 'round#8');
await au.stop(true);
});
it('children can override the transitionPlan configured for parent', async function () {
@route({ path: 'c1/:id', transitionPlan: 'replace' })
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public loading() {
this.id2 = ++CeOne.id2;
}
}
@route({ path: 'c2/:id', transitionPlan: 'invoke-lifecycles' })
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public loading() {
this.id2 = ++CeTwo.id2;
}
}
@route({ path: 'p1/:id', routes: [CeTwo], transitionPlan: 'replace' })
@customElement({ name: 'p-one', template: 'p1 ${id1} ${id2} <au-viewport></au-viewport>' })
class ParentOne {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++ParentOne.id1;
private id2: number;
public loading() {
this.id2 = ++ParentOne.id2;
}
}
@route({ path: 'p2/:id', routes: [CeOne], transitionPlan: 'invoke-lifecycles' })
@customElement({ name: 'p-two', template: 'p2 ${id1} ${id2} <au-viewport></au-viewport>' })
class ParentTwo {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++ParentTwo.id1;
private id2: number;
public loading() {
this.id2 = ++ParentTwo.id2;
}
}
@route({
routes: [ParentOne, ParentTwo]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
const queue = container.get(IPlatform).domQueue;
await router.load('p1/1/c2/1');
await queue.yield();
assert.html.textContent(host, 'p1 1 1 ce2 1 1', 'round#1');
await router.load('p1/1/c2/2');
await queue.yield();
assert.html.textContent(host, 'p1 1 1 ce2 1 2', 'round#2');
await router.load('p1/2/c2/2');
await queue.yield();
assert.html.textContent(host, 'p1 2 2 ce2 2 3', 'round#3'); // as the parent is replaced, so is the child
await router.load('p2/1/c1/1');
await queue.yield();
assert.html.textContent(host, 'p2 1 1 ce1 1 1', 'round#4');
await router.load('p2/1/c1/2');
await queue.yield();
assert.html.textContent(host, 'p2 1 1 ce1 2 2', 'round#5');
await router.load('p2/2/c1/2');
await queue.yield();
assert.html.textContent(host, 'p2 1 2 ce1 2 2', 'round#6'); // as the parent is not replaced, the child is also not replaced, as there is no change in the parameters
await router.load('p2/3/c1/3');
await queue.yield();
assert.html.textContent(host, 'p2 1 3 ce1 3 3', 'round#7');
await au.stop(true);
});
it('transitionPlan function #1', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan(current: RouteNode, next: RouteNode) {
return next.component.Type === Root ? 'replace' : 'invoke-lifecycles';
},
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce1/1"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
host.querySelector('a').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 2', 'round#2');
await au.stop(true);
});
it('transitionPlan function #2 - sibling', async function () {
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeTwo.id2;
return true;
}
}
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan(current: RouteNode, next: RouteNode) {
return next.component.Type === CeTwo ? 'invoke-lifecycles' : 'replace';
},
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2', 'ce2/:id'],
component: CeTwo,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1@$1+ce2@$2"></a><a load="ce1/2@$1+ce2/1@$2"></a><au-viewport name="$1"></au-viewport> <au-viewport name="$2"></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
host.querySelector('a').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 ce2 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 2 2 ce2 1 2', 'round#2');
await au.stop(true);
});
it('transitionPlan function #3 - parent-child - parent:replace,child:invoke-lifecycles', async function () {
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeTwo.id2;
return true;
}
}
@route({
routes: [
{
id: 'ce2',
path: ['', 'ce2'],
component: CeTwo,
},
]
})
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2} <au-viewport></au-viewport>' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan(current: RouteNode, next: RouteNode) {
return next.component.Type === CeTwo ? 'invoke-lifecycles' : 'replace';
},
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
}
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce1/1"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
host.querySelector('a').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 ce2 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 2 2 ce2 2 2', 'round#2'); // this happens as the ce-one (parent) is replaced causing replacement of child
await au.stop(true);
});
it('transitionPlan function #3 - parent-child - parent:invoke-lifecycles,child:replace', async function () {
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeTwo.id2;
return true;
}
}
@route({
routes: [
{
id: 'ce2',
path: ['', 'ce2'],
component: CeTwo,
},
]
})
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2} <au-viewport></au-viewport>' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan(current: RouteNode, next: RouteNode) {
return next.component.Type === CeOne ? 'invoke-lifecycles' : 'replace';
},
routes: [
{
id: 'ce1',
path: ['ce1', 'ce1/:id'],
component: CeOne,
}
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce1/1"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
host.querySelector('a').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 ce2 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(host, 'ce1 1 2 ce2 1 1', 'round#2'); // note that as the parent is not replaced, the child is retained.
await au.stop(true);
});
it('transitionPlan can be overridden per instruction basis', async function () {
@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2} ${id}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
private id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
this.id2 = ++CeTwo.id2;
return true;
}
}
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2} ${id}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
private id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan: 'replace',
routes: [
{
id: 'ce1',
path: ['ce1/:id'],
component: CeOne,
transitionPlan: 'invoke-lifecycles',
},
{
id: 'ce2',
path: ['ce2/:id'],
component: CeTwo,
transitionPlan: 'replace',
},
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
await router.load('ce1/42');
await queue.yield();
assert.html.textContent(host, 'ce1 1 1 42', 'round#1');
await router.load('ce1/43');
await queue.yield();
assert.html.textContent(host, 'ce1 1 2 43', 'round#2');
await router.load('ce1/44', { transitionPlan: 'replace' });
await queue.yield();
assert.html.textContent(host, 'ce1 2 3 44', 'round#3');
await router.load('ce2/42');
await queue.yield();
assert.html.textContent(host, 'ce2 1 1 42', 'round#4');
await router.load('ce2/43');
await queue.yield();
assert.html.textContent(host, 'ce2 2 2 43', 'round#5');
await router.load('ce2/44', { transitionPlan: 'invoke-lifecycles' });
await queue.yield();
assert.html.textContent(host, 'ce2 2 3 44', 'round#6');
await au.stop(true);
});
it('replace - different paths for same component', async function () {
@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}
@route({
transitionPlan: 'replace',
routes: [
{
id: 'ce1',
path: ['ce1', 'ce2'],
component: CeOne,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce2"></a><au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne] });
const queue = container.get(IPlatform).domQueue;
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'ce1 1 1', 'round#1');
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2', 'round#2');
// no change
host.querySelector<HTMLAnchorElement>('a:nth-of-type(2)').click();
await queue.yield();
assert.html.textContent(host, 'ce1 2 2', 'round#3');
await au.stop(true);
});
});
describe('history strategy', function () {
class TestData {
public constructor(
public readonly strategy: HistoryStrategy,
public readonly expectations: string[]
) { }
}
function* getTestData(): Generator<TestData> {
yield new TestData('push', [
'#1 - len: 1 - state: {"au-nav-id":1}',
'#2 - len: 2 - state: {"au-nav-id":2}',
'#3 - len: 3 - state: {"au-nav-id":3}',
'#4 - len: 4 - state: {"au-nav-id":4}',
]);
yield new TestData('replace', [
'#1 - len: 1 - state: {"au-nav-id":1}',
'#2 - len: 1 - state: {"au-nav-id":2}',
'#3 - len: 1 - state: {"au-nav-id":3}',
'#4 - len: 1 - state: {"au-nav-id":4}',
]);
yield new TestData('none', [
'#1 - len: 1 - state: {"au-nav-id":1}', // initial state replace
'#2 - len: 1 - state: {"au-nav-id":1}',
'#3 - len: 1 - state: {"au-nav-id":1}',
'#4 - len: 1 - state: {"au-nav-id":1}',
]);
}
for (const data of getTestData()) {
it(data.strategy, async function () {
@customElement({ name: 'ce-two', template: 'ce2' })
class CeTwo implements IRouteViewModel { }
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2'],
component: CeTwo,
},
]
})
@customElement({ name: 'ro-ot', template: '<a load="ce1"></a><a load="ce2"></a><span id="history">${history}</span><au-viewport></au-viewport>' })
class Root {
private history: string;
public constructor() {
let i = 0;
const history = resolve(IHistory);
resolve(IRouterEvents).subscribe('au:router:navigation-end', () => {
this.history = `#${++i} - len: ${history.length} - state: ${JSON.stringify(history.state)}`;
});
}
}
const { au, container, host } = await start({ appRoot: Root, registrations: [CeOne, CeTwo], historyStrategy: data.strategy });
const queue = container.get(IPlatform).domQueue;
const router = container.get<Router>(IRouter);
const expectations = data.expectations;
const len = expectations.length;
await queue.yield();
const history = host.querySelector<HTMLSpanElement>('#history');
assert.html.textContent(history, expectations[0], 'start');
const anchors = Array.from(host.querySelectorAll('a'));
for (let i = 1; i < len; i++) {
anchors[i % 2].click();
await router.currentTr.promise;
await queue.yield();
assert.html.textContent(history, expectations[i], `round#${i}`);
}
await au.stop(true);
});
}
(isNode() ? it.skip : it)('explicit history strategy can be used for individual navigation - configured: push', async function () {
@customElement({ name: 'ce-three', template: 'ce3' })
class CeThree implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2' })
class CeTwo implements IRouteViewModel { }
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2'],
component: CeTwo,
},
{
id: 'ce3',
path: ['ce3'],
component: CeThree,
},
]
})
@customElement({ name: 'ro-ot', template: '<span id="history">${history}</span><au-viewport></au-viewport>' })
class Root {
private history: string;
public constructor() {
let i = 0;
const history = resolve(IHistory);
resolve(IRouterEvents).subscribe('au:router:navigation-end', () => {
this.history = `#${++i} - len: ${history.length} - state: ${JSON.stringify(history.state)}`;
});
}
}
const { au, container, host } = await start({ appRoot: Root, historyStrategy: 'push', registrations: [getLocationChangeHandlerRegistration()] });
const platform = container.get(IPlatform);
const dwQueue = platform.domQueue;
await dwQueue.yield();
const historyEl = host.querySelector<HTMLSpanElement>('#history');
const vp = host.querySelector<HTMLSpanElement>('au-viewport');
const router = container.get<Router>(IRouter);
assert.html.textContent(vp, 'ce1', 'start - component');
assert.html.textContent(historyEl, '#1 - len: 1 - state: {"au-nav-id":1}', 'start - history');
await router.load('ce2');
await dwQueue.yield();
assert.html.textContent(vp, 'ce2', 'round#2 - component');
assert.html.textContent(historyEl, '#2 - len: 2 - state: {"au-nav-id":2}', 'round#2 - history');
await router.load('ce3', { historyStrategy: 'replace' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce3', 'round#3 - component');
assert.html.textContent(historyEl, '#3 - len: 2 - state: {"au-nav-id":3}', 'round#3 - history');
// going back should load the ce1
const history = container.get(IHistory);
const tQueue = platform.taskQueue;
history.back();
await tQueue.yield();
assert.html.textContent(vp, 'ce1', 'back - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#4 - len: 2 - state: {"au-nav-id":4}', 'back - history');
// going forward should load ce3
history.forward();
await tQueue.yield();
assert.html.textContent(vp, 'ce3', 'forward - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#5 - len: 2 - state: {"au-nav-id":5}', 'forward - history');
// strategy: none
await router.load('ce1', { historyStrategy: 'none' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce1', 'strategy: none - component');
assert.html.textContent(historyEl, '#6 - len: 2 - state: {"au-nav-id":5}', 'strategy: none - history');
await au.stop(true);
});
(isNode() ? it.skip : it)('explicit history strategy can be used for individual navigation - configured: replace', async function () {
@customElement({ name: 'ce-three', template: 'ce3' })
class CeThree implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2' })
class CeTwo implements IRouteViewModel { }
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2'],
component: CeTwo,
},
{
id: 'ce3',
path: ['ce3'],
component: CeThree,
},
]
})
@customElement({ name: 'ro-ot', template: '<span id="history">${history}</span><au-viewport></au-viewport>' })
class Root {
private history: string;
public constructor() {
let i = 0;
const history = resolve(IHistory);
resolve(IRouterEvents).subscribe('au:router:navigation-end', () => {
this.history = `#${++i} - len: ${history.length} - state: ${JSON.stringify(history.state)}`;
});
}
}
const { au, container, host } = await start({ appRoot: Root, historyStrategy: 'replace', registrations: [getLocationChangeHandlerRegistration()] });
const platform = container.get(IPlatform);
const dwQueue = platform.domQueue;
await dwQueue.yield();
const historyEl = host.querySelector<HTMLSpanElement>('#history');
const vp = host.querySelector<HTMLSpanElement>('au-viewport');
const router = container.get<Router>(IRouter);
assert.html.textContent(vp, 'ce1', 'start - component');
assert.html.textContent(historyEl, '#1 - len: 1 - state: {"au-nav-id":1}', 'start - history');
await router.load('ce2');
await dwQueue.yield();
assert.html.textContent(vp, 'ce2', 'round#2 - component');
assert.html.textContent(historyEl, '#2 - len: 1 - state: {"au-nav-id":2}', 'round#2 - history');
await router.load('ce3', { historyStrategy: 'push' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce3', 'round#3 - component');
assert.html.textContent(historyEl, '#3 - len: 2 - state: {"au-nav-id":3}', 'round#3 - history');
// going back should load the ce2
const history = container.get(IHistory);
const tQueue = platform.taskQueue;
history.back();
await tQueue.yield();
assert.html.textContent(vp, 'ce2', 'back - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#4 - len: 2 - state: {"au-nav-id":4}', 'back - history');
// going forward should load ce3
history.forward();
await tQueue.yield();
assert.html.textContent(vp, 'ce3', 'forward - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#5 - len: 2 - state: {"au-nav-id":5}', 'forward - history');
// strategy: none
await router.load('ce1', { historyStrategy: 'none' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce1', 'strategy: none - component');
assert.html.textContent(historyEl, '#6 - len: 2 - state: {"au-nav-id":5}', 'strategy: none - history');
await au.stop(true);
});
(isNode() ? it.skip : it)('explicit history strategy can be used for individual navigation - configured: none', async function () {
@customElement({ name: 'ce-three', template: 'ce3' })
class CeThree implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2' })
class CeTwo implements IRouteViewModel { }
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2'],
component: CeTwo,
},
{
id: 'ce3',
path: ['ce3'],
component: CeThree,
},
]
})
@customElement({ name: 'ro-ot', template: '<span id="history">${history}</span><au-viewport></au-viewport>' })
class Root {
private history: string;
public constructor() {
let i = 0;
const history = resolve(IHistory);
resolve(IRouterEvents).subscribe('au:router:navigation-end', () => {
this.history = `#${++i} - len: ${history.length} - state: ${JSON.stringify(history.state)}`;
});
}
}
const { au, container, host } = await start({ appRoot: Root, historyStrategy: 'none', registrations: [getLocationChangeHandlerRegistration()] });
const platform = container.get(IPlatform);
const dwQueue = platform.domQueue;
await dwQueue.yield();
const historyEl = host.querySelector<HTMLSpanElement>('#history');
const vp = host.querySelector<HTMLSpanElement>('au-viewport');
const router = container.get<Router>(IRouter);
assert.html.textContent(vp, 'ce1', 'start - component');
assert.html.textContent(historyEl, '#1 - len: 1 - state: {"au-nav-id":1}', 'start - history');
await router.load('ce2');
await dwQueue.yield();
assert.html.textContent(vp, 'ce2', 'round#2 - component');
assert.html.textContent(historyEl, '#2 - len: 1 - state: {"au-nav-id":1}', 'round#2 - history');
await router.load('ce3', { historyStrategy: 'push' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce3', 'round#3 - component');
assert.html.textContent(historyEl, '#3 - len: 2 - state: {"au-nav-id":3}', 'round#3 - history');
// going back should load the ce1
const history = container.get(IHistory);
const tQueue = platform.taskQueue;
history.back();
await tQueue.yield();
assert.html.textContent(vp, 'ce1', 'back - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#4 - len: 2 - state: {"au-nav-id":4}', 'back - history');
// going forward should load ce3
history.forward();
await tQueue.yield();
assert.html.textContent(vp, 'ce3', 'forward - component');
await dwQueue.yield();
assert.html.textContent(historyEl, '#5 - len: 2 - state: {"au-nav-id":5}', 'forward - history');
await router.load('ce2', { historyStrategy: 'replace' });
await dwQueue.yield();
assert.html.textContent(vp, 'ce2', 'round#4 - component');
assert.html.textContent(historyEl, '#6 - len: 2 - state: {"au-nav-id":6}', 'round#4 - history');
await au.stop(true);
});
});
it('navigate repeatedly to parent route from child route works - GH 1701', async function () {
@customElement({ name: 'c-1', template: 'c1 <a load="../c2"></a>' })
class C1 { }
@customElement({ name: 'c-2', template: 'c2 <a load="route: p; context.bind: null"></a>' })
class C2 { }
@route({
routes: [
{ path: '', component: C1 },
{ path: 'c2', component: C2 },
]
})
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: '', redirectTo: 'p' },
{ path: 'p', component: P1 },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const queue = container.get(IPlatform).taskQueue;
assert.html.textContent(host, 'c1', 'initial');
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'c2', 'round#1 of loading c2');
host.querySelector('a').click(); // <- go to parent #1
await queue.yield();
// round#2
assert.html.textContent(host, 'c1', 'navigate to parent from c2 #1');
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'c2', 'round#2 of loading c2');
host.querySelector('a').click(); // <- go to parent #2
await queue.yield();
// round#3
assert.html.textContent(host, 'c1', 'navigate to parent from c2 #2');
host.querySelector('a').click();
await queue.yield();
assert.html.textContent(host, 'c2', 'round#3 of loading c2');
host.querySelector('a').click(); // <- go to parent #3
await queue.yield();
assert.html.textContent(host, 'c1', 'navigate to parent from c2 #3');
await au.stop(true);
});
describe('multiple configurations for same component', function () {
it('multiple configurations for the same component under the same parent', async function () {
@customElement({ name: 'c-1', template: 'c1 ${id}' })
class C1 implements IRouteViewModel {
private static id: number = 0;
private readonly id: number = ++C1.id;
public data: Record<string, unknown>;
public loading(_params: Params, next: RouteNode, _current: RouteNode): void | Promise<void> {
this.data = next.data;
}
}
@route({
routes: [
{ path: '', component: C1, title: 't1', data: { foo: 'bar' } },
{ path: 'c1/:id', component: C1, title: 't2', data: { awesome: 'possum' } },
],
transitionPlan: 'replace'
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const doc = container.get(IPlatform).document;
const router = container.get(IRouter);
assert.html.textContent(host, 'c1 1');
assert.strictEqual(doc.title, 't1');
let ce = CustomElement.for<C1>(host.querySelector('c-1')).viewModel;
assert.deepStrictEqual(ce.data, { foo: 'bar' });
await router.load('c1/1');
assert.html.textContent(host, 'c1 2');
assert.strictEqual(doc.title, 't2');
ce = CustomElement.for<C1>(host.querySelector('c-1')).viewModel;
assert.deepStrictEqual(ce.data, { awesome: 'possum' });
await au.stop(true);
});
it('same component is added under different parents', async function () {
@customElement({ name: 'c-1', template: 'c1' })
class C1 implements IRouteViewModel {
public data: Record<string, unknown>;
public loading(_params: Params, next: RouteNode, _current: RouteNode): void | Promise<void> {
this.data = next.data;
}
}
@route({
routes: [
{ path: '', component: C1, title: 'p1c1', data: { foo: 'bar' } }
]
})
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: '', component: C1, title: 'p2c1', data: { awesome: 'possum' } }
]
})
@customElement({ name: 'p-2', template: '<au-viewport></au-viewport>' })
class P2 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1 },
{ path: 'p2', component: P2 },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const doc = container.get(IPlatform).document;
const router = container.get(IRouter);
assert.html.textContent(host, 'c1');
assert.strictEqual(doc.title, 'p1c1');
let ce = CustomElement.for<C1>(host.querySelector('c-1')).viewModel;
assert.deepStrictEqual(ce.data, { foo: 'bar' });
await router.load('p2');
assert.html.textContent(host, 'c1');
assert.strictEqual(doc.title, 'p2c1');
ce = CustomElement.for<C1>(host.querySelector('c-1')).viewModel;
assert.deepStrictEqual(ce.data, { awesome: 'possum' });
await au.stop(true);
});
for (const config of ['c1', { path: 'c1' }]) {
it(`component defines its own path - with redirect - config: ${JSON.stringify(config)}`, async function () {
@route(config as IRouteConfig)
@customElement({ name: 'c-1', template: '${parent}/c1' })
class C1 {
private readonly parent: string;
public constructor() {
this.parent = resolve(IRouteContext).parent.component.name;
}
}
@route({
routes: [
{ path: '', redirectTo: 'c1' },
C1,
]
})
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ path: '', redirectTo: 'c1' },
C1,
]
})
@customElement({ name: 'p-2', template: '<au-viewport></au-viewport>' })
class P2 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1 },
{ path: 'p2', component: P2 },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, 'p-1/c1');
await router.load('p2');
assert.html.textContent(host, 'p-2/c1');
await au.stop(true);
});
it(`component defines its own path - without redirect - config: ${JSON.stringify(config)}`, async function () {
@route(config as IRouteConfig)
@customElement({ name: 'c-1', template: '${parent}/c1' })
class C1 {
private readonly parent: string;
public constructor() {
this.parent = resolve(IRouteContext).parent.component.name;
}
}
@route({
routes: [
C1,
]
})
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
C1,
]
})
@customElement({ name: 'p-2', template: '<au-viewport></au-viewport>' })
class P2 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1 },
{ path: 'p2', component: P2 },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
await router.load('p1/c1');
assert.html.textContent(host, 'p-1/c1');
await router.load('p2/c1');
assert.html.textContent(host, 'p-2/c1');
await au.stop(true);
});
}
it(`component defines transition plan - parent overloads`, async function () {
@route({ path: 'c1/:id', transitionPlan: 'replace' })
@customElement({ name: 'c-1', template: '${parent}/c1 - ${routeId} - ${instanceId} - ${activationId}' })
class C1 {
private static instanceId: number = 0;
private static activationId: number = 0;
private readonly instanceId: number = ++C1.instanceId;
private activationId: number = 0;
private readonly parent: string;
private routeId: string;
public constructor() {
this.parent = resolve(IRouteContext).parent.component.name;
}
public canLoad(params: Params): boolean {
this.activationId = ++C1.activationId;
this.routeId = params.id;
return true;
}
}
@route({
routes: [
C1,
]
})
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 { }
@route({
routes: [
{ component: C1, transitionPlan: 'invoke-lifecycles' },
]
})
@customElement({ name: 'p-2', template: '<au-viewport></au-viewport>' })
class P2 { }
@route({
routes: [
{ path: ['', 'p1'], component: P1 },
{ path: 'p2', component: P2 },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
await router.load('p1/c1/42');
assert.html.textContent(host, 'p-1/c1 - 42 - 1 - 1');
await router.load('p1/c1/24');
assert.html.textContent(host, 'p-1/c1 - 24 - 2 - 2');
await router.load('p2/c1/42');
assert.html.textContent(host, 'p-2/c1 - 42 - 3 - 3');
await router.load('p2/c1/24');
await container.get(IPlatform).domQueue.yield();
assert.html.textContent(host, 'p-2/c1 - 24 - 3 - 4');
await au.stop(true);
});
it('distributed configuration', async function () {
@route('c1')
@customElement({ name: 'c-1', template: '${parent}/c1' })
class C1 implements IRouteViewModel {
private readonly parent: string;
public constructor() {
this.parent = resolve(IRouteContext).parent.component.name;
}
}
@route('p1')
@customElement({ name: 'p-1', template: '<au-viewport></au-viewport>' })
class P1 implements IRouteViewModel {
public getRouteConfig(_parentConfig: IRouteConfig, _routeNode: RouteNode): IRouteConfig | Promise<IRouteConfig> {
return {
routes: [C1]
};
}
}
@route('p2')
@customElement({ name: 'p-2', template: '<au-viewport></au-viewport>' })
class P2 implements IRouteViewModel {
public getRouteConfig(_parentConfig: IRouteConfig, _routeNode: RouteNode): IRouteConfig | Promise<IRouteConfig> {
return {
routes: [C1]
};
}
}
@route({
routes: [
P1,
P2,
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const router = container.get(IRouter);
await router.load('p1/c1');
assert.html.textContent(host, 'p-1/c1');
await router.load('p2/c1');
assert.html.textContent(host, 'p-2/c1');
await au.stop(true);
});
});
describe('custom element aliases as routing instruction', function () {
it('using the aliases as path works', async function () {
@customElement({ name: 'c-1', template: 'c1', aliases: ['c-a', 'c-one'] })
class C1 { }
@customElement({ name: 'c-2', template: 'c2', aliases: ['c-b', 'c-two'] })
class C2 { }
@route({
routes: [C1, C2]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, '');
await router.load('c-a');
assert.html.textContent(host, 'c1');
await router.load('c-b');
assert.html.textContent(host, 'c2');
await router.load('c-1');
assert.html.textContent(host, 'c1');
await router.load('c-2');
assert.html.textContent(host, 'c2');
await router.load('c-one');
assert.html.textContent(host, 'c1');
await router.load('c-two');
assert.html.textContent(host, 'c2');
await au.stop();
});
it('order of route decorator and the customElement decorator does not matter', async function () {
@route({ title: 'c1' })
@customElement({ name: 'c-1', template: 'c1', aliases: ['c-a', 'c-one'] })
class C1 { }
@customElement({ name: 'c-2', template: 'c2', aliases: ['c-b', 'c-two'] })
@route({ title: 'c2' })
class C2 { }
@route({
routes: [C1, C2]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
const doc = container.get(IPlatform).document;
assert.html.textContent(host, '');
await router.load('c-a');
assert.html.textContent(host, 'c1');
assert.strictEqual(doc.title, 'c1');
await router.load('c-b');
assert.html.textContent(host, 'c2');
assert.strictEqual(doc.title, 'c2');
await router.load('c-1');
assert.html.textContent(host, 'c1');
assert.strictEqual(doc.title, 'c1');
await router.load('c-2');
assert.html.textContent(host, 'c2');
assert.strictEqual(doc.title, 'c2');
await router.load('c-one');
assert.html.textContent(host, 'c1');
assert.strictEqual(doc.title, 'c1');
await router.load('c-two');
assert.html.textContent(host, 'c2');
assert.strictEqual(doc.title, 'c2');
await au.stop();
});
it('explicitly defined paths always override CE name or aliases', async function () {
@route('c1')
@customElement({ name: 'c-1', template: 'c1', aliases: ['c-a'] })
class C1 { }
@customElement({ name: 'c-2', template: 'c2', aliases: ['c-b'] })
class C2 { }
@route({
routes: [C1, { path: 'c2', component: C2 }]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, '');
await router.load('c1');
assert.html.textContent(host, 'c1');
await router.load('c2');
assert.html.textContent(host, 'c2');
try {
await router.load('c-1');
assert.fail('expected error 1');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+c-1/);
}
try {
await router.load('c-a');
assert.fail('expected error 2');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+c-a/);
}
try {
await router.load('c-2');
assert.fail('expected error 3');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+c-2/);
}
try {
await router.load('c-b');
assert.fail('expected error 4');
} catch (er) {
assert.match((er as Error).message, /AUR3401.+c-b/);
}
await au.stop();
});
});
it('local dependencies of the routed view model works', async function () {
@customElement({ name: 'c-11', template: 'c11' })
class C11 { }
@customElement({ name: 'c-1', template: 'c1 <c-11></c-11>', dependencies: [C11] })
class C1 { }
@customElement({ name: 'c-21', template: 'c21' })
class C21 { }
@customElement({ name: 'c-2', template: 'c2 <c-21></c-21>', dependencies: [C21] })
class C2 { }
@route({
routes: [
{ path: 'c1', component: C1 },
{ path: 'c2', component: C2 },
]
})
@customElement({ name: 'root', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, '');
await router.load('c1');
assert.html.textContent(host, 'c1 c11');
await router.load('c2');
assert.html.textContent(host, 'c2 c21');
await au.stop(true);
assert.areTaskQueuesEmpty();
});
// use-case: master page
it('custom element containing au-viewport works', async function () {
@customElement({ name: 'master-page', template: 'mp <au-viewport></au-viewport>' })
class MasterPage { }
@customElement({ name: 'c-1', template: 'c1' })
class C1 { }
@customElement({ name: 'c-2', template: 'c2' })
class C2 { }
@route({
routes: [
{ path: 'c1', component: C1 },
{ path: 'c2', component: C2 },
]
})
@customElement({ name: 'root', template: '<master-page></master-page>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root, registrations: [MasterPage] });
const router = container.get(IRouter);
assert.html.textContent(host, 'mp');
await router.load('c1');
assert.html.textContent(host, 'mp c1');
await router.load('c2');
assert.html.textContent(host, 'mp c2');
await au.stop(true);
assert.areTaskQueuesEmpty();
});
it('Alias registrations work', async function () {
@route('')
@inject(Router, RouterOptions, RouteContext)
@customElement({ name: 'c-1', template: 'c1' })
class C1 {
public readonly ictx: IRouteContext = resolve(IRouteContext);
public constructor(
public readonly router: Router,
public readonly routerOptions: RouterOptions,
public readonly ctx: RouteContext,
) { }
}
@route({ routes: [C1] })
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root {
public readonly router: IRouter = resolve(IRouter);
public readonly routerOptions: IRouterOptions = resolve(IRouterOptions);
}
const { au, host, container, rootVm } = await start({ appRoot: Root });
assert.html.textContent(host, 'c1');
const c1vm = CustomElement.for<C1>(host.querySelector('c-1')).viewModel;
const router = container.get(IRouter);
assert.strictEqual(Object.is(router, rootVm.router), true, 'router != root Router');
assert.strictEqual(Object.is(router, c1vm.router), true, 'router != c1 router');
const routerOptions = container.get(IRouterOptions);
assert.strictEqual(Object.is(routerOptions, rootVm.routerOptions), true, 'options != root options');
assert.strictEqual(Object.is(routerOptions, c1vm.routerOptions), true, 'options != c1 options');
assert.strictEqual(Object.is(c1vm.ctx, c1vm.ictx), true, 'RouteCtx != IRouteCtx');
await au.stop(true);
});
it('supports routing instruction with parenthesized parameters', async function () {
@route('c1/:id1/:id2?')
@customElement({ name: 'c-1', template: 'c1 ${id1} ${id2}' })
class C1 implements IRouteViewModel {
private id1: string;
private id2: string;
public loading(params: Params, _next: RouteNode, _current: RouteNode): void | Promise<void> {
this.id1 = params.id1;
this.id2 = params.id2;
}
}
@route({ routes: [{ id: 'c1', component: C1 }] })
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(Router);
assert.html.textContent(host, '', 'init');
await router.load('c1(id1=1)');
assert.html.textContent(host, 'c1 1', 'round#1');
await router.load('c1(id1=2,id2=3)');
assert.html.textContent(host, 'c1 2 3', 'round#2');
await au.stop(true);
});
it('self-referencing routing configuration', async function () {
@route('')
@customElement({ name: 'c-1', template: 'c1' })
class C1 { }
@route('c2')
@customElement({ name: 'c-2', template: 'c2' })
class C2 { }
@customElement({ name: 'p-1', template: 'p1 <au-viewport></au-viewport>', aliases: ['p1'] })
class P1 implements IRouteViewModel {
public getRouteConfig(_parentConfig: IRouteConfig, _routeNode: RouteNode): IRouteConfig {
return {
routes: [
C1,
C2,
P1,
]
};
}
}
@route({ routes: [{ path: ['', 'p1'], component: P1 }] })
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { au, container, host } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, 'p1 c1', 'init');
await router.load('p1/p1');
assert.html.textContent(host, 'p1 p1 c1', 'round#1');
await router.load('p1/p1/p1/c2');
assert.html.textContent(host, 'p1 p1 p1 c2', 'round#2');
await au.stop(true);
});
it('handles slash in router parameter value', async function () {
@customElement({ name: 'c-1', template: 'c1 ${id}' })
class CeOne {
private id: string;
public loading(params: Params, _next: RouteNode, _current: RouteNode): void | Promise<void> {
this.id = params.id;
}
}
@route({
routes: [
{ id: 'c1', path: 'c1/:id', component: CeOne },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { container, host, au } = await start({ appRoot: Root });
const router = container.get(IRouter);
const location = container.get(ILocation) as unknown as MockBrowserHistoryLocation;
assert.html.textContent(host, '');
await router.load('c1/abc%2Fdef');
assert.html.textContent(host, 'c1 abc/def');
assert.match(location.path, /c1\/abc%2Fdef$/);
await router.load({ component: 'c1', params: { id: '123/456' } });
assert.html.textContent(host, 'c1 123/456');
assert.match(location.path, /c1\/123%2F456$/);
await au.stop(true);
});
it('handles slash in router parameter name', async function () {
@customElement({ name: 'c-1', template: 'c1 ${id}' })
class CeOne {
private id: string;
public loading(params: Params, _next: RouteNode, _current: RouteNode): void | Promise<void> {
this.id = params['foo%2Fbar'];
}
}
@route({
routes: [
{ id: 'c1', path: 'c1/:foo%2Fbar', component: CeOne },
]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
const { container, host, au } = await start({ appRoot: Root });
const router = container.get(IRouter);
assert.html.textContent(host, '');
await router.load('c1(foo%2Fbar=fizzbuzz)');
assert.html.textContent(host, 'c1 fizzbuzz');
await router.load({ component: 'c1', params: { 'foo%2Fbar': 'awesome possum' } });
assert.html.textContent(host, 'c1 awesome possum');
await au.stop(true);
});
it('Router#load respects constrained routes', async function () {
@route('nf')
@customElement({ name: 'not-found', template: `nf` })
class NotFound { }
@route({ id: 'product', path: 'product/:id{{^\\d+$}}' })
@customElement({ name: 'pro-duct', template: `product \${id}` })
class Product {
public id: unknown;
public canLoad(params: Params, _next: RouteNode, _current: RouteNode): boolean {
this.id = params.id;
return true;
}
}
@route({ routes: [Product, NotFound], fallback: 'nf' })
@customElement({
name: 'ro-ot',
template: `<au-viewport></au-viewport>`
})
class Root { }
const { au, host, container } = await start({ appRoot: Root });
const queue = container.get(IPlatform).domQueue;
await queue.yield();
const router = container.get(IRouter);
await router.load('product/42');
await queue.yield();
assert.html.textContent(host, 'product 42', 'round#1');
await router.load('product/foo');
await queue.yield();
assert.html.textContent(host, 'nf', 'round#2');
await router.load({ component: 'product', params: { id: '42' } });
await queue.yield();
assert.html.textContent(host, 'product 42', 'round#3');
await router.load({ component: 'product', params: { id: 'foo' } });
await queue.yield();
assert.html.textContent(host, 'nf', 'round#4');
await au.stop(true);
});
});