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:
		
							parent
							
								
									be259dac7c
								
							
						
					
					
						commit
						fbaf56a13f
					
				| 
						 | 
				
			
			@ -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');
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue