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
This commit is contained in:
Max Schmitt 2024-09-10 11:15:20 +02:00 коммит произвёл GitHub
Родитель 8995ace825
Коммит 9fa06be49e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
2 изменённых файлов: 22 добавлений и 0 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -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(<Button title="Submit" />);
@ -31,3 +32,17 @@ test('render an empty component', async ({ mount, page }) => {
expect(await component.textContent()).toBe('');
await expect(component).toHaveText('');
});
function MyInlineComponent({ value }: { value: string }) {
return <>Hello {value}</>;
}
test('render inline component with an error', async ({ mount }) => {
await expect(mount(<MyInlineComponent value="Max" />)).rejects.toThrow('Component "MyInlineComponent" cannot be mounted.');
});
test('render inline component with an error if its nested', async ({ mount }) => {
await expect(mount(<DefaultChildren>
<MyInlineComponent value="Max" />
</DefaultChildren>)).rejects.toThrow('Component "MyInlineComponent" cannot be mounted.');
});