mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
chore(ct): use sticky test server if available (#29136)
This commit is contained in:
parent
f5de6e5538
commit
f7fb1e4d4e
18
package-lock.json
generated
18
package-lock.json
generated
@ -8299,7 +8299,8 @@
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-react": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@ -8314,7 +8315,8 @@
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-react17": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@ -8329,7 +8331,8 @@
|
||||
"vite-plugin-solid": "^2.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-solid": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.7.0"
|
||||
@ -8347,7 +8350,8 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-svelte": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^4.2.8"
|
||||
@ -8365,7 +8369,8 @@
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-vue": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@ -8380,7 +8385,8 @@
|
||||
"@vitejs/plugin-vue2": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-vue2": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vue": "^2.7.14"
|
||||
|
@ -169,6 +169,37 @@ export function createHttpsServer(...args: any[]): https.Server {
|
||||
return server;
|
||||
}
|
||||
|
||||
export async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void) {
|
||||
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onLog, onStdErr);
|
||||
if (statusCode === 404 && url.pathname === '/') {
|
||||
const indexUrl = new URL(url);
|
||||
indexUrl.pathname = '/index.html';
|
||||
statusCode = await httpStatusCode(indexUrl, ignoreHTTPSErrors, onLog, onStdErr);
|
||||
}
|
||||
return statusCode >= 200 && statusCode < 404;
|
||||
}
|
||||
|
||||
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void): Promise<number> {
|
||||
return new Promise(resolve => {
|
||||
onLog?.(`HTTP GET: ${url}`);
|
||||
httpRequest({
|
||||
url: url.toString(),
|
||||
headers: { Accept: '*/*' },
|
||||
rejectUnauthorized: !ignoreHTTPSErrors
|
||||
}, res => {
|
||||
res.resume();
|
||||
const statusCode = res.statusCode ?? 0;
|
||||
onLog?.(`HTTP Status: ${statusCode}`);
|
||||
resolve(statusCode);
|
||||
}, error => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
|
||||
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
|
||||
onLog?.(`Error while checking if ${url} is available: ${error.message}`);
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function decorateServer(server: http.Server | http.Server) {
|
||||
const sockets = new Set<net.Socket>();
|
||||
server.on('connection', socket => {
|
||||
|
@ -4,6 +4,9 @@ generated/indexSource.ts
|
||||
[viteDevPlugin.ts]
|
||||
generated/indexSource.ts
|
||||
|
||||
[devServer.ts]
|
||||
generated/indexSource.ts
|
||||
|
||||
[mount.ts]
|
||||
generated/serializers.ts
|
||||
injected/**
|
||||
|
92
packages/playwright-ct-core/src/devServer.ts
Normal file
92
packages/playwright-ct-core/src/devServer.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 { FullConfigInternal } from 'playwright/lib/common/config';
|
||||
import { ConfigLoader, resolveConfigFile } from 'playwright/lib/common/configLoader';
|
||||
import { Watcher } from 'playwright/lib/fsWatcher';
|
||||
import { restartWithExperimentalTsEsm } from 'playwright/lib/program';
|
||||
import { Runner } from 'playwright/lib/runner/runner';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import { source as injectedSource } from './generated/indexSource';
|
||||
import { createConfig, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils';
|
||||
import type { ComponentRegistry } from './viteUtils';
|
||||
|
||||
export async function runDevServer(configFile: string, registerSourceFile: string, frameworkPluginFactory: () => Promise<any>) {
|
||||
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
|
||||
if (restartWithExperimentalTsEsm(resolvedConfigFile))
|
||||
return;
|
||||
|
||||
const configLoader = new ConfigLoader();
|
||||
let config: FullConfigInternal;
|
||||
if (resolvedConfigFile)
|
||||
config = await configLoader.loadConfigFile(resolvedConfigFile);
|
||||
else
|
||||
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
|
||||
|
||||
const runner = new Runner(config);
|
||||
await runner.loadAllTests(true);
|
||||
const componentRegistry: ComponentRegistry = new Map();
|
||||
await populateComponentsFromTests(componentRegistry);
|
||||
|
||||
const dirs = resolveDirs(config.configDir, config.config);
|
||||
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false);
|
||||
viteConfig.plugins.push({
|
||||
name: 'playwright:component-index',
|
||||
|
||||
async transform(this: PluginContext, content: string, id: string) {
|
||||
return transformIndexFile(id, content, dirs.templateDir, registerSource, componentRegistry);
|
||||
},
|
||||
});
|
||||
|
||||
const { createServer } = await import('vite');
|
||||
const devServer = await createServer(viteConfig);
|
||||
await devServer.listen();
|
||||
const protocol = viteConfig.server.https ? 'https:' : 'http:';
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Test Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`);
|
||||
|
||||
const projectDirs = new Set<string>();
|
||||
const projectOutputs = new Set<string>();
|
||||
for (const p of config.projects) {
|
||||
projectDirs.add(p.project.testDir);
|
||||
projectOutputs.add(p.project.outputDir);
|
||||
}
|
||||
|
||||
const globalWatcher = new Watcher('deep', async () => {
|
||||
const registry: ComponentRegistry = new Map();
|
||||
await populateComponentsFromTests(registry);
|
||||
// compare componentRegistry to registry key sets.
|
||||
if (componentRegistry.size === registry.size && [...componentRegistry.keys()].every(k => registry.has(k)))
|
||||
return;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('List of components changed');
|
||||
componentRegistry.clear();
|
||||
for (const [k, v] of registry)
|
||||
componentRegistry.set(k, v);
|
||||
|
||||
const id = path.join(dirs.templateDir, 'index');
|
||||
const modules = [...devServer.moduleGraph.urlToModuleMap.values()];
|
||||
const rootModule = modules.find(m => m.file?.startsWith(id + '.ts') || m.file?.startsWith(id + '.js'));
|
||||
if (rootModule)
|
||||
devServer.moduleGraph.onFileChange(rootModule.file!);
|
||||
});
|
||||
globalWatcher.update([...projectDirs], [...projectOutputs], false);
|
||||
}
|
@ -14,4 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Command } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { program } from 'playwright/lib/program';
|
||||
import { runDevServer } from './devServer';
|
||||
export { program } from 'playwright/lib/program';
|
||||
|
||||
let registerSourceFile: string;
|
||||
let frameworkPluginFactory: () => Promise<any>;
|
||||
|
||||
export function initializePlugin(registerSource: string, factory: () => Promise<any>) {
|
||||
registerSourceFile = registerSource;
|
||||
frameworkPluginFactory = factory;
|
||||
}
|
||||
|
||||
function addDevServerCommand(program: Command) {
|
||||
const command = program.command('dev-server');
|
||||
command.description('start dev server');
|
||||
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
|
||||
command.action(options => {
|
||||
runDevServer(options.config, registerSourceFile, frameworkPluginFactory);
|
||||
});
|
||||
}
|
||||
|
||||
addDevServerCommand(program);
|
||||
|
@ -1,64 +0,0 @@
|
||||
/**
|
||||
* 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 { FullConfig } from 'playwright/test';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { TestRunnerPlugin } from '../../playwright/src/plugins';
|
||||
import { source as injectedSource } from './generated/indexSource';
|
||||
import type { ImportInfo } from './tsxTransform';
|
||||
import type { ComponentRegistry } from './viteUtils';
|
||||
import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils';
|
||||
|
||||
export function createPlugin(
|
||||
registerSourceFile: string,
|
||||
frameworkPluginFactory?: () => Promise<Plugin>): TestRunnerPlugin {
|
||||
let configDir: string;
|
||||
let config: FullConfig;
|
||||
return {
|
||||
name: 'playwright-vite-plugin',
|
||||
|
||||
setup: async (configObject: FullConfig, configDirectory: string) => {
|
||||
config = configObject;
|
||||
configDir = configDirectory;
|
||||
},
|
||||
|
||||
begin: async () => {
|
||||
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const componentRegistry: ComponentRegistry = new Map();
|
||||
await populateComponentsFromTests(componentRegistry);
|
||||
const dirs = resolveDirs(configDir, config);
|
||||
const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, hasJSComponents([...componentRegistry.values()]));
|
||||
viteConfig.plugins.push(vitePlugin(registerSource, dirs.templateDir, componentRegistry));
|
||||
const { createServer } = await import('vite');
|
||||
const devServer = await createServer(viteConfig);
|
||||
await devServer.listen();
|
||||
const protocol = viteConfig.server.https ? 'https:' : 'http:';
|
||||
process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function vitePlugin(registerSource: string, templateDir: string, importInfos: Map<string, ImportInfo>): Plugin {
|
||||
return {
|
||||
name: 'playwright:component-index',
|
||||
|
||||
async transform(this: PluginContext, content, id) {
|
||||
return transformIndexFile(id, content, templateDir, registerSource, importInfos);
|
||||
},
|
||||
};
|
||||
}
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
||||
import type http from 'http';
|
||||
import type { AddressInfo } from 'net';
|
||||
import path from 'path';
|
||||
import { assert, calculateSha1, getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||
import { assert, calculateSha1, getPlaywrightVersion, isURLAvailable } from 'playwright-core/lib/utils';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
import { internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
||||
import { stoppable } from 'playwright/lib/utilsBundle';
|
||||
@ -30,7 +30,7 @@ import type { TestRunnerPlugin } from '../../playwright/src/plugins';
|
||||
import { source as injectedSource } from './generated/indexSource';
|
||||
import type { ImportInfo } from './tsxTransform';
|
||||
import type { ComponentRegistry } from './viteUtils';
|
||||
import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils';
|
||||
import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils';
|
||||
|
||||
const log = debug('pw:vite');
|
||||
|
||||
@ -51,6 +51,19 @@ export function createPlugin(
|
||||
},
|
||||
|
||||
begin: async (suite: Suite) => {
|
||||
{
|
||||
// Detect a running dev server and use it if available.
|
||||
const endpoint = resolveEndpoint(config);
|
||||
const protocol = endpoint.https ? 'https:' : 'http:';
|
||||
const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`);
|
||||
if (process.env.PW_CT_DEV && await isURLAvailable(url, true)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Test Server is already running at ${url.toString()}, using it.\n`);
|
||||
process.env.PLAYWRIGHT_TEST_BASE_URL = url.toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dirs = resolveDirs(configDir, config);
|
||||
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');
|
||||
|
||||
|
@ -53,14 +53,24 @@ export function resolveDirs(configDir: string, config: FullConfig): ComponentDir
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveEndpoint(config: FullConfig) {
|
||||
const use = config.projects[0].use as CtConfig;
|
||||
const baseURL = new URL(use.baseURL || 'http://localhost');
|
||||
return {
|
||||
https: baseURL.protocol.startsWith('https:') ? {} : undefined,
|
||||
host: baseURL.hostname,
|
||||
port: use.ctPort || Number(baseURL.port) || 3100
|
||||
};
|
||||
}
|
||||
|
||||
export async function createConfig(dirs: ComponentDirs, config: FullConfig, frameworkPluginFactory: (() => Promise<Plugin>) | undefined, supportJsxInJs: boolean) {
|
||||
// We are going to have 3 config files:
|
||||
// - the defaults that user config overrides (baseConfig)
|
||||
// - the user config (userConfig)
|
||||
// - frameworks overrides (frameworkOverrides);
|
||||
|
||||
const endpoint = resolveEndpoint(config);
|
||||
const use = config.projects[0].use as CtConfig;
|
||||
const baseURL = new URL(use.baseURL || 'http://localhost');
|
||||
|
||||
// Compose base config from the playwright config only.
|
||||
const baseConfig: InlineConfig = {
|
||||
@ -76,16 +86,8 @@ export async function createConfig(dirs: ComponentDirs, config: FullConfig, fram
|
||||
build: {
|
||||
outDir: dirs.outDir
|
||||
},
|
||||
preview: {
|
||||
https: baseURL.protocol.startsWith('https:') ? {} : undefined,
|
||||
host: baseURL.hostname,
|
||||
port: use.ctPort || Number(baseURL.port) || 3100
|
||||
},
|
||||
server: {
|
||||
https: baseURL.protocol.startsWith('https:') ? {} : undefined,
|
||||
host: baseURL.hostname,
|
||||
port: use.ctPort || Number(baseURL.port) || 3100
|
||||
},
|
||||
preview: endpoint,
|
||||
server: endpoint,
|
||||
// Vite preview server will otherwise always return the index.html with 200.
|
||||
appType: 'mpa',
|
||||
};
|
||||
|
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const path = require('path');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
|
||||
initializePlugin(path.join(__dirname, 'registerSource.mjs'), () => import('@vitejs/plugin-react').then(plugin => plugin.default()))
|
||||
program.parse(process.argv);
|
||||
|
@ -33,6 +33,7 @@
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-react": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-react17": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
"solid-js": "^1.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-solid": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
"svelte": "^4.2.8"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-svelte": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-vue": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
"vue": "^2.7.14"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
"playwright": "cli.js",
|
||||
"pw-vue2": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,12 @@
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./lib/common/configLoader": "./lib/common/configLoader.js",
|
||||
"./lib/fsWatcher": "./lib/fsWatcher.js",
|
||||
"./lib/program": "./lib/program.js",
|
||||
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
|
||||
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
|
||||
"./lib/runner/runner": "./lib/runner/runner.js",
|
||||
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
|
||||
"./lib/transform/transform": "./lib/transform/transform.js",
|
||||
"./lib/internalsForTest": "./lib/internalsForTest.js",
|
||||
|
71
packages/playwright/src/fsWatcher.ts
Normal file
71
packages/playwright/src/fsWatcher.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 { chokidar } from './utilsBundle';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
||||
|
||||
export class Watcher {
|
||||
private _onChange: (events: FSEvent[]) => void;
|
||||
private _watchedFiles: string[] = [];
|
||||
private _ignoredFolders: string[] = [];
|
||||
private _collector: FSEvent[] = [];
|
||||
private _fsWatcher: FSWatcher | undefined;
|
||||
private _throttleTimer: NodeJS.Timeout | undefined;
|
||||
private _mode: 'flat' | 'deep';
|
||||
|
||||
constructor(mode: 'flat' | 'deep', onChange: (events: FSEvent[]) => void) {
|
||||
this._mode = mode;
|
||||
this._onChange = onChange;
|
||||
}
|
||||
|
||||
update(watchedFiles: string[], ignoredFolders: string[], reportPending: boolean) {
|
||||
if (JSON.stringify([this._watchedFiles, this._ignoredFolders]) === JSON.stringify(watchedFiles, ignoredFolders))
|
||||
return;
|
||||
|
||||
if (reportPending)
|
||||
this._reportEventsIfAny();
|
||||
|
||||
this._watchedFiles = watchedFiles;
|
||||
this._ignoredFolders = ignoredFolders;
|
||||
void this._fsWatcher?.close();
|
||||
this._fsWatcher = undefined;
|
||||
this._collector.length = 0;
|
||||
clearTimeout(this._throttleTimer);
|
||||
this._throttleTimer = undefined;
|
||||
|
||||
if (!this._watchedFiles.length)
|
||||
return;
|
||||
|
||||
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored: this._ignoredFolders }).on('all', async (event, file) => {
|
||||
if (this._throttleTimer)
|
||||
clearTimeout(this._throttleTimer);
|
||||
if (this._mode === 'flat' && event !== 'add' && event !== 'change')
|
||||
return;
|
||||
if (this._mode === 'deep' && event !== 'add' && event !== 'change' && event !== 'unlink' && event !== 'addDir' && event !== 'unlinkDir')
|
||||
return;
|
||||
this._collector.push({ event, file });
|
||||
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
|
||||
});
|
||||
}
|
||||
|
||||
private _reportEventsIfAny() {
|
||||
if (this._collector.length)
|
||||
this._onChange(this._collector.slice());
|
||||
this._collector.length = 0;
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import path from 'path';
|
||||
import net from 'net';
|
||||
|
||||
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
||||
import { raceAgainstDeadline, launchProcess, httpRequest, monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { raceAgainstDeadline, launchProcess, monotonicTime, isURLAvailable } from 'playwright-core/lib/utils';
|
||||
|
||||
import type { FullConfig } from '../../types/testReporter';
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
@ -155,37 +155,6 @@ async function isPortUsed(port: number): Promise<boolean> {
|
||||
return await innerIsPortUsed('127.0.0.1') || await innerIsPortUsed('::1');
|
||||
}
|
||||
|
||||
async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
|
||||
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onStdErr);
|
||||
if (statusCode === 404 && url.pathname === '/') {
|
||||
const indexUrl = new URL(url);
|
||||
indexUrl.pathname = '/index.html';
|
||||
statusCode = await httpStatusCode(indexUrl, ignoreHTTPSErrors, onStdErr);
|
||||
}
|
||||
return statusCode >= 200 && statusCode < 404;
|
||||
}
|
||||
|
||||
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']): Promise<number> {
|
||||
return new Promise(resolve => {
|
||||
debugWebServer(`HTTP GET: ${url}`);
|
||||
httpRequest({
|
||||
url: url.toString(),
|
||||
headers: { Accept: '*/*' },
|
||||
rejectUnauthorized: !ignoreHTTPSErrors
|
||||
}, res => {
|
||||
res.resume();
|
||||
const statusCode = res.statusCode ?? 0;
|
||||
debugWebServer(`HTTP Status: ${statusCode}`);
|
||||
resolve(statusCode);
|
||||
}, error => {
|
||||
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
|
||||
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
|
||||
debugWebServer(`Error while checking if ${url} is available: ${error.message}`);
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canceled: boolean }) {
|
||||
const logScale = [100, 250, 500];
|
||||
while (!cancellationToken.canceled) {
|
||||
@ -201,7 +170,7 @@ async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canc
|
||||
function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
|
||||
const urlObject = new URL(url);
|
||||
if (!checkPortOnly)
|
||||
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr);
|
||||
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, debugWebServer, onStdErr);
|
||||
const port = urlObject.port;
|
||||
return () => isPortUsed(+port);
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ function resolveReporter(id: string) {
|
||||
return require.resolve(id, { paths: [process.cwd()] });
|
||||
}
|
||||
|
||||
function restartWithExperimentalTsEsm(configFile: string | null): boolean {
|
||||
export function restartWithExperimentalTsEsm(configFile: string | null): boolean {
|
||||
const nodeVersion = +process.versions.node.split('.')[0];
|
||||
// New experimental loader is only supported on Node 16+.
|
||||
if (nodeVersion < 16)
|
||||
|
@ -8,3 +8,4 @@
|
||||
../util.ts
|
||||
../utilsBundle.ts
|
||||
../isomorphic/folders.ts
|
||||
../fsWatcher.ts
|
||||
|
@ -104,6 +104,24 @@ export class Runner {
|
||||
return status;
|
||||
}
|
||||
|
||||
async loadAllTests(outOfProcess?: boolean): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
const reporter = new InternalReporter(new Multiplexer([]));
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, outOfProcess ? 'out-of-process' : 'in-process', { failOnLoadErrors: true });
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
|
||||
const taskStatus = await taskRunner.run(testRun, 0);
|
||||
let status: FullResult['status'] = testRun.failureTracker.result();
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
const modifiedResult = await reporter.onEnd({ status });
|
||||
if (modifiedResult && modifiedResult.status)
|
||||
status = modifiedResult.status;
|
||||
await reporter.onExit();
|
||||
return status;
|
||||
}
|
||||
|
||||
async watchAllTests(): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
|
@ -23,13 +23,12 @@ import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
||||
import { createReporters } from './reporters';
|
||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import { chokidar } from '../utilsBundle';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { open } from 'playwright-core/lib/utilsBundle';
|
||||
import ListReporter from '../reporters/list';
|
||||
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { Watcher } from '../fsWatcher';
|
||||
|
||||
class UIMode {
|
||||
private _config: FullConfigInternal;
|
||||
@ -258,59 +257,6 @@ function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): Stdi
|
||||
return { type, text: chunk };
|
||||
}
|
||||
|
||||
type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
||||
|
||||
class Watcher {
|
||||
private _onChange: (events: FSEvent[]) => void;
|
||||
private _watchedFiles: string[] = [];
|
||||
private _ignoredFolders: string[] = [];
|
||||
private _collector: FSEvent[] = [];
|
||||
private _fsWatcher: FSWatcher | undefined;
|
||||
private _throttleTimer: NodeJS.Timeout | undefined;
|
||||
private _mode: 'flat' | 'deep';
|
||||
|
||||
constructor(mode: 'flat' | 'deep', onChange: (events: FSEvent[]) => void) {
|
||||
this._mode = mode;
|
||||
this._onChange = onChange;
|
||||
}
|
||||
|
||||
update(watchedFiles: string[], ignoredFolders: string[], reportPending: boolean) {
|
||||
if (JSON.stringify([this._watchedFiles, this._ignoredFolders]) === JSON.stringify(watchedFiles, ignoredFolders))
|
||||
return;
|
||||
|
||||
if (reportPending)
|
||||
this._reportEventsIfAny();
|
||||
|
||||
this._watchedFiles = watchedFiles;
|
||||
this._ignoredFolders = ignoredFolders;
|
||||
void this._fsWatcher?.close();
|
||||
this._fsWatcher = undefined;
|
||||
this._collector.length = 0;
|
||||
clearTimeout(this._throttleTimer);
|
||||
this._throttleTimer = undefined;
|
||||
|
||||
if (!this._watchedFiles.length)
|
||||
return;
|
||||
|
||||
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored: this._ignoredFolders }).on('all', async (event, file) => {
|
||||
if (this._throttleTimer)
|
||||
clearTimeout(this._throttleTimer);
|
||||
if (this._mode === 'flat' && event !== 'add' && event !== 'change')
|
||||
return;
|
||||
if (this._mode === 'deep' && event !== 'add' && event !== 'change' && event !== 'unlink' && event !== 'addDir' && event !== 'unlinkDir')
|
||||
return;
|
||||
this._collector.push({ event, file });
|
||||
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
|
||||
});
|
||||
}
|
||||
|
||||
private _reportEventsIfAny() {
|
||||
if (this._collector.length)
|
||||
this._onChange(this._collector.slice());
|
||||
this._collector.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function hasSomeBrowsers(): boolean {
|
||||
for (const browserName of ['chromium', 'webkit', 'firefox']) {
|
||||
try {
|
||||
|
Loading…
Reference in New Issue
Block a user