chore: update help content - #34572
Conversation
* feat: add server-side runner instance discovery and cypress cache utilities Server-side producer half of runner instance discovery (#13627): - write the discovery record on project open + clear on teardown - serve the liveness probe route (instanceId echo) via server-base - publish/clear the browser CDP ws URL on the cri client - extract resolveCypressCacheRoot into util/cypress-cache and reuse it from the bundles cache_root resolver Split out from the original instance-discovery branch so the CLI consumer can land as a separate PR. * Apply suggestions from code review Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com> * refactor: address review on runner instance discovery - rename discovery dir 'runners' -> 'instances' - rename runnerDiscovery.write -> captureRecord - remove CYPRESS_INTERNAL_RUNNER_DISCOVERY kill switch - gate discovery (record write + probe route) to open mode only - document instanceId vs pid and the atomic temp-write * refactor: rename runner discovery to runner instances per review Addresses naming-overload feedback: discovery/capture/record collided with service discovery, protocol capture, and spec recording. Renames the module, its API, the record type, the probe route, and debug namespace to runner instances. Also trims an over-reaching test comment. - runnerDiscovery -> runnerInstances - captureRecord -> addInstance - RunnerDiscoveryRecord -> RunnerInstance - /__cypress/runner-discovery/ -> /__cypress/runner-instances/
* feat: add CLI-side runner discovery consumer CLI consumer half of runner instance discovery (#13627): - read discovery records from the cypress cache (store) - verify runner liveness via the instanceId probe (liveness) - expose findLiveRunner/findReadyRunner + record types (index, record) - prune dead discovery records during `cypress cache prune` Sits on top of the server-side producer; split out for a smaller PR. * chore: address PR review feedback on runner discovery - reword NO_DISCOVERY_FILE/STALE_DISCOVERY_FILE/NO_BROWSER_ATTACHED errors to reference open mode and opening a browser - tidy store debug messages - document the cross-process record contract shared between the server producer and CLI consumer * fix: prune must spare the renamed instances/ discovery dir cache prune deletes any cache subdir not in EXTERNAL_CACHE_ENTRIES, which still listed 'runners' after the discovery dir was renamed to instances/ — so prune would wipe live discovery records while Cypress is open. Source the dir name from runner-discovery (INSTANCES_DIRNAME) so the allowlist can't drift from the actual directory again. * feat: add @packages/runner-discovery shared contract The record schema, on-disk layout (instances/ dir, <pid>.json names), and the liveness-probe route were hand-mirrored in the server producer and the CLI consumer, guarded only by "MUST stay in sync" comments. Extract them into a single pure, dependency-free package both sides can share. Kept deliberately free of fs/http/runtime deps so the CLI can bundle it via Rollup without taking on any new published dependency, while server and CLI agree on the contract by construction. Register it in the generated path map. * refactor: consume shared runner-discovery contract in server Drop the producer's duplicated record interfaces, schema version, and instances/ dir name in favor of @packages/runner-discovery, and build record paths from the shared recordFileName helper. Route the probe registration and the proxy-bypass check through the shared RUNNER_DISCOVERY_ROUTE_PREFIX so the on-disk and HTTP contract live in one place. * refactor: consume shared runner-discovery contract in CLI Re-export the record schema, validator, dir/filename, and probe-path helpers from @packages/runner-discovery instead of re-deriving them; record.ts keeps only RunnerDiscoveryError, which is consumer-side error reporting rather than part of the on-disk contract. store.ts and liveness.ts now use the shared parseRecordPid and runnerDiscoveryProbePath, dropping their local copies. The dependency is internal and bundled into the CLI via Rollup (and stripped from the published package.json by prepare-package-json), so the published package gains no new runtime dependency. * Apply suggestion from @davidr-cy * Apply suggestions from code review Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com> * refactor: move runner-discovery path layout into the shared package The `<root>/instances` dir and `<root>/instances/<pid>.json` record path are layout facts both sides depend on, so add runnerDiscoveryDir(cacheRoot) and recordPath(cacheRoot, pid) to @packages/runner-discovery. Each side keeps its own zero-arg wrapper that binds its cache root — resolution stays per-side since the server (chdir-anchored) and CLI (postinstall-hook aware) resolve it differently and pulling that in would taint the otherwise-pure package. * chore: drop unused runner-discovery re-exports from CLI barrel * refactor: rename runner-instances to cypress-instances Converge the instance-discovery contract on 'instance'/'cypress-instances' terminology so the code matches the user-facing 'Cypress instance' strings and stops overloading 'runner' (@packages/runner, the runner UI, driver). Renames the shared package, CLI dir, server producer, the HTTP probe route (/__cypress/instances/:instanceId), and all Runner* identifiers to their Instance/CypressInstance equivalents. Also reword the STALE instance error. Addresses review feedback from @mschile on #34168. * refactor: use fetch for the instance liveness probe Swap core http.get for global fetch with an AbortSignal timeout. Verified on Node 20.1.0 (the CLI's minimum supported version): no ExperimentalWarning, all probe paths (echo match, 404, timeout, refused connection) behave identically, and the process still exits promptly. * chore: drop spurious ts-ignore in cache spec The identical mockResolvedValueOnce(undefined) call compiles unsuppressed on develop; the directive was added on this branch by mistake.
… (3) (#34159) * feat: mount the schema-driven TapManager binding on the runner window Adds the browser-side TapManager binding that the cypress tap CLI drives over CDP: the schema-advertising binding mounted on the runner window, the command registry, the exec-args coercion layer, the shared contract, and its component/e2e coverage. * chore: trim verbose doc comments in tap binding module * refactor: extract shared field coercion in tap exec-args * docs: document TapManager getSchema and exec public methods * fix: coalesce null exec args to keep the tap envelope intact Default params only fill undefined, so a CDP caller passing null for args/options threw out of Object.keys before exec could build a TapExecResult. Coalesce both to {} so exec always resolves to the envelope, and cover it with a regression test. * fix: guard tap exec payloads and snapshot the advertised schema A non-object args/options payload (number, boolean, array) slipped past Object.keys with no keys and silently validated, so no-param commands honored garbage input. Guard the wire payloads at exec: nullish is treated as absent, a primitive or array is rejected as INVALID_ARGUMENTS. getSchema also handed out the registry's own param/option arrays, so a caller mutating the returned schema could alter later validation. Return a snapshot of the arrays and their elements instead. * refactor: address tap review feedback on exec envelope and schema - nest exec failures as { error: { code, message } } and split the failure codes into UNKNOWN_COMMAND / INVALID_PAYLOAD / INVALID_ARGUMENTS / INVALID_OPTIONS with a documented map - rename protocolVersion to schemaVersion and keep it out of public cli/types as an internal negotiation signal - rename asWireRecord to normalizePayload - remove the placeholder health command and its tests - align exec-args validation messages * chore: ignore the not-yet-consumed defineCommand export in knip * refactor: move the tap binding alongside the runner instead of under it * Apply suggestion from @davidr-cy
…4) (#34180) * refactor: generalize runner discovery to list and resolve live runners * refactor: drop project filter from tap runner discovery The tap CLI no longer exposes project as a discovery option; runner selection now filters only by pid, with cwd kept as an internal tiebreak. * refactor: trim doc comments from tap runner discovery * Update index.ts * refactor: split live/ready instance resolution with browser-aware selection Introduce resolveLiveInstance (browser-optional) alongside resolveInstance (browser-required), sharing a liveMatches helper for the read/probe/error path and a generic selectInstance for the only/explicit/cwd-match/arbitrary choice. resolveInstance now filters the live set down to browser-attached (ready) instances *before* selecting, so a browserless instance can no longer shadow a ready one that could serve the command — e.g. a cwd-rooted instance with no browser previously won the cwd-match and threw NO_BROWSER_ATTACHED even though another live instance had a browser open. NO_BROWSER_ATTACHED is raised only when no live instance has a browser; candidateCount reflects the pool actually selected from. resolveLiveInstance keeps selecting over every live instance for `status`, which reports instances that have no browser attached yet. Addresses mschile review feedback on #34180.
* feat: add the tap CDP transport and frozen binding contract * refactor: remove comments from tap CDP transport files * refactor: dedupe tap CDP transport helpers and drop unused error codes Extract a shared evaluateBinding helper for the repeated window binding lookup and a throwCdpError helper for the duplicated CDP_UNREACHABLE throw. Remove the INVALID_SCHEMA, INVALID_EXEC_RESULT, and UNSUPPORTED_PROTOCOL error codes that the transport never raises; they belong to the higher protocol-validation layer. * Update tap-session.ts * refactor: map tap transport failures to known cypress errors Replace the TapTransportError class + string codes with throwTapError, so each failure carries a user-facing errors.tap* description/solution. Adds a public error catalog snapshot test. Also hardens the CDP session against mid-flight context loss: - callBindingWithRetry now retries the binding *resolve* (Runtime.evaluate), not just callFunctionOn, so a context destroyed during resolution recovers once and surfaces STALE_HANDLE instead of a raw CRI error. - findRunnerPageSession detaches a probed session even when the probe throws, no longer leaking a flattened CDP session. * fix: map evaluate transport rejections during binding resolve to CDP_UNREACHABLE resolveBindingObjectId only handled Runtime.evaluate resolving with exceptionDetails; a transport-level rejection (e.g. a dropped CDP socket) escaped raw, so the user saw an uncatalogued error with no known/details. Now non-stale, non-session-gone evaluate rejections map to tapCdpUnreachable, matching the callFunctionOn path, while stale/session-gone errors still propagate raw so the retry and re-attach paths keep working. * fix: type resolve binding via Awaited<evaluateBinding> and drop stray import The previous commit annotated `evaluated` as Protocol.Runtime.EvaluateResponse without importing the Protocol namespace, and a stray vitest import had crept in. Use Awaited<ReturnType<typeof evaluateBinding>> (matching the existing pattern for the call response) so the type is self-contained. * test: add the new tap errors to the errors snapshot This slice adds the tap* error codes (tapCdpUnreachable, tapBindingNotFound, tapBindingThrew, tapStaleHandle, tapInvalidMethod); record them in the errors enumeration snapshot so the cli unit suite passes. * chore: ignore not-yet-consumed tap exports in knip contract.ts and tap-session.ts export the frozen binding contract and session helpers that later tap-cli slices consume; mirror the runner-discovery pattern and ignore their exports/types so the health-check (knip) check passes. * fix: map a repeat session-gone during the tap retry to the known stale-handle error * refactor: adopt the cypress-instances discovery API in the tap session * refactor: replace the tap CDP error regexes with shared message constants * refactor: drop the tap method-name validation by passing the method as a CDP argument * fix: use instance terminology in the tap errors and drop the unreachable version hint
…#34182) * feat: build the tap subcommand program from the advertised schema * chore: trim inline comments from tap build-program * fix: reassert catchable unknown-option rejection for the tap program The root CLI (cli/lib/cli.ts) patches Command.prototype.unknownOption to call process.exit, which defeats the tap program's exitOverride() and would kill the process on an unknown flag instead of routing the error through the orchestrator. Override unknownOption per tap command to throw a catchable CommanderError, and cover both that case and excess args on a no-param command. * refactor: simplify tap program parse-error handling per review - Route the CLI-native `instances` command through rejectExcessArguments so excess operands throw a catchable commander.excessArguments error, matching the schema-built no-param commands. - Drop the rejectUnknownOptions reassert (reverts the workaround from 4bc222c). It monkey-patched each command's unknownOption to undo cli.ts's global prototype patch, but buildTapProgram has no consumer yet and every other parse error already routes through the inherited exitOverride. An unknown flag now prints help + exits 1, consistent with the rest of the cypress CLI; catchable routing can be decided when tap is wired in. Remove the mirror test that only validated the reassert; the native unknown-option test still passes on commander's own exitOverride path. * fix: tolerate a tap schema command that omits params * fix: forward dashed tap option names past commander's camelCasing
…ion (7) (#34148) * feat: connect the cypress tap CLI to the runner binding over CDP * chore: remove inline comments from the cli handshake * refactor: simplify tap CLI help routing and defer its module load * fix: clarify the tap protocol-mismatch error message Addresses review feedback on PR #34148: the error now states the running Cypress is newer than this CLI, alongside the update guidance. * fix: address tap CLI protocol and discovery-error feedback Addresses review feedback on PR #34148: - Protocol mismatch is now direction-aware: a runner newer than the CLI tells the user to update the CLI; an older runner tells them to update the running Cypress (the inline detail was never user-visible, so this needed a second mapped error, tapOutdatedProtocol). - Bare `cypress tap` only falls back to generic help for NO_DISCOVERY_FILE; NO_BROWSER_ATTACHED and STALE_DISCOVERY_FILE now surface their specific, actionable messages instead of being swallowed. * refactor: adopt the cypress-instances discovery API in the tap CLI * test: cover the handshake tap errors in the error catalog spec * test: add the handshake tap errors to the error inventory snapshot * fix: share the tap contract so the CLI and app cannot drift The CLI declared its own tap contract (protocolVersion, an { ok } exec envelope) that had to be hand-kept in sync with the app-side binding (schemaVersion, a { result }/{ error } envelope). They drifted: every real handshake failed validateSchema with "the running Cypress returned a tap schema this CLI does not recognize", because the app emits schemaVersion while the CLI read protocolVersion. Move the contract into @packages/cypress-instances as the single source of truth both sides import, delete the duplicated cli/lib/tap/contract.ts, and align validateSchema/validateExecResult to it. cypress tap, tap --help, and tap instances now complete the handshake against a running Cypress. * fix: reject a malformed tap exec error envelope before rendering it validateExecResult accepted any object carrying an `error` key, so a `{ error: null }` (or otherwise malformed) envelope slipped through and renderFailure crashed reading code/message off it. Validate the failure branch the same way execCommand dispatches on it. * Update packages/cypress-instances/lib/tap-contract.ts
# Conflicts: # packages/server/lib/browsers/browser-cri-client.ts
* refactor: scaffold the per-command tap registry Restructure the monolithic commands.ts into a commands/ module: the defineCommand authoring helper and TapCommandError move to definition.ts, the health command to health.ts, and the registry itself to index.ts. The TapManager gains the domain-failure (TapCommandError) catch and exec-args coercion gets its own spec. This is the foundation the per-command modules (specs, run, tests, commands) register into. * refactor: trim verbose comments in the tap command registry * refactor: move the shared tap contract into @packages/runner-discovery * fix: import the tap contract from source so Vite resolves its exports The app re-exported the shared tap contract from the compiled CJS build (`@packages/runner-discovery/dist/tap-contract`). Because the workspace package resolves through a symlink to a realpath outside node_modules, Vite's commonjs transform skips it during the app build and Rollup treats the CJS file as ESM, failing with "TAP_PROTOCOL_VERSION is not exported". Re-export from the zero-dependency TypeScript source (`lib/tap-contract`) instead, matching how the app consumes every other sibling package. The CLI keeps reading the same contract from the package's CJS build. * chore: track the tap knip ignores with the registry restructure * chore: sort the cypress-instances dep and knip-ignore the scaffold defineCommand export The registry scaffold ships an empty tapCommands map, so defineCommand has no consumer until the tap-specs slice; ignore its unused-export until then. Also re-sort @packages/cypress-instances into the app dependencies alphabetically.
* feat: add the specs tap command listing runnable specs Adds the specs command, which returns the run-mode spec list (project- relative path + type) the running Cypress can run. Introduces the shared run-mode-specs reader/mapper and registers specs in the command registry, plus e2e coverage for the listing. * Apply suggestion from @davidr-cy * fix: correct grammar in the tap specs command description * Apply suggestion from @davidr-cy * chore: bump circle cache version to bust poisoned node-modules cache * refactor: derive SpecListEntry specType from FoundSpec * refactor: rename the run-mode-specs module to specs-list with getRunnableSpecs * refactor: rename the spec list entry relative field to relativePath
…0) (#34193) * feat: add the run tap command triggering a spec via the runner URL Adds the run command, which navigates the runner to a project-relative spec (a tapRun nonce makes a rerun of the active spec re-trigger) and returns the started spec entry; an empty path or no match surfaces as a domain failure (INVALID_SPEC / SPEC_NOT_FOUND). Registers run and adds schema + e2e coverage for triggering and rerunning. * refactor: trim verbose comments in the tap run command * refactor: rename the misleading assign stub to setHash in the tap run spec * fix: derive the tapRun nonce from the hash so reruns survive page reloads * refactor: drop the fire-and-forget comment from the tap run command * refactor: parse the tapRun hash without regexes
…4194) * feat: add the tests tap command reading the run's test state Adds the tests command: with no argument it lists the active run's tests (lean id/title/duration/state/retries); with a test id it details that one test (full title path, per-phase timings, latest error). Types the driver's serialized test state via @packages/types and introduces the shared test-state reader. NO_RUN / TEST_NOT_FOUND surface as domain failures. Registers tests with e2e coverage. * refactor: trim verbose comments in the tap tests command * chore: ignore the test-state seam types in knip * fix: report never-run tests as skipped and harden the tap test detail A state-less serialized test was never reached — the driver marks it.skip 'pending' explicitly, so default to 'skipped' like the driver's own run summary. Narrow error props at runtime instead of casting (non-Error throws can carry anything), type the stable SerializedTest fields, and JSON-clone timings so the detail returns a snapshot instead of a live reference into the driver's test object. * refactor: trim comments and take a typed test in serializeTestDetail * feat: default an unreached test to pending mid-run, skipped once complete * refactor: drop the unneeded bind on the tap runner getTestsState seam getTestsState closes over runner-local state and never uses `this`, so the `.bind(runner)` was unnecessary. * docs: add a tap package AGENTS.md requiring unmocked e2e contract coverage Documents the tap binding layout and the rule that every command contract must be validated end-to-end in tap-binding.cy.ts against the real runner, since the stub-based component tests cannot catch Cypress internals drifting. * fix: re-program the tap runner stub instead of double-stubbing in one test * refactor: move the runnable allowlists to constants and name the timings clone * refactor: consolidate the tap window-global seams into TapManagerDataSource * docs: point the tap AGENTS.md at the TapManagerDataSource seam * refactor: add a tap types module and move the TapTestsRunner seam type into it * refactor: move the tap test wire types to the tap types module * refactor: move SpecListEntry to the tap types module * refactor: replace the getTestsState '__never__' sentinel with getAllTestsState * test: cover the runner getAllTestsState serialization in the driver spec * fix: serialize the suite title path in getAllTestsState * refactor: rename TapManagerDataSource to tap-manager-data-source * docs: document every tap wire entry field and trim flagged comments
…34195) * feat: add the commands tap command listing a test's command log Adds the commands command, which returns the command-log entries (id, name, message, state, type) of a given test in the active run — empty for a known test that has not run, NO_RUN / TEST_NOT_FOUND as domain failures. Extends the shared test-state module with the command-log serializer and wires the command through the CLI. Also fixes the e2e binding spec, which the spec->specs rename missed: it still drove exec('spec'). * refactor: trim verbose comments in the tap commands command * refactor: extract the getTestsState sentinel lookup into one helper * feat: add getTestState to the driver runner for single-test lookup * refactor: read a single test via getTestState in the tap detail and commands * feat: select a retried test's attempt via --attempt on tap tests and commands Adds a 1-based --attempt option (attempt 1 = first run, defaults to the latest) to the tests detail and commands tap commands, selecting which of a retried test's attempts to read. Each attempt carries its own command log, state, error and timings via prevAttempts; identity fields stay sourced from the test so a past attempt still resolves its full title. Out-of-range or non-integer attempts fail as ATTEMPT_NOT_FOUND, as does --attempt in the tests list mode. Covers the contract unmocked in tap-binding.cy.ts with a new retrying fixture spec. * refactor: stub getAllTestsState in the attempt-option dispatch test * fix: serialize the suite title path in getTestState and fix the seam import * refactor: type the serialized command log and relocate CommandEntry to the tap types * fix: expect INVALID_OPTIONS when the required test option is missing * fix: drop command fields nulled by the driver memory cleanup from the wire * feat: flag memory-evicted command log entries as cleanedUp * fix: move the retrying fixture to its own project to keep the shared spec count * fix: clarify the single-attempt ATTEMPT_NOT_FOUND message and drop the command-log hyphen * chore: remove docs
…le (13) (#34196) * docs: plan for the tap status command * docs: drop the testing-type-not-selected stage from the status plan * feat: add the run-state tap binding command * feat: add the CLI-native tap status command * refactor: extract reportStatus into its own tap module Move the CLI-native status command (reportStatus and its helpers) out of the tapModule file into cli/lib/tap/status.ts. The shared exec-envelope validator moves to tap-session.ts so both tap.ts and status.ts import it one-directionally, avoiding a cycle. * chore: remove tap status plan doc and trim inline comments * fix: port the tap status feature to the throwTapError error model Reconcile the status command with the rebased foundation: validateExecResult moves to tap-session (ported to throwTapError/errors), status.ts and its specs drop TapTransportError for the .known/.details convention, --project is gone from discovery, and testingType is read directly now that it is on the record. * refactor: trim verbose comments in the tap status command * feat: add a hidden flag to the tap schema to drop run-state from the CLI listing * refactor: use string templates over array.join in tap output * fix: treat hidden tap commands as unknown in schema help renderSchemaHelp crashed with a TypeError for `cypress tap run-state --help`: the schema advertises hidden commands but buildTapProgram skips them, so the program lookup dereferenced undefined. Also drops the redundant testingType passthrough status test. * refactor: reuse the getAllTests helper in aggregateResults * chore: ignore the run-state result type in knip * fix: reject a malformed tap exec error envelope in the moved validator This slice relocates validateExecResult into tap-session.ts; carry the same hardening as the introducing slice so a `{ error: null }` (or otherwise malformed) envelope can't reach renderFailure. * fix: count never-run tests as pending mid-run, skipped once complete in the tap status rollup * refactor: read run-state tests through getAllTestsState * fix: import the renamed tap seam module in run-state * refactor: move the tap status types to a doc-stringed types file and trim comments * refactor: gather the CLI-native tap commands into a registry-driven commands folder * fix: key the run-state lifecycle on run completion so an unsettled run stays running * fix: surface the binding's error envelope from tap status instead of mislabeling it invalid * fix: assert the real exec envelope in run-state tests and route active-spec through the single seam The run-state component tests wrapped the payload in a nonexistent `ok: true` field; `TapManager.exec` resolves as `{ result }` or `{ error }`, so the assertions would fail against the real binding. Drop the wrapper to match the sibling command tests. `getActiveSpecRelative` also read `window.getEventManager` directly, splitting the runner-window globals across two seams. Move it onto `tapManagerDataSource`, the documented single seam, and add real `exec('run-state')` coverage to tap-binding.cy.ts (no run and post-run) per the package's e2e contract rule. * fix: reject excess arguments for CLI-native tap commands on the real dispatch path Native commands (`status`, `instances`) short-circuit in exec/tap.ts before buildTapProgram ever parses, so the excess-argument rejection registered on the program's native-command actions never ran for real invocations — `cypress tap status extra` silently ignored `extra`. Move the rejection onto the native branch in exec/tap.ts, reusing the same `rejectExcessArguments` helper the schema path uses so both paths behave identically (commander 6.2.1 does not reject excess args by default). The program's native-command registration is now help-listing only. Relocate the coverage from build-program.spec.ts (which exercised the unreachable program action) to exec/tap.spec.ts, which drives the real tap.start path. * refactor: derive the generic tap help's command list from the CLI command registry
* fix: tap --help must not require an attached browser (13990) `cypress tap --help`, `cypress tap <command> --help`, and a bare `cypress tap` surfaced the discovery error (e.g. NO_BROWSER_ATTACHED) when an instance was running without a test browser, instead of printing help. Help must never require a live instance or attached browser, so fall back to the static command listing on any discovery failure when help is requested. * Apply suggestion from @davidr-cy
* feat: add CLI-native tap frame commands (dom, aria, inspect)
The frame commands read the app-under-test frame directly over CDP
(Page/DOM/CSS/Accessibility), which the in-page tap binding cannot
reach. They are parsed and dispatched CLI-side rather than through the
schema-driven binding program:
- dom read the AUT DOM, whole-page or by selector
- aria read the accessibility (ARIA) tree, or a subtree at a selector
- inspect inspect one element: attributes, computed styles, box model,
and accessibility node
Exposes raw CDP access (client + live sessionId) on TapSession so the
extractors can run protocol domains against the runner page and its AUT
child frame, and resolves the AUT frame by its deterministic name prefix.
* fix: consolidate util functions, make typed cdp functions
* refactor: generate tap native-command help from structured fields
Each CLI-native tap command hand-authored a `usage` string that restated
its `description`, `params`, and `options` a second time in prose, so the
two drifted whenever a default or option changed. Drop the strings and
render per-command help through commander's generated help — the same path
the schema-discovered commands already use.
The only content commander can't derive, the descriptive blurb, moves to a
structured `details` field. `--instance` is now declared once so it renders
uniformly in every command's help (schema commands previously omitted it).
* chore: bump CI cache version to force cache rebuild
* refactor: consolidate tap AUT frame files under lib/tap/aut
Move aut-frame, frame-cdp, and frame-scripts into a single lib/tap/aut/
folder as frame.ts, cdp.ts, and scripts.ts, dropping the redundant prefixes.
Update the frame command imports and specs accordingly.
* test: split tap AUT frame spec by command domain
Extract extractDom, extractAria, and extractInspect coverage into
dom.spec.ts, aria.spec.ts, and inspect.spec.ts. frame.spec.ts now
covers only the aut/frame.ts helpers (resolveAutFrame, parsePositiveInt).
* Apply suggestion from @davidr-cy
* refactor: address tap frame help wording + style reporting review
- help text describes behavior/output, not transport internals (drop
"over CDP", "tap binding answers a liveness probe")
- dom/inspect help clarifies subtree/first-match semantics
- inspect reports every curated computed style verbatim so 0-valued and
empty-but-significant styles are not silently dropped
- add cli/lib/tap/AGENTS.md guarding user-facing help against internals
* feat: add the pin tap command to make a past command's DOM live The pin command pins a past command's DOM snapshot as the live AUT frame, so the CLI-native frame commands (dom/aria/inspect) can read the state of the app at that point in the run rather than only the current DOM. Pinning from outside the reporter (the tap CLI) is reflected in the runner UI the way a user click would be: the command is highlighted and its test opened in the command log. A stale pin from a previous run is auto-released so status never reports one that no longer exists, and run-state now emits the pinned reference so a pin is always visible and a stranded one is recoverable. * refactor: consolidate tap command helpers and the runner-window seam - move non-command helpers (test-state, specs-list) out of tap/commands/ to the package top level, so commands/ holds only command modules - fold the pin binding's runner-window access (event manager, AUT iframe, snapshot store) into tapManagerDataSource, the single documented seam; delete snapshot-pin.ts and move its interfaces to types.ts - run-state now reconciles via getSnapshotRunner() rather than casting the test-state runner, which lacked getSnapshotPropsForLog - pin can switch to a different command without --clear; validation runs before any mutation, so a failed switch leaves the existing pin intact * fix: hear every app-side unpin and never restore a stale pin's DOM The pin's external-unpin listener rode the reporterBus reporter:snapshot:unpinned event, which only the AUT overlay's ✕ emits — unpinning by clicking the pinned command in the reporter (which funnels through runner:unpin:snapshot → _unpinSnapshot) was never heard, leaving the captured DOM unrestored and the tap pin state phantom. Listen on the localBus unpin:snapshot event instead: the one signal every app-side unpin funnels through. The listener also outlives a re-run until a tap command reconciles, so an unpin fired in the new run could restore the dead run's captured DOM over the live AUT. Reconcile the pin inside the handler and drop a stale pin without restoring. * chore: drop the orphaned knip entry for the folded snapshot-pin module * test: cover the pin lifecycle against the real tap binding AGENTS.md requires every tap command's happy path and failure codes to be proven unmocked in tap-binding.cy.ts; pin had only stubbed component coverage. Adds a pin-target fixture to the dedicated tap project whose click mutates the page, so a pinned before-snapshot is visibly different from the live DOM, and covers: the pin/move/clear lifecycle with exact result shapes, run-state's pinned field appearing and clearing, the reachable failure codes (PIN_TARGET_REQUIRED, NO_RUN, TEST_NOT_FOUND, COMMAND_NOT_FOUND, SNAPSHOT_NOT_FOUND), and both app-side unpin paths (the unpin control and the reporter command click) restoring the captured DOM. RUN_IN_PROGRESS, SNAPSHOT_UNAVAILABLE, and NO_AUT are not deterministically reachable against a real runner and stay component- tested. * refactor: strip the comments from the pin command * fix: omit an unverifiable pin from run-state while the runner is being replaced * fix: never restore a pin's captured DOM unless the pin is verified live * refactor: resolve a numeric --at without the regex gate * test: re-read the live AUT document when asserting pin status * fix: emit POSIX spec paths from the tap contract on Windows
* feat: report the pinned command in tap status The run-state binding emits the currently pinned command; status now surfaces it so a pin (see the pin command) is visible from the CLI and a stranded one is recoverable. Ignore the shared PinnedRef type in knip (used only across the run-state/status contract, mirrored app-side). * chore: force artifacts for highest in stack * chore: bump ci cache version to evict poisoned node_modules cache The 07-21-2026 cache key had a corrupt node_modules artifact saved under it, causing the build job to fail with "Should have found globbed node_modules to unpack". CircleCI caches are immutable, so reruns keep restoring the bad cache; bumping the version forces a fresh cache. * chore: exclude pin-status branch from force-persist-artifacts guard * Apply suggestions from code review Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com>
…hed (#34334) * feat: show the CLI's own tap command schema when no instance is attached Hoist the tap command metadata into the shared @packages/cypress-instances contract (TAP_COMMANDS + buildTapSchema) so it is the single source both the running instance and the CLI build a schema from. The app keeps the handlers and sources each command's metadata from the contract; the CLI stamps the contract with its own version to render help with no instance running, while the live getSchema query stays authoritative when an instance is attached. * refactor: source CLI-native tap command schemas from the shared contract Lift the instances/status/dom/aria/inspect schemas out of each command file into TAP_NATIVE_COMMANDS in @packages/cypress-instances, paired with handlers via a new defineNativeCommand helper (mirroring the app's defineCommand). This keeps every tap command's declarative shape in one place so it can't drift from the help it renders. Convert TAP_COMMANDS and TAP_NATIVE_COMMANDS from keyed objects to arrays so their ordering in help is explicit array position rather than relying on object key-iteration order; name unions derive from the arrays. defineCommand keeps precise per-handler typing via an Extract lookup. * refactor: rename renderOfflineHelp to renderKnownSchema * fix: list native commands in the tap unknown-command message renderHelp derived the available-commands list and the known/unknown decision from schema.commands, which holds only the schema-driven commands. An unknown or mistyped command (e.g. `cypress tap instancs --help`) therefore reported a command set missing the CLI-native commands (instances, status, dom, aria, inspect). Drive both off program.commands, which registers every visible command — native and schema — so the known-schema help always lists both. Hidden commands stay excluded (they are never declared on the program).
* fix: verify the browser CDP endpoint before reporting it attached in tap discovery (13753) The server clears cdpBrowserWsUrl only on the browser process exit, so a closed browser window whose process lingers leaves the url stale and tap reports browserAttached: true. Confirm the browser's DevTools endpoint (/json/version) is reachable before trusting the url. * feat: report testing type and guide when empty in tap instances (13753) Drop the internal serverPort from the instances output, add the testingType (e2e/component/null), and print guidance on how to start Cypress instead of an empty array when nothing is reachable. * Apply suggestion from @davidr-cy
* feat: add tap graphql error catalog entries * feat: add a direct GraphQL transport for tap instance queries * feat: make tap specs CLI-native, reading live specs over instance GraphQL * feat: let direct tap GraphQL requests through the force-proxy guard * feat: enrich CLI-native tap specs with git last-modified Extend the TapSpecs GraphQL query to fetch `gitInfo.lastModifiedHumanReadable` and `gitInfo.lastModifiedTimestamp`, echoing them as `lastModified` (friendly, e.g. "3 days ago") and `lastModifiedTimestamp` (raw commit time) on each spec entry. Each is omitted when git has no info (e.g. untracked specs) and a non-string wire value is dropped rather than rendered. Mirrors the enrichment on the browser-side 13936 branch, now over the CLI's direct-GraphQL path — so `tap specs` reports last-modified without a browser attached. * refactor: reap dead instance records on read listLiveInstances/liveMatches now read via readLiveInstances, which prunes dead-pid leftovers as it reads so they stop masquerading as stale (alive-but-unresponsive) instances on later commands. * feat: type tap GraphQL operations from the schema via codegen Move the tap specs query into @packages/cypress-instances as a plain string (plucked via the /* GraphQL */ magic comment) and add a codegen target that validates it against the data-context schema and emits its result type. The CLI transport infers the response type from the operation descriptor, so the wire query and its type share one schema-validated source with no graphql runtime in the CLI bundle. Also refreshes the errors snapshot for the tapGraphql* entries. * fix: make dead-record reaping best-effort so it can't abort discovery An undeletable stale record (permissions, a Windows file lock) rejected the readLiveInstances Promise.all, blocking discovery of the other live instances. reapIfDead now swallows the fs.remove failure and still reports the record dead, so it's excluded from the live list without aborting. * fix: posixify tap spec paths and drop browser hint from NO_INSTANCE tap specs now posixifies each spec's relative path (GraphQL relative is OS-native, so backslash-separated on Windows) so it matches the POSIX paths run/status speak. The shared NO_INSTANCE/STALE_INSTANCE guidance no longer tells the user to open a browser, since specs/status resolve before a browser is attached; browser-requiring commands still raise NO_BROWSER_ATTACHED once an instance is found. * Apply suggestion from @davidr-cy * Apply suggestion from @davidr-cy * Apply suggestion from @davidr-cy * fix: gate direct tap GraphQL bypass behind an instance-id header The force-proxy allowlist let any non-proxied localhost request reach the mutation-capable /__cypress/graphql route, exposing it to cross-site CSRF. Require callers of that bypass to echo the running instance id via the x-cypress-instance-id header, which a cross-origin page can neither read nor attach to a simple request. Proxied app traffic is unaffected. * fix: surface GraphQL error envelopes returned with a non-200 status express-graphql answers parse/validation failures (e.g. schema skew) with a 400 and a { errors: [...] } body. Treating any non-200 as tapGraphqlUnreachable dropped those messages and wrongly implied the instance had closed. Read the envelope on a non-200 and, when it carries a GraphQL error, report it as tapGraphqlFailed with the message; fall back to unreachable only when there is no usable envelope. * refactor: debug-log the non-200 GraphQL envelope instead of surfacing it Keep the original behavior of reporting a non-200 as tapGraphqlUnreachable, but log the response envelope (which carries express-graphql's error messages) under the tap debug namespace so schema-skew and validation failures are diagnosable without changing the user-facing error. * fix: report a redirected tap GraphQL request as an unsupported instance When the server's force-proxy guard denies the direct GraphQL bypass (an older instance that predates it, or a rejected instance-id), it redirects to the runner page. fetch followed that to a 200 HTML body, which surfaced as a generic tapGraphqlFailed non-JSON error. Detect the redirect and report tapOutdatedProtocol so the real compatibility failure is clear.
…tation (#34367) * feat: make the tap run command CLI-native over the runSpec GraphQL mutation Port run off the CDP binding to the direct GraphQL transport, like specs. The CLI resolves a live instance, absolutizes the project-relative spec against the instance's project root, and POSTs the runSpec mutation — which launches the browser and switches to the spec's testing type as needed, then returns without waiting. A RunSpecError is surfaced with the instance's own code and message so the CLI reports the same reason the app would; the trigger no longer depends on surviving in-browser state. Moves run from TAP_COMMANDS to TAP_NATIVE_COMMANDS and removes the app-side run handler, its cy spec, the now-orphaned specs-list serializer, and the hash seam. The tap-binding e2e triggers runs via cy.visitApp instead of the binding. run-state/tests/commands/pin stay on the binding: the live run lifecycle has no GraphQL surface (results flow browser->reporter over the socket), so only the trigger can move today. * feat: resolve the tap run spec from the instance's own spec list Instead of resolving the project-relative input against the instance's project root CLI-side, run now fetches the TapSpecs list (which newly selects each spec's absolute path), matches the input against the posixified relative path, and sends that spec's instance-reported absolute path to the runSpec mutation — no CLI-side path math. A miss fails fast with SPEC_NOT_FOUND (the schema's own RunSpecErrorCode value, so the CLI reports the same code the server would) and never sends the mutation, mirroring the matching semantics of the removed app-side run handler. * fix: shadow a schema-advertised command that collides with a CLI-native one An older instance may still advertise a command the CLI has since made native (run, today); buildTapProgram registered both, duplicating the commander command. start() dispatches natives before ever consulting the schema, so skip the shadowed advertisement instead. Also updates build-program.spec.ts for run's move to the native commands: its fixture's schema-forwarded run (missed when the move landed, breaking unit-tests in CI) becomes the neutral launch. * fix: give the tap runSpec mutation a launch-sized timeout The runSpec mutation can launch a browser and switch testing types before it answers, so the transport's default 4s query timeout could abort the request — reporting the instance unreachable while the run actually starts. Send it with a 60s timeout instead. * fix: restart the runner when runSpec targets the already-active spec * feat: type tap command handlers from the shared contract on both sides * fix: keep the wire guards when matching the tap run spec * refactor: trust the typed graphql envelope when matching the tap run spec
…ration Conflicts were both "keep both sides": - routes.ts: develop's isProxyDisabled import alongside the instances route imports. - server-base.ts: develop moved the non-proxied-request guard behind isProxyDisabled and isTrustedInternalLoopback early returns and relocated trimmedUrl. Kept that structure, with the tap bypass (isAllowedProxyBypass) in place of the bare ALLOWED_PROXY_BYPASS_URLS.includes check so instance and tap GraphQL routes still bypass.
…34380) * feat: surface network request info on tap commands (14048) tap commands rows for network-instrumented logs (cy.intercept registrations, proxied requests, cy.request) now carry the same high-level detail the reporter renders inline: method, url, status/indicator, stubbed flag, response count, and alias. Route registrations merge into the command list in creation order, and the row message mirrors the reporter's display text (e.g. GET 200 /api). Non-network rows are unchanged; new fields stay absent, not null. * feat: add the reporter tap command with human-readable CLI rendering tap reporter renders everything the open-mode reporter shows for one test: a header, the ROUTES table, hook sections (BEFORE EACH / TEST BODY, derived from the test's timings), the command log with its display-level fields (hookId, displayName, event, group nesting, network detail), and — when the attempt failed — the error panel with its code frame. Rows follow the reporter's conventions: bold command names, dash-prefixed child commands (-assert), asserts colored by state, and the app's own palette (variables.scss) throughout. The result contract lives in the new cypress-instances lib/contracts/ dir, typed by both sides. The CLI gains the rendering seam every command will grow into: a command definition may declare renderHuman, printed by default, with --json bypassing it for the raw result. reporter is the first adopter. * feat: mirror the app reporter's numbering and instrument panels in tap output Command handles are now the exact numbers the app reporter shows (the per-hook-section counter from hook-model), with unnumbered event/system rows taking an attempt-wide e1..eN rendered a step more faded. Route registrations aren't commands and carry no id; the driver's raw log-<origin>-N id no longer crosses the wire at all — pin resolves handles app-side, preferring the test body for a duplicated number, accepting a <hookId>:<n> qualifier (surfaced in each section title, e.g. BEFORE EACH · h1), and refusing to guess between hooks with AMBIGUOUS_COMMAND. The reporter view also gains the UI's remaining instrument panels: SESSIONS (derived from command logs carrying sessionInfo — sessions are not a driver instrument) and SPIES / STUBS (from the attempt's agents bucket), plus alias detail on rows — aliases/aliasType for a row's own badge, referencedAliases to color @name mentions in messages — matching the reporter's tag palette (dom indigo, everything else purple). Failed event rows (uncaught exceptions) now take the failure red. * fix: give the exec tap rendering fixture the instrument panel collections * fix: order the tap test body in run order and strip truecolor ansi in tests serializeReporterHooks appended the synthesized test-body pseudo-hook after every timing-derived hook, so after each/after all landed before test body and broke the run-order contract; splice it between the before and after hooks instead. The render-reporter snapshot helper's hand-rolled ansi strip only matched basic SGR codes, leaving chalk.hex truecolor escapes on chalk level 2+ terminals; use the strip-ansi package the other cli specs already rely on. * chore: bump circle cache version to rebuild poisoned node_modules cache * fix: give tap pin an --attempt option so it targets the right attempt Command ids restart from 1 on every attempt, but pin always resolved the id against the latest attempt and had no way to select another. An id copied from `commands`/`reporter --attempt N` could therefore match a different command on the latest attempt and silently pin the wrong snapshot — where the old unique `log-*` ids failed loudly with COMMAND_NOT_FOUND. pin now accepts --attempt (mirroring commands/reporter), resolves the id against that attempt (defaulting to the latest), and folds the attempt into the pin's identity so re-pinning the same id on another attempt starts a fresh pin instead of moving the existing one. * fix: guard non-string tap error fields and render empty route counts as - serializeTestError kept name/message/stack for any non-nullish value once it moved to omitNullish, so a non-Error throw (`throw { message: 42 }`) could put a non-string message on the wire; the CLI renderer then crashed on `message.split`. Keep each field only when it is actually a string. The ROUTES # column rendered a missing/zero response count as `0`; match the app reporter (and this file's SPIES/STUBS CALLS column) by showing `-` instead. * Apply suggestion from @davidr-cy
…o --test (#34381) * feat: render the spec-level reporter overview when tap reporter has no --test * feat: render a tap command's details prose in its standalone help Adds an optional `details` field to the tap command schema and swaps it in for the one-line `description` when rendering `<command> --help`, matching what buildNativeProgram already does for CLI-native commands. The reporter command uses it to explain both of its modes. Also updates the cypress-instances contract spec for the reporter's now-optional --test. * feat: advertise a tap command's details prose in the instance schema TapManager.getSchema hand-builds the advertised schema from the command registry and was dropping the new details field, so an attached CLI fell back to the one-line description. Carry it through, and assert every command's details round-trips in the getSchema spec. * feat: flatten the tap spec overview's suites into joined-title sections
* feat: Guard DOM instrospection mid run on tap * feat: Update RUN_IN_PROGRESS error messages to use a shared constant Refactor the error handling in the tap command to utilize the new TAP_RUN_IN_PROGRESS_MESSAGE constant for consistency. This change updates the error messages thrown during a running spec to provide clearer guidance to users. Additionally, tests have been updated to reflect the new message format.
* feat: add the tap element-selectors binding command Adds a hidden `element-selectors` command to the tap binding: given a CSS selector it returns a selector unique to each element that matched, each carrying the index of the match it names, derived through the driver's own Cypress.ElementSelector — the generator behind the Selector Playground. Routing through the driver rather than porting a generator into the CLI means the derived selectors honor whatever selectorPriority the project configured, so what tap hands back is the selector the project's own tests would use. The command reaches the app under test through a new getElementSelectorSource() seam on the data source, keeping commands off runner globals. A shadow-scoped result (`:host > …`) is omitted rather than returned, since it resolves against no document and would fail the moment it was passed back to a command. Because a match can be omitted, each entry carries its own index rather than relying on its position in the list. Matching nothing is an answer, not a failure, so a caller can tell "no candidates exist" from "the app under test could not be reached". A selector the browser itself rejects is the one case that is a failure, reported as INVALID_SELECTOR. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * chore: drop the native-query comment from getElementSelectorSource Review feedback: the comment explained a choice the call already reads, and forward-referenced an --at flag that arrives in a later slice. * fix: cap how many matches tap element-selectors derives Deriving a unique selector tests candidates against the whole document as it walks up from the element, so deriving one per match for a selector as broad as `*` would hold the app's main thread for the size of the page. Cap it, and say on the result contract that the list is best effort. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: report tap element-selectors matches with no selector as null Dropping a match no unique selector could be derived for left the caller to infer it from a gap in the indexes. Keep its entry and say so with a null selector — the match is still there and --at still reads it. Lower the derive cap to 10 while here. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * refactor: rename the tap element-selectors command to resolve-selector Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3
… the CLI (#34487) * fix: bound every tap CDP call so an unresponsive renderer cannot hang the CLI A CDP reply carries no timer of its own, and the only thing that settles a pending one other than a matching reply is the browser-level socket closing. So a page target that stops answering left every tap command that talks to it pending forever — `status`, `reporter`, `dom`, `aria` and `inspect` each hung with zero bytes on both streams while `instances` reported `browserAttached: true`, pointing the caller back at the call that hangs. Bound the calls by wrapping the CRI client once, where the session hands it out, rather than at each call site — a protocol call added later is covered without having to remember to wrap it. Hitting a bound is now RENDERER_UNRESPONSIVE at exit 1, and `--timeout` raises it for a renderer that is legitimately slow. The runner-page scan keeps its own shorter bound so a target that cannot answer falls out of the scan instead of stopping it, and it remembers that it skipped one: a page that never answered is a different failure from a browser holding no runner page, and only the former is worth waiting longer on. `instances` gains `rendererResponsive` so the command that still answers when everything else is wedged can say which of the two states it is in, without changing what `browserAttached` has always meant. * fix: report the tap CDP timeout instead of swallowing it An expired bound reached `throwTapError`, which wrapped it in a known error whose renderer prints only the canned description — so a wedged renderer read as "the browser may have just closed". Rethrowing the coded error at that funnel also lets the runner-page scan, `instances` and `status` recognize it, makes `inspect` fail rather than exit 0 with a partial element, and gives `instances` the `--timeout` it documents. * refactor: name the tap find-instance timeout for what it finds "discovery" is dead vocabulary in this repo — the instance contract is spelled "instance" (@packages/cypress-instances, /__cypress/instances/). * docs: drop discovery from the tap agent guides The find-instance vocabulary is what the code uses; a convention doc still saying discovery keeps regenerating the term it bans. * refactor: bound tap CDP calls at the client's one send seam chrome-remote-interface generates every domain shorthand as a call to client.send, so replacing that single method bounds every protocol call the session makes — commands, the flat `Domain.method` form, and the raw-client calls the frame extractors issue — without a Proxy pair, a domain cache, or a thenable check. Events and close don't route through send, so the two things that must stay unbounded are excluded structurally rather than by inspection. Derive the call and find-instance bounds where withTapSession uses them instead of in a cdpBounds factory, and replace the factory's unit tests with behavioral ones: a never-answering probe now has to reject after advancing only FIND_INSTANCE_TIMEOUT_MS, proving it uses the short default rather than the call bound. * refactor: drop the tap session's bound-rationale comments Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * fix: print tap coded failures on stderr An agent driving `cypress tap` reads stdout as the machine-readable channel: `--json` prints the result there, and a failure is never rendered as JSON. Yet `logger.error` is `console.log`, so every coded failure landed on stdout too, leaving a `--json` consumer to parse a line of human error prose. It also split tap against itself — build-program.ts already reported its own argument errors with `console.error`. `logger` gains an `errorToStderr` rather than moving `logger.error`, whose stream every other CLI command shares; it records into the same `logs` buffer, so what `logger.print()` collects is unchanged. Results and help stay on stdout. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * fix: name the targeted instance in the tap unresponsive message Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3
#34485) * feat: read one element per tap dom/aria/inspect call, chosen with --at dom, aria, and inspect took a selector and quietly read whatever it matched: dom concatenated every match, aria and inspect silently took the first. On a real app most selectors match more than one element, so a reader could not tell whether what came back was the element they meant. Each of the three now counts the matches before reading. Exactly one match reads as before; several answer with the count and how to narrow it, in place of the read — not an error, since "which one did you mean?" is a legitimate answer that still exits 0 and honors --json. A new --at <index> names which match to read, so an ambiguous selector never needs rewriting to make progress. The count comes from a browser-side querySelectorAll().length, so it costs one number however heavy the page. With the read now scoped to a single element, dom drops its per-match concatenation and renders that element's markup on its own — dedented, since outerHTML keeps the document's indentation and a nested element would otherwise arrive ragged. The frame URL comes off all three results: it identified which frame a multi-match read came from, and there is no longer a multi-match read. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * refactor: share the tap ambiguity guard as withAmbiguous Review feedback. dom, aria, and inspect each opened with the same four lines: resolve the match, hand back the ambiguity answer if there is one, otherwise read. withAmbiguous takes the read as a callback instead, so the guard is written once and a new selector-taking command cannot open without it. resolveMatch loses its doc block along the way: its signature and early returns already read as what it decides. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: exit 1 when a tap selector matched more than one element An ambiguous selector on `tap dom`/`aria`/`inspect` printed how many elements it matched and exited 0, so a caller that only checks the exit code believed it had read the element it asked for. It had not: the read never ran. The exit code now says so. The answer itself is unchanged — same rendering, same `--json` payload, still on stdout — because it names the matches to choose between, and that is what the caller needs to retry. Collapsing it into a coded `renderFailure` would have bought convention at the cost of the payload: stdout empty under `--json`, leaving nothing to pick from. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: match tap index and limit flags instead of coercing them Number() turns a blank or whitespace-only --at into 0, so a missing or malformed flag read as index 0. It also accepts '0x10', '1e3', ' 7 ', '+3' and a single-element array for both --at and the limit flags. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: reject tap index and limit flags outside the safe-integer range A digit-only value still overflows: past 2^53 Number rounds it, and far enough past that it returns Infinity, which would have disabled the very caps --max-chars and --max-nodes exist to enforce. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * refactor: tighten the tap ambiguity guard's naming and messages Rename `resolveMatch` to `resolveAmbiguity` — it answers whether the selector is ambiguous, not which element matched. Drop the rationale clause from the selector-less `at` error and phrase the out-of-range error the way the app-side attempt error does. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * refactor: name the tap index flag as --at in its messages Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3
* fix: handle manually pinned command * feat: add test for tap's captured DOM retention during command eviction This update introduces a new test case to ensure that tap's captured DOM is preserved when its replaced command is evicted before the --clear option is applied. The test verifies that the original DOM is restored correctly, enhancing the reliability of the tap command's behavior in managing DOM snapshots. * refactor: simplify comments and enhance clarity in tap binding and snapshot handling This commit refines comments across various files related to tap binding and snapshot management. It clarifies the behavior of pinned snapshots, particularly those created manually in the reporter, and improves the readability of the code by removing redundant explanations. Additionally, it updates the handling of command arguments in the tap manager to use named flags consistently, enhancing usability and clarity. * feat: add tests for tap pin command behavior and enhance snapshot handling This commit introduces new tests to verify the behavior of the tap pin command when the selected snapshot changes in the UI and when a reporter pin replaces a tap pin. It ensures that the correct snapshot state is reported and that tap's captured DOM is preserved during command eviction. Additionally, it refines the logic for distinguishing between tap and reporter pins, improving the overall reliability of the tap command's snapshot management. * feat: enhance attempt selection in tap commands by including attempt number This update modifies the attempt selection logic to return the attempt number alongside the selected attempt. The changes ensure that the pin command accurately reflects the current attempt context, improving the reliability of snapshot management and command behavior in the tap interface. * Apply suggestions from code review Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com> --------- Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com>
* fix: capture the live page for a pin that follows no hover `_pinSnapshot` never stored the original state, so the only thing that ever captured the live page was the reporter's hover path. Every pin that arrives without a hover in front of it — the tap CLI, or a click landing faster than the 50ms hover debounce — therefore left the app with nothing to restore, and unpinning stranded the AUT on the snapshot. Pinning over a hover preview was worse: the tap pin command detached the previewed snapshot as if it were the live page, ready to restore it as one. Capture in `_pinSnapshot` when the app holds no original state, which is also the invariant the tap pin command hand-rolled by carrying a detached DOM of its own. `detachedId` now moves with the pin as well, so the deferred restore of a preview the pin replaced cannot put the live page back over it and drop the capture with it. Adds the first coverage for `IframeModel`. * refactor: derive the tap pin from the app's snapshot store The pin command tracked its own pin beside the app's snapshot store, so pin identity had two sources of truth to reconcile on every command: `currentPin` merging the two, `reconcilePin` dropping a stale record, an unpin listener, and `current === pinned` ownership checks guarding every mutation. The only thing the record held that the store could not answer was the live DOM to restore, which the app now captures for itself. Read the pin off the store on every command instead. A pin (or an unpin) made in the reporter between two tap commands is then accounted for by construction, a release is the app's own unpin, and a stale pin needs no reconciling because the store resets when a run starts. Comparing resolved log ids replaces the command id and attempt equality checks, which also makes `--at` move a pin made by hand in the reporter rather than replacing it. `pin` no longer reaches for the AUT iframe, so its NO_AUT guard goes with the seam: unreachable behind the runner guard, and the code stays reachable — and covered — through resolve-selector. * refactor: drop the tap-pin comment from the app's pin snapshot * fix: key the tap pin move on the test as well as the log id
The tap render specs import strip-ansi but cli/package.json never listed it, so it resolved off the hoisted root copy and knip failed the health check with 15 unlisted-dependency errors. Pinned to 6.0.1 to match @packages/errors; yarn.lock already carries that exact key.
* fix: read tap test state without serializing the run (AW-97) `tap status` and `tap reporter` built their payloads from `runner.getAllTestsState()`, which serializes every test and every retry attempt — and `serializeTest` runs `LogUtils.toSerializedJSON` over every command, hook, route and agent log, DOM-stringifying element references, invoking consoleProps builders and deep-cloning to strip circular refs. Measured on cypress-realworld-app's transaction-feeds spec (20 tests, 982 log entries) that call takes ~155s, synchronously, on the renderer main thread. Every later CDP call queues behind it, so the whole tap surface hangs and the app UI and AUT freeze with it. `tap reporter` paid it twice, once directly and once through aggregateResults. Neither payload reads the logs. Status needs states and a count; the spec overview needs each test's own properties. Two accessors serve exactly that: getAllTestStates for the counts and getAllTestsSummary for the props-only serialization. Both measure 0ms on that spec, with identical counts, and `getAllTestsSummary` carries no log entries at all. `reporter --testId` is unchanged: it goes through getTestState, serializes the one test it renders, and its logs are the thing being displayed. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * refactor: withhold the serializing test accessor from the tap runner seam Nothing in tap reads `getAllTestsState` now that status counts states and the spec view takes the props-only summary, so the seam stops exposing it. A command that reaches for it fails to compile rather than quietly costing a spec's worth of log serialization, which makes the two "did not call it" assertions redundant — the seam's own test asserts it is withheld instead. The driver keeps the method: it is public API, covered by its own e2e spec, and read directly by the cypress-in-cypress rerun spec. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * refactor: drop the tap state-cost rationale comments Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: stub the split test-state accessors in the reporter-pin runners The reporter-pin stubs landed on trunk while this branch split getAllTestsState into getAllTestStates and getAllTestsSummary, so they still stub the accessor the runner seam no longer exposes and run-state reads undefined off them. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3
…ctor (#34505) * feat: offer a unique selector for each match of an ambiguous tap selector When dom, aria, or inspect answer an ambiguous selector, the count alone leaves the reader to work out how to name the element they meant. The answer now carries a unique selector for each match as well, so the numbered table it prints re-runs the read either way: --at <index>, or the selector itself. The selectors come from the instance's own element-selectors binding command rather than a generator ported into the CLI, so they honor whatever selectorPriority the project configured and read as selectors the project's own tests would use. Best effort: an instance that cannot reach its app under test — a secondary origin inside cy.origin — or that fails outright leaves the match count to speak for itself, and the answer still comes back. Each entry carries its own index rather than relying on its position in the table, since a match no unique selector could be derived for is omitted and the rows would otherwise number a different element than --at would read. Claude-Session: https://claude.ai/code/session_01LpSubVAQugpnrBCWySAHsJ * fix: keep every ambiguous tap match in the numbered list Review feedback. A match no unique selector could be derived for was dropped from the table, so the list read as if only some of the matches existed even though --at reads any of them. Every match now takes a row, with a faded `-` standing in for the selector it lacks — which also retires the special case for no selectors at all, since that is now just a table of dashes. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * refactor: move the tap option parsers into lib/tap/utils Review feedback. parseIndex and parsePositiveInt parse raw commander option strings — nothing about them is the AUT frame, and only the commands import them. They move to lib/tap/utils alongside their test, so aut/frame is left with the frame resolution and the lifecycle gate it is named for. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: point the tap ambiguity guard at the resolve-selector contract The command it asks for a selector per match is `resolve-selector`, whose matches keep their entry with a `null` selector where none could be derived — a null reached `quoted`, which threw on it. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * test: give the withResolvedAutFrame ambiguity fixtures their selectors The ambiguity result grew a `selectors` list here, so the fixtures the parent's withResolvedAutFrame specs build need one too — the human renderer reads it. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: bound the tap ambiguity list at the cap the instance derives to The numbered list was built from the total match count, so a broad selector on a real page — `tap dom --selector '*'` — allocated and printed a row per match. Every row past the derivation cap could only ever be a bare index anyway, so stop numbering there and say what the list leaves out, since --at still reads any of the matches. MAX_DERIVED_SELECTORS moves to the shared contract so the app derives and the CLI numbers to the same number rather than two copies of 10. --json is untouched: it carries the true count and does not build a row per match. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: match the tap index and limit flags in lib/tap/utils too The parsers moved out of aut/frame with their Number() coercion intact, so '', ' 7 ', '0x10' and a single-element array were accepted again. Carry the match-don't-coerce fix and its malformed-input list across with them. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: reject the moved tap parsers' unsafe integers too Carries the safe-integer guard onto lib/tap/utils, where the parsers now live, so both sides of the move reject the same class of value. Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * refactor: name the tap index flag as --at in the moved parsers Claude-Session: https://claude.ai/code/session_017pwk3j9LmPkmZyVhL288a3 * fix: say why an ambiguous tap match has no unique selector A dash in the selector column told the caller nothing about what to do next. Name the two things that put it there: a selector priority the ElementSelector config can change, or an element no document-scoped CSS selector reaches. * fix: only blame the selector config when the instance said it derived nothing An empty selector list means the lookup never answered — the instance was unreachable, or could not reach its app under test. The dashes it leaves behind say nothing about why any one match has no selector, so pointing the reader at their ElementSelector config was advice for a cause that had not been established. Gate the note on a null the instance actually reported.
* feat: give tap CLI options short aliases The `alias` field on `TapCommandOptionSchema` was already rendered by `declareOptions`, but no command set one, so every tap option was long-form only. Claim a letter on the options that get typed by hand, following the `cypress open --help` convention: -t --testId -c --commandId -a --attempt -s --selector -m --max-chars/--max-nodes -d --depth -i --instance `--timeout`, `--json`, `--at` and `pin --clear` stay long-form: every single letter left for them collides within a command that already uses it, and `-t` is worth more to `--testId`. Commander silently lets the last declaration of a repeated short flag win, so a spec asserts each command claims a letter at most once. * refactor: spell the tap id options --test-id/--command-id `--testId` and `--commandId` were the only camelCased flags in the CLI; every other multi-word option (`--config-file`, `--reporter-options`, `--max-chars`) is kebab-cased. Rename them in the shared contract, which is also their wire key, so the app's handlers read them under the same spelling. `forwardedOptions` already resolved a dashed schema name to commander's camelCased attribute, so nothing new was needed to parse them. * test: finish the id option rename in the command --json test
…ration # Conflicts: # packages/server/lib/server-base.ts # packages/server/test/unit/open_project_spec.ts
Runs the main platform workflows on feat/tap-cli-integration and lets it persist build artifacts, so the branch publishes an installable pre-release binary while the tap CLI is being reviewed. Revert before merging.
CircleCI halts the setup job for a draft PR, so the branch would build no pre-release binaries until the PR leaves draft. Revert with the allowlist commit before merging.
| const commandIdField = { name: 'command-id', alias: 'c', type: 'string', description: 'command id, as listed by the reporter command — a row number (test body first when duplicated), an e-prefixed event id, or hook-qualified like "h1:3"' } as const | ||
| const attemptField = { name: 'attempt', alias: 'a', type: 'number', required: false, description: '1-based attempt (attempt 1 = first run); defaults to the latest' } as const | ||
| const selectorField = { name: 'selector', alias: 's', type: 'string', required: false, description: 'a CSS selector; omit to read the whole document' } as const | ||
| const selectorField = { name: 'selector', alias: 's', type: 'string', required: false, description: 'a CSS selector; omit to read the entire document' } as const |
There was a problem hiding this comment.
| const selectorField = { name: 'selector', alias: 's', type: 'string', required: false, description: 'a CSS selector; omit to read the entire document' } as const | |
| const selectorField = { name: 'selector', alias: 's', type: 'string', required: false, description: 'a CSS selector; omit to read the entire document from root' } as const |
what is root? body?
| // also changes what the command returns: nothing is withheld from a payload | ||
| // that is not being rendered for reading room. | ||
| { name: 'json', type: 'boolean', required: false, description: 'print the raw JSON result instead of the human-readable rendering — every console property in full, however long, rather than the long ones named by their length' }, | ||
| { name: 'json', type: 'boolean', required: false, description: 'print the raw JSON result instead of the human-readable rendering. This will output every console property in full regardless of length.' }, |
There was a problem hiding this comment.
I thought we had limits with props outputs. I have noted 1000 string chars and 10000 dom characters?
| const commandMeta = { | ||
| name: 'command', | ||
| description: 'detail one command log entry of a test — its reporter row, the DOM snapshots pinnable on it, and its console properties', | ||
| description: 'detail one command log entry of a test — outputs the reporter row, the DOM snapshots pinnable on it, and any associated console properties', |
There was a problem hiding this comment.
this reads really odd, especially detail one command log entry of a test
| your terminal. Pass --test-id <id> (test ids come from the spec overview this | ||
| same command prints with no --test-id) to see one | ||
| test's full story: its network routes, the hooks that ran, the complete | ||
| description: 'render a view of the test runner reporter, optionally limited to a specific test', |
There was a problem hiding this comment.
| description: 'render a view of the test runner reporter, optionally limited to a specific test', | |
| description: 'retrieve the test runner reporter for the spec or a specific test', |
| stats and every suite's tests including their IDs. | ||
|
|
||
| Provide a --test-id <id> to see one | ||
| test's full story: network requests, the hooks that ran, the complete |
There was a problem hiding this comment.
specifying network requests, is misleading because it's cypress events.
| description: 'inspect the element a selector matches: its tag, attributes, computed styles, box model, and accessibility node', | ||
| details: `Inspects the element the selector matches: its tag, attributes, curated | ||
| computed styles, box model, and accessibility node. | ||
| description: 'inspect a single element matching a CSS Selector: outputs the tag, attributes, computed styles, box model, and accessibility node', |
There was a problem hiding this comment.
| description: 'inspect a single element matching a CSS Selector: outputs the tag, attributes, computed styles, box model, and accessibility node', | |
| description: 'retrieve the tag, attributes, computed styles, box model, and accessibility node of a specific element', |
what is an accessibility node in this description? like the node id? Thought aria was specifically for pulling the accessiblity node?
| details: `Inspects the element the selector matches: its tag, attributes, curated | ||
| computed styles, box model, and accessibility node. | ||
| description: 'inspect a single element matching a CSS Selector: outputs the tag, attributes, computed styles, box model, and accessibility node', | ||
| details: `Inspects a single element matching a CSS Selector: outputs the tag, attributes, computed styles, box model, and accessibility node. |
There was a problem hiding this comment.
@davidr-cy why is the selector required for inspect but not for aria or dom? why wouldn't this fallback to the same "root"?
There was a problem hiding this comment.
I guess inspect didn't seem very useful if you're looking at the body.
Co-authored-by: Emily Wisniewski (Rohrbough) <emilyrohrbough@yahoo.com>
Minor word-smithing to help content, removing some of the less-digestable content that seemed very agent-focused: this feels more like content meant for a skill rather than built-in help content. If nothing else, I suggest we incorporate some of my changes for the top-level
cypress tap --help- the current command description is very vague and doesn't really convey the value/powerAlso tried to emphasize some rules around eligibility for Cypress instances - need to ensure we're clear that:
tap(no connecting to old Cypress versions)Note: Had to set this aside before I got the
cli/*content, and didn't see if I broke any linting or tests. Just putting this out for consideration