playwright-flake-checker
Version:
A tool to check for flaky Playwright tests by running them multiple times and analyzing results.
248 lines • 11 kB
JavaScript
import { spawn } from 'child_process';
import { confirm, input } from '@inquirer/prompts';
import { select } from 'inquirer-select-pro';
import { projectScripts, sleep } from '../utils/common.js';
import { getConfig } from '../config/config.js';
import { log, styled } from '../utils/logger.js';
import { runCommand } from '../utils/command.js';
const { command: commandStyling, emphasis, url } = styled;
/**
* Tracks whether the server was started in detached mode.
*/
let detachedServer = false;
/**
* Checks if the server is running and prompts the user to start it if not.
* @param url - The URL of the server to check.
* @param timeoutMs - The maximum time to wait for the server to respond, in milliseconds.
* @returns {Promise<boolean>} - Returns true if the server is running or started successfully, false otherwise.
*/
export const checkServerStatus = async (route, timeoutMs = 10000) => {
const endTime = Date.now() + timeoutMs;
const POLL_INTERVAL_MS = 1000;
const isServerUp = async () => {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), POLL_INTERVAL_MS - 100);
const response = await fetch(route, { signal: controller.signal });
clearTimeout(id);
return response.ok;
}
catch {
return false;
}
};
async function poll() {
if (Date.now() >= endTime) {
log().warning(`${url(route)} did not respond within ${timeoutMs / 1000}s.\n`);
return false;
}
if (await isServerUp()) {
log().green(`Server at ${url(route)} is up!`);
return true;
}
await sleep(POLL_INTERVAL_MS);
return poll();
}
log().info(`\nChecking server status at ${url(route)}...\n`);
return poll();
};
/**
* Determines if the server is running in detached mode.
*
* @returns {boolean} True if a detached server was started, otherwise false.
*/
export const isDetachedServerRunning = () => detachedServer;
/**
* Sets the state of the detached server.
* @param state - State of the detached server.
* @returns {void}
*/
export const setDetachedServerState = (state) => {
detachedServer = state;
};
/**
* Starts or builds a server process using either custom commands or npm scripts.
*
* This function provides an interactive CLI to select how to start or build a server:
* - Using npm scripts from package.json
* - Using a custom command provided by the user
*
* The server can run in foreground or detached (background) mode.
*
* @param {boolean} detached - When true, runs the server process detached in the background.
* When false (default), runs server in foreground.
* @param {object} options - Configuration options for the server
* @param {boolean} options.buildOnly - When true, only builds the server without starting it
* @param {('custom'|'scripts'|undefined)} options.mode - Controls how server commands are collected:
* - `'custom'` - Skip script selection and request custom command
* - `'scripts'` - Force using package.json scripts
* - `undefined` - Ask user to choose between scripts or custom command
* @returns {Promise<boolean>} A Promise resolving to true if server was started/built successfully, false otherwise
*/
export const startServer = async (detached = false, options = {}) => {
detachedServer = detached;
const allCommands = [];
const { buildOnly, mode } = options;
let useProjectScripts = true;
if (mode === 'custom') {
useProjectScripts = false;
}
else if (mode === 'scripts') {
useProjectScripts = true;
}
else {
useProjectScripts = await confirm({
message: `Do you want to ${buildOnly ? 'build' : 'start'} it using a script(s) from your ${emphasis('package.json')}?`,
default: true
});
}
if (!useProjectScripts) {
const customCmd = await input({
message: 'Please enter the custom server command to run:',
required: true
});
allCommands.push(customCmd);
const command = allCommands.join(' && ');
if (!(await confirm({ message: `Run server command: ${commandStyling(command)}?`, default: true }))) {
log().yellow('Custom command cancelled. Returning to custom command input.');
return startServer(detached, { mode: 'custom' });
}
}
else {
const selectedScripts = await select({
confirmDelete: true,
message: `Select scripts to ${buildOnly ? 'build' : 'run'} your server:`,
options: async (text) => {
const scripts = projectScripts();
if (!text)
return scripts;
return scripts.filter((script) => script.name.toLowerCase().includes(text.toLowerCase()));
},
multiple: true,
});
if (!selectedScripts || selectedScripts.length === 0) {
log().yellow('No server scripts selected.');
if (!(await confirm({
message: `Continue without ${buildOnly ? 'building the server?' : 'starting a server script?'}`,
default: false
}))) {
return startServer(detached);
}
return false;
}
selectedScripts.forEach(script => allCommands.push(`npm run ${script}`));
const command = allCommands.join(' && ');
if (!(await confirm({ message: `Run server command(s): ${commandStyling(command)}?`, default: true }))) {
log().yellow('Server start cancelled by user.');
return startServer(detached, { mode: 'scripts' });
}
}
if (allCommands.length === 0) {
log().yellow('No server commands to run.');
return false;
}
const command = allCommands.join(' && ');
log().info(`\n${buildOnly ? 'Building' : 'Starting'} server with command: ${commandStyling(command)}`);
if (detachedServer) {
const child = spawn(command, [], {
detached: true, stdio: 'ignore', env: process.env, shell: true
});
child.unref();
return true;
}
try {
await runCommand(command, [], {}, buildOnly, false);
if (buildOnly)
log().green('\nServer built successfully.\n');
return true;
}
catch (error) {
if (error instanceof Error && error.message !== 'Command interrupted by user.') {
log().error('Failed to start server in foreground:', error.message);
if (await confirm({ message: 'Would you like to try a different command?', default: true })) {
return startServer(detached, { mode: useProjectScripts ? 'scripts' : 'custom' });
}
}
else {
log().error('Failed to start server in foreground with an unknown error');
}
return false;
}
};
/**
* Handles the server prompt logic based on whether the server is already running.
* @param isServerRunning - Indicates if the server is already running.
* @returns {Promise<boolean>} - Returns true if the server is running or started successfully, false otherwise.
*/
export const handleServerPrompt = async (isServerRunning) => {
const CONFIG = getConfig();
if (!isServerRunning) {
const buildOnly = await confirm({
message: 'Do you want to build the project without starting the server?',
default: false
});
if (buildOnly) {
await startServer(false, { buildOnly: true });
}
else {
const manuallyStartServer = await confirm({
message: 'The server doesn\'t appear to be running. Do you want to start it?',
default: true
});
if (manuallyStartServer) {
const urls = CONFIG.baseUrls && CONFIG.baseUrls.length > 0 ? CONFIG.baseUrls : CONFIG.baseUrl ? [CONFIG.baseUrl] : [];
if (CONFIG.startServersSeparately && urls.length > 1) {
for (const singleUrl of urls) {
if (await checkServerStatus(singleUrl, 3000))
continue;
if (await startServer(true)) {
log().info(`\nAttempting to start server for ${url(singleUrl)}. Waiting a few seconds for it to initialize...\n`);
await sleep(5000);
if (!(await checkServerStatus(singleUrl, 30000))) {
log().warning(`${url(singleUrl)} is still not responsive after auto-start. Check server logs.`);
}
else {
log().green(`Server at ${url(singleUrl)} seems to be running now.\n`);
}
}
else {
log().error('Server could not be started by the script.\n');
return false;
}
}
return true;
}
else if (await startServer(true)) {
log().info('\nAttempting to start server. Waiting a few seconds for it to initialize...\n');
await sleep(5000);
if (urls.length > 1) {
const failed = [];
for (const serverUrl of urls) {
if (!(await checkServerStatus(serverUrl, 30000))) {
failed.push(serverUrl);
}
}
if (failed.length > 0) {
log().warning(`${failed.map(u => url(u)).join(', ')} is still not responsive after auto-start. Check server logs.`);
}
else {
log().green(`Server${urls.length > 1 ? 's' : ''} at ${urls.map(u => url(u)).join(', ')} seem to be running now.\n`);
}
}
else if (urls[0] && !(await checkServerStatus(urls[0], 30000))) {
log().warning(`${url(urls[0])} is still not responsive after auto-start. Check server logs.`);
}
else if (urls[0]) {
log().green(`Server at ${url(urls[0])} seems to be running now.\n`);
}
return true;
}
log().error('Server could not be started by the script.\n');
}
log().warning('Please ensure your server is running or Playwright is configured to start it.\n');
return confirm({ message: 'Do you want to continue with the tests anyway?', default: false });
}
}
return true;
};
//# sourceMappingURL=server.js.map