mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00: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"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-react": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
@ -8314,7 +8315,8 @@
|
|||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-react17": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
@ -8329,7 +8331,8 @@
|
|||||||
"vite-plugin-solid": "^2.7.0"
|
"vite-plugin-solid": "^2.7.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-solid": "cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"solid-js": "^1.7.0"
|
"solid-js": "^1.7.0"
|
||||||
@ -8347,7 +8350,8 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-svelte": "cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"svelte": "^4.2.8"
|
"svelte": "^4.2.8"
|
||||||
@ -8365,7 +8369,8 @@
|
|||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-vue": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
@ -8380,7 +8385,8 @@
|
|||||||
"@vitejs/plugin-vue2": "^2.2.0"
|
"@vitejs/plugin-vue2": "^2.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-vue2": "cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
|
|||||||
@ -169,6 +169,37 @@ export function createHttpsServer(...args: any[]): https.Server {
|
|||||||
return 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) {
|
function decorateServer(server: http.Server | http.Server) {
|
||||||
const sockets = new Set<net.Socket>();
|
const sockets = new Set<net.Socket>();
|
||||||
server.on('connection', socket => {
|
server.on('connection', socket => {
|
||||||
|
|||||||
@ -4,6 +4,9 @@ generated/indexSource.ts
|
|||||||
[viteDevPlugin.ts]
|
[viteDevPlugin.ts]
|
||||||
generated/indexSource.ts
|
generated/indexSource.ts
|
||||||
|
|
||||||
|
[devServer.ts]
|
||||||
|
generated/indexSource.ts
|
||||||
|
|
||||||
[mount.ts]
|
[mount.ts]
|
||||||
generated/serializers.ts
|
generated/serializers.ts
|
||||||
injected/**
|
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.
|
* 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';
|
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 http from 'http';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import path from 'path';
|
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 { debug } from 'playwright-core/lib/utilsBundle';
|
||||||
import { internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
import { internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
||||||
import { stoppable } from 'playwright/lib/utilsBundle';
|
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 { source as injectedSource } from './generated/indexSource';
|
||||||
import type { ImportInfo } from './tsxTransform';
|
import type { ImportInfo } from './tsxTransform';
|
||||||
import type { ComponentRegistry } from './viteUtils';
|
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');
|
const log = debug('pw:vite');
|
||||||
|
|
||||||
@ -51,6 +51,19 @@ export function createPlugin(
|
|||||||
},
|
},
|
||||||
|
|
||||||
begin: async (suite: Suite) => {
|
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 dirs = resolveDirs(configDir, config);
|
||||||
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');
|
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) {
|
export async function createConfig(dirs: ComponentDirs, config: FullConfig, frameworkPluginFactory: (() => Promise<Plugin>) | undefined, supportJsxInJs: boolean) {
|
||||||
// We are going to have 3 config files:
|
// We are going to have 3 config files:
|
||||||
// - the defaults that user config overrides (baseConfig)
|
// - the defaults that user config overrides (baseConfig)
|
||||||
// - the user config (userConfig)
|
// - the user config (userConfig)
|
||||||
// - frameworks overrides (frameworkOverrides);
|
// - frameworks overrides (frameworkOverrides);
|
||||||
|
|
||||||
|
const endpoint = resolveEndpoint(config);
|
||||||
const use = config.projects[0].use as CtConfig;
|
const use = config.projects[0].use as CtConfig;
|
||||||
const baseURL = new URL(use.baseURL || 'http://localhost');
|
|
||||||
|
|
||||||
// Compose base config from the playwright config only.
|
// Compose base config from the playwright config only.
|
||||||
const baseConfig: InlineConfig = {
|
const baseConfig: InlineConfig = {
|
||||||
@ -76,16 +86,8 @@ export async function createConfig(dirs: ComponentDirs, config: FullConfig, fram
|
|||||||
build: {
|
build: {
|
||||||
outDir: dirs.outDir
|
outDir: dirs.outDir
|
||||||
},
|
},
|
||||||
preview: {
|
preview: endpoint,
|
||||||
https: baseURL.protocol.startsWith('https:') ? {} : undefined,
|
server: endpoint,
|
||||||
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
|
|
||||||
},
|
|
||||||
// Vite preview server will otherwise always return the index.html with 200.
|
// Vite preview server will otherwise always return the index.html with 200.
|
||||||
appType: 'mpa',
|
appType: 'mpa',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,5 +15,8 @@
|
|||||||
* limitations under the License.
|
* 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);
|
program.parse(process.argv);
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-react": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-react17": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"solid-js": "^1.7.0"
|
"solid-js": "^1.7.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-solid": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"svelte": "^4.2.8"
|
"svelte": "^4.2.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-svelte": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-vue": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js",
|
||||||
|
"pw-vue2": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,12 @@
|
|||||||
"default": "./index.js"
|
"default": "./index.js"
|
||||||
},
|
},
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
|
"./lib/common/configLoader": "./lib/common/configLoader.js",
|
||||||
|
"./lib/fsWatcher": "./lib/fsWatcher.js",
|
||||||
"./lib/program": "./lib/program.js",
|
"./lib/program": "./lib/program.js",
|
||||||
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
|
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
|
||||||
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
|
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
|
||||||
|
"./lib/runner/runner": "./lib/runner/runner.js",
|
||||||
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
|
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
|
||||||
"./lib/transform/transform": "./lib/transform/transform.js",
|
"./lib/transform/transform": "./lib/transform/transform.js",
|
||||||
"./lib/internalsForTest": "./lib/internalsForTest.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 net from 'net';
|
||||||
|
|
||||||
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
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 { FullConfig } from '../../types/testReporter';
|
||||||
import type { TestRunnerPlugin } from '.';
|
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');
|
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 }) {
|
async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canceled: boolean }) {
|
||||||
const logScale = [100, 250, 500];
|
const logScale = [100, 250, 500];
|
||||||
while (!cancellationToken.canceled) {
|
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']) {
|
function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
|
||||||
const urlObject = new URL(url);
|
const urlObject = new URL(url);
|
||||||
if (!checkPortOnly)
|
if (!checkPortOnly)
|
||||||
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr);
|
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, debugWebServer, onStdErr);
|
||||||
const port = urlObject.port;
|
const port = urlObject.port;
|
||||||
return () => isPortUsed(+port);
|
return () => isPortUsed(+port);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,7 +271,7 @@ function resolveReporter(id: string) {
|
|||||||
return require.resolve(id, { paths: [process.cwd()] });
|
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];
|
const nodeVersion = +process.versions.node.split('.')[0];
|
||||||
// New experimental loader is only supported on Node 16+.
|
// New experimental loader is only supported on Node 16+.
|
||||||
if (nodeVersion < 16)
|
if (nodeVersion < 16)
|
||||||
|
|||||||
@ -8,3 +8,4 @@
|
|||||||
../util.ts
|
../util.ts
|
||||||
../utilsBundle.ts
|
../utilsBundle.ts
|
||||||
../isomorphic/folders.ts
|
../isomorphic/folders.ts
|
||||||
|
../fsWatcher.ts
|
||||||
|
|||||||
@ -104,6 +104,24 @@ export class Runner {
|
|||||||
return status;
|
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']> {
|
async watchAllTests(): Promise<FullResult['status']> {
|
||||||
const config = this._config;
|
const config = this._config;
|
||||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||||
|
|||||||
@ -23,13 +23,12 @@ import { InternalReporter } from '../reporters/internalReporter';
|
|||||||
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
||||||
import { createReporters } from './reporters';
|
import { createReporters } from './reporters';
|
||||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||||
import { chokidar } from '../utilsBundle';
|
|
||||||
import type { FSWatcher } from 'chokidar';
|
|
||||||
import { open } from 'playwright-core/lib/utilsBundle';
|
import { open } from 'playwright-core/lib/utilsBundle';
|
||||||
import ListReporter from '../reporters/list';
|
import ListReporter from '../reporters/list';
|
||||||
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { SigIntWatcher } from './sigIntWatcher';
|
import { SigIntWatcher } from './sigIntWatcher';
|
||||||
|
import { Watcher } from '../fsWatcher';
|
||||||
|
|
||||||
class UIMode {
|
class UIMode {
|
||||||
private _config: FullConfigInternal;
|
private _config: FullConfigInternal;
|
||||||
@ -258,59 +257,6 @@ function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): Stdi
|
|||||||
return { type, text: chunk };
|
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 {
|
function hasSomeBrowsers(): boolean {
|
||||||
for (const browserName of ['chromium', 'webkit', 'firefox']) {
|
for (const browserName of ['chromium', 'webkit', 'firefox']) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user