mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-06 01:17:33 +03:00
feat(ct): react rerender (#16560)
This commit is contained in:
parent
26623a0a77
commit
d9850e0e86
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 {
|
||||
unmount(): Promise<void>;
|
||||
rerender(component: JSX.Element): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ComponentFixtures {
|
||||
mount(component: JSX.Element, options?: { hooksConfig?: any }): Promise<MountResult>;
|
||||
mount(component: JSX.Element, options?: MountOptions): Promise<MountResult>;
|
||||
}
|
||||
|
||||
export const test: TestType<
|
||||
|
@ -82,3 +82,7 @@ window.playwrightUnmount = async rootElement => {
|
||||
if (!ReactDOM.unmountComponentAtNode(rootElement))
|
||||
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 {
|
||||
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<
|
||||
@ -60,11 +60,8 @@ export const fixtures: Fixtures<
|
||||
await window.playwrightUnmount(rootElement);
|
||||
});
|
||||
},
|
||||
rerender: async (options: Omit<MountOptions, 'hooksConfig'>) => {
|
||||
await locator.evaluate(async (element, options) => {
|
||||
const rootElement = document.getElementById('root')!;
|
||||
return await window.playwrightRerender(rootElement, options);
|
||||
}, options);
|
||||
rerender: async (component: JsxComponent | string, options?: Omit<MountOptions, 'hooksConfig'>) => {
|
||||
await innerRerender(page, component, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -72,13 +69,32 @@ export const fixtures: Fixtures<
|
||||
},
|
||||
};
|
||||
|
||||
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise<string> {
|
||||
let component: Component;
|
||||
if (typeof jsxOrType === 'string')
|
||||
component = { kind: 'object', type: jsxOrType, options };
|
||||
else
|
||||
component = jsxOrType;
|
||||
async function innerRerender(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> {
|
||||
const component = createComponent(jsxOrType, options);
|
||||
wrapFunctions(component, page, boundCallbacksForMount);
|
||||
|
||||
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);
|
||||
|
||||
// WebKit does not wait for deferred scripts.
|
||||
@ -114,6 +130,11 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
||||
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[]) {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
const type = typeof value;
|
||||
|
@ -40,6 +40,6 @@ declare global {
|
||||
interface Window {
|
||||
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): 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>;
|
||||
}
|
||||
}
|
||||
|
19
tests/components/ct-react-vite/src/components/Counter.tsx
Normal file
19
tests/components/ct-react-vite/src/components/Counter.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
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>
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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 } });
|
||||
|
||||
@ -11,6 +12,41 @@ test('props should work', async ({ mount }) => {
|
||||
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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user