Cypress vs Playwright Advent Calendar Day 11
Using API calls rather than UI to set up the initial test state
Now that we have seen how to make POST /reset network calls in our tests, we can apply the same approach to setting the entire initial state via API calls rather than doing it via UI
Playwright sets state via API
Let’s confirm that we can complete a “todo” item. We first need to create a couple of items
const { test, expect } = require(’@playwright/test’)
test.describe(’Complete todos’, () => {
test.beforeEach(async ({ request }) => {
request.post(’/reset’, { data: { todos: [] } })
})
test(’completes a todo’, async ({ page }) => {
const input = page.getByPlaceholder(’What needs to be done?’)
const todos = page.locator(’.todo-list li’)
await page.goto(’/’)
await page.locator(’body.loaded’).waitFor()
await input.fill(’Write code’)
await input.press(’Enter’)
await input.fill(’Write tests’)
await input.press(’Enter’)
await input.fill(’Make tests pass’)
await input.press(’Enter’)
// 3 incomplete todos
await expect(todos).toHaveCount(3)
await expect(todos).toHaveClass([’todo’, ‘todo’, ‘todo’])
// complete the middle todo
await todos.nth(1).locator(’.toggle’).click()
// confirm the middle todo is completed
// while the other todos are not
await expect(todos).toHaveClass([’todo’, ‘todo completed’, ‘todo’])
})
})Instead of setting an empty state before the test, let’s set the state with 3 todos via single call. We could get these JSON objects from the backend or by observing the network calls the app makes on page load.
const { test, expect } = require(’@playwright/test’)
test.describe(’Complete todos’, () => {
test.beforeEach(async ({ request }) => {
const todos = [
{
title: ‘Write code’,
completed: false,
id: ‘9719548620’,
},
{
title: ‘Write tests’,
completed: false,
id: ‘7560280342’,
},
{
title: ‘Make tests pass’,
completed: false,
id: ‘8607162111’,
},
]
request.post(’/reset’, { data: { todos } })
})
test(’completes a todo’, async ({ page }) => {
const todos = page.locator(’.todo-list li’)
await page.goto(’/’)
// 3 incomplete todos
await expect(todos).toHaveCount(3)
await expect(todos).toHaveClass([’todo’, ‘todo’, ‘todo’])
// complete the middle todo
await todos.nth(1).locator(’.toggle’).click()
// confirm the middle todo is completed
// while the other todos are not
await expect(todos).toHaveClass([’todo’, ‘todo completed’, ‘todo’])
})
})We replace typing with a single request.post call, and we already had the code to verify the 3 “todo” items before completing the middle one.
Beautiful. Of course, we should move the “todos” array into a JSON fixture file.
Cypress sets state via API
The same test can be written using Cypress cy.request command with example data
describe(’Complete todos’, () => {
beforeEach(() => {
const todos = [
{
title: ‘Write code’,
completed: false,
id: ‘9719548620’,
},
{
title: ‘Write tests’,
completed: false,
id: ‘7560280342’,
},
{
title: ‘Make tests pass’,
completed: false,
id: ‘8607162111’,
},
]
cy.request(’POST’, ‘/reset’, { todos })
})
it(’completes a todo’, () => {
const todos = ‘.todo-list li’
cy.visit(’/’)
// 3 incomplete todos
cy.get(todos).should(’have.length’, 3)
cy.get(todos).eq(0).should(’not.have.class’, ‘completed’)
cy.get(todos).eq(1).should(’not.have.class’, ‘completed’)
cy.get(todos).eq(2).should(’not.have.class’, ‘completed’)
// complete the middle todo
cy.get(todos).eq(1).find(’.toggle’).click()
// confirm the middle todo is completed
// while the other todos are not
cy.get(todos).eq(1).should(’have.class’, ‘completed’)
cy.get(todos).eq(0).should(’not.have.class’, ‘completed’)
cy.get(todos).eq(2).should(’not.have.class’, ‘completed’)
})
})Note: Playwright commands and assertions API is growing while Cypress command palette stays frozen aside from the cy.prompt anti-pattern. I have my own cypress-map plugin that adds “missing” commands and assertions. For example, we could rewrite the same test by calling getAttribute(“class”) on each found element and confirming the array of strings:
// 3 incomplete todos
// similar to PW
// await expect(todos).toHaveClass([’todo’, ‘todo’, ‘todo’])
cy.get(todos)
.mapInvoke(’getAttribute’, ‘class’)
.should(’deep.equal’, [’todo’, ‘todo’, ‘todo’])
// complete the middle todo
cy.get(todos).eq(1).find(’.toggle’).click()
// confirm the middle todo is completed
// while the other todos are not
cy.get(todos)
.mapInvoke(’getAttribute’, ‘class’)
.should(’deep.equal’, [’todo’, ‘todo completed’, ‘todo’])Bonus: test separation
Switching creating the initial “todo” items from UI to API call has one additional benefit: we allows us to test completing items even if adding them is broken. If every test uses the UI to add items before testing its own feature, any change in the input code might break all tests. By using API we can bypass the “add” UI problem and simply confirm that completing the existing todos works.
This advent calendar is based on my online course “Cypress vs Playwright“ and open-source workshop bahmutov/cypress-workshop-cy-vs-pw. You can find links to the previous advent calendar days in my blog post “Cypress vs Playwright Advent Calendar 2025“.





