diff --git a/tests/components/ct-react/playwright/index.html b/tests/components/ct-react/playwright/index.html
index 7a654f3a79..4f2691ac84 100644
--- a/tests/components/ct-react/playwright/index.html
+++ b/tests/components/ct-react/playwright/index.html
@@ -1,9 +1,10 @@
-
+
-
-
-
+
+
+
+ React App
diff --git a/tests/components/ct-react/playwright/index.js b/tests/components/ct-react/playwright/index.js
index e69de29bb2..cbed78450e 100644
--- a/tests/components/ct-react/playwright/index.js
+++ b/tests/components/ct-react/playwright/index.js
@@ -0,0 +1,10 @@
+import '../src/assets/index.css';
+import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
+
+beforeMount(async ({ hooksConfig }) => {
+ console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
+});
+
+afterMount(async ({}) => {
+ console.log(`After mount`);
+});
diff --git a/tests/components/ct-react/src/App.spec.tsx b/tests/components/ct-react/src/App.spec.tsx
deleted file mode 100644
index 9d7c57d828..0000000000
--- a/tests/components/ct-react/src/App.spec.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { test, expect } from '@playwright/experimental-ct-react';
-import App from './App';
-
-test.use({ viewport: { width: 500, height: 500 } });
-
-test('should work', async ({ mount }) => {
- const component = await mount();
- await expect(component).toContainText('Learn React');
-});
diff --git a/tests/components/ct-react/src/DelayedData.spec.tsx b/tests/components/ct-react/src/DelayedData.spec.tsx
deleted file mode 100644
index e42c57f9c0..0000000000
--- a/tests/components/ct-react/src/DelayedData.spec.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { test, expect } from '@playwright/experimental-ct-react'
-import { DelayedData } from './DelayedData';
-
-test('toHaveText works on delayed data', async ({ mount }) => {
- const component = await mount();
- await expect(component).toHaveText('complete');
-});
diff --git a/tests/components/ct-react/src/DelayedData.tsx b/tests/components/ct-react/src/DelayedData.tsx
deleted file mode 100644
index 5eb7cb21f5..0000000000
--- a/tests/components/ct-react/src/DelayedData.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-export const DelayedData: React.FC<{ data: string }> = ({ data }) => {
- const [status, setStatus] = useState('loading');
-
- useEffect(() => {
- const timeout = setTimeout(() => setStatus(data), 500);
- return () => {
- clearTimeout(timeout);
- }
- }, [data])
-
- return {status}
-};
diff --git a/tests/components/ct-react/src/Events.spec.tsx b/tests/components/ct-react/src/Events.spec.tsx
deleted file mode 100644
index ec7a4e9b57..0000000000
--- a/tests/components/ct-react/src/Events.spec.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { test, expect } from '@playwright/experimental-ct-react';
-
-test('should marshall events', async ({ mount }) => {
- let event: any;
- const component = await mount();
- await component.click();
- expect(event).toEqual(expect.objectContaining({
- type: 'click',
- pageX: expect.any(Number),
- pageY: expect.any(Number),
- ctrlKey: false,
- isTrusted: true,
- }));
-});
diff --git a/tests/components/ct-react/src/Fetch.spec.tsx b/tests/components/ct-react/src/Fetch.spec.tsx
deleted file mode 100644
index 907051a018..0000000000
--- a/tests/components/ct-react/src/Fetch.spec.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { test as _test, expect } from '@playwright/experimental-ct-react'
-import { Fetch } from './Fetch';
-import { serverFixtures } from '../../../../tests/config/serverFixtures';
-
-const test = _test.extend(serverFixtures);
-
-test('components routing should go through context', async ({ mount, context, server }) => {
- server.setRoute('/hello', (req, res) => {
- res.write('served via server');
- res.end();
- });
-
- let markRouted: (url: string) => void;
- const routedViaContext = new Promise(res => markRouted = res);
- await context.route('**/hello', async (route, request) => {
- markRouted(`${request.method()} ${request.url()}`);
- await route.fulfill({
- body: 'intercepted',
- });
- });
-
- const whoServedTheRequest = Promise.race([
- server.waitForRequest('/hello').then((req) => `served via server: ${req.method} ${req.url}`),
- routedViaContext.then(req => `served via context: ${req}`),
- ]);
-
- const component = await mount();
- await expect.soft(whoServedTheRequest).resolves.toMatch(/served via context: GET.*\/hello.*/i);
- await expect.soft(component).toHaveText('intercepted');
-});
diff --git a/tests/components/ct-react/src/Fetch.tsx b/tests/components/ct-react/src/Fetch.tsx
deleted file mode 100644
index 06cf76f20f..0000000000
--- a/tests/components/ct-react/src/Fetch.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-export const Fetch: React.FC<{ url: string }> = ({ url }) => {
- const [data, setData] = useState('no response yet');
- useEffect(() => {
- fetch(url).then(res => res.text()).then(setData);
- }, [url]);
- return {data}
;
-}
diff --git a/tests/components/ct-react/src/index.css b/tests/components/ct-react/src/assets/index.css
similarity index 100%
rename from tests/components/ct-react/src/index.css
rename to tests/components/ct-react/src/assets/index.css
diff --git a/tests/components/ct-react/src/logo.svg b/tests/components/ct-react/src/assets/logo.svg
similarity index 100%
rename from tests/components/ct-react/src/logo.svg
rename to tests/components/ct-react/src/assets/logo.svg
diff --git a/tests/components/ct-react/src/components/Button.tsx b/tests/components/ct-react/src/components/Button.tsx
new file mode 100644
index 0000000000..78b0a7791f
--- /dev/null
+++ b/tests/components/ct-react/src/components/Button.tsx
@@ -0,0 +1,7 @@
+type ButtonProps = {
+ title: string;
+ onClick?(props: string): void;
+}
+export default function Button(props: ButtonProps) {
+ return
+}
diff --git a/tests/components/ct-react/src/components/DefaultChildren.tsx b/tests/components/ct-react/src/components/DefaultChildren.tsx
new file mode 100644
index 0000000000..691b6a0806
--- /dev/null
+++ b/tests/components/ct-react/src/components/DefaultChildren.tsx
@@ -0,0 +1,15 @@
+type DefaultChildrenProps = {
+ children?: any;
+}
+
+export default function DefaultChildren(props: DefaultChildrenProps) {
+ return
+
Welcome!
+
+ {props.children}
+
+
+
+}
diff --git a/tests/components/ct-react/src/components/DelayedData.tsx b/tests/components/ct-react/src/components/DelayedData.tsx
new file mode 100644
index 0000000000..6cbbe2618c
--- /dev/null
+++ b/tests/components/ct-react/src/components/DelayedData.tsx
@@ -0,0 +1,16 @@
+import { useEffect, useState } from 'react';
+
+type DelayedData = {
+ data: string;
+}
+
+export default function DelayedData(props: DelayedData) {
+ const [status, setStatus] = useState('loading');
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setStatus(props.data), 500);
+ return () => clearTimeout(timeout);
+ }, [props.data])
+
+ return {status}
+};
diff --git a/tests/components/ct-react/src/components/Fetch.tsx b/tests/components/ct-react/src/components/Fetch.tsx
new file mode 100644
index 0000000000..52be98ff4e
--- /dev/null
+++ b/tests/components/ct-react/src/components/Fetch.tsx
@@ -0,0 +1,13 @@
+import React, { useEffect, useState } from 'react';
+
+type FetchProps = {
+ url: string;
+}
+
+export default function Fetch(props: FetchProps) {
+ const [data, setData] = useState('no response yet');
+ useEffect(() => {
+ fetch(props.url).then(res => res.text()).then(setData);
+ }, [props.url]);
+ return {data}
;
+}
diff --git a/tests/components/ct-react/src/components/MultiRoot.tsx b/tests/components/ct-react/src/components/MultiRoot.tsx
new file mode 100644
index 0000000000..f29e397c0f
--- /dev/null
+++ b/tests/components/ct-react/src/components/MultiRoot.tsx
@@ -0,0 +1,6 @@
+export default function MultiRoot() {
+ return <>
+ root 1
+ root 2
+ >
+}
diff --git a/tests/components/ct-react/src/components/MultipleChildren.tsx b/tests/components/ct-react/src/components/MultipleChildren.tsx
new file mode 100644
index 0000000000..63bd0104c6
--- /dev/null
+++ b/tests/components/ct-react/src/components/MultipleChildren.tsx
@@ -0,0 +1,18 @@
+
+type MultipleChildrenProps = {
+ children?: [any, any, any];
+}
+
+export default function MultipleChildren(props: MultipleChildrenProps) {
+ return
+
+ {props.children?.at(0)}
+
+
+ {props.children?.at(1)}
+
+
+
+}
diff --git a/tests/components/ct-react/src/index.tsx b/tests/components/ct-react/src/main.tsx
similarity index 100%
rename from tests/components/ct-react/src/index.tsx
rename to tests/components/ct-react/src/main.tsx
diff --git a/tests/components/ct-react/src/tests.spec.tsx b/tests/components/ct-react/src/tests.spec.tsx
new file mode 100644
index 0000000000..ab94e0d8db
--- /dev/null
+++ b/tests/components/ct-react/src/tests.spec.tsx
@@ -0,0 +1,118 @@
+import { test, expect } from '@playwright/experimental-ct-react'
+import { serverFixtures } from '../../../../tests/config/serverFixtures';
+import Fetch from './components/Fetch';
+import DelayedData from './components/DelayedData';
+import Button from './components/Button';
+import DefaultChildren from './components/DefaultChildren';
+import MultipleChildren from './components/MultipleChildren';
+import MultiRoot from './components/MultiRoot';
+
+test.use({ viewport: { width: 500, height: 500 } });
+
+test('props should work', async ({ mount }) => {
+ const component = await mount();
+ await expect(component).toContainText('Submit');
+});
+
+test('callback should work', async ({ mount }) => {
+ const messages: string[] = []
+ const component = await mount()
+ await component.click()
+ expect(messages).toEqual(['hello'])
+})
+
+test('default slot should work', async ({ mount }) => {
+ const component = await mount(
+ Main Content
+ )
+ await expect(component).toContainText('Main Content')
+})
+
+test('multiple children should work', async ({ mount }) => {
+ const component = await mount(
+ One
+ Two
+ )
+ await expect(component.locator('#one')).toContainText('One')
+ await expect(component.locator('#two')).toContainText('Two')
+})
+
+test('named children should work', async ({ mount }) => {
+ const component = await mount(
+ Header
+ Main Content
+ Footer
+ );
+ await expect(component).toContainText('Header')
+ await expect(component).toContainText('Main Content')
+ await expect(component).toContainText('Footer')
+})
+
+test('children should callback', async ({ mount }) => {
+ let clickFired = false;
+ const component = await mount(
+ clickFired = true}>Main Content
+ );
+ await component.locator('text=Main Content').click();
+ expect(clickFired).toBeTruthy();
+})
+
+test('should run hooks', async ({ page, mount }) => {
+ const messages: string[] = [];
+ page.on('console', m => messages.push(m.text()));
+ await mount(, {
+ hooksConfig: {
+ route: 'A'
+ }
+ });
+ expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']);
+});
+
+test('should unmount', async ({ page, mount }) => {
+ const component = await mount()
+ await expect(page.locator('#root')).toContainText('Submit')
+ await component.unmount();
+ await expect(page.locator('#root')).not.toContainText('Submit');
+});
+
+test('unmount a multi root component should work', async ({ mount, page }) => {
+ const component = await mount()
+ await expect(page.locator('#root')).toContainText('root 1')
+ await expect(page.locator('#root')).toContainText('root 2')
+ await component.unmount()
+ await expect(page.locator('#root')).not.toContainText('root 1')
+ await expect(page.locator('#root')).not.toContainText('root 2')
+})
+
+test('toHaveText works on delayed data', async ({ mount }) => {
+ const component = await mount();
+ await expect(component).toHaveText('complete');
+});
+
+const testWithServer = test.extend(serverFixtures);
+testWithServer('components routing should go through context', async ({ mount, context, server }) => {
+ server.setRoute('/hello', (req, res) => {
+ res.write('served via server');
+ res.end();
+ });
+
+ let markRouted: (url: string) => void;
+ const routedViaContext = new Promise(res => markRouted = res);
+ await context.route('**/hello', async (route, request) => {
+ markRouted(`${request.method()} ${request.url()}`);
+ await route.fulfill({
+ body: 'intercepted',
+ });
+ });
+
+ const whoServedTheRequest = Promise.race([
+ server.waitForRequest('/hello').then((req) => `served via server: ${req.method} ${req.url}`),
+ routedViaContext.then(req => `served via context: ${req}`),
+ ]);
+
+ const component = await mount();
+ await expect.soft(whoServedTheRequest).resolves.toMatch(/served via context: GET.*\/hello.*/i);
+ await expect.soft(component).toHaveText('intercepted');
+});