In Cypress, which files can cy.readFile() reach that cy.fixture() cannot?
answer
- Two commands, two different starting folders
- One is capped at the fixtures folder
- The other counts from the config file
- Only one of them retries
- Query semantics allow not.exist assertions
basics
~20 sAnything under the project root. cy.readFile() resolves its path from the folder holding the Cypress config, so it reads a downloaded reimbursement CSV or a generated report, while cy.fixture() is confined to fixturesFolder, by default cypress/fixtures.
solid answer
~40 s`cy.fixture()` only ever looks inside `fixturesFolder` — `cypress/fixtures` by default — so it can read canned inputs and nothing else. `cy.readFile()` resolves its path against the **project root**, the directory containing the Cypress configuration file, so it reaches a receipt PDF the app dropped in `cypress/downloads`, an export your test just produced, or any file in the repository. The behaviours differ too: `cy.readFile()` is a query, so it re-reads and retries until the chained assertions pass, and by default it asserts that the file exists — `cy.readFile('missing.csv').should('not.exist')` is how you assert absence. `cy.fixture()` neither retries nor appears in the Command Log. `cy.writeFile()` uses the same project-root path rule in the other direction, creating missing directories and overwriting by default.
go deeper
Be ready to say that cy.fixture() paths start at cypress/fixtures while cy.readFile() and cy.writeFile() paths start at the project root, and to read a downloaded file with the right one.
Explain the mechanics that follow from that: cy.readFile() is a query that retries and asserts existence, cy.fixture() runs once, and cy.writeFile() overwrites by default while creating any missing directories.
Show judgment about tests that touch the working tree: which files a suite is allowed to write, why refreshing a checked-in fixture on every run is a trap, and how you wait for a downloaded export without a fixed sleep.
Own the boundary between the suite and the repository: where generated artefacts live, what is cleaned up between runs, and whether tests may write anywhere other than a scratch directory the pipeline discards.
## Two different roots The single most useful fact here is that these commands do not measure paths from the same place. | | `cy.fixture()` | `cy.readFile()` / `cy.writeFile()` | |---|---|---| | Path is relative to | `fixturesFolder` (default `cypress/fixtures`) | the project root — the folder holding the Cypress config file | | Reads | canned inputs only | any file in the project | | JSON handling | parsed, unless an encoding is passed | parsed by `cy.readFile()`, unless the encoding is `null` | | Retries | no | yes, `cy.readFile()` is a query | | Command Log | not logged | logged, unless `log: false` | | Caching | file read once per path and encoding | read fresh on every attempt | So in an expense-report suite, `cy.fixture('receipts.json')` reads `cypress/fixtures/receipts.json`, while `cy.readFile('cypress/fixtures/receipts.json')` reads the very same file by spelling out the whole path from the project root. The second form is not a mistake, just a different door — and it is the only door to files the fixtures folder does not hold: - `cy.readFile('cypress/downloads/reimbursements.csv')` for a file the application under test downloaded during the run. - `cy.readFile('package.json').its('version')` for project metadata. - `cy.readFile('tmp/expense-export.txt')` for something a `cy.writeFile()` earlier in the spec produced. ## `cy.readFile()` retries; `cy.fixture()` does not `cy.readFile()` is a **query**, which is why waiting for a file works at all. It re-runs against the disk until every chained assertion and command passes or `defaultCommandTimeout` expires, so this is a legitimate way to wait for an export the app writes asynchronously: ```js cy.get('[data-testid="export-reimbursements"]').click() cy.readFile('cypress/downloads/reimbursements.csv').should('contain', 'R-1001') ``` By default the command also asserts that the file **exists**, retrying past the initial file-not-found rather than failing at once. To assert the opposite — that a file was never written, or was cleaned up — chain `.should('not.exist')`, which is the documented way to invert it. `cy.fixture()` has none of that. It runs once, resolves once, and only evaluates assertions you chained once. It uses `responseTimeout` rather than `defaultCommandTimeout`, and the docs are blunt that it should never realistically time out. ## Writing files back `cy.writeFile()` is the mirror image and shares the project-root rule: - Missing directories in the path are **created** for you. - The default file-system flag is `w`, so an existing file is **overwritten**, not appended; `{ flag: 'a+' }` appends instead. - An object or array is stringified to JSON with two-space indentation before it is written; a string is written as-is. - The command yields `null`, so chain the next step off `cy.readFile()` rather than expecting the written contents back. - Appending assumes plain text — to merge into a JSON file you read it, edit the object, and write the whole thing back. A common shape in an expense-report suite is capturing a real response once and keeping it: ```js cy.request('/api/receipts').then((response) => { cy.writeFile('cypress/fixtures/receipts.json', response.body) }) ``` That is fine as a one-off refresh. Leaving it in the suite is not: it makes every run mutate the repository, and the next run then tests against whatever the last run happened to see. ## What each one shows you when it fails The three commands are not equally visible when something goes wrong, which matters when you are reading a CI failure rather than watching the run: - `cy.readFile()` writes an entry into the Command Log with the file name, so a failed read is visible in the run and clickable in time-travel; passing `{ log: false }` suppresses it. - `cy.fixture()` logs nothing at all, so a failure surfaces only as the error message — there is no greyed-out command entry to hover over. - `cy.writeFile()` logs the resolved path and the contents it wrote, which is the quickest way to confirm that a test wrote where you thought it did rather than one directory up. ## Choosing between them 1. Canned input that ships with the suite and never changes during a run → `cy.fixture()`, for the short path and the automatic parsing. 2. A file that appears, changes, or disappears while the test runs → `cy.readFile()`, for the retrying query semantics. 3. A file your test needs to produce → `cy.writeFile()`, ideally under a scratch or downloads folder rather than into the checked-in fixtures. ## One shared constraint Both commands do their work in the Cypress Node process and hand the contents to the browser, so neither is a way to reach a file on a remote machine, and neither belongs in the middle of a tight assertion loop. They are also spec-only commands: they must be invoked from a running spec rather than from arbitrary code loaded alongside it.
- How do you assert in Cypress that an expense export file was never written?Chain `.should('not.exist')` onto `cy.readFile()`. By default the command asserts existence and retries past a missing file, so the negative assertion is the documented way to invert it: `cy.readFile('cypress/downloads/export.csv').should('not.exist')` passes only while the file is absent for the whole retry window.
- What does cy.writeFile() do when the target file already exists?It overwrites it. The default file-system flag is `w`, so the previous contents are gone; pass `{ flag: 'a+' }` to append instead. Missing directories along the path are created automatically, objects are stringified to JSON with two-space indentation, and the command yields `null`.
saying these in an interview costs you the question
- Thinks cy.readFile() also starts at the fixtures folder
- Expects cy.fixture() to retry until a file appears
- Says cy.writeFile() appends to an existing file
- Uses cy.fixture() to read a downloaded export
- Believes cy.writeFile() yields the written contents