Skip to content

Commit 82c4830

Browse files
committed
feat: named slots
1 parent d3235d7 commit 82c4830

File tree

4 files changed

+72
-47
lines changed

4 files changed

+72
-47
lines changed

Diff for: ct-web-lit/src/tests.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ test.fixme('update slots without remounting', async ({ mount }) => {
5454
await expect(component).toContainText('Default Slot');
5555

5656
await component.update({
57-
slots: { main: 'Test Slot' },
57+
slots: { main: '<div>Test Slot<div>' },
5858
});
5959
await expect(component).not.toContainText('Default Slot');
6060
await expect(component).toContainText('Test Slot');
@@ -107,12 +107,12 @@ test('render a component with multiple slots', async ({ mount }) => {
107107
await expect(component.getByTestId('two')).toContainText('Two');
108108
});
109109

110-
test.fixme('render a component with a named slot', async ({ mount }) => {
110+
test('render a component with a named slot', async ({ mount }) => {
111111
const component = await mount(NamedSlots, {
112112
slots: {
113-
header: 'Header',
114-
main: 'Main Content',
115-
footer: 'Footer',
113+
header: '<div slot="header">Header<div>', // slot="" is optional
114+
main: '<div>Main Content<div>',
115+
footer: '<div>Footer</div>',
116116
},
117117
});
118118
await expect(component).toContainText('Header');

Diff for: ct-web/src/tests.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ test.fixme('update slots without remounting', async ({ mount }) => {
5454
await expect(component).toContainText('Default Slot');
5555

5656
await component.update({
57-
slots: { main: 'Test Slot' },
57+
slots: { main: '<div>Test Slot</div>' },
5858
});
5959
await expect(component).not.toContainText('Default Slot');
6060
await expect(component).toContainText('Test Slot');
@@ -107,12 +107,12 @@ test('render a component with multiple slots', async ({ mount }) => {
107107
await expect(component.getByTestId('two')).toContainText('Two');
108108
});
109109

110-
test.fixme('render a component with a named slot', async ({ mount }) => {
110+
test('render a component with a named slot', async ({ mount }) => {
111111
const component = await mount(NamedSlots, {
112112
slots: {
113-
header: 'Header',
114-
main: 'Main Content',
115-
footer: 'Footer',
113+
header: '<div slot="header">Header<div>', // slot="" is optional
114+
main: '<div>Main Content<div>',
115+
footer: '<div>Footer</div>',
116116
},
117117
});
118118
await expect(component).toContainText('Header');

Diff for: playwright-ct-web/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sand4rt/experimental-ct-web",
3-
"version": "0.0.9",
3+
"version": "0.0.10",
44
"description": "Playwright Component Testing for Web Components",
55
"homepage": "https://playwright.dev",
66
"engines": {

Diff for: playwright-ct-web/registerSource.mjs

+61-36
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ export function register(components) {
1515
registry.set(name, value);
1616
}
1717

18+
/**
19+
* @param {ChildNode} webComponent
20+
*/
21+
function updateProps(webComponent, props = {}) {
22+
for (const [key, value] of Object.entries(props))
23+
webComponent[key] = value;
24+
}
25+
26+
/**
27+
* @param {ChildNode} webComponent
28+
*/
29+
function updateEvents(webComponent, events = {}) {
30+
for (const [key, listener] of Object.entries(events)) {
31+
webComponent.addEventListener(key, event =>
32+
listener(/** @type {CustomEvent} */ (event).detail)
33+
);
34+
}
35+
}
36+
1837
/**
1938
* @param {string} html
2039
* @return {DocumentFragment}
@@ -24,16 +43,38 @@ function stringToHtml(html) {
2443
}
2544

2645
/**
27-
* @param {string | string[]} slot
46+
* @param {ChildNode} webComponent
2847
*/
29-
function createSlots(slot) {
30-
if (typeof slot === 'string')
31-
return [stringToHtml(slot)];
32-
33-
if (Array.isArray(slot))
34-
return slot.map(stringToHtml);
48+
function updateSlots(webComponent, slots = {}) {
49+
for (const [key, value] of Object.entries(slots)) {
50+
let slotElements;
51+
if (typeof value === 'string')
52+
slotElements = [stringToHtml(value)];
53+
54+
if (Array.isArray(value))
55+
slotElements = value.map(stringToHtml);
56+
57+
if (!slotElements)
58+
throw new Error(`Invalid slot with the name: \`${key}\` supplied to \`mount()\``);
59+
60+
slotElements.forEach((fragment) => {
61+
const slotElement = fragment.firstChild;
62+
if (!slotElement)
63+
throw new Error(`Invalid slot with the name: \`${key}\` supplied to \`mount()\``);
64+
65+
if (key === 'default')
66+
return webComponent.appendChild(slotElement);
67+
68+
if (slotElement?.nodeName === '#text') {
69+
throw new Error(
70+
`Invalid slot with the name: \`${key}\` supplied to \`mount()\`, expected an HTMLElement but received a text node.`
71+
);
72+
}
3573

36-
throw Error(`Invalid slot received.`);
74+
slotElement['slot'] = key;
75+
webComponent.appendChild(fragment.firstChild);
76+
});
77+
}
3778
}
3879

3980
/**
@@ -62,53 +103,37 @@ function createComponent(component) {
62103
throw new Error('JSX mount notation is not supported');
63104

64105
const webComponent = new Component();
65-
66-
for (const [key, value] of Object.entries(component.options?.props || {}))
67-
webComponent[key] = value;
68-
69-
for (const [key, listener] of Object.entries(component.options?.on || {}))
70-
webComponent.addEventListener(key, event => listener(/** @type {CustomEvent} */ (event).detail));
71-
72-
for (const [key, value] of Object.entries(component.options?.slots || {})) {
73-
if (key !== 'default')
74-
throw new Error('named slots are not yet supported');
75-
76-
createSlots(value).forEach(slot => {
77-
webComponent.appendChild(slot);
78-
})
79-
}
80-
106+
updateProps(webComponent, component.options?.props);
107+
updateSlots(webComponent, component.options?.slots);
108+
updateEvents(webComponent, component.options?.on);
81109
return webComponent;
82110
}
83111

84112
window.playwrightUpdate = async (rootElement, component) => {
85113
if (component.kind === 'jsx')
86114
throw new Error('JSX mount notation is not supported');
87115

88-
if (component.options?.slots)
89-
throw new Error('slots in component.update() is not yet supported');
116+
const webComponent = rootElement.firstChild;
117+
if (!webComponent) throw new Error('Component was not mounted');
90118

91-
const wrapper = rootElement.firstChild;
92-
if (!wrapper)
93-
throw new Error('Component was not mounted');
94-
95-
for (const [key, value] of Object.entries(component.options?.props || {}))
96-
wrapper[key] = value;
119+
if (component.options?.slots)
120+
throw new Error('Slots in component.update() is not yet supported');
97121

98-
for (const [key, listener] of Object.entries(component.options?.on || {}))
99-
wrapper.addEventListener(key, event => listener(/** @type {CustomEvent} */ (event).detail));
122+
updateProps(webComponent, component.options?.props);
123+
updateSlots(webComponent, component.options?.slots);
124+
updateEvents(webComponent, component.options?.on);
100125
};
101126

102127
window.playwrightUnmount = async (rootElement) => {
103128
rootElement.replaceChildren();
104129
};
105130

106131
window.playwrightMount = async (component, rootElement, hooksConfig) => {
107-
for (const hook of /** @type {any} */ (window).__pw_hooks_before_mount || [])
132+
for (const hook of window['__pw_hooks_before_mount'] || [])
108133
await hook({ hooksConfig });
109134

110135
rootElement.appendChild(createComponent(component));
111136

112-
for (const hook of /** @type {any} */ (window).__pw_hooks_after_mount || [])
137+
for (const hook of window['__pw_hooks_after_mount'] || [])
113138
await hook({ hooksConfig });
114139
};

0 commit comments

Comments
 (0)