Resource library

QA How-To

Cypress cy.wrap and cy.then: Examples

Copy practical cypress cy.wrap and cy.then example patterns for objects, elements, promises, APIs, lists, aliases, TypeScript, retries, and debugging.

23 min read | 2,596 words

TL;DR

A reliable cypress cy.wrap and cy.then example makes the subject and return path obvious. Wrap plain values, elements, or promises only when they need Cypress chain behavior, then use .then() to consume or transform a settled yield.

Key Takeaways

  • Wrap plain objects when Cypress assertions or further chain operations improve the test.
  • Re-wrap jQuery elements inside callbacks before using Cypress action commands.
  • Invoke promise-producing application code inside .then() when earlier Cypress commands must finish first.
  • Return transformed values, promises, or Cypress chains explicitly so the next subject is predictable.
  • Use .should(callback) for retryable observations and .then() for single transformations or side effects.
  • Scope repeated elements before wrapping one item, and re-query after actions that can replace DOM nodes.
  • Type reusable helpers as Cypress.Chainable<T> and keep secrets out of logs.

A useful cypress cy.wrap and cy.then example should show more than syntax. It should make execution order, the incoming subject, the callback return, and retry behavior visible. The recipes below cover objects, arrays, DOM elements, promises, network responses, aliases, and typed helpers that appear in real end-to-end suites.

Each example uses current Cypress APIs and TypeScript. The application routes and data-cy values are illustrative contracts that you can replace with your own. The patterns are designed to remain correct when Cypress queues commands, retries queries, and waits for returned chains.

TL;DR

Example goal Core pattern Important rule
Assert a plain object cy.wrap(object).should(...) Wrap adds Cypress chain behavior
Act on a callback element cy.wrap($el).click() Use it immediately, then re-query after render
Parse text into a number .then(text => Number(...)) Returned number becomes the next subject
Wait for app promise cy.wrap(appPromise) Rejection fails the test
Start async work after UI action .then(() => cy.wrap(start())) Invoke inside the callback
Wait for changing state .should(callback) Callback can retry and must stay pure

The small distinction that keeps all examples sound is this: cy.wrap() yields a supplied value, while .then() receives and can replace the current yield.

1. Basic Cypress cy.wrap and cy.then Example

Start with a plain object that represents data created by a synchronous factory. cy.wrap() introduces the object as the chain's subject. its() narrows the subject, and .then() turns the value into another domain object.

type Cart = {
  items: Array<{ sku: string; price: number; quantity: number }>
}

const cart: Cart = {
  items: [
    { sku: 'KB-01', price: 80, quantity: 1 },
    { sku: 'PAD-02', price: 15, quantity: 2 },
  ],
}

cy.wrap(cart)
  .its('items')
  .should('have.length', 2)
  .then((items) => {
    const subtotal = items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0,
    )
    return { subtotal, itemCount: items.length }
  })
  .should('deep.equal', { subtotal: 110, itemCount: 2 })

The array yielded by its('items') is the callback argument. Returning the summary object replaces that array as the subject of the final assertion. This is clearer than assigning subtotal in an outer variable and reading it later.

If the calculation is completely independent of Cypress, a plain expect() is also valid. Use wrap here because the example intentionally combines property traversal, Cypress assertions, and a transformed subject. Do not interpret the final chain as the summary object in ordinary synchronous JavaScript. Consumers must continue chaining or use another callback.

2. Parse and Compare Values From Two Elements

Closures are the standard way to compare values yielded at different positions in the command queue. Query the first value, transform it, then query the second value inside the callback. Return the nested chain if later commands should receive its result.

const money = (text: string) => Number(text.replace(/[^0-9.]/g, ''))

cy.get('[data-cy=cart-subtotal]')
  .should('not.have.text', '')
  .invoke('text')
  .then(money)
  .then((subtotal) => {
    return cy.get('[data-cy=cart-total]')
      .should('not.have.text', '')
      .invoke('text')
      .then(money)
      .then((total) => ({ subtotal, total }))
  })
  .then(({ subtotal, total }) => {
    expect(total).to.be.at.least(subtotal)
    expect(total - subtotal).to.be.lessThan(50)
  })

The readiness assertions run before each text extraction. The first .then(money) is a pure one-time conversion after non-empty text has been observed. The nested chain carries both numbers forward as one object.

Do not capture the first jQuery element and inspect it after unrelated actions. Capture the primitive text or parsed number that the comparison needs. DOM nodes can be replaced; immutable snapshots of settled values are appropriate when the test intentionally compares one moment with another.

For a simpler equality comparison, aliases can be readable. Closures are preferable when the values must be transformed together or when the dependency belongs within one small chain. If the comparison is polling live values, keep the query in a retryable assertion rather than freezing it too early.

3. Re-Wrap a jQuery Element Before Clicking

Inside .then(), the subject from cy.get() is a jQuery collection. Use jQuery methods for immediate inspection, then wrap the element to use Cypress actionability checks and action commands.

cy.get('[data-cy=notification-row]').then(($rows) => {
  const unread = $rows.filter('[data-state=unread]')

  expect(unread, 'unread notifications').to.have.length.greaterThan(0)
  cy.wrap(unread.first()).find('[data-cy=mark-read]').click()
})

cy.get('[data-cy=notification-row][data-state=unread]')
  .should('have.length.lessThan', 3)

The immediate wrapped click is safe because it occurs in the same callback based on the current collection. The final assertion re-queries the application rather than continuing with a node that the click may have changed or removed.

Prefer Cypress queries for selection when they express the same intent more clearly. For example, a row can often be scoped by visible content, followed by a stable test hook:

cy.contains('[data-cy=notification-row]', 'Build completed')
  .find('[data-cy=mark-read]')
  .click()

cy.contains('[data-cy=notification-row]', 'Build completed')
  .should('have.attr', 'data-state', 'read')

Use re-wrapping when the element genuinely originates from a jQuery callback, .each(), within() callback, or third-party helper. Do not add callbacks solely to wrap something that was already chainable.

4. cy.wrap Example Inside each()

.each() supplies each item to its callback. When the subject is a DOM collection, each item is a raw DOM element. Wrap the current element to use Cypress traversal and assertions. This example validates that every visible invoice row contains a valid status and a formatted total.

const allowedStatuses = ['Draft', 'Sent', 'Paid', 'Overdue']

cy.get('[data-cy=invoice-row]')
  .should('have.length.at.least', 1)
  .each(($row) => {
    cy.wrap($row).within(() => {
      cy.get('[data-cy=invoice-status]')
        .invoke('text')
        .then((text) => text.trim())
        .should('be.oneOf', allowedStatuses)

      cy.get('[data-cy=invoice-total]')
        .invoke('text')
        .should('match', /^$[0-9]+([.][0-9]{2})?$/)
    })
  })

The callback queues commands for every row. within() scopes descendant queries to the wrapped row. Use this for homogeneous collection checks, not for a complex workflow that mutates the list during iteration. Removing rows while iterating can invalidate the original collection.

When the requirement concerns one record, locate that record by a stable business identifier and act once. When order is the behavior under test, assert the ordered values explicitly. Avoid each() merely to search until a condition is found, because Cypress does not make an arbitrary loop retryable.

A related pattern uses cy.wrap(array) with non-DOM data, but plain array methods are normally cleaner for synchronous data. Wrap an array when you need it as the subject of a Cypress custom command, alias, or chain assertion.

5. cy.wrap Promise Example After a UI Action

Suppose application code exposes a promise-returning audit client. The audit request must start only after the user saves a form. Invoke the client inside a .then() placed after the click, then wrap the returned promise.

type AuditEvent = {
  action: string
  entityId: string
}

declare const auditClient: {
  latest(): Promise<AuditEvent>
}

cy.get('[data-cy=profile-name]').clear().type('Mina Shah')
cy.get('[data-cy=save-profile]')
  .click()
  .then(() => {
    return cy.wrap(auditClient.latest(), { timeout: 10000 })
  })
  .should('deep.include', {
    action: 'profile.updated',
  })
  .its('entityId')
  .should('match', /^usr_/)

The application-specific auditClient.latest() is illustrative, while cy.wrap(), its options, and the chain assertions are current Cypress APIs. Because the function call is inside the callback, it cannot begin while the save click is merely queued.

If the application emits the audit request over HTTP, cy.intercept() is usually a better boundary. Register the intercept before clicking, alias it, and wait for the request. A direct application promise is appropriate when the app intentionally exposes a testable service or when component testing imports that service.

Never create the promise at spec definition time. It can run before the test, leak state between tests, and reject outside the expected command context. Keep promise construction inside the test and as close as possible to the event that causes it.

6. Cypress cy.wrap and cy.then Example for an API Response

cy.request() already returns a Cypress chain, so do not wrap the command itself. Use .then() to validate the full response and return only the body or a derived value needed by later commands.

type CreatedProject = {
  id: string
  name: string
  ownerId: string
}

cy.request<CreatedProject>({
  method: 'POST',
  url: '/api/projects',
  body: { name: 'Release readiness' },
}).then(({ status, headers, body }) => {
  expect(status).to.eq(201)
  expect(headers).to.have.property('content-type')
  expect(body.id).to.match(/^prj_/)
  return body
}).then((project) => {
  cy.visit(`/projects/${project.id}`)
  cy.get('[data-cy=project-title]').should('have.text', project.name)
})

Returning body makes the chain's contract narrower and easier to reason about. The final callback uses the created identifier at the correct queued time. If you want to continue after the page assertion, return the nested Cypress chain explicitly.

cy.request<{ id: string }>('/api/me').then(({ body }) => {
  return cy.get('[data-cy=current-user-id]')
    .invoke('text')
    .then((renderedId) => ({ apiId: body.id, renderedId }))
}).should(({ apiId, renderedId }) => {
  expect(renderedId).to.eq(apiId)
})

The last should(callback) asserts a settled object. For comprehensive request patterns, see Cypress cy.request API examples.

7. Use Aliases With Wrapped Data

Aliases provide a Cypress-managed name for data that several later chains need within a test. Wrap a plain fixture-derived object and call as(), then retrieve it with cy.get('@name'). Use a regular function when accessing a Mocha context alias through this, but the cy.get() style works with arrow functions and keeps timing explicit.

type TestUser = {
  email: string
  displayName: string
  role: 'member' | 'admin'
}

it('shows the signed-in identity', () => {
  const user: TestUser = {
    email: 'member@example.test',
    displayName: 'Casey Morgan',
    role: 'member',
  }

  cy.wrap(user, { log: false }).as('testUser')

  cy.get<TestUser>('@testUser').then((savedUser) => {
    cy.request('POST', '/test-support/login', savedUser)
    cy.visit('/account')
    cy.get('[data-cy=display-name]').should(
      'have.text',
      savedUser.displayName,
    )
  })
})

Do not alias secrets unless you have confirmed how your Cypress version, reporter, screenshots, and plugins expose subjects. Passing log: false only controls the wrap entry in the Command Log. It is not a complete secret-management policy.

Prefer closure scope when data is used once nearby. Prefer an alias when a Cypress-generated value has a meaningful role across several separated steps. Avoid using aliases as global mutable state between tests. Cypress resets aliases for each test, and tests should create their own preconditions.

8. Retryable should Followed by One-Time then

A reliable polling pattern waits with .should(), then performs one side effect with .then(). This separates a retryable observation from a single action.

cy.get('[data-cy=report-status]')
  .should('have.attr', 'data-state', 'ready')
  .then(($status) => {
    expect($status.text().trim()).to.eq('Report ready')
    cy.get('[data-cy=download-report]').click()
  })

cy.readFile('cypress/downloads/report.csv')
  .should('include', 'account_id,total')

Cypress can rerun the attribute assertion until the linked query finds the ready state. Only after it succeeds does .then() enqueue the click. Putting the click inside .should(callback) risks multiple downloads because that callback can run more than once.

When the desired condition requires parsing, keep the callback pure:

cy.get('[data-cy=sync-progress]').should(($progress) => {
  const value = Number($progress.attr('aria-valuenow'))
  expect(value).to.eq(100)
})
.then(() => {
  cy.get('[data-cy=continue]').click()
})

Do not use .then() plus expect() to poll a changing progress value. It gets one attempt. This distinction is a frequent interview topic and a frequent cause of CI-only failures. The Cypress retryability guide explains which queries can be retried together with assertions.

9. Conditional Example With Deterministic Server State

Conditional code is safe when its input is already settled. This example reads a response that definitively states whether a test account needs consent, then schedules the matching UI action. The branch is based on controlled server state rather than a race against DOM rendering.

type AccountState = {
  requiresConsent: boolean
}

cy.request<AccountState>('/api/me/state')
  .its('body')
  .then(({ requiresConsent }) => {
    cy.visit('/dashboard')

    if (requiresConsent) {
      return cy.get('[data-cy=consent-dialog]')
        .should('be.visible')
        .find('[data-cy=accept-consent]')
        .click()
    }

    return cy.get('[data-cy=dashboard-content]').should('be.visible')
  })

cy.get('[data-cy=dashboard-content]').should('be.visible')

Both branches return a Cypress chain, so Cypress waits for the chosen branch. The final assertion proves the shared outcome. For feature coverage, separate tests with explicit fixtures are usually stronger because each expected branch gets a descriptive test name and cannot disappear silently.

Avoid querying body once to decide whether an element that loads asynchronously exists. The snapshot may not include it yet. If the UI state is the contract, establish it through an API or fixture, then assert the expected branch directly. Conditional testing should handle legitimate variants, not uncertainty.

10. Production TypeScript Helper Example

A reusable helper should return its subject type and keep every return path consistent. This example creates a project through the API, returns the created model, and provides a separate UI helper that consumes it.

type Project = {
  id: string
  name: string
  state: 'active' | 'archived'
}

function createProject(name: string): Cypress.Chainable<Project> {
  return cy.request<Project>({
    method: 'POST',
    url: '/api/projects',
    body: { name },
  }).then((response) => {
    expect(response.status).to.eq(201)
    return response.body
  })
}

function openProject(project: Project): Cypress.Chainable<JQuery<HTMLElement>> {
  cy.visit(`/projects/${project.id}`)
  return cy.get('[data-cy=project-title]')
    .should('have.text', project.name)
}

it('archives a new project', () => {
  createProject('Migration rehearsal')
    .then(openProject)
    .then(() => {
      cy.get('[data-cy=project-menu]').click()
      cy.get('[data-cy=archive-project]').click()
    })

  cy.get('[data-cy=project-state]').should('have.text', 'Archived')
})

createProject() returns Cypress.Chainable<Project>, and openProject() accepts that project and returns the title query chain. The test composes them without external variables. The nested action callback intentionally has no value used afterward.

Keep helpers centered on domain operations. A helper named wrapAndThen() exposes mechanics rather than intent. Prefer names such as createProject(), readCartTotal(), or openInvoice(). Add custom commands only when their global chain syntax materially improves repeated use. Plain typed functions are often easier to unit test and maintain.

11. Fixture Transformation Example

cy.fixture() already returns a Cypress chain. Use .then() to validate and transform its yielded object, then return only the records needed by the scenario. Do not wrap the fixture command itself.

type AccountFixture = {
  accounts: Array<{
    id: string
    plan: 'free' | 'team'
    active: boolean
  }>
}

cy.fixture<AccountFixture>('accounts.json')
  .its('accounts')
  .then((accounts) => {
    const activeTeamAccounts = accounts.filter(
      (account) => account.active && account.plan === 'team',
    )
    expect(activeTeamAccounts, 'active team fixtures').not.to.be.empty
    return activeTeamAccounts
  })
  .then(([account]) => {
    return cy.request('POST', '/test-support/login', {
      accountId: account.id,
    })
  })
  .its('status')
  .should('eq', 204)

The first callback makes fixture assumptions explicit before any login request. Returning the filtered array changes the subject. The second callback returns the request chain, so the final status assertion receives the completed response.

Treat fixtures as input, not mutable global state. If a test changes an object in memory, another read may not express the intended original contract. Create a new object for modifications and keep generated test state in factories or API setup.

12. Window API and Browser Storage Example

cy.window() yields the application window after Cypress can access it. Use .then() to call a synchronous application API or inspect browser storage, then wrap a returned promise only when that API is asynchronous.

type PreferenceResult = {
  saved: boolean
  theme: 'light' | 'dark'
}

cy.window()
  .then((win) => {
    const app = win as typeof win & {
      preferences: {
        save(theme: 'light' | 'dark'): Promise<PreferenceResult>
      }
    }
    return cy.wrap(app.preferences.save('dark'))
  })
  .should('deep.equal', { saved: true, theme: 'dark' })

cy.reload()
cy.get('[data-cy="theme-toggle"]')
  .should('have.attr', 'aria-checked', 'true')

The application promise starts after cy.window() resolves because the call is inside the callback. The UI assertion after reload proves that the saved preference has user-visible effect.

Use this pattern only for an intentional application testing surface or component boundary. Do not bypass the user workflow so completely that the end-to-end test no longer covers meaningful behavior. Keep a separate UI path for theme selection, and use direct APIs mainly for focused setup or integration checks.

13. Error and Rejection Behavior Example

A rejected application promise passed to cy.wrap() fails the test. You should normally let that failure surface rather than adding a catch that converts it into a pass. When rejection is the expected business behavior, test through an API or application contract that represents the error as observable data.

type ValidationResult = {
  valid: boolean
  reasons: string[]
}

declare const couponService: {
  validate(code: string): Promise<ValidationResult>
}

cy.wrap(couponService.validate('EXPIRED-2025'))
  .then((result) => {
    expect(result.valid).to.eq(false)
    return result.reasons
  })
  .should('include', 'expired')

This promise fulfills with a negative domain result, so the test can assert it normally. If the service rejects for a transport failure, Cypress fails at wrap and preserves the real error.

Do not mix a custom catch() pattern into Cypress chains as if they were native promises. If the feature must show recovery from a failed request, intercept and stub the HTTP error, perform the UI action, and assert the visible fallback. That tests application behavior without suppressing an unexpected command failure.

Interview Questions and Answers

Q: Show a valid use of cy.wrap() with a DOM element.

After cy.get() yields a jQuery collection to .then(), I can call cy.wrap($element).click(). I use that element immediately and re-query for assertions after a render-changing action. This preserves Cypress actionability behavior without carrying a stale raw node.

Q: How do you transform text and pass a number to the next command?

I use invoke('text'), then return a number from .then(). That number becomes the next subject, so a following should('be.greaterThan', 0) receives it. I first add a retryable readiness assertion if the text can initially be empty.

Q: What is the correct way to wrap an application promise after a click?

I chain .then() after the click and invoke the promise-producing function inside that callback. I return cy.wrap(functionCall()) or return the promise directly. This controls invocation time rather than only waiting on a promise that already started.

Q: Why should side effects not run inside should(callback)?

Cypress may rerun that callback while retrying the linked query. A click, request, or file operation could therefore happen more than once. I keep the callback pure and place the side effect in a subsequent .then().

Q: How do aliases compare with closures for yielded values?

Closures are concise for nearby dependent work and make lexical data flow clear. Aliases are useful when a Cypress-produced subject has a meaningful name and is needed in separated steps within one test. I do not use either to share mutable state across independent tests.

Q: What should a typed Cypress helper return?

It should return Cypress.Chainable<T> where T matches the actual final yield. Every branch should resolve to a compatible subject. I avoid casting an incorrect subject simply to satisfy the compiler.

Q: When is wrapping a plain value unnecessary?

If a synchronous calculation only needs an immediate Chai assertion, expect(value) is clearer. I wrap when Cypress chain semantics add something useful, such as subject composition, aliases, built-in chain assertions, or integration with a helper that returns a chainable.

Common Mistakes

  • Wrapping cy.request() or cy.get() even though those calls already return Cypress chains.
  • Starting a promise before the intended UI action and assuming cy.wrap() will postpone it.
  • Using an outer variable before the queued callback assigns its value.
  • Mutating a list while iterating over the original jQuery collection with each().
  • Carrying a wrapped element through a state-changing render instead of re-querying it.
  • Putting a click or network request inside a callback that Cypress can retry.
  • Forgetting to return a nested chain whose result is needed by the next command.
  • Depending on Cypress's implicit last-command yield when an explicit return would be clearer.
  • Logging full API bodies, tokens, credentials, or customer data while debugging subjects.
  • Adding a custom command for every small transformation and hiding ordinary TypeScript data flow.
  • Using a conditional DOM snapshot to mask an application race.

Conclusion

The best cypress cy.wrap and cy.then example exposes its subject flow. Wrap an ordinary value only when it needs Cypress behavior, re-wrap callback elements for immediate Cypress actions, and create promises at the queued point where they should start. Use explicit returns so a reader can predict the next subject without knowing hidden callback details.

Choose one recipe that matches your scenario, replace the illustrative application contract, and run it in open mode while tracing every yield. If a test still flakes, check whether the observation should retry, whether the element was replaced, and whether async work began before Cypress reached the intended callback.

Interview Questions and Answers

Give a practical cy.wrap object example.

I can call cy.wrap(order).its('total').should('be.greaterThan', 0). The object becomes the chain subject, so I can traverse properties and apply Cypress assertions. I would use plain expect if no chain behavior adds value.

How do you safely use a jQuery element from .then()?

I inspect it synchronously with jQuery if needed, then use cy.wrap($element) for an immediate Cypress action. After any action that may re-render the component, I query the element again before asserting.

How do you pass API response data into a UI step?

I return response.body or a domain object from the request's .then() callback. The next .then() receives that typed object and can visit the relevant route. This avoids mutable outer variables.

What pattern separates retrying from side effects?

I use a query plus .should() to wait for the stable condition, followed by .then() for the one-time action. The should callback remains pure because Cypress may run it repeatedly.

How should a helper expose a transformed Cypress value?

The helper should return the Cypress chain and declare Cypress.Chainable<T> for its final subject. Callers can then compose the helper with .then(), .its(), and .should() without hidden timing.

When is a conditional .then() branch reliable?

It is reliable when the branch input is already settled, such as a controlled API response, cookie, or static configuration. I return a Cypress chain from each branch and assert a shared observable outcome. I avoid branching on an early DOM snapshot.

Why can wrapping a promise still produce wrong execution order?

The promise-producing function runs when JavaScript invokes it, before wrap receives the promise. If it must start later, I invoke it inside the .then() callback that follows the prerequisite Cypress commands.

Frequently Asked Questions

Can cy.wrap wrap an array?

Yes. cy.wrap(array) yields the same array for Cypress assertions and later chain operations. Plain array methods are often simpler when no Cypress chain behavior is needed.

How do I click an element received in cy.then?

Use cy.wrap($element).click() inside the callback. If the click causes a render, re-query the current element or outcome afterward instead of carrying the old node forward.

Can I return a number from cy.then?

Yes. A non-null synchronous value returned from .then() becomes the next subject. A following assertion or callback receives that number.

Should I wrap cy.request in cy.wrap?

No. cy.request() already returns a Cypress chainable. Chain .then(), .its(), or .should() directly from it.

How do I compare text from two Cypress elements?

Capture the first yielded primitive in a .then() closure, query the second value inside that callback, and return a combined object or assert there. Add retryable readiness assertions before freezing values.

Does cy.wrap preserve TypeScript types?

Cypress can infer many wrapped value types, and reusable helpers can declare Cypress.Chainable<T>. Confirm that the declared generic matches the actual final yield on every return path.

Can I use cy.wrap inside each?

Yes. Wrap the current DOM item to use Cypress traversal, actions, and assertions. Avoid changing the collection while iterating unless the behavior is explicitly designed and stable.

Related Guides