From 4d62784eeb36246de989d2eaa60f104d167a8dd7 Mon Sep 17 00:00:00 2001 From: Sander Date: Sat, 23 Dec 2023 05:51:59 +0100 Subject: [PATCH] feat(ct): react component as props (#28382) closes: https://github.com/microsoft/playwright/issues/28367#issuecomment-1830298864 --- packages/playwright-ct-core/src/mount.ts | 2 +- .../playwright-ct-core/src/tsxTransform.ts | 3 +- .../playwright-ct-core/types/component.d.ts | 2 + .../playwright-ct-react/registerSource.mjs | 43 +++++++++++++++++-- .../src/components/ComponentAsProp.tsx | 9 ++++ .../ct-react-vite/tests/render.spec.tsx | 12 ++++++ 6 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/components/ct-react-vite/src/components/ComponentAsProp.tsx diff --git a/packages/playwright-ct-core/src/mount.ts b/packages/playwright-ct-core/src/mount.ts index fc01ab7283..d349ba2347 100644 --- a/packages/playwright-ct-core/src/mount.ts +++ b/packages/playwright-ct-core/src/mount.ts @@ -140,7 +140,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: function createComponent(jsxOrType: JsxComponent | string, options: Omit = {}): Component { if (typeof jsxOrType !== 'string') return jsxOrType; - return { kind: 'object', type: jsxOrType, options }; + return { __pw_component_marker: true, kind: 'object', type: jsxOrType, options }; } function wrapFunctions(object: any, page: Page, callbacks: Function[]) { diff --git a/packages/playwright-ct-core/src/tsxTransform.ts b/packages/playwright-ct-core/src/tsxTransform.ts index de15d3912a..21520f1b63 100644 --- a/packages/playwright-ct-core/src/tsxTransform.ts +++ b/packages/playwright-ct-core/src/tsxTransform.ts @@ -121,7 +121,8 @@ export default declare((api: BabelAPI) => { children.push(t.spreadElement(child.expression)); } - const component = [ + const component: T.ObjectProperty[] = [ + t.objectProperty(t.identifier('__pw_component_marker'), t.booleanLiteral(true)), t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')), t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)), t.objectProperty(t.identifier('props'), t.objectExpression(props)), diff --git a/packages/playwright-ct-core/types/component.d.ts b/packages/playwright-ct-core/types/component.d.ts index ddac25ab32..03949a996c 100644 --- a/packages/playwright-ct-core/types/component.d.ts +++ b/packages/playwright-ct-core/types/component.d.ts @@ -22,6 +22,7 @@ export type JsonObject = { [Key in string]?: JsonValue }; // JsxComponentChild can be anything, consider cases like: <>{1}, <>{null} export type JsxComponentChild = JsxComponent | string | number | boolean | null; export type JsxComponent = { + __pw_component_marker: true, kind: 'jsx', type: string, props: Record, @@ -36,6 +37,7 @@ export type MountOptions = { }; export type ObjectComponent = { + __pw_component_marker: true, kind: 'object', type: string, options?: MountOptions diff --git a/packages/playwright-ct-react/registerSource.mjs b/packages/playwright-ct-react/registerSource.mjs index e8c2d6ca68..713b612fa2 100644 --- a/packages/playwright-ct-react/registerSource.mjs +++ b/packages/playwright-ct-react/registerSource.mjs @@ -32,7 +32,7 @@ const __pwRegistry = new Map(); const __pwRootRegistry = new Map(); /** - * @param {{[key: string]: () => Promise}} components + * @param {Record Promise>} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) @@ -44,7 +44,7 @@ export function pwRegister(components) { * @returns {component is JsxComponent} */ function isComponent(component) { - return !(typeof component !== 'object' || Array.isArray(component)); + return component.__pw_component_marker === true && component.kind === 'jsx'; } /** @@ -73,6 +73,23 @@ async function __pwResolveComponent(component) { if (component.children?.length) await Promise.all(component.children.map(child => __pwResolveComponent(child))); + + if (component.props) + await __resolveProps(component.props); +} + +/** + * @param {Record} props + */ +async function __resolveProps(props) { + for (const prop of Object.values(props)) { + if (Array.isArray(prop)) + await Promise.all(prop.map(child => __pwResolveComponent(child))); + else if (isComponent(prop)) + await __pwResolveComponent(prop); + else if (typeof prop === 'object' && prop !== null) + await __resolveProps(prop); + } } /** @@ -86,18 +103,37 @@ function __renderChild(child) { return child; } +/** + * @param {Record} props + */ +function __renderProps(props) { + const newProps = {}; + for (const [key, prop] of Object.entries(props)) { + if (Array.isArray(prop)) + newProps[key] = prop.map(child => __renderChild(child)); + else if (isComponent(prop)) + newProps[key] = __renderChild(prop); + else if (typeof prop === 'object' && prop !== null) + newProps[key] = __renderProps(prop); + else + newProps[key] = prop; + } + return newProps; +} + /** * @param {JsxComponent} component */ function __pwRender(component) { const componentFunc = __pwRegistry.get(component.type); + const props = __renderProps(component.props || {}); const children = component.children?.map(child => __renderChild(child)).filter(child => { if (typeof child === 'string') return !!child.trim(); return true; }); const reactChildren = Array.isArray(children) && children.length === 1 ? children[0] : children; - return __pwReact.createElement(componentFunc || component.type, component.props, reactChildren); + return __pwReact.createElement(componentFunc || component.type, props, reactChildren); } window.playwrightMount = async (component, rootElement, hooksConfig) => { @@ -117,7 +153,6 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => { 'Attempting to mount a component into an container that already has a React root' ); } - const root = __pwCreateRoot(rootElement); __pwRootRegistry.set(rootElement, root); root.render(App()); diff --git a/tests/components/ct-react-vite/src/components/ComponentAsProp.tsx b/tests/components/ct-react-vite/src/components/ComponentAsProp.tsx new file mode 100644 index 0000000000..67b8813207 --- /dev/null +++ b/tests/components/ct-react-vite/src/components/ComponentAsProp.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +type ComponentAsProp = { + component: ReactNode[] | ReactNode; +}; + +export function ComponentAsProp({ component }: ComponentAsProp) { + return
{component}
+} diff --git a/tests/components/ct-react-vite/tests/render.spec.tsx b/tests/components/ct-react-vite/tests/render.spec.tsx index 107a18bf65..1d82d21b3a 100644 --- a/tests/components/ct-react-vite/tests/render.spec.tsx +++ b/tests/components/ct-react-vite/tests/render.spec.tsx @@ -1,12 +1,24 @@ import { test, expect } from '@playwright/experimental-ct-react'; import Button from '@/components/Button'; import EmptyFragment from '@/components/EmptyFragment'; +import { ComponentAsProp } from '@/components/ComponentAsProp'; test('render props', async ({ mount }) => { const component = await mount(