feat(ct): before mount hook wrapper (#18616)

This commit is contained in:
Sander 2022-12-27 23:26:17 +01:00 committed by GitHub
parent c1b9a56079
commit ba393f51a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 145 additions and 124 deletions

View File

@ -19,7 +19,7 @@ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[]; type JsonArray = JsonValue[];
type JsonObject = { [Key in string]?: JsonValue }; type JsonObject = { [Key in string]?: JsonValue };
export declare function beforeMount<HooksConfig extends JsonObject>( export declare function beforeMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void> callback: (params: { hooksConfig: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
): void; ): void;
export declare function afterMount<HooksConfig extends JsonObject>( export declare function afterMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void> callback: (params: { hooksConfig: HooksConfig }) => Promise<void>

View File

@ -36,6 +36,7 @@ export function register(components) {
/** /**
* @param {Component} component * @param {Component} component
* @returns {JSX.Element}
*/ */
function render(component) { function render(component) {
let componentFunc = registry.get(component.type); let componentFunc = registry.get(component.type);
@ -69,10 +70,14 @@ function render(component) {
} }
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || []) let App = () => render(component);
await hook({ hooksConfig }); for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig });
if (wrapper)
App = () => wrapper;
}
ReactDOM.render(render(component), rootElement); ReactDOM.render(App(), rootElement);
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || []) for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ hooksConfig }); await hook({ hooksConfig });

View File

@ -14,12 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { JSXElement } from "solid-js";
type JsonPrimitive = string | number | boolean | null; type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray; type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[]; type JsonArray = JsonValue[];
type JsonObject = { [Key in string]?: JsonValue }; type JsonObject = { [Key in string]?: JsonValue };
export declare function beforeMount<HooksConfig extends JsonObject>( export declare function beforeMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void> callback: (params: { hooksConfig: HooksConfig, App: () => JSXElement }) => Promise<void | JSXElement>
): void; ): void;
export declare function afterMount<HooksConfig extends JsonObject>( export declare function afterMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void> callback: (params: { hooksConfig: HooksConfig }) => Promise<void>

View File

@ -78,10 +78,14 @@ function createComponent(component) {
const unmountKey = Symbol('unmountKey'); const unmountKey = Symbol('unmountKey');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || []) let App = () => createComponent(component);
await hook({ hooksConfig }); for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig });
if (wrapper)
App = () => wrapper;
}
const unmount = solidRender(() => createComponent(component), rootElement); const unmount = solidRender(App, rootElement);
rootElement[unmountKey] = unmount; rootElement[unmountKey] = unmount;
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || []) for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])

View File

@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View File

@ -1,13 +1,17 @@
//@ts-check
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';
import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ hooksConfig }) => { beforeMount<HooksConfig>(async ({ hooksConfig, App }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
if (hooksConfig?.routing)
return <BrowserRouter><App /></BrowserRouter>;
}); });
afterMount<HooksConfig>(async () => { afterMount<HooksConfig>(async () => {

View File

@ -1,5 +1,4 @@
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import Button from './components/Button'; import Button from './components/Button';
import DefaultChildren from './components/DefaultChildren'; import DefaultChildren from './components/DefaultChildren';
@ -143,7 +142,9 @@ test('get textContent of the empty fragment', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(<BrowserRouter><App /></BrowserRouter>); const component = await mount<HooksConfig>(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View File

@ -1,12 +1,17 @@
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';
import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ hooksConfig }) => { beforeMount<HooksConfig>(async ({ hooksConfig, App }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
if (hooksConfig?.routing)
return <BrowserRouter><App /></BrowserRouter>;
}); });
afterMount<HooksConfig>(async () => { afterMount<HooksConfig>(async () => {

View File

@ -1,6 +1,5 @@
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
const { serverFixtures } = require('../../../../tests/config/serverFixtures'); const { serverFixtures } = require('../../../../tests/config/serverFixtures');
import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import Fetch from './components/Fetch'; import Fetch from './components/Fetch';
import DelayedData from './components/DelayedData'; import DelayedData from './components/DelayedData';
@ -151,7 +150,9 @@ test('get textContent of the empty fragment', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(<BrowserRouter><App /></BrowserRouter>); const component = await mount<HooksConfig>(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -10,14 +10,15 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"license": "MIT", "license": "MIT",
"dependencies": {
"@solidjs/router": "^0.5.0",
"solid-js": "^1.4.7"
},
"devDependencies": { "devDependencies": {
"typescript": "^4.7.4", "typescript": "^4.7.4",
"vite": "^4.0.3", "vite": "^4.0.3",
"vite-plugin-solid": "^2.5.0" "vite-plugin-solid": "^2.5.0"
}, },
"dependencies": {
"solid-js": "^1.4.7"
},
"@standaloneDevDependencies": { "@standaloneDevDependencies": {
"@playwright/experimental-ct-solid": "^1.22.2", "@playwright/experimental-ct-solid": "^1.22.2",
"@playwright/test": "^1.22.2" "@playwright/test": "^1.22.2"

View File

@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View File

@ -1,12 +1,17 @@
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks'; import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks';
import { Router } from "@solidjs/router";
import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ hooksConfig }) => { beforeMount<HooksConfig>(async ({ hooksConfig, App }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
if (hooksConfig?.routing)
return <Router><App /></Router>;
}); });
afterMount<HooksConfig>(async () => { afterMount<HooksConfig>(async () => {

View File

@ -1,33 +0,0 @@
.App {
text-align: center;
}
.logo {
animation: logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.link {
color: #b318f0;
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,27 +1,20 @@
import type { Component } from 'solid-js'; import { Routes, Route, A } from "@solidjs/router"
import logo from './assets/logo.svg'; import logo from './assets/logo.svg';
import styles from './App.module.css'; import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage';
const App: Component = () => { export default function App() {
return ( return <>
<div class={styles.App}> <header>
<header class={styles.header}> <img src={logo} alt="logo" width={125} height={125} />
<img src={logo} class={styles.logo} alt="logo" /> <A href="/">Login</A>
<p> <A href="/dashboard">Dashboard</A>
Edit <code>src/App.tsx</code> and save to reload. </header>
</p> <Routes>
<a <Route path="/">
class={styles.link} <Route path="/" component={LoginPage} />
href="https://github.com/solidjs/solid" <Route path="dashboard" component={DashboardPage} />
target="_blank" </Route>
rel="noopener noreferrer" </Routes>
> </>
Learn Solid
</a>
</header>
</div>
);
}; };
export default App;

View File

@ -1,6 +1,7 @@
/* @refresh reload */ /* @refresh reload */
import { render } from 'solid-js/web'; import { render } from 'solid-js/web';
import { Router } from "@solidjs/router";
import App from './App'; import App from './App';
import './assets/index.css'; import './assets/index.css';
render(() => <App />, document.getElementById('root') as HTMLElement); render(() => <Router><App /></Router>, document.getElementById('root')!);

View File

@ -0,0 +1,3 @@
export default function DashboardPage() {
return <main>Dashboard</main>
}

View File

@ -0,0 +1,3 @@
export default function LoginPage() {
return <main>Login</main>
}

View File

@ -1,4 +1,5 @@
import { test, expect } from '@playwright/experimental-ct-solid'; import { test, expect } from '@playwright/experimental-ct-solid';
import App from './App';
import Button from './components/Button'; import Button from './components/Button';
import Counter from './components/Counter'; import Counter from './components/Counter';
import DefaultChildren from './components/DefaultChildren'; import DefaultChildren from './components/DefaultChildren';
@ -81,19 +82,15 @@ test('execute callback when the button is clicked', async ({ mount }) => {
}); });
test('render a default child', async ({ mount }) => { test('render a default child', async ({ mount }) => {
const component = await mount( const component = await mount(<DefaultChildren>Main Content</DefaultChildren>);
<DefaultChildren>Main Content</DefaultChildren>
);
await expect(component).toContainText('Main Content'); await expect(component).toContainText('Main Content');
}); });
test('render multiple children', async ({ mount }) => { test('render multiple children', async ({ mount }) => {
const component = await mount( const component = await mount(<DefaultChildren>
<DefaultChildren> <div id="one">One</div>
<div id="one">One</div> <div id="two">Two</div>
<div id="two">Two</div> </DefaultChildren>);
</DefaultChildren>
);
await expect(component.locator('#one')).toContainText('One'); await expect(component.locator('#one')).toContainText('One');
await expect(component.locator('#two')).toContainText('Two'); await expect(component.locator('#two')).toContainText('Two');
}); });
@ -107,13 +104,11 @@ test('render a component as slot', async ({ mount }) => {
}); });
test('render named children', async ({ mount }) => { test('render named children', async ({ mount }) => {
const component = await mount( const component = await mount(<MultipleChildren>
<MultipleChildren> <div>Header</div>
<div>Header</div> <div>Main Content</div>
<div>Main Content</div> <div>Footer</div>
<div>Footer</div> </MultipleChildren>);
</MultipleChildren>
);
await expect(component).toContainText('Header'); await expect(component).toContainText('Header');
await expect(component).toContainText('Main Content'); await expect(component).toContainText('Main Content');
await expect(component).toContainText('Footer'); await expect(component).toContainText('Footer');
@ -121,11 +116,9 @@ test('render named children', async ({ mount }) => {
test('execute callback when a child node is clicked', async ({ mount }) => { test('execute callback when a child node is clicked', async ({ mount }) => {
let clickFired = false; let clickFired = false;
const component = await mount( const component = await mount(<DefaultChildren>
<DefaultChildren> <span onClick={() => (clickFired = true)}>Main Content</span>
<span onClick={() => (clickFired = true)}>Main Content</span> </DefaultChildren>);
</DefaultChildren>
);
await component.locator('text=Main Content').click(); await component.locator('text=Main Content').click();
expect(clickFired).toBeTruthy(); expect(clickFired).toBeTruthy();
}); });
@ -163,3 +156,14 @@ test('get textContent of the empty fragment', async ({ mount }) => {
expect(await component.textContent()).toBe(''); expect(await component.textContent()).toBe('');
await expect(component).toHaveText(''); await expect(component).toHaveText('');
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount<HooksConfig>(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click();
await expect(component.getByRole('main')).toHaveText('Dashboard');
await expect(page).toHaveURL('/dashboard');
});

View File

@ -3,11 +3,13 @@ import { router } from '../src/router';
import '../src/assets/index.css'; import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => { beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
app.use(router); if (hooksConfig?.routing)
app.use(router);
console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`);
}); });

View File

@ -122,7 +122,9 @@ test('get textContent of the empty template', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(<App />); const component = await mount<HooksConfig>(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -128,7 +128,9 @@ test('get textContent of the empty template', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(App); const component = await mount<HooksConfig>(App, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -4,11 +4,13 @@ import Button from '../src/components/Button.vue';
import '../src/assets/index.css'; import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => { beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
app.use(router as any); // TODO: remove any and fix the various installed conflicting Vue versions if (hooksConfig?.routing)
app.use(router as any); // TODO: remove any and fix the various installed conflicting Vue versions
app.component('Button', Button); app.component('Button', Button);
console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`);
}); });

View File

@ -143,8 +143,10 @@ test('get textContent of the empty template', async ({ mount }) => {
await expect(component).toHaveText(''); await expect(component).toHaveText('');
}); });
test('render app and navigate to a page', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(App); const component = await mount<HooksConfig>(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -159,8 +159,10 @@ test('get textContent of the empty template', async ({ mount }) => {
await expect(component).toHaveText(''); await expect(component).toHaveText('');
}); });
test('render app and navigate to a page', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(App); const component = await mount(App, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -167,8 +167,10 @@ test('get textContent of the empty template', async ({ mount }) => {
await expect(component).toHaveText(''); await expect(component).toHaveText('');
}); });
test('render app and navigate to a page', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(App); const component = await mount<HooksConfig>(App, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -4,13 +4,17 @@ import { router } from '../src/router';
import '../src/assets/index.css'; import '../src/assets/index.css';
export type HooksConfig = { export type HooksConfig = {
route: string; route?: string;
routing?: boolean;
} }
beforeMount<HooksConfig>(async ({ Vue, hooksConfig }) => { beforeMount<HooksConfig>(async ({ Vue, hooksConfig }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`); console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
Vue.use(Router as any); // TODO: remove any and fix the various installed conflicting Vue versions
return { router } if (hooksConfig?.routing) {
Vue.use(Router as any); // TODO: remove any and fix the various installed conflicting Vue versions
return { router }
}
}); });
afterMount<HooksConfig>(async ({ instance }) => { afterMount<HooksConfig>(async ({ instance }) => {

View File

@ -143,7 +143,9 @@ test('get textContent of the empty template', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(<App />); const component = await mount(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();

View File

@ -152,7 +152,9 @@ test('get textContent of the empty template', async ({ mount }) => {
}); });
test('navigate to a page by clicking a link', async ({ page, mount }) => { test('navigate to a page by clicking a link', async ({ page, mount }) => {
const component = await mount(App); const component = await mount(App, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login'); await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/'); await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click(); await component.getByRole('link', { name: 'Dashboard' }).click();