skip to content

In Cypress, what does the Command Log show when a custom command runs?

level: middleimportance: should knowfreq 46%

answer

  1. The log names commands, not helpers
  2. Built-in commands log themselves
  3. Commands.add adds no entry
  4. Inner commands appear at the top level
  5. log false hides the chain

basics

~20 s

It shows the commands the body ran, not the command you called. Cypress writes no Command Log entry for a custom command itself, so cy.approveReport() appears as a bare run of get, click and request rows with nothing naming it.

solid answer

~50 s

A custom command is a plain function that queues `cy` commands, and Cypress does not log it. Built-in commands each call `Cypress.log()` from inside their own implementation; `Cypress.Commands.add()` adds no entry, so calling `cy.approveReport()` puts `get`, `click` and `request` rows in the Command Log at the same level as commands typed in the spec, with nothing naming the command they came from. When one of them fails, the error attaches to that inner row and the code frame opens the support file rather than the `cy.approveReport()` line you wrote. The usual cleanup — passing `{ log: false }` to the inner commands and emitting one entry of your own — makes the log tidy at the price of hiding the chain that actually ran. Either way the log stops answering *which of my steps was this?*

go deeper

for a junior

Know that the Command Log is a list of the commands that ran, and that a helper you wrote is not itself a command. Be able to read a log and match rows back to lines in the spec.

for a middle

Explain why the rows appear the way they do: built-in commands log themselves, custom commands do not, and silencing the inner chain is the only way to get one labelled row instead.

for a senior

Show that you weigh this before abstracting. Describe a failure you diagnosed where the log named a command the spec never mentioned, and what you changed so the next reader had a shorter path.

for a principal

Set the expectation for the suite. Decide how deep a command may go before its failure report stops being usable, and make that part of how helpers are reviewed rather than folklore.

## The Command Log logs commands, not helpers Every built-in Cypress command writes its own entry. `cy.get()`, `.click()`, `cy.request()` and the rest each call `Cypress.log()` from inside their implementation, which is why they appear as named rows in the Command Log with a subject, a duration and a snapshot. `Cypress.Commands.add()` adds nothing of the kind. It stores your function under a name and installs it on the `cy` chain; the function itself is not a logged step. So when a spec calls ```js cy.approveReport('exp-4417') ``` and the command body runs `cy.get()`, `.click()` and `cy.request()`, the Command Log contains three rows — `get`, `click`, `request` — sitting at the same level as commands typed directly in the spec, with no row named `approveReport` and no grouping around them. The Command Log has no idea the custom command exists. That is the opposite of what most people expect, and it is the surface cost this abstraction carries. ## Three ways the same steps read | How you wrote it | What the Command Log shows | |---|---| | Inline in the spec | `get`, `click`, `request` — and the spec line is the code frame | | Inside a custom command | the same `get`, `click`, `request` — nothing names the command | | Custom command with `{ log: false }` inside | only whatever the command logs deliberately; the chain is hidden | The third row is the standard cleanup: pass `{ log: false }` to the inner commands and emit a single entry of your own, which is what Cypress's own guidance recommends when a command issues many internal commands. It buys a tidy log and pays for it by hiding the chain that actually ran. Neither default nor cleanup gives you both. ## Where the failure gets reported When a command inside the body fails, the error attaches to **that inner command's** log entry — the `click` row, not an `approveReport` row, because there is no `approveReport` row. Two things follow: - **The code frame points into the definition.** The failing line Cypress opens is the one inside `cypress/support/commands.js`, not the `cy.approveReport('exp-4417')` line you wrote. The spec line is further down the stack. - **The log reads as if the spec did it.** A reviewer skimming the log sees a `click` on a selector that appears nowhere in the spec file, and has to work out where it came from. Time travel still works — the entry is pinnable and the before/after snapshots are there — but the question the log stops answering is *which of my steps was this?* On a five-line spec built from three custom commands, that is most of the diagnostic value. ## What to do about it There is no setting that fixes this; it is a design decision each time. 1. **Keep commands shallow.** A command that runs four commands is readable in the log even unlabelled. A command that calls two other custom commands, which each run six, is not. 2. **Log deliberately, or not at all.** If you silence the inner chain, replace it with one entry that carries the arguments that matter — the report id, the role. A silenced chain with nothing in its place is the worst of both. 3. **Use distinctive selectors.** When the log shows only inner commands, a selector like `[data-cy=approve-report]` tells the reader which step they are looking at; `button.primary` does not. Some further habits that keep the log readable: - Put the value that identifies the run in the argument, so the logged `request` row shows `/api/expense-reports/exp-4417` rather than an opaque URL built inside the command. - Resist commands whose only content is a `cy.get()` and an action — those add a name and remove nothing from the log. - When a command must be deep, treat its own failure messages as part of its contract, because the Command Log will not supply the context for you. ## Why this is worth knowing before you abstract The usual argument for a custom command is that it makes a spec read better. That is true of the spec file and false of the failure report, and the failure report is what you read at 9am when CI is red. Weigh both: a command that turns forty lines of sign-in into one is clearly worth an unlabelled chain in the log, because nobody wanted to read that chain anyway. A command that turns two readable lines into one is not, because you have given up the mapping from log row to spec line and bought nothing measurable in return.

  • With no row naming the command, how do you map a failing step back to a Cypress spec line?
    You work backwards from the arguments. The failing row shows the selector or URL the body used, and the code frame opens the definition, so you match that definition to the `cy.approveReport()` call in the spec by hand. Distinctive selectors and arguments that carry the report id make that walk short; generic ones make it long.
  • Is there a way to get one named Cypress Command Log row for a custom command?
    Only by writing it yourself. Cypress adds nothing automatically, so a command that wants a single readable row silences its inner commands with `{ log: false }` and emits its own entry through the `Cypress.log()` API. That is a deliberate trade of the underlying chain for one labelled step, not a free improvement.

saying these in an interview costs you the question

  • Believes the log shows a row named after the custom command
  • Thinks inner commands are nested under a parent entry automatically
  • Assumes Cypress logs custom commands the way it logs built-ins
  • Treats { log: false } as free rather than a tradeoff