cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
2,967 lines • 142 kB
JavaScript
'use strict';
var xvfb = require('./xvfb-BKRGwxlS.js');
var Debug = require('debug');
var commander = require('commander');
var path = require('path');
var cli = require('./cli-_xePNEPj.js');
var CRI = require('chrome-remote-interface');
var chalk = require('chalk');
var crypto = require('crypto');
require('os');
require('bluebird');
require('@cypress/xvfb');
require('common-tags');
require('lodash');
require('assert');
require('arch');
require('ospath');
require('hasha');
require('tty');
require('ci-info');
require('execa');
require('systeminformation');
require('cachedir');
require('log-symbols');
require('executable');
require('process');
require('supports-color');
require('is-installed-globally');
require('fs-extra');
require('fs');
require('untildify');
require('cli-table3');
require('dayjs');
require('dayjs/plugin/relativeTime');
require('./spawn-BkpC5qQ4.js');
require('child_process');
require('listr2');
require('readline');
require('stream');
require('string_decoder');
require('node:string_decoder');
require('timers/promises');
require('fs/promises');
require('@cypress/request');
require('request-progress');
require('proxy-from-env');
require('yauzl');
require('util');
require('pretty-bytes');
const matchesProject = (record, projectRoot) => {
return path.resolve(record.projectRoot) === path.resolve(projectRoot);
};
// An undefined pid does not constrain, so an absent filter lists
// every pid.
const matchesSession = (record, session) => {
return session === undefined || record.pid === session;
};
// A dead pid is skipped without a probe (it proves the writer is gone); the
// survivors carry the live browser CDP state from their probe response.
const probeMatches = (matches, probeTimeoutMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const probed = yield Promise.all(matches.map((record) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
return cli.isPidAlive(record.pid) ? cli.verifySessionRecord(record, probeTimeoutMs) : null;
})));
return probed.filter((session) => session !== null);
});
const listLiveSessions = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
const records = yield cli.readLiveSessions();
const matches = records.filter((record) => matchesSession(record, options.session));
return probeMatches(matches, options.probeTimeoutMs);
});
let lastResolvedIdentity = null;
// Read rather than threaded through every caller: each tap command resolves its
// own session, several of them below this module.
const resolvedSessionIdentity = () => lastResolvedIdentity;
const lowestPid = (sessions) => {
return [...sessions].sort((a, b) => a.pid - b.pid)[0];
};
const selectSession = (candidates, options) => {
if (candidates.length === 1) {
const filtered = options.session !== undefined;
return { session: candidates[0], reason: filtered ? 'explicit' : 'only' };
}
const cwdMatches = candidates.filter((record) => matchesProject(record, options.cwd));
if (cwdMatches.length > 0) {
return { session: lowestPid(cwdMatches), reason: 'cwd-match' };
}
return { session: lowestPid(candidates), reason: 'arbitrary' };
};
// Reads, filters by pid, and probes for liveness. Throws when nothing matches —
// SESSION_NOT_FOUND for a pid that named nothing, NO_SESSION when none was asked
// for — STALE_SESSION when matches exist but none responds, and
// UNSUPPORTED_BROWSER when every one that does has a browser tap cannot drive.
const liveMatches = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { session, probeTimeoutMs } = options;
const records = yield cli.readLiveSessions();
const matches = records.filter((record) => matchesSession(record, session));
if (matches.length === 0) {
throw session === undefined
? new cli.distExports.TapError('NO_SESSION')
: new cli.distExports.SessionNotFoundTapError(session);
}
const live = yield probeMatches(matches, probeTimeoutMs);
if (live.length === 0) {
throw new cli.distExports.TapError('STALE_SESSION');
}
// Dropped before selection so a session running an unsupported browser never
// shadows one that can serve the command; when it is the only candidate the
// caller hears why rather than "no browser attached".
const supported = live.filter((record) => cli.distExports.isTapSupportedBrowser(record.browserFamily));
if (supported.length === 0) {
throw new cli.distExports.TapError('UNSUPPORTED_BROWSER');
}
return supported;
});
// Resolves a live session without requiring a browser; `status` reports
// sessions that have no browser attached yet.
const resolveLiveSession = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const live = yield liveMatches(options);
const { session, reason } = selectSession(live, options);
lastResolvedIdentity = { sessionId: session.sessionId, machineId: session.machineId, userId: session.userId };
return { session, reason, candidateCount: live.length };
});
// Adds the browser-readiness requirement to resolveLiveSession: the session
// it returns is guaranteed to have a browser attached. Gate on the browser
// before selecting so a browserless session never shadows a ready one that
// could serve the command.
const resolveSession = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const live = yield liveMatches(options);
const ready = live.filter((record) => record.cdpBrowserWsUrl !== null);
if (ready.length === 0) {
throw new cli.distExports.TapError('NO_BROWSER_ATTACHED');
}
const { session: selected, reason } = selectSession(ready, options);
lastResolvedIdentity = { sessionId: selected.sessionId, machineId: selected.machineId, userId: selected.userId };
return { session: selected, reason, candidateCount: ready.length };
});
/** Bound for a protocol call, including one awaiting app-side work. */
const DEFAULT_CDP_TIMEOUT_MS = 30000;
/**
* Bound for the calls that locate the runner page. A healthy renderer answers
* these in milliseconds, so keeping them short is what lets the scan skip an
* unresponsive target rather than stop on it.
*/
const FIND_SESSION_TIMEOUT_MS = 2000;
// Which call went unanswered is a protocol detail, so it stays on the diagnostic;
// how long we waited is the part the user can act on with `--timeout`.
const unresponsive = (what, ms) => {
return new cli.distExports.TapError('RENDERER_UNRESPONSIVE', {
detail: `No response within the specified timeout (${ms}ms).`,
message: `No reply to ${what} within ${ms}ms.`,
});
};
const isRendererUnresponsive = (err) => {
return cli.distExports.isTapError(err) && err.code === 'RENDERER_UNRESPONSIVE';
};
/**
* A pending CDP reply has no timer of its own, and the only thing that settles
* one other than a matching reply is the browser-level socket closing — so a
* target that stops answering leaves the promise orphaned forever. Stop waiting
* on our side; the orphan settles when the connection closes its client.
*/
const withCdpDeadline = (work, what, ms) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let timer;
try {
return yield Promise.race([
work,
new Promise((_resolve, reject) => {
timer = setTimeout(() => reject(unresponsive(what, ms)), ms);
}),
]);
}
finally {
clearTimeout(timer);
}
});
/**
* Every domain shorthand (`client.Runtime.evaluate(...)`) is generated as a call
* to `client.send`, so replacing that one method bounds every protocol call the
* connection makes, including the raw-client ones the frame extractors issue. Event
* subscriptions and `close` don't go through it and stay unbounded.
*/
const boundCdpCalls = (client, ms) => {
const send = client.send.bind(client);
client.send = ((method, ...rest) => {
return withCdpDeadline(send(method, ...rest), method, ms);
});
};
const debug$5 = Debug('cypress:cli:tap');
// Chrome reports these CDP failures under the generic -32000 "server error"
// protocol code, so the exact message text is the only way to recognize them.
const CdpErrorMessage = {
objectNotFound: 'Could not find object with given id',
contextNotFound: 'Cannot find context with specified id',
contextDestroyed: 'Execution context was destroyed',
targetGone: 'Inspected target navigated or closed',
sessionNotFound: 'Session with given id not found',
};
const staleObjectMessages = [CdpErrorMessage.objectNotFound, CdpErrorMessage.contextNotFound, CdpErrorMessage.contextDestroyed];
const sessionGoneMessages = [CdpErrorMessage.targetGone, CdpErrorMessage.sessionNotFound];
const matchesAnyMessage = (err, messages) => {
return err instanceof Error && messages.some((message) => err.message.includes(message));
};
/**
* Raises a transport or protocol failure under the code its copy is registered
* against. `message` is the diagnostic — the call that failed, the version that
* disagreed — which reaches the logs rather than the rendered output.
*
* An unresponsive renderer is reported as itself rather than as whatever call
* happened to time out: it is the condition worth waiting longer on.
*/
const throwTapError = (code, message, cause) => {
if (isRendererUnresponsive(cause)) {
throw cause;
}
throw new cli.distExports.TapError(code, { message, cause });
};
// The code is the whole of a failure envelope; `detail` is optional, so a payload
// carrying only a code is well-formed.
const isFailureError = (error) => {
return !!error && typeof error === 'object' && typeof error.code === 'string';
};
const validateExecResult = (value) => {
const outcome = value;
const fail = () => throwTapError('PROTOCOL_MISMATCH', `${cli.distExports.TAP_EXEC_METHOD} returned an unrecognizable result.`);
if (!outcome || typeof outcome !== 'object')
return fail();
if ('error' in outcome)
return isFailureError(outcome.error) ? outcome : fail();
if ('result' in outcome)
return outcome;
return fail();
};
const isStaleHandleError = (err) => {
return matchesAnyMessage(err, staleObjectMessages);
};
const isSessionGoneError = (err) => {
if (!(err instanceof Error)) {
return false;
}
return matchesAnyMessage(err, sessionGoneMessages) || matchesAnyMessage(err.cause, sessionGoneMessages);
};
const connectToBrowser = (wsUrl) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
return yield CRI({ target: wsUrl });
}
catch (err) {
return throwTapError('CDP_UNREACHABLE', `Could not open a debugging connection to the browser: ${err.message}`, err);
}
});
const listTargets = (client) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
return yield client.Target.getTargets();
}
catch (err) {
return throwTapError('CDP_UNREACHABLE', `Listing the browser's targets failed: ${err.message}`, err);
}
});
const attachToPage = (client, targetId) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
const { sessionId } = yield client.Target.attachToTarget({ targetId, flatten: true });
return sessionId;
}
catch (err) {
return throwTapError('CDP_UNREACHABLE', `Could not attach to the Cypress runner page: ${err.message}`, err);
}
});
const evaluateBinding = (client, sessionId) => {
return client.Runtime.evaluate({ expression: `window.${cli.distExports.TAP_BINDING_GLOBAL}` }, sessionId);
};
const probeForBinding = (client, sessionId, findSessionMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { result, exceptionDetails } = yield withCdpDeadline(evaluateBinding(client, sessionId), 'the runner-page probe', findSessionMs);
return !exceptionDetails && result.type !== 'undefined' && !!result.objectId;
});
const findRunnerPageSession = (client, targetInfos, findSessionMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let unresponsive;
for (const target of targetInfos) {
if (target.type !== 'page') {
continue;
}
let sessionId;
try {
sessionId = yield attachToPage(client, target.targetId);
if (yield probeForBinding(client, sessionId, findSessionMs)) {
debug$5('matched runner page target %o', { targetId: target.targetId, url: target.url });
return sessionId;
}
}
catch (err) {
if (isRendererUnresponsive(err)) {
unresponsive = err;
}
debug$5('probing target %s failed: %s', target.targetId, err.message);
}
if (sessionId) {
yield client.Target.detachFromTarget({ sessionId }).catch(() => { });
}
}
// A page that never answered the probe is a different failure from a browser
// holding no runner page at all, and only the former is worth waiting longer on.
if (unresponsive) {
throw unresponsive;
}
return throwTapError('BINDING_NOT_FOUND', `Failed to connect to the runner page.`);
});
const resolveBindingObjectId = (client, sessionId) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let evaluated;
try {
evaluated = yield evaluateBinding(client, sessionId);
}
catch (err) {
if (isStaleHandleError(err) || isSessionGoneError(err)) {
throw err;
}
return throwTapError('CDP_UNREACHABLE', `Evaluating the tap binding failed: ${err.message}`, err);
}
const { result, exceptionDetails } = evaluated;
if (exceptionDetails) {
return throwTapError('CDP_UNREACHABLE', `Failed to connect to the session.`);
}
if (result.type === 'undefined' || !result.objectId) {
return throwTapError('BINDING_NOT_FOUND', `Connected to an unsupported session.`);
}
return result.objectId;
});
const callBindingMethod = (client, sessionId, objectId, method, args) => {
return client.Runtime.callFunctionOn({
objectId,
functionDeclaration: `function (method, ...args) { return this[method](...args) }`,
arguments: [method, ...args].map((value) => ({ value })),
returnByValue: true,
awaitPromise: true,
}, sessionId);
};
const throwCdpError = (method, err) => {
return throwTapError('CDP_UNREACHABLE', `The CDP call for ${method} failed: ${err.message}`, err);
};
const callBindingWithRetry = (client, sessionId, method, args) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const attempt = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const objectId = yield resolveBindingObjectId(client, sessionId);
try {
return yield callBindingMethod(client, sessionId, objectId, method, args);
}
catch (err) {
if (isStaleHandleError(err)) {
throw err;
}
return throwCdpError(method, err);
}
});
try {
return yield attempt();
}
catch (err) {
if (!isStaleHandleError(err)) {
throw err;
}
debug$5('stale binding handle; re-acquiring and retrying once');
try {
return yield attempt();
}
catch (retryErr) {
if (isStaleHandleError(retryErr)) {
return throwTapError('STALE_HANDLE', retryErr.message, retryErr);
}
throw retryErr;
}
}
});
const withTapConnection = (session, fn, timeoutMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const callMs = timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : DEFAULT_CDP_TIMEOUT_MS;
const findSessionMs = timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : FIND_SESSION_TIMEOUT_MS;
debug$5('opening tap connection for session %o', { pid: session.pid, cdpBrowserWsUrl: session.cdpBrowserWsUrl, callMs, findSessionMs });
const client = yield connectToBrowser(session.cdpBrowserWsUrl);
boundCdpCalls(client, callMs);
try {
const attach = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { targetInfos } = yield listTargets(client);
return findRunnerPageSession(client, targetInfos, findSessionMs);
});
let sessionId = yield attach();
const call = (method_1, ...args_1) => xvfb.__awaiter(void 0, [method_1, ...args_1], void 0, function* (method, args = []) {
var _a;
let response;
try {
response = yield callBindingWithRetry(client, sessionId, method, args);
}
catch (err) {
if (!isSessionGoneError(err)) {
throw err;
}
debug$5('session gone (%s); re-attaching to the runner page', err.message);
sessionId = yield attach();
try {
response = yield callBindingWithRetry(client, sessionId, method, args);
}
catch (retryErr) {
if (isSessionGoneError(retryErr)) {
return throwTapError('STALE_HANDLE', retryErr.message, retryErr);
}
throw retryErr;
}
}
if (response === null || response === void 0 ? void 0 : response.exceptionDetails) {
return throwTapError('BINDING_THREW', `${method} threw: ${((_a = response.exceptionDetails.exception) === null || _a === void 0 ? void 0 : _a.description) || response.exceptionDetails.text}`);
}
return response.result.value;
});
const connection = {
call,
client,
get sessionId() {
return sessionId;
},
};
return yield fn(connection);
}
finally {
yield client.close().catch(() => { });
}
});
// The shared formatting vocabulary every tap command's human-readable renderer
// borrows from, so their output reads as one tool: the reporter's palette, its
// state icons, and the table / definition-list / heading / block primitives.
// A renderer composes these into `string[][]` blocks and hands them to `layout`.
// The reporter's own palette (packages/reporter/src/lib/variables.scss and
// commands.scss), so the CLI rendering matches the app: $pass/$fail map to
// jade-400/red-400, assert messages render jade-300/red-400 with jade-200/
// red-300 emphasis, the network dots use the command-message-indicator colors,
// and aliases take the purple badge hue. chalk downsamples the hex values on
// terminals without truecolor.
const color = {
pass: chalk.hex('#1fa971'), // $jade-400
fail: chalk.hex('#e45770'), // $red-400
passMessage: chalk.hex('#69d3a7'), // $jade-300
passStrong: chalk.hex('#a3e7cb'), // $jade-200
failStrong: chalk.hex('#f59aa9'), // $red-300
errHeaderText: chalk.hex('#f59aa9'), // $err-header-text = $red-300
aborted: chalk.hex('#db7903'), // $orange-400
warn: chalk.hex('#edbb4a'), // $warn-text = $orange-300
bad: chalk.hex('#c62b49'), // $red-500
pending: chalk.hex('#6470f3'), // $indigo-400
alias: chalk.hex('#c8a7f5'), // $purple-300
aliasDom: chalk.hex('#9aa2fc'), // $indigo-300 — the reporter colors dom aliases indigo
muted: chalk.hex('#9095ad'), // $gray-500
fadedId: chalk.hex('#5a5f7a'), // $gray-700 — event ids sit back from the command numbers
};
const stateBadge = {
passed: { icon: color.pass('✓'), word: color.pass('passed') },
failed: { icon: color.fail('✖'), word: color.fail('failed') },
pending: { icon: color.pending('○'), word: color.pending('pending') },
skipped: { icon: color.muted('-'), word: color.muted('skipped') },
};
// The app header renders a zero count as `--` (its stats strip's `count`
// helper); skipped has no strip slot there, so it only appears when non-zero.
const count = (num) => (num > 0 ? String(num) : '--');
// The reporter's per-outcome strip: `✓ 2 ✖ 1 ○ 1 - 3`. Shared by the reporter
// header and the status command's results line.
const countsLine = (counts) => {
return [
`${stateBadge.passed.icon} ${count(counts.passed)}`,
`${stateBadge.failed.icon} ${count(counts.failed)}`,
`${stateBadge.pending.icon} ${count(counts.pending)}`,
...(counts.skipped > 0 ? [`${stateBadge.skipped.icon} ${counts.skipped}`] : []),
].join(' ');
};
// A dim panel title, optionally carrying its row count: `ROUTES (2)`.
const heading = (title, itemCount) => {
return chalk.dim(itemCount === undefined ? title : `${title} (${itemCount})`);
};
// A bold title line led by a state/action icon, e.g. `✓ App > loads passed`.
const titleLine = (icon, text, suffix) => {
return `${icon} ${chalk.bold(text)}${suffix ? ` ${suffix}` : ''}`;
};
// The clock time a run started, trailing a spec title. Local time reads against
// the terminal's own clock; the ISO instant stays in --json for machines.
const startedAtLabel = (startedAt) => {
return color.muted(`(started at ${new Date(startedAt).toLocaleTimeString('en-US')})`);
};
// A dim note standing in for an absent panel, e.g. `No specs to run.`
const emptyState = (message) => chalk.dim(message);
// A selector, quoted so it pastes straight back into a shell as one argument.
// Single quotes throughout, since attribute selectors carry double ones
// (`[data-test="x"]`); a single quote in the value becomes `'\''`.
const quoted = (selector) => `'${selector.split('\'').join('\'\\\'\'')}'`;
const indent = (depth) => ' '.repeat(depth);
// The column count to lay a row out against, falling back to a readable width
// when the output is piped and the terminal reports none.
const terminalWidth = () => process.stdout.columns || 120;
// Keep a value on its own row: a soft-wrapped line breaks out of the column it
// was padded into, so anything longer than the room left for it ends in an
// ellipsis. Clamp before coloring, the way the tables pad before coloring.
const clamp = (text, width) => {
return text.length <= width ? text : `${text.slice(0, Math.max(1, width - 1))}…`;
};
// Pad before coloring: the escape codes chalk adds would otherwise count
// toward the column width. `colorize` styles the padded cells — it also gets the
// row index, since a padded cell no longer compares equal to the value it holds
// — and defaults to leaving them plain for the tables that don't tint a column.
const columns = (header, rows, colorize = (cells) => cells) => {
const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
const pad = (cells) => cells.map((cell, column) => cell.padEnd(widths[column]));
return [
pad(header).map((cell) => chalk.dim(cell)).join(' '),
...rows.map((row, index) => colorize(pad(row), index).join(' ')),
];
};
// Columns rendered as a standalone nested block.
const tableRows = (header, rows, colorize) => {
return columns(header, rows, colorize).map((line) => `${indent(1)}${line}`);
};
// A counted panel title with its content indented beneath it. Content rendered
// on its own — no title to sit under — keeps the left margin.
const panel = (title, count, lines) => {
return [heading(title, count), ...lines.map((line) => `${indent(1)}${line}`)];
};
// Columns under a counted panel title.
const table = (title, header, rows, colorize) => {
return panel(title, rows.length, columns(header, rows, colorize));
};
// Aligned `label value` rows. Values arrive already styled — callers color
// them before padding matters, since only the labels share a column width.
const definitionList = (entries) => {
const width = Math.max(0, ...entries.map(([label]) => label.length));
return entries.map(([label, value]) => ` ${label.padEnd(width)} ${value}`);
};
// Assemble rendered blocks into the final string: lines within a block on their
// own rows, blocks separated by a blank line, trailing whitespace trimmed.
const layout = (blocks) => {
return blocks.map((block) => block.map((line) => line.trimEnd()).join('\n')).join('\n\n');
};
// dom, aria, and inspect all answer an ambiguous selector the same way, so they
// render it the same way: what went wrong, then the matches. Either column
// re-runs the read — `--at <index>`, or the unique selector.
const renderAmbiguousHuman = (result) => {
const headline = color.warn(`⚠ selector ${chalk.bold(quoted(result.selector))} matched ${chalk.bold(result.count)} elements but must be unique`);
const note = color.warn(`provide ${chalk.bold('--at')} with an index to select an element from the list or update the selector.`);
// Number no further than the session derives selectors for: `*` on a real
// page matches thousands, and every row past the cap could only ever be a
// bare index. --at still reads any of them, so say what the list leaves out.
const numbered = Math.min(result.count, cli.distExports.MAX_DERIVED_SELECTORS);
// A match no unique selector could be derived for still keeps its row, since
// --at reads it either way.
const derived = new Map(result.selectors.flatMap(({ index, selector }) => (selector ? [[index, quoted(selector)]] : [])));
const rows = Array.from({ length: numbered }, (_, index) => { var _a; return [String(index), (_a = derived.get(index)) !== null && _a !== void 0 ? _a : '-']; });
const colorize = (cells, index) => (derived.has(index) ? cells : [cells[0], color.muted(cells[1])]);
const table = [headline, note, ...columns(['index', 'selector'], rows, colorize)];
// Only a null the session reported says a match has no derivable selector. A
// list that stops short — the lookup failed, or never reached the app under
// test — leaves its rows as dashes with nothing known about why, so there is
// nothing to send the reader back to their selector config over.
const underivable = result.selectors.some(({ selector }) => !selector);
const notes = [
...(numbered === result.count ? [] : [`showing the first ${numbered} of ${result.count} matches — --at takes any index up to ${result.count - 1}.`]),
...(underivable ? ['- means no unique selector could be derived for that match — you may need to adjust your Cypress.ElementSelector config, or the element may be one no standard CSS selector can identify.'] : []),
].map((line) => color.muted(line));
return layout(notes.length ? [table, notes] : [table]);
};
// The title of a hook section of the command log. The hook id in the title is
// the qualifier a duplicated row number needs (`pin r8 h1:1`), since numbers
// restart per section.
const sectionHeading = (hookName, hookId) => {
const qualifier = hookId ? ` · ${hookId}` : '';
return heading(`${(hookName !== null && hookName !== void 0 ? hookName : 'commands').toUpperCase()}${qualifier}`);
};
// The reporter's status dot for a network row.
const INDICATORS = {
successful: color.pass('●'),
pending: color.pending('○'),
aborted: color.aborted('●'),
bad: color.bad('●'),
};
// The reporter's tag palette: dom aliases indigo, everything else
// (route/agent/primitive) purple.
const aliasColor = (aliasType) => (aliasType === 'dom' ? color.aliasDom : color.alias);
// The reporter badges a `.as()` handle with the `@` you'd pass to `cy.get()`, and
// leaves route and agent aliases bare.
const aliasBadge = (alias, aliasType) => {
const handle = aliasType === 'route' || aliasType === 'agent' ? alias : `@${alias}`;
return aliasColor(aliasType)(handle);
};
// Driver messages emphasize with markdown-style `**`; render the emphasis
// instead of the markers, on one line.
const emphasize = (message, strong) => {
return message
.replace(/\s+/g, ' ')
.trim()
.replace(/\*\*([^*]+)\*\*/g, (_, part) => strong(part));
};
// The `@name`s a row references (cy.get('@x') / cy.wait('@x')) appear verbatim
// in its message — give them the alias badge color in place.
const colorizeAliasReferences = (message, command) => {
const { referencedAliases, aliasType } = command;
if (!(referencedAliases === null || referencedAliases === void 0 ? void 0 : referencedAliases.length)) {
return message;
}
const names = new Set(referencedAliases);
return message.replace(/@([\w-]+)/g, (match, name) => (names.has(name) ? aliasColor(aliasType)(match) : match));
};
// Asserts take the reporter's state colors — passing green, failing red —
// while other messages keep the default text with bold emphasis.
const formatMessage = (command) => {
var _a;
const message = (_a = command.message) !== null && _a !== void 0 ? _a : '';
if (!message) {
return '';
}
if (command.name === 'assert') {
if (command.state === 'passed') {
return color.passMessage(emphasize(message, (part) => color.passStrong.bold(part)));
}
if (command.state === 'failed') {
return color.fail(emphasize(message, (part) => color.failStrong.bold(part)));
}
}
return colorizeAliasReferences(emphasize(message, (part) => chalk.bold(part)), command);
};
// Child commands render dash-prefixed, the way the reporter marks a command
// chained off the previous subject.
const commandLabel = (command) => {
var _a;
return `${command.type === 'child' ? '-' : ''}${(_a = command.name) !== null && _a !== void 0 ? _a : ''}`;
};
const networkDot = (network) => {
return (network === null || network === void 0 ? void 0 : network.indicator) ? `${INDICATORS[network.indicator]} ` : '';
};
// A row's alias badge(s): its own aliases (`.as()` definitions, spy/stub call
// rows) or the alias its request matched.
const aliasSuffix = (command, network) => {
var _a, _b;
const badges = (_b = (_a = command.aliases) === null || _a === void 0 ? void 0 : _a.map((name) => aliasBadge(name, command.aliasType))) !== null && _b !== void 0 ? _b : ((network === null || network === void 0 ? void 0 : network.alias) != null ? [aliasBadge(network.alias, 'route')] : []);
return badges.length ? ` ${badges.join(' ')}` : '';
};
const networkSuffix = (network) => {
return (network === null || network === void 0 ? void 0 : network.stubbed) ? ` ${chalk.dim('(stubbed)')}` : '';
};
const cleanedSuffix = (command) => {
return command.cleanedUp ? ` ${chalk.dim('(cleaned up)')}` : '';
};
// The panel's status badge colors: red for a failed session, orange while one
// is being recreated, the reporter's jade otherwise.
const sessionStatus = (status) => {
if (status === undefined) {
return '';
}
if (status === 'failed') {
return color.fail(status);
}
if (status.startsWith('recreat')) {
return color.aborted(status);
}
return color.passMessage(status);
};
const sessionsPanel = (sessions) => {
return [
heading('SESSIONS', sessions.length),
...sessions.map((session) => {
const global = session.global ? ` ${chalk.dim('(global)')}` : '';
return ` ${session.name}${global} ${sessionStatus(session.status)}`;
}),
];
};
const agentsTable = (agents) => {
const rows = agents.map((agent) => {
var _a, _b, _c;
return [
(_a = agent.type) !== null && _a !== void 0 ? _a : '',
(_b = agent.functionName) !== null && _b !== void 0 ? _b : '',
((_c = agent.aliases) !== null && _c !== void 0 ? _c : []).join(', '),
agent.callCount ? String(agent.callCount) : '-',
];
});
return table('SPIES / STUBS', ['TYPE', 'FUNCTION', 'ALIAS(ES)', 'CALLS'], rows, (cells) => {
return [chalk.bold(cells[0]), cells[1], color.alias(cells[2]), cells[3]];
});
};
const routesTable = (routes) => {
const rows = routes.map((route) => {
var _a, _b, _c;
return [
(_a = route.method) !== null && _a !== void 0 ? _a : '',
(_b = route.url) !== null && _b !== void 0 ? _b : '',
route.stubbed ? 'yes' : 'no',
(_c = route.alias) !== null && _c !== void 0 ? _c : '',
route.numResponses ? String(route.numResponses) : '-',
];
});
return table('ROUTES', ['METHOD', 'MATCHER', 'STUBBED', 'ALIAS', '#'], rows, (cells) => {
return [
chalk.bold(cells[0]),
cells[1],
cells[2].startsWith('yes') ? color.aborted(cells[2]) : cells[2],
color.alias(cells[3]),
cells[4],
];
});
};
// Consecutive rows sharing a hookId form one section, preserving the true
// chronology of the log rather than re-bucketing it.
const sectionize = (commands) => {
const sections = [];
for (const command of commands) {
const current = sections[sections.length - 1];
if (current && current.hookId === command.hookId) {
current.rows.push(command);
}
else {
sections.push({ hookId: command.hookId, rows: [command] });
}
}
return sections;
};
const isEventRow = (command) => {
return command.event === true || command.type === 'system';
};
const stateSuffix = (command) => {
if (command.state === 'failed') {
return ` ${color.fail('✖')}`;
}
if (command.state === 'pending') {
return ` ${chalk.dim('…')}`;
}
return '';
};
const rowParts = (command) => {
var _a;
return {
groupIndent: ' '.repeat((_a = command.groupLevel) !== null && _a !== void 0 ? _a : 0),
dot: networkDot(command.network),
message: formatMessage(command),
cleaned: cleanedSuffix(command),
};
};
// Event logs render the way the reporter shows them: labeled by their display
// name, as an annotation of the surrounding command — but with their own tap id,
// so an xhr or uncaught-exception row is referenceable like any command. A
// failed event (an uncaught exception) takes the failure red, like the reporter.
const renderEventRow = (command, idWidth) => {
var _a, _b;
const { groupIndent, dot, message, cleaned } = rowParts(command);
const failed = command.state === 'failed';
const labelColor = failed ? color.fail : chalk.dim;
const label = labelColor(`(${(_b = (_a = command.displayName) !== null && _a !== void 0 ? _a : command.name) !== null && _b !== void 0 ? _b : '?'})`);
const id = color.fadedId(command.id.padStart(idWidth));
const text = failed ? color.fail(chalk.italic(message)) : chalk.italic(message);
return ` ${id} ${groupIndent} ${label} ${dot}${text}${aliasSuffix(command, command.network)}${networkSuffix(command.network)}${stateSuffix(command)}${cleaned}`;
};
const renderCommandRow = (command, idWidth, nameWidth) => {
const { groupIndent, dot, message, cleaned } = rowParts(command);
const id = chalk.dim(command.id.padStart(idWidth));
const name = commandLabel(command).padEnd(nameWidth);
const styledName = command.state === 'failed' ? color.fail.bold(name) : chalk.bold(name);
return ` ${id} ${groupIndent}${styledName} ${dot}${message}${aliasSuffix(command, command.network)}${networkSuffix(command.network)}${stateSuffix(command)}${cleaned}`;
};
const renderRow = (command, idWidth, nameWidth) => {
return isEventRow(command)
? renderEventRow(command, idWidth)
: renderCommandRow(command, idWidth, nameWidth);
};
const renderSection = (section, hookName, idWidth) => {
const commandRows = section.rows.filter((command) => !isEventRow(command));
const nameWidth = Math.max(0, ...commandRows.map((command) => commandLabel(command).length));
return [
sectionHeading(hookName, section.hookId),
...section.rows.map((command) => renderRow(command, idWidth, nameWidth)),
];
};
// One hook section on its own, for the surfaces that show a slice of the command
// log rather than a whole attempt (the pinned command). Ids align within the
// rows given, since there is no wider log to line up with.
const renderCommandSection = (rows, hookName) => {
var _a;
const idWidth = Math.max(2, ...rows.map((command) => command.id.length));
return renderSection({ hookId: (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.hookId, rows }, hookName !== null && hookName !== void 0 ? hookName : 'commands', idWidth);
};
// The reporter's error panel: name, message, and the code frame with its
// `>`-marked failing line.
const renderError = (error) => {
var _a;
const lines = [`${color.fail('✖')} ${color.fail.bold((_a = error.name) !== null && _a !== void 0 ? _a : 'Error')}`];
if (error.message) {
lines.push(...error.message.split('\n').map((line) => ` ${color.errHeaderText(line)}`));
}
const { codeFrame } = error;
if (codeFrame === null || codeFrame === void 0 ? void 0 : codeFrame.file) {
lines.push('', ` ${chalk.dim([codeFrame.file, codeFrame.line, codeFrame.column].filter((part) => part != null).join(':'))}`);
}
if (codeFrame === null || codeFrame === void 0 ? void 0 : codeFrame.frame) {
lines.push(...codeFrame.frame.replace(/\n+$/, '').split('\n').map((line) => {
return ` ${line.startsWith('>') ? color.fail(line) : color.muted(line)}`;
}));
}
return lines;
};
const renderReporterHuman = (view) => {
const { icon, word } = stateBadge[view.test.state];
const hookNames = new Map(view.hooks.map(({ hookId, hookName }) => [hookId, hookName]));
const sections = sectionize(view.commands);
const idWidth = Math.max(2, ...view.commands.map((command) => command.id.length));
const blocks = [
[titleLine(icon, view.test.fullTitle, word)],
...(view.sessions.length ? [sessionsPanel(view.sessions)] : []),
...(view.agents.length ? [agentsTable(view.agents)] : []),
...(view.routes.length ? [routesTable(view.routes)] : []),
...sections.map((section) => { var _a, _b; return renderSection(section, (_b = hookNames.get((_a = section.hookId) !== null && _a !== void 0 ? _a : '')) !== null && _b !== void 0 ? _b : 'commands', idWidth); }),
];
if (!view.commands.length) {
blocks.push([emptyState('No commands were logged for this test.')]);
}
if (view.error) {
blocks.push(renderError(view.error));
}
return layout(blocks);
};
// The reporter header's clock format (packages/reporter/src/lib/util.ts
// formatDuration, inlined — the CLI can't import the reporter bundle).
const formatDuration = (duration) => {
if (!duration) {
return '--';
}
if (duration < 1000) {
return `${duration}ms`;
}
const seconds = Math.round(duration / 1000);
const displaySeconds = String(seconds % 60).padStart(2, '0');
const displayMinutes = String(Math.floor((seconds / 60) % 60)).padStart(2, '0');
const displayHours = String(Math.floor(seconds / (60 * 60)));
return displayHours === '0' ? `${displayMinutes}:${displaySeconds}` : `${displayHours}:${displayMinutes}:${displaySeconds}`;
};
const statsLine = (stats) => {
return `${countsLine(stats)} ${chalk.dim(formatDuration(stats.duration))}`;
};
// Sub-second durations keep their ms precision; longer ones read as seconds,
// the way the run-mode spec output reports test times.
const testDuration = (duration) => {
if (duration == null) {
return '';
}
return ` ${chalk.dim(duration < 1000 ? `${duration}ms` : `${+(duration / 1000).toFixed(1)}s`)}`;
};
const renderSpecTests = (tests, indent) => {
return tests.flatMap((test) => {
var _a, _b;
// Only a retried test carries attempts, so the count is always 2 or more.
const attempts = ((_a = test.attempts) === null || _a === void 0 ? void 0 : _a.length) ? ` ${color.aborted(`(${test.attempts.length} attempts)`)}` : '';
return [
`${indent}${chalk.dim(test.id.padStart(3))} ${stateBadge[test.state].icon} ${test.title}${testDuration(test.duration)}${attempts}`,
// Nested under the title, the way the app reporter lists a retried
// test's attempts; the id column stays empty so the rows read as one test.
...((_b = test.attempts) !== null && _b !== void 0 ? _b : []).map((attempt) => {
return `${indent} ${stateBadge[attempt.state].icon} ${chalk.dim(`attempt ${attempt.attempt}`)}${testDuration(attempt.duration)}`;
}),
];
});
};
// The app reporter renders each suite as its own section headed by the full
// suite path — depth shows in the breadcrumb, not in indentation. The wire shape
// is already flattened that way: one entry per suite with direct tests, title
// pre-joined. Titles keep the case the spec wrote them in, so they can be pasted
// into a case-sensitive search; only the hook sections are upper-cased, since
// those names are ours rather than the author's.
const specSuiteSections = (suites) => {
return suites.map((suite) => [heading(suite.title), ...renderSpecTests(suite.tests, ' ')]);
};
const renderReporterSpecHuman = (view) => {
const specTitle = view.startedAt ? `${chalk.bold(view.spec)} ${startedAtLabel(view.startedAt)}` : chalk.bold(view.spec);
const header = [
...(view.spec ? [specTitle] : []),
statsLine(view.stats),
];
const sections = [
...(view.tests.length ? [renderSpecTests(view.tests, ' ')] : []),
...specSuiteSections(view.suites),
];
const blocks = [
header,
...(sections.length ? sections : [[emptyState('No tests were found in this spec.')]]),
];
return layout(blocks);
};
// The run command starts a spec and returns immediately (poll `status` for
// progress), so this confirms what was launched rather than reporting an outcome.
const renderRunHuman = (result) => {
return layout([
[titleLine(color.pass('▶'), result.spec)],
definitionList([
['testing type', result.testingType],
['browser', result.browser],
]),
]);
};
// An open browser reads by its name alone only when tap can actually drive it;
// each way that can fail — a browser tap does not support, one it has lost its
// connection to, one whose page will not answer — is the state every other
// command fails in, so each says which.
const browserState = (session) => {
if (session.browserSupported === false) {
return 'unsupported';
}
if (session.browserAttached === false) {
return 'not attached';
}
return session.rendererResponsive === false ? 'not responding' : null;
};
const browserCell = (session) => {
if (session.browserName === null) {
return '—';
}
const state = browserState(session);
return state === null ? session.browserName : `${session.browserName} (${state})`;
};
const browserColor = (session) => {
if (session.browserName === null) {
return color.muted;
}
const state = browserState(session);
if (state === null) {
return color.pass;
}
return state === 'unsupported' ? color.warn : color.aborted;
};
// One row per session. PID is bold — it's the handle the other tap commands
// accept via `--session` — and an attached browser reads green by its name, an
// absent one (or testing type) as a muted dash.
const sessionColumns = (sessions) => {
const rows = sessions.map((session) => {
var _a;
return [
String(session.pid),
session.projectRoot,
(_a = session.testingType) !== null && _a !== void 0 ? _a : '—',
browserCell(session),
];
});
return columns(['PID', 'PROJECT', 'TYPE', 'BROWSER'], rows, (cells, index) => [
chalk.bold(cells[0]),
cells[1],
cells[2],
browserColor(sessions[index])(cells[3]),
]);
};
// The reachable open-mode sessions under a counted heading.
const renderSessionsHuman = (sessions) => {
return layout([panel('SESSIONS', sessions.length, sessionColumns(sessions))]);
};
// A flat, headed list of runnable specs. The git last-modified time trails each
// path, aligned into a muted column; the machine-facing timestamp stays in --json.
// An empty project keeps the same `SPECS (n)` frame so the shape reads the same
// whether or not any specs exist.
const renderSpecsHuman = (specs) => {
if (!specs.length) {
return layout([[heading('SPECS', 0), ` ${emptyState('[EMPTY PROJECT]')}`]]);
}
const width = Math.max(...specs.map((spec) => spec.relativePath.length));
const rows = specs.map((spec) => {
const modified = spec.lastModified ? ` ${color.muted(spec.lastModified)}` : '';
return ` ${spec.relativePath.padEnd(width)}${modified}`;
});
return layout([[heading('SPECS', specs.length), ...rows]]);
};
/** The pin glyph every pin line leads with — pinned, cleared, or failed to clear. */
const PIN_ICON = '⚲';
// A pin reads as its own reporter row: which snapshot of the command is showing,
// then the row itself under its hook section, exactly as `reporter` prints it.
// Shared so `pin` and `status` can't drift.
const pinnedBlock = (view) => {
const { index, total, name } = view.at;
const snapshot = `(${index}/${total})${name ? ` ${name}` : ''}`;
return [
color.alias(`${PIN_ICON} PINNED - ${snapshot}`),
...renderCommandSection([view.command], view.hookName),
];
};
// The lifecycle phase's dot and tint: a filled green/red check for a finished
// run, indigo while running, a muted ring for the pre-run "coming up" stages.
const PHASE = {
passed: { icon: stateBadge.passed.icon, tint: color.pass },
failed: { icon: stateBadge.failed.icon, tint: color.fail },
running: { icon: color.pending('●'), tint: color.pending },
};
const phaseOf = (status) => { var _a; return (_a = PHASE[status]) !== null && _a !== void 0 ? _a : { icon: color.muted('●'), tint: color.muted }; };
// Where the session is: the selected spec led by its phase icon and trailed by
// the run's start time, or — before a spec is selected — the phase on its own.
// The icon carries the phase over a spec, so the line doesn't also spell it out.
const phaseLine = (status) => {
const { icon, tint } = phaseOf(status.status);
if (!status.spec) {
return `${icon} ${tint(status.status)}`;
}
return titleLine(icon, status.spec, status.startedAt ? startedAtLabel(status.startedAt) : undefined);
};
const renderStatusHuman = (status) => {
var _a, _b;
const { pid, projectRoot } = status;
// Nothing to target — the phase is the whole answer.
if (pid === undefined || projectRoot === undefined) {
return phaseLine(status);
}
const session = {
pid,
projectRoot,
testingType: (_a = status.testingType) !== null && _a !== void 0 ? _a : null,
browserName: (_b = status.browserName) !== null && _b !== void 0 ? _b : null,
browserAttached: status.browserAttached,
};
const progress = [phaseLine(status)];
if (status.results) {
progress.push(countsLine(status.results));
}
const blocks = [sessionColumns([session]), progress];
// A spec that failed to build has no results to show, so the failure is the
// only thing the run has to say.
if (status.error) {
blocks.push([color.fail(status.error)]);
}
if (status.pinned) {
blocks.push(pinnedBlock(status.pinned));
}
return layout(blocks);
};
/**
* `outerHTML` starts at the element but its inner lines keep the indentation
* they had in the document, so a deeply nested element arrives ragged — first
* line at the margin, the rest pushed right. Removing the smallest indent they
* all share pulls the markup back to the margin without touching its internal
* shape. Rendering-only: `--json` keeps the document's own whitespace.
*/
const dedent = (html) => {
const [first, ...rest] = html.split('\n');
const shared = rest
.filter((line) => line.trim().length)
.reduce((min, line) => Math.min(min, line.length - line.trimStart().length), Infinity);
if (!rest.length || shared === Infinity || shared === 0) {
return html;
}
return [first, ...rest.map((line) => line.slice(shared))].join('\n');
};
// Nothing but the HTML — no title framing it, so the output reads (and pipes)
// as the markup it is. A browser-side clip still adds a muted trailer, since
// silently handing back half a document would be worse than a little furniture.
const renderDomHuman = (result) => {
if (result.found === false) {
return emptyState('No element matched the selector.');
}
return layout([
...(result.html !== undefined ? [[dedent(result.html)]] : []),
...(result.truncated ? [[color.muted('(output truncated)')]] : []),
]);
};
// One node per line, indented by its own depth so the root sits flush: bold
// role, then its accessible name, an `= value` for value-bearing controls, and
// any notable states as a muted bracketed list — the compact role/name tree
// DevTools shows.
const renderNode = (node) => {
var _a;
const name = node.name ? ` ${node.name}` : '';
const value = node.value !== undefined ? ` = ${node.value}` : '';
const states = ((_a = node.states) === null || _a === void 0 ? void 0 : _a.length) ? ` ${color.muted(`[${node.states.join(', ')}]`)}` : '';
return `${indent(node.depth)}${chalk.bold(node.role)}${name}${value}${states}`;
};
const renderAriaHuman = (result) => {
if (!result.nodes.length) {
return emptyState('No accessibility nodes found.');
}
const blocks = [result.nodes.map(renderNode)];
if (result.truncated) {
blocks.push([color.muted('(output truncated)')]);
}
return layout(blocks);
};
// A record renders as a counted heading over an aligned key/value list; an empty
// or absent record contributes no block.
const recordBlock = (title, record) => {
const entries = Object.entries(record !== null && record !== void 0 ? record : {});
return entries.length ? [[heading(title, entries.length), ...definitionList(entries)]] : [];
};
const ariaBlock = (aria) => {
var _a;
const rows = [
['role', aria === null || aria === void 0 ? void 0 : aria.role],
['name', aria === null || aria === void 0 ? void 0 : aria.name],
['states', ((_a = aria === null || aria === void 0 ? void 0 : aria.states) === null || _a === void 0 ? void 0 : _a.length) ? aria.states.join(', ') : undefined],
];
const entries = rows.filter((row) => row[1] !== undefined);
return entries.length ? [[heading('ACCESSIBILITY'), ...definitionList(entries)]] : [];
};
const boxBlock = (box) => {
return box ? [[heading('BOX'), ` x ${box.x} y ${box.y} width ${box.width} height ${box.height}`]] : [];
};
const renderInspectHuman = (result) => {
if (!result.found) {
return `${chalk.bold(quoted(result.selector))} ${color.muted('not found')}`;
}
return layout([
...recordBlock('ATTRIBUTES', result.attributes),
...ariaBlock(result.aria),
...boxBlock(result.box),
...recordBlock('STYLES', result.styles),
]);
};
// A pin prints the same block wherever it's reported, so `pin` and `status`
// agree; `--clear` has nothing to show but whether the pin let go.
const renderCleared = (result) => {
return result.cleared
? chalk.dim(`${PIN_ICON} PIN CLEARED`)
: color.fail(`${PIN_ICON} FAILED TO CLEAR PIN`);
};
const renderPinHuman = (result) => {
return 'pinned' in result ? layout([pinnedBlock(result.pinned)]) : renderCleared(result);
};
// A command's console properties are the deepest payload the tap returns — a
// `cy.request` row carries its matcher, request, response and every header of
// each. Printed whole it is pages of indentation, so this renders the shape
// first, the way the browser console panel opens collapsed: a few levels expand,
// and a section that is deeper or too long to read at a glance is summarized as
// `{n keys}` / `[n items]` until --depth asks for it. The full payload is one
// --json away.
// Deep enough that a request's matcher, its response and that response's body
// all read without asking, since a payload is usually consulted for something
// several levels in. The row budget below is what keeps that from running away.
const DEFAULT_DEPTH = 3;
// A header map runs to twenty-odd rows and buries everything around it, so the
// default view folds any section that long however shallow it sits. An explicit
// --depth is taken at its word and lifts the cap: it asked for levels, not for a
// judgement about size.
const DEFAULT_ROW_BUDGET = 8;
// However narrow the terminal, a value keeps enough room to be worth reading.
const MIN_VALUE_WIDTH = 24;
// Console prop labels are short prose (`Request Headers`); anything past this is
// a key carrying data, and it does not get to own the level's whole row.
const MAX_KEY_WIDTH = 32;
const isRecord = (value) => {
return typeof value === 'object' && value !== null && !Array.isArray(value);
};
const isContainer = (value) => typeof value === 'object' && value !== null;
// The serializer names a value too long to ship by its length rather than
// returning it (see the driver's `withheld`); color the marker so it doesn't
// read as content.
const WITHHELD = /^\[[\d,]+ characters? withheld — pass --json to include it\]$/;
// A response body or stack trace carries its own newlines; inlining it would
// break the aligned column, so it reads as a block under its key.
const isBlock = (value) => typeof value === 'string' && value.includes('\n');
// A response body or header value arrives with whatever bytes the server sent.
// A tab re-tabulates the row and a lone carriage return overwrites it, so on a
// row — where the alignment is the only thing holding the output together — they
// read as spaces. Escape codes are dropped rather than passed through: they would
// tint a row the renderer never meant to and their bytes count toward the clamp.
const onOneRow = (text) => {
let sanitized = '';
for (let index = 0; index < text.length; index++) {
const code = text.charCodeAt(index);
if (code === 0x1B && text.charCodeAt(index + 1) === 0x5B) {
let end = index + 2;
while (end < text.length) {
const sequenceCode = text.charCodeAt(end);
const isParameter = (sequenceCode >= 0x30 && sequenceCode <= 0x39) || sequenceCode === 0x3B;
const isTerminator = (sequenceCode >= 0x41 && sequenceCode <= 0x5A)
|| (sequenceCode >= 0x61 && sequenceCode <= 0x7A);
if (isTerminator) {
index = end;
break;
}
if (!isParameter) {
break;
}
end++;
}
if (index === end) {
continue;
}
}
sanitized += code <= 0x09 || (code >= 0x0B && code <= 0x1F) ? ' ' : text[index];
}
return sanitized;
};
// A key is always one row, newline or not — the control range above spares `\n`
// for the values that render as a block under their key.
const asKey = (text) => onOneRow(text).replace(/\n/g, ' ').trim();
const scalarInline = (value) => {
if (value === null) {
return { text: 'null', style: chalk.dim };
}
const text = onOneRow(String(value));
if (typeof value === 'string' && WITHHELD.test(text)) {
return { text, style: color.aborted };
}
if (!text.length) {
return { text: '(empty string)', style: chalk.dim };
}
return { text, clampable: true };
};
const emptyContainerInline = (value) => {
if (Array.isArray(value) && !value.length) {
return { text: '[]', style: chalk.dim };
}
if (isRecord(value) && !Object.keys(value).length) {
return { text: '{}', style: chalk.dim };
}
return undefined;
};
// What a collapsed container reads as: its size, so the shape is still legible
// and the cost of expanding it is known before you do.
const summaryInline = (value) => {
const isArray = Array.isArray(value);
const size = isArray ? value.length : Object.keys(value).length;
const unit = isArray ? 'item' : 'key';
const [open, close] = isArray ? ['[', ']'] : ['{', '}'];
return { text: `${open}${size} ${unit}${size === 1 ? '' : 's'}${close}`, style: chalk.dim };
};
// A cell keeps the column alignment that makes a table readable, so it stays one
// line of bounded width.
const MAX_CELL = 40;
const cell = (value) => {
if (value === undefined) {
return '';
}
if (Array.isArray(value)) {
return '[…]';
}
if (isRecord(value)) {
return '{…}';
}
// A cell cannot hold the newlines of a multi-line value: they would end the
// row the table is aligning.
const text = String(value).replace(/\s+/g, ' ').trim();
return WITHHELD.test(text) ? text : clamp(text, MAX_CELL);
};
// Rows of like-shaped objects are what the driver's `table` console prop holds
// (its keyboard/mouse event tables) — render them the way the reporter renders a
// table, the row keys themselves as the column headers. A lone row reads better
// as plain key/values, so it is left to the caller.
const rowsTable = (values, indent) => {
if (values.length < 2 || !values.every(isRecord)) {
return undefined;
}
const columnKeys = [...new Set(values.flatMap((row) => Object.keys(row)))];
if (!columnKeys.length) {
return undefined;
}
const rows = values.map((row) => columnKeys.map((column) => cell(row[column])));
// Cells are plain here, so coloring them after padding is a no-op width-wise;
// the withheld marker still needs its hue.
return tableRows(columnKeys, rows, (cells) => cells.map((text) => (WITHHELD.test(text.trim()) ? color.aborted(text) : text)))
.map((line) => `${indent}${line}`);
};
const entriesOf = (value) => {
return Array.isArray(value)
? value.map((item, index) => [String(index + 1), item])
: Object.entries(value);
};
const createPropsRenderer = (maxDepth, rowBudget) => {
let collapsedCount = 0;
const block = (text, indent) => blockLines(text, indent);
const rows = (values, indent) => rowsTable(values, indent);
// Scalars align in one column with their sibling scalars; a container gets its
// own key line with its children indented beneath it, so nesting reads as
// structure rather than punctuation.
const renderProps = (value, level) => {
const indent = ' '.repeat(level + 1);
const childIndent = ' '.repeat(level + 2);
const entries = entriesOf(value);
const inlined = new Map(entries.flatMap(([key, child]) => {
if (isBlock(child)) {
return [];
}
if (!isContainer(child)) {
return [[key, scalarInline(child)]];
}
const empty = emptyContainerInline(child);
if (empty) {
return [[key, empty]];
}
if (level < maxDepth && entriesOf(child).length <= rowBudget) {
return [];
}
collapsedCount++;
return [[key, summaryInline(child)]];
}));
// One outlier key would otherwise push every value on the level out past the
// terminal, so the column it shares stops at a width a row can carry.
const label = (key) => clamp(asKey(key) || '(empty key)', MAX_KEY_WIDTH);
const width = Math.max(0, ...[...inlined.keys()].map((key) => label(key).length));
const valueWidth = Math.max(MIN_VALUE_WIDTH, terminalWidth() - indent.length - width - 2);
return entries.flatMap(([key, child]) => {
const inline = inlined.get(key);
const keyLine = `${indent}${chalk.dim(label(key))}`;
if (inline) {
const text = inline.clampable ? clamp(inline.text, valueWidth) : inline.text;
return [`${indent}${chalk.dim(label(key).padEnd(width))} ${inline.style ? inline.style(text) : text}`];
}
if (isBlock(child)) {
return [keyLine, ...block(child, childIndent)];
}
const table = Array.isArray(child) ? rows(child, indent) : undefined;
return [keyLine, ...(table !== null && table !== void 0 ? table : renderProps(child, level + 1))];
});
};
return { renderProps, block, rows, collapsedCount: () => collapsedCount };
};
const blockLines = (text, indent) => {
const width = Math.max(MIN_VALUE_WIDTH, terminalWidth() - indent.length);
// A body split on its newlines still carries the `\r` of a CRLF payload, which
// would drag the cursor back over the line it just printed.
return text.split('\n').map((line) => `${indent}${clamp(onOneRow(line), width)}`);
};
const readDepth = (value) => {
const byDefault = { depth: DEFAULT_DEPTH, rowBudget: DEFAULT_ROW_BUDGET };
if (value === undefined) {
return byDefault;
}
if (value.toLowerCase() === 'all') {
return { depth: Infinity, rowBudget: Infinity };
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) {
return Object.assign(Object.assign({}, byDefault), { note: `--depth takes a whole number or "all"; showing depth ${DEFAULT_DEPTH}.` });
}
return { depth: parsed, rowBudget: Infinity };
};
// The driver wraps every log's console properties in a fixed envelope
// (see wrapConsoleProps): the command's own key/values live under `props`, with
// `table`/`groups`/`error`/`args` as siblings. Rendering the envelope rather
// than the raw payload is what lifts the interesting keys to the top level, the
// way the browser console panel shows them.
const ENVELOPE_KEYS = new Set(['name', 'type', 'props', 'table', 'groups', 'error', 'args']);
const propsHeader = () => heading('CONSOLE PROPS');
// The reporter's failure palette (see its error panel): a red title over the
// lighter hue it prints a failure's own text in.
const ERROR_TINT = { title: color.fail, line: color.errHeaderText };
// An envelope key beside `props`, as its own titled section. A tinted section
// colors its title and its lines — the error arrives as a stack, so what carries
// the color is the block, not a props tree.
const extraSection = (title, value, render, tint) => {
if (value == null) {
return [];
}
const sectionTitle = tint ? tint.title(title) : heading(title);
if (isContainer(value)) {
return emptyContainerInline(value) ? [] : [[sectionTitle, ...render.renderProps(value, 0)]];
}
const lines = render.block(String(value), ' ');
return [[sectionTitle, ...(tint ? lines.map((line) => tint.line(line)) : lines)]];
};
// Each table the driver logged is a slot in `table`, keyed by the order it
// should render in and carrying its own display name — the reporter's tables,
// straight across.
const tableSections = (value, render) => {
if (!isRecord(value)) {
return extraSection('TABLE', value, render);
}
return Object.keys(value)
.sort((a, b) => Number(a) - Number(b))
.flatMap((slot) => {
const entry = value[slot];
if (!isRecord(entry)) {
return [];
}
const title = (typeof entry.name === 'string' ? entry.name : `table ${slot}`).toUpperCase();
const data = entry.data;
const table = Array.isArray(data) ? render.rows(data, '') : undefined;
if (table) {
return [[heading(title, data.length), ...table]];
}
const body = isContainer(data) ? data : entry;
return [[heading(title), ...render.renderProps(body, 0)]];
});
};
const collapsedFooter = (count) => {
if (!count) {
return [];
}
const sections = `${count} section${count === 1 ? '' : 's'}`;
return [[chalk.dim(`${sections} collapsed — open all of it with --depth all`)]];
};
// A section can be present and hold nothing. A heading with a void under it
// reads as a rendering bug, so the emptiness is stated.
const withBody = (lines) => {
return lines.length ? lines : [` ${emptyState('(nothing here)')}`];
};
const renderConsolePropsHuman = (envelope, options = {}) => {
if (!Object.keys(envelope).length) {
return emptyState('This command logged no console properties.');
}
const { depth, rowBudget, note } = readDepth(options.depth);
const noteBlock = note ? [[emptyState(note)]] : [];
const render = createPropsRenderer(depth, rowBudget);
const props = envelope.props;
// A payload with no envelope — the driver's stand-in for a command whose
// details it has since evicted — is rendered as it arrives.
if (!isRecord(props)) {
return layout([[heading('CONSOLE PROPS'), ...withBody(render.renderProps(envelope, 0))], ...collapsedFooter(render.collapsedCount()), ...noteBlock]);
}
const { table: tables, groups, error, args } = envelope;
const unexpected = Object.fromEntries(Object.entries(envelope).filter(([key]) => !ENVELOPE_KEYS.has(key)));
return layout([
[propsHeader(), ...withBody(render.renderProps(props, 0))],
...(tables === undefined ? [] : tableSections(tables, render)),
...(groups === undefined ? [] : extraSection('GROUPS', groups, render)),
...(args === undefined ? [] : extraSection('ARGS', args, render)),
...(error === undefined ? [] : extraSection('ERROR', error, render, ERROR_TINT)),
...(Object.keys(unexpected).length ? extraSection('OTHER', unexpected, render) : []),
...collapsedFooter(render.collapsedCount()),
...noteBlock,
]);
};
// The reporter's inline network detail, expanded into its own panel: the row
// message already summarizes the request, so this is where the parts a consumer
// might act on — matcher, status, alias — read individually. Ordered the way the
// reporter's ROUTES table columns are.
const networkEntries = (network) => {
const entries = [];
const add = (label, value) => {
if (value) {
entries.push([label, value]);
}
};
add('METHOD', network.method && chalk.bold(network.method));
add('URL', network.url);
add('STATUS', network.status != null ? String(network.status) : undefined);
add('INDICATOR', network.indicator && `${networkDot(network)}${network.indicator}`);
add('STUBBED', network.stubbed === undefined ? undefined : network.stubbed ? color.aborted('yes') : 'no');
add('RESPONSES', network.numResponses != null ? String(network.numResponses) : undefined);
add('ALIAS', network.alias && color.alias(`@${network.alias}`));
return entries;
};
// One command rendered as the reporter renders its row — state icon, id, name
// (dash-prefixed when chained), network dot, styled message, alias badge — with
// the state spelled out the way the reporter's test header does. A row with no
// state yet (a route registration) simply has no icon and no word.
const entryHeader = (entry) => {
const state = entry.state ? stateBadge[entry.state] : undefined;
const name = commandLabel(entry);
const styledName = entry.state === 'failed' ? color.fail.bold(name) : chalk.bold(name);
// Unlike the reporter's log, a lone row has no columns to align to, so the
// parts a row happens to lack close up instead of leaving a gap.
const head = [
state === null || state === void 0 ? void 0 : state.icon,
entry.id && chalk.dim(entry.id),
styledName,
`${networkDot(entry.network)}${formatMessage(entry)}`.trim(),
].filter(Boolean).join(' ');
const suffixes = `${aliasSuffix(entry, entry.network)}${networkSuffix(entry.network)}${cleanedSuffix(entry)}`;
return `${head}${suffixes}${state ? ` ${state.word}` : ''}`;
};
// Which section of the reporter panel the row sits under, printed above it as
// the reporter's own log prints its section titles — the context a lone row has
// no way to show, and what its `<hookId>:<number>` handle qualifies.
const hookLine = (hook) => sectionHeading(hook.hookName, hook.hookId);
// Wall clock rather than an offset: a snapshot's time is only useful lined up
// against something else — another command's snapshots, a server log — and the
// row itself carries no start to offset from.
const snapshotTime = (timestamp) => {
if (timestamp === undefined) {
return '—';
}
const at = new Date(timestamp);
const clock = [at.getHours(), at.getMinutes(), at.getSeconds()].map((part) => String(part).padStart(2, '0')).join(':');
return `${clock}.${String(at.getMilliseconds()).padStart(3, '0')}`;
};
// The DOM snapshots this row captured, addressed the way `pin --at` takes them:
// by name or by position. Always rendered — a row with none is the answer to
// "can I pin this?", so it keeps the panel rather than dropping it.
const snapshotsBlock = (snapshots) => {
if (!snapshots.length) {
return [heading('SNAPSHOTS', 0), `${indent(1)}${emptyState('[NO SNAPSHOTS]')}`];
}
const rows = snapshots.map((snapshot) => {
var _a;
return [
String(snapshot.index),
(_a = snapshot.name) !== null && _a !== void 0 ? _a : '—',
snapshotTime(snapshot.timestamp),
];
});
// Mute from the snapshot rather than the padded cell: what reads as absent is
// the field being unset, which only the row's own data knows.
return table('SNAPSHOTS', ['#', 'NAME', 'TIME'], rows, (cells, index) => {
const { name, timestamp } = snapshots[index];
return [cells[0], name === undefined ? color.muted(cells[1]) : cells[1], timestamp === undefined ? color.muted(cells[2]) : cells[2]];
});
};
// The console panel's payload, closing the view the way the app's does: the row
// above, its properties below. A row that logged none keeps the section rather
// than dropping it, so the output reads the same shape either way.
const consolePropsBlock = (props, options) => {
if (!props) {
return [heading('CONSOLE PROPS'), `${indent(1)}${emptyState('[NO CONSOLE PROPS]')}`];
}
return renderConsolePropsHuman(props, options).split('\n');
};
const renderCommandHuman = (result, options = {}) => {
const network = result.network && networkEntries(result.network);
return layout([
[hookLine(result.hook), entryHeader(result)],
...((network === null || network === void 0 ? void 0 : network.length) ? [[heading('NETWORK'), ...definitionList(network)]] : []),
snapshotsBlock(result.snapshots),
consolePropsBlock(result.consoleProps, options),
]);
};
// The selector-taking AUT reads answer an ambiguous selector in place of the
// read they were asked for, so each one renders that answer instead of its own.
const orAmbiguous = (render) => {
return (result) => {
const ambiguous = result;
return ambiguous.ambiguous ? renderAmbiguousHuman(ambiguous) : render(result);
};
};
const renderings = {
reporter: {
renderHuman: (result) => {
// Only the no-test spec overview carries `stats`; the single-test view never does.
const view = result;
return 'stats' in view ? renderReporterSpecHuman(view) : renderReporterHuman(view);
},
},
command: {
renderHuman: (result, options) => {
return renderCommandHuman(result, {
depth: options.depth,
});
},
},
run: { renderHuman: (result) => renderRunHuman(result) },
sessions: { renderHuman: (result) => renderSessionsHuman(result) },
specs: { renderHuman: (result) => renderSpecsHuman(result) },
status: { renderHuman: (result) => renderStatusHuman(result) },
dom: { renderHuman: orAmbiguous(renderDomHuman) },
aria: { renderHuman: orAmbiguous(renderAriaHuman) },
inspect: { renderHuman: orAmbiguous(renderInspectHuman) },
pin: { renderHuman: (result) => renderPinHuman(result) },
};
const renderingFor = (command) => {
return renderings[command];
};
var dist = {};
var hasRequiredDist;
function requireDist () {
if (hasRequiredDist) return dist;
hasRequiredDist = 1;
(function (exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.isAgent = exports$1.detectAgent = void 0;
const envMatcher = (key, regex, opts = {}) => {
return (env) => {
var _a, _b;
// Some vars are set by both an IDE's integrated terminal and its CLI agent. A TTY on
// either end means a human is at the terminal, not an agent-spawned subprocess. Both
// ends are checked because redirecting one still leaves the other attached, as in
// `cypress run | tee log.txt`.
if (opts.noTTY && (((_a = process.stdout) === null || _a === void 0 ? void 0 : _a.isTTY) || ((_b = process.stdin) === null || _b === void 0 ? void 0 : _b.isTTY))) {
return false;
}
const value = env[key];
return value ? regex.test(value) : false;
};
};
// IDEs are checked last so an agent running inside one is reported as the agent.
const AGENTS = [
['claude', ['CLAUDECODE', 'CLAUDE_CODE']],
['replit', ['REPL_ID']],
['gemini', ['GEMINI_CLI']],
['codex', ['CODEX_SANDBOX', 'CODEX_THREAD_ID']],
['opencode', ['OPENCODE']],
['pi', [envMatcher('PATH', /\.pi[\\/]agent/)]],
['auggie', ['AUGMENT_AGENT']],
['goose', ['GOOSE_PROVIDER']],
['junie', ['JUNIE_DATA', 'JUNIE_SHIM_PATH']],
['devin', [envMatcher('EDITOR', /(^|[\\/])devin(\.exe)?$/)]],
['cursor', ['CURSOR_AGENT']],
['kiro', [envMatcher('TERM_PROGRAM', /kiro/, { noTTY: true })]],
];
const KNOWN_NAMES = AGENTS.map(([name]) => name);
// AI_AGENT is free-form and often carries a version (Claude Code sets
// "claude-code_2-1-221_agent"), so narrow it to a known name instead of passing it
// along verbatim — callers report this value, and only fixed names may leave the machine.
const fromAiAgent = (value) => {
var _a;
const normalized = value.toLowerCase();
// The name has to end where it ends, so a short one like `pi` cannot claim an
// unrelated `pipecat`.
return (_a = KNOWN_NAMES.find((name) => new RegExp(`^${name}($|[^a-z0-9])`).test(normalized))) !== null && _a !== void 0 ? _a : 'other';
};
const detectAgent = (env = process.env) => {
for (const [name, checks] of AGENTS) {
for (const check of checks) {
if (typeof check === 'string' ? env[check] : check(env)) {
return name;
}
}
}
return env.AI_AGENT ? fromAiAgent(env.AI_AGENT) : undefined;
};
exports$1.detectAgent = detectAgent;
const isAgent = (env = process.env) => {
return !!(0, exports$1.detectAgent)(env);
};
exports$1.isAgent = isAgent;
} (dist));
return dist;
}
var distExports = requireDist();
const debug$4 = Debug('cypress:cli:tap');
const CAMPAIGN = 'Tap Command';
const MEDIUM = 'tap-cli';
const POST_TIMEOUT_MS = 2000;
// Only flags a command declares reach a trace, and no command declares close to
// this many, so the cap is a backstop rather than the real bound.
const MAX_REPORTED_FLAGS = 25;
// Duplicated from packages/data-context/src/util/cloudUrls.ts, which the CLI
// cannot import.
const CLOUD_URLS = {
development: 'http://localhost:3000',
staging: 'https://cloud-staging.cypress.io',
production: 'https://cloud.cypress.io',
};
// Which collector the environment names, if it names one this CLI has a URL for:
// the collector variable the app reads (see EventCollectorActions), then the
// internal environment it is normally derived from. An unrecognized value is no
// collector at all, so a typo cannot pass for naming one and land a source
// checkout's traffic in the production analytics.
const namedCollectorEnv = () => {
var _a;
const named = ((_a = process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV) !== null && _a !== void 0 ? _a : process.env.CYPRESS_INTERNAL_ENV);
return Object.prototype.hasOwnProperty.call(CLOUD_URLS, named) ? named : undefined;
};
const eventCollectorUrl = (includeMachineId = false) => {
var _a;
return `${CLOUD_URLS[(_a = namedCollectorEnv()) !== null && _a !== void 0 ? _a : 'production']}/${includeMachineId ? 'machine-collect' : 'anon-collect'}`;
};
const newTrace = (command = 'none', flags = []) => ({
messageId: crypto.randomUUID(),
startedAt: Date.now(),
command,
flags,
});
let trace = newTrace();
const beginTapTrace = ({ command, flags }) => {
trace = newTrace(command, flags);
};
// Names only: an option's value carries selectors, spec paths and test titles,
// so the trace takes the keys of what commander parsed, never the values.
const noteTapCommand = (dispatched, ...parsed) => {
trace.command = dispatched;
trace.flags = [...new Set([...trace.flags, ...parsed.flatMap((values) => Object.keys(values))])];
};
const noteTapFailure = (code) => {
trace.errorCode = code;
};
// Tap error messages interpolate selectors, spec paths and project roots, and so
// do option values, so the payload is a fixed field list of names and codes —
// spelled out here rather than spread from the trace, so a new trace field
// cannot silently become a new wire field.
const reportTapTrace = (exitCode) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c;
// This runs from the `finally` the CLI exits on, so nothing here may throw: a
// failure while assembling the event would replace the command's own outcome.
try {
// Read through getEnv so the opt-out can also come from npm config, the way
// the CLI's other public variables are set.
if (xvfb.util.getEnv('CYPRESS_DISABLE_GUEST_TELEMETRY')) {
debug$4('skipped tap event: telemetry disabled');
return;
}
const cypressVersion = xvfb.util.pkgVersion();
// Local development reports nothing unless it names the collector to use, so
// working on tap cannot put its own traffic in the production analytics.
if (cypressVersion === xvfb.DEVELOPMENT_VERSION && !namedCollectorEnv()) {
debug$4('skipped tap event: development build');
return;
}
const identity = resolvedSessionIdentity();
const payload = {
command: trace.command,
flags: trace.flags.slice(0, MAX_REPORTED_FLAGS),
agent: distExports.detectAgent(),
sessionId: (_a = identity === null || identity === void 0 ? void 0 : identity.sessionId) !== null && _a !== void 0 ? _a : undefined,
userId: (_b = identity === null || identity === void 0 ? void 0 : identity.userId) !== null && _b !== void 0 ? _b : undefined,
exitCode,
errorCode: trace.errorCode,
durationMs: Date.now() - trace.startedAt,
};
// The identity travels in the session probe response, so an invocation that
// never resolved a session has no machineId and stays on the anonymous
// collector, mirroring EventCollectorActions.recordEvent app-side.
const machineId = (_c = identity === null || identity === void 0 ? void 0 : identity.machineId) !== null && _c !== void 0 ? _c : undefined;
const url = eventCollectorUrl(machineId !== undefined);
yield fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': cypressVersion,
},
body: JSON.stringify({ campaign: CAMPAIGN, medium: MEDIUM, messageId: trace.messageId, machineId, payload }),
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
});
debug$4('recorded tap event to %s %o', url, payload);
}
catch (err) {
debug$4('failed to record tap event for %o due to error %o', trace, err);
}
});
const debug$3 = Debug('cypress:cli:tap');
// The registry keeps its copy dependency-free, so the commands it names arrive in
// backticks for the CLI to colour the way the rest of the catalogue already does.
const highlight = (copy) => copy.replace(/`([^`]+)`/g, (_match, command) => chalk.cyan(command));
/**
* The remedy and the trailing blocks an entry asks for, so guidance repeated across
* errors is declared once per entry rather than written out again in each one. An
* entry that asks for the help takes it in place of its solution, which is written
* to point at the very help now being printed.
*/
const remedyFor = (copy, help) => {
const attached = copy.attachHelp && help ? help.trimEnd() : undefined;
const solution = attached !== null && attached !== void 0 ? attached : (copy.solution ? highlight(copy.solution) : undefined);
const parts = solution ? [solution] : [];
if (copy.docs) {
parts.push(`Learn more:\n\n ${chalk.blue(`${xvfb.docsUrl}${copy.docs}`)}`);
}
if (copy.recommendGhIssue) {
parts.push(`If the problem persists, search for an existing issue or open a GitHub issue at\n\n ${chalk.blue(xvfb.util.issuesUrl)}`);
}
return parts;
};
/**
* The single exit for every tap failure, whether the CLI raised it or it arrived
* from the session as a wire payload: the code selects the copy and `detail`
* carries whatever was specific to this one. It prints as paragraphs — the
* condition, then the specifics that explain it, then what to do about it. The code
* itself is never printed.
*
* Anything else — a TypeError from a command handler, an ENOTDIR from a read that
* should not have failed — was never raised as a tap failure, so there is no
* condition to state and nothing on it is copy a reader was meant to see: it renders
* as UNKNOWN_ERROR, and its stack goes to the debug log, which is where whoever is
* asked for one after reading the report will look.
*
* Which is why a code alone does not make a failure ours: `err.code` is a string on
* every Node system error too, and rendering an ENOENT against the registry would
* pick whatever the fallback copy happens to be. Only a TapError, or the wire payload
* the session sends and its raisers pass here as-is, is read for its code.
*
* `help` is the generated help of the command that was called, passed by the callers
* that have a parsed program to hand. Only a failure about the invocation prints it;
* the session raises those too, and has no help of its own to send.
*/
const renderTapFailure = (err, help) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const raised = cli.distExports.isTapError(err) || (!(err instanceof Error) && typeof (err === null || err === void 0 ? void 0 : err.code) === 'string');
const failure = raised ? err : new cli.distExports.UnknownTapError(err);
if (!raised) {
debug$3('rendering an unrecognized failure as %s: %s', failure.code, failure.message);
}
noteTapFailure(failure.code);
const copy = cli.distExports.tapErrorCopy(failure.code);
const condition = copy.description ? [highlight(copy.description)] : [];
const detail = typeof failure.detail === 'string' && failure.detail !== '' ? [highlight(failure.detail)] : [];
xvfb.loggerModule.errorToStderr([...condition, ...detail, ...remedyFor(copy, help)].join('\n\n'));
return 1;
});
/**
* The help for the command that was called, or the whole program's when the name
* matched nothing — which is the case a reader who mistyped a command needs.
*/
const helpFor = (program, command) => {
const subcommand = program.commands.find((sub) => sub.name() === command);
return (subcommand !== null && subcommand !== void 0 ? subcommand : program).helpInformation();
};
const renderResult = (result) => {
xvfb.loggerModule.always(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
};
/**
* Print a command's result: its human-readable rendering when the command
* defines one (see `./render`), the raw JSON otherwise or when `--json` asks
* for it explicitly. The invoked options are forwarded because a command's
* result shape can depend on them (`command --json`) — as can whether a
* rendering applies at all, which a renderer signals by returning undefined.
*/
const renderOutcome = (command, result, json, options = {}) => {
var _a;
const rendered = json ? undefined : (_a = renderingFor(command)) === null || _a === void 0 ? void 0 : _a.renderHuman(result, options);
if (rendered !== undefined) {
xvfb.loggerModule.always(rendered);
return;
}
renderResult(result);
};
const renderNativeHelp = (program, command) => {
xvfb.loggerModule.always(program.commands.find((subcommand) => subcommand.name() === command).helpInformation());
};
const sessionBanner = (schema, selection) => {
const { session, candidateCount } = selection;
const target = `Target:
${session.projectRoot}
v${schema.cypressVersion}
pid:${session.pid}`;
if (candidateCount > 1) {
return `${target}\n${candidateCount} running sessions matched; targeting pid ${session.pid}. Pass --session <pid> to target another.`;
}
return target;
};
const renderHelp = (program, schema, command, banner) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const prefix = banner ? `${banner}\n\n` : '';
if (command) {
const subcommand = program.commands.find((sub) => sub.name() === command);
if (!subcommand) {
return yield renderTapFailure(new cli.distExports.UnknownCommandTapError(command, program.helpInformation()));
}
// Standalone help is the only place a schema command's full `details` prose
// renders, so it stands in for the one-line `description` — the same swap
// buildNativeProgram does for CLI-native commands.
const details = (_a = schema.commands.find(({ name }) => name === command)) === null || _a === void 0 ? void 0 : _a.details;
if (details) {
subcommand.description(details);
}
xvfb.loggerModule.always(`${prefix}${subcommand.helpInformation()}`);
return 0;
}
xvfb.loggerModule.always(`${prefix}${program.helpInformation()}`);
return 0;
});
const renderSchemaHelp = (program, schema, selection, command) => {
return renderHelp(program, schema, command, sessionBanner(schema, selection));
};
const renderStaticHelp = (program, schema, command) => {
return renderHelp(program, schema, command);
};
/**
* Authoring helper for one CLI-native tap subcommand. The command's declarative
* schema lives in the shared TAP_NATIVE_COMMANDS contract so it can't drift from
* the help it renders; this pairs that metadata with the CLI-side handler and
* types the handler against the named entry, mirroring the app's defineCommand —
* no annotations, `handler: (options, { spec }) => …`.
*/
const defineNativeCommand = (name, handler) => {
const meta = cli.distExports.TAP_NATIVE_COMMANDS.find((command) => command.name === name);
return Object.assign(Object.assign({}, meta), { name, handler });
};
const NO_SESSIONS_GUIDANCE = `No running ${cli.distExports.TAP_TARGET} found. Start Cypress in open mode (e.g. \`cypress open\`) and select a testing type to get started.`;
// Bounded, and never throws: `sessions` is what a caller reaches for when
// everything else is failing, so an unanswered probe is a reported fact rather
// than an error. Absent means there was nothing to ask, not that it went unasked.
const probeRenderer = (session, timeoutMs) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
if (session.cdpBrowserWsUrl === null) {
return undefined;
}
try {
return yield withTapConnection(session, () => xvfb.__awaiter(void 0, void 0, void 0, function* () { return true; }), timeoutMs);
}
catch (err) {
return isRendererUnresponsive(err) ? false : undefined;
}
});
const listSessions = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const sessions = yield listLiveSessions({ session: options.session });
if (sessions.length === 0) {
renderResult(NO_SESSIONS_GUIDANCE);
return 0;
}
const timeoutMs = (_a = options.timeout) !== null && _a !== void 0 ? _a : FIND_SESSION_TIMEOUT_MS;
const responsive = yield Promise.all(sessions.map((session) => probeRenderer(session, timeoutMs)));
const summaries = sessions.map((session, index) => (Object.assign({ pid: session.pid, projectRoot: session.projectRoot, testingType: session.testingType, browserAttached: session.cdpBrowserWsUrl !== null, browserName: session.browserName, browserSupported: cli.distExports.isTapSupportedBrowser(session.browserFamily) }, (responsive[index] === undefined ? {} : { rendererResponsive: responsive[index] }))));
renderOutcome('sessions', summaries, options.json);
return 0;
});
const sessionsCommand = defineNativeCommand('sessions', listSessions);
// The ways a session can fail to resolve that are lifecycle stages `status`
// reports rather than failures it exits on; anything else really did fail — a
// browser tap cannot drive is not a stage polling can outlast, so it is absent.
// A named `--session` that has gone is one of them, so a poller watching one pid
// keeps reading `not connected` after it exits rather than starting to error.
const NOT_CONNECTED_CODES = new Set(['NO_SESSION', 'SESSION_NOT_FOUND', 'STALE_SESSION']);
const mergeRunState = (base, runState) => {
var _a;
const pinned = runState.pinned ? { pinned: runState.pinned } : {};
if (runState.state === undefined) {
return Object.assign(Object.assign(Object.assign({}, base), { status: 'spec not selected', totalSpecs: runState.totalSpecs }), pinned);
}
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, base), { status: runState.state, totalSpecs: runState.totalSpecs }), (runState.spec !== null ? { spec: runState.spec } : {})), { startedAt: (_a = runState.startedAt) !== null && _a !== void 0 ? _a : null, totalTests: runState.totalTests, results: runState.results }), (runState.error ? { error: runState.error } : {})), pinned);
};
const reportStatus = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let selection;
try {
selection = yield resolveLiveSession({ session: options.session, cwd: process.cwd() });
}
catch (err) {
// No live session is a status a poller waits on, not a failure.
if (cli.distExports.isTapError(err) && NOT_CONNECTED_CODES.has(err.code)) {
renderOutcome('status', { status: 'not connected' }, options.json);
return 0;
}
throw err;
}
const { session } = selection;
const browserAttached = session.cdpBrowserWsUrl !== null;
const base = {
status: 'browser not selected',
pid: session.pid,
projectRoot: session.projectRoot,
testingType: session.testingType,
browserAttached,
browserName: session.browserName,
};
if (!browserAttached) {
renderOutcome('status', base, options.json);
return 0;
}
try {
const outcome = yield withTapConnection(session, (connection) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
return validateExecResult(yield connection.call(cli.distExports.TAP_EXEC_METHOD, ['run-state', {}, {}]));
}), options.timeout);
if ('error' in outcome) {
return yield renderTapFailure(outcome.error);
}
renderOutcome('status', mergeRunState(base, outcome.result), options.json);
return 0;
}
catch (err) {
// A degraded session reached this far (e.g. an unresponsive renderer); the
// earlier catch only covers sessions that never resolved.
return yield renderTapFailure(err);
}
});
const statusCommand = defineNativeCommand('status', reportStatus);
const debug$2 = Debug('cypress:cli:tap');
const GRAPHQL_HOST = '127.0.0.1';
const GRAPHQL_PATH = '/__cypress/graphql';
const DEFAULT_QUERY_TIMEOUT_MS = 4000;
const firstErrorMessage = (envelopeErrors) => {
const first = Array.isArray(envelopeErrors) ? envelopeErrors[0] : undefined;
if (first === undefined) {
return null;
}
return typeof (first === null || first === void 0 ? void 0 : first.message) === 'string' ? first.message : 'The session reported an unnamed GraphQL error.';
};
const validateEnvelope = (operationName, envelope) => {
if (!envelope || typeof envelope !== 'object') {
return throwTapError('GRAPHQL_FAILED', `The session answered ${operationName} with an unrecognizable response.`);
}
const errorMessage = firstErrorMessage(envelope.errors);
if (errorMessage !== null) {
return throwTapError('GRAPHQL_FAILED', `The session failed to run ${operationName}: ${errorMessage}`);
}
if (!envelope.data || typeof envelope.data !== 'object') {
return throwTapError('GRAPHQL_FAILED', `The session answered ${operationName} without data.`);
}
return envelope.data;
};
const querySessionGraphql = (session_1, operation_1, ...args_1) => xvfb.__awaiter(void 0, [session_1, operation_1, ...args_1], void 0, function* (session, operation, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS) {
const { operationName, query, variables } = operation;
const url = `http://${GRAPHQL_HOST}:${session.serverPort}${GRAPHQL_PATH}/${operationName}`;
let response;
try {
response = yield fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', [cli.distExports.SESSION_ID_HEADER]: session.sessionId },
body: JSON.stringify({ operationName, query, variables: variables !== null && variables !== void 0 ? variables : {} }),
signal: AbortSignal.timeout(timeoutMs),
});
}
catch (err) {
debug$2('graphql request %s to pid %d failed: %o', operationName, session.pid, err);
return throwTapError('GRAPHQL_UNREACHABLE', `Could not reach the session to run ${operationName}: ${err.message}`, err);
}
// A valid request passes the server's force-proxy guard untouched; a redirect
// means the guard sent us to the runner page because the session doesn't allow
// direct tap GraphQL — an older Cypress that predates it (or one that rejected
// our session-id). Report that instead of the runner HTML as a data error.
if (response.redirected) {
return throwTapError('SESSION_OUTDATED', `The session redirected the ${operationName} request instead of answering it, so it does not support direct tap GraphQL.`);
}
if (response.status !== 200) {
// express-graphql answers parse/validation failures with 400 + { errors: [...] };
// log it for debugging, but still report unreachable to the user.
const envelope = yield response.json().catch(() => null);
debug$2('graphql request %s to pid %d answered %d: %o', operationName, session.pid, response.status, envelope);
return throwTapError('GRAPHQL_UNREACHABLE', `The session answered ${operationName} with status ${response.status}.`);
}
const envelope = yield response.json().catch((err) => {
return throwTapError('GRAPHQL_FAILED', `The session answered ${operationName} with a non-JSON response.`, err);
});
return validateEnvelope(operationName, envelope);
});
const modifiedAt = (spec) => {
const parsed = spec.lastModifiedTimestamp === undefined ? NaN : Date.parse(spec.lastModifiedTimestamp);
return Number.isNaN(parsed) ? -Infinity : parsed;
};
const byMostRecentlyModified = (a, b) => {
const left = modifiedAt(a);
const right = modifiedAt(b);
return left === right ? 0 : right - left;
};
// `TapSpecsQuery` is the schema shape, but the value crosses the wire unvalidated,
// so entries are guarded against nulls and non-string fields before rendering.
const toSpecList = (data) => {
var _a, _b;
const specs = (_b = (_a = data.currentProject) === null || _a === void 0 ? void 0 : _a.specs) !== null && _b !== void 0 ? _b : [];
return specs
.filter((spec) => typeof (spec === null || spec === void 0 ? void 0 : spec.relative) === 'string')
.map((spec) => {
var _a, _b;
const lastModified = (_a = spec.gitInfo) === null || _a === void 0 ? void 0 : _a.lastModifiedHumanReadable;
const lastModifiedTimestamp = (_b = spec.gitInfo) === null || _b === void 0 ? void 0 : _b.lastModifiedTimestamp;
return Object.assign(Object.assign({ relativePath: spec.relative.replace(/\\/g, '/') }, (typeof lastModified === 'string' ? { lastModified } : {})), (typeof lastModifiedTimestamp === 'string' ? { lastModifiedTimestamp } : {}));
})
.sort(byMostRecentlyModified);
};
const listSpecs = (options) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
const { session } = yield resolveLiveSession({ session: options.session, cwd: process.cwd() });
const data = yield querySessionGraphql(session, cli.distExports.TapSpecsOperation);
renderOutcome('specs', toSpecList(data), options.json);
return 0;
}
catch (err) {
return yield renderTapFailure(err);
}
});
const specsCommand = defineNativeCommand('specs', listSpecs);
const RUN_SPEC_TIMEOUT_MS = 60000;
// The session's runSpec mutation names its failures with its own codes; each maps
// to the tap code whose copy describes it. A code this CLI does not know reads as
// the session failing to start the spec, which is what it observed.
const RUN_SPEC_FAILURES = {
GENERAL_ERROR: 'SPEC_START_FAILED',
NO_PROJECT: 'NO_PROJECT',
NO_SPEC_PATTERN_MATCH: 'SPEC_NOT_FOUND',
SPEC_NOT_FOUND: 'SPEC_NOT_FOUND',
TESTING_TYPE_NOT_CONFIGURED: 'TESTING_TYPE_NOT_CONFIGURED',
};
const findTargetSpec = (session, relative) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const specsData = yield querySessionGraphql(session, cli.distExports.TapSpecsOperation);
const wanted = xvfb.posixify(relative);
return ((_b = (_a = specsData.currentProject) === null || _a === void 0 ? void 0 : _a.specs) !== null && _b !== void 0 ? _b : []).find((spec) => xvfb.posixify(spec.relative) === wanted);
});
const runSpec = (options, args) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
try {
const { session } = yield resolveLiveSession({ session: options.session, cwd: process.cwd() });
const match = yield findTargetSpec(session, args.spec);
if (!match) {
return yield renderTapFailure({ code: 'SPEC_NOT_FOUND', detail: `Looked for "${args.spec}".` });
}
const { runSpec: result } = yield querySessionGraphql(session, cli.distExports.tapRunSpecOperation(match.absolute), RUN_SPEC_TIMEOUT_MS);
if ((result === null || result === void 0 ? void 0 : result.__typename) === 'RunSpecResponse') {
const launched = {
spec: xvfb.posixify(result.spec.relative),
testingType: result.testingType,
browser: result.browser.displayName,
};
renderOutcome('run', launched, options.json);
return 0;
}
const failure = (result === null || result === void 0 ? void 0 : result.__typename) === 'RunSpecError'
? { code: (_a = RUN_SPEC_FAILURES[result.code]) !== null && _a !== void 0 ? _a : 'SPEC_START_FAILED', detail: (_b = result.detailMessage) !== null && _b !== void 0 ? _b : undefined }
: { code: 'SPEC_START_FAILED', detail: `The ${cli.distExports.TAP_TARGET} returned no result for "${args.spec}".` };
return yield renderTapFailure(failure);
}
catch (err) {
return yield renderTapFailure(err);
}
});
const runCommand = defineNativeCommand('run', runSpec);
const debug$1 = Debug('cypress:cli:tap');
// The app names the AUT iframe deterministically (packages/app/src/runner/
// aut-iframe.ts): its name is `Your project: '<name>'`. That prefix is how we
// pick it out of the runner page's frame tree — distinct from the snapshot
// double-buffer frames (`AUT Snapshot - N`) and the spec bridge (`Your Spec`).
const AUT_FRAME_NAME_PREFIX = 'Your project:';
// Three readers reject a malformed selector (the match counter, the DOM reader, and
// the single-node lookup), and the app-side `resolve-selector` reports the same
// condition over the wire — they must all say it the same way.
const invalidSelectorError = (selector) => {
return new cli.distExports.InvalidValueTapError('--selector', 'a valid CSS selector', selector);
};
const findAutFrame = (node) => {
var _a, _b, _c;
if (((_a = node.frame.name) !== null && _a !== void 0 ? _a : '').startsWith(AUT_FRAME_NAME_PREFIX)) {
return { id: node.frame.id, url: (_b = node.frame.url) !== null && _b !== void 0 ? _b : '' };
}
for (const child of (_c = node.childFrames) !== null && _c !== void 0 ? _c : []) {
const found = findAutFrame(child);
if (found) {
return found;
}
}
return undefined;
};
/**
* Locates the app-under-test frame within the runner page. Verified empirically:
* the AUT is a same-process child frame of the runner-page target, so one
* attached connection reaches it — no separate target attach. A pinned snapshot is
* always same-super-domain, so it is reliably this child frame too.
*/
const resolveAutFrame = (client, sessionId) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
yield client.Page.enable({}, sessionId);
// getFrameTree takes no params, so its typed signature is (sessionId?); CRI
// routes the string arg to sessionId by type, not position.
const { frameTree } = yield client.Page.getFrameTree(sessionId);
const found = findAutFrame(frameTree);
if (!found) {
throw new cli.distExports.TapError('NO_AUT');
}
debug$1('resolved AUT frame %o', found);
return { frameId: found.id };
});
/**
* Gates the AUT-frame reads on the same run lifecycle `status` reports. The
* frame is only worth reading once a spec has settled: while a spec is running
* the app is in flux — commands are still executing, snapshots are swapping,
* the page may still be navigating — so a read captures a transient page; and
* short of a verdict, with no run of its own to read, the resolved frame is the
* runner shell or the run this one displaces. Both are rejected with typed
* errors a poller can branch on, mirroring the `status` lifecycle contract
* (wait until `passed`/`failed`).
*/
const assertFrameReadable = (connection) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const outcome = validateExecResult(yield connection.call(cli.distExports.TAP_EXEC_METHOD, ['run-state', {}, {}]));
if ('error' in outcome) {
// The session already named the condition; re-raise it as ours so it renders
// through the one path rather than being reported as a frame read failure.
throw cli.distExports.TapError.fromPayload(outcome.error);
}
const { state, spec } = outcome.result;
if (state === 'running') {
throw new cli.distExports.SpecInProgressTapError(spec);
}
if (state !== 'passed' && state !== 'failed') {
throw new cli.distExports.TapError('SPEC_NOT_STARTED');
}
});
/**
* Shared flow for the AUT-frame commands: resolve a running session, open a
* tap connection, gate on the run lifecycle, locate the AUT frame, run `read`, and
* render the result. Every failure along the way — the read's own, the run-state
* gate's, discovery's, transport's — is a `TapError`, so one catch renders them
* all the way the schema commands render theirs.
*/
const withResolvedAutFrame = (options, read, command) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
const selection = yield resolveSession({ session: options.session, cwd: process.cwd() });
return yield withTapConnection(selection.session, (connection) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
yield assertFrameReadable(connection);
const frame = yield resolveAutFrame(connection.client, connection.sessionId);
const result = yield read(connection, frame);
renderOutcome(command, result, options.json);
// The ambiguity answer is still a result — it names the matches to
// choose between, so it prints on stdout like any other. But it is not
// the read that was asked for, and the exit code has to say so.
return result.ambiguous ? 1 : 0;
}), options.timeout);
}
catch (err) {
return yield renderTapFailure(err);
}
});
const WHOLE_NUMBER = /^\d+$/;
const parseWholeNumber = (raw) => {
if (typeof raw !== 'string' || !WHOLE_NUMBER.test(raw)) {
return undefined;
}
const value = Number(raw);
return Number.isSafeInteger(value) ? value : undefined;
};
const parseIndex = (raw) => {
if (raw === undefined) {
return undefined;
}
const value = parseWholeNumber(raw);
if (value === undefined) {
throw new cli.distExports.InvalidValueTapError('--at', 'a whole number, 0 or greater', raw);
}
return value;
};
const parsePositiveInt = (raw, label) => {
const value = parseWholeNumber(raw);
if (value === undefined || value <= 0) {
throw new cli.distExports.InvalidValueTapError(`--${label}`, 'a positive integer', raw);
}
return value;
};
// A separate JS context that shares the frame's DOM but not its globals, so
// nothing the tap reads pollutes the page.
const TAP_WORLD_NAME = 'cypress-tap';
const createFrameIsolatedWorld = (connection, frame) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { client, sessionId } = connection;
const { executionContextId } = yield client.Page.createIsolatedWorld({
frameId: frame.frameId,
worldName: TAP_WORLD_NAME,
}, sessionId);
return executionContextId;
});
/**
* Resolves a CSS selector to the matched element's CDP objectId, querying in an
* isolated world on the AUT frame. `index` picks one of several matches
* (`--at`), already checked against the match count by the caller. Throws
* `INVALID_VALUE` when the selector is malformed; returns undefined when the
* selector is valid but nothing matched.
*/
const querySelectorObjectId = (connection, frame, selector, index) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { client, sessionId } = connection;
const executionContextId = yield createFrameIsolatedWorld(connection, frame);
const { result, exceptionDetails } = yield client.Runtime.callFunctionOn({
functionDeclaration: 'function (selector, index) { return document.querySelectorAll(selector)[index] }',
executionContextId,
arguments: [{ value: selector }, { value: index }],
}, sessionId);
if (exceptionDetails) {
throw invalidSelectorError(selector);
}
// querySelector returned null — a real "nothing matched" answer, not an error.
if (!result.objectId || result.subtype === 'null') {
return undefined;
}
return result.objectId;
});
/**
* The subset of a node's boolean accessibility properties present in `reported`
* and currently true — the states worth surfacing; the rest are rarely
* actionable.
*/
const collectTrueStates = (properties, reported) => {
return (properties !== null && properties !== void 0 ? properties : [])
.filter((property) => { var _a; return reported.has(property.name) && ((_a = property.value) === null || _a === void 0 ? void 0 : _a.value) === true; })
.map((property) => property.name);
};
/**
* Functions injected into the AUT frame over CDP `Runtime.callFunctionOn`,
* serialized with `fn.toString()`. They read the app-under-test's live DOM, so
* they run in the browser, not in this Node process — two constraints follow
* and MUST hold, or the injected copy diverges from this (tested) source:
*
* 1. Fully self-contained. A function may reference only its own parameters
* and browser globals (`document`, `getComputedStyle`, `Math`); never an
* import or module-scope binding, which does not exist in the AUT frame.
* (This is why `readElementInfo` takes the style list as an argument
* rather than closing over a constant.)
* 2. ES2015 syntax only. The shipped bundle targets es2016 (tsconfig.build),
* where object spread / async / optional chaining lower to `tslib` helpers
* that reference module scope — those would be `undefined` once injected.
* `const`/`let`, template literals, `for…of`, and `Array.from` all emit
* verbatim at es2016, so keep to those.
*
* Because they touch only a few DOM globals, the unit tests stub those globals
* and call the real functions directly.
*/
// Caps output, so a heavy page never serializes megabytes across CDP. Returns a
// tagged object rather than throwing, so a bad selector round-trips as data
// instead of a CDP exception. `index` picks one of several matches (`--at`); the
// caller has already checked it against the match count.
function readDom(selector, maxChars, index) {
let el;
try {
el = document.querySelectorAll(selector)[index];
}
catch (_e) {
return { invalidSelector: true };
}
if (!el) {
return { found: false };
}
const html = el.outerHTML;
return html.length > maxChars ? { found: true, html: html.slice(0, maxChars), truncated: true } : { found: true, html };
}
// Counts matches without serializing any of them, so the single-element guard
// costs one number however heavy the page. Tags a bad selector as data, the way
// `readDom` does.
function countMatches(selector) {
try {
return { count: document.querySelectorAll(selector).length };
}
catch (_e) {
return { invalidSelector: true };
}
}
// Reads the element's tag, attributes, curated computed styles, and box rect in
// one call on the element itself (bound as `this` via the CDP objectId) —
// avoiding the DOM/CSS node-id dance (`requestNode` needs a fetched document
// tree and is brittle across worlds). `reportedStyles` is the curated property
// list, passed in to keep this function self-contained (see file header).
function readElementInfo(reportedStyles) {
const computed = getComputedStyle(this);
const styles = {};
// Report every curated property verbatim rather than filtering: the list is
// already hand-picked, and a truthiness guard would drop meaningful values
// (a resolved `0`, an empty `content`) and make the output non-deterministic.
for (const prop of reportedStyles) {
styles[prop] = computed.getPropertyValue(prop);
}
const attributes = {};
for (const attr of Array.from(this.attributes)) {
attributes[attr.name] = attr.value;
}
const rect = this.getBoundingClientRect();
return {
tag: this.tagName.toLowerCase(),
attributes,
styles,
box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}
/**
* A unique selector for each match, to offer in place of the ambiguous one.
* Best effort: these come from the session itself, which derives them the way
* its Selector Playground does — so they honor any selectorPriority the project
* configured, and are selectors the user's own tests would use. A session that
* can't reach its app under test (a secondary origin inside `cy.origin`) just
* leaves the match count to speak for itself.
*/
const disambiguatingSelectors = (connection, selector) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
try {
const outcome = validateExecResult(yield connection.call(cli.distExports.TAP_EXEC_METHOD, ['resolve-selector', { selector }, {}]));
return 'error' in outcome ? [] : outcome.result.selectors;
}
catch (_a) {
return [];
}
});
const resolveAmbiguity = (connection, frame, selector, at) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const { client, sessionId } = connection;
const executionContextId = yield createFrameIsolatedWorld(connection, frame);
const { result, exceptionDetails } = yield client.Runtime.callFunctionOn({
functionDeclaration: countMatches.toString(),
executionContextId,
arguments: [{ value: selector }],
returnByValue: true,
}, sessionId);
if (exceptionDetails) {
throw new cli.distExports.TapError('FRAME_READ_FAILED', { message: `resolving "${selector}" in the app under test failed: ${((_a = exceptionDetails.exception) === null || _a === void 0 ? void 0 : _a.description) || exceptionDetails.text}` });
}
const { count, invalidSelector } = result.value;
if (invalidSelector) {
throw invalidSelectorError(selector);
}
// Nothing matched: each command reports that in its own shape, and an `--at`
// has no range to be out of.
if (count === undefined || count === 0) {
return undefined;
}
if (at !== undefined) {
if (at >= count) {
throw new cli.distExports.InvalidValueTapError('--at', `0 to ${count - 1}, since "${selector}" matched ${count} element${count === 1 ? '' : 's'}`, at);
}
return undefined;
}
if (count === 1) {
return undefined;
}
return {
ambiguous: true,
selector,
count,
selectors: yield disambiguatingSelectors(connection, selector),
};
});
const withAmbiguous = (connection, frame, selector, at, read) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const ambiguous = yield resolveAmbiguity(connection, frame, selector, at);
return ambiguous !== null && ambiguous !== void 0 ? ambiguous : read();
});
const extractDom = (connection, frame, selector, maxChars, at) => withAmbiguous(connection, frame, selector, at, () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const { client, sessionId } = connection;
const executionContextId = yield createFrameIsolatedWorld(connection, frame);
const { result, exceptionDetails } = yield client.Runtime.callFunctionOn({
functionDeclaration: readDom.toString(),
executionContextId,
arguments: [{ value: selector }, { value: maxChars }, { value: at !== null && at !== void 0 ? at : 0 }],
returnByValue: true,
}, sessionId);
if (exceptionDetails) {
throw new cli.distExports.TapError('FRAME_READ_FAILED', { message: `reading the app-under-test DOM failed: ${((_a = exceptionDetails.exception) === null || _a === void 0 ? void 0 : _a.description) || exceptionDetails.text}` });
}
const value = result.value;
// Only a selector the reader was given can come back rejected.
if (value.invalidSelector) {
throw invalidSelectorError(selector);
}
return Object.assign(Object.assign(Object.assign({}, (value.found !== undefined ? { found: value.found } : {})), (value.html !== undefined ? { html: value.html } : {})), (value.truncated ? { truncated: true } : {}));
}));
// The options are read before a session is resolved, so a value this command
// cannot use is reported as itself rather than as whatever the search for a
// Cypress to run it against happened to find.
const domCommand = defineNativeCommand('dom', (options, _args, commandOptions) => {
const maxChars = parsePositiveInt(commandOptions['max-chars'], 'max-chars');
const at = parseIndex(commandOptions.at);
return withResolvedAutFrame(options, (connection, frame) => {
return extractDom(connection, frame, commandOptions.selector, maxChars, at);
}, 'dom');
});
// Structural/text roles carry no semantic signal on their own — dropping them
// yields the compact role/name tree DevTools shows, not the raw render tree.
const NOISE_ROLES = new Set(['InlineTextBox', 'StaticText', 'LineBreak', 'generic', 'none', 'GenericContainer', 'paragraph']);
// The boolean states worth reporting when true; the rest are rarely actionable.
const REPORTED_STATES$1 = new Set(['focused', 'disabled', 'required', 'invalid', 'checked', 'expanded', 'selected', 'pressed', 'readonly', 'hidden', 'modal', 'busy']);
const projectNode = (node, depth) => {
var _a, _b, _c;
const role = (_a = node.role) === null || _a === void 0 ? void 0 : _a.value;
if (typeof role !== 'string' || node.ignored || NOISE_ROLES.has(role)) {
return undefined;
}
const out = { depth, role };
const name = (_b = node.name) === null || _b === void 0 ? void 0 : _b.value;
if (typeof name === 'string' && name.length > 0) {
out.name = name;
}
const value = (_c = node.value) === null || _c === void 0 ? void 0 : _c.value;
if (value !== undefined && value !== null && value !== '') {
out.value = String(value);
}
const states = collectTrueStates(node.properties, REPORTED_STATES$1);
if (states.length) {
out.states = states;
}
return out;
};
/**
* Walks the AX forest from `rootId` depth-first. A node that projects to a
* meaningful role deepens the indent for its descendants; a noise node is
* skipped but its children keep flowing, so the output is a clean semantic
* tree rather than the raw render tree. Stops at `maxNodes`.
*/
const projectTree = (byId, rootId, maxNodes) => {
const nodes = [];
let truncated = false;
const walk = (id, depth) => {
var _a;
if (truncated) {
return;
}
const node = byId.get(id);
if (!node) {
return;
}
const projected = projectNode(node, depth);
if (projected) {
if (nodes.length >= maxNodes) {
truncated = true;
return;
}
nodes.push(projected);
}
const childDepth = projected ? depth + 1 : depth;
for (const childId of (_a = node.childIds) !== null && _a !== void 0 ? _a : []) {
walk(childId, childDepth);
}
};
walk(rootId, 0);
return { nodes, truncated };
};
const resolveSelectorBackendNodeId = (connection, frame, selector, index) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const { client, sessionId } = connection;
const objectId = yield querySelectorObjectId(connection, frame, selector, index);
if (objectId === undefined) {
return undefined;
}
const { node } = yield client.DOM.describeNode({ objectId }, sessionId);
return node.backendNodeId;
});
const extractAria = (connection, frame, selector, maxNodes, at) => {
// Ahead of the tree fetch: an ambiguous selector shouldn't cost a full AX tree.
return withAmbiguous(connection, frame, selector, at, () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const { client, sessionId } = connection;
yield client.DOM.enable({}, sessionId);
yield client.Accessibility.enable(sessionId);
const { nodes: axNodes } = yield client.Accessibility.getFullAXTree({ frameId: frame.frameId }, sessionId);
const byId = new Map();
for (const node of axNodes) {
byId.set(node.nodeId, node);
}
const base = { nodes: [], nodeCount: 0 };
const backendNodeId = yield resolveSelectorBackendNodeId(connection, frame, selector, at !== null && at !== void 0 ? at : 0);
if (backendNodeId === undefined) {
// Selector matched nothing, or the match is absent from the a11y tree.
return base;
}
const rootId = (_a = axNodes.find((node) => node.backendDOMNodeId === backendNodeId)) === null || _a === void 0 ? void 0 : _a.nodeId;
if (rootId === undefined) {
return base;
}
const { nodes, truncated } = projectTree(byId, rootId, maxNodes);
return Object.assign(Object.assign(Object.assign({}, base), { nodes, nodeCount: nodes.length }), (truncated ? { truncated: true } : {}));
}));
};
const ariaCommand = defineNativeCommand('aria', (options, _args, commandOptions) => {
const maxNodes = parsePositiveInt(commandOptions['max-nodes'], 'max-nodes');
const at = parseIndex(commandOptions.at);
return withResolvedAutFrame(options, (connection, frame) => {
return extractAria(connection, frame, commandOptions.selector, maxNodes, at);
}, 'aria');
});
// A full computed style is ~350 properties; this curated set answers the
// "why does it look/behave this way" questions (layout, visibility, box).
const REPORTED_STYLES = [
'display', 'visibility', 'opacity', 'position', 'top', 'right', 'bottom', 'left',
'width', 'height', 'margin', 'padding', 'border', 'box-sizing',
'color', 'background-color', 'font-size', 'font-weight', 'line-height', 'text-align',
'z-index', 'overflow', 'pointer-events', 'cursor',
];
const REPORTED_STATES = new Set(['focused', 'disabled', 'required', 'invalid', 'checked', 'expanded', 'selected', 'pressed', 'readonly', 'hidden']);
const projectAria = (node) => {
var _a, _b;
if (!node || node.ignored) {
return undefined;
}
const aria = {};
const role = (_a = node.role) === null || _a === void 0 ? void 0 : _a.value;
if (typeof role === 'string') {
aria.role = role;
}
const name = (_b = node.name) === null || _b === void 0 ? void 0 : _b.value;
if (typeof name === 'string' && name.length > 0) {
aria.name = name;
}
const states = collectTrueStates(node.properties, REPORTED_STATES);
if (states.length) {
aria.states = states;
}
return Object.keys(aria).length ? aria : undefined;
};
const readAriaNode = (connection, objectId) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const { client, sessionId } = connection;
try {
const { node } = yield client.DOM.describeNode({ objectId }, sessionId);
const { nodes } = yield client.Accessibility.getPartialAXTree({ backendNodeId: node.backendNodeId, fetchRelatives: false }, sessionId);
const axNodes = nodes;
return projectAria((_a = axNodes.find((candidate) => candidate.backendDOMNodeId === node.backendNodeId)) !== null && _a !== void 0 ? _a : axNodes[0]);
}
catch (err) {
if (isRendererUnresponsive(err)) {
throw err;
}
return undefined;
}
});
const extractInspect = (connection, frame, selector, at) => withAmbiguous(connection, frame, selector, at, () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
var _a;
const { client, sessionId } = connection;
yield client.DOM.enable({}, sessionId);
yield client.Accessibility.enable(sessionId);
const base = { selector, found: false };
const objectId = yield querySelectorObjectId(connection, frame, selector, at !== null && at !== void 0 ? at : 0);
if (!objectId) {
return base;
}
const info = yield client.Runtime.callFunctionOn({
functionDeclaration: readElementInfo.toString(),
objectId,
arguments: [{ value: REPORTED_STYLES }],
returnByValue: true,
}, sessionId);
if (info.exceptionDetails) {
throw new cli.distExports.TapError('FRAME_READ_FAILED', { message: `inspecting the element failed: ${((_a = info.exceptionDetails.exception) === null || _a === void 0 ? void 0 : _a.description) || info.exceptionDetails.text}` });
}
const { tag, attributes, styles, box } = info.result.value;
const aria = yield readAriaNode(connection, objectId);
return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { found: true, tag,
attributes }), (aria ? { aria } : {})), { box,
styles });
}));
const inspectCommand = defineNativeCommand('inspect', (options, _args, commandOptions) => {
const at = parseIndex(commandOptions.at);
return withResolvedAutFrame(options, (connection, frame) => {
return extractInspect(connection, frame, commandOptions.selector, at);
}, 'inspect');
});
/**
* The tap subcommands implemented entirely in the CLI, in the order they
* appear in help output — ahead of the commands the running session's
* schema advertises.
*/
const tapCliCommands = [sessionsCommand, statusCommand, specsCommand, runCommand, domCommand, ariaCommand, inspectCommand];
const argumentsOf = (params) => {
return params.map(({ name, required }) => required ? `<${name}>` : `[${name}]`).join(' ');
};
const argumentDescriptions = (params) => {
return Object.fromEntries(params.map(({ name, description }) => [name, description]));
};
const JSON_DESCRIPTION = 'print the raw JSON result instead of the human-readable rendering';
// commander wraps the option and command descriptions it renders but prints a
// program description verbatim, so this one is wrapped to the same width by hand.
const PROGRAM_DESCRIPTION = `Discover, control, and query an open-mode Cypress session from the command
line. Programmatically run specs, review results, and inspect the
application-under-test through the full test lifecycle.`;
// commander both applies a declared default and appends `(default: …)` to the
// help it generates, which is the whole reason a schema carries one. Its types
// cap the value at a string, but it stores and renders whatever it is handed,
// and a number has to arrive as one to render as 30000 rather than "30000".
const declareOption = (command, flags, description, defaultValue) => {
command.option(flags, description, defaultValue);
};
// Every tap command accepts `--session`, `--json` and `--timeout`; all are
// consumed by the top-level `cypress tap` command before a subprogram parses, so
// declaring them on a command is purely so they render in its generated help.
// `--timeout` keeps no alias: `-t` is worth more to `--test-id`, which is typed
// far more often, and the shared flags have to spell the same on every command.
const declareSharedOptions = (command, jsonDescription) => {
command.option('-s, --session <pid>', 'target a local Cypress session by its process id (PID)');
command.option('--json', jsonDescription);
declareOption(command, '--timeout <ms>', 'how long to wait on any single call into the Cypress session, in milliseconds', DEFAULT_CDP_TIMEOUT_MS);
};
const declareOptions = (command, options) => {
var _a;
// A command that declares `--json` in its schema does so to receive it, not to
// add a second flag — it is declared below with the ones every command shares,
// so the schema contributes only its description.
const json = options.find(({ name }) => name === 'json');
for (const option of options.filter((candidate) => candidate !== json)) {
const { name, alias, type, required, description } = option;
const lead = alias ? `-${alias}, ` : '';
const flags = type === 'boolean' ? `${lead}--${name}` : `${lead}--${name} <${name}>`;
if (required && type !== 'boolean') {
command.requiredOption(flags, description);
}
else {
declareOption(command, flags, description, option.defaultValue);
}
}
declareSharedOptions(command, (_a = json === null || json === void 0 ? void 0 : json.description) !== null && _a !== void 0 ? _a : JSON_DESCRIPTION);
};
const forwardedArgs = (params, args) => {
const forwarded = {};
args.forEach((arg, index) => {
const param = params[index];
if (param) {
forwarded[param.name] = arg;
}
});
return forwarded;
};
const answerUnknownOption = (command) => {
command.unknownOption = function (flag) {
if (this._allowUnknownOption) {
return;
}
throw new cli.distExports.UnknownOptionTapError(flag);
};
};
const answerUnknownCommand = (program) => {
program.unknownCommand = function () {
throw new cli.distExports.UnknownCommandTapError(this.args[0]);
};
};
// The flag as the reader would have typed it: every tap option declares a long
// form, and the raw flags stand in for anything that somehow does not.
const flagOf = (option) => option.long || option.flags;
/**
* The three ways commander finds an invocation short of what the command declares.
* A missing positional is reported for all of them at once — commander announces
* only the first, while the reader is better served by the whole list — hence the
* declared params rather than the name commander passed, which stands in only if
* nothing else can be matched to it.
*/
const answerMissingInput = (command, name, params) => {
const handlers = command;
handlers.missingArgument = function (missing) {
const absent = params.filter(({ required }, index) => required && this.args[index] === undefined).map((param) => param.name);
throw new cli.distExports.MissingArgumentsTapError(name, absent.length ? absent : [missing]);
};
handlers.optionMissingArgument = function (option) {
throw new cli.distExports.TapError('INVALID_OPTIONS', { detail: `"${name}" was given the ${flagOf(option)} option without a value.` });
};
handlers.missingMandatoryOptionValue = function (option) {
throw new cli.distExports.MissingOptionTapError(name, flagOf(option).replace(/^--/, ''));
};
};
const rejectExcessArguments = (name, params, args) => {
if (args.length <= params.length) {
return;
}
const given = args.length === 1 ? '1 argument was' : `${args.length} arguments were`;
const takes = params.length === 0 ? 'takes none' : `takes at most ${params.length}`;
throw new cli.distExports.TapError('INVALID_ARGUMENTS', { detail: `${given} passed to "${name}", but it ${takes}.` });
};
const forwardedOptions = (command, options) => {
const opts = command.opts();
const declared = command.options;
const forwarded = {};
// commander stores `--dry-run` under opts().dryRun, so resolve each schema
// name to its declared option's attribute key rather than assuming they match
for (const { name } of options) {
const option = declared.find((declaredOption) => declaredOption.long === `--${name}`);
const value = option && opts[option.attributeName()];
if (value !== undefined) {
forwarded[name] = String(value);
}
}
return forwarded;
};
const declareCommand = (program, spec, dispatch) => {
const { name, description, params = [], options = [] } = spec;
const command = program.command(name);
// Keep option values in opts() so schema names such as `command` cannot
// collide with Command instance methods.
command.storeOptionsAsProperties(false);
if (params.length) {
command.arguments(argumentsOf(params));
command.description(description, argumentDescriptions(params));
}
else {
command.description(description);
}
declareOptions(command, options);
answerUnknownOption(command);
answerMissingInput(command, name, params);
if (dispatch) {
command.action(() => {
rejectExcessArguments(name, params, command.args);
return dispatch(name, forwardedArgs(params, command.args), forwardedOptions(command, options));
});
}
};
const newProgram = () => {
const program = new commander.Command('cypress tap');
program.exitOverride();
program.addHelpCommand(false);
program.description(PROGRAM_DESCRIPTION);
program.usage('[command] [args...] [options]');
answerUnknownOption(program);
answerUnknownCommand(program);
answerMissingInput(program, program.name(), []);
return program;
};
const buildTapProgram = (schema, dispatch) => {
const program = newProgram();
// The outer `tap` command owns --session/--json/--timeout and parses them
// before this program runs, so they never reach here — declared only so help
// lists them. The outer command disables its own help, making this the sole
// place they surface.
declareSharedOptions(program, JSON_DESCRIPTION);
for (const native of tapCliCommands) {
declareCommand(program, native);
}
// An older session may still advertise a command the CLI has since made
// native; start() dispatches natives before ever consulting the schema, so
// skip the shadowed advertisement rather than registering a duplicate.
const nativeNames = new Set(tapCliCommands.map(({ name }) => name));
for (const command of schema.commands.filter(({ name, hidden }) => !hidden && !nativeNames.has(name))) {
declareCommand(program, command, dispatch);
}
return program;
};
// A one-command program used by exec/tap.ts to parse a single CLI-native
// command, so its positionals and options validate through the same commander
// grammar the schema commands use.
const buildNativeProgram = (native, dispatch) => {
var _a;
const program = newProgram();
// A native command's standalone help is rendered only here, so its full
// `details` prose stands in for the one-line `description` commander prints
// between the usage line and the generated Arguments/Options sections.
declareCommand(program, Object.assign(Object.assign({}, native), { description: (_a = native.details) !== null && _a !== void 0 ? _a : native.description }), dispatch);
return program;
};
// A name this CLI does not know is whatever the user typed, so it is reported
// as unknown rather than sent verbatim.
const getKnownCommand = (command) => cli.distExports.KNOWN_COMMANDS.has(command) ? command : 'unknown';
/**
* What an invocation reports before it runs: the command it named, and the flags
* `cypress tap` handles itself rather than passing to a command. The flags a
* command declares are reported once commander has parsed them, by noteTapCommand.
*/
const reportedInvocation = (command, wantsHelp, options) => ({
command: command ? getKnownCommand(command) : undefined,
flags: [
...wantsHelp ? ['help'] : [],
...Object.keys(options),
],
});
const debug = Debug('cypress:cli:tap');
// The failures that mean no session was reachable to ask. Help is answerable
// without one, so these — and only these — lose to a help invocation.
const DISCOVERY_CODES = new Set([
'NO_SESSION',
'SESSION_NOT_FOUND',
'STALE_SESSION',
'NO_BROWSER_ATTACHED',
'UNSUPPORTED_BROWSER',
'RENDERER_UNRESPONSIVE',
]);
const INVALID_USAGE = 'INVALID_USAGE';
const validateSchema = (value) => {
const schema = value;
if (!schema || typeof schema !== 'object' || typeof schema.schemaVersion !== 'number' || !Array.isArray(schema.commands)) {
return throwTapError('PROTOCOL_MISMATCH', `${cli.distExports.TAP_SCHEMA_METHOD} returned an unrecognizable schema.`);
}
if (schema.schemaVersion !== cli.distExports.TAP_SCHEMA_VERSION) {
throw new cli.distExports.VersionSkewTapError({
sessionSchema: schema.schemaVersion,
cliSchema: cli.distExports.TAP_SCHEMA_VERSION,
sessionCypress: schema.cypressVersion,
cliCypress: xvfb.util.pkgVersion(),
});
}
return schema;
};
const isHelpFlag = (arg) => arg === '--help' || arg === '-h';
const buildCommandInfo = (operands) => {
const wantsHelp = operands.some(isHelpFlag);
const positionals = operands.filter((arg) => !isHelpFlag(arg));
const command = positionals[0];
return { wantsHelp, positionals, command };
};
// `--json` is parsed by the outer `cypress tap` command, so commander never
// routes it to the command being run. A command that declares it in its own
// schema needs it anyway — for that command the flag also changes what the
// session returns, not just how the CLI prints it — so it is handed over here.
const withJson = (schema, name, options, json) => {
var _a;
const declared = (_a = schema.commands.find((command) => command.name === name)) === null || _a === void 0 ? void 0 : _a.options.some((option) => option.name === 'json');
return json && declared ? Object.assign(Object.assign({}, options), { json: 'true' }) : options;
};
const runNativeCommand = (native, positionals, options, wantsHelp) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let dispatchCode;
const program = buildNativeProgram(native, (name, args, commandOptions) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
noteTapCommand(name, args, commandOptions);
dispatchCode = yield native.handler(options, args, commandOptions);
}));
if (wantsHelp) {
renderNativeHelp(program, native.name);
return 0;
}
try {
yield program.parseAsync(positionals, { from: 'user' });
}
catch (err) {
if (err instanceof commander.CommanderError) {
noteTapFailure(INVALID_USAGE);
return 1;
}
// A command that reads its own options before resolving a session — as the
// AUT readers do, so a bad value is answered as itself — raises outside the
// flow that renders the rest of its failures.
return yield renderTapFailure(err, helpFor(program, native.name));
}
return dispatchCode !== null && dispatchCode !== void 0 ? dispatchCode : 1;
});
const execCommand = (connection, command, commandArgs, commandOptions, json, help) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const outcome = validateExecResult(yield connection.call(cli.distExports.TAP_EXEC_METHOD, [command, commandArgs, commandOptions]));
if ('error' in outcome) {
return yield renderTapFailure(outcome.error, help);
}
renderOutcome(command, outcome.result, json, commandOptions);
return 0;
});
// With no session to query, fall back to the schema this CLI ships with so the
// help listing still reflects every command the CLI knows — the query path stays
// authoritative when a session is attached (it may run a different version).
const renderKnownSchema = (command) => {
const schema = cli.distExports.buildTapSchema(xvfb.util.pkgVersion());
const program = buildTapProgram(schema, () => { });
return renderStaticHelp(program, schema, command);
};
const runTap = (_a, options_1) => xvfb.__awaiter(void 0, [_a, options_1], void 0, function* ({ wantsHelp, positionals, command }, options) {
const native = tapCliCommands.find(({ name }) => name === command);
if (native) {
return runNativeCommand(native, positionals, options, wantsHelp);
}
try {
const selection = yield resolveSession({ session: options.session, cwd: process.cwd() });
return yield withTapConnection(selection.session, (connection) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const schema = validateSchema(yield connection.call(cli.distExports.TAP_SCHEMA_METHOD));
let dispatchCode = 0;
const program = buildTapProgram(schema, (name, args, commandOptions) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
noteTapCommand(name, args, commandOptions);
dispatchCode = yield execCommand(connection, name, args, withJson(schema, name, commandOptions, options.json), options.json, helpFor(program, name));
}));
if (wantsHelp || !command) {
return renderSchemaHelp(program, schema, selection, command);
}
try {
yield program.parseAsync(positionals, { from: 'user' });
}
catch (err) {
if (err instanceof commander.CommanderError) {
noteTapFailure(INVALID_USAGE);
return 1;
}
// A name or flag commander could not place is answered here, where the
// program that knows the real ones is still in scope to list them.
if (cli.distExports.isTapError(err)) {
return yield renderTapFailure(err, helpFor(program, command));
}
throw err;
}
return dispatchCode;
}), options.timeout);
}
catch (err) {
// Help is answerable without a session, so a discovery failure falls back
// to the schema the CLI ships with rather than reporting the failure.
if (cli.distExports.isTapError(err) && DISCOVERY_CODES.has(err.code) && (wantsHelp || !command)) {
return yield renderKnownSchema(command);
}
debug('tap %s failed: %s %s', command || '(help)', err.code, err.message);
return yield renderTapFailure(err);
}
});
const tapModule = {
start() {
return xvfb.__awaiter(this, arguments, void 0, function* (operands = [], options = {}) {
debug('tap invocation %o with options %o', operands, options);
const info = buildCommandInfo(operands);
let exitCode = 1;
beginTapTrace(reportedInvocation(info.command, info.wantsHelp, options));
// The CLI exits the moment this returns, so the trace is reported before it
// does rather than left in flight.
try {
exitCode = yield runTap(info, options);
return exitCode;
}
finally {
yield reportTapTrace(exitCode);
}
});
},
};
exports.default = tapModule;