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 JsonObject = { [Key in string]?: JsonValue };
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;
export declare function afterMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,17 @@
//@ts-check
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';
import '../src/assets/index.css';
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)}`);
if (hooksConfig?.routing)
return <BrowserRouter><App /></BrowserRouter>;
});
afterMount<HooksConfig>(async () => {

View File

@ -1,5 +1,4 @@
import { test, expect } from '@playwright/experimental-ct-react';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import Button from './components/Button';
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 }) => {
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(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click();

View File

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

View File

@ -1,12 +1,17 @@
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';
import '../src/assets/index.css';
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)}`);
if (hooksConfig?.routing)
return <BrowserRouter><App /></BrowserRouter>;
});
afterMount<HooksConfig>(async () => {

View File

@ -1,6 +1,5 @@
import { test, expect } from '@playwright/experimental-ct-react';
const { serverFixtures } = require('../../../../tests/config/serverFixtures');
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import Fetch from './components/Fetch';
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 }) => {
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(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click();

View File

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

View File

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

View File

@ -1,12 +1,17 @@
import '../src/assets/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks';
import { Router } from "@solidjs/router";
import '../src/assets/index.css';
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)}`);
if (hooksConfig?.routing)
return <Router><App /></Router>;
});
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 styles from './App.module.css';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage';
const App: Component = () => {
return (
<div class={styles.App}>
<header class={styles.header}>
<img src={logo} class={styles.logo} alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
class={styles.link}
href="https://github.com/solidjs/solid"
target="_blank"
rel="noopener noreferrer"
>
Learn Solid
</a>
</header>
</div>
);
export default function App() {
return <>
<header>
<img src={logo} alt="logo" width={125} height={125} />
<A href="/">Login</A>
<A href="/dashboard">Dashboard</A>
</header>
<Routes>
<Route path="/">
<Route path="/" component={LoginPage} />
<Route path="dashboard" component={DashboardPage} />
</Route>
</Routes>
</>
};
export default App;

View File

@ -1,6 +1,7 @@
/* @refresh reload */
import { render } from 'solid-js/web';
import { Router } from "@solidjs/router";
import App from './App';
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 App from './App';
import Button from './components/Button';
import Counter from './components/Counter';
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 }) => {
const component = await mount(
<DefaultChildren>Main Content</DefaultChildren>
);
const component = await mount(<DefaultChildren>Main Content</DefaultChildren>);
await expect(component).toContainText('Main Content');
});
test('render multiple children', async ({ mount }) => {
const component = await mount(
<DefaultChildren>
<div id="one">One</div>
<div id="two">Two</div>
</DefaultChildren>
);
const component = await mount(<DefaultChildren>
<div id="one">One</div>
<div id="two">Two</div>
</DefaultChildren>);
await expect(component.locator('#one')).toContainText('One');
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 }) => {
const component = await mount(
<MultipleChildren>
<div>Header</div>
<div>Main Content</div>
<div>Footer</div>
</MultipleChildren>
);
const component = await mount(<MultipleChildren>
<div>Header</div>
<div>Main Content</div>
<div>Footer</div>
</MultipleChildren>);
await expect(component).toContainText('Header');
await expect(component).toContainText('Main Content');
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 }) => {
let clickFired = false;
const component = await mount(
<DefaultChildren>
<span onClick={() => (clickFired = true)}>Main Content</span>
</DefaultChildren>
);
const component = await mount(<DefaultChildren>
<span onClick={() => (clickFired = true)}>Main Content</span>
</DefaultChildren>);
await component.locator('text=Main Content').click();
expect(clickFired).toBeTruthy();
});
@ -163,3 +156,14 @@ test('get textContent of the empty fragment', async ({ mount }) => {
expect(await component.textContent()).toBe('');
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';
export type HooksConfig = {
route: string;
route?: string;
routing?: boolean;
}
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
app.use(router);
if (hooksConfig?.routing)
app.use(router);
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 }) => {
const component = await mount(<App />);
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();

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 }) => {
const component = await mount(App);
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();

View File

@ -4,11 +4,13 @@ import Button from '../src/components/Button.vue';
import '../src/assets/index.css';
export type HooksConfig = {
route: string;
route?: string;
routing?: boolean;
}
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);
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('');
});
test('render app and navigate to a page', async ({ page, mount }) => {
const component = await mount(App);
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();

View File

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

View File

@ -4,13 +4,17 @@ import { router } from '../src/router';
import '../src/assets/index.css';
export type HooksConfig = {
route: string;
route?: string;
routing?: boolean;
}
beforeMount<HooksConfig>(async ({ Vue, 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 }) => {

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 }) => {
const component = await mount(<App />);
const component = await mount(<App />, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/');
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 }) => {
const component = await mount(App);
const component = await mount(App, {
hooksConfig: { routing: true }
});
await expect(component.getByRole('main')).toHaveText('Login');
await expect(page).toHaveURL('/');
await component.getByRole('link', { name: 'Dashboard' }).click();