chore(e2e): Add Cypress for link checking and end-to-end tests. Fix broken links revealed by tests.

- Adds Cypress and a few basic tests for the global topnav, the home page, and link-checking.
- For link-checking, pass a comma-delimited list of URLs in an exported cypress_test_subjects environment variable. For examples, see the convenience commands in package.json
pull/5816/head
Jason Stirnaman 2025-02-02 23:05:28 -06:00
parent 3be6e79a76
commit ef106dd3a1
10 changed files with 2546 additions and 83 deletions

29
cypress.config.js Normal file
View File

@ -0,0 +1,29 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
baseUrl: 'http://localhost:1313',
projectId: 'influxdata-docs',
setupNodeEvents(on, config) {
// implement node event listeners here
on('task', {
// Fetch the product list configured in /data/products.yml
getData(filename) {
return new Promise((resolve, reject) => {
const yq = require('js-yaml');
const fs = require('fs');
const cwd = process.cwd();
try {
resolve(
yq.load(fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8'))
);
} catch (e) {
reject(e);
}
});
},
});
},
},
});

View File

@ -0,0 +1,27 @@
/// <reference types="cypress" />
describe('Article links', () => {
const subjects = Cypress.env('test_subjects').split(',');
subjects.forEach((subject) => {
it('contains valid internal links', function () {
cy.visit(`${subject}`);
cy.get('article a[href^="/"]') //.filter('[href^="/"]')
.each(($a) => {
cy.log(`** Testing internal link ${$a.attr('href')} **`);
// cy.request doesn't show in your browser's Developer Tools
// because the request comes from Node, not from the browser.
cy.request($a.attr('href')).its('status').should('eq', 200);
});
});
it('contains valid external links', function () {
cy.visit(`${subject}`);
cy.get('article a[href^="http"]')
.each(($a) => {
// cy.request doesn't show in your browser's Developer Tools
cy.log(`** Testing external link ${$a.attr('href')} **`);
// because the request comes from Node, not from the browser.
cy.request($a.attr('href')).its('status').should('eq', 200);
});
});
});
});

39
cypress/e2e/index.cy.js Normal file
View File

@ -0,0 +1,39 @@
/// <reference types="cypress" />
describe('Docs home', function() {
beforeEach(() => cy.visit('/'));
it('has metadata', function() {
cy.title().should('eq', 'InfluxData Documentation');
});
it('can search with mispellings', function() {
cy.get('.sidebar--search').within(() => {
cy.get('input#algolia-search-input').type('sql uery');
cy.get('#algolia-autocomplete-listbox-0')
.should('contain', 'Basic query examples')
cy.get('input#algolia-search-input')
.type('{esc}')
cy.get('#algolia-autocomplete-listbox-0')
.should('not.be.visible');
});
});
it('main heading', function() {
cy.get('h1').should('contain', 'InfluxData Documentation');
});
it('content has links to all products', function() {
cy.task('getData', 'products').then((productData) => {
Object.values(productData).forEach((p) => {
let name = p.altname?.length > p.name.length ? p.altname : p.name;
name = name.replace(/\((.*)\)/, '$1');
cy.get('.home-content a').filter(`:contains(${name})`).first().click();
const urlFrag = p.latest.replace(/(v\d+)\.\w+/, '$1');
cy.url().should('include', urlFrag);
cy.go('back');
});
});
});
});

40
cypress/e2e/topnav.cy.js Normal file
View File

@ -0,0 +1,40 @@
describe('global top navigation', function () {
beforeEach(function () {
// Visit the Docs home page
cy.visit('/');
cy.get('.notification').filter(':visible').find('.close-notification').click({ force: true });
});
describe('theme switcher', function () {
it('switches light to dark', function () {
// Default is light theme
cy.get('body.home').should('have.css', 'background-color', 'rgb(243, 244, 251)');
cy.get('#theme-switch-dark').click();
cy.get('body.home').should('have.css', 'background-color', 'rgb(7, 7, 14)');
cy.get('#theme-switch-light').click();
cy.get('body.home').should('have.css', 'background-color', 'rgb(243, 244, 251)');
});
});
describe('product dropdown', function () {
it('has links to all products', function () {
cy.get('#product-dropdown .selected').contains('Select product').click({ force: true });
cy.task('getData', 'products').then((productData) => {
Object.values(productData).forEach((p) => {
cy.get('#dropdown-items a').should('be.visible');
let name = p.altname?.length > p.name.length ? p.altname : p.name;
name = name.replace(/\((.*)\)/, '$1');
cy.get('#dropdown-items a')
.filter(`:contains(${name})`)
.first()
.click();
const urlFrag = p.latest.replace(/(v\d+)\.\w+/, '$1');
cy.url().should('include', urlFrag);
// Test that the selected option is for the current product.
// Reopen the dropdown.
cy.get('#product-dropdown .selected').contains(name).click({ force: true });
});
});
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

17
cypress/support/e2e.js Normal file
View File

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'

View File

@ -11,7 +11,9 @@
"autoprefixer": ">=10.2.5",
"eslint": "^9.18.0",
"globals": "^15.14.0",
"cypress": "^14.0.1",
"hugo-extended": ">=0.101.0",
"nightwatch": "^3.11.0",
"postcss": ">=8.4.31",
"postcss-cli": ">=9.1.0",
"prettier": "^3.2.5",
@ -27,6 +29,10 @@
"vanillajs-datepicker": "^1.3.4"
},
"scripts": {
"e2e:chrome": "npx cypress run --browser chrome",
"e2e:o": "npx cypress open",
"e2e:o:links": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/,http://localhost:1313/influxdb3/enterprise/\"; npx cypress open cypress/e2e/article-links.cy.js",
"e2e:links": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/,http://localhost:1313/influxdb3/enterprise/\"; npx cypress run --spec cypress/e2e/article-links.cy.js",
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
"pre-commit": "lefthook run pre-commit",
"test-content": "docker compose --profile test up"

2446
yarn.lock

File diff suppressed because it is too large Load Diff