packages/__tests__/src/router-lite/resources/viewport.spec.ts
import { IRouter, IRouteViewModel, Params, route, Router, ViewportCustomElement } from '@aurelia/router-lite';
import { CustomElement, customElement, IPlatform } from '@aurelia/runtime-html';
import { assert } from '@aurelia/testing';
import { start } from '../_shared/create-fixture.js';
describe('router-lite/resources/viewport.spec.ts', function () {
function assertText(vps: Element[], expected: string[]) {
for (let i = 0; i < expected.length; i++) {
assert.html.textContent(vps[i], expected[i], `content #${i + 1}`);
}
}
it('sibling viewports with non-default routes are supported by binding the default property to null', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2' })
class CeTwo implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-one'],
component: CeOne,
},
{
path: 'ce-two',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></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);
await queue.yield();
const [vp1, vp2] = Array.from(host.querySelectorAll('au-viewport'));
const vm1 = CustomElement.for<ViewportCustomElement>(vp1).viewModel;
const vm2 = CustomElement.for<ViewportCustomElement>(vp2).viewModel;
assert.strictEqual(vm1.name, '$1');
assert.strictEqual(vm2.name, '$2');
assert.html.textContent(vp1, 'ce1');
assert.html.textContent(vp2, '');
await router.load('ce-two');
await queue.yield();
assert.html.textContent(vp1, 'ce2');
assert.html.textContent(vp2, '');
await router.load('ce-one');
await queue.yield();
assert.html.textContent(vp1, 'ce1');
assert.html.textContent(vp2, '');
await router.load('ce-two@$1+ce-one@$2');
await queue.yield();
assert.html.textContent(vp1, 'ce2');
assert.html.textContent(vp2, 'ce1');
await au.stop(true);
});
it('sibling viewports in children with non-default routes are supported by binding the default property to null', async function () {
@customElement({ name: 'ce-11', template: `ce11` })
class CeOneOne implements IRouteViewModel { }
@customElement({ name: 'ce-21', template: `ce21` })
class CeTwoOne implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-11'],
component: CeOneOne,
}
]
})
@customElement({
name: 'ce-one', template: `ce1
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></au-viewport>` })
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-21'],
component: CeTwoOne,
}
]
})
@customElement({
name: 'ce-two', template: `ce2
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></au-viewport>` })
class CeTwo implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-one'],
component: CeOne,
},
{
path: 'ce-two',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></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);
await queue.yield();
assert.html.textContent(host, 'ce1 ce11');
await router.load('ce-two');
await queue.yield();
assert.html.textContent(host, 'ce2 ce21');
await router.load('ce-one/ce-11');
await queue.yield();
assert.html.textContent(host, 'ce1 ce11');
await router.load('ce-two@$1+ce-one@$2');
await queue.yield();
assert.html.textContent(host, 'ce2 ce21 ce1 ce11');
await au.stop(true);
});
it('sibling viewports in children with non-default routes are supported by binding the default property to null - transition plan: invoke-lifecycle', async function () {
@customElement({ name: 'ce-21', template: `ce21` })
class CeTwoOne implements IRouteViewModel { }
@customElement({
name: 'ce-one', template: `ce1`
})
class CeOne implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-21'],
component: CeTwoOne,
}
]
})
@customElement({
name: 'ce-two', template: `ce2
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></au-viewport>` })
class CeTwo implements IRouteViewModel { }
@route({
routes: [
{
path: ['', 'ce-one'],
component: CeOne,
},
{
path: 'ce-two',
component: CeTwo,
},
],
transitionPlan() { return 'invoke-lifecycles'; }
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></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);
await queue.yield();
const [vp1, vp2] = Array.from(host.querySelectorAll('au-viewport'));
const vm1 = CustomElement.for<ViewportCustomElement>(vp1).viewModel;
const vm2 = CustomElement.for<ViewportCustomElement>(vp2).viewModel;
assert.strictEqual(vm1.name, '$1');
assert.strictEqual(vm2.name, '$2');
assert.html.textContent(vp1, 'ce1');
assert.html.textContent(vp2, '');
await router.load('ce-two/ce-21');
await queue.yield();
assert.html.textContent(vp1, 'ce2 ce21');
assert.html.textContent(vp2, '');
await router.load('ce-two@$2+ce-one@$1');
await queue.yield();
assert.html.textContent(vp1, 'ce1');
assert.html.textContent(vp2, 'ce2 ce21');
await au.stop(true);
});
it('sibling viewports - load non-empty-route@non-default-vp+empty-alias-route@default-vp', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2 ${id}' })
class CeTwo implements IRouteViewModel {
id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
return true;
}
}
@route({
routes: [
{
path: ['', 'ce-one'],
component: CeOne,
},
{
path: 'ce-two/:id',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2" default.bind="null"></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);
await queue.yield();
assert.html.textContent(host, 'ce1');
await router.load('ce-two/42@$2+ce-one@$1');
await queue.yield();
assert.html.textContent(host, 'ce1 ce2 42');
await au.stop(true);
});
// precondition: exists a mixture of named and unnamed viewports
// action: components are attempted to be loaded into named viewports
// expectation: components are loaded into named viewports
it('targeted components can be loaded into named viewports even when default viewports are present', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2 ${id}' })
class CeTwo implements IRouteViewModel {
id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
return true;
}
}
@route({
routes: [
{
path: 'ce-one',
component: CeOne,
},
{
path: 'ce-two/:id',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport></au-viewport>
<au-viewport name="$1"></au-viewport>
<au-viewport></au-viewport>
<au-viewport name="$2"></au-viewport>
<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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
const vms = vps.map(vp => CustomElement.for<ViewportCustomElement>(vp).viewModel);
assert.deepStrictEqual(vms.map(vm => vm.name), ['default', '$1', 'default', '$2', 'default']);
await router.load('ce-one@$1');
await queue.yield();
assertText(vps, ['', 'ce1', '', '', '']);
await router.load('ce-one@$2+ce-two/42@$1');
await queue.yield();
assertText(vps, ['', 'ce2 42', '', 'ce1', '']);
await router.load('ce-one+ce-two/42');
await queue.yield();
assertText(vps, ['ce1', 'ce2 42', '', '', '']);
await au.stop(true);
});
it('viewport configuration for route is respected', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2 ${id}' })
class CeTwo implements IRouteViewModel {
id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
return true;
}
}
@route({
routes: [
{
path: 'ce-one',
component: CeOne,
viewport: '$2',
},
{
path: 'ce-two/:id',
component: CeTwo,
viewport: '$1',
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport></au-viewport>
<au-viewport name="$1"></au-viewport>
<au-viewport></au-viewport>
<au-viewport name="$2"></au-viewport>
<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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
const vms = vps.map(vp => CustomElement.for<ViewportCustomElement>(vp).viewModel);
assert.deepStrictEqual(vms.map(vm => vm.name), ['default', '$1', 'default', '$2', 'default']);
await router.load('ce-one');
await queue.yield();
assertText(vps, ['', '', '', 'ce1', '']);
await router.load('ce-one+ce-two/42');
await queue.yield();
assertText(vps, ['', 'ce2 42', '', 'ce1', '']);
try {
await router.load('ce-one@$1');
assert.fail('expected error for loading ce-one@$1');
} catch {
/** ignore */
}
try {
await router.load('ce-two/42@$2');
assert.fail('expected error for loading ce-two/42@$2');
} catch {
/** ignore */
}
await au.stop(true);
});
it('multiple routes can use the same viewport', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: 'ce2 ${id}' })
class CeTwo implements IRouteViewModel {
id: string;
public canLoad(params: Params): boolean {
this.id = params.id;
return true;
}
}
@route({
routes: [
{
path: 'ce-one',
component: CeOne,
viewport: '$1',
},
{
path: 'ce-two/:id',
component: CeTwo,
viewport: '$1',
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport></au-viewport>
<au-viewport name="$1"></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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
const vms = vps.map(vp => CustomElement.for<ViewportCustomElement>(vp).viewModel);
assert.deepStrictEqual(vms.map(vm => vm.name), ['default', '$1']);
await router.load('ce-one');
await queue.yield();
assertText(vps, ['', 'ce1']);
await router.load('ce-two/42');
await queue.yield();
assertText(vps, ['', 'ce2 42']);
try {
await router.load('ce-one+ce-two/42');
assert.fail('expected error for loading ce-one+ce-two/42');
} catch {
/** ignore */
}
await au.stop(true);
});
it('used-by is respected', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: '${id1} ce2 ${id2}' })
class CeTwo implements IRouteViewModel {
id1: string;
id2: string;
public canLoad(params: Params): boolean {
this.id1 = params.id1;
this.id2 = params.id2;
return true;
}
}
@route({
routes: [
{
id: 'ce1',
path: 'ce-one',
component: CeOne,
},
{
id: 'ce2',
path: ':id1/foo/:id2',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport used-by="whatever"></au-viewport>
<au-viewport used-by="ce-two"></au-viewport>
<au-viewport used-by="ce-one"></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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
await router.load('ce-one');
await queue.yield();
assertText(vps, ['', '', 'ce1']);
await router.load('42/foo/43');
await queue.yield();
assertText(vps, ['', '42 ce2 43', '']);
await router.load('ce1+43/foo/42');
await queue.yield();
assertText(vps, ['', '43 ce2 42', 'ce1']);
await au.stop(true);
});
it('comma-separated used-by is respected', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: '${id1} ce2 ${id2}' })
class CeTwo implements IRouteViewModel {
id1: string;
id2: string;
public canLoad(params: Params): boolean {
this.id1 = params.id1;
this.id2 = params.id2;
return true;
}
}
@route({
routes: [
{
id: 'ce1',
path: 'ce-one',
component: CeOne,
},
{
id: 'ce2',
path: ':id1/foo/:id2',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport used-by="whatever"></au-viewport>
<au-viewport used-by="ce-one,ce-two"></au-viewport>
<au-viewport used-by="ce-one"></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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
await router.load('ce-one');
await queue.yield();
assertText(vps, ['', 'ce1', '']);
await router.load('42/foo/43');
await queue.yield();
assertText(vps, ['', '42 ce2 43', '']);
await router.load('43/foo/42+ce1');
await queue.yield();
assertText(vps, ['', '43 ce2 42', 'ce1']);
try {
await router.load('ce1+43/foo/42');
assert.fail('expected failure due to no free viewport to handle "43/foo/42" from the instruction "ce1+43/foo/42"');
} catch {
/* ignore */
}
await au.stop(true);
});
it('a preceding default (without used-by) can load components', async function () {
@customElement({ name: 'ce-one', template: 'ce1' })
class CeOne implements IRouteViewModel { }
@customElement({ name: 'ce-two', template: '${id1} ce2 ${id2}' })
class CeTwo implements IRouteViewModel {
id1: string;
id2: string;
public canLoad(params: Params): boolean {
this.id1 = params.id1;
this.id2 = params.id2;
return true;
}
}
@route({
routes: [
{
id: 'ce1',
path: 'ce-one',
component: CeOne,
},
{
id: 'ce2',
path: ':id1/foo/:id2',
component: CeTwo,
},
]
})
@customElement({
name: 'ro-ot',
template: `
<au-viewport></au-viewport>
<au-viewport used-by="ce-one,ce-two"></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);
await queue.yield();
const vps = Array.from(host.querySelectorAll('au-viewport'));
await router.load('ce-one');
await queue.yield();
assertText(vps, ['ce1', '']);
await router.load('42/foo/43');
await queue.yield();
assertText(vps, ['42 ce2 43', '']);
await router.load('43/foo/42+ce1');
await queue.yield();
assertText(vps, ['43 ce2 42', 'ce1']);
await au.stop(true);
});
});