In Cypress, what does Cypress.log() give a custom command that cy.log() does not?
answer
- One narrates, the other builds the entry
- Built-in commands use the second one
- A function, not an object, for the console
- Returns something with end and set
- consoleProps runs when you click
basics
~20 scy.log() only appends a plain message entry. Cypress.log() creates a real Command Log entry you control: its name, display name and message, a consoleProps function whose object prints in DevTools when the entry is clicked, and a Log object with end, error, set and snapshot methods.
solid answer
~40 s`cy.log(message, ...args)` is a queued command that appends one narration line to the Command Log. It accepts a Markdown message and any number of extra arguments, which print to the console when you click the entry, and it yields `null` and takes no assertions. `Cypress.log(options)` is the API Cypress's own built-in commands use, so a custom command can present itself the same way theirs do. You control `name`, `displayName`, `message`, `$el`, `type` and — the important one — `consoleProps`, a **function returning an object** whose keys are printed to DevTools when the entry is clicked. It returns a `Log` object with `end()`, `error()`, `set()`, `get()` and `snapshot()`, so with `autoEnd: false` an async helper can keep its entry open, fill in `consoleProps` once the response lands, and close it.
go deeper
Know that cy.log() adds a plain message line to the Command Log, and that Cypress.log() is the separate API custom commands use to build an entry of their own.
Explain the main options — name, displayName, message and consoleProps — and why consoleProps is a function evaluated on click rather than an object captured up front.
Show the async shape: autoEnd: false, log.set() once the data exists, log.end() on success, log.error() plus a rethrow on failure, and why the rethrow is not optional.
Decide what the team's helpers owe a reader of a failing run, and whether hand-maintained consoleProps that can drift from the API is a cost your suite should carry.
## Two different jobs Both write to the Command Log, but they are not variants of one another. `cy.log()` is a **command**: you call it from a test, it takes its place in the queue, and it appends one entry that says what you told it to say. Its message accepts Markdown, it takes any number of extra arguments (`cy.log('assigned', ticketId, agent)`) which are printed to the console when the entry is clicked, it yields `null`, and no assertion can be chained onto it. It is narration — a signpost in a long spec. `Cypress.log()` is the **internal API for controlling what gets printed to the Command Log**, and it is what the built-in commands themselves call. You reach for it when a custom command would otherwise be a black box: the log shows its name and nothing about what it did, so a failure inside it reads as one opaque line. ## The options that matter | Option | Default | What it does | |---|---|---| | `name` | the command's name | the name shown in the Command Log | | `displayName` | the command's name | overrides `name` for display only — useful for a short label | | `message` | the command args | the text beside the name in the entry | | `consoleProps` | `function() {}` | a function returning an object, printed to the DevTools console when the entry is clicked | | `$el` | `undefined` | the jQuery element the command acted on; highlights it in the app preview | | `type` | `'parent'` | `'parent'` or `'child'` — child entries render indented | | `autoEnd` | `true` | set `false` to close the entry yourself, which async commands need | | `end` | `false` | set `true` to mark the entry finished the moment it is created | The one people underuse is `consoleProps`. It is a **function**, not an object, and Cypress calls it when you click the entry — so it can close over values that only exist inside your command and present them under readable keys. That is where the ticket id, the response status and the response body belong: one click away in DevTools, instead of gone. ## The Log object and asynchronous helpers `Cypress.log()` returns a `Log` you can keep hold of: - `log.end()` — mark it passed and finished. - `log.error(err)` — turn the entry red. **This only changes how the entry looks; it does not fail the test.** You must still rethrow the error, or the run carries on green with a red line in the log. - `log.set(key, value)` or `log.set(options)` — update attributes later, which is how you attach `consoleProps` containing data that did not exist when the entry was created. - `log.get()` / `log.get(attr)` — read the entry's current attributes. - `log.snapshot()` / `log.snapshot(name)` — capture the DOM against this entry; named snapshots become labelled tabs when the entry is pinned. For anything that finishes asynchronously, the pattern is: 1. Create the entry with `autoEnd: false` so it stays open. 2. Do the work. 3. `log.set({ consoleProps: () => ({ ...now-known data }) })`. 4. `log.end()` on success, or `log.error(err)` **and rethrow** on failure. Forget step 4 and the entry sits pending forever, which reads to the next person as a hung command. ## The other half of the same decision: `{ log: false }` Shaping a command's log entry usually comes in pairs. `Cypress.log()` adds the entry you want; `{ log: false }` — accepted by most built-in commands — suppresses the entries you do not. A helper that fires a `cy.request()` and a couple of `cy.window()` calls to set up a ticket produces three lines that say nothing useful; silencing them and adding one `Cypress.log()` for the helper as a whole turns that into one legible step. What `{ log: false }` does **not** do is change behaviour. The command still runs, and if it fails the test still fails — you have simply removed the line that would have said where. ## A worked shape ```js Cypress.Commands.add('assignTicket', (ticketId, agent) => { const log = Cypress.log({ name: 'assignTicket', displayName: 'assign', message: `#${ticketId} to ${agent}`, autoEnd: false, }) cy.request({ method: 'POST', url: `/api/tickets/${ticketId}/assign`, body: { agent }, log: false, }).then((res) => { log.set({ consoleProps: () => ({ ticketId, agent, status: res.status, body: res.body }) }) log.end() }) }) ``` In the Command Log that is one `assign` entry reading `#4821 to dana`; clicking it prints the ticket id, the agent, the HTTP status and the response body. Compare it with the same helper writing `cy.log('assigning ticket')` — a line that tells you the helper started and nothing about why it did not finish.
- Why must a Cypress consoleProps be a function rather than a plain object?Because Cypress calls it when you click the entry, not when the entry is created. That lets it read values that were not known at creation time and keeps the work off the hot path of a run where nobody clicks. It also means `log.set({ consoleProps: () => ({...}) })` after an async step is the normal way to attach data that only exists once a response has landed.
- A Cypress custom command calls log.error(err) and the run still reports green. Why?`log.error()` only changes the entry's visual state — it paints it red and attaches the error object. It does not fail the test. You have to rethrow the error (or reject the promise) so Cypress sees the command failed. A helper that catches, calls `log.error()` and swallows produces the worst possible artefact: a red line in a passing run.
saying these in an interview costs you the question
- Thinks cy.log() and Cypress.log() are the same call
- Passes consoleProps a plain object instead of a function
- Says log.error() fails the test on its own
- Uses autoEnd: false and never calls log.end()