packages/__tests__/src/3-runtime-html/controller.hook-timings.integration.spec.ts
import { Registration, Writable, DI, resolve } from '@aurelia/kernel';
import {
Aurelia,
customElement,
ICustomElementController,
IPlatform,
IViewModel,
IHydratedController as HC,
IHydratedParentController as HPC,
} from '@aurelia/runtime-html';
import { assert, TestContext } from '@aurelia/testing';
function createFixture() {
const ctx = TestContext.create();
const cfg = new NotifierConfig([], 100);
const { container } = ctx;
container.register(Registration.instance(INotifierConfig, cfg));
const mgr = container.get(INotifierManager);
const p = container.get(IPlatform);
const host = ctx.createElement('div');
const au = new Aurelia(container);
return { mgr, p, au, host };
}
describe('3-runtime-html/controller.hook-timings.integration.spec.ts', function () {
const allSyncSpecs: IDelayedInvokerSpec = {
binding: (mgr, p) => DelayedInvoker.binding(mgr, p),
bound: (mgr, p) => DelayedInvoker.bound(mgr, p),
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p),
attached: (mgr, p) => DelayedInvoker.attached(mgr, p),
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p),
dispose: (mgr, p) => DelayedInvoker.dispose(mgr, p),
toString() { return 'allSync'; },
};
function getAllAsyncSpecs(ticks: number): IDelayedInvokerSpec {
return {
binding: (mgr, p) => DelayedInvoker.binding(mgr, p, ticks),
bound: (mgr, p) => DelayedInvoker.bound(mgr, p, ticks),
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p, ticks),
attached: (mgr, p) => DelayedInvoker.attached(mgr, p, ticks),
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, ticks),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, ticks),
dispose: (mgr, p) => DelayedInvoker.dispose(mgr, p),
toString() { return 'allAsync'; },
};
}
describe('basic single child component', function () {
function $(prefix: 'start' | 'stop') {
switch (prefix) {
case 'start': return function (component: 'app' | 'a-1') {
return function (hook?: HookName) {
switch (hook) {
case void 0: return [
`start.${component}.binding.enter`,
`start.${component}.binding.leave`,
`start.${component}.bound.enter`,
`start.${component}.bound.leave`,
`start.${component}.attaching.enter`,
`start.${component}.attaching.leave`,
`start.${component}.attached.enter`,
`start.${component}.attached.leave`,
];
default: return [
`${prefix}.${component}.${hook}.enter`,
`${prefix}.${component}.${hook}.leave`,
];
}
};
};
case 'stop': return function (component: 'app' | 'a-1') {
return function (hook: HookName) {
return [
`${prefix}.${component}.${hook}.enter`,
`${prefix}.${component}.${hook}.leave`,
];
};
};
}
}
const stop_allSync = [
...$('stop')('a-1')('detaching'),
...$('stop')('app')('detaching'),
...$('stop')('a-1')('unbinding'),
...$('stop')('app')('unbinding'),
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
];
const start_allSync = [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')(),
...$('start')('app')('attached'),
];
const allSync = [
...start_allSync,
...stop_allSync,
];
interface ISpec {
app: IDelayedInvokerSpec;
a1: IDelayedInvokerSpec;
expected: string[];
}
const syncLikeSpecs: ISpec[] = [
{
app: allSyncSpecs,
a1: allSyncSpecs,
expected: allSync,
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
binding: (mgr, p) => DelayedInvoker.binding(mgr, p, 1),
toString() { return 'async_binding'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
`start.a-1.binding.enter`,
`start.a-1.binding.tick(1)`,
`start.a-1.binding.leave`,
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
binding: (mgr, p) => DelayedInvoker.binding(mgr, p, 1),
toString() { return 'async_binding'; },
},
a1: allSyncSpecs,
expected: [
`start.app.binding.enter`,
`start.app.binding.tick(1)`,
`start.app.binding.leave`,
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
binding: (mgr, p) => DelayedInvoker.binding(mgr, p, 1),
toString() { return 'async_binding'; },
},
a1: {
...allSyncSpecs,
binding: (mgr, p) => DelayedInvoker.binding(mgr, p, 1),
toString() { return 'async_binding'; },
},
expected: [
`start.app.binding.enter`,
`start.app.binding.tick(1)`,
`start.app.binding.leave`,
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
`start.a-1.binding.enter`,
`start.a-1.binding.tick(1)`,
`start.a-1.binding.leave`,
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
bound: (mgr, p) => DelayedInvoker.bound(mgr, p, 1),
toString() { return 'async_bound'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
`start.a-1.bound.enter`,
`start.a-1.bound.tick(1)`,
`start.a-1.bound.leave`,
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
bound: (mgr, p) => DelayedInvoker.bound(mgr, p, 1),
toString() { return 'async_bound'; },
},
a1: allSyncSpecs,
expected: [
...$('start')('app')('binding'),
`start.app.bound.enter`,
`start.app.bound.tick(1)`,
`start.app.bound.leave`,
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
bound: (mgr, p) => DelayedInvoker.bound(mgr, p, 1),
toString() { return 'async_bound'; },
},
a1: {
...allSyncSpecs,
bound: (mgr, p) => DelayedInvoker.bound(mgr, p, 1),
toString() { return 'async_bound'; },
},
expected: [
...$('start')('app')('binding'),
`start.app.bound.enter`,
`start.app.bound.tick(1)`,
`start.app.bound.leave`,
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
`start.a-1.bound.enter`,
`start.a-1.bound.tick(1)`,
`start.a-1.bound.leave`,
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p, 1),
toString() { return 'async_attaching'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
`start.a-1.attaching.enter`,
`start.a-1.attaching.tick(1)`,
`start.a-1.attaching.leave`,
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p, 1),
toString() { return 'async_attaching'; },
},
a1: allSyncSpecs,
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
`start.app.attaching.enter`,
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
`start.app.attaching.tick(1)`,
`start.app.attaching.leave`,
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p, 1),
toString() { return 'async_attaching'; },
},
a1: {
...allSyncSpecs,
attaching: (mgr, p) => DelayedInvoker.attaching(mgr, p, 1),
toString() { return 'async_attaching'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
`start.app.attaching.enter`,
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
`start.a-1.attaching.enter`,
`start.app.attaching.tick(1)`,
`start.app.attaching.leave`,
`start.a-1.attaching.tick(1)`,
`start.a-1.attaching.leave`,
...$('start')('a-1')('attached'),
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
attached: (mgr, p) => DelayedInvoker.attached(mgr, p, 1),
toString() { return 'async_attached'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
`start.a-1.attached.enter`,
`start.a-1.attached.tick(1)`,
`start.a-1.attached.leave`,
...$('start')('app')('attached'),
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
attached: (mgr, p) => DelayedInvoker.attached(mgr, p, 1),
toString() { return 'async_attached'; },
},
a1: allSyncSpecs,
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
...$('start')('a-1')('attached'),
`start.app.attached.enter`,
`start.app.attached.tick(1)`,
`start.app.attached.leave`,
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
attached: (mgr, p) => DelayedInvoker.attached(mgr, p, 1),
toString() { return 'async_attached'; },
},
a1: {
...allSyncSpecs,
attached: (mgr, p) => DelayedInvoker.attached(mgr, p, 1),
toString() { return 'async_attached'; },
},
expected: [
...$('start')('app')('binding'),
...$('start')('app')('bound'),
...$('start')('app')('attaching'),
...$('start')('a-1')('binding'),
...$('start')('a-1')('bound'),
...$('start')('a-1')('attaching'),
`start.a-1.attached.enter`,
`start.a-1.attached.tick(1)`,
`start.a-1.attached.leave`,
`start.app.attached.enter`,
`start.app.attached.tick(1)`,
`start.app.attached.leave`,
...stop_allSync,
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
a1: allSyncSpecs,
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
`stop.app.detaching.enter`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
...$('stop')('a-1')('unbinding'),
...$('stop')('app')('unbinding'),
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
...$('stop')('app')('detaching'),
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
...$('stop')('a-1')('unbinding'),
...$('stop')('app')('unbinding'),
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
`stop.app.detaching.enter`,
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
...$('stop')('a-1')('unbinding'),
...$('stop')('app')('unbinding'),
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
a1: allSyncSpecs,
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
...$('stop')('app')('detaching'),
...$('stop')('a-1')('unbinding'),
`stop.app.unbinding.enter`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
...$('stop')('app')('detaching'),
`stop.a-1.unbinding.enter`,
...$('stop')('app')('unbinding'),
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
a1: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
...$('stop')('app')('detaching'),
`stop.a-1.unbinding.enter`,
`stop.app.unbinding.enter`,
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
a1: allSyncSpecs,
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
`stop.app.detaching.enter`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
...$('stop')('a-1')('unbinding'),
`stop.app.unbinding.enter`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: allSyncSpecs,
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
...$('stop')('app')('detaching'),
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.a-1.unbinding.enter`,
...$('stop')('app')('unbinding'),
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
`stop.app.detaching.enter`,
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
...$('stop')('a-1')('unbinding'),
`stop.app.unbinding.enter`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
a1: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
expected: [
...start_allSync,
...$('stop')('a-1')('detaching'),
`stop.app.detaching.enter`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
`stop.a-1.unbinding.enter`,
`stop.app.unbinding.enter`,
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
toString() { return 'async_detaching'; },
},
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
`stop.app.detaching.enter`,
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
`stop.a-1.unbinding.enter`,
...$('stop')('app')('unbinding'),
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_unbinding'; },
},
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
...$('stop')('app')('detaching'),
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.a-1.unbinding.enter`,
`stop.app.unbinding.enter`,
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
{
app: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
a1: {
...allSyncSpecs,
detaching: (mgr, p) => DelayedInvoker.detaching(mgr, p, 1),
unbinding: (mgr, p) => DelayedInvoker.unbinding(mgr, p, 1),
toString() { return 'async_detaching+unbinding'; },
},
expected: [
...start_allSync,
`stop.a-1.detaching.enter`,
`stop.app.detaching.enter`,
`stop.a-1.detaching.tick(1)`,
`stop.a-1.detaching.leave`,
`stop.app.detaching.tick(1)`,
`stop.app.detaching.leave`,
`stop.a-1.unbinding.enter`,
`stop.app.unbinding.enter`,
`stop.a-1.unbinding.tick(1)`,
`stop.a-1.unbinding.leave`,
`stop.app.unbinding.tick(1)`,
`stop.app.unbinding.leave`,
...$('stop')('app')('dispose'),
...$('stop')('a-1')('dispose'),
],
},
];
for (const { app, a1, expected } of syncLikeSpecs) {
it(`app ${app}, a-1 ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 if.bind ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 if.bind="true"></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 else ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 if.bind="false"></a-1><a-1 else></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 with ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 with.bind="{}"></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 repeat.for ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 repeat.for="i of 1"></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 switch.bind case.bind ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 switch.bind="1" case.bind="1"></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 switch.bind default-case ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 switch.bind="1" default-case></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 flags ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 flags></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
it(`app ${app}, a-1 portal ${a1}`, async function () {
const { mgr, p, au, host } = createFixture();
@customElement({ name: 'a-1', template: null })
class A1 extends TestVM { public constructor() { super(mgr, p, a1); } }
@customElement({ name: 'app', template: '<a-1 portal></a-1>', dependencies: [A1] })
class App extends TestVM { public constructor() { super(mgr, p, app); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, expected);
});
}
});
// Note: these tests don't necessarily test scenarios that aren't covered elsewhere - their purpose is to provide an easy to understand
// set of smoke tests for how the controllers deal with async hooks in various arrangements.
// Therefore, the assertions are intentionally verbose and hand-coded to make them as easy as possible to understand,
// even if this comes at a cost of making them harder to maintain/modify (which is not really supposed to happen anyway).
describe('parallelism', function () {
it(`parent 'attaching' can overlap with grandchild 'attached'`, async function () {
const { mgr, p, au, host } = createFixture();
const appSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
attaching: () => DelayedInvoker.attaching(mgr, p, 1),
};
const parentSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
};
const childSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, childSpec); } }
@customElement({ name: 'p-1', template: '<c-1></c-1>', dependencies: [C1] })class P1 extends TestVM { public constructor() { super(mgr, p, parentSpec); } }
@customElement({ name: 'app', template: '<p-1></p-1>', dependencies: [P1] })class App extends TestVM { public constructor() { super(mgr, p, appSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.p-1.binding.enter',
'start.p-1.binding.leave',
'start.p-1.bound.enter',
'start.p-1.bound.leave',
'start.p-1.attaching.enter',
'start.p-1.attaching.leave',
'start.c-1.binding.enter',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.leave',
'start.p-1.attached.enter',
'start.p-1.attached.leave',
// app.'attaching' still ongoing after c-1.'attached' finished
'start.app.attaching.tick(1)',
'start.app.attaching.leave',
'start.app.attached.enter',
'start.app.attached.leave',
// nothing of interest here: all of these are synchronous and do not demonstrate parallelism (not part of this test)
'stop.c-1.detaching.enter',
'stop.c-1.detaching.leave',
'stop.p-1.detaching.enter',
'stop.p-1.detaching.leave',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-1.unbinding.enter',
'stop.c-1.unbinding.leave',
'stop.p-1.unbinding.enter',
'stop.p-1.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.p-1.dispose.enter',
'stop.p-1.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
]);
});
it(`'attaching' is awaited before 'attached' starts, and child 'attached' is awaited before parent 'attached' starts`, async function () {
const { mgr, p, au, host } = createFixture();
const appSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
};
const childSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
attaching: () => DelayedInvoker.attaching(mgr, p, 1),
attached: () => DelayedInvoker.attached(mgr, p, 1),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, childSpec); } }
@customElement({ name: 'app', template: '<c-1></c-1>', dependencies: [C1] })class App extends TestVM { public constructor() { super(mgr, p, appSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.app.attaching.leave',
'start.c-1.binding.enter',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.c-1.attaching.tick(1)',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.tick(1)',
'start.c-1.attached.leave',
'start.app.attached.enter',
'start.app.attached.leave',
// nothing of interest here: all of these are synchronous and do not demonstrate parallelism (not part of this test)
'stop.c-1.detaching.enter',
'stop.c-1.detaching.leave',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-1.unbinding.enter',
'stop.c-1.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
]);
});
it(`parent and child 'attaching' can overlap`, async function () {
const { mgr, p, au, host } = createFixture();
const hookSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
attaching: () => DelayedInvoker.attaching(mgr, p, 2),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'app', template: '<c-1></c-1>', dependencies: [C1] })class App extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.c-1.binding.enter',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.app.attaching.tick(1)',
'start.c-1.attaching.tick(1)',
'start.app.attaching.tick(2)',
'start.app.attaching.leave',
'start.c-1.attaching.tick(2)',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.leave',
'start.app.attached.enter',
'start.app.attached.leave',
// nothing of interest here: all of these are synchronous and do not demonstrate parallelism (not part of this test)
'stop.c-1.detaching.enter',
'stop.c-1.detaching.leave',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-1.unbinding.enter',
'stop.c-1.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
]);
});
it(`'binding' and 'bound' are sequential relative to each other and across parent-child hierarchies`, async function () {
const { mgr, p, au, host } = createFixture();
const hookSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
binding: () => DelayedInvoker.binding(mgr, p, 1),
bound: () => DelayedInvoker.bound(mgr, p, 1),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'app', template: '<c-1></c-1>', dependencies: [C1] })class App extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.tick(1)',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.tick(1)',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.app.attaching.leave',
'start.c-1.binding.enter',
'start.c-1.binding.tick(1)',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.tick(1)',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.leave',
'start.app.attached.enter',
'start.app.attached.leave',
// nothing of interest here: all of these are synchronous and do not demonstrate parallelism (not part of this test)
'stop.c-1.detaching.enter',
'stop.c-1.detaching.leave',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-1.unbinding.enter',
'stop.c-1.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
]);
});
it(`'detaching' and 'unbinding' are individually awaited bottom-up in parallel`, async function () {
const { mgr, p, au, host } = createFixture();
const hookSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
detaching: () => DelayedInvoker.detaching(mgr, p, 2),
unbinding: () => DelayedInvoker.unbinding(mgr, p, 2),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'p-1', template: '<c-1></c-1>', dependencies: [C1] })class P1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'app', template: '<p-1></p-1>', dependencies: [P1] })class App extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.app.attaching.leave',
'start.p-1.binding.enter',
'start.p-1.binding.leave',
'start.p-1.bound.enter',
'start.p-1.bound.leave',
'start.p-1.attaching.enter',
'start.p-1.attaching.leave',
'start.c-1.binding.enter',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.leave',
'start.p-1.attached.enter',
'start.p-1.attached.leave',
'start.app.attached.enter',
'start.app.attached.leave',
// all 3 'detaching' started bottom-up in parallel, and awaited before 'unbinding' is started
'stop.c-1.detaching.enter',
'stop.p-1.detaching.enter',
'stop.app.detaching.enter',
'stop.c-1.detaching.tick(1)',
'stop.p-1.detaching.tick(1)',
'stop.app.detaching.tick(1)',
'stop.c-1.detaching.tick(2)',
'stop.c-1.detaching.leave',
'stop.p-1.detaching.tick(2)',
'stop.p-1.detaching.leave',
'stop.app.detaching.tick(2)',
'stop.app.detaching.leave',
// all 3 'unbinding' started bottom-up in parallel, and awaited before 'dispose' is started
'stop.c-1.unbinding.enter',
'stop.p-1.unbinding.enter',
'stop.app.unbinding.enter',
'stop.c-1.unbinding.tick(1)',
'stop.p-1.unbinding.tick(1)',
'stop.app.unbinding.tick(1)',
'stop.c-1.unbinding.tick(2)',
'stop.c-1.unbinding.leave',
'stop.p-1.unbinding.tick(2)',
'stop.p-1.unbinding.leave',
'stop.app.unbinding.tick(2)',
'stop.app.unbinding.leave',
// 'dispose' runs top-down (and is always synchronous)
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.p-1.dispose.enter',
'stop.p-1.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
]);
});
it(`all hooks are arranged as expected in a complex tree when all are async with the same timings`, async function () {
const { mgr, p, au, host } = createFixture();
const hookSpec: IDelayedInvokerSpec = {
...getAllAsyncSpecs(1),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'c-2', template: null })class C2 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'p-1', template: '<c-1></c-1>', dependencies: [C1] })class P1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'p-2', template: '<c-2></c-2>', dependencies: [C2] })class P2 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'app', template: '<p-1></p-1><p-2></p-2>', dependencies: [P1, P2] })class App extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.tick(1)',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.tick(1)',
'start.app.bound.leave',
'start.app.attaching.enter',
// parent 'attaching' starts in parallel with child activation
'start.p-1.binding.enter',
'start.p-2.binding.enter',
'start.app.attaching.tick(1)',
'start.app.attaching.leave',
'start.p-1.binding.tick(1)',
'start.p-1.binding.leave',
'start.p-2.binding.tick(1)',
'start.p-2.binding.leave',
'start.p-1.bound.enter',
'start.p-2.bound.enter',
'start.p-1.bound.tick(1)',
'start.p-1.bound.leave',
'start.p-2.bound.tick(1)',
'start.p-2.bound.leave',
'start.p-1.attaching.enter',
// parent 'attaching' starts in parallel with child activation
'start.c-1.binding.enter',
'start.p-2.attaching.enter',
// parent 'attaching' starts in parallel with child activation
'start.c-2.binding.enter',
'start.p-1.attaching.tick(1)',
'start.p-1.attaching.leave',
'start.c-1.binding.tick(1)',
'start.c-1.binding.leave',
'start.p-2.attaching.tick(1)',
'start.p-2.attaching.leave',
'start.c-2.binding.tick(1)',
'start.c-2.binding.leave',
'start.c-1.bound.enter',
'start.c-2.bound.enter',
'start.c-1.bound.tick(1)',
'start.c-1.bound.leave',
'start.c-2.bound.tick(1)',
'start.c-2.bound.leave',
'start.c-1.attaching.enter',
'start.c-2.attaching.enter',
'start.c-1.attaching.tick(1)',
'start.c-1.attaching.leave',
'start.c-2.attaching.tick(1)',
'start.c-2.attaching.leave',
// 'attached' runs bottom-up and children are awaited before parents
'start.c-1.attached.enter',
'start.c-2.attached.enter',
'start.c-1.attached.tick(1)',
'start.c-1.attached.leave',
'start.c-2.attached.tick(1)',
'start.c-2.attached.leave',
'start.p-1.attached.enter',
'start.p-2.attached.enter',
'start.p-1.attached.tick(1)',
'start.p-1.attached.leave',
'start.p-2.attached.tick(1)',
'start.p-2.attached.leave',
'start.app.attached.enter',
'start.app.attached.tick(1)',
'start.app.attached.leave',
// all 'detaching' hooks are started bottom-up in parallel, and all awaited before any 'unbinding' hooks are started
'stop.c-1.detaching.enter',
'stop.p-1.detaching.enter',
'stop.c-2.detaching.enter',
'stop.p-2.detaching.enter',
'stop.app.detaching.enter',
'stop.c-1.detaching.tick(1)',
'stop.c-1.detaching.leave',
'stop.p-1.detaching.tick(1)',
'stop.p-1.detaching.leave',
'stop.c-2.detaching.tick(1)',
'stop.c-2.detaching.leave',
'stop.p-2.detaching.tick(1)',
'stop.p-2.detaching.leave',
'stop.app.detaching.tick(1)',
'stop.app.detaching.leave',
// all 'unbinding' hooks are started bottom-up in parallel, and all awaited before any 'dispose' hooks are started
'stop.c-1.unbinding.enter',
'stop.p-1.unbinding.enter',
'stop.c-2.unbinding.enter',
'stop.p-2.unbinding.enter',
'stop.app.unbinding.enter',
'stop.c-1.unbinding.tick(1)',
'stop.c-1.unbinding.leave',
'stop.p-1.unbinding.tick(1)',
'stop.p-1.unbinding.leave',
'stop.c-2.unbinding.tick(1)',
'stop.c-2.unbinding.leave',
'stop.p-2.unbinding.tick(1)',
'stop.p-2.unbinding.leave',
'stop.app.unbinding.tick(1)',
'stop.app.unbinding.leave',
// all 'dispose' hooks are run top-down
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.p-1.dispose.enter',
'stop.p-1.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
'stop.p-2.dispose.enter',
'stop.p-2.dispose.leave',
'stop.c-2.dispose.enter',
'stop.c-2.dispose.leave',
]);
});
it(`activation hooks are arranged as expected in a complex tree when all are async but 'attaching' taking much longer than the rest`, async function () {
const { mgr, p, au, host } = createFixture();
const hookSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
binding: () => DelayedInvoker.binding(mgr, p, 1),
bound: () => DelayedInvoker.bound(mgr, p, 1),
attaching: () => DelayedInvoker.attaching(mgr, p, 10),
attached: () => DelayedInvoker.attached(mgr, p, 1),
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'c-2', template: null })class C2 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'p-1', template: '<c-1></c-1>', dependencies: [C1] })class P1 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'p-2', template: '<c-2></c-2>', dependencies: [C2] })class P2 extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
@customElement({ name: 'app', template: '<p-1></p-1><p-2></p-2>', dependencies: [P1, P2] })class App extends TestVM { public constructor() { super(mgr, p, hookSpec); } }
au.app({ host, component: App });
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
// app.'binding' is awaited before starting app.'bound'
'start.app.binding.enter',
'start.app.binding.tick(1)',
'start.app.binding.leave',
// app.'bound' is awaited before starting app.'attaching'
'start.app.bound.enter',
'start.app.bound.tick(1)',
'start.app.bound.leave',
// app.'attaching' is awaited in parallel with p-1 & p-2 activation before starting app.'attached'
'start.app.attaching.enter',
'start.p-1.binding.enter',
'start.p-2.binding.enter',
'start.app.attaching.tick(1)',
'start.p-1.binding.tick(1)',
'start.p-1.binding.leave',
'start.p-2.binding.tick(1)',
'start.p-2.binding.leave',
'start.app.attaching.tick(2)',
'start.p-1.bound.enter',
'start.p-2.bound.enter',
'start.app.attaching.tick(3)',
'start.p-1.bound.tick(1)',
'start.p-1.bound.leave',
'start.p-2.bound.tick(1)',
'start.p-2.bound.leave',
'start.app.attaching.tick(4)',
// p-1.'attaching' is awaited in parallel with its children (c-1) activation before starting p-1.'attached'
'start.p-1.attaching.enter',
'start.c-1.binding.enter',
// p-2.'attaching' is awaited in parallel with its children (c-2) activation before starting p-2.'attached'
'start.p-2.attaching.enter',
'start.c-2.binding.enter',
'start.app.attaching.tick(5)',
'start.p-1.attaching.tick(1)',
'start.c-1.binding.tick(1)',
'start.c-1.binding.leave',
'start.p-2.attaching.tick(1)',
'start.c-2.binding.tick(1)',
'start.c-2.binding.leave',
'start.app.attaching.tick(6)',
'start.p-1.attaching.tick(2)',
'start.c-1.bound.enter',
'start.p-2.attaching.tick(2)',
'start.c-2.bound.enter',
'start.app.attaching.tick(7)',
'start.p-1.attaching.tick(3)',
'start.c-1.bound.tick(1)',
'start.c-1.bound.leave',
'start.p-2.attaching.tick(3)',
'start.c-2.bound.tick(1)',
'start.c-2.bound.leave',
'start.app.attaching.tick(8)',
'start.p-1.attaching.tick(4)',
// c-1.'attaching' is awaited before starting c-1.'attached'
'start.c-1.attaching.enter',
'start.p-2.attaching.tick(4)',
// c-2.'attaching' is awaited before starting c-2.'attached'
'start.c-2.attaching.enter',
'start.app.attaching.tick(9)',
'start.p-1.attaching.tick(5)',
'start.c-1.attaching.tick(1)',
'start.p-2.attaching.tick(5)',
'start.c-2.attaching.tick(1)',
'start.app.attaching.tick(10)',
// app.'attaching' is now done, but p-1/c-1 & p-2/c-2 activation is still ongoing so not starting app.'attached' yet
'start.app.attaching.leave',
'start.p-1.attaching.tick(6)',
'start.c-1.attaching.tick(2)',
'start.p-2.attaching.tick(6)',
'start.c-2.attaching.tick(2)',
'start.p-1.attaching.tick(7)',
'start.c-1.attaching.tick(3)',
'start.p-2.attaching.tick(7)',
'start.c-2.attaching.tick(3)',
'start.p-1.attaching.tick(8)',
'start.c-1.attaching.tick(4)',
'start.p-2.attaching.tick(8)',
'start.c-2.attaching.tick(4)',
'start.p-1.attaching.tick(9)',
'start.c-1.attaching.tick(5)',
'start.p-2.attaching.tick(9)',
'start.c-2.attaching.tick(5)',
'start.p-1.attaching.tick(10)',
// p-1.'attaching' is now done, but c-1 activation is still ongoing so not starting p-1.'attached' yet
'start.p-1.attaching.leave',
'start.c-1.attaching.tick(6)',
'start.p-2.attaching.tick(10)',
// p-2.'attaching' is now done, but c-2 activation is still ongoing so not starting p-2.'attached' yet
'start.p-2.attaching.leave',
'start.c-2.attaching.tick(6)',
'start.c-1.attaching.tick(7)',
'start.c-2.attaching.tick(7)',
'start.c-1.attaching.tick(8)',
'start.c-2.attaching.tick(8)',
'start.c-1.attaching.tick(9)',
'start.c-2.attaching.tick(9)',
// c-1.'attaching' and c-2.'attaching' are now done
'start.c-1.attaching.tick(10)',
'start.c-1.attaching.leave',
'start.c-2.attaching.tick(10)',
'start.c-2.attaching.leave',
// c-1 and c-2 'attaching' finished, starting c-1 and c-2 'attached'
'start.c-1.attached.enter',
'start.c-2.attached.enter',
'start.c-1.attached.tick(1)',
'start.c-1.attached.leave',
'start.c-2.attached.tick(1)',
'start.c-2.attached.leave',
// c-1 and c-2 'attached' (last part of activation) finished, starting p-1 and p-2 'attached'
'start.p-1.attached.enter',
'start.p-2.attached.enter',
'start.p-1.attached.tick(1)',
'start.p-1.attached.leave',
'start.p-2.attached.tick(1)',
'start.p-2.attached.leave',
// p-1 and p-2 'attached' (last part of activation) finished, starting app 'attached'
'start.app.attached.enter',
'start.app.attached.tick(1)',
'start.app.attached.leave',
// nothing of interest here: all of these are synchronous and do not demonstrate parallelism (not part of this test)
'stop.c-1.detaching.enter',
'stop.c-1.detaching.leave',
'stop.p-1.detaching.enter',
'stop.p-1.detaching.leave',
'stop.c-2.detaching.enter',
'stop.c-2.detaching.leave',
'stop.p-2.detaching.enter',
'stop.p-2.detaching.leave',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-1.unbinding.enter',
'stop.c-1.unbinding.leave',
'stop.p-1.unbinding.enter',
'stop.p-1.unbinding.leave',
'stop.c-2.unbinding.enter',
'stop.c-2.unbinding.leave',
'stop.p-2.unbinding.enter',
'stop.p-2.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.p-1.dispose.enter',
'stop.p-1.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
'stop.p-2.dispose.enter',
'stop.p-2.dispose.leave',
'stop.c-2.dispose.enter',
'stop.c-2.dispose.leave',
]);
});
it(`separate activate + deactivate can be aligned on attaching/detaching`, async function () {
const { mgr, p, au, host } = createFixture();
const componentSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
attaching: () => DelayedInvoker.attaching(mgr, p, 3),
detaching: () => DelayedInvoker.detaching(mgr, p, 3),
};
const appSpec: IDelayedInvokerSpec = {
...allSyncSpecs,
};
@customElement({ name: 'c-1', template: null })class C1 extends TestVM { public constructor() { super(mgr, p, componentSpec); } }
@customElement({ name: 'c-2', template: null })class C2 extends TestVM { public constructor() { super(mgr, p, componentSpec); } }
@customElement({ name: 'p-1', template: '<c-1></c-1>', dependencies: [C1] })class P1 extends TestVM { public constructor() { super(mgr, p, componentSpec); } }
@customElement({ name: 'p-2', template: '<c-2></c-2>', dependencies: [C2] })class P2 extends TestVM { public constructor() { super(mgr, p, componentSpec); } }
@customElement({ name: 'app', template: '<p-1 if.bind="n===1"></p-1><p-2 if.bind="n===2"></p-2>', dependencies: [P1, P2] })
class App extends TestVM {
public n: number = 1;
public constructor() { super(mgr, p, appSpec); }
}
au.app({ host, component: App });
const app = au.root.controller.viewModel as App;
mgr.setPrefix('start');
await au.start();
mgr.setPrefix('swap');
app.n = 2;
await waitForTick(6);
mgr.setPrefix('stop');
await au.stop(true);
verifyInvocationsEqual(mgr.fullNotifyHistory, [
'start.app.binding.enter',
'start.app.binding.leave',
'start.app.bound.enter',
'start.app.bound.leave',
'start.app.attaching.enter',
'start.app.attaching.leave',
'start.p-1.binding.enter',
'start.p-1.binding.leave',
'start.p-1.bound.enter',
'start.p-1.bound.leave',
'start.p-1.attaching.enter',
'start.c-1.binding.enter',
'start.c-1.binding.leave',
'start.c-1.bound.enter',
'start.c-1.bound.leave',
'start.c-1.attaching.enter',
'start.p-1.attaching.tick(1)',
'start.c-1.attaching.tick(1)',
'start.p-1.attaching.tick(2)',
'start.c-1.attaching.tick(2)',
'start.p-1.attaching.tick(3)',
'start.p-1.attaching.leave',
'start.c-1.attaching.tick(3)',
'start.c-1.attaching.leave',
'start.c-1.attached.enter',
'start.c-1.attached.leave',
'start.p-1.attached.enter',
'start.p-1.attached.leave',
'start.app.attached.enter',
'start.app.attached.leave',
'swap.c-1.detaching.enter',
'swap.p-1.detaching.enter',
'swap.p-2.binding.enter',
'swap.p-2.binding.leave',
'swap.p-2.bound.enter',
'swap.p-2.bound.leave',
'swap.p-2.attaching.enter',
'swap.c-2.binding.enter',
'swap.c-2.binding.leave',
'swap.c-2.bound.enter',
'swap.c-2.bound.leave',
'swap.c-2.attaching.enter',
// start of the part that's relevant to this test
'swap.c-1.detaching.tick(1)',
'swap.p-1.detaching.tick(1)',
'swap.p-2.attaching.tick(1)',
'swap.c-2.attaching.tick(1)',
'swap.c-1.detaching.tick(2)',
'swap.p-1.detaching.tick(2)',
'swap.p-2.attaching.tick(2)',
'swap.c-2.attaching.tick(2)',
'swap.c-1.detaching.tick(3)',
'swap.c-1.detaching.leave',
'swap.p-1.detaching.tick(3)',
'swap.p-1.detaching.leave',
'swap.p-2.attaching.tick(3)',
'swap.p-2.attaching.leave',
'swap.c-2.attaching.tick(3)',
'swap.c-2.attaching.leave',
// end of the part that's relevant to this test
'swap.c-1.unbinding.enter',
'swap.c-1.unbinding.leave',
'swap.p-1.unbinding.enter',
'swap.p-1.unbinding.leave',
'swap.c-2.attached.enter',
'swap.c-2.attached.leave',
'swap.p-2.attached.enter',
'swap.p-2.attached.leave',
'stop.c-2.detaching.enter',
'stop.p-2.detaching.enter',
'stop.app.detaching.enter',
'stop.app.detaching.leave',
'stop.c-2.detaching.tick(1)',
'stop.p-2.detaching.tick(1)',
'stop.c-2.detaching.tick(2)',
'stop.p-2.detaching.tick(2)',
'stop.c-2.detaching.tick(3)',
'stop.c-2.detaching.leave',
'stop.p-2.detaching.tick(3)',
'stop.p-2.detaching.leave',
'stop.c-2.unbinding.enter',
'stop.c-2.unbinding.leave',
'stop.p-2.unbinding.enter',
'stop.p-2.unbinding.leave',
'stop.app.unbinding.enter',
'stop.app.unbinding.leave',
'stop.app.dispose.enter',
'stop.app.dispose.leave',
'stop.p-1.dispose.enter',
'stop.p-1.dispose.leave',
'stop.c-1.dispose.enter',
'stop.c-1.dispose.leave',
'stop.p-2.dispose.enter',
'stop.p-2.dispose.leave',
'stop.c-2.dispose.enter',
'stop.c-2.dispose.leave',
]);
});
});
});
async function waitForTick(count: number) {
while (count-- > 0) {
await Promise.resolve();
}
}
const hookNames = ['binding', 'bound', 'attaching', 'attached', 'detaching', 'unbinding'] as const;
type HookName = typeof hookNames[number] | 'dispose';
interface IDelayedInvokerSpec {
binding(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'binding'>;
bound(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'bound'>;
attaching(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'attaching'>;
attached(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'attached'>;
detaching(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'detaching'>;
unbinding(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'unbinding'>;
dispose(mgr: INotifierManager, p: IPlatform): DelayedInvoker<'dispose'>;
toString(): string;
}
abstract class TestVM implements IViewModel {
public readonly $controller!: ICustomElementController<this>;
public get name(): string { return this.$controller.definition.name; }
public readonly bindingDI: DelayedInvoker<'binding'>;
public readonly boundDI: DelayedInvoker<'bound'>;
public readonly attachingDI: DelayedInvoker<'attaching'>;
public readonly attachedDI: DelayedInvoker<'attached'>;
public readonly detachingDI: DelayedInvoker<'detaching'>;
public readonly unbindingDI: DelayedInvoker<'unbinding'>;
public readonly disposeDI: DelayedInvoker<'dispose'>;
public constructor(mgr: INotifierManager, p: IPlatform, { binding, bound, attaching, attached, detaching, unbinding, dispose }: IDelayedInvokerSpec) {
this.bindingDI = binding(mgr, p);
this.boundDI = bound(mgr, p);
this.attachingDI = attaching(mgr, p);
this.attachedDI = attached(mgr, p);
this.detachingDI = detaching(mgr, p);
this.unbindingDI = unbinding(mgr, p);
this.disposeDI = dispose(mgr, p);
}
public binding(i: HC, p: HPC): void | Promise<void> { return this.bindingDI.invoke(this, () => { this.$binding(i, p); }); }
public bound(i: HC, p: HPC): void | Promise<void> { return this.boundDI.invoke(this, () => { this.$bound(i, p); }); }
public attaching(i: HC, p: HPC): void | Promise<void> { return this.attachingDI.invoke(this, () => { this.$attaching(i, p); }); }
public attached(i: HC): void | Promise<void> { return this.attachedDI.invoke(this, () => { this.$attached(i); }); }
public detaching(i: HC, p: HPC): void | Promise<void> { return this.detachingDI.invoke(this, () => { this.$detaching(i, p); }); }
public unbinding(i: HC, p: HPC): void | Promise<void> { return this.unbindingDI.invoke(this, () => { this.$unbinding(i, p); }); }
public dispose(): void { void this.disposeDI.invoke(this, () => { this.$dispose(); }); }
protected $binding(_i: HC, _p: HPC): void { /* do nothing */ }
protected $bound(_i: HC, _p: HPC): void { /* do nothing */ }
protected $attaching(_i: HC, _p: HPC): void { /* do nothing */ }
protected $attached(_i: HC): void { /* do nothing */ }
protected $detaching(_i: HC, _p: HPC): void { /* do nothing */ }
protected $unbinding(_i: HC, _p: HPC): void { /* do nothing */ }
protected $dispose(this: Partial<Writable<this>>): void {
this.bindingDI = void 0;
this.boundDI = void 0;
this.attachingDI = void 0;
this.attachedDI = void 0;
this.detachingDI = void 0;
this.unbindingDI = void 0;
this.disposeDI = void 0;
}
}
class Notifier {
public readonly p: IPlatform;
public readonly entryHistory: string[] = [];
public readonly fullHistory: string[] = [];
public constructor(
public readonly mgr: NotifierManager,
public readonly name: HookName,
) {
this.p = mgr.p;
}
public enter(vm: TestVM): void {
this.entryHistory.push(vm.name);
this.fullHistory.push(`${vm.name}.enter`);
this.mgr.enter(vm, this);
}
public leave(vm: TestVM): void {
this.fullHistory.push(`${vm.name}.leave`);
this.mgr.leave(vm, this);
}
public tick(vm: TestVM, i: number): void {
this.fullHistory.push(`${vm.name}.tick(${i})`);
this.mgr.tick(vm, this, i);
}
public dispose(this: Partial<Writable<this>>): void {
this.entryHistory = void 0;
this.fullHistory = void 0;
this.p = void 0;
this.mgr = void 0;
}
}
const INotifierConfig = DI.createInterface<INotifierConfig>('INotifierConfig');
interface INotifierConfig extends NotifierConfig {}
class NotifierConfig {
public constructor(
public readonly resolveLabels: string[],
public readonly resolveTimeoutMs: number,
) {}
}
const INotifierManager = DI.createInterface<INotifierManager>('INotifierManager', x => x.singleton(NotifierManager));
interface INotifierManager extends NotifierManager {}
class NotifierManager {
public readonly entryNotifyHistory: string[] = [];
public readonly fullNotifyHistory: string[] = [];
public prefix: string = '';
public readonly p: IPlatform = resolve(IPlatform);
public readonly binding: Notifier = new Notifier(this, 'binding');
public readonly bound: Notifier = new Notifier(this, 'bound');
public readonly attaching: Notifier = new Notifier(this, 'attaching');
public readonly attached: Notifier = new Notifier(this, 'attached');
public readonly detaching: Notifier = new Notifier(this, 'detaching');
public readonly unbinding: Notifier = new Notifier(this, 'unbinding');
public readonly dispose: Notifier = new Notifier(this, 'dispose');
public enter(vm: TestVM, tracker: Notifier): void {
const label = `${this.prefix}.${vm.name}.${tracker.name}`;
this.entryNotifyHistory.push(label);
this.fullNotifyHistory.push(`${label}.enter`);
}
public leave(vm: TestVM, tracker: Notifier): void {
const label = `${this.prefix}.${vm.name}.${tracker.name}`;
this.fullNotifyHistory.push(`${label}.leave`);
}
public tick(vm: TestVM, tracker: Notifier, i: number): void {
const label = `${this.prefix}.${vm.name}.${tracker.name}`;
this.fullNotifyHistory.push(`${label}.tick(${i})`);
}
public setPrefix(prefix: string): void {
this.prefix = prefix;
}
public $dispose(this: Partial<Writable<this>>): void {
this.binding.dispose();
this.bound.dispose();
this.attaching.dispose();
this.attached.dispose();
this.detaching.dispose();
this.unbinding.dispose();
this.dispose.dispose();
this.entryNotifyHistory = void 0;
this.fullNotifyHistory = void 0;
this.p = void 0;
this.binding = void 0;
this.bound = void 0;
this.attaching = void 0;
this.attached = void 0;
this.detaching = void 0;
this.unbinding = void 0;
this.$dispose = void 0;
}
}
class DelayedInvoker<T extends HookName> {
public constructor(
public readonly mgr: INotifierManager,
public readonly p: IPlatform,
public readonly name: T,
public readonly ticks: number | null,
) {}
public static binding(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'binding'> { return new DelayedInvoker(mgr, p, 'binding', ticks); }
public static bound(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'bound'> { return new DelayedInvoker(mgr, p, 'bound', ticks); }
public static attaching(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'attaching'> { return new DelayedInvoker(mgr, p, 'attaching', ticks); }
public static attached(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'attached'> { return new DelayedInvoker(mgr, p, 'attached', ticks); }
public static detaching(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'detaching'> { return new DelayedInvoker(mgr, p, 'detaching', ticks); }
public static unbinding(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'unbinding'> { return new DelayedInvoker(mgr, p, 'unbinding', ticks); }
public static dispose(mgr: INotifierManager, p: IPlatform, ticks: number | null = null): DelayedInvoker<'dispose'> { return new DelayedInvoker(mgr, p, 'dispose', ticks); }
public invoke(vm: TestVM, cb: () => void): void | Promise<void> {
if (this.ticks === null) {
this.mgr[this.name].enter(vm);
cb();
this.mgr[this.name].leave(vm);
} else {
let i = -1;
let resolve: () => void;
const p = new Promise<void>(r => {
resolve = r;
});
const next = (): void => {
if (++i === 0) {
this.mgr[this.name].enter(vm);
} else {
this.mgr[this.name].tick(vm, i);
}
if (i < this.ticks) {
void Promise.resolve().then(next);
} else {
cb();
this.mgr[this.name].leave(vm);
resolve();
}
};
next();
return p;
}
}
public toString(): string {
let str = this.name as string;
if (this.ticks !== null) { str = `${str}.${this.ticks}t`; }
return str;
}
}
function verifyInvocationsEqual(actual: string[], expected: string[]): void {
const groupNames = new Set<string>();
actual.forEach(x => groupNames.add(x.slice(0, x.indexOf('.'))));
expected.forEach(x => groupNames.add(x.slice(0, x.indexOf('.'))));
const expectedGroups: Record<string, string[]> = {};
const actualGroups: Record<string, string[]> = {};
for (const groupName of groupNames) {
expectedGroups[groupName] = expected.filter(x => x.startsWith(`${groupName}.`));
actualGroups[groupName] = actual.filter(x => x.startsWith(`${groupName}.`));
}
const errors: string[] = [];
for (const prefix in expectedGroups) {
expected = expectedGroups[prefix];
actual = actualGroups[prefix];
const len = Math.max(actual.length, expected.length);
for (let i = 0; i < len; ++i) {
const $actual = actual[i];
const $expected = expected[i];
if ($actual === $expected) {
errors.push(` OK : ${$actual}`);
} else {
errors.push(`NOT OK : ${$actual} (expected: ${$expected})`);
}
}
}
if (errors.some(e => e.startsWith('N'))) {
throw new Error(`Failed assertion: invocation mismatch\n - ${errors.join('\n - ')})`);
} else {
// fallback just to make sure there's no bugs in this function causing false positives
assert.deepStrictEqual(actual, expected);
}
}