mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: add find-related-tests command (#29439)
This commit is contained in:
parent
f0244b8a76
commit
586d14f02c
@ -16,42 +16,30 @@
|
||||
|
||||
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 { loadConfigFromFile } from 'playwright/lib/common/configLoader';
|
||||
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 loadConfig(configFile: string): Promise<FullConfigInternal | null> {
|
||||
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
|
||||
if (restartWithExperimentalTsEsm(resolvedConfigFile))
|
||||
return null;
|
||||
|
||||
const configLoader = new ConfigLoader();
|
||||
let config: FullConfigInternal;
|
||||
if (resolvedConfigFile)
|
||||
config = await configLoader.loadConfigFile(resolvedConfigFile);
|
||||
else
|
||||
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function runDevServer(configFile: string, registerSourceFile: string, frameworkPluginFactory: () => Promise<any>) {
|
||||
const config = await loadConfig(configFile);
|
||||
const config = await loadConfigFromFile(configFile);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
const runner = new Runner(config);
|
||||
await runner.loadAllTests(true);
|
||||
await runner.loadAllTests();
|
||||
const componentRegistry: ComponentRegistry = new Map();
|
||||
await populateComponentsFromTests(componentRegistry);
|
||||
|
||||
const dirs = await resolveDirs(config.configDir, config.config);
|
||||
if (!dirs) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Template file playwright/index.html is missing.`);
|
||||
return;
|
||||
}
|
||||
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false);
|
||||
viteConfig.plugins.push({
|
||||
|
||||
@ -16,55 +16,56 @@
|
||||
|
||||
import type { Command } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import fs from 'fs';
|
||||
import { program } from 'playwright/lib/program';
|
||||
import { loadConfig, runDevServer } from './devServer';
|
||||
import path from 'path';
|
||||
import { program, removeFolder, setClearCacheCommandOverride, setFindRelatedTestsCommandOverride, withRunnerAndMutedWrite } from 'playwright/lib/program';
|
||||
import { runDevServer } from './devServer';
|
||||
import { resolveDirs } from './viteUtils';
|
||||
import { cacheDir } from 'playwright/lib/transform/compilationCache';
|
||||
import { affectedTestFiles, cacheDir } from 'playwright/lib/transform/compilationCache';
|
||||
import { loadConfigFromFile } from 'playwright/lib/common/configLoader';
|
||||
import { buildBundle } from './vitePlugin';
|
||||
export { program } from 'playwright/lib/program';
|
||||
|
||||
let registerSourceFile: string;
|
||||
let frameworkPluginFactory: () => Promise<any>;
|
||||
let _framework: { registerSource: string, frameworkPluginFactory: () => Promise<any> };
|
||||
|
||||
export function initializePlugin(registerSource: string, factory: () => Promise<any>) {
|
||||
registerSourceFile = registerSource;
|
||||
frameworkPluginFactory = factory;
|
||||
export function initializePlugin(framework: { registerSource: string, frameworkPluginFactory: () => Promise<any> }) {
|
||||
_framework = framework;
|
||||
}
|
||||
|
||||
function addDevServerCommand(program: Command) {
|
||||
const command = program.command('dev-server');
|
||||
command.description('start dev server');
|
||||
command.option('-c, --config <file>', `Configuration file.`);
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.action(options => {
|
||||
runDevServer(options.config, registerSourceFile, frameworkPluginFactory);
|
||||
runDevServer(options.config, _framework.registerSource, _framework.frameworkPluginFactory);
|
||||
});
|
||||
}
|
||||
|
||||
function addClearCacheCommand(program: Command) {
|
||||
const command = program.command('clear-caches');
|
||||
command.description('clears build and test caches');
|
||||
command.option('-c, --config <file>', `Configuration file.`);
|
||||
command.action(async options => {
|
||||
const configFile = options.config;
|
||||
const config = await loadConfig(configFile);
|
||||
if (!config)
|
||||
return;
|
||||
const { outDir } = await resolveDirs(config.configDir, config.config);
|
||||
await removeFolder(outDir);
|
||||
await removeFolder(cacheDir);
|
||||
setFindRelatedTestsCommandOverride(async (files, options) => {
|
||||
await withRunnerAndMutedWrite(options.config, async (runner, config, configDir) => {
|
||||
const result = await runner.loadAllTests();
|
||||
if (result.status !== 'passed' || !result.suite)
|
||||
return { errors: result.errors };
|
||||
await buildBundle({
|
||||
config,
|
||||
configDir,
|
||||
suite: result.suite,
|
||||
registerSourceFile: _framework.registerSource,
|
||||
frameworkPluginFactory: _framework.frameworkPluginFactory,
|
||||
});
|
||||
const resolvedFiles = (files as string[]).map(file => path.resolve(process.cwd(), file));
|
||||
return { relatedTests: affectedTestFiles(resolvedFiles) };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function removeFolder(folder: string) {
|
||||
try {
|
||||
if (!fs.existsSync(folder))
|
||||
return;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Removing ${await fs.promises.realpath(folder)}`);
|
||||
await fs.promises.rm(folder, { recursive: true, force: true });
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
setClearCacheCommandOverride(async options => {
|
||||
const configFile = options.config;
|
||||
const config = await loadConfigFromFile(configFile);
|
||||
if (!config)
|
||||
return;
|
||||
const dirs = await resolveDirs(config.configDir, config.config);
|
||||
if (dirs)
|
||||
await removeFolder(dirs.outDir);
|
||||
await removeFolder(cacheDir);
|
||||
});
|
||||
|
||||
addDevServerCommand(program);
|
||||
addClearCacheCommand(program);
|
||||
|
||||
@ -52,103 +52,18 @@ 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 (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 = await resolveDirs(configDir, config);
|
||||
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');
|
||||
|
||||
let buildExists = false;
|
||||
let buildInfo: BuildInfo;
|
||||
|
||||
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const registerSourceHash = calculateSha1(registerSource);
|
||||
|
||||
const { version: viteVersion, build, preview, mergeConfig } = await import('vite');
|
||||
|
||||
try {
|
||||
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
|
||||
assert(buildInfo.version === playwrightVersion);
|
||||
assert(buildInfo.viteVersion === viteVersion);
|
||||
assert(buildInfo.registerSourceHash === registerSourceHash);
|
||||
buildExists = true;
|
||||
} catch (e) {
|
||||
buildInfo = {
|
||||
version: playwrightVersion,
|
||||
viteVersion,
|
||||
registerSourceHash,
|
||||
components: [],
|
||||
sources: {},
|
||||
deps: {},
|
||||
};
|
||||
}
|
||||
log('build exists:', buildExists);
|
||||
|
||||
const componentRegistry: ComponentRegistry = new Map();
|
||||
const componentsByImportingFile = new Map<string, string[]>();
|
||||
// 1. Populate component registry based on tests' component imports.
|
||||
await populateComponentsFromTests(componentRegistry, componentsByImportingFile);
|
||||
|
||||
// 2. Check if the set of required components has changed.
|
||||
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
||||
log('has new components:', hasNewComponents);
|
||||
|
||||
// 3. Check component sources.
|
||||
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
|
||||
log('sourcesDirty:', sourcesDirty);
|
||||
|
||||
// 4. Update component info.
|
||||
buildInfo.components = [...componentRegistry.values()];
|
||||
|
||||
const jsxInJS = hasJSComponents(buildInfo.components);
|
||||
const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, jsxInJS);
|
||||
|
||||
if (sourcesDirty) {
|
||||
// Only add out own plugin when we actually build / transform.
|
||||
log('build');
|
||||
const depsCollector = new Map<string, string[]>();
|
||||
const buildConfig = mergeConfig(viteConfig, {
|
||||
plugins: [vitePlugin(registerSource, dirs.templateDir, buildInfo, componentRegistry, depsCollector)]
|
||||
});
|
||||
await build(buildConfig);
|
||||
buildInfo.deps = Object.fromEntries(depsCollector.entries());
|
||||
|
||||
// Update dependencies based on the vite build.
|
||||
for (const projectSuite of suite.suites) {
|
||||
for (const fileSuite of projectSuite.suites) {
|
||||
// For every test file...
|
||||
const testFile = fileSuite.location!.file;
|
||||
const deps = new Set<string>();
|
||||
// Collect its JS dependencies (helpers).
|
||||
for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) {
|
||||
// For each helper, get all the imported components.
|
||||
for (const componentFile of componentsByImportingFile.get(file) || []) {
|
||||
// For each component, get all the dependencies.
|
||||
for (const d of depsCollector.get(componentFile) || [])
|
||||
deps.add(d);
|
||||
}
|
||||
}
|
||||
// Now we have test file => all components along with dependencies.
|
||||
setExternalDependencies(testFile, [...deps]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewComponents || sourcesDirty) {
|
||||
log('write manifest');
|
||||
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
||||
}
|
||||
const result = await buildBundle({
|
||||
config,
|
||||
configDir,
|
||||
suite,
|
||||
registerSourceFile,
|
||||
frameworkPluginFactory: frameworkPluginFactory,
|
||||
});
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
const { viteConfig } = result;
|
||||
const { preview } = await import('vite');
|
||||
const previewServer = await preview(viteConfig);
|
||||
stoppableServer = stoppable(previewServer.httpServer as http.Server, 0);
|
||||
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
||||
@ -181,6 +96,120 @@ type BuildInfo = {
|
||||
}
|
||||
};
|
||||
|
||||
export async function buildBundle(options: {
|
||||
config: FullConfig,
|
||||
configDir: string,
|
||||
suite: Suite,
|
||||
registerSourceFile: string,
|
||||
frameworkPluginFactory?: () => Promise<Plugin>
|
||||
}): Promise<{ buildInfo: BuildInfo, viteConfig: Record<string, any> } | null> {
|
||||
{
|
||||
// Detect a running dev server and use it if available.
|
||||
const endpoint = resolveEndpoint(options.config);
|
||||
const protocol = endpoint.https ? 'https:' : 'http:';
|
||||
const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`);
|
||||
if (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 null;
|
||||
}
|
||||
}
|
||||
|
||||
const dirs = await resolveDirs(options.configDir, options.config);
|
||||
if (!dirs) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Template file playwright/index.html is missing.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');
|
||||
|
||||
let buildExists = false;
|
||||
let buildInfo: BuildInfo;
|
||||
|
||||
const registerSource = injectedSource + '\n' + await fs.promises.readFile(options.registerSourceFile, 'utf-8');
|
||||
const registerSourceHash = calculateSha1(registerSource);
|
||||
|
||||
const { version: viteVersion, build, mergeConfig } = await import('vite');
|
||||
|
||||
try {
|
||||
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
|
||||
assert(buildInfo.version === playwrightVersion);
|
||||
assert(buildInfo.viteVersion === viteVersion);
|
||||
assert(buildInfo.registerSourceHash === registerSourceHash);
|
||||
buildExists = true;
|
||||
} catch (e) {
|
||||
buildInfo = {
|
||||
version: playwrightVersion,
|
||||
viteVersion,
|
||||
registerSourceHash,
|
||||
components: [],
|
||||
sources: {},
|
||||
deps: {},
|
||||
};
|
||||
}
|
||||
log('build exists:', buildExists);
|
||||
|
||||
const componentRegistry: ComponentRegistry = new Map();
|
||||
const componentsByImportingFile = new Map<string, string[]>();
|
||||
// 1. Populate component registry based on tests' component imports.
|
||||
await populateComponentsFromTests(componentRegistry, componentsByImportingFile);
|
||||
|
||||
// 2. Check if the set of required components has changed.
|
||||
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
||||
log('has new components:', hasNewComponents);
|
||||
|
||||
// 3. Check component sources.
|
||||
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
|
||||
log('sourcesDirty:', sourcesDirty);
|
||||
|
||||
// 4. Update component info.
|
||||
buildInfo.components = [...componentRegistry.values()];
|
||||
|
||||
const jsxInJS = hasJSComponents(buildInfo.components);
|
||||
const viteConfig = await createConfig(dirs, options.config, options.frameworkPluginFactory, jsxInJS);
|
||||
|
||||
if (sourcesDirty) {
|
||||
// Only add out own plugin when we actually build / transform.
|
||||
log('build');
|
||||
const depsCollector = new Map<string, string[]>();
|
||||
const buildConfig = mergeConfig(viteConfig, {
|
||||
plugins: [vitePlugin(registerSource, dirs.templateDir, buildInfo, componentRegistry, depsCollector)]
|
||||
});
|
||||
await build(buildConfig);
|
||||
buildInfo.deps = Object.fromEntries(depsCollector.entries());
|
||||
}
|
||||
|
||||
{
|
||||
// Update dependencies based on the vite build.
|
||||
for (const projectSuite of options.suite.suites) {
|
||||
for (const fileSuite of projectSuite.suites) {
|
||||
// For every test file...
|
||||
const testFile = fileSuite.location!.file;
|
||||
const deps = new Set<string>();
|
||||
// Collect its JS dependencies (helpers).
|
||||
for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) {
|
||||
// For each helper, get all the imported components.
|
||||
for (const componentFile of componentsByImportingFile.get(file) || []) {
|
||||
// For each component, get all the dependencies.
|
||||
for (const d of buildInfo.deps[componentFile] || [])
|
||||
deps.add(d);
|
||||
}
|
||||
}
|
||||
// Now we have test file => all components along with dependencies.
|
||||
setExternalDependencies(testFile, [...deps]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewComponents || sourcesDirty) {
|
||||
log('write manifest');
|
||||
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
||||
}
|
||||
return { buildInfo, viteConfig };
|
||||
}
|
||||
|
||||
async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
|
||||
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
|
||||
try {
|
||||
|
||||
@ -39,13 +39,15 @@ export type ComponentDirs = {
|
||||
templateDir: string;
|
||||
};
|
||||
|
||||
export async function resolveDirs(configDir: string, config: FullConfig): Promise<ComponentDirs> {
|
||||
export async function resolveDirs(configDir: string, config: FullConfig): Promise<ComponentDirs | null> {
|
||||
const use = config.projects[0].use as CtConfig;
|
||||
// FIXME: use build plugin to determine html location to resolve this.
|
||||
// TemplateDir must be relative, otherwise we can't move the final index.html into its target location post-build.
|
||||
// This regressed in https://github.com/microsoft/playwright/pull/26526
|
||||
const relativeTemplateDir = use.ctTemplateDir || 'playwright';
|
||||
const templateDir = await fs.promises.realpath(path.normalize(path.join(configDir, relativeTemplateDir)));
|
||||
const templateDir = await fs.promises.realpath(path.normalize(path.join(configDir, relativeTemplateDir))).catch(() => undefined);
|
||||
if (!templateDir)
|
||||
return null;
|
||||
const outDir = use.ctCacheDir ? path.resolve(configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache');
|
||||
return {
|
||||
configDir,
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(path.join(__dirname, 'registerSource.mjs'), () => import('@vitejs/plugin-react').then(plugin => plugin.default()))
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('@vitejs/plugin-react').then(plugin => plugin.default());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('@vitejs/plugin-react').then(plugin => plugin.default()));
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('@vitejs/plugin-react').then(plugin => plugin.default());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('@vitejs/plugin-react').then(plugin => plugin.default()));
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('vite-plugin-solid').then(plugin => plugin.default());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('vite-plugin-solid').then(plugin => plugin.default()));
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('@sveltejs/vite-plugin-svelte').then(plugin => plugin.svelte());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('@sveltejs/vite-plugin-svelte').then(plugin => plugin.svelte()));
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('@vitejs/plugin-vue').then(plugin => plugin.default());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('@vitejs/plugin-vue').then(plugin => plugin.default()));
|
||||
}
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -15,5 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
|
||||
const { _framework } = require('./index');
|
||||
|
||||
initializePlugin(_framework);
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -17,13 +17,14 @@
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const registerSource = path.join(__dirname, 'registerSource.mjs');
|
||||
const frameworkPluginFactory = () => import('@vitejs/plugin-vue2').then(plugin => plugin.default());
|
||||
|
||||
const plugin = () => {
|
||||
// Only fetch upon request to avoid resolution in workers.
|
||||
const { createPlugin } = require('@playwright/experimental-ct-core/plugin');
|
||||
return createPlugin(
|
||||
path.join(__dirname, 'registerSource.mjs'),
|
||||
() => import('@vitejs/plugin-vue2').then(plugin => plugin.default()));
|
||||
return createPlugin(registerSource, frameworkPluginFactory);
|
||||
};
|
||||
const defineConfig = (config, ...configs) => originalDefineConfig({ ...config, _plugins: [plugin] }, ...configs);
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
||||
module.exports = { test, expect, devices, defineConfig, _framework: { registerSource, frameworkPluginFactory } };
|
||||
|
||||
@ -16,15 +16,16 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isRegExp } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang, isRegExp } from 'playwright-core/lib/utils';
|
||||
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
|
||||
import { requireOrImport } from '../transform/transform';
|
||||
import type { Config, Project } from '../../types/test';
|
||||
import { errorWithFile } from '../util';
|
||||
import { errorWithFile, fileIsModule } from '../util';
|
||||
import { setCurrentConfig } from './globals';
|
||||
import { FullConfigInternal } from './config';
|
||||
import { addToCompilationCache } from '../transform/compilationCache';
|
||||
import { initializeEsmLoader } from './esmLoaderHost';
|
||||
import { initializeEsmLoader, registerESMLoader } from './esmLoaderHost';
|
||||
import { execArgvWithExperimentalLoaderOptions, execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
||||
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
|
||||
export const defineConfig = (...configs: any[]) => {
|
||||
@ -339,3 +340,55 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null
|
||||
return configFile!;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfigFromFile(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> {
|
||||
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
|
||||
if (restartWithExperimentalTsEsm(resolvedConfigFile))
|
||||
return null;
|
||||
const configLoader = new ConfigLoader(overrides);
|
||||
let config: FullConfigInternal;
|
||||
if (resolvedConfigFile)
|
||||
config = await configLoader.loadConfigFile(resolvedConfigFile, ignoreDeps);
|
||||
else
|
||||
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
|
||||
return config;
|
||||
}
|
||||
|
||||
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)
|
||||
return false;
|
||||
if (!configFile)
|
||||
return false;
|
||||
if (process.env.PW_DISABLE_TS_ESM)
|
||||
return false;
|
||||
// Node.js < 20
|
||||
if ((globalThis as any).__esmLoaderPortPreV20) {
|
||||
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
|
||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||
return false;
|
||||
}
|
||||
if (!fileIsModule(configFile))
|
||||
return false;
|
||||
// Node.js < 20
|
||||
if (!require('node:module').register) {
|
||||
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('../../cli'), process.argv.slice(2), {
|
||||
env: {
|
||||
...process.env,
|
||||
PW_TS_ESM_LEGACY_LOADER_ON: '1',
|
||||
},
|
||||
execArgv: execArgvWithExperimentalLoaderOptions(),
|
||||
});
|
||||
|
||||
innerProcess.on('close', (code: number | null) => {
|
||||
if (code !== 0 && code !== null)
|
||||
gracefullyProcessExitDoNotHang(code);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Nodejs >= 21
|
||||
registerESMLoader();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -21,21 +21,20 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Runner } from './runner/runner';
|
||||
import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||
import { fileIsModule, serializeError } from './util';
|
||||
import { serializeError } from './util';
|
||||
import { showHTMLReport } from './reporters/html';
|
||||
import { createMergedReport } from './reporters/merge';
|
||||
import { ConfigLoader, resolveConfigFile } from './common/configLoader';
|
||||
import { ConfigLoader, loadConfigFromFile } from './common/configLoader';
|
||||
import type { ConfigCLIOverrides } from './common/ipc';
|
||||
import type { FullResult, TestError } from '../types/testReporter';
|
||||
import type { TraceMode } from '../types/test';
|
||||
import type { FullConfig, TraceMode } from '../types/test';
|
||||
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
|
||||
import type { FullConfigInternal } from './common/config';
|
||||
import { program } from 'playwright-core/lib/cli/program';
|
||||
export { program } from 'playwright-core/lib/cli/program';
|
||||
import type { ReporterDescription } from '../types/test';
|
||||
import { prepareErrorStack } from './reporters/base';
|
||||
import { registerESMLoader } from './common/esmLoaderHost';
|
||||
import { execArgvWithExperimentalLoaderOptions, execArgvWithoutExperimentalLoaderOptions } from './transform/esmUtils';
|
||||
import { affectedTestFiles, cacheDir } from './transform/compilationCache';
|
||||
|
||||
function addTestCommand(program: Command) {
|
||||
const command = program.command('test [test-filter...]');
|
||||
@ -76,6 +75,54 @@ function addListFilesCommand(program: Command) {
|
||||
});
|
||||
}
|
||||
|
||||
let clearCacheCommandOverride: (opts: any) => Promise<void>;
|
||||
export function setClearCacheCommandOverride(body: (opts: any) => Promise<void>) {
|
||||
clearCacheCommandOverride = body;
|
||||
}
|
||||
|
||||
function addClearCacheCommand(program: Command) {
|
||||
const command = program.command('clear-cache');
|
||||
command.description('clears build and test caches');
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.action(async opts => {
|
||||
if (clearCacheCommandOverride)
|
||||
return clearCacheCommandOverride(opts);
|
||||
await removeFolder(cacheDir);
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeFolder(folder: string) {
|
||||
try {
|
||||
if (!fs.existsSync(folder))
|
||||
return;
|
||||
console.log(`Removing ${await fs.promises.realpath(folder)}`);
|
||||
await fs.promises.rm(folder, { recursive: true, force: true });
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
let findRelatedTestsCommandOverride: (files: string[], opts: any) => Promise<void>;
|
||||
export function setFindRelatedTestsCommandOverride(body: (files: string[], opts: any) => Promise<void>) {
|
||||
findRelatedTestsCommandOverride = body;
|
||||
}
|
||||
|
||||
function addFindRelatedTestsCommand(program: Command) {
|
||||
const command = program.command('find-related-tests [source-files...]');
|
||||
command.description('Returns the list of related tests to the given files');
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.action(async (files, options) => {
|
||||
if (findRelatedTestsCommandOverride)
|
||||
return findRelatedTestsCommandOverride(files, options);
|
||||
await withRunnerAndMutedWrite(options.config, async runner => {
|
||||
const result = await runner.loadAllTests();
|
||||
if (result.status !== 'passed' || !result.suite)
|
||||
return { errors: result.errors };
|
||||
const resolvedFiles = (files as string[]).map(file => path.resolve(process.cwd(), file));
|
||||
return { relatedTests: affectedTestFiles(resolvedFiles) };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addShowReportCommand(program: Command) {
|
||||
const command = program.command('show-report [report]');
|
||||
command.description('show HTML report');
|
||||
@ -115,21 +162,10 @@ Examples:
|
||||
|
||||
async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
await startProfiling();
|
||||
|
||||
// When no --config option is passed, let's look for the config file in the current directory.
|
||||
const configFileOrDirectory = opts.config ? path.resolve(process.cwd(), opts.config) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
|
||||
if (restartWithExperimentalTsEsm(resolvedConfigFile))
|
||||
const config = await loadConfigFromFile(opts.config, overridesFromOptions(opts), opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
const overrides = overridesFromOptions(opts);
|
||||
const configLoader = new ConfigLoader(overrides);
|
||||
let config: FullConfigInternal;
|
||||
if (resolvedConfigFile)
|
||||
config = await configLoader.loadConfigFile(resolvedConfigFile, opts.deps === false);
|
||||
else
|
||||
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
|
||||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
@ -151,47 +187,44 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
}
|
||||
|
||||
async function listTestFiles(opts: { [key: string]: any }) {
|
||||
export async function withRunnerAndMutedWrite(configFile: string | undefined, callback: (runner: Runner, config: FullConfig, configDir: string) => Promise<any>) {
|
||||
// Redefine process.stdout.write in case config decides to pollute stdio.
|
||||
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (() => {}) as any;
|
||||
process.stderr.write = (() => {}) as any;
|
||||
const configFileOrDirectory = opts.config ? path.resolve(process.cwd(), opts.config) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory)!;
|
||||
if (restartWithExperimentalTsEsm(resolvedConfigFile))
|
||||
return;
|
||||
|
||||
process.stdout.write = ((a: any, b: any, c: any) => process.stderr.write(a, b, c)) as any;
|
||||
try {
|
||||
const configLoader = new ConfigLoader();
|
||||
const config = await configLoader.loadConfigFile(resolvedConfigFile);
|
||||
const config = await loadConfigFromFile(configFile);
|
||||
if (!config)
|
||||
return;
|
||||
const runner = new Runner(config);
|
||||
const report = await runner.listTestFiles(opts.project);
|
||||
stdoutWrite(JSON.stringify(report), () => {
|
||||
const result = await callback(runner, config.config, config.configDir);
|
||||
stdoutWrite(JSON.stringify(result, undefined, 2), () => {
|
||||
gracefullyProcessExitDoNotHang(0);
|
||||
});
|
||||
} catch (e) {
|
||||
const error: TestError = serializeError(e);
|
||||
error.location = prepareErrorStack(e.stack).location;
|
||||
stdoutWrite(JSON.stringify({ error }), () => {
|
||||
stdoutWrite(JSON.stringify({ error }, undefined, 2), () => {
|
||||
gracefullyProcessExitDoNotHang(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function listTestFiles(opts: { [key: string]: any }) {
|
||||
await withRunnerAndMutedWrite(opts.config, async runner => runner.listTestFiles(opts.project));
|
||||
}
|
||||
|
||||
async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) {
|
||||
let configFile = opts.config;
|
||||
const configFile = opts.config;
|
||||
let config: FullConfigInternal | null;
|
||||
if (configFile) {
|
||||
configFile = path.resolve(process.cwd(), configFile);
|
||||
if (!fs.existsSync(configFile))
|
||||
throw new Error(`${configFile} does not exist`);
|
||||
if (!fs.statSync(configFile).isFile())
|
||||
throw new Error(`${configFile} is not a file`);
|
||||
config = await loadConfigFromFile(configFile);
|
||||
} else {
|
||||
const configLoader = new ConfigLoader();
|
||||
config = await configLoader.loadEmptyConfig(process.cwd());
|
||||
}
|
||||
if (restartWithExperimentalTsEsm(configFile))
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
const configLoader = new ConfigLoader();
|
||||
const config = await (configFile ? configLoader.loadConfigFile(configFile) : configLoader.loadEmptyConfig(process.cwd()));
|
||||
const dir = path.resolve(process.cwd(), reportDir || '');
|
||||
const dirStat = await fs.promises.stat(dir).catch(e => null);
|
||||
if (!dirStat)
|
||||
@ -272,44 +305,6 @@ function resolveReporter(id: string) {
|
||||
return require.resolve(id, { paths: [process.cwd()] });
|
||||
}
|
||||
|
||||
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)
|
||||
return false;
|
||||
if (!configFile)
|
||||
return false;
|
||||
if (process.env.PW_DISABLE_TS_ESM)
|
||||
return false;
|
||||
// Node.js < 20
|
||||
if ((globalThis as any).__esmLoaderPortPreV20) {
|
||||
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
|
||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||
return false;
|
||||
}
|
||||
if (!fileIsModule(configFile))
|
||||
return false;
|
||||
// Node.js < 20
|
||||
if (!require('node:module').register) {
|
||||
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('../cli'), process.argv.slice(2), {
|
||||
env: {
|
||||
...process.env,
|
||||
PW_TS_ESM_LEGACY_LOADER_ON: '1',
|
||||
},
|
||||
execArgv: execArgvWithExperimentalLoaderOptions(),
|
||||
});
|
||||
|
||||
innerProcess.on('close', (code: number | null) => {
|
||||
if (code !== 0 && code !== null)
|
||||
gracefullyProcessExitDoNotHang(code);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Nodejs >= 21
|
||||
registerESMLoader();
|
||||
return false;
|
||||
}
|
||||
|
||||
const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure'];
|
||||
|
||||
const testOptions: [string, string][] = [
|
||||
@ -349,3 +344,5 @@ addTestCommand(program);
|
||||
addShowReportCommand(program);
|
||||
addListFilesCommand(program);
|
||||
addMergeReportsCommand(program);
|
||||
addClearCacheCommand(program);
|
||||
addFindRelatedTestsCommand(program);
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import type { FullResult } from '../../types/testReporter';
|
||||
import type { FullResult, TestError } from '../../types/testReporter';
|
||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||
import { collectFilesForProject, filterProjects } from './projectUtils';
|
||||
import { createReporters } from './reporters';
|
||||
@ -27,6 +27,8 @@ import { runWatchModeLoop } from './watchMode';
|
||||
import { runUIMode } from './uiMode';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import type { Suite } from '../common/test';
|
||||
import { wrapReporterAsV2 } from '../reporters/reporterV2';
|
||||
|
||||
type ProjectConfigWithFiles = {
|
||||
name: string;
|
||||
@ -104,10 +106,15 @@ export class Runner {
|
||||
return status;
|
||||
}
|
||||
|
||||
async loadAllTests(outOfProcess?: boolean): Promise<FullResult['status']> {
|
||||
async loadAllTests(): Promise<{ status: FullResult['status'], suite?: Suite, errors: TestError[] }> {
|
||||
const config = this._config;
|
||||
const reporter = new InternalReporter(new Multiplexer([]));
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, outOfProcess ? 'out-of-process' : 'in-process', { failOnLoadErrors: true });
|
||||
const errors: TestError[] = [];
|
||||
const reporter = new InternalReporter(new Multiplexer([wrapReporterAsV2({
|
||||
onError(error: TestError) {
|
||||
errors.push(error);
|
||||
}
|
||||
})]));
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true });
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
|
||||
@ -119,7 +126,7 @@ export class Runner {
|
||||
if (modifiedResult && modifiedResult.status)
|
||||
status = modifiedResult.status;
|
||||
await reporter.onExit();
|
||||
return status;
|
||||
return { status, suite: testRun.rootSuite, errors };
|
||||
}
|
||||
|
||||
async watchAllTests(): Promise<FullResult['status']> {
|
||||
|
||||
@ -200,7 +200,8 @@ export function fileDependenciesForTest() {
|
||||
}
|
||||
|
||||
export function collectAffectedTestFiles(dependency: string, testFileCollector: Set<string>) {
|
||||
testFileCollector.add(dependency);
|
||||
if (fileDependencies.has(dependency))
|
||||
testFileCollector.add(dependency);
|
||||
for (const [testFile, deps] of fileDependencies) {
|
||||
if (deps.has(dependency))
|
||||
testFileCollector.add(testFile);
|
||||
@ -211,6 +212,13 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
|
||||
}
|
||||
}
|
||||
|
||||
export function affectedTestFiles(changes: string[]): string[] {
|
||||
const result = new Set<string>();
|
||||
for (const change of changes)
|
||||
collectAffectedTestFiles(change, result);
|
||||
return [...result];
|
||||
}
|
||||
|
||||
export function internalDependenciesForTestFile(filename: string): Set<string> | undefined{
|
||||
return fileDependencies.get(filename);
|
||||
}
|
||||
|
||||
@ -96,6 +96,8 @@ export class TestChildProcess {
|
||||
params: TestChildParams;
|
||||
process: ChildProcess;
|
||||
output = '';
|
||||
stdout = '';
|
||||
stderr = '';
|
||||
fullOutput = '';
|
||||
onOutput?: (chunk: string | Buffer) => void;
|
||||
exited: Promise<{ exitCode: number | null, signal: string | null }>;
|
||||
@ -121,8 +123,12 @@ export class TestChildProcess {
|
||||
process.stdout.write(`\n\nLaunching ${params.command.join(' ')}\n`);
|
||||
this.onOutput = params.onOutput;
|
||||
|
||||
const appendChunk = (chunk: string | Buffer) => {
|
||||
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
|
||||
@ -133,8 +139,8 @@ export class TestChildProcess {
|
||||
this._outputCallbacks.clear();
|
||||
};
|
||||
|
||||
this.process.stderr!.on('data', appendChunk);
|
||||
this.process.stdout!.on('data', appendChunk);
|
||||
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);
|
||||
|
||||
88
tests/playwright-test/find-related-tests.spec.ts
Normal file
88
tests/playwright-test/find-related-tests.spec.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 { test, expect } from './playwright-test-fixtures';
|
||||
import path from 'path';
|
||||
|
||||
export const ctReactCliEntrypoint = path.join(__dirname, '../../packages/playwright-ct-react/cli.js');
|
||||
|
||||
test('should list related tests', async ({ runCLICommand }) => {
|
||||
const result = await runCLICommand({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
export default defineConfig({});
|
||||
`,
|
||||
'helper.ts': `
|
||||
export const value = 42;
|
||||
`,
|
||||
'helper2.ts': `
|
||||
export { value } from './helper';
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
import { value } from './helper2';
|
||||
if (value) {}
|
||||
test('', () => {});
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
import { value } from './helper';
|
||||
if (value) {}
|
||||
test('', () => {});
|
||||
`,
|
||||
}, 'find-related-tests', ['helper.ts']);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stderr).toBeFalsy();
|
||||
const data = JSON.parse(result.stdout);
|
||||
expect(data).toEqual({
|
||||
relatedTests: [
|
||||
expect.stringContaining('a.spec.ts'),
|
||||
expect.stringContaining('b.spec.ts'),
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('should list related tests for ct', async ({ runCLICommand }) => {
|
||||
const result = await runCLICommand({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||
export default defineConfig({});
|
||||
`,
|
||||
'playwright/index.html': `<script type="module" src="./index.js"></script>`,
|
||||
'playwright/index.js': ``,
|
||||
'helper.tsx': `
|
||||
export const HelperButton = () => <button>Click me</button>;
|
||||
`,
|
||||
'button.tsx': `
|
||||
import { HelperButton } from './helper';
|
||||
export const Button = () => <HelperButton>Click me</HelperButton>;
|
||||
`,
|
||||
'button.spec.tsx': `
|
||||
import { test } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './button';
|
||||
test('foo', async ({ mount }) => {
|
||||
await mount(<Button />);
|
||||
});
|
||||
`,
|
||||
}, 'find-related-tests', ['helper.tsx'], ctReactCliEntrypoint);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const data = JSON.parse(result.stdout);
|
||||
expect(data).toEqual({
|
||||
relatedTests: [
|
||||
expect.stringContaining('button.spec.tsx'),
|
||||
]
|
||||
});
|
||||
});
|
||||
@ -16,16 +16,16 @@
|
||||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
|
||||
test('should list files', async ({ runListFiles }) => {
|
||||
const result = await runListFiles({
|
||||
test('should list files', async ({ runCLICommand }) => {
|
||||
const result = await runCLICommand({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [{ name: 'foo' }, { name: 'bar' }] };
|
||||
`,
|
||||
'a.test.js': ``
|
||||
});
|
||||
}, 'list-files');
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const data = JSON.parse(result.output);
|
||||
const data = JSON.parse(result.stdout);
|
||||
expect(data).toEqual({
|
||||
projects: [
|
||||
{
|
||||
@ -48,18 +48,18 @@ test('should list files', async ({ runListFiles }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should include testIdAttribute', async ({ runListFiles }) => {
|
||||
const result = await runListFiles({
|
||||
test('should include testIdAttribute', async ({ runCLICommand }) => {
|
||||
const result = await runCLICommand({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
use: { testIdAttribute: 'myid' }
|
||||
};
|
||||
`,
|
||||
'a.test.js': ``
|
||||
});
|
||||
}, 'list-files');
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const data = JSON.parse(result.output);
|
||||
const data = JSON.parse(result.stdout);
|
||||
expect(data).toEqual({
|
||||
projects: [
|
||||
{
|
||||
@ -76,17 +76,17 @@ test('should include testIdAttribute', async ({ runListFiles }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should report error', async ({ runListFiles }) => {
|
||||
const result = await runListFiles({
|
||||
test('should report error', async ({ runCLICommand }) => {
|
||||
const result = await runCLICommand({
|
||||
'playwright.config.ts': `
|
||||
const a = 1;
|
||||
a = 2;
|
||||
`,
|
||||
'a.test.js': ``
|
||||
});
|
||||
}, 'list-files');
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const data = JSON.parse(result.output);
|
||||
const data = JSON.parse(result.stdout);
|
||||
expect(data).toEqual({
|
||||
error: {
|
||||
location: {
|
||||
|
||||
@ -37,6 +37,8 @@ type CliRunResult = {
|
||||
export type RunResult = {
|
||||
exitCode: number,
|
||||
output: string,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
outputLines: string[],
|
||||
rawOutput: string,
|
||||
passed: number,
|
||||
@ -185,19 +187,21 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
|
||||
...parsed,
|
||||
exitCode,
|
||||
rawOutput: output,
|
||||
stdout: testProcess.stdout,
|
||||
stderr: testProcess.stderr,
|
||||
report,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPlaywrightListFiles(childProcess: CommonFixtures['childProcess'], baseDir: string, env: NodeJS.ProcessEnv): Promise<{ output: string, exitCode: number }> {
|
||||
async function runPlaywrightCLI(childProcess: CommonFixtures['childProcess'], args: string[], baseDir: string, env: NodeJS.ProcessEnv, entryPoint?: string): Promise<{ output: string, stdout: string, stderr: string, exitCode: number }> {
|
||||
const testProcess = childProcess({
|
||||
command: ['node', cliEntrypoint, 'list-files'],
|
||||
command: ['node', entryPoint || cliEntrypoint, ...args],
|
||||
env: cleanEnv(env),
|
||||
cwd: baseDir,
|
||||
});
|
||||
const { exitCode } = await testProcess.exited;
|
||||
return { exitCode, output: testProcess.output };
|
||||
return { exitCode, output: testProcess.output, stdout: testProcess.stdout, stderr: testProcess.stderr };
|
||||
}
|
||||
|
||||
export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
@ -235,7 +239,7 @@ type Fixtures = {
|
||||
writeFiles: (files: Files) => Promise<string>;
|
||||
deleteFile: (file: string) => Promise<void>;
|
||||
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
|
||||
runListFiles: (files: Files) => Promise<{ output: string, exitCode: number }>;
|
||||
runCLICommand: (files: Files, command: string, args?: string[], entryPoint?: string) => Promise<{ stdout: string, stderr: string, exitCode: number }>;
|
||||
runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
interactWithTestRunner: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
runTSC: (files: Files) => Promise<TSCResult>;
|
||||
@ -268,11 +272,11 @@ export const test = base
|
||||
await removeFolders([cacheDir]);
|
||||
},
|
||||
|
||||
runListFiles: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
runCLICommand: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
|
||||
await use(async (files: Files) => {
|
||||
await use(async (files: Files, command: string, args?: string[], entryPoint?: string) => {
|
||||
const baseDir = await writeFiles(testInfo, files, true);
|
||||
return await runPlaywrightListFiles(childProcess, baseDir, { PWTEST_CACHE_DIR: cacheDir });
|
||||
return await runPlaywrightCLI(childProcess, [command, ...(args || [])], baseDir, { PWTEST_CACHE_DIR: cacheDir }, entryPoint);
|
||||
});
|
||||
await removeFolders([cacheDir]);
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user