lecture 4 of 160 completed

Designing the layer your specs are written against

Between raw Cypress and your specs sits a vocabulary you design. Getting that layer right is what makes a large suite readable.

Read these two tests. Both check the same thing.

it('removes a contact', () => {
  cy.visit('/login');
  cy.get('[data-testid="email"]').type('demo@nimbus.app');
  cy.get('[data-testid="password"]').type('secure123');
  cy.get('[data-testid="submit"]').click();
  cy.visit('/contacts');
  cy.contains('[data-testid="row"]', 'Dana Fox').find('[data-testid="delete"]').click();
  cy.get('[data-testid="confirm"]').click();
  cy.contains('[data-testid="row"]', 'Dana Fox').should('not.exist');
});
it('removes a contact', () => {
  cy.loginAs('standard');
  contacts.open();
  contacts.delete('Dana Fox');
  contacts.expectMissing('Dana Fox');
});

The second is not shorter for its own sake. It is shorter because it says what and not how, and that difference decides whether a suite survives a redesign.

When the delete flow changes from a confirm dialog to an undo toast, the first test is rewritten. So are the other thirty like it. The second test does not change at all, because the change lives in one place.

This layer has a name in your project even if you never name it: the vocabulary your specs are written in. Designing it deliberately is senior work, and there are three rules that decide whether it helps or hurts.

Rule one: the layer speaks in domain language, not UI language.

contacts.delete('Dana Fox');        // domain: what the user achieves
contacts.clickDeleteButton();       // UI: how the screen currently works

The second name leaks the implementation into the name itself, so when the button becomes a menu item, the name lies. Name things after intent and the name stays true.

Rule two: the layer does not assert. Specs assert.

This is the rule most teams get wrong, and it is worth being precise about why.

// wrong: the helper decides what success means
contacts.deleteAndVerify('Dana Fox');

// right: the helper acts, the spec judges
contacts.delete('Dana Fox');
contacts.expectMissing('Dana Fox');

The first version cannot be used in a test about deletion failing, or about permissions preventing deletion. You have hidden the assertion inside the action, so the helper only supports one story.

Providing assertion helpers like expectMissing is fine, and different: the spec still chooses which one to call.

Rule three: one level of abstraction per test.

A spec that mixes contacts.delete('Dana Fox') with cy.get('[data-testid="row-3"] > td:nth-child(2)') is harder to read than one written entirely at either level. When you need to drop to raw selectors, that is a signal the layer is missing something. Add it to the layer.

Where this layer lives. Objects, page objects, plain modules, custom commands, or a mix. The shape matters far less than the three rules. What matters is that there is one and that it is consistent, because inconsistency is worse than either choice made uniformly.

The failure mode to watch for. Abstraction has a cost: indirection. A test that reads beautifully but requires opening four files to understand is not obviously better than an explicit one. The check I apply: can a new engineer read the spec and correctly predict what the screen does? If yes, the abstraction is earning its keep. If they have to go spelunking, it has gone too far.

Remember this: design the vocabulary specs are written in. Domain names, no assertions inside actions, one level per test.

Reference: Cypress best practices.