Skip to content

Commit da767f5

Browse files
committed
chore(test): e2e test improvements:
- Link checker should report the first broken link - Link checker should only test external links if the domains are in the allowed list - If test subjects don't start with 'content/', treat them as URL paths and don't send them to map-files-to-urls.js.
1 parent 02e1006 commit da767f5

File tree

5 files changed

+322
-73
lines changed

5 files changed

+322
-73
lines changed

cypress.config.js

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as fs from 'fs';
44
import * as yaml from 'js-yaml';
55
import {
66
BROKEN_LINKS_FILE,
7+
FIRST_BROKEN_LINK_FILE,
78
initializeReport,
89
readBrokenLinksReport,
910
} from './cypress/support/link-reporter.js';
@@ -90,8 +91,23 @@ export default defineConfig({
9091
return initializeReport();
9192
},
9293

94+
// Special case domains are now handled directly in the test without additional reporting
95+
// This task is kept for backward compatibility but doesn't do anything special
96+
reportSpecialCaseLink(linkData) {
97+
console.log(
98+
`✅ Expected status code: ${linkData.url} (status: ${linkData.status}) is valid for this domain`
99+
);
100+
return true;
101+
},
102+
93103
reportBrokenLink(linkData) {
94104
try {
105+
// Validate link data
106+
if (!linkData || !linkData.url || !linkData.page) {
107+
console.error('Invalid link data provided');
108+
return false;
109+
}
110+
95111
// Read current report
96112
const report = readBrokenLinksReport();
97113

@@ -102,29 +118,63 @@ export default defineConfig({
102118
report.push(pageReport);
103119
}
104120

105-
// Add the broken link to the page's report
106-
pageReport.links.push({
107-
url: linkData.url,
108-
status: linkData.status,
109-
type: linkData.type,
110-
linkText: linkData.linkText,
111-
});
112-
113-
// Write updated report back to file
114-
fs.writeFileSync(
115-
BROKEN_LINKS_FILE,
116-
JSON.stringify(report, null, 2)
121+
// Check if link is already in the report to avoid duplicates
122+
const isDuplicate = pageReport.links.some(
123+
(link) => link.url === linkData.url && link.type === linkData.type
117124
);
118125

119-
// Log the broken link immediately to console
120-
console.error(
121-
`❌ BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
122-
);
126+
if (!isDuplicate) {
127+
// Add the broken link to the page's report
128+
pageReport.links.push({
129+
url: linkData.url,
130+
status: linkData.status,
131+
type: linkData.type,
132+
linkText: linkData.linkText,
133+
});
134+
135+
// Write updated report back to file
136+
fs.writeFileSync(
137+
BROKEN_LINKS_FILE,
138+
JSON.stringify(report, null, 2)
139+
);
140+
141+
// Store first broken link if not already recorded
142+
const firstBrokenLinkExists =
143+
fs.existsSync(FIRST_BROKEN_LINK_FILE) &&
144+
fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8').trim() !== '';
145+
146+
if (!firstBrokenLinkExists) {
147+
// Store first broken link with complete information
148+
const firstBrokenLink = {
149+
url: linkData.url,
150+
status: linkData.status,
151+
type: linkData.type,
152+
linkText: linkData.linkText,
153+
page: linkData.page,
154+
time: new Date().toISOString(),
155+
};
156+
157+
fs.writeFileSync(
158+
FIRST_BROKEN_LINK_FILE,
159+
JSON.stringify(firstBrokenLink, null, 2)
160+
);
161+
162+
console.error(
163+
`🔴 FIRST BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
164+
);
165+
}
166+
167+
// Log the broken link immediately to console
168+
console.error(
169+
`❌ BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
170+
);
171+
}
123172

124173
return true;
125174
} catch (error) {
126175
console.error(`Error reporting broken link: ${error.message}`);
127-
return false;
176+
// Even if there's an error, we want to ensure the test knows there was a broken link
177+
return true;
128178
}
129179
},
130180
});

cypress/e2e/content/article-links.cy.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ describe('Article', () => {
55
// Always use HEAD for downloads to avoid timeouts
66
const useHeadForDownloads = true;
77

8+
// Set up initialization for tests
9+
before(() => {
10+
// Initialize the broken links report
11+
cy.task('initializeBrokenLinksReport');
12+
});
13+
814
// Helper function to identify download links
915
function isDownloadLink(href) {
1016
// Check for common download file extensions
@@ -56,7 +62,7 @@ describe('Article', () => {
5662
};
5763

5864
function handleFailedLink(url, status, type, redirectChain = '') {
59-
// Report broken link to the task which will handle reporting
65+
// Report the broken link
6066
cy.task('reportBrokenLink', {
6167
url: url + redirectChain,
6268
status,
@@ -65,6 +71,7 @@ describe('Article', () => {
6571
page: pageUrl,
6672
});
6773

74+
// Throw error for broken links
6875
throw new Error(
6976
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
7077
);
@@ -109,11 +116,7 @@ describe('Article', () => {
109116
}
110117
}
111118

112-
// Before all tests, initialize the report
113-
before(() => {
114-
cy.task('initializeBrokenLinksReport');
115-
});
116-
119+
// Test implementation for subjects
117120
subjects.forEach((subject) => {
118121
it(`${subject} has valid internal links`, function () {
119122
cy.visit(`${subject}`, { timeout: 20000 });
@@ -186,8 +189,19 @@ describe('Article', () => {
186189
});
187190

188191
it(`${subject} has valid external links`, function () {
192+
// Check if we should skip external links entirely
193+
if (Cypress.env('skipExternalLinks') === true) {
194+
cy.log(
195+
'Skipping all external links as configured by skipExternalLinks'
196+
);
197+
return;
198+
}
199+
189200
cy.visit(`${subject}`);
190201

202+
// Define allowed external domains to test
203+
const allowedExternalDomains = ['github.com', 'kapa.ai'];
204+
191205
// Test external links
192206
cy.get('article, .api-content').then(($article) => {
193207
// Find links without failing the test if none are found
@@ -197,8 +211,29 @@ describe('Article', () => {
197211
return;
198212
}
199213

200-
cy.debug(`Found ${$links.length} external links`);
201-
cy.wrap($links).each(($a) => {
214+
// Filter links to only include allowed domains
215+
const $allowedLinks = $links.filter((_, el) => {
216+
const href = el.getAttribute('href');
217+
try {
218+
const url = new URL(href);
219+
return allowedExternalDomains.some(
220+
(domain) =>
221+
url.hostname === domain || url.hostname.endsWith(`.${domain}`)
222+
);
223+
} catch (e) {
224+
return false;
225+
}
226+
});
227+
228+
if ($allowedLinks.length === 0) {
229+
cy.log('No links to allowed external domains found on this page');
230+
return;
231+
}
232+
233+
cy.log(
234+
`Found ${$allowedLinks.length} links to allowed external domains to test`
235+
);
236+
cy.wrap($allowedLinks).each(($a) => {
202237
const href = $a.attr('href');
203238
const linkText = $a.text().trim();
204239
testLink(href, linkText, subject);

cypress/support/link-reporter.js

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import fs from 'fs';
66

77
export const BROKEN_LINKS_FILE = '/tmp/broken_links_report.json';
8+
export const FIRST_BROKEN_LINK_FILE = '/tmp/first_broken_link.json';
89
const SOURCES_FILE = '/tmp/test_subjects_sources.json';
910

1011
/**
@@ -18,7 +19,29 @@ export function readBrokenLinksReport() {
1819

1920
try {
2021
const fileContent = fs.readFileSync(BROKEN_LINKS_FILE, 'utf8');
21-
return fileContent && fileContent !== '[]' ? JSON.parse(fileContent) : [];
22+
23+
// Check if the file is empty or contains only an empty array
24+
if (!fileContent || fileContent.trim() === '' || fileContent === '[]') {
25+
return [];
26+
}
27+
28+
// Try to parse the JSON content
29+
try {
30+
const parsedContent = JSON.parse(fileContent);
31+
32+
// Ensure the parsed content is an array
33+
if (!Array.isArray(parsedContent)) {
34+
console.error('Broken links report is not an array');
35+
return [];
36+
}
37+
38+
return parsedContent;
39+
} catch (parseErr) {
40+
console.error(
41+
`Error parsing broken links report JSON: ${parseErr.message}`
42+
);
43+
return [];
44+
}
2245
} catch (err) {
2346
console.error(`Error reading broken links report: ${err.message}`);
2447
return [];
@@ -57,11 +80,29 @@ export function displayBrokenLinksReport(brokenLinksReport = null) {
5780
brokenLinksReport = readBrokenLinksReport();
5881
}
5982

60-
if (!brokenLinksReport || brokenLinksReport.length === 0) {
61-
console.log('✅ No broken links detected');
83+
// Check both the report and first broken link file to determine if we have broken links
84+
const firstBrokenLink = readFirstBrokenLink();
85+
86+
// Only report "no broken links" if both checks pass
87+
if (
88+
(!brokenLinksReport || brokenLinksReport.length === 0) &&
89+
!firstBrokenLink
90+
) {
91+
console.log('✅ No broken links detected in the validation report');
6292
return 0;
6393
}
6494

95+
// Special case: check if the single broken link file could be missing from the report
96+
if (
97+
firstBrokenLink &&
98+
(!brokenLinksReport || brokenLinksReport.length === 0)
99+
) {
100+
console.error(
101+
'\n⚠️ Warning: First broken link record exists but no links in the report.'
102+
);
103+
console.error('This could indicate a reporting issue.');
104+
}
105+
65106
// Load sources mapping
66107
const sourcesMapping = readSourcesMapping();
67108

@@ -70,6 +111,21 @@ export function displayBrokenLinksReport(brokenLinksReport = null) {
70111
console.error(' 🚨 BROKEN LINKS DETECTED 🚨 ');
71112
console.error('='.repeat(80));
72113

114+
// Show first failing link if available
115+
if (firstBrokenLink) {
116+
console.error('\n🔴 FIRST FAILING LINK:');
117+
console.error(` URL: ${firstBrokenLink.url}`);
118+
console.error(` Status: ${firstBrokenLink.status}`);
119+
console.error(` Type: ${firstBrokenLink.type}`);
120+
console.error(` Page: ${firstBrokenLink.page}`);
121+
if (firstBrokenLink.linkText) {
122+
console.error(
123+
` Link text: "${firstBrokenLink.linkText.substring(0, 50)}${firstBrokenLink.linkText.length > 50 ? '...' : ''}"`
124+
);
125+
}
126+
console.error('-'.repeat(40));
127+
}
128+
73129
let totalBrokenLinks = 0;
74130

75131
brokenLinksReport.forEach((report) => {
@@ -106,12 +162,51 @@ export function displayBrokenLinksReport(brokenLinksReport = null) {
106162
}
107163

108164
/**
109-
* Initialize the broken links report file
165+
* Reads the first broken link info from the file system
166+
* @returns {Object|null} First broken link data or null if not found
167+
*/
168+
export function readFirstBrokenLink() {
169+
if (!fs.existsSync(FIRST_BROKEN_LINK_FILE)) {
170+
return null;
171+
}
172+
173+
try {
174+
const fileContent = fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8');
175+
176+
// Check if the file is empty or contains whitespace only
177+
if (!fileContent || fileContent.trim() === '') {
178+
return null;
179+
}
180+
181+
// Try to parse the JSON content
182+
try {
183+
return JSON.parse(fileContent);
184+
} catch (parseErr) {
185+
console.error(
186+
`Error parsing first broken link JSON: ${parseErr.message}`
187+
);
188+
return null;
189+
}
190+
} catch (err) {
191+
console.error(`Error reading first broken link: ${err.message}`);
192+
return null;
193+
}
194+
}
195+
196+
/**
197+
* Initialize the broken links report files
110198
* @returns {boolean} True if initialization was successful
111199
*/
112200
export function initializeReport() {
113201
try {
202+
// Create an empty array for the broken links report
114203
fs.writeFileSync(BROKEN_LINKS_FILE, '[]', 'utf8');
204+
205+
// Reset the first broken link file by creating an empty file
206+
// Using empty string as a clear indicator that no broken link has been recorded yet
207+
fs.writeFileSync(FIRST_BROKEN_LINK_FILE, '', 'utf8');
208+
209+
console.debug('🔄 Initialized broken links reporting system');
115210
return true;
116211
} catch (err) {
117212
console.error(`Error initializing broken links report: ${err.message}`);

0 commit comments

Comments
 (0)