ScreenshotNeo

BlogHow-to

How to Test Form Submission in Cypress

Test Cypress form submissions from the user’s perspective, verify requests and responses, and cover validation, keyboard input, failures, and redirects.

By the ScreenshotNeo team4 October 20268 min read

To test form submission in Cypress, visit the page, fill in the fields, click the submit button, and assert the visible result. If the request matters, register cy.intercept() before submitting, wait for its alias, and check the request and response-driven UI. Use .submit() to exercise the form submit event directly, and test Enter-key submission separately if your users rely on it.

The selectors, endpoint, payload, and success state below are examples. Replace them with the behavior of your application. The patterns use Cypress’s documented commands for intercepting requests, network testing, and typing.

1. Test a successful form submission

Start with a test that performs the same interaction a user performs. Stub the response when you want a deterministic front-end test; the stub below returns a created user. Register the intercept before clicking so Cypress is ready to observe the request.

describe('new user form', () => {
  beforeEach(() => {
    cy.intercept('POST', '/users', {
      statusCode: 201,
      body: { id: 123, name: 'Ada Lovelace' },
    }).as('createUser')

    cy.visit('/users/new')
  })

  it('submits valid values and shows the created user', () => {
    cy.get('[data-testid="name"]').type('Ada Lovelace')
    cy.get('[data-testid="email"]').type('ada@example.test')
    cy.get('button[type="submit"]').click()

    cy.wait('@createUser').then(({ request, response }) => {
      expect(request.body).to.include({
        name: 'Ada Lovelace',
        email: 'ada@example.test',
      })
      expect(response.statusCode).to.equal(201)
    })

    cy.contains('Ada Lovelace').should('be.visible')
  })
})

This checks the interaction, the outgoing request data, the response status, and a visible outcome. Adjust the response body and assertions to match your app. If the application encodes form data as URL-encoded content or sends JSON with a different shape, assert that actual representation.

2. Choose what the test should prove

Approach Useful for What it does not establish by itself
Click submit and observe with cy.intercept() Visible user flow and the browser request it causes Whether the real backend behaves correctly when the response is stubbed
cy.get('form').submit() Form submit-event behavior That a user can find, enable, and operate the visible controls
.type('{enter}') Keyboard submission behavior Behavior for other form structures or disabled submit controls
cy.request() Direct endpoint checks and API setup or teardown That the browser UI submitted the form correctly

Use a UI-driven test for the user flow. A direct request is useful for API behavior or preparing test data, but Cypress documents that cy.intercept() observes application browser traffic, not a direct cy.request() call. See cy.request().

3. Assert the request and response

Match both the method and path when you know them. If no method is specified, an intercept may match requests with any method. An alias lets the test wait for the matching request and inspect it without arbitrary delays.

cy.intercept('POST', '/api/contact', (req) => {
  expect(req.body).to.include({ subject: 'Project inquiry' })
  req.reply({ statusCode: 200, body: { ok: true } })
}).as('sendContact')

cy.visit('/contact')
cy.get('[name="subject"]').type('Project inquiry')
cy.get('[name="message"]').type('Please contact me.')
cy.get('form').find('button[type="submit"]').click()

cy.wait('@sendContact').its('response.statusCode').should('eq', 200)
cy.contains('Message sent').should('be.visible')

For a controlled front-end test, stub the response. For an integration path, allow the request to reach a test backend and assert the returned result. Cypress supports both stubbing and passing requests through; decide which layer the test is intended to cover.

4. Test direct form submission and Enter

Calling .submit() triggers the form submission behavior directly. Use it when that event is the subject of the test, such as a handler attached to the form. It skips the user’s interaction with the submit control, so keep a click-based test for the normal user journey.

cy.visit('/users/new')
cy.get('[data-testid="name"]').type('Ada Lovelace')
cy.get('[data-testid="email"]').type('ada@example.test')
cy.get('form').submit()
cy.contains('User created').should('be.visible')

Where Enter should submit, test it explicitly. Native implicit submission depends on the form structure and submit-button configuration, including whether the button is disabled.

cy.intercept('POST', '/users', { statusCode: 201, body: { id: 123 } }).as('createUser')
cy.visit('/users/new')
cy.get('[data-testid="email"]').type('ada@example.test{enter}')
cy.wait('@createUser')
cy.contains('User created').should('be.visible')

For forms with several inputs, send Enter from the field where users are expected to press it. This catches differences in form markup and focus behavior.

5. Cover validation, server errors, and redirects

Client-side validation

Keep invalid-input coverage separate from the success case. Assert the actual validation message, invalid state, or disabled-submit behavior your product promises.

cy.visit('/users/new')
cy.get('[data-testid="email"]').type('not-an-email')
cy.get('button[type="submit"]').click()
cy.get('[data-testid="email-error"]')
  .should('be.visible')
  .and('contain', 'Enter a valid email')

If invalid data should never reach the server, also assert that no request was made using the application behavior and intercept setup appropriate to your Cypress version. Prefer checking the visible validation result as the primary user-facing assertion.

Server failure

Stub a failure to verify that the form keeps useful input and presents a recoverable error. Match the actual status and error shape consumed by your app.

cy.intercept('POST', '/users', {
  statusCode: 500,
  body: { message: 'Please try again' },
}).as('createUserFailure')

cy.visit('/users/new')
cy.get('[data-testid="name"]').type('Ada Lovelace')
cy.get('[data-testid="email"]').type('ada@example.test')
cy.get('button[type="submit"]').click()
cy.wait('@createUserFailure')
cy.contains('Please try again').should('be.visible')
cy.get('[data-testid="email"]').should('have.value', 'ada@example.test')

Redirects and cross-origin destinations

For a same-origin redirect, assert the destination or resulting page state. If submission redirects to another origin and Cypress commands must continue there, use cy.origin() for those commands. Cypress’s cross-origin guide discusses form submissions and redirected flows, including SSO-style cases.

6. Keep tests independent and reliable

  • Set up the page and relevant data in each test or shared setup. Do not depend on a previous test leaving the form in a particular state.
  • Register intercepts before the action that triggers them; intercepts are cleared before each test.
  • Wait on request aliases instead of using fixed sleeps. A sleep can be too short on a slow run and waste time on a fast one.
  • Use selectors that are stable and meaningful for tests, such as dedicated data-testid attributes or accessible labels.
  • Assert a user-visible outcome as well as the request when the behavior includes a UI response.
  • Keep stubbed UI tests and real-backend integration coverage distinct enough that a stub does not give false confidence about server behavior.

Cypress recommends independent tests that initialize their own state; see its best practices.

7. Troubleshooting common failures

Symptom Likely cause Fix
cy.wait('@submit') times out The intercept was registered after the action, its method or path does not match, validation prevented submission, or the app did not send a request. Register it before the click; check the browser request method and URL; verify the form is valid and the submit control is enabled.
The intercept never sees a cy.request() cy.request() is a direct HTTP call, not browser application traffic. Assert the direct request’s result directly, or use the UI to trigger a browser request when testing the form flow.
Enter does not submit The form structure, focused field, submit button configuration, or disabled state affects implicit submission. Check the rendered form and test Enter from the intended field; make sure the submit control is available.
The request is missing unexpectedly Client validation stopped submission, the app uses a different endpoint or method, or the request was served from cache where relevant. Inspect the app’s actual behavior and network traffic; correct the route matcher and ensure the test exercises a network request.
Test passes alone but fails in a suite It relies on state left by another test or shared mutable setup. Initialize the page and data independently in each test.
Commands fail after an external redirect The next page has a different origin. Use cy.origin() for subsequent interaction on that origin.
UI assertion runs before completion The test asserted a response-driven state before the request completed. Wait for the route alias, then assert the rendered result.

Cypress notes that browser caching can mean a request does not reach the network layer and therefore does not trigger an intercept. For form submissions, first verify that the application really issues the expected request.

8. Performance, reliability, and cost considerations

Network stubs make form UI tests predictable and avoid depending on a live service for every run. Keep a smaller set of tests against the real test backend when the integration contract matters. Alias-based waits improve reliability over timing guesses. Independent setup reduces order-dependent failures. Avoid adding redundant assertions for every implementation detail: focus on the user-visible result and the request contract your application depends on.

Cypress itself is a software testing framework; the relevant cost and runtime tradeoffs depend on your project’s Cypress setup, test environment, and how many real service calls you make. This guide does not assume a specific pricing plan or benchmark.

9. Or skip the browser setup

If you need a screenshot of the form or its post-submit state for debugging or documentation, ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as PNG, JPEG, WebP, or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/contact -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/contact"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/contact',
})
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
await Bun.write('shot.webp', res)

Cookie banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, and failed loads are not billed, and responses identify the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Get 1,000 free screenshots a month with no card.

10. FAQ

Should I stub the form’s POST request?

Stub it when isolating the front-end response behavior. Include real-backend coverage separately when the server integration is part of the requirement.

Does cy.get('form').submit() prove the submit button works?

No. It exercises submission directly. Click the visible submit control to verify the normal user interaction.

Can I use cy.intercept() to verify a direct API call?

It observes browser application traffic, not a direct cy.request(). Check the direct request’s response with that command.

Why should I avoid fixed waits?

They guess when a request will finish. Waiting on an intercept alias ties the test to the event it needs to observe.