feat(ct-react): Support React 18 only (#19814)

BREAKING CHANGE: Drop support for React 17 and earlier
Support for React 17 an earlier is provided by
`@playwright/experimental-ct-react-17`

Closes #19923
This commit is contained in:
Sebastian Silbermann 2023-03-03 23:28:33 +01:00 коммит произвёл GitHub
Родитель be259dac7c
Коммит fbaf56a13f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 468 добавлений и 39 удалений

28
package-lock.json сгенерированный
Просмотреть файл

@ -1297,6 +1297,10 @@
"resolved": "packages/playwright-ct-react",
"link": true
},
"node_modules/@playwright/experimental-ct-react17": {
"resolved": "packages/playwright-ct-react17",
"link": true
},
"node_modules/@playwright/experimental-ct-solid": {
"resolved": "packages/playwright-ct-solid",
"link": true
@ -5990,6 +5994,22 @@
"node": ">=14"
}
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
"version": "1.32.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/test": "1.32.0-next",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.1.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid",
"version": "1.32.0-next",
@ -6908,6 +6928,14 @@
"vite": "^4.1.1"
}
},
"@playwright/experimental-ct-react17": {
"version": "file:packages/playwright-ct-react17",
"requires": {
"@playwright/test": "1.32.0-next",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.1.1"
}
},
"@playwright/experimental-ct-solid": {
"version": "file:packages/playwright-ct-solid",
"requires": {

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

@ -84,6 +84,8 @@ test('should show the project names', async ({ mount }) => {
>
</HeaderView>);
await expect(component.getByText('Project: my-project')).toBeVisible();
await component.unmount();
});
await test.step('with 1 project and empty projectName', async () => {
const component = await mount(<HeaderView
@ -94,6 +96,8 @@ test('should show the project names', async ({ mount }) => {
>
</HeaderView>);
await expect(component.getByText('Project:')).toBeHidden();
await component.unmount();
});
await test.step('with more than 1 project', async () => {
const component = await mount(<HeaderView
@ -105,5 +109,7 @@ test('should show the project names', async ({ mount }) => {
</HeaderView>);
await expect(component.getByText('my-project')).toBeHidden();
await expect(component.getByText('great-project')).toBeHidden();
await component.unmount();
});
});

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

@ -17,14 +17,16 @@
// @ts-check
// This file is injected into the registry as text, no dependencies are allowed.
import React from 'react';
import ReactDOM from 'react-dom';
import * as React from 'react';
import { createRoot } from 'react-dom/client';
/** @typedef {import('../playwright-test/types/component').Component} Component */
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
/** @type {Map<string, FrameworkComponent>} */
const registry = new Map();
/** @type {Map<Element, import('react-dom/client').Root>>} */
const rootRegistry = new Map();
/**
* @param {{[key: string]: FrameworkComponent}} components
@ -79,17 +81,33 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
App = () => wrapper;
}
ReactDOM.render(App(), rootElement);
if (rootRegistry.has(rootElement)) {
throw new Error(
'Attempting to mount a component into an container that already has a React root'
);
}
const root = createRoot(rootElement);
rootRegistry.set(rootElement, root);
root.render(App());
for (const hook of window.__pw_hooks_after_mount || [])
await hook({ hooksConfig });
};
window.playwrightUnmount = async rootElement => {
if (!ReactDOM.unmountComponentAtNode(rootElement))
const root = rootRegistry.get(rootElement);
if (root === undefined)
throw new Error('Component was not mounted');
root.unmount();
rootRegistry.delete(rootElement);
};
window.playwrightUpdate = async (rootElement, component) => {
ReactDOM.render(render(/** @type {Component} */(component)), rootElement);
const root = rootRegistry.get(rootElement);
if (root === undefined)
throw new Error('Component was not mounted');
root.render(render(/** @type {Component} */ (component)));
};

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

@ -0,0 +1,12 @@
**/*
!README.md
!LICENSE
!cli.js
!register.d.ts
!register.mjs
!registerSource.mjs
!index.d.ts
!index.js
!hooks.d.ts
!hooks.mjs

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

@ -0,0 +1,3 @@
> **BEWARE** This package is EXPERIMENTAL and does not respect semver.
Read more at https://playwright.dev/docs/test-components

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

@ -0,0 +1,17 @@
#!/usr/bin/env node
/**
* 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.
*/
module.exports = require('playwright-core/cli');

26
packages/playwright-ct-react17/hooks.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,26 @@
/**
* 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.
*/
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[];
type JsonObject = { [Key in string]?: JsonValue };
export declare function beforeMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
): void;
export declare function afterMount<HooksConfig extends JsonObject>(
callback: (params: { hooksConfig: HooksConfig }) => Promise<void>
): void;

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

@ -0,0 +1,29 @@
/**
* 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.
*/
const __pw_hooks_before_mount = [];
const __pw_hooks_after_mount = [];
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
export const beforeMount = callback => {
__pw_hooks_before_mount.push(callback);
};
export const afterMount = callback => {
__pw_hooks_after_mount.push(callback);
};

70
packages/playwright-ct-react17/index.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,70 @@
/**
* 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 type {
TestType,
PlaywrightTestArgs,
PlaywrightTestConfig as BasePlaywrightTestConfig,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
Locator,
} from '@playwright/test';
import type { InlineConfig } from 'vite';
export type PlaywrightTestConfig<T = {}, W = {}> = Omit<BasePlaywrightTestConfig<T, W>, 'use'> & {
use?: BasePlaywrightTestConfig<T, W>['use'] & {
ctPort?: number;
ctTemplateDir?: string;
ctCacheDir?: string;
ctViteConfig?: InlineConfig | (() => Promise<InlineConfig>);
};
};
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[];
type JsonObject = { [Key in string]?: JsonValue };
export interface MountOptions<HooksConfig extends JsonObject> {
hooksConfig?: HooksConfig;
}
interface MountResult extends Locator {
unmount(): Promise<void>;
update(component: JSX.Element): Promise<void>;
}
export interface ComponentFixtures {
mount<HooksConfig extends JsonObject>(
component: JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;
}
export const test: TestType<
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures,
PlaywrightWorkerArgs & PlaywrightWorkerOptions
>;
/**
* Defines Playwright config
*/
export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export { expect, devices } from '@playwright/test';

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

@ -0,0 +1,31 @@
/**
* 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.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/test/lib/mount');
const path = require('path');
const plugin = () => {
// Only fetch upon request to avoid resolution in workers.
const { createPlugin } = require('@playwright/test/lib/plugins/vitePlugin');
return createPlugin(
path.join(__dirname, 'registerSource.mjs'),
() => import('@vitejs/plugin-react').then(plugin => plugin.default()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

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

@ -0,0 +1,36 @@
{
"name": "@playwright/experimental-ct-react17",
"version": "1.32.0-next",
"description": "Playwright Component Testing for React",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
"engines": {
"node": ">=14"
},
"author": {
"name": "Microsoft Corporation"
},
"license": "Apache-2.0",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
},
"./register": {
"types": "./register.d.ts",
"default": "./register.mjs"
},
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"dependencies": {
"@vitejs/plugin-react": "^3.1.0",
"@playwright/test": "1.32.0-next",
"vite": "^4.1.1"
},
"bin": {
"playwright": "./cli.js"
}
}

19
packages/playwright-ct-react17/register.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,19 @@
/**
* 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.
*/
export default function register(
components: { [key: string]: any },
): void

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

@ -0,0 +1,21 @@
/**
* 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 { register } from './registerSource.mjs';
export default components => {
register(components);
};

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

@ -0,0 +1,93 @@
/**
* 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.
*/
// @ts-check
// This file is injected into the registry as text, no dependencies are allowed.
import React from 'react';
import ReactDOM from 'react-dom';
/** @typedef {import('../playwright-test/types/component').Component} Component */
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
/** @type {Map<string, FrameworkComponent>} */
const registry = new Map();
/**
* @param {{[key: string]: FrameworkComponent}} components
*/
export function register(components) {
for (const [name, value] of Object.entries(components))
registry.set(name, value);
}
/**
* @param {Component} component
* @returns {JSX.Element}
*/
function render(component) {
let componentFunc = registry.get(component.type);
if (!componentFunc) {
// Lookup by shorthand.
for (const [name, value] of registry) {
if (component.type.endsWith(`_${name}`)) {
componentFunc = value;
break;
}
}
}
if (!componentFunc && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...registry.keys()]}`);
const componentFuncOrString = componentFunc || component.type;
if (component.kind !== 'jsx')
throw new Error('Object mount notation is not supported');
return React.createElement(componentFuncOrString, component.props, ...component.children.map(child => {
if (typeof child === 'string')
return child;
return render(child);
}).filter(child => {
if (typeof child === 'string')
return !!child.trim();
return true;
}));
}
window.playwrightMount = async (component, rootElement, hooksConfig) => {
let App = () => render(component);
for (const hook of window.__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig });
if (wrapper)
App = () => wrapper;
}
ReactDOM.render(App(), rootElement);
for (const hook of window.__pw_hooks_after_mount || [])
await hook({ hooksConfig });
};
window.playwrightUnmount = async rootElement => {
if (!ReactDOM.unmountComponentAtNode(rootElement))
throw new Error('Component was not mounted');
};
window.playwrightUpdate = async (rootElement, component) => {
ReactDOM.render(render(/** @type {Component} */(component)), rootElement);
};

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

@ -9,19 +9,19 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.4.2"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.6.1"
},
"devDependencies": {
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.0.0",
"typescript": "^4.5.4",
"vite": "^4.1.1"
},
"@standaloneDevDependencies": {
"@playwright/experimental-ct-react": "^1.22.2",
"@playwright/experimental-ct-react": "^1.22.0",
"@playwright/test": "^1.22.2"
}
}

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

@ -1,4 +1,4 @@
import { useRef } from "react"
import { useLayoutEffect, useRef, useState } from "react"
type CounterProps = {
count?: number;
@ -9,11 +9,17 @@ import { useRef } from "react"
let _remountCount = 1;
export default function Counter(props: CounterProps) {
const remountCount = useRef(_remountCount++);
const [remountCount] = useState(_remountCount);
const didMountRef = useRef(false)
useLayoutEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
_remountCount++;
}
}, [])
return <div onClick={() => props.onClick?.('hello')}>
<div id="props">{ props.count }</div>
<div id="remount-count">{ remountCount.current }</div>
<div id="remount-count">{ remountCount }</div>
{ props.children }
</div>
}

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

@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './assets/index.css';
ReactDOM.render(
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter><App /></BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
)
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

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

@ -3,19 +3,19 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.4.2"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.6.1"
},
"devDependencies": {
"@types/node": "^16.11.26",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"react-scripts": "5.0.0",
"typescript": "^4.6.2"
},
"@standaloneDevDependencies": {
"@playwright/experimental-ct-react": "^1.22.2",
"@playwright/experimental-ct-react": "^1.2.2",
"@playwright/test": "^1.22.2"
},
"scripts": {

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

@ -1,4 +1,4 @@
import { useRef } from "react"
import { useLayoutEffect, useRef, useState } from "react"
type CounterProps = {
count?: number;
@ -9,11 +9,17 @@ import { useRef } from "react"
let _remountCount = 1;
export default function Counter(props: CounterProps) {
const remountCount = useRef(_remountCount++);
const [remountCount] = useState(_remountCount);
const didMountRef = useRef(false)
useLayoutEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
_remountCount++;
}
}, [])
return <div onClick={() => props.onClick?.('hello')}>
<div id="props">{ props.count }</div>
<div id="remount-count">{ remountCount.current }</div>
<div id="remount-count">{ remountCount }</div>
{ props.children }
</div>
}

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

@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './assets/index.css';
ReactDOM.render(
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter><App /></BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

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

@ -45,7 +45,7 @@ test('should work with the empty component list', async ({ runInlineTest }, test
expect(metainfo.version).toEqual(require('playwright-core/package.json').version);
expect(metainfo.viteVersion).toEqual(require('vite/package.json').version);
expect(Object.entries(metainfo.tests)).toHaveLength(1);
expect(Object.entries(metainfo.sources)).toHaveLength(8);
expect(Object.entries(metainfo.sources)).toHaveLength(9);
});
test('should extract component list', async ({ runInlineTest }, testInfo) => {

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

@ -34,6 +34,7 @@
"packages/*/lib",
"packages/html-reporter",
"packages/playwright-ct-react",
"packages/playwright-ct-react17",
"packages/playwright-ct-solid",
"packages/playwright-ct-svelte",
"packages/playwright-ct-vue",

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

@ -178,6 +178,11 @@ const workspace = new Workspace(ROOT_PATH, [
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react'),
files: ['LICENSE'],
}),
new PWPackage({
name: '@playwright/experimental-ct-react17',
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react17'),
files: ['LICENSE'],
}),
new PWPackage({
name: '@playwright/experimental-ct-solid',
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-solid'),