ScreenshotNeo

BlogHow-to

How to Control an Angular App from Cypress End-to-End Tests

Run Angular independently, control it through Cypress’s browser UI, and make startup API requests deterministic with intercepts, assertions, and practical fixes.

By the ScreenshotNeo team4 October 20269 min read

To control an Angular app from a Cypress end-to-end (E2E) test, start the Angular development server separately, set Cypress’s baseUrl, visit an app route with cy.visit(), interact with rendered controls, and assert visible outcomes. If Angular makes API requests during startup, register cy.intercept() before cy.visit() so Cypress can observe or stub them.

This tests the running application through the browser, as a user would. Keep the app server outside the spec; Cypress’s E2E guidance explicitly says, “Don’t try to start a web server from within Cypress scripts.” See the Cypress E2E guide and intercept API reference.

1. Start Angular and configure Cypress

Install Cypress in the Angular project if it is not already installed, then open Cypress and choose E2E testing to generate its initial configuration and support files. The examples below assume the project has Cypress configured and that the Angular development server is available at http://localhost:4200.

npm install --save-dev cypress
npx cypress open

Keep the server running in a separate terminal. For a standard Angular CLI project, the development command is commonly:

ng serve

Use the script your project defines if it differs. Set the URL in cypress.config.ts so specs can visit app-relative paths.

import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:4200',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
  },
})

If the project already has a Cypress config, add or adjust e2e.baseUrl without replacing its other settings. Cypress configuration options are documented in the configuration reference.

2. Visit the app and control it through the UI

A useful E2E test follows setup, action, assertion: visit the route, do what a user would do, and check the observable result. This runnable example assumes the Angular app has a task input, an Add button, and displays the submitted task.

describe('task list', () => {
  it('adds a task', () => {
    cy.visit('/')

    cy.get('[data-cy="task-input"]').type('Review pull request')
    cy.get('[data-cy="add-task"]').click()

    cy.contains('[data-cy="task-item"]', 'Review pull request')
      .should('be.visible')
  })
})

The selectors in this sample are a contract the app must implement. Add stable attributes to the relevant Angular template elements, for example:

<input data-cy="task-input" aria-label="New task" />
<button data-cy="add-task">Add</button>
<li data-cy="task-item">{{ task.name }}</li>

Cypress recommends dedicated data-* attributes when a test needs a stable selector. They avoid coupling the test to styling classes or incidental markup. Use cy.contains() when text is the meaningful user-facing identifier. See Cypress selector guidance.

Check navigation and form behavior

For navigation, assert the URL and then check the destination’s content. For forms, assert validation or the resulting state users can see.

cy.get('[data-cy="settings-link"]').click()
cy.location('pathname').should('eq', '/settings')
cy.get('h1').should('contain.text', 'Settings')

cy.get('[data-cy="email"]').type('not-an-email')
cy.get('[data-cy="save"]').click()
cy.get('[data-cy="email-error"]').should('be.visible')

Prefer assertions about rendered behavior over checking Angular internals. Cypress commands retry queries and assertions while the page updates, so a fixed sleep is usually unnecessary and can make a test slower or flaky. See Cypress retry-ability.

3. Intercept an API request Angular makes at startup

Angular may fetch configuration, a user profile, or page data while bootstrapping. Register the intercept before navigation because the request can begin before cy.visit() finishes. This example stubs a predictable response so the test does not depend on a running API.

describe('dashboard', () => {
  it('renders data returned during startup', () => {
    cy.intercept('GET', '/api/dashboard', {
      statusCode: 200,
      body: {
        greeting: 'Welcome, Ada',
        openTasks: 3,
      },
    }).as('getDashboard')

    cy.visit('/dashboard')

    cy.wait('@getDashboard').its('response.statusCode').should('eq', 200)
    cy.contains('Welcome, Ada').should('be.visible')
    cy.contains('3').should('be.visible')
  })
})

Choose the request matcher to match the actual request. For a fully qualified API host, match that URL or a pattern rather than only the relative path. You can also inspect the intercepted request and response:

cy.wait('@getDashboard').then(({ request, response }) => {
  expect(request.method).to.equal('GET')
  expect(response?.statusCode).to.equal(200)
})

Stubbed response or real backend?

Approach Use it when Tradeoff
Stub with cy.intercept() You need a controlled case such as empty data, an error, or a specific response. The test verifies the UI’s handling of the supplied response, not the backend integration.
Allow the request through You need to cover integration with a test backend and its data. The backend and test data must be available and predictable.

Cypress can observe, modify, or stub network traffic. Keep some tests focused on deterministic UI states and use real test-backend responses where integration behavior is the goal. An intercept that has no static response or request handler can spy on matching traffic while allowing it through.

Test loading and failure states

Intercepts also let a spec assert how the app behaves when the API is slow or fails. Make the response explicit so the case is repeatable.

cy.intercept('GET', '/api/dashboard', {
  statusCode: 503,
  body: { message: 'Service unavailable' },
}).as('getDashboard')

cy.visit('/dashboard')
cy.wait('@getDashboard')
cy.get('[data-cy="load-error"]').should('be.visible')

cy.intercept('GET', '/api/dashboard', {
  delay: 500,
  statusCode: 200,
  body: { greeting: 'Welcome back', openTasks: 0 },
}).as('slowDashboard')

cy.visit('/dashboard')
cy.get('[data-cy="loading-indicator"]').should('be.visible')
cy.wait('@slowDashboard')
cy.get('[data-cy="loading-indicator"]').should('not.exist')

Align the asserted loading state with the app’s design: a very fast response may make a transient indicator difficult to observe unless the test deliberately delays the stubbed response.

4. Handle caching and request timing

An intercept only sees requests that reach the browser’s network layer. If the browser serves a response from its HTTP cache, there may be no network request for Cypress to intercept. This can make an apparently correct route alias time out on repeat visits.

  • Register the intercept before cy.visit() or before the action that triggers the request.
  • Check the browser’s developer tools to see whether the response came from cache.
  • If the test is meant to exercise a network request, configure the test server to return appropriate cache-control headers for that endpoint.
  • Use a fresh or unique test URL only when that reflects a meaningful test setup; avoid hiding the underlying cache behavior.

Do not use a fixed cy.wait(1000) as a substitute for knowing when a request or page state is ready. Wait for an aliased request when network completion matters, then assert the UI outcome.

5. E2E tests versus Angular component tests

E2E test Component test
Unit under test The running app and an integrated user journey A component mounted directly in a browser
How it starts cy.visit('/route') cy.mount(Component)
Best for Routing, app wiring, and behavior across page boundaries Focused component behavior and rendering
Setup App server and E2E configuration Component testing support and component-specific providers/imports

For a whole Angular application journey, use E2E tests and visit the running app. Use Cypress Component Testing when the component itself is the unit you need to control. A component test does not replace visiting the application route. Cypress’s current Angular component-testing overview lists Angular ^21.0.0 and ^22.0.0 and requires @angular-devkit/build-angular; these are version-sensitive component-testing requirements, not a definition of all Angular E2E compatibility. Check the current Angular component testing guide before setting up that separate workflow.

6. Run specs in the browser or from the command line

Use Cypress’s interactive runner while developing a spec. For a command-line run, leave the Angular server running separately and invoke Cypress from another terminal:

npx cypress run --e2e

In continuous integration, the app server still needs to be available at the configured baseUrl before the E2E run begins. Use the project’s CI orchestration to start the server and wait for it to become ready; do not start it inside the Cypress spec. Keep test data isolated or reset between runs when the test backend is shared.

7. Troubleshooting Cypress and Angular E2E tests

Symptom Likely cause Fix
cy.visit() cannot connect Angular server is stopped, on another port, or Cypress baseUrl is wrong. Start the app in a separate terminal and make baseUrl match the server address.
Unknown or incorrect page at the route The app route differs, or the server does not serve the Angular app for that path. Verify the route in the app and that the development server handles it.
cy.wait('@alias') times out Intercept registered too late, matcher does not match, request never occurred, or response came from cache. Register before navigation/action, inspect the actual request URL and method, and check cache behavior.
Element not found Selector does not match the template, app has not rendered it, or a prior state differs. Verify the rendered markup and state; use a stable data-* selector and a retryable assertion.
Test passes locally but fails in CI Server readiness, environment configuration, test data, or timing differs. Confirm CI serves the configured URL, wait for readiness in orchestration, and make data setup deterministic.
Assertion sees stale content The test checks before the relevant request or UI update completes. Wait for the matching request or assert a state that Cypress can retry until it appears.

8. Performance, reliability, and cost

For reliable tests, keep each spec’s setup explicit, use stable selectors, stub external or variable API data where integration is not under test, and wait on meaningful network or UI conditions. Avoid arbitrary sleeps and shared mutable test data. Use real backend calls intentionally for integration coverage, and make the test environment responsible for repeatable data and server readiness.

Test runtime depends on the app, browser, network, and amount of setup; no universal timing figure applies. Cypress itself is a development dependency, while CI cost depends on the runners and infrastructure you choose. This browser-testing workflow does not require a screenshot API. If your test workflow separately needs screenshot files from URLs, ScreenshotNeo is a website screenshot API and MCP server; it is not a replacement for Cypress interaction tests.

Or skip the browser setup

If the task is to capture a page rather than exercise Angular controls, ScreenshotNeo can return an image or PDF from one request. See the ScreenshotNeo API documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
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, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status.
  • An MCP server lets AI agents, including Claude and Cursor, take screenshots.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can Cypress test an Angular app without mocking every API?

Yes. Let matching requests reach a test backend when the integration is part of the test. Stub only the responses that need controlled data or failure conditions.

Should I use cy.mount() to test my Angular page?

Use cy.mount() for a component test. To test an app route and the integrated experience, start the app and use cy.visit().

Why does an intercept work on the first visit but not the next?

The later response may come from browser cache and never reach the network layer. Check developer tools and the endpoint’s cache headers.

Do E2E tests prove the Angular backend is correct?

A UI test with a stub proves how the app handles that supplied response. A request reaching a real test backend covers more integration, but backend correctness also needs appropriate backend tests.