참고소스 수정본
This commit is contained in:
24
참고/playwright-main/tests/components/ct-react-vite/.gitignore
vendored
Normal file
24
참고/playwright-main/tests/components/ct-react-vite/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
13
참고/playwright-main/tests/components/ct-react-vite/index.html
Normal file
13
참고/playwright-main/tests/components/ct-react-vite/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "ct-react-vite",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"msw": "^2.3.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 { defineConfig, devices } from '@playwright/experimental-ct-react';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? 'html' : 'line',
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
ctViteConfig: {
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src'),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import '../src/assets/index.css';
|
||||
|
||||
export type HooksConfig = {
|
||||
route?: string;
|
||||
routing?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ hooksConfig, App }) => {
|
||||
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
|
||||
|
||||
if (hooksConfig?.routing)
|
||||
return <BrowserRouter><App /></BrowserRouter>;
|
||||
});
|
||||
|
||||
afterMount<HooksConfig>(async () => {
|
||||
console.log(`After mount`);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Routes, Route, Link } from 'react-router-dom';
|
||||
import logo from './assets/logo.svg';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
|
||||
export default function App({ title }: { title?: string }) {
|
||||
return <>
|
||||
<header>
|
||||
<img src={logo} alt="logo" width={125} height={125} />
|
||||
{title && <h1>{title}</h1>}
|
||||
<Link to="/">Login</Link>
|
||||
<Link to="/dashboard">Dashboard</Link>
|
||||
</header>
|
||||
<Routes>
|
||||
<Route path="/">
|
||||
<Route index element={<LoginPage />} />
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #e3e3e3;
|
||||
background-color: #1b1b1d;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'pwtest-iconfont';
|
||||
/* See tests/assets/webfont/README.md */
|
||||
src: url('./iconfont.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
type ButtonProps = {
|
||||
title: string;
|
||||
onClick?(props: string): void;
|
||||
className?: string;
|
||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;
|
||||
|
||||
export default function Button({ onClick, title, ...attributes }: ButtonProps) {
|
||||
return <button {...attributes} onClick={() => onClick?.('hello')}>
|
||||
{title}
|
||||
</button>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type DefaultChildrenProps = PropsWithChildren<{}>;
|
||||
|
||||
export default function CheckChildrenProp(props: DefaultChildrenProps) {
|
||||
return <>{'children' in props ? props.children : 'No Children'}</>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type ComponentAsProp = {
|
||||
component: ReactNode[] | ReactNode;
|
||||
};
|
||||
|
||||
export function ComponentAsProp({ component }: ComponentAsProp) {
|
||||
return <div>{component}</div>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useLayoutEffect, useRef, useState } from "react"
|
||||
|
||||
type CounterProps = {
|
||||
count?: number;
|
||||
onClick?(props: string): void;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
let _remountCount = 1;
|
||||
|
||||
export default function Counter(props: CounterProps) {
|
||||
const [remountCount] = useState(_remountCount);
|
||||
const didMountRef = useRef(false)
|
||||
useLayoutEffect(() => {
|
||||
if (!didMountRef.current) {
|
||||
didMountRef.current = true;
|
||||
_remountCount++;
|
||||
}
|
||||
}, [])
|
||||
return <button onClick={() => props.onClick?.('hello')}>
|
||||
<span data-testid="props">{ props.count }</span>
|
||||
<span data-testid="remount-count">{ remountCount }</span>
|
||||
{ props.children }
|
||||
</button>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
type DefaultChildrenProps = {
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export default function DefaultChildren(props: DefaultChildrenProps) {
|
||||
return <div>
|
||||
<h1>Welcome!</h1>
|
||||
<main>
|
||||
{props.children}
|
||||
</main>
|
||||
<footer>
|
||||
Thanks for visiting.
|
||||
</footer>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function EmptyFragment(props: unknown) {
|
||||
Object.assign(window, { props });
|
||||
return <>{[]}</>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export default function Fetcher() {
|
||||
const [data, setData] = useState<{ name: string }>({ name: '<none>' });
|
||||
const [fetched, setFetched] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const doFetch = async () => {
|
||||
try {
|
||||
const response = await fetch('/data.json');
|
||||
setData(await response.json());
|
||||
} catch {
|
||||
setData({ name: '<error>' });
|
||||
}
|
||||
setFetched(true);
|
||||
}
|
||||
|
||||
if (!fetched)
|
||||
doFetch();
|
||||
}, [fetched, setFetched, setData]);
|
||||
|
||||
return <div>
|
||||
<div data-testid='name'>{data.name}</div>
|
||||
<button onClick={() => {
|
||||
setFetched(false);
|
||||
setData({ name: '<none>' });
|
||||
}}>Reset</button>
|
||||
<button onClick={() => {
|
||||
fetch('/post', { method: 'POST', body: 'hello from the page' });
|
||||
}}>Post it</button>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function MultiRoot() {
|
||||
return <>
|
||||
<div>root 1</div>
|
||||
<div>root 2</div>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
type MultipleChildrenProps = {
|
||||
children?: [any, any, any];
|
||||
}
|
||||
|
||||
export default function MultipleChildren(props: MultipleChildrenProps) {
|
||||
return <div>
|
||||
<header>
|
||||
{props.children?.at(0)}
|
||||
</header>
|
||||
<main>
|
||||
{props.children?.at(1)}
|
||||
</main>
|
||||
<footer>
|
||||
{props.children?.at(2)}
|
||||
</footer>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.title-with-font {
|
||||
font-family: pwtest-iconfont, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import './TitleWithFont.css';
|
||||
|
||||
export default function TitleWithFont() {
|
||||
return <div className='title-with-font'>+-</div>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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';
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function DashboardPage() {
|
||||
return <main>Dashboard</main>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function LoginPage() {
|
||||
return <main>Login</main>
|
||||
}
|
||||
1
참고/playwright-main/tests/components/ct-react-vite/src/vite-env.d.ts
vendored
Normal file
1
참고/playwright-main/tests/components/ct-react-vite/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import Button from '@/components/Button';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
|
||||
test('execute callback when the button is clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(
|
||||
<Button
|
||||
title="Submit"
|
||||
onClick={(data) => {
|
||||
messages.push(data);
|
||||
}}
|
||||
></Button>
|
||||
);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
|
||||
test('execute callback when a child node is clicked', async ({ mount }) => {
|
||||
let clickFired = false;
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<span onClick={() => (clickFired = true)}>Main Content</span>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await component.getByText('Main Content').click();
|
||||
expect(clickFired).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import Button from '@/components/Button';
|
||||
import CheckChildrenProp from '@/components/CheckChildrenProp';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
import MultipleChildren from '@/components/MultipleChildren';
|
||||
|
||||
test('render a default child', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>Main Content</DefaultChildren>
|
||||
);
|
||||
await expect(component).toContainText('Main Content');
|
||||
});
|
||||
|
||||
test('render a component as child', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<Button title="Submit" />
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render multiple children', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<div data-testid="one">One</div>
|
||||
<div data-testid="two">Two</div>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByTestId('one')).toContainText('One');
|
||||
await expect(component.getByTestId('two')).toContainText('Two');
|
||||
});
|
||||
|
||||
test('render named children', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<MultipleChildren>
|
||||
<div>Header</div>
|
||||
<div>Main Content</div>
|
||||
<div>Footer</div>
|
||||
</MultipleChildren>
|
||||
);
|
||||
await expect(component).toContainText('Header');
|
||||
await expect(component).toContainText('Main Content');
|
||||
await expect(component).toContainText('Footer');
|
||||
});
|
||||
|
||||
test('render string as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{'string'}</DefaultChildren>);
|
||||
await expect(component).toContainText('string');
|
||||
});
|
||||
|
||||
test('render array as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{[<h4>{[4]}</h4>,[[<p>[2,3]</p>]]]}</DefaultChildren>);
|
||||
await expect(component.getByRole('heading', { level: 4 })).toHaveText('4');
|
||||
await expect(component.getByRole('paragraph')).toHaveText('[2,3]');
|
||||
});
|
||||
|
||||
test('render number as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{1337}</DefaultChildren>);
|
||||
await expect(component).toContainText('1337');
|
||||
});
|
||||
|
||||
test('absence of children when children prop is not provided', async ({ mount }) => {
|
||||
const component = await mount(<CheckChildrenProp />);
|
||||
await expect(component).toContainText('No Children');
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import App from '@/App';
|
||||
import type { HooksConfig } from '../playwright';
|
||||
|
||||
test('navigate to a page by clicking a link', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(<App />, {
|
||||
hooksConfig: { routing: true },
|
||||
});
|
||||
await expect(component.getByRole('main')).toHaveText('Login');
|
||||
await expect(page).toHaveURL('/');
|
||||
await component.getByRole('link', { name: 'Dashboard' }).click();
|
||||
await expect(component.getByRole('main')).toHaveText('Dashboard');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
|
||||
test('update should not reset mount hooks', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(<App title='before'/>, {
|
||||
hooksConfig: { routing: true },
|
||||
});
|
||||
await expect(component.getByRole('heading')).toHaveText('before');
|
||||
await expect(component.getByRole('main')).toHaveText('Login');
|
||||
|
||||
await component.update(<App title='after'/>);
|
||||
await expect(component.getByRole('heading')).toHaveText('after');
|
||||
await expect(component.getByRole('main')).toHaveText('Login');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
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" />);
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render component as props', async ({ mount }) => {
|
||||
const component = await mount(<ComponentAsProp component={<Button title="Submit" />} />);
|
||||
await expect(component.getByRole('button', { name: 'submit' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('render jsx array as props', async ({ mount }) => {
|
||||
const component = await mount(<ComponentAsProp component={[<h4>{[4]}</h4>,[[<p>[2,3]</p>]]]} />);
|
||||
await expect(component.getByRole('heading', { level: 4 })).toHaveText('4');
|
||||
await expect(component.getByRole('paragraph')).toHaveText('[2,3]');
|
||||
});
|
||||
|
||||
test('render attributes', async ({ mount }) => {
|
||||
const component = await mount(<Button className="primary" title="Submit" />);
|
||||
await expect(component).toHaveClass('primary');
|
||||
});
|
||||
|
||||
test('render an empty component', async ({ mount, page }) => {
|
||||
const component = await mount(<EmptyFragment />);
|
||||
expect(await page.evaluate(() => 'props' in window && window.props)).toEqual({});
|
||||
expect(await component.allTextContents()).toEqual(['']);
|
||||
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.');
|
||||
});
|
||||
|
||||
test('render Fragment shorthand notation', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32853' } }, async ({ mount }) => {
|
||||
const component = await mount(<>Learn React</>);
|
||||
await expect(component).toContainText('Learn React');
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import TitleWithFont from '@/components/TitleWithFont';
|
||||
import Fetcher from '@/components/Fetcher';
|
||||
import { http, HttpResponse, passthrough, bypass } from 'msw';
|
||||
import httpServer from 'http';
|
||||
import type net from 'net';
|
||||
|
||||
test('should load font without routes', async ({ mount, page }) => {
|
||||
const promise = page.waitForEvent('requestfinished', request => request.url().includes('iconfont'));
|
||||
await mount(<TitleWithFont />);
|
||||
const request = await promise;
|
||||
const response = await request.response();
|
||||
const body = await response!.body();
|
||||
expect(body.length).toBe(348);
|
||||
});
|
||||
|
||||
test('should load font with routes', async ({ mount, page }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27294' });
|
||||
await page.route('**/*.json', r => r.continue());
|
||||
const promise = page.waitForEvent('requestfinished', request => request.url().includes('iconfont'));
|
||||
await mount(<TitleWithFont />);
|
||||
const request = await promise;
|
||||
const response = await request.response();
|
||||
const body = await response!.body();
|
||||
expect(body.length).toBe(348);
|
||||
});
|
||||
|
||||
test.describe('request handlers', () => {
|
||||
test('should handle requests', async ({ page, mount, router }) => {
|
||||
let respond: (() => void) = () => {};
|
||||
const promise = new Promise<void>(f => respond = f);
|
||||
|
||||
let postReceived: ((body: string) => void) = () => {};
|
||||
const postBody = new Promise<string>(f => postReceived = f);
|
||||
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
await promise;
|
||||
return HttpResponse.json({ name: 'John Doe' });
|
||||
}),
|
||||
http.post('/post', async ({ request }) => {
|
||||
postReceived(await request.text());
|
||||
return HttpResponse.text('ok');
|
||||
}),
|
||||
);
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<none>');
|
||||
|
||||
respond();
|
||||
await expect(component.getByTestId('name')).toHaveText('John Doe');
|
||||
|
||||
await component.getByRole('button', { name: 'Post it' }).click();
|
||||
expect(await postBody).toBe('hello from the page');
|
||||
});
|
||||
|
||||
test('should add dynamically', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<original>');
|
||||
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
return HttpResponse.json({ name: 'John Doe' });
|
||||
}),
|
||||
);
|
||||
|
||||
await component.getByRole('button', { name: 'Reset' }).click();
|
||||
await expect(component.getByTestId('name')).toHaveText('John Doe');
|
||||
});
|
||||
|
||||
test('should passthrough', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
return passthrough();
|
||||
}),
|
||||
);
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<error>');
|
||||
});
|
||||
|
||||
test('should fallback when nothing is returned', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
let called = false;
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
called = true;
|
||||
}),
|
||||
);
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<original>');
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
test('should bypass(request)', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: `<original>` }) });
|
||||
});
|
||||
|
||||
await router.use(
|
||||
http.get('/data.json', async ({ request }) => {
|
||||
return await fetch(bypass(request));
|
||||
}),
|
||||
);
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<error>');
|
||||
});
|
||||
|
||||
test('should bypass(url) and get cookies', async ({ page, mount, router, browserName }) => {
|
||||
let cookie = '';
|
||||
const server = new httpServer.Server();
|
||||
server.on('request', (req, res) => {
|
||||
cookie = req.headers['cookie']!;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ name: '<server>' }));
|
||||
});
|
||||
await new Promise<void>(f => server.listen(0, f));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: `<original>` }) });
|
||||
});
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<original>');
|
||||
|
||||
await page.evaluate(() => document.cookie = 'foo=bar');
|
||||
await router.use(
|
||||
http.get('/data.json', async ({ request }) => {
|
||||
if (browserName !== 'webkit') {
|
||||
// WebKit does not have cookies while intercepting.
|
||||
expect(request.headers.get('cookie')).toBe('foo=bar');
|
||||
}
|
||||
return await fetch(bypass(`http://localhost:${port}`));
|
||||
}),
|
||||
);
|
||||
await component.getByRole('button', { name: 'Reset' }).click();
|
||||
await expect(component.getByTestId('name')).toHaveText('<server>');
|
||||
|
||||
expect(cookie).toBe('foo=bar');
|
||||
await new Promise(f => server.close(f));
|
||||
});
|
||||
|
||||
test('should ignore navigation requests', async ({ page, mount, router }) => {
|
||||
await router.route('**/newpage', async route => {
|
||||
await route.fulfill({ body: `<div>original</div>`, contentType: 'text/html' });
|
||||
});
|
||||
|
||||
await router.use(
|
||||
http.get('/newpage', async ({ request }) => {
|
||||
return new Response(`<div>intercepted</div>`, {
|
||||
headers: new Headers({ 'Content-Type': 'text/html' }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await mount(<div />);
|
||||
await page.goto('/newpage');
|
||||
await expect(page.locator('div')).toHaveText('original');
|
||||
});
|
||||
|
||||
test('should throw when calling fetch(bypass) outside of a handler', async ({ page, router, baseURL }) => {
|
||||
await router.use(http.get('/data.json', async () => {}));
|
||||
|
||||
const error = await fetch(bypass(baseURL + '/hello')).catch(e => e);
|
||||
expect(error.message).toContain(`Cannot call fetch(bypass()) outside of a request handler`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import Button from '@/components/Button';
|
||||
import MultiRoot from '@/components/MultiRoot';
|
||||
|
||||
test('unmount', async ({ page, mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await expect(page.locator('#root')).toContainText('Submit');
|
||||
await component.unmount();
|
||||
await expect(page.locator('#root')).not.toContainText('Submit');
|
||||
});
|
||||
|
||||
test('unmount a multi root component', async ({ mount, page }) => {
|
||||
const component = await mount(<MultiRoot />);
|
||||
await expect(page.locator('#root')).toContainText('root 1');
|
||||
await expect(page.locator('#root')).toContainText('root 2');
|
||||
await component.unmount();
|
||||
await expect(page.locator('#root')).not.toContainText('root 1');
|
||||
await expect(page.locator('#root')).not.toContainText('root 2');
|
||||
});
|
||||
|
||||
test('unmount twice throws an error', async ({ mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import Counter from '@/components/Counter';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
|
||||
test('update props without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter count={9001} />);
|
||||
await expect(component.getByTestId('props')).toContainText('9001');
|
||||
|
||||
await component.update(<Counter count={1337} />);
|
||||
await expect(component).not.toContainText('9001');
|
||||
await expect(component.getByTestId('props')).toContainText('1337');
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('update child props without remounting', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren><Counter count={9001} /></DefaultChildren>);
|
||||
await expect(component.getByTestId('props')).toContainText('9001');
|
||||
|
||||
await component.update(<DefaultChildren><Counter count={1337} /></DefaultChildren>);
|
||||
await expect(component).not.toContainText('9001');
|
||||
await expect(component.getByTestId('props')).toContainText('1337');
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('update callbacks without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter />);
|
||||
|
||||
const messages: string[] = [];
|
||||
await component.update(
|
||||
<Counter
|
||||
onClick={(message) => {
|
||||
messages.push(message);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('update child callbacks without remounting', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren><Counter /></DefaultChildren>);
|
||||
|
||||
const messages: string[] = [];
|
||||
await component.update(
|
||||
<DefaultChildren>
|
||||
<Counter
|
||||
onClick={(message) => {
|
||||
messages.push(message);
|
||||
}}
|
||||
/>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await component.getByRole('button').click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('update children without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter>Default Slot</Counter>);
|
||||
await expect(component).toContainText('Default Slot');
|
||||
|
||||
await component.update(<Counter>Test Slot</Counter>);
|
||||
await expect(component).not.toContainText('Default Slot');
|
||||
await expect(component).toContainText('Test Slot');
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('update grandchild without remounting', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<Counter>Default Slot</Counter>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByRole('button')).toContainText('Default Slot');
|
||||
|
||||
await component.update(
|
||||
<DefaultChildren>
|
||||
<Counter>Test Slot</Counter>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByRole('button')).not.toContainText('Default Slot');
|
||||
await expect(component.getByRole('button')).toContainText('Test Slot');
|
||||
|
||||
await expect(component.getByTestId('remount-count')).toContainText('1');
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"*": ["_"],
|
||||
}
|
||||
},
|
||||
"include": ["src", "tests"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
Reference in New Issue
Block a user