Skip to content

Commit cfe3c40

Browse files
committed
chore(ci): closes #5887 Improve and automate pre-commit link-checking
1 parent 4941aec commit cfe3c40

8 files changed

+241
-21
lines changed

cypress.config.js

+16
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
const { defineConfig } = require('cypress');
2+
const process = require('process');
23

34
module.exports = defineConfig({
45
e2e: {
56
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
67
baseUrl: 'http://localhost:1313',
8+
defaultCommandTimeout: 10000,
9+
pageLoadTimeout: 30000,
10+
responseTimeout: 30000,
11+
experimentalMemoryManagement: true,
12+
numTestsKeptInMemory: 5,
713
projectId: 'influxdata-docs',
814
setupNodeEvents(on, config) {
915
// implement node event listeners here
16+
on('before:browser:launch', (browser, launchOptions) => {
17+
if (browser.name === 'chrome' && browser.isHeadless) {
18+
// Force Chrome to use a less memory-intensive approach
19+
launchOptions.args.push('--disable-dev-shm-usage');
20+
launchOptions.args.push('--disable-gpu');
21+
launchOptions.args.push('--disable-extensions');
22+
return launchOptions;
23+
}
24+
});
1025
on('task', {
1126
// Fetch the product list configured in /data/products.yml
1227
getData(filename) {
@@ -24,6 +39,7 @@ module.exports = defineConfig({
2439
});
2540
},
2641
});
42+
return config;
2743
},
2844
},
2945
});
+112-17
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,122 @@
11
/// <reference types="cypress" />
2+
23
describe('Article links', () => {
34
const subjects = Cypress.env('test_subjects').split(',');
5+
// Always use HEAD for downloads to avoid timeouts
6+
const useHeadForDownloads = true;
7+
8+
// Helper function to identify download links - improved
9+
function isDownloadLink(href) {
10+
// Check for common download file extensions
11+
const downloadExtensions = [
12+
'.pdf', '.zip', '.tar.gz', '.tgz', '.rar', '.exe', '.dmg', '.pkg',
13+
'.deb', '.rpm', '.xlsx', '.csv', '.doc', '.docx', '.ppt', '.pptx'
14+
];
15+
16+
// Check for download domains or paths
17+
const downloadDomains = [
18+
'dl.influxdata.com',
19+
'downloads.influxdata.com'
20+
];
21+
22+
// Check if URL contains a download extension
23+
const hasDownloadExtension = downloadExtensions.some(ext =>
24+
href.toLowerCase().endsWith(ext)
25+
);
26+
27+
// Check if URL is from a download domain
28+
const isFromDownloadDomain = downloadDomains.some(domain =>
29+
href.toLowerCase().includes(domain)
30+
);
31+
32+
// Return true if either condition is met
33+
return hasDownloadExtension || isFromDownloadDomain;
34+
}
35+
36+
// Helper function to make appropriate request based on link type
37+
function testLink(href) {
38+
if (useHeadForDownloads && isDownloadLink(href)) {
39+
cy.log(`** Testing download link with HEAD: ${href} **`);
40+
cy.request({
41+
method: 'HEAD',
42+
url: href,
43+
failOnStatusCode: false,
44+
timeout: 10000 // 10 second timeout for download links
45+
}).then(response => {
46+
expect(response.status).to.be.lt(400);
47+
});
48+
} else {
49+
cy.log(`** Testing link: ${href} **`);
50+
cy.request({
51+
url: href,
52+
failOnStatusCode: false,
53+
timeout: 30000 // 30 second timeout for regular links
54+
}).then(response => {
55+
expect(response.status).to.be.lt(400);
56+
});
57+
}
58+
}
459

560
subjects.forEach((subject) => {
6-
it('contains valid internal links', function () {
61+
it(`contains valid internal links on ${subject}`, function () {
62+
cy.visit(`${subject}`);
63+
64+
// Test internal links (including anchor links)
65+
cy.get('article a[href^="/"]').each(($a) => {
66+
const href = $a.attr('href');
67+
testLink(href);
68+
});
69+
});
70+
71+
it(`checks anchor links on ${subject} (with warnings for missing targets)`, function () {
772
cy.visit(`${subject}`);
8-
cy.get('article a[href^="/"]') //.filter('[href^="/"]')
9-
.each(($a) => {
10-
cy.log(`** Testing internal link ${$a.attr('href')} **`);
11-
// cy.request doesn't show in your browser's Developer Tools
12-
// because the request comes from Node, not from the browser.
13-
cy.request($a.attr('href')).its('status').should('eq', 200);
14-
});
73+
74+
// Track missing anchors for summary
75+
const missingAnchors = [];
76+
77+
// Process anchor links individually
78+
cy.get('article a[href^="#"]').each(($a) => {
79+
const href = $a.prop('href'); // Use prop() instead of attr()
80+
if (href && href.length > 1) { // Skip empty anchors (#)
81+
// Get just the fragment part
82+
const url = new URL(href);
83+
const anchorId = url.hash.substring(1); // Remove the # character
84+
85+
if (!anchorId) {
86+
cy.log(`Skipping empty anchor in ${href}`);
87+
return;
88+
}
89+
90+
// Use DOM to check if the element exists, but don't fail if missing
91+
cy.window().then(win => {
92+
const element = win.document.getElementById(anchorId);
93+
if (element) {
94+
cy.log(`✅ Anchor target exists: #${anchorId}`);
95+
} else {
96+
// Just warn about the missing anchor
97+
cy.log(`⚠️ WARNING: Missing anchor target: #${anchorId}`);
98+
missingAnchors.push(anchorId);
99+
}
100+
});
101+
}
102+
}).then(() => {
103+
// After checking all anchors, log a summary
104+
if (missingAnchors.length > 0) {
105+
cy.log(`⚠️ Found ${missingAnchors.length} missing anchor targets: ${missingAnchors.join(', ')}`);
106+
} else {
107+
cy.log('✅ All anchor targets are valid');
108+
}
15109
});
16-
it('contains valid external links', function () {
110+
});
111+
112+
it(`contains valid external links on ${subject}`, function () {
17113
cy.visit(`${subject}`);
18-
cy.get('article a[href^="http"]')
19-
.each(($a) => {
20-
// cy.request doesn't show in your browser's Developer Tools
21-
cy.log(`** Testing external link ${$a.attr('href')} **`);
22-
// because the request comes from Node, not from the browser.
23-
cy.request($a.attr('href')).its('status').should('eq', 200);
24-
});
114+
115+
// Test external links
116+
cy.get('article a[href^="http"]').each(($a) => {
117+
const href = $a.attr('href');
118+
testLink(href);
119+
});
25120
});
26121
});
27-
});
122+
});

cypress/support/map-files-to-urls.mjs

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env node
2+
3+
import { execSync } from 'child_process';
4+
import process from 'process';
5+
6+
// Get file paths from command line arguments
7+
const filePaths = process.argv.slice(2);
8+
9+
// Parse options
10+
const debugMode = process.argv.includes('--debug');
11+
12+
// Filter for content files
13+
const contentFiles = filePaths.filter(file =>
14+
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
15+
);
16+
17+
if (contentFiles.length === 0) {
18+
console.log('No content files to check.');
19+
process.exit(0);
20+
}
21+
22+
// Map file paths to URL paths
23+
function mapFilePathToUrl(filePath) {
24+
// Remove content/ prefix
25+
let url = filePath.replace(/^content/, '');
26+
27+
// Handle _index files (both .html and .md)
28+
url = url.replace(/\/_index\.(html|md)$/, '/');
29+
30+
// Handle regular .md files
31+
url = url.replace(/\.md$/, '/');
32+
33+
// Handle regular .html files
34+
url = url.replace(/\.html$/, '/');
35+
36+
// Ensure URL starts with a slash
37+
if (!url.startsWith('/')) {
38+
url = '/' + url;
39+
}
40+
41+
return url;
42+
}
43+
44+
const urls = contentFiles.map(mapFilePathToUrl);
45+
const urlList = urls.join(',');
46+
47+
console.log(`Testing links in URLs: ${urlList}`);
48+
49+
// Create environment object with the cypress_test_subjects variable
50+
const envVars = {
51+
...process.env,
52+
cypress_test_subjects: urlList,
53+
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
54+
};
55+
56+
// Run Cypress tests with the mapped URLs
57+
try {
58+
// Choose run mode based on debug flag
59+
if (debugMode) {
60+
// For debug mode, set the environment variable and open Cypress
61+
// The user will need to manually select the test file
62+
console.log('Opening Cypress in debug mode.');
63+
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');
64+
65+
execSync('npx cypress open --e2e', {
66+
stdio: 'inherit',
67+
env: envVars
68+
});
69+
} else {
70+
// For normal mode, run the test automatically
71+
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
72+
stdio: 'inherit',
73+
env: envVars
74+
});
75+
}
76+
} catch (error) {
77+
console.error('Link check failed');
78+
process.exit(1);
79+
}

layouts/partials/article/alpha.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ <h4>{{ $displayName }} is in Public Alpha</h4>
1919
invite you to <strong>join our public channels</strong> for updates and to share feedback.
2020
</p>
2121
<div class="expand-wrapper">
22-
<div class="expand" id="#alpha-expecations">
22+
<div class="expand" id="alpha-expecations">
2323
<p class="expand-label">
2424
<span class="expand-toggle"></span><span>Alpha expectations and recommendations</span>
2525
</p>
@@ -39,7 +39,7 @@ <h4>{{ $displayName }} is in Public Alpha</h4>
3939
</ul>
4040
</div>
4141
</div>
42-
<div class="expand" id="#alpha-feedback-channels">
42+
<div class="expand" id="alpha-feedback-channels">
4343
<p class="expand-label">
4444
<span class="expand-toggle"></span><span>Join our public channels</span>
4545
</p>

lefthook.yml

+31
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,37 @@ pre-commit:
5050
run: '.ci/vale/vale.sh
5151
--config=content/influxdb/v2/.vale.ini
5252
--minAlertLevel=error {staged_files}'
53+
54+
# Link checking for InfluxDB v2
55+
v2-links:
56+
tags: test,links,v2
57+
glob: "content/influxdb/v2/**/*.{md,html}"
58+
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
59+
60+
# Link checking for InfluxDB v3 core
61+
v3-core-links:
62+
tags: test,links,v3
63+
glob: "content/influxdb3/core/**/*.{md,html}"
64+
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
65+
66+
# Link checking for InfluxDB v3 enterprise
67+
v3-enterprise-links:
68+
tags: test,links,v3
69+
glob: "content/influxdb3/enterprise/**/*.{md,html}"
70+
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
71+
72+
# Link checking for Cloud products
73+
cloud-links:
74+
tags: test,links,cloud
75+
glob: "content/influxdb/{cloud,cloud-dedicated,cloud-serverless}/**/*.{md,html}"
76+
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
77+
78+
# Link checking for Telegraf
79+
telegraf-links:
80+
tags: test,links
81+
glob: "content/telegraf/**/*.{md,html}"
82+
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
83+
5384
cloud-pytest:
5485
glob: content/influxdb/cloud/**/*.md
5586
tags: test,codeblocks,v2

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@
3535
"scripts": {
3636
"e2e:chrome": "npx cypress run --browser chrome",
3737
"e2e:o": "npx cypress open",
38-
"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",
39-
"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",
38+
"e2e:o:links": "node cypress/support/map-files-to-urls.mjs content/influxdb3/core/get-started/_index.md --debug",
4039
"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",
4140
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
4241
"pre-commit": "lefthook run pre-commit",

0 commit comments

Comments
 (0)