From a62accf8aebd0d6cc08fd461ee8281f72ab86d33 Mon Sep 17 00:00:00 2001 From: sand4rt Date: Mon, 15 Aug 2022 22:10:38 +0200 Subject: [PATCH] feat(ct): react rerender (#16549) --- 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/playwright/index.ts | 4 +- .../components/ct-react-vite/src/App.spec.tsx | 27 ---- tests/components/ct-react-vite/src/App.tsx | 3 +- .../src/{ => assets}/favicon.svg | 0 .../ct-react-vite/src/{ => assets}/index.css | 0 .../ct-react-vite/src/{ => assets}/logo.svg | 0 .../ct-react-vite/src/components/Button.tsx | 7 + .../ct-react-vite/src/components/Counter.tsx | 18 +++ .../src/components/DefaultChildren.tsx | 15 +++ .../src/components/MultiRoot.tsx | 6 + .../src/components/MultipleChildren.tsx | 18 +++ tests/components/ct-react-vite/src/main.tsx | 2 +- .../ct-react-vite/src/tests.spec.tsx | 120 ++++++++++++++++++ 17 files changed, 231 insertions(+), 47 deletions(-) delete mode 100644 tests/components/ct-react-vite/src/App.spec.tsx rename tests/components/ct-react-vite/src/{ => assets}/favicon.svg (100%) rename tests/components/ct-react-vite/src/{ => assets}/index.css (100%) rename tests/components/ct-react-vite/src/{ => assets}/logo.svg (100%) create mode 100644 tests/components/ct-react-vite/src/components/Button.tsx create mode 100644 tests/components/ct-react-vite/src/components/Counter.tsx create mode 100644 tests/components/ct-react-vite/src/components/DefaultChildren.tsx create mode 100644 tests/components/ct-react-vite/src/components/MultiRoot.tsx create mode 100644 tests/components/ct-react-vite/src/components/MultipleChildren.tsx create mode 100644 tests/components/ct-react-vite/src/tests.spec.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/playwright/index.ts b/tests/components/ct-react-vite/playwright/index.ts index 3d3b46bdc0..267e6fd64f 100644 --- a/tests/components/ct-react-vite/playwright/index.ts +++ b/tests/components/ct-react-vite/playwright/index.ts @@ -1,7 +1,5 @@ //@ts-check - -import '../src/index.css'; - +import '../src/assets/index.css'; import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks'; beforeMount(async ({ hooksConfig }) => { diff --git a/tests/components/ct-react-vite/src/App.spec.tsx b/tests/components/ct-react-vite/src/App.spec.tsx deleted file mode 100644 index 4de184d7c0..0000000000 --- a/tests/components/ct-react-vite/src/App.spec.tsx +++ /dev/null @@ -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(); - 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(, { - hooksConfig: { - route: 'A' - } - }); - expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']); -}); - -test('should unmount', async ({ page, mount }) => { - const component = await mount(); - await expect(page.locator('#root')).toContainText('Hello Vite + React!'); - await component.unmount(); - await expect(page.locator('#root')).not.toContainText('Hello Vite + React!'); -}); diff --git a/tests/components/ct-react-vite/src/App.tsx b/tests/components/ct-react-vite/src/App.tsx index 78b8518c54..1a5c71390f 100644 --- a/tests/components/ct-react-vite/src/App.tsx +++ b/tests/components/ct-react-vite/src/App.tsx @@ -1,6 +1,5 @@ -import React from 'react' import { useState } from 'react' -import logo from './logo.svg' +import logo from './components/logo.svg' import './App.css' function App() { diff --git a/tests/components/ct-react-vite/src/favicon.svg b/tests/components/ct-react-vite/src/assets/favicon.svg similarity index 100% rename from tests/components/ct-react-vite/src/favicon.svg rename to tests/components/ct-react-vite/src/assets/favicon.svg diff --git a/tests/components/ct-react-vite/src/index.css b/tests/components/ct-react-vite/src/assets/index.css similarity index 100% rename from tests/components/ct-react-vite/src/index.css rename to tests/components/ct-react-vite/src/assets/index.css diff --git a/tests/components/ct-react-vite/src/logo.svg b/tests/components/ct-react-vite/src/assets/logo.svg similarity index 100% rename from tests/components/ct-react-vite/src/logo.svg rename to tests/components/ct-react-vite/src/assets/logo.svg diff --git a/tests/components/ct-react-vite/src/components/Button.tsx b/tests/components/ct-react-vite/src/components/Button.tsx new file mode 100644 index 0000000000..78b0a7791f --- /dev/null +++ b/tests/components/ct-react-vite/src/components/Button.tsx @@ -0,0 +1,7 @@ +type ButtonProps = { + title: string; + onClick?(props: string): void; +} +export default function Button(props: ButtonProps) { + return +} 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..cfba1e3057 --- /dev/null +++ b/tests/components/ct-react-vite/src/components/Counter.tsx @@ -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
props.onClick?.('hello')}> +
{ props.count }
+
{ remountCount.current }
+ { props.children } +
+} diff --git a/tests/components/ct-react-vite/src/components/DefaultChildren.tsx b/tests/components/ct-react-vite/src/components/DefaultChildren.tsx new file mode 100644 index 0000000000..691b6a0806 --- /dev/null +++ b/tests/components/ct-react-vite/src/components/DefaultChildren.tsx @@ -0,0 +1,15 @@ +type DefaultChildrenProps = { + children?: any; +} + +export default function DefaultChildren(props: DefaultChildrenProps) { + return
+

Welcome!

+
+ {props.children} +
+
+ Thanks for visiting. +
+
+} diff --git a/tests/components/ct-react-vite/src/components/MultiRoot.tsx b/tests/components/ct-react-vite/src/components/MultiRoot.tsx new file mode 100644 index 0000000000..f29e397c0f --- /dev/null +++ b/tests/components/ct-react-vite/src/components/MultiRoot.tsx @@ -0,0 +1,6 @@ +export default function MultiRoot() { + return <> +
root 1
+
root 2
+ +} diff --git a/tests/components/ct-react-vite/src/components/MultipleChildren.tsx b/tests/components/ct-react-vite/src/components/MultipleChildren.tsx new file mode 100644 index 0000000000..63bd0104c6 --- /dev/null +++ b/tests/components/ct-react-vite/src/components/MultipleChildren.tsx @@ -0,0 +1,18 @@ + +type MultipleChildrenProps = { + children?: [any, any, any]; +} + +export default function MultipleChildren(props: MultipleChildrenProps) { + return
+
+ {props.children?.at(0)} +
+
+ {props.children?.at(1)} +
+
+ {props.children?.at(2)} +
+
+} diff --git a/tests/components/ct-react-vite/src/main.tsx b/tests/components/ct-react-vite/src/main.tsx index 606a3cf44e..919f50052b 100644 --- a/tests/components/ct-react-vite/src/main.tsx +++ b/tests/components/ct-react-vite/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom' -import './index.css' +import './assets/index.css' import App from './App' ReactDOM.render( diff --git a/tests/components/ct-react-vite/src/tests.spec.tsx b/tests/components/ct-react-vite/src/tests.spec.tsx new file mode 100644 index 0000000000..dd57589eaf --- /dev/null +++ b/tests/components/ct-react-vite/src/tests.spec.tsx @@ -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() + await component.click() + expect(messages).toEqual(['hello']) +}) + +test('default slot should work', async ({ mount }) => { + const component = await mount( + Main Content + ) + await expect(component).toContainText('Main Content') +}) + +test('multiple children should work', async ({ mount }) => { + const component = await mount( +
One
+
Two
+
) + 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( +
Header
+
Main Content
+
Footer
+
); + 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( + clickFired = true}>Main Content + ); + 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(