---
title: "Testing the HREF Anchor Links"
url: "https://www.cypress.io/blog/testing-the-anchor-links"
description: "Even our open-source heroes can miss something. Did you test HREF anchor links to make sure users can navigate? Learn how to use Cypress for anchor link testing."
---

[Blog](/blog/)[Education: Tutorials](/blog/tag/tutorials)

# Testing the HREF Anchor Links

December 10, 2020

•

By Gleb Bahmutov

Once upon a time [Zach Bloomquist](https://github.com/flotwig?ref=cypress-io.ghost.io) and I were discussing how to achieve an all-time hero status. We were talking about our open source heroes of course, and I wanted to send Zach a link to the testing section of the [Develop, Preview, Test](https://rauchg.com/2020/develop-preview-test?ref=cypress-io.ghost.io) blog post written by [Guillermo Rauch](https://twitter.com/rauchg?ref=cypress-io.ghost.io). The testing section was in the middle of the post, I scrolled, clicked on the `#` link, and ... screamed in horror: the anchor URL was `#undefined`

The anchor link leads nowhere

Of course, Guillermo [quickly fixed](https://github.com/rauchg/blog/commit/b766f9d3e1dd685fc91938e31e36be850ee9e40e?ref=cypress-io.ghost.io) the missing element IDs. Yet even then, he missed one of the problems and had to make [another commit](https://github.com/rauchg/blog/commit/5aa648ca201b798e9e345516a1d0560ee344024e?ref=cypress-io.ghost.io). Which brings to mind this [popular tweet](https://twitter.com/rauchg/status/807626710350839808?ref=cypress-io.ghost.io):

Write tests. Not too many. Mostly integration.

## Anchor Link Testing — How It’s Done

Let's write an integration test to ensure that none of our anchor links _ever_ go undefined. Luckily I already had a [fork of Guillermo's blog](https://github.com/bahmutov/blog-1?ref=cypress-io.ghost.io) for another blog post of mine called [Components People Test](https://glebbahmutov.com/blog/components-people-test/?ref=cypress-io.ghost.io) so everything was ready.

The blog runs on the [Next.js](https://nextjs.org/?ref=cypress-io.ghost.io) system, a simple `yarn dev` and the posts are available at `localhost:3000`. Perfect. Let's start writing a `cypress/integration/spec.js` test file (get it? "mostly integration").

## Checking an anchor

```js
describe("Blog", () => {
  it("has anchor tags", () => {
    cy.visit("2020/develop-preview-test");
    cy.contains("a", "#");
  });
});
```

I place the `localhost:3000` base url into the `cypress.json` file so I do not have to repeat myself in every test. The test runs and finds ... a single `<a>` element with text `#` inside. It is not even our anchor tag - it is just the first tag matching the command.

Hmm, [cy.contains](https://on.cypress.io/contains?ref=cypress-io.ghost.io) returns the first matching element, we could probably write a test and fix every anchor element one by one.

**Tip:** the Command Log shows a crossed eye icon. This unfortunate event happens when the found element only becomes visible on hover. For our purposes, it does not matter if the element is visible or note - we are only interested in its `href` attribute.

The anchor element is hidden by default, but present in the DOM

The test can simply check if the `href` attribute is equal to the string value `#undefined`:

```js
it("has anchor tags", () => {
  cy.visit("2020/develop-preview-test");
  cy.contains("a", "#").should("not.have.attr", "href", "#undefined");
});
```

The test works - it fails as expected

Yet we do not know which element has the invalid attribute, and it would be hard to find it in the long post. Here is a little trick - use [.scrollIntoView](https://on.cypress.io/scrollintoview?ref=cypress-io.ghost.io) command to bring the element into the viewport.

```js
it("has anchor tags", () => {
  cy.visit("2020/develop-preview-test");
  cy.contains("a", "#")
    .scrollIntoView()
    .should("not.have.attr", "href", "#undefined");
});
```

Much better - the heading with the undefined anchor is now at the top of the viewport.

Even better would be to print the H2 element that contains the anchor. Here is the markup we are working with:

The A element is for H2 element

Let's print the H2 element when making the assertion.

```js
it("has anchor tags", () => {
  cy.visit("2020/develop-preview-test");
  cy.contains("a", "#")
    .scrollIntoView()
    .should($a => {
      const message = $a.parent().parent().text();
      expect($a, message).to.not.have.attr("href", "#undefined");
    });
});
```

Beautiful - the screenshot shows the heading element with missing anchor.

One more trick - we could replace [cy.contains](https://on.cypress.io/contains?ref=cypress-io.ghost.io) that returns a single element with [cy.get](https://on.cypress.io/get?ref=cypress-io.ghost.io) command, which returns all elements matching the selector. Because `cy.get` allows any jQuery selector, we could use [:contains](https://api.jquery.com/contains-selector/?ref=cypress-io.ghost.io) to find anchor elements with text containing "#". Then we can iterate over every element using [.each](https://on.cypress.io/each?ref=cypress-io.ghost.io) Cypress command.

```js
it("has anchor tags using cy.get and .each", () => {
  cy.visit("2020/develop-preview-test");
  cy.get("a:contains(#)").each($a => {
    const message = $a.parent().parent().text();
    expect($a, message).to.not.have.attr("href", "#undefined");
  });
});
```

We lost the scroll command, but our text message is still enough. The Command Log also shows that there are four anchor elements. Let's fix the anchor tags until the integration test passes.

Great - notice how the passing assertions show the heading text. The four anchor elements are all accounted for.

## Checking every post

We have written a test for a single blog post. But the site contains multiple posts - all in the `pages` subfolders:

We probably should check every post against the broken anchors. This is an example of [dynamic tests](https://github.com/cypress-io/cypress-example-recipes?ref=cypress-io.ghost.io#fundamentals) - we need to generate tests from the data. In this case, we do not even need to glob the folder - all posts are linked in ... `posts.json` file:

```json
{
  "posts": [
    {
      "id": "books-people-reread",
      "date": "August 2, 2020",
      "title": "Books people re(read)"
    },
    {
      "id": "develop-preview-test",
      "date": "June 11, 2020",
      "title": "Develop, Preview, Test"
    },
    ...
  ]
}
```

In our spec we can import this JSON file directly - even destructure the import to grab just the `posts` property. Then we can iterate over every post, creating a separate `it` test block. We just need to extract the year to form the right URL to visit.

```js
import { posts } from "../../posts.json";
context("Post", () => {
  posts.forEach(post => {
    it(`"${post.title}" has no broken anchors`, () => {
      const year = new Date(post.date).getFullYear();
      const url = `${year}/${post.id}`;
      cy.visit(url);
      cy.get("a:contains(#)").each($a => {
        const message = $a.parent().parent().text();
        expect($a, message).to.not.have.attr("href", "#undefined");
      });
    });
  });
});
```

Almost works. Most posts are checked successfully, but there are a couple of posts with no anchor links - and thus the `cy.get(...)` command fails because it does expect to find at least one element.

Here is a little trick to avoid the problem. Every page has a few links in the navigation element at the top. There is the logo link, the source link, and "Follow me" link.

![A screenshot showing that the HREF anchor link test works as intended.](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2020/12/Screen-Shot-2020-12-01-at-11.25.22-PM.png)

Let's grab heading links AND one of those always-present anchors - and validate them all. We can use `,` operator to combine selectors like this:

```js
cy.get("a:contains(#), a.src").each($a => {
  const message = $a.parent().parent().text();
  expect($a, message).to.not.have.attr("href", "#undefined");
});
```

Now every dynamically generated test passes. Every blog post is ok.

![A screenshot showing how to fix the problem by telling the program to register navigation elements as HREF anchors on the page.](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2020/12/Screen-Shot-2020-12-01-at-11.24.56-PM.png)

## Checking every link

Finally, if we are checking some anchor links, then we probably should check them all. After all - if somehow we got `#undefined`, then it is possible to get `undefined` in the middle - after all, JavaScript is notorious for typecasting values into links like `/posts/undefined/comments/`

Let's check every anchor element against this problem.

```js
cy.visit(url);
cy.get("a").each($a => {
  const message = $a.text();
  expect($a, message).to.have.attr("href").not.contain("undefined");
});
```

Notice how every `expect` assertion really has two assertions chained together. First, we confirm that the anchor element has `href` attribute. Second, the value of that attribute is checked against containing the string "undefined". There are a lot of anchors, and every anchor is checked this way.

![A screenshot showing how the assertions work within the test.](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2020/12/Screen-Shot-2020-12-01-at-11.37.18-PM.png)

All links are 👍

You can find the source code and the tests [here](https://github.com/bahmutov/blog-1?ref=cypress-io.ghost.io).

## **We'd like to hear from you!**

The Cypress team has been working hard to deliver this improved experience. We're excited to bring these new APIs to Cypress users, and as always, we're eager to hear your feedback.

You can submit an [issue](https://github.com/cypress-io/cypress/issues?ref=cypress.io) on Github or chat with us on our [Discord](https://discord.gg/Cd4CdSx?ref=cypress.io). Especially while this feature is experimental, issue submissions are critical. Thanks for your support, and happy testing!

\-Cypress Team