From 9fa06be49ef83e65e83ebeb42a3e6aa7487479d9 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 10 Sep 2024 11:15:20 +0200 Subject: [PATCH] fix(ct): throw error if inline component is getting mounted (#32531) What was happening? - When we use CT, we go over the test files, look at the imports using `tsxTransform.ts` and store them inside a map, these we feed into the import registry which we build using Vite and have access inside the browser - In case of an inline component in the same file as where the test file is, this is not happening. - jsx-runtime via babel kicks in, transforms every JSX component in something like that: ``` { __pw_type: 'jsx', type: [Function: MyInlineComponent], props: { value: 'Max' }, key: undefined } ``` this then gets passed into `wrapObject` which maps any function from the Node.js side into expose function calls so they work inside the browser. The assumption for `wrapObject` was to do it mostly for callbacks. So it does for `type` - which is actually our component. We then pass this to the React render function, which calls back the exposed function but we never return anything, so it mounts `undefined`. --- While there have been experiments from certain vendors to get the 'client only' code inside a server side file, we should throw for now to not confuse users. We might revisit this in the future since Babel / TSX doesn't support it outside of the box. Fixes https://github.com/microsoft/playwright/issues/32167 --- .../src/injected/serializers.ts | 7 +++++++ .../ct-react-vite/tests/render.spec.tsx | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/playwright-ct-core/src/injected/serializers.ts b/packages/playwright-ct-core/src/injected/serializers.ts index d07be7c1f8..be083e27db 100644 --- a/packages/playwright-ct-core/src/injected/serializers.ts +++ b/packages/playwright-ct-core/src/injected/serializers.ts @@ -66,6 +66,13 @@ export function transformObject(value: any, mapping: (v: any) => { result: any } result.push(transformObject(item, mapping)); return result; } + if (value?.__pw_type === 'jsx' && typeof value.type === 'function') { + throw new Error([ + `Component "${value.type.name}" cannot be mounted.`, + `Most likely, this component is defined in the test file. Create a test story instead.`, + `For more information, see https://playwright.dev/docs/test-components#test-stories.`, + ].join('\n')); + } const result2: any = {}; for (const [key, prop] of Object.entries(value)) result2[key] = transformObject(prop, mapping); diff --git a/tests/components/ct-react-vite/tests/render.spec.tsx b/tests/components/ct-react-vite/tests/render.spec.tsx index 1d82d21b3a..d00a313e9e 100644 --- a/tests/components/ct-react-vite/tests/render.spec.tsx +++ b/tests/components/ct-react-vite/tests/render.spec.tsx @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-react'; import Button from '@/components/Button'; import EmptyFragment from '@/components/EmptyFragment'; import { ComponentAsProp } from '@/components/ComponentAsProp'; +import DefaultChildren from '@/components/DefaultChildren'; test('render props', async ({ mount }) => { const component = await mount(