;
+
+ componentWillLoad() {
+ return new Promise((resolve) => setTimeout(resolve, 20));
+ }
+
+ selectCar(car: CarData) {
+ this.selected = car;
+ this.carSelected.emit(car);
+ }
+
+ render() {
+ if (!Array.isArray(this.cars)) {
+ return null;
+ }
+
+ return (
+
+ {this.cars.map((car) => {
+ return (
+ -
+
+
+ );
+ })}
+
+ );
+ }
+}
diff --git a/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx
new file mode 100644
index 000000000000..f1693706e9c2
--- /dev/null
+++ b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx
@@ -0,0 +1,17 @@
+import { Component, h, Prop, State } from '@stencil/core';
+
+@Component({
+ tag: 'cmp-dsd',
+ shadow: true,
+})
+export class ComponentDSD {
+ @State()
+ counter = 0;
+
+ @Prop()
+ initialCounter = 0;
+
+ render() {
+ return ;
+ }
+}
diff --git a/test/end-to-end/src/declarative-shadow-dom/readme.md b/test/end-to-end/src/declarative-shadow-dom/readme.md
new file mode 100644
index 000000000000..763587e9cbbf
--- /dev/null
+++ b/test/end-to-end/src/declarative-shadow-dom/readme.md
@@ -0,0 +1,10 @@
+# cmp-server-vs-client
+
+
+
+
+
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/test/end-to-end/src/declarative-shadow-dom/server-vs-client.tsx b/test/end-to-end/src/declarative-shadow-dom/server-vs-client.tsx
new file mode 100644
index 000000000000..37eefd2ffc1b
--- /dev/null
+++ b/test/end-to-end/src/declarative-shadow-dom/server-vs-client.tsx
@@ -0,0 +1,12 @@
+import { Component, h } from '@stencil/core';
+
+@Component({
+ tag: 'cmp-server-vs-client',
+ shadow: true,
+})
+export class ServerVSClientCmp {
+ render() {
+ const winner = globalThis.constructor.name === 'MockWindow' ? 'Server' : 'Client';
+ return Server vs Client? Winner: {winner}
;
+ }
+}
diff --git a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts
new file mode 100644
index 000000000000..003d0c97a4bb
--- /dev/null
+++ b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts
@@ -0,0 +1,196 @@
+import { Readable } from 'node:stream';
+
+import { newE2EPage } from '@stencil/core/testing';
+
+import { CarData } from '../car-list/car-data';
+
+const vento = new CarData('VW', 'Vento', 2024);
+const beetle = new CarData('VW', 'Beetle', 2023);
+
+async function readableToString(readable: Readable) {
+ return new Promise((resolve, reject) => {
+ let data = '';
+
+ readable.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ readable.on('end', () => {
+ resolve(data);
+ });
+
+ readable.on('error', (err) => {
+ reject(err);
+ });
+ });
+}
+
+jest.setTimeout(3000000);
+
+// @ts-ignore may not be existing when project hasn't been built
+type HydrateModule = typeof import('../../hydrate');
+let renderToString: HydrateModule['renderToString'];
+let streamToString: HydrateModule['streamToString'];
+
+describe('renderToString', () => {
+ beforeAll(async () => {
+ // @ts-ignore may not be existing when project hasn't been built
+ const mod = await import('../../hydrate');
+ renderToString = mod.renderToString;
+ streamToString = mod.streamToString;
+ });
+
+ it('can render a simple dom node', async () => {
+ const { html } = await renderToString('Hello World
');
+ expect(html).toContain('Hello World
');
+ });
+
+ it('can render a simple dom node (sync)', async () => {
+ const input = 'Hello World
';
+ const renderedHTML = 'Hello World
';
+ const stream = renderToString(input, {}, true);
+ expect(await readableToString(stream)).toContain(renderedHTML);
+ expect(await readableToString(streamToString(input))).toContain(renderedHTML);
+ });
+
+ it('renders scoped component', async () => {
+ const { html } = await renderToString('', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+ expect(html).toMatchSnapshot();
+ });
+
+ it('supports passing props to components', async () => {
+ const { html } = await renderToString(
+ '',
+ {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ },
+ );
+ expect(html).toMatchSnapshot();
+ expect(html).toContain('2024 VW Vento');
+ });
+
+ it('supports passing props to components with a simple object', async () => {
+ const { html } = await renderToString(``, {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+ expect(html).toMatchSnapshot();
+ expect(html).toContain('2024 VW Vento');
+ });
+
+ it('does not fail if provided object is not a valid JSON', async () => {
+ const { html } = await renderToString(
+ ``,
+ {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ },
+ );
+ expect(html).toContain('');
+ });
+
+ it('supports styles for DSD', async () => {
+ const { html } = await renderToString('', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+ expect(html).toContain('section.sc-another-car-detail{color:green}');
+ });
+
+ it('only returns the element if we render to DSD', async () => {
+ const { html } = await renderToString('Hello World
', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+ expect(html).toBe('Hello World
');
+ });
+
+ it('can render nested components', async () => {
+ const { html } = await renderToString(
+ ``,
+ {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ },
+ );
+ expect(html).toMatchSnapshot();
+ expect(html).toContain('2024 VW Vento');
+ expect(html).toContain('2023 VW Beetle');
+ });
+
+ it('can render a scoped component within a shadow component', async () => {
+ const { html } = await renderToString(``, {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+ expect(html).toMatchSnapshot();
+ expect(html).toContain(
+ '',
+ );
+ expect(html).toContain(
+ '',
+ );
+ });
+
+ it('can render a scoped component within a shadow component (sync)', async () => {
+ const input = ``;
+ const expectedResults = [
+ '',
+ '',
+ ] as const;
+ const opts = {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ };
+
+ const resultRenderToString = await readableToString(renderToString(input, opts, true));
+ expect(resultRenderToString).toContain(expectedResults[0]);
+ expect(resultRenderToString).toContain(expectedResults[1]);
+
+ const resultStreamToString = await readableToString(streamToString(input, opts));
+ expect(resultStreamToString).toContain(expectedResults[0]);
+ expect(resultStreamToString).toContain(expectedResults[1]);
+ });
+
+ it('can take over a server side rendered component and re-render it in the browser', async () => {
+ const { html } = await renderToString('', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+
+ expect(html).toContain('Count me: 0!');
+ const page = await newE2EPage({ html, url: 'https://stencil.com' });
+ const button = await page.find('cmp-dsd >>> button');
+ await button.click();
+ expect(button).toEqualText('Count me: 1!');
+ });
+
+ it('can take over a server side rendered component and re-render it in the browser with applied prop', async () => {
+ const { html } = await renderToString('', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+
+ expect(html).toContain('Count me: 42!');
+ const page = await newE2EPage({ html, url: 'https://stencil.com' });
+ const button = await page.find('cmp-dsd >>> button');
+ await button.click();
+ expect(button).toEqualText('Count me: 43!');
+ });
+
+ it('can render server side component when client sender renders differently', async () => {
+ const { html } = await renderToString('', {
+ serializeShadowRoot: true,
+ fullDocument: false,
+ });
+
+ expect(html).toContain('Server vs Client? Winner: Server');
+ const page = await newE2EPage({ html, url: 'https://stencil.com' });
+ const button = await page.find('cmp-server-vs-client');
+ expect(button.shadowRoot.querySelector('div')).toEqualText('Server vs Client? Winner: Client');
+ });
+});
diff --git a/test/end-to-end/src/prerender-cmp/prerender-cmp.tsx b/test/end-to-end/src/prerender-cmp/prerender-cmp.tsx
index b3a0f3f3625a..d774e0973967 100644
--- a/test/end-to-end/src/prerender-cmp/prerender-cmp.tsx
+++ b/test/end-to-end/src/prerender-cmp/prerender-cmp.tsx
@@ -1,4 +1,5 @@
import { Component, h } from '@stencil/core';
+
import styles from './prerender-cmp.css';
@Component({
diff --git a/test/end-to-end/stencil.config.ts b/test/end-to-end/stencil.config.ts
index 4f0e05fe8fad..e810f3266ea0 100644
--- a/test/end-to-end/stencil.config.ts
+++ b/test/end-to-end/stencil.config.ts
@@ -1,9 +1,10 @@
-import { Config } from '../../internal';
-import builtins from 'rollup-plugin-node-builtins';
-import linaria from 'linaria/rollup';
-import css from 'rollup-plugin-css-only';
import { reactOutputTarget } from '@stencil/react-output-target';
+import linaria from 'linaria/rollup';
import path from 'path';
+import css from 'rollup-plugin-css-only';
+import builtins from 'rollup-plugin-node-builtins';
+
+import { Config } from '../../internal';
export const config: Config = {
namespace: 'EndToEnd',
diff --git a/test/end-to-end/tsconfig.json b/test/end-to-end/tsconfig.json
index 4084a890fbef..c67ece8da0cc 100644
--- a/test/end-to-end/tsconfig.json
+++ b/test/end-to-end/tsconfig.json
@@ -9,6 +9,7 @@
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
"lib": [
"dom",
"es2017"
diff --git a/test/wdio/declarative-shadow-dom/cmp.test.tsx b/test/wdio/declarative-shadow-dom/cmp.test.tsx
new file mode 100644
index 000000000000..0d04224debf0
--- /dev/null
+++ b/test/wdio/declarative-shadow-dom/cmp.test.tsx
@@ -0,0 +1,17 @@
+import { render } from '@wdio/browser-runner/stencil';
+
+import { renderToString } from '../hydrate/index.mjs';
+
+describe('dsd-cmp', () => {
+ it('verifies that Stencil properly picks up the Declarative Shadow DOM', async () => {
+ const { html } = await renderToString(``, {
+ fullDocument: true,
+ serializeShadowRoot: true,
+ constrainTimeouts: false,
+ });
+
+ expect(html).toContain('I am rendered on the Server!');
+ render({ html });
+ await expect($('dsd-cmp')).toHaveText('I am rendered on the Client!');
+ });
+});
diff --git a/test/wdio/declarative-shadow-dom/cmp.tsx b/test/wdio/declarative-shadow-dom/cmp.tsx
new file mode 100644
index 000000000000..e815e9d855cb
--- /dev/null
+++ b/test/wdio/declarative-shadow-dom/cmp.tsx
@@ -0,0 +1,12 @@
+import { Component, h } from '@stencil/core';
+
+@Component({
+ tag: 'dsd-cmp',
+ shadow: true,
+})
+export class DsdComponent {
+ render() {
+ const env = globalThis.constructor.name === 'MockWindow' ? 'Server' : 'Client';
+ return I am rendered on the {env}!
;
+ }
+}
diff --git a/test/wdio/prerender-test/cmp.test.tsx b/test/wdio/prerender-test/cmp.test.tsx
index 142ce61dba7c..f8a8755fd7c2 100644
--- a/test/wdio/prerender-test/cmp.test.tsx
+++ b/test/wdio/prerender-test/cmp.test.tsx
@@ -105,7 +105,10 @@ describe('prerender', () => {
expect(scopedStyle.color).toBe('rgb(255, 0, 0)');
const shadow = iframe.querySelector('cmp-client-shadow');
- const shadowStyle = getComputedStyle(shadow.shadowRoot.querySelector('article'));
+ await browser.waitUntil(async () => shadow.shadowRoot.querySelector('article'));
+ const article = shadow.shadowRoot.querySelector('article');
+
+ const shadowStyle = getComputedStyle(article);
await browser.waitUntil(async () => shadowStyle.color === 'rgb(0, 155, 0)');
expect(shadowStyle.color).toBe('rgb(0, 155, 0)');
diff --git a/test/wdio/slot-reorder/cmp.test.tsx b/test/wdio/slot-reorder/cmp.test.tsx
index a88b71f3f86d..6060208c55db 100644
--- a/test/wdio/slot-reorder/cmp.test.tsx
+++ b/test/wdio/slot-reorder/cmp.test.tsx
@@ -20,10 +20,8 @@ describe('slot-reorder', () => {
});
it('renders', async () => {
- let r: HTMLElement;
-
function ordered() {
- r = document.querySelector('.results1 div');
+ let r = document.querySelector('.results1 div');
expect(r.children[0].textContent.trim()).toBe('fallback default');
expect(r.children[0].hasAttribute('hidden')).toBe(false);
expect(r.children[0].getAttribute('name')).toBe(null);
@@ -76,7 +74,7 @@ describe('slot-reorder', () => {
}
function reordered() {
- r = document.querySelector('.results1 div');
+ let r = document.querySelector('.results1 div');
expect(r.children[0].textContent.trim()).toBe('fallback slot-b');
expect(r.children[0].hasAttribute('hidden')).toBe(false);
expect(r.children[0].getAttribute('name')).toBe('slot-b');
@@ -128,6 +126,7 @@ describe('slot-reorder', () => {
expect(r.children[5].textContent.trim()).toBe('slot-a content');
}
+ await $('.results1 div').waitForExist();
ordered();
await $('button').click();
@@ -135,6 +134,7 @@ describe('slot-reorder', () => {
return document.querySelector('div.reordered');
});
+ await $('.results1 div').waitForExist();
reordered();
await $('button').click();
@@ -142,6 +142,7 @@ describe('slot-reorder', () => {
return !document.querySelector('div.reordered');
});
+ await $('.results1 div').waitForExist();
ordered();
await $('button').click();
@@ -149,6 +150,7 @@ describe('slot-reorder', () => {
return document.querySelector('div.reordered');
});
+ await $('.results1 div').waitForExist();
reordered();
});
});
diff --git a/test/wdio/stencil.config.ts b/test/wdio/stencil.config.ts
index 50fc5ac31b85..d4454c70fd2a 100644
--- a/test/wdio/stencil.config.ts
+++ b/test/wdio/stencil.config.ts
@@ -15,6 +15,10 @@ export const config: Config = {
customElementsExportBehavior: 'bundle',
isPrimaryPackageOutputTarget: true,
},
+ {
+ type: 'dist-hydrate-script',
+ dir: 'hydrate',
+ },
],
plugins: [sass()],
buildDist: true,
diff --git a/test/wdio/test-prerender/prerender.js b/test/wdio/test-prerender/prerender.js
index 24cc100564c6..c5989f955bf0 100644
--- a/test/wdio/test-prerender/prerender.js
+++ b/test/wdio/test-prerender/prerender.js
@@ -11,8 +11,8 @@ async function run() {
});
const filePath = path.join(__dirname, '..', 'www-prerender-script', 'prerender', 'index.html');
- const updatedHTML = results.html.replace(/(href|src)="\/prerender\//g, (a) =>
- a.replace('/prerender/', '/www-prerender-script/prerender/'),
+ const updatedHTML = results.html.replace(/(href|src)="\/prerender\//g, (html) =>
+ html.replace('/prerender/', '/www-prerender-script/prerender/'),
);
fs.writeFileSync(filePath, updatedHTML);
diff --git a/test/wdio/util.ts b/test/wdio/util.ts
index 6d20aedd0641..c225a507883a 100644
--- a/test/wdio/util.ts
+++ b/test/wdio/util.ts
@@ -13,11 +13,13 @@ export declare namespace SomeTypes {
}
export async function setupIFrameTest(htmlFile: string): Promise {
- if (document.querySelector('iframe')) {
- document.body.removeChild(document.querySelector('iframe'));
+ const oldFrame = document.querySelector('iframe');
+ if (oldFrame) {
+ document.body.removeChild(oldFrame);
}
const htmlFilePath = path.resolve(
+ // @ts-ignore globalThis is a WebdriverIO global variable
path.dirname(globalThis.__wdioSpec__),
'..',
htmlFile.slice(htmlFile.startsWith('/') ? 1 : 0),
@@ -37,5 +39,5 @@ export async function setupIFrameTest(htmlFile: string): Promise {
* wait for the iframe to load
*/
await new Promise((resolve) => (iframe.onload = resolve));
- return iframe.contentDocument.body;
+ return iframe.contentDocument!.body;
}