참고소스 수정본
This commit is contained in:
50
참고/playwright-main/tests/config/baseTest.ts
Normal file
50
참고/playwright-main/tests/config/baseTest.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { mergeTests } from '@playwright/test';
|
||||
import { test } from '@playwright/test';
|
||||
import type { CommonFixtures, CommonWorkerFixtures } from './commonFixtures';
|
||||
import { commonFixtures } from './commonFixtures';
|
||||
import type { ServerFixtures, ServerWorkerOptions } from './serverFixtures';
|
||||
import { serverFixtures } from './serverFixtures';
|
||||
import { platformTest } from './platformFixtures';
|
||||
import { testModeTest } from './testModeFixtures';
|
||||
import type { Builtins } from '../../packages/injected/src/utilityScript';
|
||||
|
||||
export const base = test;
|
||||
|
||||
export const baseTest = mergeTests(base, platformTest, testModeTest)
|
||||
.extend<CommonFixtures, CommonWorkerFixtures>(commonFixtures)
|
||||
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures);
|
||||
|
||||
export function step<This extends Object, Args extends any[], Return>(
|
||||
target: (this: This, ...args: Args) => Promise<Return>,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>
|
||||
) {
|
||||
function replacementMethod(this: This, ...args: Args): Promise<Return> {
|
||||
const name = this.constructor.name + '.' + (context.name as string) + '(' + args.map(a => JSON.stringify(a)).join(',') + ')';
|
||||
return test.step(name, async () => {
|
||||
return await target.call(this, ...args);
|
||||
});
|
||||
}
|
||||
return replacementMethod;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
builtins: Builtins;
|
||||
}
|
||||
}
|
||||
220
참고/playwright-main/tests/config/browserTest.ts
Normal file
220
참고/playwright-main/tests/config/browserTest.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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 * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { baseTest } from './baseTest';
|
||||
import { RunServer, RemoteServer } from './remoteServer';
|
||||
import { utils } from '../../packages/playwright-core/lib/coreBundle';
|
||||
import { isBidiChannel, parseHar } from '../config/utils';
|
||||
import { createSkipTestPredicate } from '../bidi/expectationUtil';
|
||||
import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
|
||||
import type { RemoteServerOptions, PlaywrightServer } from './remoteServer';
|
||||
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
|
||||
import type { Log } from '../../packages/trace/src/har';
|
||||
import type { TestInfo } from '@playwright/test';
|
||||
|
||||
const { removeFolders, hostPlatform } = utils;
|
||||
|
||||
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
||||
browserVersion: string;
|
||||
defaultSameSiteCookieValue: string;
|
||||
allowsThirdParty: boolean;
|
||||
browserMajorVersion: number;
|
||||
browserType: BrowserType;
|
||||
isAndroid: boolean;
|
||||
isElectron: boolean;
|
||||
isHeadlessShell: boolean;
|
||||
isFrozenWebkit: boolean;
|
||||
nodeVersion: { major: number, minor: number, patch: number };
|
||||
isBidi: boolean;
|
||||
bidiTestSkipPredicate: (info: TestInfo) => boolean;
|
||||
};
|
||||
|
||||
interface StartRemoteServer {
|
||||
(kind: 'run-server' | 'launchServer', options?: RemoteServerOptions): Promise<PlaywrightServer>;
|
||||
(kind: 'launchServer', options?: RemoteServerOptions): Promise<RemoteServer>;
|
||||
}
|
||||
|
||||
type BrowserTestTestFixtures = PageTestFixtures & {
|
||||
createUserDataDir: () => Promise<string>;
|
||||
launchPersistent: (options?: Parameters<BrowserType['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>;
|
||||
startRemoteServer: StartRemoteServer;
|
||||
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||
pageWithHar(options?: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean }): Promise<{ context: BrowserContext, page: Page, getLog: () => Promise<Log>, getZip: () => Promise<Map<string, Buffer>> }>
|
||||
autoSkipBidiTest: void;
|
||||
};
|
||||
|
||||
const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>({
|
||||
browserVersion: [async ({ browser }, run) => {
|
||||
await run(browser.version());
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
browserType: [async ({ playwright, browserName, mode }, run) => {
|
||||
await run(playwright[browserName]);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
allowsThirdParty: [async ({ browserName, channel }, run) => {
|
||||
if (browserName === 'firefox')
|
||||
await run(true);
|
||||
else
|
||||
await run(false);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
defaultSameSiteCookieValue: [async ({ browserName, platform, channel, isBidi }, run) => {
|
||||
if (browserName === 'chromium' || isBidi)
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl'))
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit')
|
||||
await run('None'); // Windows + older macOS
|
||||
else if (browserName === 'firefox')
|
||||
await run('None');
|
||||
else
|
||||
throw new Error('unknown browser - ' + browserName);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
browserMajorVersion: [async ({ browserVersion }, run) => {
|
||||
await run(Number(browserVersion.split('.')[0]));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
nodeVersion: [async ({}, use) => {
|
||||
const [major, minor, patch] = process.versions.node.split('.');
|
||||
await use({ major: +major, minor: +minor, patch: +patch });
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
isBidi: [async ({ channel }, use) => {
|
||||
await use(isBidiChannel(channel));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
isAndroid: [false, { scope: 'worker' }],
|
||||
isElectron: [false, { scope: 'worker' }],
|
||||
electronMajorVersion: [0, { scope: 'worker' }],
|
||||
|
||||
isHeadlessShell: [async ({ browserName, channel, headless }, use) => {
|
||||
const isShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
const isToTShell = channel === 'chromium-tip-of-tree-headless-shell' || (channel === 'chromium-tip-of-tree' && headless);
|
||||
await use(browserName === 'chromium' && (isShell || isToTShell));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
isFrozenWebkit: [async ({ browserName, isMac, macVersion }, use) => {
|
||||
await use(browserName === 'webkit' && (hostPlatform.startsWith('debian11') || hostPlatform.startsWith('ubuntu20.04') || (isMac && macVersion < 15)));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
contextFactory: async ({ _contextFactory }: any, run) => {
|
||||
await run(async options => {
|
||||
const { context } = await _contextFactory(options);
|
||||
return context;
|
||||
});
|
||||
},
|
||||
|
||||
createUserDataDir: async ({ mode }, run) => {
|
||||
test.skip(mode.startsWith('service'));
|
||||
const dirs: string[] = [];
|
||||
// We do not put user data dir in testOutputPath,
|
||||
// because we do not want to upload them as test result artifacts.
|
||||
//
|
||||
// Additionally, it is impossible to upload user data dir after test run:
|
||||
// - Firefox removes lock file later, presumably from another watchdog process?
|
||||
// - WebKit has circular symlinks that makes CI go crazy.
|
||||
await run(async () => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-'));
|
||||
dirs.push(dir);
|
||||
return dir;
|
||||
});
|
||||
await removeFolders(dirs);
|
||||
},
|
||||
|
||||
launchPersistent: async ({ createUserDataDir, browserType, mode }, run) => {
|
||||
test.skip(mode !== 'default', 'Remote persistent contexts are not supported');
|
||||
|
||||
let persistentContext: BrowserContext | undefined;
|
||||
await run(async options => {
|
||||
if (persistentContext)
|
||||
throw new Error('can only launch one persistent context');
|
||||
const userDataDir = await createUserDataDir();
|
||||
persistentContext = await browserType.launchPersistentContext(userDataDir, { ...options });
|
||||
const page = persistentContext.pages()[0];
|
||||
return { context: persistentContext, page };
|
||||
});
|
||||
if (persistentContext)
|
||||
await persistentContext.close();
|
||||
},
|
||||
|
||||
startRemoteServer: async ({ childProcess, browserType, channel, mode }, run) => {
|
||||
test.skip(mode !== 'default', 'Starting remote server is not supported in remote modes');
|
||||
|
||||
let server: PlaywrightServer | undefined;
|
||||
const fn = async (kind: 'launchServer' | 'run-server', options?: RemoteServerOptions) => {
|
||||
if (server)
|
||||
throw new Error('can only start one remote server');
|
||||
if (kind === 'launchServer') {
|
||||
const remoteServer = new RemoteServer();
|
||||
await remoteServer._start(childProcess, browserType, channel, options);
|
||||
server = remoteServer;
|
||||
} else {
|
||||
const runServer = new RunServer();
|
||||
await runServer.start(childProcess, { artifactsDir: options?.artifactsDir });
|
||||
server = runServer;
|
||||
}
|
||||
return server;
|
||||
};
|
||||
await run(fn as any);
|
||||
if (server) {
|
||||
await server.close();
|
||||
// Give any connected browsers a chance to disconnect to avoid
|
||||
// poisoning next test with quasy-alive browsers.
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
}
|
||||
},
|
||||
pageWithHar: async ({ contextFactory }, use, testInfo) => {
|
||||
const pageWithHar = async (options: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean } = {}) => {
|
||||
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
|
||||
const context = await contextFactory({ recordHar: { path: harPath, content: options.content, omitContent: options.omitContent }, ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
return {
|
||||
page,
|
||||
context,
|
||||
getLog: async () => {
|
||||
await context.close();
|
||||
return JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log;
|
||||
},
|
||||
getZip: async () => {
|
||||
await context.close();
|
||||
return parseHar(harPath);
|
||||
},
|
||||
};
|
||||
};
|
||||
await use(pageWithHar);
|
||||
},
|
||||
|
||||
bidiTestSkipPredicate: [async ({ }, run) => {
|
||||
const filter = await createSkipTestPredicate(test.info().project.name);
|
||||
await run(filter);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
autoSkipBidiTest: [async ({ bidiTestSkipPredicate }, run) => {
|
||||
test.fixme(bidiTestSkipPredicate(test.info()), 'marked as timeout in bidi expectations');
|
||||
await run();
|
||||
}, { auto: true, scope: 'test' }],
|
||||
});
|
||||
|
||||
export const playwrightTest = test;
|
||||
export const browserTest = test;
|
||||
export const contextTest = test;
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
316
참고/playwright-main/tests/config/commonFixtures.ts
Normal file
316
참고/playwright-main/tests/config/commonFixtures.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures } from '@playwright/test';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import net from 'net';
|
||||
import fs from 'fs';
|
||||
import { inheritAndCleanEnv, stripAnsi } from './utils';
|
||||
|
||||
type TestChildParams = {
|
||||
command: string[],
|
||||
cwd?: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
shell?: boolean,
|
||||
onOutput?: () => void;
|
||||
};
|
||||
|
||||
import childProcess from 'child_process';
|
||||
|
||||
type ProcessData = {
|
||||
pid: number, // process ID
|
||||
pgrp: number, // process group ID
|
||||
children: Set<ProcessData>, // direct children of the process
|
||||
};
|
||||
|
||||
function readAllProcessesLinux(): { pid: number, ppid: number, pgrp: number }[] {
|
||||
const result: {pid: number, ppid: number, pgrp: number}[] = [];
|
||||
for (const dir of fs.readdirSync('/proc')) {
|
||||
const pid = +dir;
|
||||
if (isNaN(pid))
|
||||
continue;
|
||||
try {
|
||||
const statFile = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
||||
// Format of /proc/*/stat is described https://man7.org/linux/man-pages/man5/proc.5.html
|
||||
const match = statFile.match(/^(?<pid>\d+)\s+\((?<comm>.*)\)\s+(?<state>R|S|D|Z|T|t|W|X|x|K|W|P)\s+(?<ppid>\d+)\s+(?<pgrp>\d+)/);
|
||||
if (match && match.groups) {
|
||||
result.push({
|
||||
pid: +match.groups.pid,
|
||||
ppid: +match.groups.ppid,
|
||||
pgrp: +match.groups.pgrp,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// We don't have access to some /proc/<pid>/stat file.
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function readAllProcessesMacOS(): { pid: number, ppid: number, pgrp: number }[] {
|
||||
const result: {pid: number, ppid: number, pgrp: number}[] = [];
|
||||
const processTree = childProcess.spawnSync('ps', ['-eo', 'pid,pgid,ppid']);
|
||||
const lines = processTree.stdout.toString().trim().split('\n');
|
||||
for (const line of lines) {
|
||||
const [pid, pgrp, ppid] = line.trim().split(/\s+/).map(token => +token);
|
||||
// On linux, the very first line of `ps` is the header with "PID PGID PPID".
|
||||
if (isNaN(pid) || isNaN(pgrp) || isNaN(ppid))
|
||||
continue;
|
||||
result.push({ pid, ppid, pgrp });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildProcessTreePosix(pid: number): ProcessData | undefined {
|
||||
// Certain Linux distributions might not have `ps` installed.
|
||||
const allProcesses = process.platform === 'darwin' ? readAllProcessesMacOS() : readAllProcessesLinux();
|
||||
const pidToProcess = new Map<number, ProcessData>();
|
||||
for (const { pid, pgrp } of allProcesses)
|
||||
pidToProcess.set(pid, { pid, pgrp, children: new Set() });
|
||||
for (const { pid, ppid } of allProcesses) {
|
||||
const parent = pidToProcess.get(ppid);
|
||||
const child = pidToProcess.get(pid);
|
||||
// On POSIX, certain processes might not have parent (e.g. PID=1 and occasionally PID=2)
|
||||
// or we might not have access to it proc info.
|
||||
if (parent && child)
|
||||
parent.children.add(child);
|
||||
}
|
||||
return pidToProcess.get(pid);
|
||||
}
|
||||
|
||||
export class TestChildProcess {
|
||||
params: TestChildParams;
|
||||
process: ChildProcess;
|
||||
output = '';
|
||||
stdout = '';
|
||||
stderr = '';
|
||||
fullOutput = '';
|
||||
onOutput?: (chunk: string | Buffer) => void;
|
||||
exited: Promise<{ exitCode: number | null, signal: string | null }>;
|
||||
exitCode: Promise<number | null>;
|
||||
|
||||
private _outputCallbacks = new Set<() => void>();
|
||||
|
||||
constructor(params: TestChildParams) {
|
||||
this.params = params;
|
||||
// See https://nodejs.org/api/deprecations.html#DEP0190
|
||||
const command = params.shell ? params.command.join(' ') : params.command[0];
|
||||
const args = params.shell ? [] : params.command.slice(1);
|
||||
this.process = spawn(command, args, {
|
||||
env: inheritAndCleanEnv(params.env),
|
||||
cwd: params.cwd,
|
||||
shell: params.shell,
|
||||
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
||||
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
||||
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
||||
detached: process.platform !== 'win32',
|
||||
});
|
||||
if (process.env.PWTEST_DEBUG)
|
||||
process.stdout.write(`\n\nLaunching ${params.command.join(' ')}\n`);
|
||||
this.onOutput = params.onOutput;
|
||||
|
||||
const appendChunk = (type: 'stdout' | 'stderr', chunk: string | Buffer) => {
|
||||
this.output += String(chunk);
|
||||
if (type === 'stderr')
|
||||
this.stderr += String(chunk);
|
||||
else
|
||||
this.stdout += String(chunk);
|
||||
if (process.env.PWTEST_DEBUG)
|
||||
process.stdout.write(String(chunk));
|
||||
else
|
||||
this.fullOutput += String(chunk);
|
||||
this.onOutput?.(chunk);
|
||||
for (const cb of this._outputCallbacks)
|
||||
cb();
|
||||
this._outputCallbacks.clear();
|
||||
};
|
||||
|
||||
this.process.stderr!.on('data', appendChunk.bind(null, 'stderr'));
|
||||
this.process.stdout!.on('data', appendChunk.bind(null, 'stdout'));
|
||||
|
||||
const killProcessGroup = this._killProcessTree.bind(this, 'SIGKILL');
|
||||
process.on('exit', killProcessGroup);
|
||||
this.exited = new Promise(f => {
|
||||
this.process.on('exit', (exitCode, signal) => f({ exitCode, signal }));
|
||||
process.off('exit', killProcessGroup);
|
||||
});
|
||||
this.exitCode = this.exited.then(r => r.exitCode);
|
||||
}
|
||||
|
||||
outputLines(): string[] {
|
||||
const strippedOutput = stripAnsi(this.output);
|
||||
return strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim());
|
||||
}
|
||||
|
||||
async kill(signal: 'SIGINT' | 'SIGKILL' = 'SIGKILL') {
|
||||
this._killProcessTree(signal);
|
||||
return this.exited;
|
||||
}
|
||||
|
||||
private _killProcessTree(signal: 'SIGINT' | 'SIGKILL') {
|
||||
if (!this.process.pid || !this.process.kill(0))
|
||||
return;
|
||||
killProcessGroup(this.process.pid, signal);
|
||||
}
|
||||
|
||||
async cleanExit() {
|
||||
const r = await this.exited;
|
||||
if (r.exitCode)
|
||||
throw new Error(`Process failed with exit code ${r.exitCode}. Output:\n${this.output}`);
|
||||
if (r.signal)
|
||||
throw new Error(`Process received signal: ${r.signal}. Output:\n${this.output}`);
|
||||
}
|
||||
|
||||
async waitForOutput(substring: string, count = 1) {
|
||||
while (countTimes(stripAnsi(this.output), substring) < count)
|
||||
await new Promise<void>(f => this._outputCallbacks.add(f));
|
||||
}
|
||||
|
||||
clearOutput() {
|
||||
this.output = '';
|
||||
}
|
||||
|
||||
write(chars: string) {
|
||||
this.process.stdin!.write(chars);
|
||||
}
|
||||
}
|
||||
|
||||
export function killProcessGroup(pid: number, signal: 'SIGINT' | 'SIGKILL' = 'SIGKILL') {
|
||||
// On Windows, we always call `taskkill` no matter signal.
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
execSync(`taskkill /pid ${pid} /T /F /FI "MEMUSAGE gt 0"`, { stdio: 'ignore' });
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In case of POSIX and `SIGINT` signal, send it to the main process group only.
|
||||
if (signal === 'SIGINT') {
|
||||
try {
|
||||
process.kill(-pid, 'SIGINT');
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In case of POSIX and `SIGKILL` signal, we should send it to all descendant process groups.
|
||||
const rootProcess = buildProcessTreePosix(pid);
|
||||
if (!rootProcess)
|
||||
return;
|
||||
|
||||
const descendantProcessGroups = (function flatten(processData: ProcessData, result: Set<number> = new Set()) {
|
||||
// Process can nullify its own process group with `setpgid`. Use its PID instead.
|
||||
result.add(processData.pgrp || processData.pid);
|
||||
processData.children.forEach(child => flatten(child, result));
|
||||
return result;
|
||||
})(rootProcess);
|
||||
for (const pgrp of descendantProcessGroups) {
|
||||
try {
|
||||
process.kill(-pgrp, 'SIGKILL');
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type CommonFixtures = {
|
||||
childProcess: (params: TestChildParams) => TestChildProcess;
|
||||
waitForPort: (port: number) => Promise<void>;
|
||||
findFreePort: () => Promise<number>;
|
||||
};
|
||||
|
||||
export type CommonWorkerFixtures = {
|
||||
daemonProcess: (params: TestChildParams) => TestChildProcess;
|
||||
};
|
||||
|
||||
export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
|
||||
childProcess: async ({}, use, testInfo) => {
|
||||
const processes: TestChildProcess[] = [];
|
||||
await use(params => {
|
||||
const process = new TestChildProcess(params);
|
||||
processes.push(process);
|
||||
return process;
|
||||
});
|
||||
await Promise.all(processes.map(async child => child.kill()));
|
||||
if (testInfo.status !== 'passed' && testInfo.status !== 'skipped' && !process.env.PWTEST_DEBUG) {
|
||||
for (const process of processes) {
|
||||
console.log('====== ' + process.params.command.join(' '));
|
||||
console.log(process.fullOutput.replace(/\x1Bc/g, ''));
|
||||
console.log('=========================================');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
daemonProcess: [async ({}, use) => {
|
||||
const processes: TestChildProcess[] = [];
|
||||
await use(params => {
|
||||
const process = new TestChildProcess(params);
|
||||
processes.push(process);
|
||||
return process;
|
||||
});
|
||||
await Promise.all(processes.map(child => child.kill('SIGINT')));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
waitForPort: async ({}, use) => {
|
||||
const token = { canceled: false };
|
||||
await use(async port => {
|
||||
while (!token.canceled) {
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
const conn = net.connect(port, '127.0.0.1')
|
||||
.on('error', () => resolve(false))
|
||||
.on('connect', () => {
|
||||
conn.end();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
if (await promise)
|
||||
return;
|
||||
await new Promise(x => setTimeout(x, 100));
|
||||
}
|
||||
});
|
||||
token.canceled = true;
|
||||
},
|
||||
|
||||
findFreePort: async ({}, use) => {
|
||||
await use(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const { port } = server.address() as net.AddressInfo;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.on('error', reject);
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function countTimes(s: string, sub: string): number {
|
||||
let result = 0;
|
||||
for (let index = 0; index !== -1;) {
|
||||
index = s.indexOf(sub, index);
|
||||
if (index !== -1) {
|
||||
result++;
|
||||
index += sub.length;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
28
참고/playwright-main/tests/config/comparator.ts
Normal file
28
참고/playwright-main/tests/config/comparator.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 { utils } from '../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
const { getComparator } = utils;
|
||||
|
||||
const pngComparator = getComparator('image/png');
|
||||
type ComparatorResult = { diff?: Buffer; errorMessage: string; } | null;
|
||||
type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number };
|
||||
|
||||
export function comparePNGs(actual: Buffer, expected: Buffer, options: ImageComparatorOptions = {}): ComparatorResult {
|
||||
// Strict threshold by default in our tests.
|
||||
return pngComparator(actual, expected, { comparator: 'ssim-cie94', threshold: 0, ...options });
|
||||
}
|
||||
163
참고/playwright-main/tests/config/debugControllerBackend.ts
Normal file
163
참고/playwright-main/tests/config/debugControllerBackend.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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 WebSocket from 'ws';
|
||||
import { EventEmitter } from 'events';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export type ProtocolRequest = {
|
||||
id: number;
|
||||
method: string;
|
||||
params: any;
|
||||
};
|
||||
|
||||
export type ProtocolResponse = {
|
||||
id?: number;
|
||||
method?: string;
|
||||
error?: { message: string; data: any; };
|
||||
params?: any;
|
||||
result?: any;
|
||||
};
|
||||
|
||||
export interface ConnectionTransport {
|
||||
send(s: ProtocolRequest): void;
|
||||
close(): void; // Note: calling close is expected to issue onclose at some point.
|
||||
isClosed(): boolean,
|
||||
onmessage?: (message: ProtocolResponse) => void,
|
||||
onclose?: () => void,
|
||||
}
|
||||
|
||||
class WebSocketTransport implements ConnectionTransport {
|
||||
private _ws: WebSocket;
|
||||
|
||||
onmessage?: (message: ProtocolResponse) => void;
|
||||
onclose?: () => void;
|
||||
readonly wsEndpoint: string;
|
||||
|
||||
static async connect(url: string, headers: Record<string, string> = {}): Promise<WebSocketTransport> {
|
||||
const transport = new WebSocketTransport(url, headers);
|
||||
await new Promise<WebSocketTransport>((fulfill, reject) => {
|
||||
transport._ws.addEventListener('open', async () => {
|
||||
fulfill(transport);
|
||||
});
|
||||
transport._ws.addEventListener('error', event => {
|
||||
reject(new Error('WebSocket error: ' + event.message));
|
||||
transport._ws.close();
|
||||
});
|
||||
});
|
||||
return transport;
|
||||
}
|
||||
|
||||
constructor(url: string, headers: Record<string, string> = {}) {
|
||||
this.wsEndpoint = url;
|
||||
this._ws = new WebSocket(url, [], {
|
||||
perMessageDeflate: false,
|
||||
maxPayload: 256 * 1024 * 1024, // 256Mb,
|
||||
handshakeTimeout: 30000,
|
||||
headers
|
||||
});
|
||||
|
||||
this._ws.addEventListener('message', event => {
|
||||
try {
|
||||
if (this.onmessage)
|
||||
this.onmessage.call(null, JSON.parse(event.data.toString()));
|
||||
} catch (e) {
|
||||
this._ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
this._ws.addEventListener('close', event => {
|
||||
if (this.onclose)
|
||||
this.onclose.call(null);
|
||||
});
|
||||
// Prevent Error: read ECONNRESET.
|
||||
this._ws.addEventListener('error', () => {});
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
return this._ws.readyState === WebSocket.CLOSING || this._ws.readyState === WebSocket.CLOSED;
|
||||
}
|
||||
|
||||
send(message: ProtocolRequest) {
|
||||
this._ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
close() {
|
||||
this._ws.close();
|
||||
}
|
||||
|
||||
async closeAndWait() {
|
||||
const promise = new Promise(f => this._ws.once('close', f));
|
||||
this.close();
|
||||
await promise; // Make sure to await the actual disconnect.
|
||||
}
|
||||
}
|
||||
|
||||
export class Backend extends EventEmitter {
|
||||
private static _lastId = 0;
|
||||
private _callbacks = new Map<number, { fulfill: (a: any) => void, reject: (e: Error) => void }>();
|
||||
private _transport!: WebSocketTransport;
|
||||
channel: channels.DebugControllerChannel;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async connect(wsEndpoint: string) {
|
||||
this._transport = await WebSocketTransport.connect(wsEndpoint + '?debug-controller');
|
||||
this._transport.onmessage = (message: any) => {
|
||||
if (!message.id) {
|
||||
this.emit(message.method, message.params);
|
||||
return;
|
||||
}
|
||||
const pair = this._callbacks.get(message.id);
|
||||
if (!pair)
|
||||
return;
|
||||
this._callbacks.delete(message.id);
|
||||
if (message.error) {
|
||||
const error = new Error(message.error.error?.message || message.error.value);
|
||||
error.stack = message.error.error?.stack;
|
||||
pair.reject(error);
|
||||
} else {
|
||||
pair.fulfill(message.result);
|
||||
}
|
||||
};
|
||||
this.channel = new Proxy(this, {
|
||||
get: (target, propKey) => {
|
||||
if (['on', 'once'].includes(String(propKey)))
|
||||
return target[propKey].bind(target);
|
||||
return (...args: any) => this._send(String(propKey), ...args);
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.channel.initialize({ codegenId: 'playwright-test', sdkLanguage: 'javascript' });
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._transport.closeAndWait();
|
||||
}
|
||||
|
||||
private _send(method: string, params: any = {}): Promise<any> {
|
||||
return new Promise((fulfill, reject) => {
|
||||
const id = ++Backend._lastId;
|
||||
const command = { id, guid: 'DebugController', method, params, metadata: {} };
|
||||
this._transport.send(command as any);
|
||||
this._callbacks.set(id, { fulfill, reject });
|
||||
});
|
||||
}
|
||||
}
|
||||
17
참고/playwright-main/tests/config/errors.ts
Normal file
17
참고/playwright-main/tests/config/errors.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const kTargetClosedErrorMessage = 'Target page, context or browser has been closed';
|
||||
289
참고/playwright-main/tests/config/ghaMarkdownReporter.ts
Normal file
289
참고/playwright-main/tests/config/ghaMarkdownReporter.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
import type { IssueCommentEdge, Repository } from '@octokit/graphql-schema';
|
||||
import type { FullConfig, FullResult, Reporter, Suite, TestCase } from '@playwright/test/reporter';
|
||||
|
||||
type MarkdownReporterOptions = {
|
||||
configDir: string, // TODO: make it public?
|
||||
outputFile?: string;
|
||||
};
|
||||
|
||||
class MarkdownReporter implements Reporter {
|
||||
private _options: MarkdownReporterOptions;
|
||||
private _fatalErrors: TestError[] = [];
|
||||
protected _config!: FullConfig;
|
||||
private _suite!: Suite;
|
||||
|
||||
constructor(options: MarkdownReporterOptions) {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
printsToStdio() {
|
||||
return false;
|
||||
}
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
this._config = config;
|
||||
this._suite = suite;
|
||||
}
|
||||
|
||||
onError(error: TestError) {
|
||||
this._fatalErrors.push(error);
|
||||
}
|
||||
|
||||
async onEnd(result: FullResult) {
|
||||
const summary = this._generateSummary();
|
||||
const lines: string[] = [];
|
||||
const incompleteWarning = this._incompleteRunWarning();
|
||||
if (incompleteWarning) {
|
||||
lines.push(incompleteWarning);
|
||||
lines.push(``);
|
||||
}
|
||||
if (this._fatalErrors.length)
|
||||
lines.push(`**${this._fatalErrors.length} fatal errors, not part of any test**`);
|
||||
if (summary.unexpected.length) {
|
||||
lines.push(`**${summary.unexpected.length} failed**`);
|
||||
this._printTestList(':x:', summary.unexpected, lines);
|
||||
}
|
||||
if (summary.flaky.length) {
|
||||
lines.push(`<details>`);
|
||||
lines.push(`<summary><b>${summary.flaky.length} flaky</b></summary>`);
|
||||
this._printTestList(':warning:', summary.flaky, lines, ' <br/>');
|
||||
lines.push(`</details>`);
|
||||
lines.push(``);
|
||||
}
|
||||
if (summary.interrupted.length) {
|
||||
lines.push(`<details>`);
|
||||
lines.push(`<summary><b>${summary.interrupted.length} interrupted</b></summary>`);
|
||||
this._printTestList(':warning:', summary.interrupted, lines, ' <br/>');
|
||||
lines.push(`</details>`);
|
||||
lines.push(``);
|
||||
}
|
||||
const skipped = summary.skipped ? `, ${summary.skipped} skipped` : '';
|
||||
const didNotRun = summary.didNotRun ? `, ${summary.didNotRun} did not run` : '';
|
||||
lines.push(`**${summary.expected} passed${skipped}${didNotRun}**`);
|
||||
lines.push(``);
|
||||
|
||||
await this.publishReport(lines.join('\n'));
|
||||
}
|
||||
|
||||
protected async publishReport(report: string): Promise<void> {
|
||||
const maybeRelativeFile = this._options.outputFile || 'report.md';
|
||||
const reportFile = path.resolve(this._options.configDir, maybeRelativeFile);
|
||||
await fs.promises.mkdir(path.dirname(reportFile), { recursive: true });
|
||||
await fs.promises.writeFile(reportFile, report);
|
||||
}
|
||||
|
||||
protected _incompleteRunWarning(): string | undefined {
|
||||
const conclusion = process.env.WORKFLOW_RUN_CONCLUSION;
|
||||
if (!conclusion || conclusion === 'success' || conclusion === 'failure')
|
||||
return undefined;
|
||||
return `> [!WARNING]\n> The triggering workflow run ended with status \`${conclusion}\`. Results below may be incomplete — blob reports from cancelled or timed-out shards are missing, so passing/failing counts do not reflect the full test suite.`;
|
||||
}
|
||||
|
||||
protected _generateSummary() {
|
||||
let didNotRun = 0;
|
||||
let skipped = 0;
|
||||
let expected = 0;
|
||||
const interrupted: TestCase[] = [];
|
||||
const interruptedToPrint: TestCase[] = [];
|
||||
const unexpected: TestCase[] = [];
|
||||
const flaky: TestCase[] = [];
|
||||
|
||||
this._suite.allTests().forEach(test => {
|
||||
switch (test.outcome()) {
|
||||
case 'skipped': {
|
||||
if (test.results.some(result => result.status === 'interrupted')) {
|
||||
if (test.results.some(result => !!result.error))
|
||||
interruptedToPrint.push(test);
|
||||
interrupted.push(test);
|
||||
} else if (!test.results.length || test.expectedStatus !== 'skipped') {
|
||||
++didNotRun;
|
||||
} else {
|
||||
++skipped;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'expected': ++expected; break;
|
||||
case 'unexpected': unexpected.push(test); break;
|
||||
case 'flaky': flaky.push(test); break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
didNotRun,
|
||||
skipped,
|
||||
expected,
|
||||
interrupted,
|
||||
unexpected,
|
||||
flaky,
|
||||
};
|
||||
}
|
||||
|
||||
private _printTestList(prefix: string, tests: TestCase[], lines: string[], suffix?: string) {
|
||||
for (const test of tests)
|
||||
lines.push(`${prefix} ${formatTestTitle(this._config.rootDir, test)}${suffix || ''}`);
|
||||
lines.push(``);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTestTitle(rootDir: string, test: TestCase): string {
|
||||
// root, project, file, ...describes, test
|
||||
const [, projectName, , ...titles] = test.titlePath();
|
||||
const relativeTestPath = path.relative(rootDir, test.location.file);
|
||||
// intentionally leave out column to prevent writing test.spec.ts:100:5 - GitHub turns that into 💯
|
||||
const location = `${relativeTestPath}:${test.location.line}`;
|
||||
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
||||
const testTitle = `${projectTitle}${location} › ${titles.join(' › ')}`;
|
||||
const extraTags = test.tags.filter(t => !testTitle.includes(t));
|
||||
const formattedTags = extraTags.map(t => `\`${t}\``).join(' ');
|
||||
return `${testTitle}${extraTags.length ? ' ' + formattedTags : ''}`;
|
||||
}
|
||||
|
||||
class GHAMarkdownReporter extends MarkdownReporter {
|
||||
private octokit: ReturnType<typeof import('@actions/github').getOctokit>;
|
||||
private context: typeof import('@actions/github').context;
|
||||
private core: typeof import('@actions/core');
|
||||
|
||||
override async publishReport(report: string) {
|
||||
this.core = await import('@actions/core');
|
||||
const token = process.env.GITHUB_TOKEN || this.core.getInput('github-token');
|
||||
if (!token) {
|
||||
this.core.setFailed('Missing "github-token" input');
|
||||
throw new Error('Missing "github-token" input');
|
||||
}
|
||||
const { context, getOctokit } = await import('@actions/github');
|
||||
this.context = context;
|
||||
this.octokit = getOctokit(token);
|
||||
|
||||
this.core.info('Publishing report to PR.');
|
||||
const { prNumber, prHref } = this.pullRequestFromMetadata();
|
||||
if (!prNumber) {
|
||||
this.core.info(`No PR number found, skipping GHA comment. PR href: ${prHref}`);
|
||||
return;
|
||||
}
|
||||
this.core.info(`Posting comment to PR ${prHref}`);
|
||||
|
||||
const prNodeId = await this.collapsePreviousComments(prNumber);
|
||||
if (!prNodeId) {
|
||||
this.core.warning(`No PR node ID found, skipping GHA comment. PR href: ${prHref}`);
|
||||
return;
|
||||
}
|
||||
await this.addNewReportComment(prNodeId, report);
|
||||
}
|
||||
|
||||
private async collapsePreviousComments(prNumber: number) {
|
||||
const { owner, repo } = this.context.repo;
|
||||
const data = await this.octokit.graphql<{ repository: Repository }>(`
|
||||
query {
|
||||
repository(owner: "${owner}", name: "${repo}") {
|
||||
pullRequest(number: ${prNumber}) {
|
||||
id
|
||||
comments(last: 100) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
author {
|
||||
__typename
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const comments = data.repository.pullRequest?.comments.nodes?.filter(comment =>
|
||||
comment?.author?.__typename === 'Bot' &&
|
||||
comment?.author?.login === 'github-actions' &&
|
||||
comment.body?.includes(this._magicComment()));
|
||||
const prId = data.repository.pullRequest?.id;
|
||||
if (!comments?.length)
|
||||
return prId;
|
||||
const mutations = comments.map((comment, i) =>
|
||||
`m${i}: minimizeComment(input: { subjectId: "${comment!.id}", classifier: OUTDATED }) { clientMutationId }`);
|
||||
await this.octokit.graphql(`
|
||||
mutation {
|
||||
${mutations.join('\n')}
|
||||
}
|
||||
`);
|
||||
return prId;
|
||||
}
|
||||
|
||||
private _magicComment() {
|
||||
return `<!-- Generated by Playwright markdown reporter for ${this._workflowRunName()} in job ${process.env.GITHUB_JOB} -->`;
|
||||
}
|
||||
|
||||
private _workflowRunName() {
|
||||
// When used via 'workflow_run' event.
|
||||
const workflowRunName = this.context.payload.workflow_run?.name;
|
||||
if (workflowRunName)
|
||||
return workflowRunName;
|
||||
// When used via 'pull_request'/'push' event.
|
||||
// This is the name of the workflow file, e.g. 'ci.yml' or name if set.
|
||||
return process.env.GITHUB_WORKFLOW;
|
||||
}
|
||||
|
||||
private async addNewReportComment(prNodeId: string, report: string) {
|
||||
const reportUrl = process.env.HTML_REPORT_URL;
|
||||
const mergeWorkflowUrl = `${this.context.serverUrl}/${this.context.repo.owner}/${this.context.repo.repo}/actions/runs/${this.context.runId}`;
|
||||
|
||||
const body = formatComment([
|
||||
this._magicComment(),
|
||||
`### ${reportUrl ? `[Test results](${reportUrl})` : 'Test results'} for "${this._workflowRunName()}"`,
|
||||
report,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
`Merge [workflow run](${mergeWorkflowUrl}).`
|
||||
]);
|
||||
|
||||
const response = await this.octokit.graphql<{ addComment: { commentEdge: IssueCommentEdge } }>(`
|
||||
mutation {
|
||||
addComment(input: {subjectId: "${prNodeId}", body: """${body}"""}) {
|
||||
commentEdge {
|
||||
node {
|
||||
... on IssueComment {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
this.core.info(`Posted comment: ${response.addComment.commentEdge.node?.url}`);
|
||||
}
|
||||
|
||||
private pullRequestFromMetadata() {
|
||||
const metadata = this._config.metadata as MetadataWithCommitInfo;
|
||||
const prHref = metadata.ci?.prHref;
|
||||
return { prNumber: parseInt(prHref?.split('/').pop() ?? '', 10), prHref };
|
||||
}
|
||||
}
|
||||
|
||||
function formatComment(lines: string[]) {
|
||||
let body = lines.join('\n');
|
||||
if (body.length > 65535)
|
||||
body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`;
|
||||
return body;
|
||||
}
|
||||
|
||||
export default GHAMarkdownReporter;
|
||||
54
참고/playwright-main/tests/config/platformFixtures.ts
Normal file
54
참고/playwright-main/tests/config/platformFixtures.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { test } from '@playwright/test';
|
||||
import os from 'os';
|
||||
|
||||
export type PlatformWorkerFixtures = {
|
||||
platform: 'win32' | 'darwin' | 'linux';
|
||||
isWindows: boolean;
|
||||
isMac: boolean;
|
||||
isLinux: boolean;
|
||||
macVersion: number; // major only, 11 or later, zero if not mac
|
||||
};
|
||||
|
||||
function platform(): 'win32' | 'darwin' | 'linux' {
|
||||
if (process.env.PLAYWRIGHT_SERVICE_OS === 'linux')
|
||||
return 'linux';
|
||||
if (process.env.PLAYWRIGHT_SERVICE_OS === 'windows')
|
||||
return 'win32';
|
||||
if (process.env.PLAYWRIGHT_SERVICE_OS === 'macos')
|
||||
return 'darwin';
|
||||
return process.platform as 'win32' | 'darwin' | 'linux';
|
||||
}
|
||||
|
||||
function macVersion() {
|
||||
if (process.platform !== 'darwin')
|
||||
return 0;
|
||||
const darwinMajor = +os.release().split('.')[0];
|
||||
// Apple jumped from macOS 15 (Sequoia) to macOS 26 (Tahoe), so Darwin 25 = macOS 26.
|
||||
if (darwinMajor >= 25)
|
||||
return darwinMajor + 1;
|
||||
return darwinMajor - 9;
|
||||
}
|
||||
|
||||
export const platformTest = test.extend<{}, PlatformWorkerFixtures>({
|
||||
platform: [platform(), { scope: 'worker' }],
|
||||
isWindows: [platform() === 'win32', { scope: 'worker' }],
|
||||
isMac: [platform() === 'darwin', { scope: 'worker' }],
|
||||
isLinux: [platform() === 'linux', { scope: 'worker' }],
|
||||
macVersion: [macVersion(), { scope: 'worker' }],
|
||||
});
|
||||
182
참고/playwright-main/tests/config/proxy.ts
Normal file
182
참고/playwright-main/tests/config/proxy.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IncomingMessage } from 'http';
|
||||
import type { ProxyServer } from '../third_party/proxy';
|
||||
import { createProxy } from '../third_party/proxy';
|
||||
import net from 'net';
|
||||
import { utils } from '../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
type SocksSocketClosedPayload = utils.SocksSocketClosedPayload;
|
||||
type SocksSocketDataPayload = utils.SocksSocketDataPayload;
|
||||
type SocksSocketRequestedPayload = utils.SocksSocketRequestedPayload;
|
||||
const { SocksProxy } = utils;
|
||||
|
||||
// Certain browsers perform telemetry requests which we want to ignore.
|
||||
const kConnectHostsToIgnore = new Set([
|
||||
'www.bing.com:443',
|
||||
'www.google.com:443',
|
||||
]);
|
||||
|
||||
export class TestProxy {
|
||||
readonly HOST: string;
|
||||
readonly PORT: number;
|
||||
readonly URL: string;
|
||||
|
||||
connectHosts: string[] = [];
|
||||
requestUrls: string[] = [];
|
||||
wsUrls: string[] = [];
|
||||
|
||||
private readonly _server: ProxyServer;
|
||||
private readonly _sockets = new Set<net.Socket>();
|
||||
private _handlers: { event: string, handler: (...args: any[]) => void }[] = [];
|
||||
|
||||
static async create(port: number): Promise<TestProxy> {
|
||||
const proxy = new TestProxy(port);
|
||||
await new Promise<void>(f => proxy._server.listen(port, f));
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private constructor(port: number) {
|
||||
this.PORT = port;
|
||||
this.URL = `http://localhost:${port}`;
|
||||
this.HOST = new URL(this.URL).host;
|
||||
this._server = createProxy();
|
||||
this._server.on('connection', socket => this._onSocket(socket));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.reset();
|
||||
for (const socket of this._sockets)
|
||||
socket.destroy();
|
||||
this._sockets.clear();
|
||||
await new Promise(x => this._server.close(x));
|
||||
}
|
||||
|
||||
forwardTo(port: number, options?: { allowConnectRequests?: boolean, removePrefix?: string, preserveHostname?: boolean }) {
|
||||
this._prependHandler('request', (req: IncomingMessage) => {
|
||||
this.requestUrls.push(req.url);
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (options?.preserveHostname)
|
||||
url.port = '' + port;
|
||||
else
|
||||
url.host = `127.0.0.1:${port}`;
|
||||
if (options?.removePrefix)
|
||||
url.pathname = url.pathname.replace(options.removePrefix, '');
|
||||
req.url = url.toString();
|
||||
});
|
||||
this._prependHandler('connect', (req: IncomingMessage) => {
|
||||
if (!options?.allowConnectRequests)
|
||||
return;
|
||||
if (kConnectHostsToIgnore.has(req.url))
|
||||
return;
|
||||
this.connectHosts.push(req.url);
|
||||
req.url = `127.0.0.1:${port}`;
|
||||
});
|
||||
this._prependHandler('upgrade', (req: IncomingMessage) => {
|
||||
this.wsUrls.push(req.url);
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
if (options?.preserveHostname)
|
||||
url.port = '' + port;
|
||||
else
|
||||
url.host = `127.0.0.1:${port}`;
|
||||
if (options?.removePrefix)
|
||||
url.pathname = url.pathname.replace(options.removePrefix, '');
|
||||
if (url.protocol === 'ws:')
|
||||
url.protocol = 'http:';
|
||||
else if (url.protocol === 'wss:')
|
||||
url.protocol = 'https:';
|
||||
req.url = url.toString();
|
||||
});
|
||||
}
|
||||
|
||||
setAuthHandler(handler: (req: IncomingMessage) => boolean) {
|
||||
this._server.authenticate = (req: IncomingMessage) => {
|
||||
try {
|
||||
return handler(req);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.connectHosts = [];
|
||||
this.requestUrls = [];
|
||||
for (const { event, handler } of this._handlers)
|
||||
this._server.removeListener(event, handler);
|
||||
this._handlers = [];
|
||||
this._server.authenticate = undefined;
|
||||
}
|
||||
|
||||
private _prependHandler(event: string, handler: (...args: any[]) => void) {
|
||||
this._handlers.push({ event, handler });
|
||||
this._server.prependListener(event, handler);
|
||||
}
|
||||
|
||||
private _onSocket(socket: net.Socket) {
|
||||
this._sockets.add(socket);
|
||||
// ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given
|
||||
// that tab closing aborts outgoing connections to the server.
|
||||
socket.on('error', (error: any) => {
|
||||
if (error.code !== 'ECONNRESET' && error.code !== 'HPE_INVALID_EOF_STATE')
|
||||
throw error;
|
||||
});
|
||||
socket.once('close', () => this._sockets.delete(socket));
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupSocksForwardingServer({
|
||||
port, forwardPort, allowedTargetPort
|
||||
}: {
|
||||
port: number, forwardPort: number, allowedTargetPort: number
|
||||
}) {
|
||||
const connectHosts = [];
|
||||
const connections = new Map<string, net.Socket>();
|
||||
const socksProxy = new SocksProxy();
|
||||
socksProxy.setPattern('*');
|
||||
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
||||
if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
|
||||
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
|
||||
return;
|
||||
}
|
||||
const target = new net.Socket();
|
||||
target.on('error', error => socksProxy.sendSocketError({ uid: payload.uid, error: error.toString() }));
|
||||
target.on('end', () => socksProxy.sendSocketEnd({ uid: payload.uid }));
|
||||
target.on('data', data => socksProxy.sendSocketData({ uid: payload.uid, data }));
|
||||
target.setKeepAlive(false);
|
||||
target.connect(forwardPort, '127.0.0.1');
|
||||
target.on('connect', () => {
|
||||
connections.set(payload.uid, target);
|
||||
if (!connectHosts.includes(`${payload.host}:${payload.port}`))
|
||||
connectHosts.push(`${payload.host}:${payload.port}`);
|
||||
socksProxy.socketConnected({ uid: payload.uid, host: target.localAddress, port: target.localPort });
|
||||
});
|
||||
});
|
||||
socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => {
|
||||
connections.get(payload.uid)?.write(payload.data);
|
||||
});
|
||||
socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => {
|
||||
connections.get(payload.uid)?.destroy();
|
||||
connections.delete(payload.uid);
|
||||
});
|
||||
await socksProxy.listen(port, '127.0.0.1');
|
||||
return {
|
||||
closeProxyServer: () => socksProxy.close(),
|
||||
proxyServerAddr: `socks5://127.0.0.1:${port}`,
|
||||
connectHosts,
|
||||
};
|
||||
}
|
||||
42
참고/playwright-main/tests/config/queryObjects.ts
Normal file
42
참고/playwright-main/tests/config/queryObjects.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export async function queryObjectCount(type: Function): Promise<number> {
|
||||
globalThis.typeForQueryObjects = type;
|
||||
const session: import('inspector').Session = new (require('node:inspector').Session)();
|
||||
session.connect();
|
||||
try {
|
||||
await new Promise(f => session.post('Runtime.enable', f));
|
||||
const { result: constructorFunction } = await new Promise(f => session.post('Runtime.evaluate', {
|
||||
expression: `globalThis.typeForQueryObjects.prototype`,
|
||||
includeCommandLineAPI: true,
|
||||
}, (_, result) => f(result))) as any;
|
||||
|
||||
const { objects: instanceArray } = await new Promise(f => session.post('Runtime.queryObjects', {
|
||||
prototypeObjectId: constructorFunction.objectId
|
||||
}, (_, result) => f(result))) as any;
|
||||
|
||||
const { result: { value } } = await new Promise<any>(f => session.post('Runtime.callFunctionOn', {
|
||||
functionDeclaration: 'function (arr) { return this.length; }',
|
||||
objectId: instanceArray.objectId,
|
||||
arguments: [{ objectId: instanceArray.objectId }],
|
||||
}, (_, result) => f(result as any)));
|
||||
|
||||
return value;
|
||||
} finally {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
62
참고/playwright-main/tests/config/remote-server-impl.js
Normal file
62
참고/playwright-main/tests/config/remote-server-impl.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const fs = require('fs');
|
||||
const cluster = require('cluster');
|
||||
|
||||
async function start() {
|
||||
const { browserTypeName, launchOptions, stallOnClose, disconnectOnSIGHUP, exitOnFile, exitOnWarning, startStopAndRunHttp } = JSON.parse(process.argv[2]);
|
||||
if (stallOnClose) {
|
||||
launchOptions.__testHookGracefullyClose = () => {
|
||||
console.log(`(stalled=>true)`);
|
||||
return new Promise(() => { });
|
||||
};
|
||||
}
|
||||
if (exitOnWarning)
|
||||
process.on('warning', () => process.exit(43));
|
||||
if (disconnectOnSIGHUP)
|
||||
launchOptions.handleSIGHUP = false;
|
||||
|
||||
const playwright = require('playwright-core');
|
||||
|
||||
if (startStopAndRunHttp) {
|
||||
const browser = await playwright[browserTypeName].launch(launchOptions);
|
||||
await browser.close();
|
||||
console.log(`(wsEndpoint=>none)`);
|
||||
console.log(`(closed=>success)`);
|
||||
require('http').createServer(() => {}).listen();
|
||||
return;
|
||||
}
|
||||
|
||||
const browserServer = await playwright[browserTypeName].launchServer(launchOptions);
|
||||
if (disconnectOnSIGHUP)
|
||||
process.on('SIGHUP', () => browserServer._disconnectForTest());
|
||||
|
||||
if (exitOnFile) {
|
||||
(async function waitForFileAndExit() {
|
||||
while (true) {
|
||||
if (fs.existsSync(exitOnFile))
|
||||
break;
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
process.exit(42);
|
||||
})();
|
||||
}
|
||||
|
||||
browserServer.on('close', (exitCode, signal) => {
|
||||
console.log(`(exitCode=>${exitCode})`);
|
||||
console.log(`(signal=>${signal})`);
|
||||
});
|
||||
console.log(`(tempDir=>${browserServer._userDataDirForTest})`);
|
||||
console.log(`(pid=>${browserServer.process().pid})`);
|
||||
console.log(`(wsEndpoint=>${browserServer.wsEndpoint()})`);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', error => console.log(error));
|
||||
process.on('unhandledRejection', reason => console.log(reason));
|
||||
|
||||
if (cluster.isWorker || !JSON.parse(process.argv[2]).inCluster) {
|
||||
start();
|
||||
} else {
|
||||
cluster.fork();
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
171
참고/playwright-main/tests/config/remoteServer.ts
Normal file
171
참고/playwright-main/tests/config/remoteServer.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 path from 'path';
|
||||
import type { BrowserType, Browser } from 'playwright-core';
|
||||
import type { CommonFixtures, TestChildProcess } from './commonFixtures';
|
||||
|
||||
export interface PlaywrightServer {
|
||||
wsEndpoint(): string;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class RunServer implements PlaywrightServer {
|
||||
private _process!: TestChildProcess;
|
||||
_wsEndpoint!: string;
|
||||
|
||||
async start(childProcess: CommonFixtures['childProcess'], options?: { mode?: 'extension' | 'default', env?: NodeJS.ProcessEnv, artifactsDir?: string }) {
|
||||
const command = ['node', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'cli.js'), 'run-server'];
|
||||
if (options?.mode === 'extension')
|
||||
command.push('--mode=extension');
|
||||
if (options?.artifactsDir)
|
||||
command.push(`--artifacts-dir=${options.artifactsDir}`);
|
||||
this._process = childProcess({
|
||||
command,
|
||||
env: { NODE_OPTIONS: process.env.NODE_OPTIONS, ...options?.env },
|
||||
});
|
||||
|
||||
let wsEndpointCallback: (value: string) => void;
|
||||
const wsEndpointPromise = new Promise<string>(f => wsEndpointCallback = f);
|
||||
this._process.onOutput = data => {
|
||||
const prefix = 'Listening on ';
|
||||
const line = data.toString();
|
||||
if (line.startsWith(prefix))
|
||||
wsEndpointCallback(line.substr(prefix.length));
|
||||
};
|
||||
|
||||
this._wsEndpoint = await wsEndpointPromise;
|
||||
}
|
||||
|
||||
wsEndpoint() {
|
||||
return this._wsEndpoint;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._process.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
|
||||
export type RemoteServerOptions = {
|
||||
stallOnClose?: boolean;
|
||||
disconnectOnSIGHUP?: boolean;
|
||||
exitOnFile?: string;
|
||||
exitOnWarning?: boolean;
|
||||
inCluster?: boolean;
|
||||
url?: string;
|
||||
startStopAndRunHttp?: boolean;
|
||||
sharedBrowser?: boolean;
|
||||
artifactsDir?: string;
|
||||
};
|
||||
|
||||
export class RemoteServer implements PlaywrightServer {
|
||||
private _process!: TestChildProcess;
|
||||
readonly _output = new Map<string, string>();
|
||||
readonly _outputCallback = new Map<string, () => void>();
|
||||
_browserType!: BrowserType;
|
||||
_exitAndDisconnectPromise: Promise<any> | undefined;
|
||||
_browser: Browser | undefined;
|
||||
_wsEndpoint!: string;
|
||||
|
||||
async _start(childProcess: CommonFixtures['childProcess'], browserType: BrowserType, channel: string, remoteServerOptions: RemoteServerOptions = {}) {
|
||||
this._browserType = browserType;
|
||||
const browserOptions = (browserType as any)._playwright._defaultLaunchOptions;
|
||||
// Copy options to prevent a large JSON string when launching subprocess.
|
||||
// Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows.
|
||||
const launchOptions: Parameters<BrowserType['launchServer']>[0] = {
|
||||
args: browserOptions.args,
|
||||
headless: browserOptions.headless,
|
||||
channel: browserOptions.channel,
|
||||
executablePath: browserOptions.executablePath,
|
||||
handleSIGINT: true,
|
||||
handleSIGTERM: true,
|
||||
handleSIGHUP: true,
|
||||
logger: undefined,
|
||||
};
|
||||
if (remoteServerOptions.sharedBrowser)
|
||||
(launchOptions as any)._sharedBrowser = true;
|
||||
if (remoteServerOptions.artifactsDir)
|
||||
launchOptions.artifactsDir = remoteServerOptions.artifactsDir;
|
||||
const options = {
|
||||
browserTypeName: browserType.name(),
|
||||
channel,
|
||||
launchOptions,
|
||||
...remoteServerOptions,
|
||||
};
|
||||
this._process = childProcess({
|
||||
command: ['node', path.join(__dirname, 'remote-server-impl.js'), JSON.stringify(options)],
|
||||
env: { NODE_OPTIONS: process.env.NODE_OPTIONS },
|
||||
});
|
||||
|
||||
let index = 0;
|
||||
this._process.onOutput = () => {
|
||||
let match;
|
||||
while ((match = this._process.output.substring(index).match(/\(([^()]+)=>([^()]+)\)/))) {
|
||||
const key = match[1];
|
||||
const value = match[2];
|
||||
this._addOutput(key, value);
|
||||
index += match.index! + match[0].length;
|
||||
}
|
||||
};
|
||||
|
||||
this._wsEndpoint = await this.out('wsEndpoint');
|
||||
|
||||
if (remoteServerOptions.url) {
|
||||
this._browser = await this._browserType.connect(this._wsEndpoint);
|
||||
const page = await this._browser.newPage();
|
||||
await page.goto(remoteServerOptions.url);
|
||||
}
|
||||
}
|
||||
|
||||
_addOutput(key: string, value: string) {
|
||||
this._output.set(key, value);
|
||||
const cb = this._outputCallback.get(key);
|
||||
this._outputCallback.delete(key);
|
||||
if (cb)
|
||||
cb();
|
||||
}
|
||||
|
||||
async out(key: string): Promise<string> {
|
||||
if (!this._output.has(key))
|
||||
await new Promise<void>(f => this._outputCallback.set(key, f));
|
||||
return this._output.get(key)!;
|
||||
}
|
||||
|
||||
wsEndpoint() {
|
||||
return this._wsEndpoint;
|
||||
}
|
||||
|
||||
child() {
|
||||
return this._process.process;
|
||||
}
|
||||
|
||||
async childExitCode() {
|
||||
return await this._process.exitCode;
|
||||
}
|
||||
|
||||
async childSignal() {
|
||||
return (await this._process.exited).signal;
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this._browser) {
|
||||
await this._browser.close();
|
||||
this._browser = undefined;
|
||||
}
|
||||
await this._process.kill('SIGINT');
|
||||
await this.childExitCode();
|
||||
}
|
||||
}
|
||||
136
참고/playwright-main/tests/config/serverFixtures.ts
Normal file
136
참고/playwright-main/tests/config/serverFixtures.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { TestServer } from './testserver';
|
||||
import { TestProxy } from './proxy';
|
||||
import { utils } from '../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
type SocksSocketRequestedPayload = utils.SocksSocketRequestedPayload;
|
||||
const { SocksProxy } = utils;
|
||||
type SocksProxy = InstanceType<typeof SocksProxy>;
|
||||
|
||||
export type ServerWorkerOptions = {
|
||||
loopback?: string;
|
||||
__servers: ServerFixtures;
|
||||
};
|
||||
|
||||
export type ServerFixtures = {
|
||||
server: TestServer;
|
||||
httpsServer: TestServer;
|
||||
socksPort: number;
|
||||
proxyServer: TestProxy;
|
||||
asset: (p: string) => string;
|
||||
};
|
||||
|
||||
export const serverFixtures: Fixtures<ServerFixtures, ServerWorkerOptions> = {
|
||||
loopback: [undefined, { scope: 'worker', option: true }],
|
||||
__servers: [async ({ loopback }, run, workerInfo) => {
|
||||
const assetsPath = path.join(__dirname, '..', 'assets');
|
||||
const cachedPath = path.join(__dirname, '..', 'assets', 'cached');
|
||||
|
||||
const port = 8907 + workerInfo.workerIndex * 4;
|
||||
const server = await TestServer.create(assetsPath, port, loopback);
|
||||
server.enableHTTPCache(cachedPath);
|
||||
|
||||
const httpsPort = port + 1;
|
||||
const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort, loopback);
|
||||
httpsServer.enableHTTPCache(cachedPath);
|
||||
|
||||
const socksServer = new MockSocksServer();
|
||||
const socksPort = port + 2;
|
||||
await socksServer.listen(socksPort);
|
||||
|
||||
const proxyPort = port + 3;
|
||||
const proxyServer = await TestProxy.create(proxyPort);
|
||||
|
||||
await run({
|
||||
asset: (p: string) => path.join(__dirname, '..', 'assets', ...p.split('/')),
|
||||
server,
|
||||
httpsServer,
|
||||
socksPort,
|
||||
proxyServer,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
server.stop(),
|
||||
httpsServer.stop(),
|
||||
socksServer.close(),
|
||||
proxyServer.stop(),
|
||||
]);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
server: async ({ __servers }, run) => {
|
||||
__servers.server.reset();
|
||||
await run(__servers.server);
|
||||
},
|
||||
|
||||
httpsServer: async ({ __servers }, run) => {
|
||||
__servers.httpsServer.reset();
|
||||
await run(__servers.httpsServer);
|
||||
},
|
||||
|
||||
socksPort: async ({ __servers }, run) => {
|
||||
await run(__servers.socksPort);
|
||||
},
|
||||
|
||||
proxyServer: async ({ __servers }, run) => {
|
||||
__servers.proxyServer.reset();
|
||||
await run(__servers.proxyServer);
|
||||
},
|
||||
|
||||
asset: async ({ __servers }, run) => {
|
||||
await run(__servers.asset);
|
||||
},
|
||||
};
|
||||
|
||||
export class MockSocksServer {
|
||||
private _socksProxy: SocksProxy;
|
||||
|
||||
constructor() {
|
||||
this._socksProxy = new SocksProxy();
|
||||
this._socksProxy.setPattern('*');
|
||||
this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
||||
this._socksProxy.socketConnected({
|
||||
uid: payload.uid,
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
});
|
||||
});
|
||||
this._socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketRequestedPayload) => {
|
||||
const body = '<html><title>Served by the SOCKS proxy</title></html>';
|
||||
const data = Buffer.from([
|
||||
'HTTP/1.1 200 OK',
|
||||
'Connection: close',
|
||||
'Content-Type: text/html',
|
||||
'Content-Length: ' + Buffer.byteLength(body),
|
||||
'',
|
||||
body
|
||||
].join('\r\n'));
|
||||
this._socksProxy.sendSocketData({ uid: payload.uid, data });
|
||||
this._socksProxy.sendSocketEnd({ uid: payload.uid });
|
||||
});
|
||||
}
|
||||
|
||||
async listen(port: number, hostname?: string) {
|
||||
await this._socksProxy.listen(port, hostname);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._socksProxy.close();
|
||||
}
|
||||
}
|
||||
50
참고/playwright-main/tests/config/testMode.ts
Normal file
50
참고/playwright-main/tests/config/testMode.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { oop, client } from '../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
export type TestModeName = 'default' | 'driver';
|
||||
|
||||
const { start } = oop;
|
||||
|
||||
interface TestMode {
|
||||
setup(): Promise<client.Playwright>;
|
||||
teardown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DriverTestMode implements TestMode {
|
||||
private _impl: { playwright: client.Playwright; stop: () => Promise<void>; };
|
||||
|
||||
async setup() {
|
||||
this._impl = await start({
|
||||
NODE_OPTIONS: undefined, // Hide driver process while debugging.
|
||||
});
|
||||
return this._impl.playwright;
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
await this._impl.stop();
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultTestMode implements TestMode {
|
||||
async setup() {
|
||||
return require('playwright-core');
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
}
|
||||
}
|
||||
55
참고/playwright-main/tests/config/testModeFixtures.ts
Normal file
55
참고/playwright-main/tests/config/testModeFixtures.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 { test } from '@playwright/test';
|
||||
import type { TestModeName } from './testMode';
|
||||
import { DefaultTestMode, DriverTestMode } from './testMode';
|
||||
|
||||
export type TestModeWorkerOptions = {
|
||||
mode: TestModeName;
|
||||
};
|
||||
|
||||
export type TestModeTestFixtures = {
|
||||
toImpl: (rpcObject?: any) => any;
|
||||
};
|
||||
|
||||
export type TestModeWorkerFixtures = {
|
||||
toImplInWorkerScope: (rpcObject?: any) => any;
|
||||
playwright: typeof import('@playwright/test');
|
||||
};
|
||||
|
||||
export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({
|
||||
mode: ['default', { scope: 'worker', option: true }],
|
||||
playwright: [async ({ mode }, run) => {
|
||||
const testMode = {
|
||||
'default': new DefaultTestMode(),
|
||||
'driver': new DriverTestMode(),
|
||||
}[mode];
|
||||
const playwright = await testMode.setup();
|
||||
await run(playwright);
|
||||
await testMode.teardown();
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||
await use((playwright as any)._connection.toImpl);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
toImpl: async ({ toImplInWorkerScope: toImplWorker, mode }, use, testInfo) => {
|
||||
if (mode !== 'default' || process.env.PW_TEST_REUSE_CONTEXT)
|
||||
testInfo.skip();
|
||||
await use(toImplWorker);
|
||||
},
|
||||
});
|
||||
29
참고/playwright-main/tests/config/testserver/cert.pem
Normal file
29
참고/playwright-main/tests/config/testserver/cert.pem
Normal file
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFCjCCAvKgAwIBAgIULU/gkDm8IqC7PG8u3RID0AYyP6gwDQYJKoZIhvcNAQEL
|
||||
BQAwGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MB4XDTIzMDgxMDIyNTc1MFoX
|
||||
DTMzMDgwNzIyNTc1MFowGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MIICIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArbS99qjKcnHr5G0Zc2xhDaOZnjQv
|
||||
Fbiqxf/nbXt/7WaqryzpVKu7AT1ainBvuPEo7If9DhVnfF//2pGl0gbU31OU4/mr
|
||||
ymQmczGEyZvOBDsZhtCif54o5OoO0BjhODNT8OWec9RT87n6RkH58MHlOi8xsPxQ
|
||||
9n5U1CN/h2DyQF3aRKunEFCgtwPKWSjG+J/TAI9i0aSENXPiR8wjTrjg79s8Ehuj
|
||||
NN8Wk6rKLU3sepG3GIMID5vLsVa2t9xqn562sP95Ee+Xp2YX3z7oYK99QCJdzacw
|
||||
alhMHob1GCEKjDyxsD2IFRi7Dysiutfyzy3pMo6NALxFrwKVhWX0L4zVFIsI6JlV
|
||||
dK8dHmDk0MRSqgB9sWXvEfSTXADEe8rncFSFpFz4Z8RNLmn5YSzQJzokNn41DUCP
|
||||
dZTlTkcGTqvn5NqoY4sOV8rkFbgmTcqyijV/sebPjxCbJNcNmaSWa9FJ5IjRTpzM
|
||||
38wLmxn+eKGK68n2JB3P7JP6LtsBShQEpXAF3rFfyNsP1bjquvGZVSjV8w/UwPE4
|
||||
kV5eq3j3D4913Zfxvzjp6PEmhStG0EQtIXvx/TRoYpaNWypIgZdbkZQp1HUIQL15
|
||||
D2Web4nazP3so1FC3ZgbrJZ2ozoadjLMp49NcSFdh+WRyVKuo0DIqR0zaiAzzf2D
|
||||
G1q7TLKimM3XBMUCAwEAAaNIMEYwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLAYD
|
||||
VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG
|
||||
SIb3DQEBCwUAA4ICAQAvC5M1JFc21WVSLPvE2iVbt4HmirO3EENdDqs+rTYG5VJG
|
||||
iE5ZuI6h/LjS5ptTfKovXQKaMr3pwp1pLMd/9q+6ZR1Hs9Z2wF6OZan4sb0uT32Y
|
||||
1KGlj86QMiiSLdrJ/1Z9JHskHYNCep1ZTsUhGk0qqiNv+G3K2y7ZpvrT/xlnYMth
|
||||
KLTuSVUwM8BBEPrCRLoXuaEy0LnvMvMVepIfP8tnMIL6zqmj3hXMPe4r4OFV/C5o
|
||||
XX25bC7GyuPWIRYn2OWP92J1CODZD1rGRoDtmvqrQpHdeX9RYcKH0ZLZoIf5L3Hf
|
||||
pPUtVkw3QGtjvKeG3b9usxaV9Od2Z08vKKk1PRkXFe8gqaeyicK7YVIOMTSuspAf
|
||||
JeJEHns6Hg61Exbo7GwdX76xlmQ/Z43E9BPHKgLyZ9WuJ0cysqN4aCyvS9yws9to
|
||||
ki7iMZqJUsmE2o09n9VaEsX6uQANZtLjI9wf+IgJuueDTNrkzQkhU7pbaPMsSG40
|
||||
AgGY/y4BR0H8sbhNnhqtZH7RcXV9VCJoPBAe+YiuXRiXyZHWxwBRyBE3e7g4MKHg
|
||||
hrWtaWUAs7gbavHwjqgU63iVItDSk7t4fCiEyObjK09AaNf2DjjaSGf8YGza4bNy
|
||||
BjYinYJ6/eX//gp+abqfocFbBP7D9zRDgMIbVmX/Ey6TghKiLkZOdbzcpO4Wgg==
|
||||
-----END CERTIFICATE-----
|
||||
350
참고/playwright-main/tests/config/testserver/index.ts
Normal file
350
참고/playwright-main/tests/config/testserver/index.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications 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 fs from 'fs';
|
||||
import type http from 'http';
|
||||
import mime from 'mime';
|
||||
import type net from 'net';
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
import type stream from 'stream';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import zlib, { gzip } from 'zlib';
|
||||
import { utils } from '../../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
const { createHttpServer, createHttpsServer } = utils;
|
||||
|
||||
const fulfillSymbol = Symbol('fulfil callback');
|
||||
const rejectSymbol = Symbol('reject callback');
|
||||
|
||||
const gzipAsync = util.promisify(gzip.bind(zlib));
|
||||
|
||||
type UpgradeActions = {
|
||||
doUpgrade: () => void;
|
||||
socket: stream.Duplex;
|
||||
};
|
||||
|
||||
type IncomingMessageWithBody = http.IncomingMessage & { postBody: Promise<Buffer> };
|
||||
|
||||
export class TestServer {
|
||||
private _server: http.Server;
|
||||
private _wsServer: WebSocketServer;
|
||||
private _dirPath: string;
|
||||
readonly debugServer: any;
|
||||
private _startTime: Date;
|
||||
private _cachedPathPrefix: string | null;
|
||||
private _routes = new Map<string, (arg0: IncomingMessageWithBody, arg1: http.ServerResponse) => any>();
|
||||
private _auths = new Map<string, { username: string; password: string; }>();
|
||||
private _csp = new Map<string, string>();
|
||||
private _extraHeaders = new Map<string, object>();
|
||||
private _gzipRoutes = new Set<string>();
|
||||
private _requestSubscribers = new Map<string, Promise<any>>();
|
||||
private _upgradeCallback: (actions: UpgradeActions) => void | undefined;
|
||||
readonly PORT: number;
|
||||
readonly PREFIX: string;
|
||||
readonly CROSS_PROCESS_PREFIX: string;
|
||||
readonly EMPTY_PAGE: string;
|
||||
readonly HOST: string;
|
||||
readonly HOSTNAME: string;
|
||||
readonly HELLO_WORLD: string;
|
||||
|
||||
static async create(dirPath: string, port: number, loopback?: string): Promise<TestServer> {
|
||||
const server = new TestServer(dirPath, port, loopback);
|
||||
await server.waitUntilReady();
|
||||
return server;
|
||||
}
|
||||
|
||||
static async certOptions() {
|
||||
return {
|
||||
key: await fs.promises.readFile(path.join(__dirname, 'key.pem')),
|
||||
cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')),
|
||||
passphrase: 'aaaa',
|
||||
};
|
||||
}
|
||||
|
||||
static async createHTTPS(dirPath: string, port: number, loopback?: string): Promise<TestServer> {
|
||||
const server = new TestServer(dirPath, port, loopback, await this.certOptions());
|
||||
await server.waitUntilReady();
|
||||
return server;
|
||||
}
|
||||
|
||||
constructor(dirPath: string, port: number, loopback?: string, sslOptions?: object) {
|
||||
if (sslOptions)
|
||||
this._server = createHttpsServer(sslOptions, this._onRequest.bind(this));
|
||||
else
|
||||
this._server = createHttpServer(this._onRequest.bind(this));
|
||||
this._server.on('connection', socket => this._onSocket(socket));
|
||||
this._wsServer = new WebSocketServer({ noServer: true });
|
||||
this._server.on('upgrade', async (request, socket, head) => {
|
||||
const doUpgrade = () => {
|
||||
this._wsServer.handleUpgrade(request, socket, head, ws => {
|
||||
// Next emit is only for our internal 'connection' listeners.
|
||||
this._wsServer.emit('connection', ws, request);
|
||||
});
|
||||
};
|
||||
if (this._upgradeCallback) {
|
||||
this._upgradeCallback({ doUpgrade, socket });
|
||||
return;
|
||||
}
|
||||
const pathname = new URL(request.url, 'http://localhost').pathname;
|
||||
if (pathname === '/ws-401') {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized body');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (pathname === '/ws-slow')
|
||||
await new Promise(f => setTimeout(f, 2000));
|
||||
if (!['/ws', '/ws-slow'].includes(pathname)) {
|
||||
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
doUpgrade();
|
||||
});
|
||||
this._server.listen(port);
|
||||
this._dirPath = dirPath;
|
||||
this.debugServer = require('debug')('pw:testserver');
|
||||
|
||||
this._startTime = new Date();
|
||||
this._cachedPathPrefix = null;
|
||||
|
||||
const cross_origin = loopback || '127.0.0.1';
|
||||
const same_origin = loopback || 'localhost';
|
||||
const protocol = sslOptions ? 'https' : 'http';
|
||||
this.PORT = port;
|
||||
this.PREFIX = `${protocol}://${same_origin}:${port}`;
|
||||
this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}`;
|
||||
this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`;
|
||||
this.HOST = new URL(this.EMPTY_PAGE).host;
|
||||
this.HOSTNAME = new URL(this.EMPTY_PAGE).hostname;
|
||||
this.HELLO_WORLD = `${this.PREFIX}/hello-world`;
|
||||
}
|
||||
|
||||
async waitUntilReady() {
|
||||
await new Promise(x => this._server.once('listening', x));
|
||||
}
|
||||
|
||||
_onSocket(socket: net.Socket) {
|
||||
// ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given
|
||||
// that tab closing aborts outgoing connections to the server.
|
||||
// HPE_INVALID_METHOD is a legit error when a client (e.g. Chromium which
|
||||
// makes https requests to http sites) makes a https connection to a http server.
|
||||
socket.on('error', error => {
|
||||
if (!['ECONNRESET', 'HPE_INVALID_EOF_STATE', 'HPE_INVALID_METHOD'].includes((error as any).code))
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
enableHTTPCache(pathPrefix: string) {
|
||||
this._cachedPathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
setAuth(path: string, username: string, password: string) {
|
||||
this.debugServer(`set auth for ${path} to ${username}:${password}`);
|
||||
this._auths.set(path, { username, password });
|
||||
}
|
||||
|
||||
enableGzip(path: string) {
|
||||
this._gzipRoutes.add(path);
|
||||
}
|
||||
|
||||
setCSP(path: string, csp: string) {
|
||||
this._csp.set(path, csp);
|
||||
}
|
||||
|
||||
setExtraHeaders(path: string, object: Record<string, string>) {
|
||||
this._extraHeaders.set(path, object);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.reset();
|
||||
await new Promise(x => this._server.close(x));
|
||||
}
|
||||
|
||||
setContent(path: string, content: string, mimeType: string) {
|
||||
this.setRoute(path, (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': mimeType });
|
||||
res.end(mimeType === 'text/html' ? `<!DOCTYPE html>${content}` : content);
|
||||
});
|
||||
}
|
||||
|
||||
setRoute(path: string, handler: (arg0: IncomingMessageWithBody, arg1: http.ServerResponse) => any) {
|
||||
this._routes.set(path, handler);
|
||||
}
|
||||
|
||||
setRedirect(from: string, to: string) {
|
||||
this.setRoute(from, (req, res) => {
|
||||
const headers = this._extraHeaders.get(req.url!) || {};
|
||||
res.writeHead(302, { ...headers, location: to });
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
waitForRequest(path: string): Promise<IncomingMessageWithBody> {
|
||||
let promise = this._requestSubscribers.get(path);
|
||||
if (promise)
|
||||
return promise;
|
||||
let fulfill;
|
||||
let reject;
|
||||
promise = new Promise((f, r) => {
|
||||
fulfill = f;
|
||||
reject = r;
|
||||
});
|
||||
promise[fulfillSymbol] = fulfill;
|
||||
const error = new Error(`Request ${path} was not received before the test finished.`);
|
||||
promise[rejectSymbol] = () => reject(error);
|
||||
this._requestSubscribers.set(path, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._routes.clear();
|
||||
this._auths.clear();
|
||||
this._csp.clear();
|
||||
this._extraHeaders.clear();
|
||||
this._gzipRoutes.clear();
|
||||
this._upgradeCallback = undefined;
|
||||
this._wsServer.removeAllListeners('connection');
|
||||
this._server.closeAllConnections();
|
||||
for (const subscriber of this._requestSubscribers.values())
|
||||
subscriber[rejectSymbol].call(null);
|
||||
this._requestSubscribers.clear();
|
||||
}
|
||||
|
||||
_onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
request.on('error', error => {
|
||||
if ((error as any).code === 'ECONNRESET')
|
||||
response.end();
|
||||
else
|
||||
throw error;
|
||||
});
|
||||
(request as any).postBody = new Promise(resolve => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
request.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
const url = new URL(request.url, 'http://localhost');
|
||||
const pathWithSearch = url.pathname + url.search;
|
||||
this.debugServer(`request ${request.method} ${pathWithSearch}`);
|
||||
if (this._auths.has(pathWithSearch)) {
|
||||
const auth = this._auths.get(pathWithSearch)!;
|
||||
const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString();
|
||||
this.debugServer(`request credentials ${credentials}`);
|
||||
this.debugServer(`actual credentials ${auth.username}:${auth.password}`);
|
||||
if (credentials !== `${auth.username}:${auth.password}`) {
|
||||
this.debugServer(`request write www-auth`);
|
||||
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' });
|
||||
response.end('HTTP Error 401 Unauthorized: Access is denied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Notify request subscriber.
|
||||
if (this._requestSubscribers.has(pathWithSearch)) {
|
||||
this._requestSubscribers.get(pathWithSearch)![fulfillSymbol].call(null, request);
|
||||
this._requestSubscribers.delete(pathWithSearch);
|
||||
}
|
||||
const handler = this._routes.get(pathWithSearch);
|
||||
if (handler)
|
||||
handler.call(null, request as IncomingMessageWithBody, response);
|
||||
else
|
||||
this.serveFile(request, response);
|
||||
}
|
||||
|
||||
serveFile(request: http.IncomingMessage, response: http.ServerResponse, filePath?: string): void {
|
||||
this._serveFile(request, response, filePath).catch(e => {
|
||||
this.debugServer(`error: ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async _serveFile(request: http.IncomingMessage, response: http.ServerResponse, filePath?: string): Promise<void> {
|
||||
let pathName = new URL(request.url, 'http://localhost').pathname;
|
||||
if (!filePath) {
|
||||
if (pathName === '/')
|
||||
pathName = '/index.html';
|
||||
filePath = path.join(this._dirPath, pathName.substring(1));
|
||||
}
|
||||
|
||||
if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
|
||||
if (request.headers['if-modified-since']) {
|
||||
response.statusCode = 304; // not modified
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
response.setHeader('Cache-Control', 'public, max-age=31536000, no-cache');
|
||||
response.setHeader('Last-Modified', this._startTime.toISOString());
|
||||
} else {
|
||||
response.setHeader('Cache-Control', 'no-cache, no-store');
|
||||
}
|
||||
if (this._csp.has(pathName))
|
||||
response.setHeader('Content-Security-Policy', this._csp.get(pathName)!);
|
||||
|
||||
if (this._extraHeaders.has(pathName)) {
|
||||
const object = this._extraHeaders.get(pathName);
|
||||
for (const key in object)
|
||||
response.setHeader(key, object[key]);
|
||||
}
|
||||
|
||||
const { err, data } = await fs.promises.readFile(filePath).then(data => ({ data, err: undefined })).catch(err => ({ data: undefined, err }));
|
||||
// The HTTP transaction might be already terminated after async hop here - do nothing in this case.
|
||||
if (response.writableEnded)
|
||||
return;
|
||||
if (err) {
|
||||
response.statusCode = 404;
|
||||
response.setHeader('Content-Type', 'text/plain');
|
||||
response.end(request.method !== 'HEAD' ? `File not found: ${filePath}` : null);
|
||||
return;
|
||||
}
|
||||
const extension = filePath.substring(filePath.lastIndexOf('.') + 1);
|
||||
const mimeType = mime.getType(extension) || 'application/octet-stream';
|
||||
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(mimeType);
|
||||
const contentType = isTextEncoding ? `${mimeType}; charset=utf-8` : mimeType;
|
||||
response.setHeader('Content-Type', contentType);
|
||||
if (this._gzipRoutes.has(pathName)) {
|
||||
response.setHeader('Content-Encoding', 'gzip');
|
||||
const result = await gzipAsync(data);
|
||||
// The HTTP transaction might be already terminated after async hop here.
|
||||
if (!response.writableEnded)
|
||||
response.end(request.method !== 'HEAD' ? result : null);
|
||||
} else {
|
||||
response.end(request.method !== 'HEAD' ? data : null);
|
||||
}
|
||||
}
|
||||
|
||||
onceWebSocketConnection(handler: (socket: WebSocket, request: http.IncomingMessage) => void) {
|
||||
this._wsServer.once('connection', handler);
|
||||
}
|
||||
|
||||
waitForWebSocketConnectionRequest() {
|
||||
return new Promise<http.IncomingMessage & { headers: http.IncomingHttpHeaders }>(fulfil => {
|
||||
this._wsServer.once('connection', (ws, req) => fulfil(req));
|
||||
});
|
||||
}
|
||||
|
||||
waitForUpgrade() {
|
||||
return new Promise<UpgradeActions>(fulfill => this._upgradeCallback = fulfill);
|
||||
}
|
||||
|
||||
waitForWebSocket() {
|
||||
return new Promise<WebSocket>(fulfill => this._wsServer.once('connection', (ws, req) => fulfill(ws)));
|
||||
}
|
||||
|
||||
sendOnWebSocketConnection(data) {
|
||||
this.onceWebSocketConnection(ws => ws.send(data));
|
||||
}
|
||||
}
|
||||
52
참고/playwright-main/tests/config/testserver/key.pem
Normal file
52
참고/playwright-main/tests/config/testserver/key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCttL32qMpycevk
|
||||
bRlzbGENo5meNC8VuKrF/+dte3/tZqqvLOlUq7sBPVqKcG+48Sjsh/0OFWd8X//a
|
||||
kaXSBtTfU5Tj+avKZCZzMYTJm84EOxmG0KJ/nijk6g7QGOE4M1Pw5Z5z1FPzufpG
|
||||
QfnwweU6LzGw/FD2flTUI3+HYPJAXdpEq6cQUKC3A8pZKMb4n9MAj2LRpIQ1c+JH
|
||||
zCNOuODv2zwSG6M03xaTqsotTex6kbcYgwgPm8uxVra33Gqfnraw/3kR75enZhff
|
||||
Puhgr31AIl3NpzBqWEwehvUYIQqMPLGwPYgVGLsPKyK61/LPLekyjo0AvEWvApWF
|
||||
ZfQvjNUUiwjomVV0rx0eYOTQxFKqAH2xZe8R9JNcAMR7yudwVIWkXPhnxE0uaflh
|
||||
LNAnOiQ2fjUNQI91lOVORwZOq+fk2qhjiw5XyuQVuCZNyrKKNX+x5s+PEJsk1w2Z
|
||||
pJZr0UnkiNFOnMzfzAubGf54oYrryfYkHc/sk/ou2wFKFASlcAXesV/I2w/VuOq6
|
||||
8ZlVKNXzD9TA8TiRXl6rePcPj3Xdl/G/OOno8SaFK0bQRC0he/H9NGhilo1bKkiB
|
||||
l1uRlCnUdQhAvXkPZZ5vidrM/eyjUULdmBuslnajOhp2Msynj01xIV2H5ZHJUq6j
|
||||
QMipHTNqIDPN/YMbWrtMsqKYzdcExQIDAQABAoICAGqXttpdyZ1g+vg5WpzRrNzJ
|
||||
v8KtExepMmI+Hq24U1BC6AqG7MfgeejQ1XaOeIBsvEgpSsgRqmdQIZjmN3Mibg59
|
||||
I6ih1SFlQ5L8mBd/XHSML6Xi8VSOoVmXp29bVRk/pgr1XL6HVN0DCumCIvXyhc+m
|
||||
lj+dFbGs5DEpd2CDxSRqcz4gd2wzjevAj7MWqsJ2kOyPEHzFD7wdWIXmZuQv3xhQ
|
||||
2BPkkcon+5qx+07BupOcR1brUU8Cs4QnSgiZYXSB2GnU215+P/mhVJTR7ZcnGRz5
|
||||
+cXxCmy3sj4pYs1juS1FMWSM3azUeDVeqvks+vrXmXpEr5H79mbmlwo8/hMPwNDO
|
||||
07HRZwa8T01aT9EYVm0lIOYjMF/2f6j6cu2apJtjXICOksR2HefRBVXQirOxRHma
|
||||
9XAYfNkZ/2164ZbgFmJv9khFnegPEuth9tLVdFIeGSmsG0aX9tH63zGT2NROyyLc
|
||||
QXPqsDl2CxCYPRs2oiGkM9dnfP1wAOp96sq42GIuN7ykfqfRnwAIvvnLKvyCq1vR
|
||||
pIno3CIX6vnzt+1/Hrmv13b0L6pJPitpXwKWHv9zJKBTpN8HEzP3Qmth2Ef60/7/
|
||||
CBo1PVTd1A6zcU7816flg7SCY+Vk+OxVHV3dGBIIqN9SfrQ8BPcOl6FNV5Anbrnv
|
||||
CpSw+LzH9n5xympDnk0BAoIBAQDjenvDfCnrNVeqx8+sYaYey4/WPVLXOQhREvRY
|
||||
oOtX9eqlNSi20+Wl+iuXmyj8wdHrDET7rfjCbpDQ7u105yzLw4gy4qIRDKZ1nE45
|
||||
YX+tm8mZgBqRnTp0DoGOArqmp3IKXJtUYmpbTz9tOfY7Usb1o1epb4winEB+Pl+8
|
||||
mgXOEo8xvWBzKeRA7tE73V64Mwbvbo9Ff2EguhXweQP29yBkEjT4iViayuHUmyPt
|
||||
hOVSMj2oFQuQGPdhAk7nUXojSGK/Zas/AGpH9CHH9De0h4m08vd3oM4vj0HwzgjU
|
||||
Co9aRa9SAH7EiaocOTcjDRPxWdZPHhxmrVRIYlF0MNmOAkXJAoIBAQDDfEqu4sNi
|
||||
pq74VXVatQqhzCILZo+o48bdgEjF7mF99mqPj8rwIDrEoEriDK861kenLc3vWKRY
|
||||
5wh1iX3S896re9kUMoxx6p4heYTcsOJ9BbkcpT8bJPZx9gBJb4jJENeVf1exf6sG
|
||||
RhFnulpzReRRaUjX2yAkyUPfc8YcUt+Nalrg+2W0fzeLCUpABCAcj2B1Vv7qRZHj
|
||||
oEtlCV5Nz+iMhrwIa16g9c8wGt5DZb4PI+VIJ6EYkdsjhgqIF0T/wDq9/habGBPo
|
||||
mHN+/DX3hCJWN2QgoVGJskHGt0zDMgiEgXfLZ2Grl02vQtq+mW2O2vGVeUd9Y5Ew
|
||||
RUiY4bSRTrUdAoIBAHxL1wiP9c/By+9TUtScXssA681ioLtdPIAgXUd4VmAvzVEM
|
||||
ZPzRd/BjbCJg89p4hZ1rjN4Ax6ZmB9dCVpnEH6QPaYJ0d53dTa+CAvQzpDJWp6eq
|
||||
adobEW+M5ZmVQCwD3rpus6k+RWMzQDMMstDjgDeEU0gP3YCj5FGW/3TsrDNXzMqe
|
||||
8e67ey9Hzyho43K+3xFBViPhYE8jnw1Q8quliRtlH3CWi8W5CgDD7LPCJBPvw+Tt
|
||||
6u2H1tQ5EKgwyw4wZVSz1wiLz4cVjMfXWADa9pHbGQFS6pbuLlfIHObQBliLLysd
|
||||
ficiGcNmOAx8/uKn9gQxLc+k8iLDJkLY1mdUMpECggEAJLl87k37ltTpmg2z9k58
|
||||
qNjIrIugAYKJIaOwCD84YYmhi0bgQSxM3hOe/ciUQuFupKGeRpDIj0sX87zYvoDC
|
||||
HEUwCvNUHzKMco15wFwasJIarJ7+tALFqbMlaqZhdCSN27AIsXfikVMogewoge9n
|
||||
bUPyQ1sPNtn4vknptfh7tv18BTg1aytbK+ua31vnDHaDEIg/a5OWTMUYZOrVpJii
|
||||
f4PwX0SMioCjY84oY1EB26ZKtLt9MDh2ir3rzJVSiRl776WEaa6kTtYVHI4VNWLF
|
||||
cJ0HWnnz74JliQd2jFUh9IK+FqBdYPcTyREuNxBr3KKVMBeQrqW96OubL913JrU6
|
||||
oQKCAQEA0yzORUouT0yleWs7RmzBlT9OLD/3cBYJMf/r1F8z8OQjB8fU1jKbO1Cs
|
||||
q4l+o9FmI+eHkgc3xbEG0hahOFWm/hTTli9vzksxurgdawZELThRkK33uTU9pKla
|
||||
Okqx3Ru/iMOW2+DQUx9UB+jK+hSAgq4gGqLeJVyaBerIdLQLlvqxrwSxjvvj+wJC
|
||||
Y66mgRzdCi6VDF1vV0knCrQHK6tRwcPozu/k4zjJzvdbMJnKEy2S7Vh6vO8lEPJm
|
||||
MQtaHPpmz+F4z14b9unNIiSbHO60Q4O+BwIBCzxApQQbFg63vBLYYwEMRd7hh92s
|
||||
ZkZVSOEp+sYBf/tmptlKr49nO+dTjQ==
|
||||
-----END PRIVATE KEY-----
|
||||
19
참고/playwright-main/tests/config/testserver/san.cnf
Normal file
19
참고/playwright-main/tests/config/testserver/san.cnf
Normal file
@@ -0,0 +1,19 @@
|
||||
# openssl req -new -x509 -days 3650 -key key.pem -out cert.pem -config san.cnf -extensions v3_req
|
||||
|
||||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = playwright-test
|
||||
|
||||
[v3_req]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
IP.2 = ::1
|
||||
206
참고/playwright-main/tests/config/traceViewerFixtures.ts
Normal file
206
참고/playwright-main/tests/config/traceViewerFixtures.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
|
||||
import { step } from './baseTest';
|
||||
import path from 'path';
|
||||
import { CommonFixtures, TestChildProcess } from './commonFixtures';
|
||||
|
||||
type BaseTestFixtures = CommonFixtures & {
|
||||
context: BrowserContext;
|
||||
};
|
||||
|
||||
type BaseWorkerFixtures = {
|
||||
headless: boolean;
|
||||
browser: Browser;
|
||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
||||
playwright: typeof import('@playwright/test');
|
||||
};
|
||||
|
||||
export type TraceViewerFixtures = {
|
||||
showTraceViewer: (trace: string | undefined, options?: {host?: string, port?: number, stdin?: boolean}) => Promise<TraceViewerPage>;
|
||||
runAndTrace: (body: () => Promise<void>, optsOverrides?: Parameters<BrowserContext['tracing']['start']>[0]) => Promise<TraceViewerPage>;
|
||||
};
|
||||
|
||||
class TraceViewerPage {
|
||||
actionTitles: Locator;
|
||||
actionsTree: Locator;
|
||||
callLines: Locator;
|
||||
consoleLines: Locator;
|
||||
logLines: Locator;
|
||||
errorMessages: Locator;
|
||||
consoleLineMessages: Locator;
|
||||
consoleStacks: Locator;
|
||||
networkRequests: Locator;
|
||||
metadataTab: Locator;
|
||||
snapshotContainer: Locator;
|
||||
sourceCodeTab: Locator;
|
||||
networkTab: Locator;
|
||||
|
||||
settingsDialog: Locator;
|
||||
themeSetting: Locator;
|
||||
displayCanvasContentSetting: Locator;
|
||||
|
||||
constructor(public page: Page, public process: TestChildProcess) {
|
||||
this.actionTitles = page.locator('.action-title');
|
||||
this.actionsTree = page.getByTestId('actions-tree');
|
||||
this.callLines = page.locator('.call-tab .call-line');
|
||||
this.logLines = page.getByRole('listbox', { name: 'Log entries' }).getByRole('option');
|
||||
this.consoleLines = page.getByRole('tabpanel', { name: 'Console' }).getByRole('option');
|
||||
this.consoleLineMessages = page.locator('.console-line-message');
|
||||
this.errorMessages = page.locator('.error-message');
|
||||
this.consoleStacks = page.locator('.console-stack');
|
||||
this.networkRequests = page.getByRole('listbox', { name: 'Network requests' }).getByRole('option');
|
||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||
this.metadataTab = page.getByRole('tabpanel', { name: 'Metadata' });
|
||||
this.sourceCodeTab = page.getByRole('tabpanel', { name: 'Source' });
|
||||
this.networkTab = page.getByRole('tabpanel', { name: 'Network' });
|
||||
|
||||
this.settingsDialog = page.getByTestId('settings-toolbar-dialog');
|
||||
this.themeSetting = this.settingsDialog.getByRole('combobox', { name: 'Theme' });
|
||||
this.displayCanvasContentSetting = page.locator('.setting').getByText('Display canvas content');
|
||||
}
|
||||
|
||||
@step
|
||||
async showAllActions() {
|
||||
await this.page.getByRole('button', { name: 'Filter actions' }).click();
|
||||
await this.page.locator('.setting').getByText('Network routes').click();
|
||||
await this.page.locator('.setting').getByText('Getters').click();
|
||||
await this.page.locator('.setting').getByText('Configuration').click();
|
||||
await this.page.getByRole('button', { name: 'Filter actions' }).click();
|
||||
}
|
||||
|
||||
stackFrames(options: { selected?: boolean } = {}) {
|
||||
return this.page.getByRole('listbox', { name: 'Stack trace' }).getByRole('option', options);
|
||||
}
|
||||
|
||||
actionIconsText(action: string) {
|
||||
const entry = this.actionsTree.getByRole('treeitem', { name: action });
|
||||
return entry.locator('.action-icon-value').filter({ visible: true });
|
||||
}
|
||||
|
||||
actionIcons(action: string) {
|
||||
return this.actionsTree.getByRole('treeitem', { name: action }).locator('.action-icons').filter({ visible: true });
|
||||
}
|
||||
|
||||
@step
|
||||
async expandAction(title: string) {
|
||||
await this.actionsTree.getByRole('treeitem', { name: title }).locator('.codicon-chevron-right').click();
|
||||
}
|
||||
|
||||
@step
|
||||
async selectAction(title: string, ordinal: number = 0) {
|
||||
await this.actionsTree.getByTitle(title).nth(ordinal).click();
|
||||
}
|
||||
|
||||
@step
|
||||
async hoverAction(title: string, ordinal: number = 0) {
|
||||
await this.actionsTree.getByRole('treeitem', { name: title }).nth(ordinal).hover();
|
||||
}
|
||||
|
||||
@step
|
||||
async selectSnapshot(name: string) {
|
||||
await this.page.getByRole('tab', { name }).click();
|
||||
}
|
||||
|
||||
async showErrorsTab() {
|
||||
await this.page.getByRole('tab', { name: 'Errors' }).click();
|
||||
}
|
||||
|
||||
async showConsoleTab() {
|
||||
await this.page.getByRole('tab', { name: 'Console' }).click();
|
||||
}
|
||||
|
||||
async showSourceTab() {
|
||||
await this.page.getByRole('tab', { name: 'Source' }).click();
|
||||
}
|
||||
|
||||
async showNetworkTab() {
|
||||
await this.page.getByRole('tab', { name: 'Network' }).click();
|
||||
}
|
||||
|
||||
async showMetadataTab() {
|
||||
await this.page.getByRole('tab', { name: 'Metadata' }).click();
|
||||
}
|
||||
|
||||
async showSettings() {
|
||||
await this.page.getByRole('button', { name: 'Settings' }).click();
|
||||
}
|
||||
|
||||
@step
|
||||
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<FrameLocator> {
|
||||
await this.selectAction(actionName, ordinal);
|
||||
while (this.page.frames().length < (hasSubframe ? 4 : 3))
|
||||
await this.page.waitForEvent('frameattached');
|
||||
return this.page.frameLocator('iframe.snapshot-visible[name=snapshot]');
|
||||
}
|
||||
}
|
||||
|
||||
export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixtures, BaseWorkerFixtures> = {
|
||||
showTraceViewer: async ({ playwright, childProcess, browserName }, use, testInfo) => {
|
||||
const browsers: Browser[] = [];
|
||||
const tracings: any[] = [];
|
||||
await use(async (trace: string | undefined, { host, port, stdin } = {}) => {
|
||||
const command = [
|
||||
'node',
|
||||
path.join(__dirname, '../../packages/playwright-core/cli.js'),
|
||||
'show-trace',
|
||||
'--port', '' + (port ?? '0'),
|
||||
];
|
||||
if (host)
|
||||
command.push('--host', host);
|
||||
if (stdin)
|
||||
command.push('--stdin');
|
||||
if (trace)
|
||||
command.push(trace);
|
||||
const cp = childProcess({ command });
|
||||
await cp.waitForOutput('Listening on');
|
||||
const browser = await playwright.chromium.launch({
|
||||
...(browserName === 'chromium' ? {} : { channel: 'chromium' }),
|
||||
executablePath: process.env.CRPATH, // without this, setting FFPATH makes us launch Firefox with Chromium args
|
||||
});
|
||||
browsers.push(browser);
|
||||
const page = await browser.newPage();
|
||||
if (process.env.PWTEST_DEBUG_TRACE_VIEWER) {
|
||||
const tracing = page.context().tracing;
|
||||
await tracing.start({ snapshots: true, screenshots: true });
|
||||
tracings.push(tracing);
|
||||
}
|
||||
const url = cp.output.match(/Listening on (http:\/\/[^\s]+)/)![1];
|
||||
await page.goto(url);
|
||||
return new TraceViewerPage(page, cp);
|
||||
});
|
||||
for (const [index, tracing] of tracings.entries()) {
|
||||
const path = testInfo.outputPath(`viewer-trace-${index}.zip`);
|
||||
await tracing.stop({ path });
|
||||
await testInfo.attach(`viewer-trace-${index}.zip`, { path, contentType: 'application/zip' });
|
||||
}
|
||||
for (const browser of browsers)
|
||||
await browser.close();
|
||||
},
|
||||
|
||||
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
||||
await use(async (body: () => Promise<void>, optsOverrides = {}) => {
|
||||
const traceFile = testInfo.outputPath('trace.zip');
|
||||
await context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides });
|
||||
await body();
|
||||
await context.tracing.stop({ path: traceFile });
|
||||
if (process.env.PWTEST_DEBUG_TRACE_VIEWER)
|
||||
await testInfo.attach('recorded-trace.zip', { path: traceFile, contentType: 'application/zip' });
|
||||
return showTraceViewer(traceFile);
|
||||
});
|
||||
},
|
||||
};
|
||||
269
참고/playwright-main/tests/config/utils.ts
Normal file
269
참고/playwright-main/tests/config/utils.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 { tools } from '../../packages/playwright-core/lib/coreBundle';
|
||||
import { utils, iso } from '../../packages/playwright-core/lib/coreBundle';
|
||||
|
||||
import type { iso as isoType } from '../../packages/playwright-core/lib/coreBundle';
|
||||
import type { Locator, Frame, Page } from 'playwright-core';
|
||||
import type { StackFrame } from '../../packages/protocol/src/channels';
|
||||
import type { ActionTraceEvent, TraceEvent } from '@trace/trace';
|
||||
|
||||
const { TraceLoader, TraceModel } = iso;
|
||||
type TraceModel = InstanceType<typeof TraceModel>;
|
||||
type SnapshotStorage = isoType.SnapshotStorage;
|
||||
|
||||
export type BoundingBox = Awaited<ReturnType<Locator['boundingBox']>>;
|
||||
|
||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
||||
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.src = url;
|
||||
frame.id = frameId;
|
||||
document.body.appendChild(frame);
|
||||
await new Promise(x => frame.onload = x);
|
||||
return frame;
|
||||
}, { frameId, url });
|
||||
return handle.asElement().contentFrame() as Promise<Frame>;
|
||||
}
|
||||
|
||||
export async function detachFrame(page: Page, frameId: string) {
|
||||
await page.evaluate(frameId => {
|
||||
document.getElementById(frameId)!.remove();
|
||||
}, frameId);
|
||||
}
|
||||
|
||||
export async function verifyViewport(page: Page, width: number, height: number) {
|
||||
// `expect` may clash in test runner tests if imported eagerly.
|
||||
const { expect } = require('@playwright/test');
|
||||
expect(page.viewportSize()!.width).toBe(width);
|
||||
expect(page.viewportSize()!.height).toBe(height);
|
||||
expect(await page.evaluate('window.innerWidth')).toBe(width);
|
||||
expect(await page.evaluate('window.innerHeight')).toBe(height);
|
||||
}
|
||||
|
||||
export function expectedSSLError(browserName: string, platform: string, channel: string | undefined): RegExp {
|
||||
if (browserName === 'chromium')
|
||||
return /net::(ERR_CERT_AUTHORITY_INVALID|ERR_CERT_INVALID)/;
|
||||
if (browserName === 'webkit') {
|
||||
if (platform === 'darwin')
|
||||
return /The certificate for this server is invalid/;
|
||||
else if (platform === 'win32' && channel !== 'webkit-wsl')
|
||||
return /SSL peer certificate or SSH remote key was not OK/;
|
||||
else
|
||||
return /Unacceptable TLS certificate|Operation was cancelled/;
|
||||
}
|
||||
if (browserName === 'firefox' && isBidiChannel(channel))
|
||||
return /MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT/;
|
||||
return /SSL_ERROR_UNKNOWN/;
|
||||
}
|
||||
|
||||
export function isBidiChannel(channel: string | undefined): boolean {
|
||||
return channel?.startsWith('bidi-chrom') || channel?.startsWith('moz-firefox') || false;
|
||||
}
|
||||
|
||||
export function chromiumVersionLessThan(a: string, b: string) {
|
||||
const left: number[] = a.split('.').map(e => Number(e));
|
||||
const right: number[] = b.split('.').map(e => Number(e));
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (left[i] > right[i])
|
||||
return false;
|
||||
if (left[i] < right[i])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let didSuppressUnverifiedCertificateWarning = false;
|
||||
let originalEmitWarning: (warning: string | Error, ...args: any[]) => void;
|
||||
export function suppressCertificateWarning() {
|
||||
if (didSuppressUnverifiedCertificateWarning)
|
||||
return;
|
||||
didSuppressUnverifiedCertificateWarning = true;
|
||||
// Suppress one-time warning:
|
||||
// https://github.com/nodejs/node/blob/1bbe66f432591aea83555d27dd76c55fea040a0d/lib/internal/options.js#L37-L49
|
||||
originalEmitWarning = process.emitWarning;
|
||||
process.emitWarning = (warning, ...args) => {
|
||||
if (typeof warning === 'string' && warning.includes('NODE_TLS_REJECT_UNAUTHORIZED')) {
|
||||
process.emitWarning = originalEmitWarning;
|
||||
return;
|
||||
}
|
||||
return originalEmitWarning.call(process, warning, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], actionObjects: ActionTraceEvent[], stacks: Map<string, StackFrame[]> }> {
|
||||
const zipFS = new utils.ZipFile(file);
|
||||
const resources = new Map<string, Buffer>();
|
||||
for (const entry of await zipFS.entries())
|
||||
resources.set(entry, await zipFS.read(entry));
|
||||
zipFS.close();
|
||||
|
||||
const actionMap = new Map<string, ActionTraceEvent>();
|
||||
const events: any[] = [];
|
||||
for (const traceFile of [...resources.keys()].filter(name => name.endsWith('.trace'))) {
|
||||
for (const line of resources.get(traceFile)!.toString().split('\n')) {
|
||||
if (line) {
|
||||
const event = JSON.parse(line) as TraceEvent;
|
||||
events.push(event);
|
||||
|
||||
if (event.type === 'before') {
|
||||
const action: ActionTraceEvent = {
|
||||
...event,
|
||||
type: 'action',
|
||||
endTime: 0,
|
||||
};
|
||||
actionMap.set(event.callId, action);
|
||||
} else if (event.type === 'input') {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing.inputSnapshot = event.inputSnapshot;
|
||||
existing.point = event.point;
|
||||
} else if (event.type === 'after') {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing.afterSnapshot = event.afterSnapshot;
|
||||
existing.endTime = event.endTime;
|
||||
existing.error = event.error;
|
||||
existing.result = event.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const networkFile of [...resources.keys()].filter(name => name.endsWith('.network'))) {
|
||||
for (const line of resources.get(networkFile)!.toString().split('\n')) {
|
||||
if (line)
|
||||
events.push(JSON.parse(line));
|
||||
}
|
||||
}
|
||||
|
||||
const stacks: Map<string, StackFrame[]> = new Map();
|
||||
for (const stacksFile of [...resources.keys()].filter(name => name.endsWith('.stacks'))) {
|
||||
for (const [key, value] of iso.parseClientSideCallMetadata(JSON.parse(resources.get(stacksFile)!.toString())))
|
||||
stacks.set(key, value);
|
||||
}
|
||||
|
||||
const actionObjects = [...actionMap.values()];
|
||||
actionObjects.sort((a, b) => a.startTime - b.startTime);
|
||||
return {
|
||||
events,
|
||||
resources,
|
||||
actions: actionObjects.map(a => iso.renderTitleForCall({ ...a, type: a.class })),
|
||||
actionObjects,
|
||||
stacks,
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseTrace(file: string): Promise<{ snapshots: SnapshotStorage, model: TraceModel }> {
|
||||
const dir = file + '.extracted';
|
||||
await tools.extractTrace(file, dir);
|
||||
const backend = new tools.DirTraceLoaderBackend(dir);
|
||||
const loader = new TraceLoader();
|
||||
await loader.load(backend);
|
||||
return { model: new TraceModel(dir, loader.contextEntries), snapshots: loader.storage() };
|
||||
}
|
||||
|
||||
export async function parseHar(file: string): Promise<Map<string, Buffer>> {
|
||||
const zipFS = new utils.ZipFile(file);
|
||||
const resources = new Map<string, Buffer>();
|
||||
for (const entry of await zipFS.entries())
|
||||
resources.set(entry, await zipFS.read(entry));
|
||||
zipFS.close();
|
||||
return resources;
|
||||
}
|
||||
|
||||
export function waitForTestLog<T>(page: Page, prefix: string): Promise<T> {
|
||||
return new Promise<T>(resolve => {
|
||||
page.on('console', message => {
|
||||
const text = message.text();
|
||||
if (text.startsWith(prefix)) {
|
||||
const json = text.substring(prefix.length);
|
||||
resolve(JSON.parse(json));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function rafraf(target: Page | Frame, count = 1) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await target.evaluate(async () => {
|
||||
await new Promise(f => window.builtins.requestAnimationFrame(() => window.builtins.requestAnimationFrame(f)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureSomeFrames(page: Page) {
|
||||
await rafraf(page, 100);
|
||||
await page.screenshot();
|
||||
}
|
||||
|
||||
export function roundBox(box: BoundingBox): BoundingBox {
|
||||
return {
|
||||
x: Math.round(box.x),
|
||||
y: Math.round(box.y),
|
||||
width: Math.round(box.width),
|
||||
height: Math.round(box.height),
|
||||
};
|
||||
}
|
||||
|
||||
export function unshift(snapshot: string): string {
|
||||
const lines = snapshot.split('\n');
|
||||
let whitespacePrefixLength = 100;
|
||||
for (const line of lines) {
|
||||
if (!line.trim())
|
||||
continue;
|
||||
const match = line.match(/^(\s*)/);
|
||||
if (match && match[1].length < whitespacePrefixLength)
|
||||
whitespacePrefixLength = match[1].length;
|
||||
}
|
||||
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
|
||||
}
|
||||
|
||||
const ansiRegex = new RegExp('[\\u001B\\u009B][[\\]()#?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{0,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g');
|
||||
export function stripAnsi(str: string): string {
|
||||
return str.replace(ansiRegex, '');
|
||||
}
|
||||
|
||||
export function inheritAndCleanEnv(env: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
// BEGIN: Reserved CI
|
||||
CI: undefined,
|
||||
BUILD_URL: undefined,
|
||||
CI_COMMIT_SHA: undefined,
|
||||
CI_JOB_URL: undefined,
|
||||
CI_PROJECT_URL: undefined,
|
||||
GITHUB_ACTIONS: undefined,
|
||||
GITHUB_REPOSITORY: undefined,
|
||||
GITHUB_RUN_ID: undefined,
|
||||
GITHUB_SERVER_URL: undefined,
|
||||
GITHUB_SHA: undefined,
|
||||
GITHUB_EVENT_PATH: undefined,
|
||||
// END: Reserved CI
|
||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||
PW_TEST_DEBUG_REPORTERS: undefined,
|
||||
PW_TEST_REPORTER: undefined,
|
||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||
PW_TEST_SOURCE_TRANSFORM_SCOPE: undefined,
|
||||
PWTEST_BOT_NAME: undefined,
|
||||
PWTEST_SHARD_WEIGHTS: undefined,
|
||||
TEST_WORKER_INDEX: undefined,
|
||||
TEST_PARALLEL_INDEX: undefined,
|
||||
NODE_OPTIONS: undefined,
|
||||
...env,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user