playwright/packages/playwright-ct-core/src/injected/serializers.ts
Max Schmitt 9fa06be49e
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
2024-09-10 11:15:20 +02:00

101 lines
3.3 KiB
TypeScript

/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isImportRef } from './importRegistry';
type FunctionRef = {
__pw_type: 'function';
ordinal: number;
};
function isFunctionRef(value: any): value is FunctionRef {
return value && typeof value === 'object' && value.__pw_type === 'function';
}
export function wrapObject(value: any, callbacks: Function[]): any {
return transformObject(value, (v: any) => {
if (typeof v === 'function') {
const ordinal = callbacks.length;
callbacks.push(v as Function);
const result: FunctionRef = {
__pw_type: 'function',
ordinal,
};
return { result };
}
});
}
export async function unwrapObject(value: any): Promise<any> {
return transformObjectAsync(value, async (v: any) => {
if (isFunctionRef(v)) {
const result = (...args: any[]) => {
window.__ctDispatchFunction(v.ordinal, args);
};
return { result };
}
if (isImportRef(v))
return { result: await window.__pwRegistry.resolveImportRef(v) };
});
}
export function transformObject(value: any, mapping: (v: any) => { result: any } | undefined): any {
const result = mapping(value);
if (result)
return result.result;
if (value === null || typeof value !== 'object')
return value;
if (value instanceof Date || value instanceof RegExp || value instanceof URL)
return value;
if (Array.isArray(value)) {
const result = [];
for (const item of value)
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);
return result2;
}
export async function transformObjectAsync(value: any, mapping: (v: any) => Promise<{ result: any } | undefined>): Promise<any> {
const result = await mapping(value);
if (result)
return result.result;
if (value === null || typeof value !== 'object')
return value;
if (value instanceof Date || value instanceof RegExp || value instanceof URL)
return value;
if (Array.isArray(value)) {
const result = [];
for (const item of value)
result.push(await transformObjectAsync(item, mapping));
return result;
}
const result2: any = {};
for (const [key, prop] of Object.entries(value))
result2[key] = await transformObjectAsync(prop, mapping);
return result2;
}