This episode opens the hood on Cypress: the Test Runner, Dashboard, and plugin components, the two-process architecture in the browser and Node.js, the test lifecycle with hooks, and the project structure and basic configuration in cypress.config.js.

Episode 1 explained why to choose Cypress. Episode 2 goes deeper: how Cypress works under the hood. You will understand the Test Runner, Dashboard, and plugin components, the execution flow in the browser and Node.js, the test lifecycle with hooks, and the project structure created by installation.
Understanding this architecture matters because many Cypress behaviors — for example, why commands run sequentially, or why Cypress cleans up state between tests — only make sense once you have seen its foundations.
The Test Runner is the interactive application you open with npx cypress open. Inside it there are three panels: the list of spec files, the command log that records every command, and the window showing the application under test. The Test Runner is where you write and debug tests in real time.
The Cypress Dashboard is a cloud service for recording runs, storing videos and screenshots, and detecting flaky tests — we will cover it fully in episode 11. Meanwhile, plugins extend Cypress's capabilities: custom commands, visual testing integrations, or file system access through the task API. We will discuss plugins in episode 18.
Cypress has a two-process architecture. The first process is a Node.js process that runs test logic, reads spec files, and communicates with the application server. The second process is the controlled browser — Cypress injects the test code directly into the application page.
Because tests run in the browser alongside the application, Cypress has full access to the DOM and the page's event loop. This is what makes automatic waiting and per-command snapshots possible. As a trade-off, Cypress can only automate what runs in the same browser tab — different domains require cy.origin(), which we will cover in episode 8.
There is one detail that often confuses beginners: why do Cypress commands seem to run without waiting? Cypress queues each command in a command queue that the driver executes sequentially. Each command hands its result to the next one, so you can chain .should() or .click() without worrying about promises — we will see this in practice in episode 4.
Communication between the Node.js process and the browser is bridged by a proxy. Cypress routes all application network traffic through this proxy, so it can observe incoming and outgoing requests. This is the foundation of cy.intercept(), which we will use for stubbing in episode 12.
A spec file contains a collection of tests wrapped in describe and executed by Cypress:
describe("Login page", () => {
beforeEach(() => {
cy.visit("/login");
});
it("displays the login form", () => {
cy.get("form").should("be.visible");
});
});describe groups tests, it defines a single test case, and beforeEach runs code before each test. Other available hooks: before, after, and afterEach — used for setup and teardown, which we will cover in episode 10.
The execution order is simple but important:
before(() => cy.task("seedDatabase"));
beforeEach(() => cy.visit("/"));
afterEach(() => cy.log("test finished"));
after(() => cy.task("cleanupDatabase"));before runs once before all tests in the file, beforeEach before each test, afterEach after each test, and after once at the end. With this pattern, setup and teardown stay organized without repeating code.
When it runs, Cypress loads the spec file, executes the hooks in order, then runs each test one by one. Between tests, Cypress cleans up browser state automatically.
Installing Cypress produces a standard structure:
cypress/
e2e/
example.cy.js
fixtures/
example.json
support/
commands.js
e2e.js
cypress.config.jscy.fixture().Basic configuration uses cypress.config.js with the e2e property:
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
viewportWidth: 1280,
viewportHeight: 720,
},
});module.exports = defineConfig({ ... }) exports the configuration Cypress reads on startup. The baseUrl property lets cy.visit("/") use that domain, while viewportWidth and viewportHeight set the default screen size.
Besides the three properties above, there are other commonly used options: defaultCommandTimeout to increase automatic waiting tolerance, retries to re-run failed tests, and testIsolation to control state cleanup between tests. We will go deeper into all of these options in episodes 5 and 14.
Episode 2 opened the hood on Cypress: the Test Runner, Dashboard, and plugin components; the two-process Node.js and browser architecture; the test lifecycle with describe, it, and beforeEach hooks; and the project structure with cypress.config.js at the center of configuration.
The key takeaways:
describe groups, it defines tests, and hooks manage the lifecycle.cypress.config.js is the configuration hub with the e2e property.In the next episode, episode 3, we will cover installation and basic configuration — installing Cypress as a dev dependency, opening the Test Runner, writing the first test, setting up cypress.config.js, and adding test scripts to package.json. Your environment is ready; it's time to light the fire.