From d9850e0e864869955cc2313424d38c5e4fe04b65 Mon Sep 17 00:00:00 2001 From: sand4rt Date: Tue, 16 Aug 2022 19:47:33 +0200 Subject: [PATCH] feat(ct): react rerender (#16560) --- packages/playwright-ct-react/index.d.ts | 7 ++- .../playwright-ct-react/registerSource.mjs | 4 ++ packages/playwright-test/src/mount.ts | 45 ++++++++++++++----- packages/playwright-test/types/component.d.ts | 2 +- .../ct-react-vite/src/components/Counter.tsx | 19 ++++++++ .../ct-react-vite/src/tests.spec.tsx | 36 +++++++++++++++ 6 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 tests/components/ct-react-vite/src/components/Counter.tsx diff --git a/packages/playwright-ct-react/index.d.ts b/packages/playwright-ct-react/index.d.ts index 4be9b96787..17c946ac1b 100644 --- a/packages/playwright-ct-react/index.d.ts +++ b/packages/playwright-ct-react/index.d.ts @@ -34,12 +34,17 @@ export type PlaywrightTestConfig = Omit & { } }; +export interface MountOptions { + hooksConfig?: any; +} + interface MountResult extends Locator { unmount(): Promise; + rerender(component: JSX.Element): Promise; } export interface ComponentFixtures { - mount(component: JSX.Element, options?: { hooksConfig?: any }): Promise; + mount(component: JSX.Element, options?: MountOptions): Promise; } export const test: TestType< diff --git a/packages/playwright-ct-react/registerSource.mjs b/packages/playwright-ct-react/registerSource.mjs index a6948efe7d..f1183f5392 100644 --- a/packages/playwright-ct-react/registerSource.mjs +++ b/packages/playwright-ct-react/registerSource.mjs @@ -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); +}; diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index feeb6c9bf5..17ab6c3cde 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -21,7 +21,7 @@ let boundCallbacksForMount: Function[] = []; interface MountResult extends Locator { unmount(locator: Locator): Promise; - rerender(options: Omit): Promise; + rerender(options: Omit | string | JsxComponent): Promise; } export const fixtures: Fixtures< @@ -60,11 +60,8 @@ export const fixtures: Fixtures< await window.playwrightUnmount(rootElement); }); }, - rerender: async (options: Omit) => { - 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) => { + await innerRerender(page, component, options); } }); }); @@ -72,13 +69,32 @@ export const fixtures: Fixtures< }, }; -async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise { - 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 = {}): Promise { + 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 { + 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 = {}): 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; diff --git a/packages/playwright-test/types/component.d.ts b/packages/playwright-test/types/component.d.ts index 561d400832..5fc210c814 100644 --- a/packages/playwright-test/types/component.d.ts +++ b/packages/playwright-test/types/component.d.ts @@ -40,6 +40,6 @@ declare global { interface Window { playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise; playwrightUnmount(rootElement: Element): Promise; - playwrightRerender(rootElement: Element, options: Omit): Promise; + playwrightRerender(rootElement: Element, optionsOrComponent: Omit | Component): Promise; } } diff --git a/tests/components/ct-react-vite/src/components/Counter.tsx b/tests/components/ct-react-vite/src/components/Counter.tsx new file mode 100644 index 0000000000..2548202aec --- /dev/null +++ b/tests/components/ct-react-vite/src/components/Counter.tsx @@ -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
props.onClick?.('hello')}> +
{ props.count }
+
{ remountCount.current }
+ { props.children } +
+ } + \ No newline at end of file diff --git a/tests/components/ct-react-vite/src/tests.spec.tsx b/tests/components/ct-react-vite/src/tests.spec.tsx index 2fb1a5be69..5895ec7c57 100644 --- a/tests/components/ct-react-vite/src/tests.spec.tsx +++ b/tests/components/ct-react-vite/src/tests.spec.tsx @@ -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() + await expect(component.locator('#props')).toContainText('9001') + + await component.rerender() + 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() + + const messages: string[] = [] + await component.rerender( { + 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(Default Slot) + await expect(component).toContainText('Default Slot') + + await component.rerender(Test Slot) + 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(