| name | cypress-e2e |
| description | Write or refactor Cypress E2E tests following Page Object Model, action/assertion separation, and project conventions. Use when creating new tests, refactoring existing ones, or adding page object functions. |
| argument-hint | [test-description-or-file-path] |
| allowed-tools | ["Read","Edit","Write","Bash","Grep","Glob","Agent"] |
Cypress E2E Test Automation
Write or refactor Cypress E2E tests for $ARGUMENTS.
Read apps/web/cypress/CLAUDE.md and apps/web/cypress/AGENTS.md for project-specific conventions before proceeding.
Phase 1: Understand Context
- Read the relevant test file(s) and page object(s) in
apps/web/cypress/e2e/pages/
- Read
apps/web/cypress/CLAUDE.md for conventions
- Identify which page object file to use (or create)
- Check existing functions in ALL
*.page*.js files and main.page.js before creating anything new
- Check the actual React component DOM to understand what
data-testid attributes exist and which component renders them
Phase 2: Page Object Structure
Organize page object files in clear sections with this order:
Selector Rules
- Use
data-testid (preferred), aria-label, or semantic HTML — never class names
- If a component lacks a
data-testid, add one to the React component
- Selectors only used within the page object:
const (no export)
- Selectors used by test files:
export const
- Before adding a new
data-testid, check if one already exists on the element
data-testid Rules
Function Rules
-
Action functions (click*, open*, expand*, type*, visit*): perform one user action AND wait for the result to be ready. An action that opens a popover must wait for the popover to appear. An action that navigates must wait for the target page to load. This prevents flaky tests where the next step runs before the UI has settled:
export function clickOnExpandWalletBtn() {
cy.get(expandWalletBtn).should('be.visible').click()
cy.get(sentinelStart).next().should('exist')
}
export function clickOnExpandWalletBtn() {
cy.get(expandWalletBtn).click()
}
-
Verify functions (verify*): assert state only, no user actions
-
Functions used only within the page object: no export
-
Functions used by test files: export
-
General functions (3+ page files): put in main.page.js
-
Page-specific functions: put in that page's .pages.js
-
Wallet/navigation functions belong in navigation.page.js — not in feature page objects. Feature page objects import and call them. When the same action has different UI across contexts (e.g. legacy vs spaces wallet button), create separate functions in navigation.page.js rather than duplicating selectors in feature page objects:
export function clickOnWalletExpandMoreIcon() { ... }
export function clickOnExpandWalletBtn() { ... }
export function disconnectFromSpaceLevel() {
navigation.clickOnExpandWalletBtn()
navigation.clickOnDisconnectBtn()
}
-
Prefer one parameterized function over multiple similar functions — use a type/variant parameter with a selector lookup table when the same verification applies to different component variants:
const selectors = {
single: { name: singleName, address: singleAddress },
multichain: { name: multichainName, address: multichainAddress },
}
export function verifyAccountRowDetails(type, rowIndex, details) {
const sel = selectors[type]
}
-
Check main.page.js first for general utilities like verifyElementsCount, verifyMinimumElementsCount, verifyValuesExist, verifyElementsIsVisible — use them instead of writing custom versions
Composite Flows
Multi-step flows (onboarding, account creation, member invite) should be split into small private helper functions and composed into one exported function:
function navigateToCreateSpacePage() { ... }
function submitSpaceName(name) { ... }
function skipSelectSafesStep() { ... }
function skipInviteMembersStep() { ... }
function verifySpaceDashboardLoaded() { ... }
export function createSpaceViaOnboardingWithSkip(name) {
navigateToCreateSpacePage()
submitSpaceName(name)
skipSelectSafesStep()
skipInviteMembersStep()
verifySpaceDashboardLoaded()
}
Rules for composite flows:
Function Naming Convention
| Prefix | Purpose | Example |
|---|
click* | Click an element | clickAccountItemByIndex(index) |
open* | Open a dropdown/modal/panel | openSpaceSelector() |
expand* | Expand a collapsible section | expandAccountRow(index) |
type* | Type into an input | typeSpaceName(name) |
visit* | Navigate to a URL | visitSpaceDashboard(id) |
verify* | Assert state (visibility, URL) | verifySpaceSidebarItemsVisible() |
Phase 3: Write Tests
Test Structure
it('Verify that [expected behavior]', () => {
space.verifySpaceDashboardWidgetVisible('Accounts')
space.clickAccountItemByIndex(0)
space.verifyAccountRowDetails('single', 0, { name: 'My Safe', address: '0x...' })
})
Rules
- Test names: always
'Verify that [expected behavior]'
- No raw Cypress commands in test files: every
cy.get(), cy.url().should(), cy.contains().click() must be in a page object function
- No hardcoded selectors in test files: import from page objects
- No
cy.wait(N) hard waits: use assertion-based waits (cy.get(sel, { timeout: 30000 }).should('be.visible')) or waitFor* functions
- No
.only: never commit it.only or describe.only
- No hardcoded amounts or counts in regex: use format-only validation (e.g.
nonZeroBalanceRegex not $875Regex)
- Test data in fixtures: put static data in
cypress/fixtures/, not inline in tests. Use staticSpaces, getSafes(CATEGORIES.static), etc.
- Separate actions from assertions: blank line between action block and assertion block
- Reuse
main.verifyElementsCount for counting elements instead of inline .should('have.length', N)
- Extract repeated data access: if the same fixture path is referenced multiple times, extract to a
const at the top of the test (e.g. const safeData = staticSpaces.dashboardWithSafes.pendingTxAccount)
Test Data
- Safe addresses:
getSafes(CATEGORIES.static) — never hardcode
- Static space data:
cypress/fixtures/spaces/staticSpaces.js
- localStorage: payloads in
support/localstorage_data.js, keys in support/constants.js
- API mocks:
cy.intercept() + cy.fixture() from fixtures/
Phase 4: Adding data-testid to Components
When a component needs a data-testid for E2E tests:
- Read the component source to understand the DOM structure — especially for accordion/collapsible/expandable patterns where the trigger and content have different structures
- Check if different component variants exist (e.g.
AccountWidgetItem for single-chain vs ExpandableAccountItem for multichain) — they need distinct test-ids
- Add the
data-testid to the correct element in the correct component
- Verify the element will be visible when Cypress looks for it — collapsed accordion content won't be found until expanded
- Add the corresponding selector to the page object file
- Cross-reference: after all changes, verify every new
data-testid in source is referenced in at least one page object selector
Phase 5: Data Separation
Keep test data and UI selectors in separate places — never mix them.
Fixture files (cypress/fixtures/) — test data only
- Space IDs, names
- Account addresses, names, chain info, row indices
- Counts (row counts, sub-accounts)
- Sub-account chain details (chainId, query params)
- Any data that varies per environment or test scenario
Page object files (e2e/pages/) — UI selectors and labels only
data-testid selectors
aria-label selectors
- Static UI text labels ("Getting started", "Add member", etc.)
- Regex patterns for format validation
- Functions (actions, verifiers)
Rules
- Never duplicate fixture data in page objects — if an address or name is in a fixture, don't also define it as a
const in the page object
- Never put selectors in fixtures — selectors belong in page objects
- Never re-export fixtures from page objects — tests should import fixtures directly (
import staticSpaces from '../../fixtures/spaces/staticSpaces.js')
- Never import fixtures in page objects unless a function internally needs the data (rare — prefer passing as parameters)
- Regex patterns belong in page objects not fixtures — they validate UI format, not test data
Phase 6: Cleanup Checklist
After writing or refactoring tests, verify: