Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate link-checking #5889

Merged
merged 2 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
const { defineConfig } = require('cypress');
const process = require('process');

module.exports = defineConfig({
e2e: {
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
baseUrl: 'http://localhost:1313',
defaultCommandTimeout: 10000,
pageLoadTimeout: 30000,
responseTimeout: 30000,
experimentalMemoryManagement: true,
numTestsKeptInMemory: 5,
projectId: 'influxdata-docs',
setupNodeEvents(on, config) {
// implement node event listeners here
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// Force Chrome to use a less memory-intensive approach
launchOptions.args.push('--disable-dev-shm-usage');
launchOptions.args.push('--disable-gpu');
launchOptions.args.push('--disable-extensions');
return launchOptions;
}
});
on('task', {
// Fetch the product list configured in /data/products.yml
getData(filename) {
Expand All @@ -24,6 +39,7 @@ module.exports = defineConfig({
});
},
});
return config;
},
},
});
129 changes: 112 additions & 17 deletions cypress/e2e/content/article-links.cy.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,122 @@
/// <reference types="cypress" />

describe('Article links', () => {
const subjects = Cypress.env('test_subjects').split(',');
// Always use HEAD for downloads to avoid timeouts
const useHeadForDownloads = true;

// Helper function to identify download links - improved
function isDownloadLink(href) {
// Check for common download file extensions
const downloadExtensions = [
'.pdf', '.zip', '.tar.gz', '.tgz', '.rar', '.exe', '.dmg', '.pkg',
'.deb', '.rpm', '.xlsx', '.csv', '.doc', '.docx', '.ppt', '.pptx'
];

// Check for download domains or paths
const downloadDomains = [
'dl.influxdata.com',
'downloads.influxdata.com'
];

// Check if URL contains a download extension
const hasDownloadExtension = downloadExtensions.some(ext =>
href.toLowerCase().endsWith(ext)
);

// Check if URL is from a download domain
const isFromDownloadDomain = downloadDomains.some(domain =>
href.toLowerCase().includes(domain)
);

// Return true if either condition is met
return hasDownloadExtension || isFromDownloadDomain;
}

// Helper function to make appropriate request based on link type
function testLink(href) {
if (useHeadForDownloads && isDownloadLink(href)) {
cy.log(`** Testing download link with HEAD: ${href} **`);
cy.request({
method: 'HEAD',
url: href,
failOnStatusCode: false,
timeout: 10000 // 10 second timeout for download links
}).then(response => {
expect(response.status).to.be.lt(400);
});
} else {
cy.log(`** Testing link: ${href} **`);
cy.request({
url: href,
failOnStatusCode: false,
timeout: 30000 // 30 second timeout for regular links
}).then(response => {
expect(response.status).to.be.lt(400);
});
}
}

subjects.forEach((subject) => {
it('contains valid internal links', function () {
it(`contains valid internal links on ${subject}`, function () {
cy.visit(`${subject}`);

// Test internal links (including anchor links)
cy.get('article a[href^="/"]').each(($a) => {
const href = $a.attr('href');
testLink(href);
});
});

it(`checks anchor links on ${subject} (with warnings for missing targets)`, 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);
});

// Track missing anchors for summary
const missingAnchors = [];

// Process anchor links individually
cy.get('article a[href^="#"]').each(($a) => {
const href = $a.prop('href'); // Use prop() instead of attr()
if (href && href.length > 1) { // Skip empty anchors (#)
// Get just the fragment part
const url = new URL(href);
const anchorId = url.hash.substring(1); // Remove the # character

if (!anchorId) {
cy.log(`Skipping empty anchor in ${href}`);
return;
}

// Use DOM to check if the element exists, but don't fail if missing
cy.window().then(win => {
const element = win.document.getElementById(anchorId);
if (element) {
cy.log(`✅ Anchor target exists: #${anchorId}`);
} else {
// Just warn about the missing anchor
cy.log(`⚠️ WARNING: Missing anchor target: #${anchorId}`);
missingAnchors.push(anchorId);
}
});
}
}).then(() => {
// After checking all anchors, log a summary
if (missingAnchors.length > 0) {
cy.log(`⚠️ Found ${missingAnchors.length} missing anchor targets: ${missingAnchors.join(', ')}`);
} else {
cy.log('✅ All anchor targets are valid');
}
});
it('contains valid external links', function () {
});

it(`contains valid external links on ${subject}`, 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);
});

// Test external links
cy.get('article a[href^="http"]').each(($a) => {
const href = $a.attr('href');
testLink(href);
});
});
});
});
});
Binary file not shown.
Binary file not shown.
79 changes: 79 additions & 0 deletions cypress/support/map-files-to-urls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node

import { execSync } from 'child_process';
import process from 'process';

// Get file paths from command line arguments
const filePaths = process.argv.slice(2);

// Parse options
const debugMode = process.argv.includes('--debug');

// Filter for content files
const contentFiles = filePaths.filter(file =>
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
);

if (contentFiles.length === 0) {
console.log('No content files to check.');
process.exit(0);
}

// Map file paths to URL paths
function mapFilePathToUrl(filePath) {
// Remove content/ prefix
let url = filePath.replace(/^content/, '');

// Handle _index files (both .html and .md)
url = url.replace(/\/_index\.(html|md)$/, '/');

// Handle regular .md files
url = url.replace(/\.md$/, '/');

// Handle regular .html files
url = url.replace(/\.html$/, '/');

// Ensure URL starts with a slash
if (!url.startsWith('/')) {
url = '/' + url;
}

return url;
}

const urls = contentFiles.map(mapFilePathToUrl);
const urlList = urls.join(',');

console.log(`Testing links in URLs: ${urlList}`);

// Create environment object with the cypress_test_subjects variable
const envVars = {
...process.env,
cypress_test_subjects: urlList,
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
};

// Run Cypress tests with the mapped URLs
try {
// Choose run mode based on debug flag
if (debugMode) {
// For debug mode, set the environment variable and open Cypress
// The user will need to manually select the test file
console.log('Opening Cypress in debug mode.');
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');

execSync('npx cypress open --e2e', {
stdio: 'inherit',
env: envVars
});
} else {
// For normal mode, run the test automatically
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
stdio: 'inherit',
env: envVars
});
}
} catch (error) {
console.error('Link check failed');
process.exit(1);
}
4 changes: 2 additions & 2 deletions layouts/partials/article/alpha.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h4>{{ $displayName }} is in Public Alpha</h4>
invite you to <strong>join our public channels</strong> for updates and to share feedback.
</p>
<div class="expand-wrapper">
<div class="expand" id="#alpha-expecations">
<div class="expand" id="alpha-expecations">
<p class="expand-label">
<span class="expand-toggle"></span><span>Alpha expectations and recommendations</span>
</p>
Expand All @@ -39,7 +39,7 @@ <h4>{{ $displayName }} is in Public Alpha</h4>
</ul>
</div>
</div>
<div class="expand" id="#alpha-feedback-channels">
<div class="expand" id="alpha-feedback-channels">
<p class="expand-label">
<span class="expand-toggle"></span><span>Join our public channels</span>
</p>
Expand Down
31 changes: 31 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,37 @@ pre-commit:
run: '.ci/vale/vale.sh
--config=content/influxdb/v2/.vale.ini
--minAlertLevel=error {staged_files}'

# Link checking for InfluxDB v2
v2-links:
tags: test,links,v2
glob: "content/influxdb/v2/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'

# Link checking for InfluxDB v3 core
v3-core-links:
tags: test,links,v3
glob: "content/influxdb3/core/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'

# Link checking for InfluxDB v3 enterprise
v3-enterprise-links:
tags: test,links,v3
glob: "content/influxdb3/enterprise/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'

# Link checking for Cloud products
cloud-links:
tags: test,links,cloud
glob: "content/influxdb/{cloud,cloud-dedicated,cloud-serverless}/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'

# Link checking for Telegraf
telegraf-links:
tags: test,links
glob: "content/telegraf/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'

cloud-pytest:
glob: content/influxdb/cloud/**/*.md
tags: test,codeblocks,v2
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@
"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/content/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/content/article-links.cy.js",
"e2e:api-docs": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/api/,http://localhost:1313/influxdb3/enterprise/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/management/,http://localhost:1313/influxdb3/cloud-dedicated/api/management/\"; npx cypress run --spec cypress/e2e/content/article-links.cy.js",
"e2e:o:links": "node cypress/support/map-files-to-urls.mjs content/influxdb3/core/get-started/_index.md --debug",
"e2e:api-docs": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/api/,http://localhost:1313/influxdb3/enterprise/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/management/,http://localhost:1313/influxdb3/cloud-dedicated/api/management/\"; 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"
Expand Down
Loading