참고소스 수정본
1
참고/playwright-main/tests/components/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json
|
||||
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
@@ -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 |
@@ -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
@@ -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()]
|
||||
})
|
||||
23
참고/playwright-main/tests/components/ct-react17/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
46
참고/playwright-main/tests/components/ct-react17/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
40
참고/playwright-main/tests/components/ct-react17/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "ct-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^6.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"eject": "react-scripts eject",
|
||||
"typecheck": "echo \"typecheck disabled because of zod v4 being incompatible with ts 4\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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-react17';
|
||||
import { resolve } from 'path';
|
||||
|
||||
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: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './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" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>React 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-react17/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`);
|
||||
});
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
21
참고/playwright-main/tests/components/ct-react17/src/App.tsx
Normal file
@@ -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,20 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<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,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,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type DelayedData = {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export default function DelayedData(props: DelayedData) {
|
||||
const [status, setStatus] = useState('loading');
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setStatus(props.data), 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [props.data])
|
||||
|
||||
return <p>{status}</p>
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function EmptyFragment(props: unknown) {
|
||||
Object.assign(window, { props });
|
||||
return <>{[]}</>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
type FetchProps = {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function Fetch(props: FetchProps) {
|
||||
const [data, setData] = useState('no response yet');
|
||||
useEffect(() => {
|
||||
fetch(props.url).then(res => res.text()).then(setData);
|
||||
}, [props.url]);
|
||||
return <p>{data}</p>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function MultiRoot() {
|
||||
return <>
|
||||
<div>root 1</div>
|
||||
<div>root 2</div>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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>
|
||||
}
|
||||
13
참고/playwright-main/tests/components/ct-react17/src/index.js
Normal file
@@ -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-react17/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react17';
|
||||
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-react17';
|
||||
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-react17';
|
||||
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,76 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react17';
|
||||
import Fetch from '@/components/Fetch';
|
||||
import DelayedData from '@/components/DelayedData';
|
||||
import Button from '@/components/Button';
|
||||
import EmptyFragment from '@/components/EmptyFragment';
|
||||
const { serverFixtures } = require('../../../../tests/config/serverFixtures');
|
||||
|
||||
test('render props', async ({ mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render attributes', async ({ mount }) => {
|
||||
const component = await mount(<Button className="primary" title="Submit" />);
|
||||
await expect(component).toHaveClass('primary');
|
||||
});
|
||||
|
||||
test('render delayed data', async ({ mount }) => {
|
||||
const component = await mount(<DelayedData data="complete" />);
|
||||
await expect(component).toHaveText('complete');
|
||||
});
|
||||
|
||||
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('');
|
||||
});
|
||||
|
||||
const testWithServer = test.extend(serverFixtures);
|
||||
testWithServer(
|
||||
'components routing should go through context',
|
||||
// @ts-ignore "serverFixtures" are imported from the impl without any types
|
||||
async ({ mount, context, server }) => {
|
||||
server.setRoute('/hello', (req: any, res: any) => {
|
||||
res.write('served via server');
|
||||
res.end();
|
||||
});
|
||||
|
||||
let markRouted: (url: string) => void;
|
||||
const routedViaContext = new Promise((res) => (markRouted = res));
|
||||
await context.route('**/hello', async (route, request) => {
|
||||
markRouted(`${request.method()} ${request.url()}`);
|
||||
await route.fulfill({
|
||||
body: 'intercepted',
|
||||
});
|
||||
});
|
||||
|
||||
const whoServedTheRequest = Promise.race([
|
||||
server
|
||||
.waitForRequest('/hello')
|
||||
.then((req: any) => `served via server: ${req.method} ${req.url}`),
|
||||
routedViaContext.then((req) => `served via context: ${req}`),
|
||||
]);
|
||||
|
||||
const component = await mount(<Fetch url={server.PREFIX + '/hello'} />);
|
||||
await expect
|
||||
.soft(whoServedTheRequest)
|
||||
.resolves.toMatch(/served via context: GET.*\/hello.*/i);
|
||||
await expect.soft(component).toHaveText('intercepted');
|
||||
}
|
||||
);
|
||||
|
||||
test('should return 404 if server does not handle the request', async ({ page }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23364' });
|
||||
const helloPromise = page.waitForResponse('/hello');
|
||||
const statusCode = await page.evaluate(async () => {
|
||||
const response = await fetch('/hello');
|
||||
return response.status;
|
||||
});
|
||||
expect(statusCode).toBe(404);
|
||||
const response = await helloPromise;
|
||||
expect(response.status()).toBe(404);
|
||||
expect(response.statusText()).toBe('Not Found');
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react17';
|
||||
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 ({ page, mount }) => {
|
||||
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-react17';
|
||||
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');
|
||||
});
|
||||
32
참고/playwright-main/tests/components/ct-react17/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"*": ["_"],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
**/*
|
||||
23
참고/playwright-main/tests/components/ct-vue-cli/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
*.tsbuildinfo
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
참고/playwright-main/tests/components/ct-vue-cli/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# ct-vue-cli
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
47
참고/playwright-main/tests/components/ct-vue-cli/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "ct-vue-cli",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"typecheck": "tsc --noEmit --project tsconfig.test.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.2.36",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/eslint-parser": "^7.22.15",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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-vue';
|
||||
import { resolve } from 'path';
|
||||
|
||||
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: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue/hooks';
|
||||
import { router } from '../src/router';
|
||||
import Button from '../src/components/Button.vue';
|
||||
import '../src/assets/index.css';
|
||||
|
||||
export type HooksConfig = {
|
||||
route?: string;
|
||||
routing?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
|
||||
if (hooksConfig?.routing)
|
||||
app.use(router as any);
|
||||
app.component('Button', Button);
|
||||
console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`);
|
||||
});
|
||||
|
||||
afterMount<HooksConfig>(async ({ instance }) => {
|
||||
console.log(`After mount el: ${instance.$el.constructor.name}`);
|
||||
});
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="./assets/logo.png" width="125" height="125" />
|
||||
<router-link to="/">Login</router-link>
|
||||
<router-link to="/dashboard">Dashboard</router-link>
|
||||
</header>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAttrs } from 'vue';
|
||||
defineProps<{ title: string }>();
|
||||
const attrs = useAttrs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
@click="$emit('submit', 'hello')"
|
||||
@dblclick="() => attrs.dbclick('fallthroughEvent')"
|
||||
>
|
||||
{{ title }}
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>test</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<button @click="$emit('submit', 'hello')">
|
||||
<span data-testid="props">{{ count }}</span>
|
||||
<span data-testid="remount-count">{{ remountCount }}</span>
|
||||
<slot name="main" />
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
let remountCount = 0
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
count: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
remountCount++
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Welcome!</h1>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<footer>
|
||||
Thanks for visiting.
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSlots } from 'vue';
|
||||
const slots = useSlots();
|
||||
Object.assign(window, { slots });
|
||||
</script>
|
||||
<template>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<div>root 1</div>
|
||||
<div>root 2</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<header>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<main>
|
||||
<slot name="main" />
|
||||
</main>
|
||||
<footer>
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
import { router } from './router';
|
||||
import App from './App.vue';
|
||||
import './assets/index.css';
|
||||
|
||||
createApp(App).use(router).mount('#app');
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<main>Dashboard</main>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<main>Login</main>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
import DashboardPage from '../pages/DashboardPage.vue';
|
||||
import LoginPage from '../pages/LoginPage.vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory('/'),
|
||||
routes: [
|
||||
{ path: '/', component: LoginPage },
|
||||
{ path: '/dashboard', component: DashboardPage },
|
||||
],
|
||||
})
|
||||
4
참고/playwright-main/tests/components/ct-vue-cli/src/vue.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.vue' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-vue';
|
||||
import Button from '@/components/Button.vue';
|
||||
|
||||
test('emit an submit event when the button is clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
on: {
|
||||
submit: (data: string) => messages.push(data),
|
||||
},
|
||||
});
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
|
||||
test('emit a fallthrough event when the button is double clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
on: {
|
||||
dbclick: (message: string) => messages.push(message),
|
||||
},
|
||||
});
|
||||
await component.dblclick();
|
||||
expect(messages).toEqual(['fallthroughEvent']);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-vue';
|
||||
import Button from '@/components/Button.vue';
|
||||
import DefaultSlot from '@/components/DefaultSlot.vue';
|
||||
|
||||
test('emit an submit event when the button is clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(
|
||||
<Button
|
||||
title="Submit"
|
||||
v-on:submit={(data: string) => {
|
||||
messages.push(data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
|
||||
test('emit a fallthrough event when the button is double clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(
|
||||
<Button
|
||||
title="Submit"
|
||||
v-on:dbclick={(message: string) => {
|
||||
messages.push(message)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await component.dblclick();
|
||||
expect(messages).toEqual(['fallthroughEvent']);
|
||||
});
|
||||
|
||||
test('emit a event when a slot is clicked', async ({ mount }) => {
|
||||
let clickFired = false;
|
||||
const component = await mount(
|
||||
<DefaultSlot>
|
||||
<span v-on:click={() => (clickFired = true)}>Main Content</span>
|
||||
</DefaultSlot>
|
||||
);
|
||||
await component.getByText('Main Content').click();
|
||||
expect(clickFired).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-vue';
|
||||
import Button from '@/components/Button.vue';
|
||||
import Component from '@/components/Component.vue';
|
||||
import EmptyTemplate from '@/components/EmptyTemplate.vue';
|
||||
|
||||
test('render props', async ({ mount }) => {
|
||||
const component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
});
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render a component without options', async ({ mount }) => {
|
||||
const component = await mount(Component);
|
||||
await expect(component).toContainText('test');
|
||||
});
|
||||
|
||||
test('get textContent of the empty template', async ({ mount }) => {
|
||||
const component = await mount(EmptyTemplate);
|
||||
expect(await component.allTextContents()).toEqual(['']);
|
||||
expect(await component.textContent()).toBe('');
|
||||
await expect(component).toHaveText('');
|
||||
});
|
||||