ScreenshotNeo

BlogHow-to

How to Test Anchor Links With Cypress

Test the exact link behavior your users depend on: href values, same-page fragments, internal routes, external destinations, and generated anchors.

By the ScreenshotNeo team4 October 20269 min read

The right Cypress test depends on what “works” means for the anchor: its declared href, a changed URL after clicking, arrival at the correct section, or valid values across a generated set of links. Assert that behavior directly. For a same-page fragment, click the link and verify both the location hash and target; for an uncontrolled external destination, verify the href without navigating to a third-party site.

1. Set up Cypress and choose the behavior to verify

Use a baseUrl so tests can visit application routes without embedding a local host or port. A representative configuration is:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000'
  }
})

Start the application using your project’s normal development or test command, then run Cypress in the project’s configured mode. The application and its routes must be reachable at baseUrl. Cypress’s first-test guide uses the same basic pattern of visiting a page, clicking a link, and checking the resulting URL.

Before writing the assertion, identify the requirement:

Requirement What to assert
The link is configured correctly The exact href attribute
A same-page link reaches a fragment The location hash and target element
An internal link navigates The resulting path or URL and destination content
An external link points to the right place The href; navigate only when the destination is controlled and necessary
Generated links are well formed Each intended anchor has a non-malformed href

Use stable, application-owned selectors such as data-cy where available. They make clear which element the test interacts with and avoid coupling the test to incidental styling.

A fragment link such as #pricing points to a location in the current document. Test the click if the interaction itself matters, then verify the hash and that the target exists:

describe('page anchor links', () => {
  it('moves to the pricing section', () => {
    cy.visit('/plans')
    cy.get('[data-cy="pricing-link"]').click()

    cy.location('hash').should('eq', '#pricing')
    cy.get('#pricing').should('exist')
  })
})

The target can be identified with an id, for example <section id="pricing">. If the requirement includes scrolling the section into view, assert visibility too:

cy.get('#pricing').should('be.visible')

A hash and an existing element do not by themselves prove that the target is visible in the viewport. Include the visibility assertion only when that is part of the behavior you need to protect.

3. Test internal navigation and destination content

For an internal route, click the link and verify the resulting location and a meaningful element on the destination page. The URL confirms navigation; the content assertion confirms the expected page rendered.

it('opens the account page', () => {
  cy.visit('/')
  cy.get('[data-cy="account-link"]').click()

  cy.url().should('include', '/account')
  cy.get('h1').should('contain', 'Account')
})

cy.url() yields the active full URL. Cypress retries chained assertions until they pass or time out, so assert the state you expect rather than adding an arbitrary wait. If the path is stable, an exact path assertion avoids accidentally matching a different route with a similar substring:

cy.location('pathname').should('eq', '/account')

For a link whose navigation is central to a user journey, check both the new location and the resulting page state. A URL-only assertion can pass even if the destination content is wrong.

When the requirement concerns the address in the markup, inspect the attribute directly. This keeps the test focused on link configuration and avoids making it depend on a destination server.

it('points to the expected documentation page', () => {
  cy.visit('/')
  cy.get('[data-cy="docs-link"]')
    .should('have.attr', 'href', 'https://docs.example.test/guide')
})

Replace the example destination with the expected address in your application. Prefer an exact expected value when it is stable. If the link is intentionally generated from data, assert the relevant path or fragment derived from that same test data; a loose substring can match an incorrect URL.

This assertion verifies the declared address. It does not prove that the destination responds, that navigation succeeds, or that a fragment target exists on another page.

For a third-party site your team does not control, Cypress recommends checking the configured href instead of navigating there. That keeps a link test from depending on the external site’s availability or behavior.

it('links to the vendor guide', () => {
  cy.visit('/')
  cy.get('[data-cy="vendor-guide"]')
    .should('have.attr', 'href', 'https://vendor.example.test/guide')
})

If your team controls the destination and the test needs to verify its page, Cypress provides cy.origin() for commands on another origin. Follow the current Cypress cross-origin guidance for the installed Cypress version and keep the assertions focused on the behavior under your control. A secure page linking to an insecure destination can fail because of mixed content; treat that as a security issue to investigate rather than hiding it with a test workaround.

6. Check generated anchors across a page

For article content, generated navigation, or other repeated links, iterate the intended set and report enough context to identify a bad value:

it('renders content links with usable href values', () => {
  cy.visit('/guide')
  cy.get('article a').each(($link) => {
    const label = $link.text().trim()
    const href = $link.attr('href')

    expect($link, label).to.have.attr('href')
    expect(href, label).not.to.contain('undefined')
  })
})

The assertion catches missing href attributes and values containing undefined. Add checks for other malformed values only when they correspond to real bugs your application can generate. Include link text in assertion messages so a failure points to the relevant link.

Think deliberately about an empty set. Some pages may have no content anchors, and cy.get('article a') will fail if none exist. If at least one link is a page requirement, that failure is useful. If an empty set is valid, make the test handle it deliberately or scope the check to a section that is expected to contain links. This checks markup integrity; it does not check whether every destination URL resolves over the network.

  1. Choose the narrowest meaningful assertion. Check href for configuration, hash or path for location, and destination content for rendered state.
  2. Use stable selectors. Prefer an application-owned selector when the link needs to be targeted directly.
  3. Re-query after clicking. A click can rerender or remove the original element. Start a new chain from cy for the resulting page or location rather than relying on the clicked element as the subject.
  4. Let retryable assertions wait. Assert the expected URL or DOM state instead of sleeping for a fixed duration.
  5. Isolate external dependencies. For uncontrolled domains, assert the href. Test live navigation only when it is genuinely part of the requirement and the destination is controlled.

8. Troubleshooting common failures

Symptom Likely cause Fix
cy.visit('/plans') cannot reach the page The app is not running at the configured baseUrl, or the route differs. Start the app, check the configured origin and confirm the route manually.
The click times out because Cypress cannot find the link The selector is wrong, the link has not rendered, or the page has no matching anchor. Inspect the rendered DOM, correct the selector, and assert the relevant page state before looking for the link.
The expected hash never appears The clicked link has a different fragment, the element is not a fragment link, or application code changes navigation. Inspect the actual href, then assert the intended hash or route based on the page’s behavior.
The hash changes but the target assertion fails The document has no matching id, or the target renders conditionally. Match the fragment to the target id and wait on a meaningful rendering assertion if it is conditional.
An href assertion fails despite a link looking correct The rendered value differs from the expected absolute URL, or a relative URL is intentional. Inspect the actual attribute and choose an exact assertion suited to the intended relative or absolute value.
A cross-origin click fails The test depends on an uncontrolled origin, Cypress cross-origin handling, or a secure-to-insecure transition. For an uncontrolled destination, assert the href. For a controlled destination, use the current cy.origin() guidance and investigate mixed content.
A generated-link test fails on a page with no anchors The test assumes the collection is non-empty. Decide whether zero links is valid; encode that requirement explicitly and scope the integrity check accordingly.
A test passes on the URL but the wrong page appears The URL assertion is too broad or does not check rendered content. Use an exact path when stable and assert a destination heading or other identifying state.

9. Reliability, runtime, and cost considerations

These Cypress tests exercise the browser and application, so their reliability depends on stable selectors, deterministic test data, and the state the app renders. A markup-only href check is generally more isolated than a live cross-origin navigation check because it does not depend on a remote server. A click-and-destination test covers more user behavior but also depends on the application route and its rendering.

Keep broad generated-link checks scoped to the anchors that matter. Retrying assertions are useful for asynchronous rendering, while arbitrary sleeps add waiting without tying it to a condition. External network checks can be slow or fail for reasons outside your app; do not turn a configuration test into a remote availability test by accident. No universal runtime or cost figure applies to these patterns; it depends on your app, browser run environment, and how many pages and links you inspect.

10. Frequently asked questions

Should I click every anchor in the page?

No. Click links whose navigation behavior is part of the user journey. For broad content checks, inspect href values; reserve navigation assertions for representative or critical paths.

Does checking the href prove the destination works?

No. It verifies the address declared by the page. A separate test is needed to verify a controlled destination or its network response.

A fragment such as #pricing identifies a location within a document. Assert the hash and matching target; test scrolling only if visibility in the viewport is part of the requirement.

Assert its expected href. This verifies your outgoing link without making the test depend on a third party.

11. Or skip the browser setup

If your task also needs a screenshot of the page or destination, ScreenshotNeo provides a website screenshot API and MCP server. The Cypress assertions above remain the right way to verify your application’s anchor behavior; a screenshot can help inspect the rendered result.

Make one GET request with a URL. See the ScreenshotNeo API documentation for request options:

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}`);

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers indicate the page verdict and billing status. Its MCP server gives AI agents screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.