packages/__tests__/src/router-lite/config-tests.spec.ts
import { Aurelia, CustomElement, customElement, ICustomElementController } from '@aurelia/runtime-html';
import { IRouteConfig, IRouter, IRouteViewModel, route, Route, RouteConfig, RouteNode, RouterConfiguration } from '@aurelia/router-lite';
import { assert, TestContext } from '@aurelia/testing';
import { IHookInvocationAggregator, IHIAConfig, HookName } from './_shared/hook-invocation-tracker.js';
import { HookSpecs, TestRouteViewModelBase } from './_shared/view-models.js';
import { hookSpecsMap, verifyInvocationsEqual } from './_shared/hook-spec.js';
import { createFixture, IActivityTracker } from './_shared/create-fixture.js';
import { LogLevel, resolve } from '@aurelia/kernel';
import { TestRouterConfiguration } from './_shared/configuration.js';
function vp(count: number): string {
if (count === 1) {
return `<au-viewport></au-viewport>`;
}
let template = '';
for (let i = 0; i < count; ++i) {
template = `${template}<au-viewport name="$${i}"></au-viewport>`;
}
return template;
}
function getDefaultHIAConfig(): IHIAConfig {
return {
resolveTimeoutMs: 100,
resolveLabels: [],
};
}
export function* prepend(
prefix: string,
component: string,
...calls: (HookName | '')[]
): Generator<string, void> {
for (const call of calls) {
if (call === '') {
yield '';
} else {
yield `${prefix}.${component}.${call}`;
}
}
}
export function* interleave(
...generators: Generator<string, void>[]
): Generator<string, void> {
while (generators.length > 0) {
for (let i = 0, ii = generators.length; i < ii; ++i) {
const gen = generators[i];
const next = gen.next();
if (next.done) {
generators.splice(i, 1);
--i;
--ii;
} else {
const value = next.value as string;
if (value) {
yield value;
}
}
}
}
}
export interface IComponentSpec {
kind: 'all-sync' | 'all-async';
hookSpecs: HookSpecs;
}
export abstract class SimpleActivityTrackingVMBase {
public readonly $controller!: ICustomElementController;
public readonly tracker: IActivityTracker = resolve(IActivityTracker);
public attached(): void {
this.tracker.setActive(this.$controller.definition.name);
}
public setNonActive(): void {
this.tracker.setActive(this.$controller.definition.name);
}
}
describe('router-lite/config-tests.spec.ts', function () {
describe('monomorphic timings', function () {
const componentSpecs: IComponentSpec[] = [
{
kind: 'all-sync',
hookSpecs: HookSpecs.create({
binding: hookSpecsMap.binding.sync,
bound: hookSpecsMap.bound.sync,
attaching: hookSpecsMap.attaching.sync,
attached: hookSpecsMap.attached.sync,
detaching: hookSpecsMap.detaching.sync,
unbinding: hookSpecsMap.unbinding.sync,
canLoad: hookSpecsMap.canLoad.sync,
loading: hookSpecsMap.loading.sync,
canUnload: hookSpecsMap.canUnload.sync,
unloading: hookSpecsMap.unloading.sync,
}),
},
{
kind: 'all-async',
hookSpecs: getAllAsyncSpecs(1),
},
];
for (const componentSpec of componentSpecs) {
const { kind, hookSpecs } = componentSpec;
@customElement({ name: 'a01', template: null })
class A01 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a02', template: null })
class A02 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a03', template: null })
class A03 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a04', template: null })
class A04 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
const A0 = [A01, A02, A03, A04];
@customElement({ name: 'root1', template: vp(1) })
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class Root1 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a11', template: vp(1) })
class A11 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a12', template: vp(1) })
class A12 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a13', template: vp(1) })
class A13 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a14', template: vp(1) })
class A14 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
const A1 = [A11, A12, A13, A14];
@customElement({ name: 'root2', template: vp(2) })
class Root2 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a21', template: vp(2) })
class A21 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
@customElement({ name: 'a22', template: vp(2) })
class A22 extends TestRouteViewModelBase {
public constructor() { super(resolve(IHookInvocationAggregator), hookSpecs); }
}
const A2 = [A21, A22];
const A = [...A0, ...A1, ...A2];
describe(`componentSpec.kind:'${kind}'`, function () {
describe('single', function () {
interface ISpec {
t1: [string, string];
t2: [string, string];
t3: [string, string];
t4: [string, string];
configure(): void;
}
function runTest(spec: ISpec) {
const { t1: [t1, t1c], t2: [t2, t2c], t3: [t3, t3c], t4: [t4, t4c] } = spec;
spec.configure();
it(`'${t1}' -> '${t2}' -> '${t3}' -> '${t4}'`, async function () {
const { router, hia, tearDown } = await createFixture(Root2, A, getDefaultHIAConfig);
const phase1 = `('' -> '${t1}')#1`;
const phase2 = `('${t1}' -> '${t2}')#2`;
const phase3 = `('${t2}' -> '${t3}')#3`;
const phase4 = `('${t3}' -> '${t4}')#4`;
hia.setPhase(phase1);
await router.load(t1);
hia.setPhase(phase2);
await router.load(t2);
hia.setPhase(phase3);
await router.load(t3);
hia.setPhase(phase4);
await router.load(t4);
await tearDown();
const expected = [...(function* () {
yield `start.root2.binding`;
yield `start.root2.bound`;
yield `start.root2.attaching`;
yield `start.root2.attached`;
yield* prepend(phase1, t1c, 'canLoad', 'loading', 'binding', 'bound', 'attaching', 'attached');
for (const [phase, { $t1c, $t2c }] of [
[phase2, { $t1c: t1c, $t2c: t2c }],
[phase3, { $t1c: t2c, $t2c: t3c }],
[phase4, { $t1c: t3c, $t2c: t4c }],
] as const) {
yield `${phase}.${$t1c}.canUnload`;
yield `${phase}.${$t2c}.canLoad`;
yield `${phase}.${$t1c}.unloading`;
yield `${phase}.${$t2c}.loading`;
yield* prepend(phase, $t1c, 'detaching', 'unbinding', 'dispose');
yield* prepend(phase, $t2c, 'binding', 'bound', 'attaching', 'attached');
}
yield `stop.${t4c}.detaching`;
yield `stop.root2.detaching`;
yield `stop.${t4c}.unbinding`;
yield `stop.root2.unbinding`;
yield `stop.root2.dispose`;
yield `stop.${t4c}.dispose`;
})()];
verifyInvocationsEqual(hia.notifyHistory, expected);
hia.dispose();
});
}
const specs: ISpec[] = [
{
t1: ['1', 'a01'],
t2: ['2', 'a02'],
t3: ['1', 'a01'],
t4: ['2', 'a02'],
configure() {
Route.configure(
{
routes: [
{
path: '1',
component: A01,
},
{
path: '2',
component: A02,
},
],
},
Root2,
);
},
},
];
for (const spec of specs) {
runTest(spec);
}
});
});
}
});
for (const inDependencies of [true, false]) {
describe(`inDependencies: ${inDependencies}`, function () {
it(`can load a configured child route with direct path and explicit component`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a', component: A01 }] })
@customElement({ name: 'root', template: vp(1), dependencies: inDependencies ? [A01] : [] })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig);
await router.load('a');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
it(`can load a configured child route with indirect path and explicit component`, async function () {
@route({ path: 'a' })
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [A01] })
@customElement({ name: 'root', template: vp(1), dependencies: inDependencies ? [A01] : [] })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig);
await router.load('a');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
});
}
it(`can load a configured child route by name`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [A01] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig);
await router.load('a01');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
it(`works with single multi segment static path`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a/x', component: A01 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('a/x');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
it(`works with single multi segment dynamic path`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a/:x', component: A01 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('a/1');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
it(`works with single multi segment static path with single child`, async function () {
@customElement({ name: 'b01', template: null })
class B01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'b', component: B01 }] })
@customElement({ name: 'a11', template: vp(1) })
class A11 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a/x', component: A11 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('a/x/b');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a11', 'b01']);
});
it(`works with single multi segment static path with single multi segment static child`, async function () {
@customElement({ name: 'b01', template: null })
class B01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'b/x', component: B01 }] })
@customElement({ name: 'a11', template: vp(1) })
class A11 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a/x', component: A11 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('a/x/b/x');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a11', 'b01']);
});
it(`works with single static path with single multi segment static child`, async function () {
@customElement({ name: 'b01', template: null })
class B01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'b/x', component: B01 }] })
@customElement({ name: 'a11', template: vp(1) })
class A11 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'a', component: A11 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('a/b/x');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a11', 'b01']);
});
it(`works with single empty static path redirect`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: '', redirectTo: 'a' }, { path: 'a', component: A01 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
it(`works with single static path redirect`, async function () {
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [{ path: 'x', redirectTo: 'a' }, { path: 'a', component: A01 }] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router, activityTracker } = await createFixture(Root, [], getDefaultHIAConfig, () => ({}));
await router.load('x');
verifyInvocationsEqual(activityTracker.activeVMs, ['root', 'a01']);
});
describe(`throw error when`, function () {
it(`load a configured child route with indirect path by name`, async function () {
@route({ path: 'a' })
@customElement({ name: 'a01', template: null })
class A01 extends SimpleActivityTrackingVMBase {}
@route({ routes: [A01] })
@customElement({ name: 'root', template: vp(1) })
class Root extends SimpleActivityTrackingVMBase {}
const { router } = await createFixture(Root, [], getDefaultHIAConfig);
let e: Error | null = null;
try {
await router.load('a01');
} catch (err) {
e = err;
}
assert.notStrictEqual(e, null);
assert.match(e.message, /AUR3401/);
});
});
it('routes can be configured using the getRouteConfig hook', async function () {
@customElement({name: 'ce-c1', template: 'c1'})
class C1 { }
@customElement({name: 'ce-c2', template: 'c2'})
class C2 { }
@customElement({ name: 'ce-p', template: 'p <au-viewport></au-viewport>' })
class P implements IRouteViewModel {
public getRouteConfigCalled: number = 0;
public async getRouteConfig(parentDefinition: RouteConfig | null, routeNode: RouteNode | null): Promise<IRouteConfig> {
assert.notEqual(parentDefinition, null, 'P parentDefinition');
assert.notEqual(routeNode, null, 'P routeNode');
assert.strictEqual(this.getRouteConfigCalled, 0);
this.getRouteConfigCalled++;
await new Promise((resolve) => setTimeout(resolve, 10));
return {
routes:[
{ path:'c1', component: C1 },
{ path:'c2', component: C2 },
]
};
}
}
@customElement({ name: 'ro-ot', template: 'root <au-viewport></au-viewport>' })
class Root implements IRouteViewModel {
public getRouteConfigCalled: number = 0;
public getRouteConfig(parentDefinition: RouteConfig | null, routeNode: RouteNode | null): IRouteConfig {
assert.strictEqual(parentDefinition, null, 'root parentDefinition');
assert.strictEqual(routeNode, null, 'root routeNode');
assert.strictEqual(this.getRouteConfigCalled, 0);
this.getRouteConfigCalled++;
return {
routes:[
{ path:'p', component: P }
]
};
}
}
const ctx = TestContext.create();
const { container } = ctx;
container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration,
C1,
C2,
P,
Root,
);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Root, host }).start();
const router = container.get(IRouter);
await router.load('p/c1');
assert.html.textContent(host, 'root p c1');
await router.load('p/c2');
assert.html.textContent(host, 'root p c2');
assert.strictEqual((au.root.controller.viewModel as Root).getRouteConfigCalled, 1);
assert.strictEqual(CustomElement.for<P>(host.querySelector('ce-p')).viewModel.getRouteConfigCalled, 1);
await au.stop(true);
});
});
function getAllAsyncSpecs(count: number): HookSpecs {
return HookSpecs.create({
binding: hookSpecsMap.binding.async(count),
bound: hookSpecsMap.bound.async(count),
attaching: hookSpecsMap.attaching.async(count),
attached: hookSpecsMap.attached.async(count),
detaching: hookSpecsMap.detaching.async(count),
unbinding: hookSpecsMap.unbinding.async(count),
canLoad: hookSpecsMap.canLoad.async(count),
loading: hookSpecsMap.loading.async(count),
canUnload: hookSpecsMap.canUnload.async(count),
unloading: hookSpecsMap.unloading.async(count),
});
}