Why does a Cypress `after:spec` hook run under `cypress run` but never in `cypress open`?
answer
- The two modes do not behave alike
- Silence, not an error message
- An experiment gates the interactive case
- Enabling it changes what arrives
- Results exist in only one mode
basics
~20 sBecause before:run, after:run, before:spec and after:spec only fire in interactive mode when experimentalInteractiveRunEvents is enabled, and it defaults to false. Cypress logs nothing when it skips them, so the handler looks broken rather than switched off.
solid answer
~40 sThose four run and spec lifecycle events always fire under `cypress run`, but under `cypress open` they are gated behind the `experimentalInteractiveRunEvents` option, which is `false` by default. No message is printed when a handler is skipped, so a reporting hook simply appears dead locally. Turning the option on is only half the fix: in open mode `after:spec` and `after:run` are handed **undefined** results, and `before:run` gets a `details` object with no `specs` or `browser`, so a handler that reads `results.stats.failures` swaps a silent no-op for a `TypeError`. Guard the argument before using it, restart Cypress after changing the option, and keep anything the suite's correctness depends on out of these hooks entirely.
go deeper
Know that these hooks live in setupNodeEvents and that the interactive and headless modes are not interchangeable. Recognising the four event names is the bar here.
Explain which option gates them, what its default is, and how the arguments differ between the two modes. Naming the undefined results is what separates this tier.
Show the diagnosis: prove which mode is at fault, guard the payload, and explain why a run-level hook fires once per machine under parallelisation rather than once per run.
Decide what may depend on these hooks at all, given that they are off by default in the mode developers use daily and that a suite relying on them behaves differently in CI than on a laptop.
## The four events that behave differently Cypress exposes four lifecycle events to `setupNodeEvents` that bracket a run and each spec: `before:run`, `after:run`, `before:spec` and `after:spec`. Under `cypress run` they always fire. Under `cypress open` they fire **only** when the `experimentalInteractiveRunEvents` configuration option is set to `true`, and as of Cypress 16 it still defaults to `false`. That single default is the whole answer to "the hook works in CI and does nothing locally". Nothing is logged when a handler is skipped for this reason, so it reads as broken rather than disabled. | event | under `cypress run` | under `cypress open`, with the option enabled | |---|---|---| | `before:run` | once per `cypress run` invocation | when the project is opened | | `before:spec` | before each spec | when the browser launches | | `after:spec` | after each spec | when the browser closes | | `after:run` | when the run finishes | when the project is closed | ## The second half of the surprise Enabling the option gets the handler called, but not with the same payload it gets in a run: - `after:spec` is called with `(spec, results)`, and **`results` is undefined in open mode**. - `after:run` is called with `results`, and that is **undefined in open mode** as well. - `before:run` is called with a `details` object whose **`specs` and `browser` are undefined in open mode**; only `config`, `system` and `cypressVersion` are populated there. - `before:spec` is the one that behaves identically either way, always receiving the spec's `name`, `relative` and `absolute` paths. So a reporting hook written against `results.stats.failures` does not start working when the option is turned on - it starts throwing a `TypeError` instead. Every handler in this family needs to guard its argument before reading into it. ## Why the design is like this Open mode is a live editing session rather than a run. You re-run one spec twenty times, you switch specs, you never really finish, and the browser restarts whenever you ask it to. There is no honest set of totals to hand an `after:run` handler in that world, and firing run-level hooks on every restart would make a timing or reporting plugin do its work dozens of times over. `cypress run` is the mode with a definite beginning and end, so it is the mode that gets the full contract. The same reasoning explains a detail that catches teams out in CI rather than locally: `before:run` and `after:run` fire **once per `cypress run` process**. Under parallelisation each machine runs its own `cypress run`, so a four-machine run calls those handlers four times. Anything that has to happen exactly once for the whole run - stamping a build record, posting a summary - does not belong in them without a guard of its own. ## Diagnosing a silent handler 1. **Confirm which mode is the problem.** Put a `console.log` on the first line of the handler and run the same spec with `cypress run`. Printing there and not in open mode identifies the option as the cause immediately. 2. **Enable it deliberately.** Set `experimentalInteractiveRunEvents: true` and restart Cypress. The setting is read when the server starts, so an already-running session will not pick it up. 3. **Guard the payload,** then re-test in both modes rather than only the one you were in. 4. **Rule out the event name.** `on()` accepts only the documented event names and fails the load with the list of valid ones when given anything else, so a handler that neither fires nor errors points at the mode, never at a spelling mistake. ## What belongs in these hooks - **Bracketing work**: starting and stopping a timer, opening and closing a reporting session, writing a run summary that a later pipeline step reads. - **Artefact management**: `after:spec` receives the spec's results, including the path of the recorded video and the list of screenshots, and a promise returned from it is awaited before Cypress processes that video - which is what makes conditional artefact handling possible there and nowhere else. - **Not per-test setup.** There is no per-test node event at all. These four bracket runs and specs, never individual tests. - **Not correctness.** Because they are off by default in the mode developers use all day, nothing the suite's results depend on may live here. A spec that only passes because an `after:spec` handler tidied up after it will pass in CI and fail on the laptop of whoever inherits it.
- Under `cypress run --parallel` on four machines, how often does a Cypress `before:run` handler fire?Four times, once on each machine. The event fires each time `cypress run` executes, and parallelisation means one execution per machine rather than one coordinated run. Anything that must happen exactly once for the whole run - creating a build record, posting a summary - has to be guarded or moved into the pipeline itself.
- What does a Cypress `after:spec` handler receive when it does run under `cypress run`?Two arguments. The `spec` object carries `name`, `relative` and `absolute` paths; the `results` object carries `stats` (tests, passes, failures, pending, skipped), the per-test entries with their attempts, any run error, the path of the recorded video and the screenshots taken. A promise returned from the handler is awaited before Cypress processes that video.
saying these in an interview costs you the question
- Assumes node events fire identically in both Cypress modes
- Reads results.stats without checking results exists
- Thinks a skipped handler prints a warning somewhere
- Expects before:run to fire once across a parallel run
- Puts cleanup the specs depend on into after:spec