packages/__tests__/src/3-runtime-html/template-compiler.au-slot.spec.ts
import { IExpressionParser } from '@aurelia/expression-parser';
import {
BindingMode,
AuSlot,
CustomElement,
CustomElementDefinition,
CustomElementType,
} from '@aurelia/runtime-html';
import {
HydrateElementInstruction,
InstructionType,
IInstruction,
PropertyBindingInstruction,
TextBindingInstruction,
} from '@aurelia/template-compiler';
import {
assert, TestContext
} from '@aurelia/testing';
export function createAttribute(name: string, value: string): Attr {
const attr = document.createAttribute(name);
attr.value = value;
return attr;
}
describe('3-runtime-html/template-compiler.au-slot.spec.ts', function () {
/** Used to ignore some assertion when needed, e.g nested instructions */
const anything = {} as any;
function createFixture() {
const ctx = TestContext.create();
const container = ctx.container;
const sut = ctx.templateCompiler;
return { ctx, container, sut };
}
function $createCustomElement(template: string, name: string) {
return CustomElement.define({ name, template, bindables: { people: { mode: BindingMode.default } }, }, class MyElement { });
}
class ExpectedSlotFallbackInfo {
public constructor(
public readonly slotName: string,
public readonly content: string,
) { }
}
class TestData {
public constructor(
public readonly template: string,
public readonly customElements: CustomElementType[],
public readonly allExpectedProjections: [string, Record<string, string>][] | null,
public readonly expectedSlotInfos: ExpectedSlotFallbackInfo[],
public readonly only = false,
) {
}
}
it('compiles default <au-slot> as the only child', function () {
const { template, instructions } = compileTemplate('<au-slot></au-slot>');
assertTemplateEqual(template, '<!--au*--><!--au-start--><!--au-end-->');
assertAuSlotFallback(instructions[0][0], null);
});
it('compiles 2 default <au-slot>s', function () {
const { template, instructions } = compileTemplate(
'<au-slot></au-slot><au-slot></au-slot>'
);
assertTemplateEqual(
template,
'<!--au*--><!--au-start--><!--au-end--><!--au*--><!--au-start--><!--au-end-->'
);
assertAuSlotFallback(instructions[0][0], null);
assertAuSlotFallback(instructions[1][0], null);
});
it('compiles default <au-slot> with fallback', function () {
const { template, instructions, createProp } = compileTemplate(
'<au-slot><div a.bind="b"></div></au-slot>'
);
assertTemplateEqual(
template,
'<!--au*--><!--au-start--><!--au-end-->'
);
assertAuSlotFallback(
instructions[0][0],
{ template: '<!--au*--><div></div>', instructions: [
[createProp({ from: 'b', to: 'a' })]
]}
);
});
it('compiles default <au-slot> with [interpolation] fallback', function () {
const { template, instructions, createTextInterpolation } = compileTemplate(
'<au-slot>${message}</au-slot>'
);
assertTemplateEqual(
template,
'<!--au*--><!--au-start--><!--au-end-->'
);
assertAuSlotFallback(
instructions[0][0],
{ template: '<!--au*--> ', instructions: [
[createTextInterpolation({ from: 'message' })]
]}
);
});
it('compiles together with slot', function () {
const { template, instructions } = compileTemplate('<slot></slot><au-slot></au-slot>');
assertTemplateEqual(template, '<slot></slot><!--au*--><!--au-start--><!--au-end-->');
assertAuSlotFallback(instructions[0][0], null);
});
it('compiles named <au-slot>', function () {
const { template, instructions } = compileTemplate('<au-slot name="s1"></au-slot>');
assertTemplateEqual(template, '<!--au*--><!--au-start--><!--au-end-->');
assertAuSlotFallback(instructions[0][0], null);
});
it('compiles default <au-slot> mixed with named <au-slot>', function () {
const { template, instructions } = compileTemplate('<au-slot name="s1"></au-slot><au-slot></au-slot>');
assertTemplateEqual(template, '<!--au*--><!--au-start--><!--au-end--><!--au*--><!--au-start--><!--au-end-->');
assertAuSlotFallback(instructions[0][0], null);
assertAuSlotFallback(instructions[1][0], null);
});
it('compiles projection with default [au-slot]', function () {
const { template, instructions, createProp } = compileTemplate(
'<el><div au-slot a.bind="b">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { default: {
template: '<!--au*--><div></div>', instructions: [[createProp({ from: 'b', to: 'a' })]]
} });
});
it('compiles content without the need of [au-slot]', function () {
const { template, instructions } = compileTemplate(
'<el><div>',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { default: {
template: '<div></div>', instructions: []
} });
});
it('compiles projection with default [au-slot] as empty string', function () {
const { template, instructions } = compileTemplate(
'<el><div au-slot="">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { default: {
template: '<div></div>', instructions: []
} });
});
it('does not get confused when theres a slot with the same name with project in the template', function () {
const { template, instructions } = compileTemplate(
'<au-slot></au-slot><el><div au-slot="">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><!--au-start--><!--au-end--><!--au*--><el></el>');
assertAuSlotFallback(instructions[0][0], null);
assertProjection(instructions[1][0], { default: {
template: '<div></div>', instructions: []
} });
});
it('compiles projection with specific [au-slot] name', function () {
const { template, instructions, createProp } = compileTemplate(
'<el><div au-slot="s1" a.bind="b">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { s1: {
template: '<!--au*--><div></div>', instructions: [[createProp({ from: 'b', to: 'a' })]]
} });
});
it('compiles auto projection with named projection', function () {
const { template, instructions } = compileTemplate(
'<el><div></div><div au-slot="s1">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], {
default: { template: '<div></div>', instructions: [] },
s1: { template: '<div></div>', instructions: [] },
});
});
it('compiles projection that has <au-slot>', function () {
const { template, instructions } = compileTemplate(
'<el><au-slot au-slot>',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { default: {
template: '<!--au*--><!--au-start--><!--au-end-->', instructions: anything
} });
assertAuSlotFallback(
(instructions[0][0] as HydrateElementInstruction).projections.default.instructions[0][0],
null,
);
});
it('compiles default <au-slot> in projection with fallback', function () {
const { template, instructions, createProp } = compileTemplate(
'<el><au-slot au-slot"><div a.bind="b">',
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], { default: {
template: '<!--au*--><!--au-start--><!--au-end-->', instructions: anything
} });
assertAuSlotFallback(
(instructions[0][0] as HydrateElementInstruction).projections.default.instructions[0][0],
{ template: '<!--au*--><div></div>', instructions: [[createProp({ from: 'b', to: 'a' })]] }
);
});
it('compiles multiple <au-slot>s in projection with fallback', function () {
const { template, instructions, createProp } = compileTemplate(
['<el>',
'<au-slot au-slot">',
'<div a.bind="b"></div>',
'</au-slot>',
'<au-slot au-slot="s1">',
'<div b.bind="a"></div>',
'</au-slot>'
].join(''),
$createCustomElement('', 'el')
);
assertTemplateEqual(template, '<!--au*--><el></el>');
assertProjection(instructions[0][0], {
default: { template: '<!--au*--><!--au-start--><!--au-end-->', instructions: anything },
s1: { template: '<!--au*--><!--au-start--><!--au-end-->', instructions: anything }
});
assertAuSlotFallback(
(instructions[0][0] as HydrateElementInstruction).projections.default.instructions[0][0],
{ template: '<!--au*--><div></div>', instructions: [[createProp({ from: 'b', to: 'a' })]] }
);
assertAuSlotFallback(
(instructions[0][0] as HydrateElementInstruction).projections.s1.instructions[0][0],
{ template: '<!--au*--><div></div>', instructions: [[createProp({ from: 'a', to: 'b' })]] }
);
});
function* getTestData() {
// projections verification
yield new TestData(
`<my-element><div au-slot></div></my-element>`,
[$createCustomElement('', 'my-element')],
[['my-element', { 'default': '<div></div>' }]],
[],
);
yield new TestData(
`<my-element><div au-slot="s1">p1</div><div au-slot="s2">p2</div></my-element>`,
[$createCustomElement('', 'my-element')],
[['my-element', { s1: '<div>p1</div>', s2: '<div>p2</div>' }]],
[],
);
yield new TestData(
`<my-element><div au-slot="s1">p1</div><div au-slot="s1">p2</div></my-element>`,
[$createCustomElement('', 'my-element')],
[['my-element', { s1: '<div>p1</div><div>p2</div>' }]],
[],
);
yield new TestData(
`<my-element><au-slot au-slot><div au-slot="s1">p1</div><div au-slot="s1">p2</div></au-slot></my-element>`,
[$createCustomElement('', 'my-element')],
[['my-element', { 'default': '<!--au*--><!--au-start--><!--au-end-->' }]],
[],
);
// fallback verification
yield new TestData(
`<au-slot name="s1">s1fb</au-slot><au-slot name="s2"><div>s2fb</div></au-slot>`,
[],
null,
[
new ExpectedSlotFallbackInfo('s1', 's1fb'),
new ExpectedSlotFallbackInfo('s2', '<div>s2fb</div>'),
],
);
yield new TestData(
`<au-slot name="s1">s1fb</au-slot><au-slot name="s2"><div>s2fb</div></au-slot>`,
[],
null,
[
new ExpectedSlotFallbackInfo('s1', 's1fb'),
new ExpectedSlotFallbackInfo('s2', '<div>s2fb</div>'),
],
);
yield new TestData(
`<au-slot name="s1">s1fb</au-slot><au-slot name="s2"><div>s2fb</div></au-slot>`,
[],
null,
[
new ExpectedSlotFallbackInfo('s1', 's1fb'),
new ExpectedSlotFallbackInfo('s2', '<div>s2fb</div>'),
],
);
yield new TestData(
`<au-slot name="s1">s1fb</au-slot><my-element><div au-slot>p</div></my-element>`,
[$createCustomElement('', 'my-element')],
null,
[
new ExpectedSlotFallbackInfo('s1', 's1fb'),
],
);
}
for (const { only, customElements, template, expectedSlotInfos, allExpectedProjections } of getTestData()) {
(only ? it.only : it)(`compiles - ${template}`, function () {
const { sut, container } = createFixture();
container.register(...customElements);
const compiledDefinition = sut.compile(
CustomElementDefinition.create({ name: 'my-ce', template }, class MyCe { }),
container,
);
type HEI = HydrateElementInstruction;
const allInstructions = compiledDefinition.instructions.flat();
for (const expectedSlotInfo of expectedSlotInfos) {
const actualInstruction = allInstructions.find((i) =>
i.type === InstructionType.hydrateElement
&& ((i as HEI).res === 'au-slot'
|| (i as HEI).res === CustomElement.getDefinition(AuSlot)
)
&& expectedSlotInfo.slotName === (i as HEI).data.name
) as HydrateElementInstruction;
assert.notEqual(actualInstruction, void 0, 'instruction');
const slotFallback = actualInstruction.projections?.default;
assert.deepStrictEqual((slotFallback?.template as HTMLElement)?.outerHTML, `<template>${expectedSlotInfo.content}</template>`, 'content');
assert.deepStrictEqual(slotFallback?.needsCompile, false, 'needsCompile');
}
// for each element instruction found
// verify projections for it compiles properly
if (allExpectedProjections == null) {
return;
}
for (const [elName, projections] of allExpectedProjections) {
const elementInstruction = allInstructions.find(i =>
i.type === InstructionType.hydrateElement
&& (typeof (i as HEI).res === 'string' && ((i as HEI).res as string) === elName
|| (i as HEI).res === CustomElement.find(container, elName)
)
) as HydrateElementInstruction;
assert.notEqual(elementInstruction, void 0, `Instruction for element "${elName}" missing`);
const actualProjections = elementInstruction.projections;
for (const slotName in projections) {
const def = actualProjections[slotName];
// assert.instanceOf(def, CustomElementDefinition);
assert.deepStrictEqual((def.template as HTMLElement).outerHTML, `<template>${projections[slotName]}</template>`, 'content');
assert.deepStrictEqual(def.needsCompile, false, 'needsCompile');
}
}
});
}
function compileTemplate(template: string, ...registrations: unknown[]) {
const { container, sut } = createFixture();
container.register(...registrations);
const templateDefinition = CustomElementDefinition.create({
name: 'ano',
template,
instructions: [],
surrogates: [],
shadowOptions: { mode: 'open' },
});
const parser = container.get(IExpressionParser);
return {
...sut.compile(templateDefinition, container),
createProp: ({ from, to, mode = BindingMode.toView }: { from: string; to: string; mode?: BindingMode }) =>
new PropertyBindingInstruction(parser.parse(from, 'IsProperty'), to, mode),
createTextInterpolation: ({ from }: { from: string }) =>
new TextBindingInstruction(parser.parse(from, 'IsProperty')),
};
}
function assertTemplateEqual(template: string | Node, expected: string | Node, message?: string) {
assert.strictEqual(
typeof template === 'string' ? template : (template as Element).innerHTML,
typeof expected === 'string' ? expected : (expected as Element).innerHTML,
message
);
}
function assertAuSlotFallback(
instruction: IInstruction,
expectedAuslotFallback: null | Pick<CustomElementDefinition, 'template' | 'instructions'> & { name?: string },
message?: string,
) {
const $auslotInstruction = instruction as HydrateElementInstruction;
if (expectedAuslotFallback === null) {
assert.strictEqual($auslotInstruction.projections, null, `<au-slot>.projections === null`);
return;
}
const {
name: expectedName = 'default',
template: expectedTemplate,
instructions: expectedInstructions
} = expectedAuslotFallback ?? {};
const { data: { name }, projections: { default: { template, instructions } } = { default: {} } } = $auslotInstruction;
assert.strictEqual($auslotInstruction.type, InstructionType.hydrateElement, `#instruction.type ${message}`);
assert.strictEqual(name, expectedName, `#fallback.slotname ${message}`);
assertTemplateEqual(template, expectedTemplate, `#fallback.template ${message}`);
if (expectedInstructions !== anything) {
assert.deepStrictEqual(instructions, expectedInstructions, `#fallback.instructions ${message}`);
}
}
function assertProjection(
instruction: IInstruction,
expected: Record<string, Pick<CustomElementDefinition, 'template' | 'instructions'>>,
message?: string,
) {
const $instruction = instruction as HydrateElementInstruction;
const projections = $instruction.projections;
for (const slotName in expected) {
const projection = projections[slotName];
assert.notEqual(projection, null, `#projection@slot[${slotName}] is [null] - ${message}`);
const { template, instructions } = projection;
const { template: expectedTemplate, instructions: expectedInstructions } = expected[slotName];
assertTemplateEqual(template, expectedTemplate, `#projection@slot[${slotName}] ${message}`);
if (expectedInstructions !== anything) {
assert.deepStrictEqual(instructions, expectedInstructions, `#projection@slot[${slotName}] ${message}`);
}
}
}
});