packages/__tests__/src/3-runtime-html/repeater-custom-element.spec.ts
import { Class } from '@aurelia/kernel';
import {
BindingBehavior,
customElement,
bindable,
CustomElement,
Aurelia,
IPlatform,
ICustomElementViewModel,
ICustomElementController,
} from '@aurelia/runtime-html';
import {
assert,
getVisibleText,
TestContext,
} from '@aurelia/testing';
import {
createSpecFunction,
TestExecutionContext,
TestFunction,
} from '../util.js';
describe('3-runtime-html/repeater-custom-element.spec.ts', function () {
interface TestSetupContext<TApp> {
template: string;
registrations: any[];
app: Class<TApp>;
}
async function testRepeatForCustomElement<TApp extends object>(
testFunction: TestFunction<TestExecutionContext<TApp>>,
{
template,
registrations = [],
app,
}: Partial<TestSetupContext<TApp>>
) {
const ctx = TestContext.create();
const host = ctx.doc.createElement('div');
ctx.doc.body.appendChild(host);
const container = ctx.container;
const au = new Aurelia(container);
await au.register(...registrations, BindingBehavior.define('keyed', class NoopKeyedBindingBehavior {}))
.app({
host,
component: CustomElement.define({ name: 'app', template }, app ?? class { })
})
.start();
const component = au.root.controller.viewModel as any;
try {
await testFunction({ app: component, container, ctx, host, platform: container.get(IPlatform) });
} catch (ex) {
ctx.doc.body.removeChild(host);
throw ex;
} finally {
await au.stop();
}
try {
assert.strictEqual(host.textContent, '', `host.textContent`);
} finally {
ctx.doc.body.removeChild(host);
}
}
const $it = createSpecFunction(testRepeatForCustomElement);
// repeater with custom element
{
@customElement({ name: 'foo', template: 'a' })
class Foo { }
class App {
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count"></foo>`,
};
$it('static child content', async function ({ app, platform, host }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
const q = platform.domQueue;
await q.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '${prop}' })
class Foo {
@bindable public prop: unknown;
}
class App { }
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template:
`<template>
<template repeat.for="i of 3">
<foo prop.bind="i"></foo>
</template>
</template>`,
};
$it('dynamic child content', async function ({ platform, host }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.html.innerEqual(host, '<foo>0</foo> <foo>1</foo> <foo>2</foo>', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '${prop}' })
class Foo {
@bindable public prop: unknown;
}
class App { }
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template:
`<let items.bind="[{p: 1}, {p: 2}, {p: 3}]"></let>` +
`<template repeat.for="item of items">
<foo prop.bind="item.p"></foo>
</template>`,
};
$it('let integration', async function ({ platform, host }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.html.innerEqual(host, '<foo>1</foo> <foo>2</foo> <foo>3</foo>', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'bar', template: 'bar' })
class Bar {
private static id: number = 1;
@bindable public id: number = Bar.id++;
}
@customElement({ name: 'foo', template: '${prop}' })
class Foo {
@bindable public prop: unknown;
}
class App { }
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Bar, Foo],
template:
`<template>
<template repeat.for="i of 2">
<let id.bind="null"></let>
<bar id.from-view="id"></bar>
<foo prop.bind="id"></foo>
</template>
</template>`,
};
$it('from-view integration', async function ({ platform, host }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.html.innerEqual(host, '<bar>bar</bar> <foo>1</foo> <bar>bar</bar> <foo>2</foo>', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public theText = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo text.bind="theText" repeat.for="i of count"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.theText = 'a';
// await platform.domQueue.yield();
platform.domQueue.flush();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public text = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo text.bind="text" repeat.for="i of count"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.text = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public theText: string;
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property, reversed - uninitialized property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.theText = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public text: string;
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" text.bind="text"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property, reversed - uninitialized property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.text = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text; }
class App {
public theText: string = 'a';
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property, reversed - initialized property', async function ({ platform, host }: TestExecutionContext<App>) {
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text; }
class App {
public theText: string = 'a';
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property, reversed - initialized property', async function ({ platform, host }: TestExecutionContext<App>) {
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div repeat.for="item of todos">${item}</div>', instructions: [] })
class Foo { @bindable public todos: any[]; }
class App {
public todos = ['a', 'b', 'c'];
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" todos.bind="todos"></foo>`,
};
$it('repeater with custom element with repeater', async function ({ platform, host, app }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
app.count = 1;
await q.yield();
assert.strictEqual(host.textContent, 'abc', `host.textContent`);
app.count = 3;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div repeat.for="innerTodos of todos"><div repeat.for="item of innerTodos">${item}</div></div>', instructions: [] })
class Foo { @bindable public todos: any[]; }
class App {
public todos = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']];
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count" todos.bind="todos"></foo>`,
};
$it('repeater with custom element with repeater, nested arrays', async function ({ platform, host, app }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabcabcabcabcabcabcabc', `host.textContent`);
app.count = 1;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
app.count = 3;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabcabcabcabcabcabcabc', `host.textContent`);
}, setup);
}
{
let childrenCount = 0;
@customElement({
name: 'foo-el',
template: `\${txt}<foo-el if.bind="cur<max" cnt.bind="cnt" max.bind="max" cur.bind="cur+1" txt.bind="txt" repeat.for="i of cnt"></foo-el>`,
shadowOptions: { mode: 'open' }
})
class FooEl implements ICustomElementViewModel {
@bindable public cnt;
@bindable public max;
@bindable public cur;
@bindable public txt;
public $controller: ICustomElementController<this>;
public attached() {
childrenCount += this.$controller.children.length;
}
}
@customElement({
name: 'app',
shadowOptions: { mode: 'open' }
})
class App implements ICustomElementViewModel {
public cnt = 10;
public max = 3;
public txt = 'a';
public $controller: ICustomElementController<this>;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [FooEl],
template: `<foo-el cnt.bind="cnt" max.bind="max" cur="0" txt.bind="txt" repeat.for="i of cnt" ref.bind="'foo'+i"></foo-el>`,
};
$it('repeater with custom element and children observer', async function ({ host, app }: TestExecutionContext<App>) {
const content = getVisibleText(host, true);
let expectedCount = 10 + 10 ** 2 + 10 ** 3;
assert.strictEqual(content.length, expectedCount, `getVisibleText(au, host).length`);
assert.strictEqual(content, 'a'.repeat(expectedCount), `getVisibleText(au, host)`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount #1`);
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length #1`);
app.cnt = 11;
await Promise.resolve();
// TODO: find out why this shadow dom mutation observer thing doesn't work correctly in jsdom
if (typeof window !== 'undefined') {
expectedCount = 11 + 11 ** 2 + 11 ** 3;
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length #2`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount #2`);
}
}, setup);
}
{
let childrenCount = 0;
@customElement({
name: 'foo-el',
template: `\${txt}<foo-el if.bind="cur<max" cnt.bind="cnt" max.bind="max" cur.bind="cur+1" txt.bind="txt" repeat.for="i of cnt;key:n"></foo-el>`,
shadowOptions: { mode: 'open' }
})
class FooEl implements ICustomElementViewModel {
@bindable public cnt;
@bindable public max;
@bindable public cur;
@bindable public txt;
public $controller: ICustomElementController<this>;
public attached() {
childrenCount += this.$controller.children.length;
}
}
@customElement({
name: 'app',
shadowOptions: { mode: 'open' }
})
class App implements ICustomElementViewModel {
public cnt = Array(10).fill(null).map((v, i) => ({n:i}));
public max = 3;
public txt = 'a';
public $controller: ICustomElementController<this>;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [FooEl],
template: `<foo-el cnt.bind="cnt" max.bind="max" cur="0" txt.bind="txt" repeat.for="i of cnt;key:n" ref.bind="'foo'+i.n"></foo-el>`,
};
$it('repeater with custom element and children observer', async function ({ host, app }: TestExecutionContext<App>) {
const content = getVisibleText(host, true);
let expectedCount = 10 + 10 ** 2 + 10 ** 3;
assert.strictEqual(content.length, expectedCount, `getVisibleText(au, host).length`);
assert.strictEqual(content, 'a'.repeat(expectedCount), `getVisibleText(au, host)`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount #1`);
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length #1`);
app.cnt = Array(11).fill(null).map((v, i) => ({n:i}));
await Promise.resolve();
// TODO: find out why this shadow dom mutation observer thing doesn't work correctly in jsdom
if (typeof window !== 'undefined') {
expectedCount = 11 + 11 ** 2 + 11 ** 3;
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length #2`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount #2`);
}
}, setup);
}
{
@customElement({ name: 'foo', template: 'a', instructions: [] })
class Foo { }
class App {
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed"></foo>`,
};
$it('repeater with custom element', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public theText = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo text.bind="theText" repeat.for="i of count & keyed"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.theText = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public text = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo text.bind="text" repeat.for="i of count & keyed"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.text = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public theText = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property, reversed, uninitialized property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.theText = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text: string; }
class App {
public text = 'b';
public count: number;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" text.bind="text"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property, reversed, uninitialized property', async function ({ platform, host, app }: TestExecutionContext<App>) {
assert.strictEqual(host.textContent, '', `host.textContent`);
app.count = 3;
app.text = 'a';
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text; }
class App {
public theText = 'a';
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with different name than outer property, reversed', async function ({ platform, host }: TestExecutionContext<App>) {
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div>${text}</div>', instructions: [] })
class Foo { @bindable public text; }
class App {
public theText = 'a';
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" text.bind="theText"></foo>`,
};
$it('repeater with custom element + inner bindable with same name as outer property, reversed', async function ({ platform, host }: TestExecutionContext<App>) {
await platform.domQueue.yield();
assert.strictEqual(host.textContent, 'aaa', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div repeat.for="item of todos & keyed">${item}</div>', instructions: [] })
class Foo { @bindable public todos: any[]; }
class App {
public todos = ['a', 'b', 'c'];
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" todos.bind="todos"></foo>`,
};
$it('repeater with custom element with repeater', async function ({ platform, host, app }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
app.count = 1;
await q.yield();
assert.strictEqual(host.textContent, 'abc', `host.textContent`);
app.count = 3;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
}, setup);
}
{
@customElement({ name: 'foo', template: '<div repeat.for="innerTodos of todos & keyed"><div repeat.for="item of innerTodos & keyed">${item}</div></div>', instructions: [] })
class Foo { @bindable public todos: any[]; }
class App {
public todos = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']];
public count: number = 3;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [Foo],
template: `<foo repeat.for="i of count & keyed" todos.bind="todos"></foo>`,
};
$it('repeater with custom element with repeater, nested arrays', async function ({ platform, host, app }: TestExecutionContext<App>) {
const q = platform.domQueue;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabcabcabcabcabcabcabc', `host.textContent`);
app.count = 1;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabc', `host.textContent`);
app.count = 3;
await q.yield();
assert.strictEqual(host.textContent, 'abcabcabcabcabcabcabcabcabc', `host.textContent`);
}, setup);
}
// TODO: figure out why repeater in keyed mode gives different numbers
{
let childrenCount = 0;
@customElement({
name: 'foo-el',
template: `\${txt}<foo-el if.bind="cur<max" cnt.bind="cnt" max.bind="max" cur.bind="cur+1" txt.bind="txt" repeat.for="i of cnt"></foo-el>`,
shadowOptions: { mode: 'open' }
})
class FooEl implements ICustomElementViewModel {
@bindable public cnt;
@bindable public max;
@bindable public cur;
@bindable public txt;
public $controller: ICustomElementController<this>;
public attached() {
childrenCount += this.$controller.children.length;
}
}
@customElement({
name: 'app',
shadowOptions: { mode: 'open' }
})
class App implements ICustomElementViewModel {
public cnt = 10;
public max = 3;
public txt = 'a';
public $controller: ICustomElementController<this>;
}
const setup: Partial<TestSetupContext<App>> = {
app: App,
registrations: [FooEl],
template: `<foo-el cnt.bind="cnt" max.bind="max" cur="0" txt.bind="txt" repeat.for="i of cnt" ref.bind="'foo'+i"></foo-el>`,
};
$it('repeater with custom element and children observer', async function ({ host, app }: TestExecutionContext<App>) {
const content = getVisibleText(host, true);
let expectedCount = 10 + 10 ** 2 + 10 ** 3;
assert.strictEqual(content.length, expectedCount, `getVisibleText(au, host).length`);
assert.strictEqual(content, 'a'.repeat(expectedCount), `getVisibleText(au, host)`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount - #1`);
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length - #1`);
app['cnt'] = 11;
await Promise.resolve();
// TODO: find out why this shadow dom mutation observer thing doesn't work correctly in jsdom
if (typeof window !== 'undefined') {
expectedCount = 11 + 11 ** 2 + 11 ** 3;
assert.strictEqual(app.$controller.children.length, 1, `app['$children'].length - #2`);
assert.strictEqual(childrenCount, expectedCount, `childrenCount - #2`);
}
}, setup);
}
});