mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
feat(ct): react rerender (#16549)
This commit is contained in:
parent
d133b58a96
commit
a62accf8ae
7
packages/playwright-ct-react/index.d.ts
vendored
7
packages/playwright-ct-react/index.d.ts
vendored
@ -34,12 +34,17 @@ export type PlaywrightTestConfig = Omit<BasePlaywrightTestConfig, 'use'> & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MountOptions {
|
||||||
|
hooksConfig?: any;
|
||||||
|
}
|
||||||
|
|
||||||
interface MountResult extends Locator {
|
interface MountResult extends Locator {
|
||||||
unmount(): Promise<void>;
|
unmount(): Promise<void>;
|
||||||
|
rerender(component: JSX.Element): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentFixtures {
|
export interface ComponentFixtures {
|
||||||
mount(component: JSX.Element, options?: { hooksConfig?: any }): Promise<MountResult>;
|
mount(component: JSX.Element, options?: MountOptions): Promise<MountResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const test: TestType<
|
export const test: TestType<
|
||||||
|
@ -82,3 +82,7 @@ window.playwrightUnmount = async rootElement => {
|
|||||||
if (!ReactDOM.unmountComponentAtNode(rootElement))
|
if (!ReactDOM.unmountComponentAtNode(rootElement))
|
||||||
throw new Error('Component was not mounted');
|
throw new Error('Component was not mounted');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.playwrightRerender = async (rootElement, component) => {
|
||||||
|
ReactDOM.render(render(/** @type {Component} */(component)), rootElement);
|
||||||
|
};
|
||||||
|
@ -21,7 +21,7 @@ let boundCallbacksForMount: Function[] = [];
|
|||||||
|
|
||||||
interface MountResult extends Locator {
|
interface MountResult extends Locator {
|
||||||
unmount(locator: Locator): Promise<void>;
|
unmount(locator: Locator): Promise<void>;
|
||||||
rerender(options: Omit<MountOptions, 'hooksConfig'>): Promise<void>;
|
rerender(options: Omit<MountOptions, 'hooksConfig'> | string | JsxComponent): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fixtures: Fixtures<
|
export const fixtures: Fixtures<
|
||||||
@ -60,11 +60,8 @@ export const fixtures: Fixtures<
|
|||||||
await window.playwrightUnmount(rootElement);
|
await window.playwrightUnmount(rootElement);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
rerender: async (options: Omit<MountOptions, 'hooksConfig'>) => {
|
rerender: async (component: JsxComponent | string, options?: Omit<MountOptions, 'hooksConfig'>) => {
|
||||||
await locator.evaluate(async (element, options) => {
|
await innerRerender(page, component, options);
|
||||||
const rootElement = document.getElementById('root')!;
|
|
||||||
return await window.playwrightRerender(rootElement, options);
|
|
||||||
}, options);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -72,13 +69,32 @@ export const fixtures: Fixtures<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise<string> {
|
async function innerRerender(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> {
|
||||||
let component: Component;
|
const component = createComponent(jsxOrType, options);
|
||||||
if (typeof jsxOrType === 'string')
|
wrapFunctions(component, page, boundCallbacksForMount);
|
||||||
component = { kind: 'object', type: jsxOrType, options };
|
|
||||||
else
|
|
||||||
component = jsxOrType;
|
|
||||||
|
|
||||||
|
await page.evaluate(async ({ component }) => {
|
||||||
|
const unwrapFunctions = (object: any) => {
|
||||||
|
for (const [key, value] of Object.entries(object)) {
|
||||||
|
if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) {
|
||||||
|
const ordinal = +value.substring('__pw_func_'.length);
|
||||||
|
object[key] = (...args: any[]) => {
|
||||||
|
(window as any)['__ct_dispatch'](ordinal, args);
|
||||||
|
};
|
||||||
|
} else if (typeof value === 'object' && value) {
|
||||||
|
unwrapFunctions(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unwrapFunctions(component);
|
||||||
|
const rootElement = document.getElementById('root')!;
|
||||||
|
return await window.playwrightRerender(rootElement, component);
|
||||||
|
}, { component });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise<string> {
|
||||||
|
const component = createComponent(jsxOrType, options);
|
||||||
wrapFunctions(component, page, boundCallbacksForMount);
|
wrapFunctions(component, page, boundCallbacksForMount);
|
||||||
|
|
||||||
// WebKit does not wait for deferred scripts.
|
// WebKit does not wait for deferred scripts.
|
||||||
@ -114,6 +130,11 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
|||||||
return selector;
|
return selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createComponent(jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Component {
|
||||||
|
if (typeof jsxOrType !== 'string') return jsxOrType;
|
||||||
|
return { kind: 'object', type: jsxOrType, options };
|
||||||
|
}
|
||||||
|
|
||||||
function wrapFunctions(object: any, page: Page, callbacks: Function[]) {
|
function wrapFunctions(object: any, page: Page, callbacks: Function[]) {
|
||||||
for (const [key, value] of Object.entries(object)) {
|
for (const [key, value] of Object.entries(object)) {
|
||||||
const type = typeof value;
|
const type = typeof value;
|
||||||
|
@ -40,6 +40,6 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>;
|
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>;
|
||||||
playwrightUnmount(rootElement: Element): Promise<void>;
|
playwrightUnmount(rootElement: Element): Promise<void>;
|
||||||
playwrightRerender(rootElement: Element, options: Omit<MountOptions, 'hooksConfig'>): Promise<void>;
|
playwrightRerender(rootElement: Element, optionsOrComponent: Omit<MountOptions, 'hooksConfig'> | Component): Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
//@ts-check
|
//@ts-check
|
||||||
|
import '../src/assets/index.css';
|
||||||
import '../src/index.css';
|
|
||||||
|
|
||||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
|
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
|
||||||
|
|
||||||
beforeMount(async ({ hooksConfig }) => {
|
beforeMount(async ({ hooksConfig }) => {
|
||||||
|
@ -1,27 +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(<App></App>);
|
|
||||||
await expect(component).toContainText('Hello Vite + React!');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should configure app', async ({ page, mount }) => {
|
|
||||||
const messages: string[] = [];
|
|
||||||
page.on('console', m => messages.push(m.text()));
|
|
||||||
await mount(<App></App>, {
|
|
||||||
hooksConfig: {
|
|
||||||
route: 'A'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should unmount', async ({ page, mount }) => {
|
|
||||||
const component = await mount(<App></App>);
|
|
||||||
await expect(page.locator('#root')).toContainText('Hello Vite + React!');
|
|
||||||
await component.unmount();
|
|
||||||
await expect(page.locator('#root')).not.toContainText('Hello Vite + React!');
|
|
||||||
});
|
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import logo from './logo.svg'
|
import logo from './components/logo.svg'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
7
tests/components/ct-react-vite/src/components/Button.tsx
Normal file
7
tests/components/ct-react-vite/src/components/Button.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
type ButtonProps = {
|
||||||
|
title: string;
|
||||||
|
onClick?(props: string): void;
|
||||||
|
}
|
||||||
|
export default function Button(props: ButtonProps) {
|
||||||
|
return <button onClick={() => props.onClick?.('hello')}>{props.title}</button>
|
||||||
|
}
|
18
tests/components/ct-react-vite/src/components/Counter.tsx
Normal file
18
tests/components/ct-react-vite/src/components/Counter.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useRef } from "react"
|
||||||
|
|
||||||
|
type CounterProps = {
|
||||||
|
count?: number;
|
||||||
|
onClick?(props: string): void;
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _remountCount = 1;
|
||||||
|
|
||||||
|
export default function Counter(props: CounterProps) {
|
||||||
|
const remountCount = useRef(_remountCount++);
|
||||||
|
return <div onClick={() => props.onClick?.('hello')}>
|
||||||
|
<div id="props">{ props.count }</div>
|
||||||
|
<div id="remount-count">{ remountCount.current }</div>
|
||||||
|
{ props.children }
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
type DefaultChildrenProps = {
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DefaultChildren(props: DefaultChildrenProps) {
|
||||||
|
return <div>
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<main>
|
||||||
|
{props.children}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
Thanks for visiting.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export default function MultiRoot() {
|
||||||
|
return <>
|
||||||
|
<div>root 1</div>
|
||||||
|
<div>root 2</div>
|
||||||
|
</>
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
type MultipleChildrenProps = {
|
||||||
|
children?: [any, any, any];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MultipleChildren(props: MultipleChildrenProps) {
|
||||||
|
return <div>
|
||||||
|
<header>
|
||||||
|
{props.children?.at(0)}
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{props.children?.at(1)}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
{props.children?.at(2)}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import './index.css'
|
import './assets/index.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
120
tests/components/ct-react-vite/src/tests.spec.tsx
Normal file
120
tests/components/ct-react-vite/src/tests.spec.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import Button from './components/Button';
|
||||||
|
import DefaultChildren from './components/DefaultChildren';
|
||||||
|
import MultipleChildren from './components/MultipleChildren';
|
||||||
|
import MultiRoot from './components/MultiRoot';
|
||||||
|
import Counter from './components/Counter';
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
|
test('props should work', async ({ mount }) => {
|
||||||
|
const component = await mount(<Button title="Submit" />);
|
||||||
|
await expect(component).toContainText('Submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderer updates props without remounting', async ({ mount }) => {
|
||||||
|
const component = await mount(<Counter count={9001} />)
|
||||||
|
await expect(component.locator('#props')).toContainText('9001')
|
||||||
|
|
||||||
|
await component.rerender(<Counter count={1337} />)
|
||||||
|
await expect(component).not.toContainText('9001')
|
||||||
|
await expect(component.locator('#props')).toContainText('1337')
|
||||||
|
|
||||||
|
await expect(component.locator('#remount-count')).toContainText('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renderer updates callbacks without remounting', async ({ mount }) => {
|
||||||
|
const component = await mount(<Counter />)
|
||||||
|
|
||||||
|
const messages: string[] = []
|
||||||
|
await component.rerender(<Counter onClick={message => {
|
||||||
|
messages.push(message)
|
||||||
|
}} />)
|
||||||
|
await component.click();
|
||||||
|
expect(messages).toEqual(['hello'])
|
||||||
|
|
||||||
|
await expect(component.locator('#remount-count')).toContainText('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renderer updates slots without remounting', async ({ mount }) => {
|
||||||
|
const component = await mount(<Counter>Default Slot</Counter>)
|
||||||
|
await expect(component).toContainText('Default Slot')
|
||||||
|
|
||||||
|
await component.rerender(<Counter>Test Slot</Counter>)
|
||||||
|
await expect(component).not.toContainText('Default Slot')
|
||||||
|
await expect(component).toContainText('Test Slot')
|
||||||
|
|
||||||
|
await expect(component.locator('#remount-count')).toContainText('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('callback should work', async ({ mount }) => {
|
||||||
|
const messages: string[] = []
|
||||||
|
const component = await mount(<Button title="Submit" onClick={data => {
|
||||||
|
messages.push(data)
|
||||||
|
}}></Button>)
|
||||||
|
await component.click()
|
||||||
|
expect(messages).toEqual(['hello'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('default slot should work', async ({ mount }) => {
|
||||||
|
const component = await mount(<DefaultChildren>
|
||||||
|
Main Content
|
||||||
|
</DefaultChildren>)
|
||||||
|
await expect(component).toContainText('Main Content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('multiple children should work', async ({ mount }) => {
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('named children should work', async ({ mount }) => {
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('children should callback', async ({ mount }) => {
|
||||||
|
let clickFired = false;
|
||||||
|
const component = await mount(<DefaultChildren>
|
||||||
|
<span onClick={() => clickFired = true}>Main Content</span>
|
||||||
|
</DefaultChildren>);
|
||||||
|
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(<Button title="Submit" />, {
|
||||||
|
hooksConfig: {
|
||||||
|
route: 'A'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should unmount', async ({ page, mount }) => {
|
||||||
|
const component = await mount(<Button title="Submit" />)
|
||||||
|
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(<MultiRoot />)
|
||||||
|
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')
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user