Learn Cypress - End-to-End Flows
Episode 10 of 23

Learn Cypress - End-to-End Flows

This episode covers structuring end-to-end test scenarios: login, checkout, and multi-page journeys, managing the state of the application under test with cy.session, and deterministic data setup and teardown.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

Component testing strengthens the foundation. Now you tie it all together: complete end-to-end scenarios from UI to backend. Episode 10 covers structuring E2E test scenarios, login, checkout, and multi-page journeys, managing the state of the application under test, and deterministic data setup and teardown.

E2E is the top layer of the test pyramid — the closest to the real user experience. However, that power comes at the cost of speed and stability, so disciplined scenario design becomes the deciding factor for success.

Structuring End-to-End Test Scenarios

One Flow, One Scenario

The core principle: one test verifies one user flow. Do not combine login, search, and checkout into one giant it — it is hard to trace when it fails and expensive to rerun.

JSA focused E2E scenario
describe("Alur pencarian produk", () => {
  it("menampilkan hasil yang sesuai kata kunci", () => {
    cy.visit("/katalog");
    cy.get("[data-cy=cari]").type("kopi");
    cy.get("[data-cy=hasil]").should("contain.text", "Kopi Gayo");
  });
});

cy.visit("/katalog") opens the starting page of the scenario. This test has a single goal: making sure the search shows the right results. There are no unneeded steps.

Writing Scenarios from the User's Perspective

Structure the steps along the user journey, not the implementation order. Start from a real state, perform actions, then verify outcomes the user can see. Test language should be readable by non-technical teams:

JSA flow from the user's point of view
it("memesan produk dari halaman detail", () => {
  cy.visit("/produk/kopi-gayo");
  cy.get("[data-cy=jumlah]").type("2");
  cy.get("[data-cy=masukkan-keranjang]").click();
  cy.get("[data-cy=keranjang]").should("have.text", "2");
});

cy.get("[data-cy=jumlah]").type("2") mimics a user entering the order quantity. Each line describes one real step: open the product, set the quantity, add to cart, then confirm the cart updated.

Login Flows, Checkout, and Multi-Page Journeys

Login Flow

Login is the foundation of many scenarios. Use the custom command created in episode 7 so you do not repeat the details:

JSTesting the login flow
it("berhasil login dan diarahkan ke dashboard", () => {
  cy.visit("/login");
  cy.get("[data-cy=email]").type("user@example.com");
  cy.get("[data-cy=password]").type("rahasia123");
  cy.get("[data-cy=submit]").click();
 
  cy.url().should("include", "/dashboard");
  cy.contains("Selamat datang").should("be.visible");
});

cy.url().should("include", "/dashboard") verifies the redirect after login. The second assertion ensures the page content actually loaded — not just that the URL changed.

Checkout and Multi-Page Journeys

Checkout usually crosses several pages: cart, address, payment, confirmation. Write it as one organized test with a step per page:

JSMulti-page checkout flow
it("menyelesaikan checkout", () => {
  cy.visit("/checkout");
  cy.get("[data-cy=alamat]").type("Jl. Merdeka 1");
  cy.get("[data-cy=lanjut-pembayaran]").click();
 
  cy.get("[data-cy=kartu]").type("4111 1111 1111 1111");
  cy.get("[data-cy=konfirmasi]").click();
 
  cy.contains("Pesanan berhasil dibuat").should("be.visible");
  cy.url().should("include", "/pesanan-selesai");
});

cy.get("[data-cy=lanjut-pembayaran]").click() moves the test to the next page. The pattern fill a form → continue → fill the next form → verify the final result is the core of multi-page journey testing.

State Management in the Application Under Test

Setting Up Initial State

Good tests do not depend on previous user history. Every test sets up its own state. There are several ways: injecting data through the task API, stubbing responses, or setting a session token:

JSSetting a session via localStorage
cy.visit("/");
cy.window().then((win) => {
  win.localStorage.setItem("token", "token-tiruan");
});
cy.reload();

win.localStorage.setItem("token", "token-tiruan") injects a mock session before reloading. This way the login flow does not need to run in every test — saving time without sacrificing realism.

Keeping Sessions Across Tests

Sometimes, on the other hand, you want to mimic a user staying logged in across pages. Cypress provides cy.session() to store sessions:

JSStoring a login session
beforeEach(() => {
  cy.session("login-user", () => {
    cy.visit("/login");
    cy.get("[data-cy=email]").type("user@example.com");
    cy.get("[data-cy=password]").type("rahasia123");
    cy.get("[data-cy=submit]").click();
  });
});

cy.session("login-user", ...) runs the login block once and then stores the session state. Subsequent tests reuse the stored session — speeding up execution while still mimicking a logged-in user.

Data Setup and Teardown

Cleaning Up and Setting Up Data

Data leaking between tests is a source of flakiness. Use hooks for deterministic setup and teardown:

JSData setup and teardown
beforeEach(() => {
  cy.task("seedDatabase");
  cy.visit("/");
});
 
afterEach(() => {
  cy.task("clearDatabase");
});

cy.task("seedDatabase") calls a Node function on the Cypress side to inject test data. cy.task("clearDatabase") cleans up after the test. We will dig into implementing this task API in episode 18.

Safe Teardown

If a test fails halfway through, afterEach still runs — make sure your teardown does not assume the test succeeded. Avoid sending cleanup requests from inside the test; put everything in hooks so it always executes regardless of the test outcome.

Info

Combine cy.session() with cy.task(): sessions for user state, tasks for backend data. This separation keeps tests fast and gives each state source a clear owner.

Closing

Episode 10 wove interactions into real flows: one test per scenario, multi-page login and checkout, state management with cy.session() and localStorage, and data setup and teardown via the task API and hooks.

Key takeaways:

  • One test verifies one user flow; avoid giant it blocks.
  • Write scenarios from the user's perspective, not the implementation order.
  • cy.session() stores a login session so subsequent tests run fast.
  • Inject initial state via localStorage, the task API, or stubs.
  • Put data setup and teardown in hooks so they always run.

In the next episode, episode 11, we will cover dashboard and reporting — Cypress Dashboard features, video recording and screenshots, the Mochawesome reporter for CI, and how to analyze test runs and debug failures. Your test results start becoming readable by others.

Learn Cypress - End-to-End Flows | Learn Cypress