chore(ci): Removes old Cypress link checker test code

jts-link-checker
Jason Stirnaman 2025-08-18 10:51:57 -05:00
parent f5df3cb6f0
commit a8578bb0af
8 changed files with 5 additions and 1063 deletions

View File

@ -2,14 +2,6 @@ import { defineConfig } from 'cypress';
import { cwd as _cwd } from 'process';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import {
BROKEN_LINKS_FILE,
FIRST_BROKEN_LINK_FILE,
initializeReport,
readBrokenLinksReport,
saveCacheStats,
saveValidationStrategy,
} from './cypress/support/link-reporter.js';
export default defineConfig({
e2e: {
@ -88,98 +80,6 @@ export default defineConfig({
}
},
// Broken links reporting tasks
initializeBrokenLinksReport() {
return initializeReport();
},
// Special case domains are now handled directly in the test without additional reporting
// This task is kept for backward compatibility but doesn't do anything special
reportSpecialCaseLink(linkData) {
console.log(
`✅ Expected status code: ${linkData.url} (status: ${linkData.status}) is valid for this domain`
);
return true;
},
reportBrokenLink(linkData) {
try {
// Validate link data
if (!linkData || !linkData.url || !linkData.page) {
console.error('Invalid link data provided');
return false;
}
// Read current report
const report = readBrokenLinksReport();
// Find or create entry for this page
let pageReport = report.find((r) => r.page === linkData.page);
if (!pageReport) {
pageReport = { page: linkData.page, links: [] };
report.push(pageReport);
}
// Check if link is already in the report to avoid duplicates
const isDuplicate = pageReport.links.some(
(link) => link.url === linkData.url && link.type === linkData.type
);
if (!isDuplicate) {
// Add the broken link to the page's report
pageReport.links.push({
url: linkData.url,
status: linkData.status,
type: linkData.type,
linkText: linkData.linkText,
});
// Write updated report back to file
fs.writeFileSync(
BROKEN_LINKS_FILE,
JSON.stringify(report, null, 2)
);
// Store first broken link if not already recorded
const firstBrokenLinkExists =
fs.existsSync(FIRST_BROKEN_LINK_FILE) &&
fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8').trim() !== '';
if (!firstBrokenLinkExists) {
// Store first broken link with complete information
const firstBrokenLink = {
url: linkData.url,
status: linkData.status,
type: linkData.type,
linkText: linkData.linkText,
page: linkData.page,
time: new Date().toISOString(),
};
fs.writeFileSync(
FIRST_BROKEN_LINK_FILE,
JSON.stringify(firstBrokenLink, null, 2)
);
console.error(
`🔴 FIRST BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
);
}
// Log the broken link immediately to console
console.error(
`❌ BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
);
}
return true;
} catch (error) {
console.error(`Error reporting broken link: ${error.message}`);
// Even if there's an error, we want to ensure the test knows there was a broken link
return true;
}
},
// Cache and incremental validation tasks
saveCacheStatistics(stats) {
try {

View File

@ -1,370 +0,0 @@
/// <reference types="cypress" />
describe('Article', () => {
let subjects = Cypress.env('test_subjects')
? Cypress.env('test_subjects')
.split(',')
.filter((s) => s.trim() !== '')
: [];
// Cache will be checked during test execution at the URL level
// Always use HEAD for downloads to avoid timeouts
const useHeadForDownloads = true;
// Set up initialization for tests
before(() => {
// Initialize the broken links report
cy.task('initializeBrokenLinksReport');
// Clean up expired cache entries
cy.task('cleanupCache').then((cleaned) => {
if (cleaned > 0) {
cy.log(`🧹 Cleaned up ${cleaned} expired cache entries`);
}
});
});
// Display cache statistics after all tests complete
after(() => {
cy.task('getCacheStats').then((stats) => {
cy.log('📊 Link Validation Cache Statistics:');
cy.log(` • Cache hits: ${stats.hits}`);
cy.log(` • Cache misses: ${stats.misses}`);
cy.log(` • New entries stored: ${stats.stores}`);
cy.log(` • Hit rate: ${stats.hitRate}`);
cy.log(` • Total validations: ${stats.total}`);
if (stats.total > 0) {
const message = stats.hits > 0
? `✨ Cache optimization saved ${stats.hits} link validations`
: '🔄 No cache hits - all links were validated fresh';
cy.log(message);
}
// Save cache statistics for the reporter to display
cy.task('saveCacheStatsForReporter', {
hitRate: parseFloat(stats.hitRate.replace('%', '')),
cacheHits: stats.hits,
cacheMisses: stats.misses,
totalValidations: stats.total,
newEntriesStored: stats.stores,
cleanups: stats.cleanups
});
});
});
// Helper function to identify download links
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 for handling failed links
function handleFailedLink(url, status, type, redirectChain = '', linkText = '', pageUrl = '') {
// Report the broken link
cy.task('reportBrokenLink', {
url: url + redirectChain,
status,
type,
linkText,
page: pageUrl,
});
// Throw error for broken links
throw new Error(
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
);
}
// Helper function to test a link with cache integration
function testLink(href, linkText = '', pageUrl) {
// Check cache first
return cy.task('isLinkCached', href).then((isCached) => {
if (isCached) {
cy.log(`✅ Cache hit: ${href}`);
return cy.task('getLinkCache', href).then((cachedResult) => {
if (cachedResult && cachedResult.result && cachedResult.result.status >= 400) {
// Cached result shows this link is broken
handleFailedLink(href, cachedResult.result.status, cachedResult.result.type || 'cached', '', linkText, pageUrl);
}
// For successful cached results, just return - no further action needed
});
} else {
// Not cached, perform actual validation
return performLinkValidation(href, linkText, pageUrl);
}
});
}
// Helper function to perform actual link validation and cache the result
function performLinkValidation(href, linkText = '', pageUrl) {
// Common request options for both methods
const requestOptions = {
failOnStatusCode: true,
timeout: 15000, // Increased timeout for reliability
followRedirect: true, // Explicitly follow redirects
retryOnNetworkFailure: true, // Retry on network issues
retryOnStatusCodeFailure: true, // Retry on 5xx errors
};
if (useHeadForDownloads && isDownloadLink(href)) {
cy.log(`** Testing download link with HEAD: ${href} **`);
return cy.request({
method: 'HEAD',
url: href,
...requestOptions,
}).then((response) => {
// Prepare result for caching
const result = {
status: response.status,
type: 'download',
timestamp: new Date().toISOString()
};
// Check final status after following any redirects
if (response.status >= 400) {
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
// Cache the failed result
cy.task('setLinkCache', { url: href, result });
handleFailedLink(href, response.status, 'download', redirectInfo, linkText, pageUrl);
} else {
// Cache the successful result
cy.task('setLinkCache', { url: href, result });
}
});
} else {
cy.log(`** Testing link: ${href} **`);
return cy.request({
url: href,
...requestOptions,
}).then((response) => {
// Prepare result for caching
const result = {
status: response.status,
type: 'regular',
timestamp: new Date().toISOString()
};
if (response.status >= 400) {
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
// Cache the failed result
cy.task('setLinkCache', { url: href, result });
handleFailedLink(href, response.status, 'regular', redirectInfo, linkText, pageUrl);
} else {
// Cache the successful result
cy.task('setLinkCache', { url: href, result });
}
});
}
}
// Test setup validation
it('Test Setup Validation', function () {
cy.log(`📋 Test Configuration:`);
cy.log(` • Test subjects: ${subjects.length}`);
cy.log(` • Cache: URL-level caching with 30-day TTL`);
cy.log(` • Link validation: Internal, anchor, and allowed external links`);
cy.log('✅ Test setup validation completed');
});
subjects.forEach((subject) => {
it(`${subject} has valid internal links`, function () {
// Add error handling for page visit failures
cy.visit(`${subject}`, { timeout: 20000 }).then(() => {
cy.log(`✅ Successfully loaded page: ${subject}`);
});
// Test internal links
cy.get('article, .api-content').then(($article) => {
// Find links without failing the test if none are found
const $links = $article.find('a[href^="/"]');
if ($links.length === 0) {
cy.log('No internal links found on this page');
return;
}
cy.log(`🔍 Testing ${$links.length} internal links on ${subject}`);
// Now test each link
cy.wrap($links).each(($a) => {
const href = $a.attr('href');
const linkText = $a.text().trim();
try {
testLink(href, linkText, subject);
} catch (error) {
cy.log(`❌ Error testing link ${href}: ${error.message}`);
throw error; // Re-throw to fail the test
}
});
});
});
it(`${subject} has valid anchor links`, function () {
cy.visit(`${subject}`).then(() => {
cy.log(`✅ Successfully loaded page for anchor testing: ${subject}`);
});
// Define selectors for anchor links to ignore, such as behavior triggers
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
const anchorSelector =
'a[href^="#"]:not(' + ignoreLinks.join('):not(') + ')';
cy.get('article, .api-content').then(($article) => {
const $anchorLinks = $article.find(anchorSelector);
if ($anchorLinks.length === 0) {
cy.log('No anchor links found on this page');
return;
}
cy.log(`🔗 Testing ${$anchorLinks.length} anchor links on ${subject}`);
cy.wrap($anchorLinks).each(($a) => {
const href = $a.prop('href');
const linkText = $a.text().trim();
if (href && href.length > 1) {
// 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
cy.window().then((win) => {
const element = win.document.getElementById(anchorId);
if (!element) {
cy.task('reportBrokenLink', {
url: `#${anchorId}`,
status: 404,
type: 'anchor',
linkText,
page: subject,
});
cy.log(`⚠️ Missing anchor target: #${anchorId}`);
}
});
}
});
});
});
it(`${subject} has valid external links`, function () {
// Check if we should skip external links entirely
if (Cypress.env('skipExternalLinks') === true) {
cy.log(
'Skipping all external links as configured by skipExternalLinks'
);
return;
}
cy.visit(`${subject}`).then(() => {
cy.log(
`✅ Successfully loaded page for external link testing: ${subject}`
);
});
// Define allowed external domains to test
const allowedExternalDomains = ['github.com', 'kapa.ai'];
// Test external links
cy.get('article, .api-content').then(($article) => {
// Find links without failing the test if none are found
const $links = $article.find('a[href^="http"]');
if ($links.length === 0) {
cy.log('No external links found on this page');
return;
}
cy.log(`🔍 Found ${$links.length} total external links on ${subject}`);
// Filter links to only include allowed domains
const $allowedLinks = $links.filter((_, el) => {
const href = el.getAttribute('href');
try {
const url = new URL(href);
return allowedExternalDomains.some(
(domain) =>
url.hostname === domain || url.hostname.endsWith(`.${domain}`)
);
} catch (urlError) {
cy.log(`⚠️ Invalid URL found: ${href}`);
return false;
}
});
if ($allowedLinks.length === 0) {
cy.log('No links to allowed external domains found on this page');
cy.log(` • Allowed domains: ${allowedExternalDomains.join(', ')}`);
return;
}
cy.log(
`🌐 Testing ${$allowedLinks.length} links to allowed external domains`
);
cy.wrap($allowedLinks).each(($a) => {
const href = $a.attr('href');
const linkText = $a.text().trim();
try {
testLink(href, linkText, subject);
} catch (error) {
cy.log(`❌ Error testing external link ${href}: ${error.message}`);
throw error;
}
});
});
});
});
});

View File

@ -1,215 +0,0 @@
/**
* Link Cache Manager for Cypress Tests
* Manages caching of link validation results at the URL level
*/
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const CACHE_VERSION = 'v2';
const CACHE_KEY_PREFIX = 'link-validation';
const LOCAL_CACHE_DIR = path.join(process.cwd(), '.cache', 'link-validation');
/**
* Cache manager for individual link validation results
*/
export class LinkCacheManager {
constructor(options = {}) {
this.localCacheDir = options.localCacheDir || LOCAL_CACHE_DIR;
// Configurable cache TTL - default 30 days
this.cacheTTLDays =
options.cacheTTLDays || parseInt(process.env.LINK_CACHE_TTL_DAYS) || 30;
this.maxAge = this.cacheTTLDays * 24 * 60 * 60 * 1000;
this.ensureLocalCacheDir();
// Track cache statistics
this.stats = {
hits: 0,
misses: 0,
stores: 0,
cleanups: 0
};
}
ensureLocalCacheDir() {
if (!fs.existsSync(this.localCacheDir)) {
fs.mkdirSync(this.localCacheDir, { recursive: true });
}
}
/**
* Generate cache key for a URL
* @param {string} url - The URL to cache
* @returns {string} Cache key
*/
generateCacheKey(url) {
const urlHash = crypto
.createHash('sha256')
.update(url)
.digest('hex')
.substring(0, 16);
return `${CACHE_KEY_PREFIX}-${CACHE_VERSION}-${urlHash}`;
}
/**
* Get cache file path for a URL
* @param {string} url - The URL
* @returns {string} File path
*/
getCacheFilePath(url) {
const cacheKey = this.generateCacheKey(url);
return path.join(this.localCacheDir, `${cacheKey}.json`);
}
/**
* Check if a URL's validation result is cached
* @param {string} url - The URL to check
* @returns {Object|null} Cached result or null
*/
get(url) {
const cacheFile = this.getCacheFilePath(url);
if (!fs.existsSync(cacheFile)) {
this.stats.misses++;
return null;
}
try {
const content = fs.readFileSync(cacheFile, 'utf8');
const cached = JSON.parse(content);
// TTL check
const age = Date.now() - new Date(cached.cachedAt).getTime();
if (age > this.maxAge) {
fs.unlinkSync(cacheFile);
this.stats.misses++;
this.stats.cleanups++;
return null;
}
this.stats.hits++;
return cached;
} catch (error) {
// Clean up corrupted cache
try {
fs.unlinkSync(cacheFile);
this.stats.cleanups++;
} catch (cleanupError) {
// Ignoring cleanup errors as they are non-critical, but logging for visibility
console.warn(`Failed to clean up corrupted cache file: ${cleanupError.message}`);
}
this.stats.misses++;
return null;
}
}
/**
* Store validation result for a URL
* @param {string} url - The URL
* @param {Object} result - Validation result
* @returns {boolean} True if successfully cached, false otherwise
*/
set(url, result) {
const cacheFile = this.getCacheFilePath(url);
const cacheData = {
url,
result,
cachedAt: new Date().toISOString(),
ttl: new Date(Date.now() + this.maxAge).toISOString()
};
try {
fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2));
this.stats.stores++;
return true;
} catch (error) {
console.warn(`Failed to cache validation result for ${url}: ${error.message}`);
return false;
}
}
/**
* Check if a URL is cached and valid
* @param {string} url - The URL to check
* @returns {boolean} True if cached and valid
*/
isCached(url) {
return this.get(url) !== null;
}
/**
* Get cache statistics
* @returns {Object} Cache statistics
*/
getStats() {
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) : 0;
return {
...this.stats,
total,
hitRate: `${hitRate}%`
};
}
/**
* Clean up expired cache entries
* @returns {number} Number of entries cleaned up
*/
cleanup() {
let cleaned = 0;
try {
const files = fs.readdirSync(this.localCacheDir);
const cacheFiles = files.filter(file =>
file.startsWith(CACHE_KEY_PREFIX) && file.endsWith('.json')
);
for (const file of cacheFiles) {
const filePath = path.join(this.localCacheDir, file);
try {
const content = fs.readFileSync(filePath, 'utf8');
const cached = JSON.parse(content);
const age = Date.now() - new Date(cached.cachedAt).getTime();
if (age > this.maxAge) {
fs.unlinkSync(filePath);
cleaned++;
}
} catch (error) {
console.warn(`Failed to process cache file "${filePath}": ${error.message}`);
// Remove corrupted files
fs.unlinkSync(filePath);
cleaned++;
}
}
} catch (error) {
console.warn(`Cache cleanup failed: ${error.message}`);
}
this.stats.cleanups += cleaned;
return cleaned;
}
}
/**
* Cypress task helper to integrate cache with Cypress tasks
*/
export const createCypressCacheTasks = (options = {}) => {
const cache = new LinkCacheManager(options);
return {
getLinkCache: (url) => cache.get(url),
setLinkCache: ({ url, result }) => cache.set(url, result),
isLinkCached: (url) => cache.isCached(url),
getCacheStats: () => cache.getStats(),
cleanupCache: () => cache.cleanup()
};
};

View File

@ -1,310 +0,0 @@
/**
* Broken Links Reporter
* Handles collecting, storing, and reporting broken links found during tests
*/
import fs from 'fs';
export const BROKEN_LINKS_FILE = '/tmp/broken_links_report.json';
export const FIRST_BROKEN_LINK_FILE = '/tmp/first_broken_link.json';
const SOURCES_FILE = '/tmp/test_subjects_sources.json';
const CACHE_STATS_FILE = '/tmp/cache_statistics.json';
const VALIDATION_STRATEGY_FILE = '/tmp/validation_strategy.json';
/**
* Reads the broken links report from the file system
* @returns {Array} Parsed report data or empty array if file doesn't exist
*/
export function readBrokenLinksReport() {
if (!fs.existsSync(BROKEN_LINKS_FILE)) {
return [];
}
try {
const fileContent = fs.readFileSync(BROKEN_LINKS_FILE, 'utf8');
// Check if the file is empty or contains only an empty array
if (!fileContent || fileContent.trim() === '' || fileContent === '[]') {
return [];
}
// Try to parse the JSON content
try {
const parsedContent = JSON.parse(fileContent);
// Ensure the parsed content is an array
if (!Array.isArray(parsedContent)) {
console.error('Broken links report is not an array');
return [];
}
return parsedContent;
} catch (parseErr) {
console.error(
`Error parsing broken links report JSON: ${parseErr.message}`
);
return [];
}
} catch (err) {
console.error(`Error reading broken links report: ${err.message}`);
return [];
}
}
/**
* Reads the sources mapping file
* @returns {Object} A mapping from URLs to their source files
*/
function readSourcesMapping() {
try {
if (fs.existsSync(SOURCES_FILE)) {
const sourcesData = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf8'));
return sourcesData.reduce((acc, item) => {
if (item.url && item.source) {
acc[item.url] = item.source;
}
return acc;
}, {});
}
} catch (err) {
console.warn(`Warning: Could not read sources mapping: ${err.message}`);
}
return {};
}
/**
* Read cache statistics from file
* @returns {Object|null} Cache statistics or null if not found
*/
function readCacheStats() {
try {
if (fs.existsSync(CACHE_STATS_FILE)) {
const content = fs.readFileSync(CACHE_STATS_FILE, 'utf8');
return JSON.parse(content);
}
} catch (err) {
console.warn(`Warning: Could not read cache stats: ${err.message}`);
}
return null;
}
/**
* Read validation strategy from file
* @returns {Object|null} Validation strategy or null if not found
*/
function readValidationStrategy() {
try {
if (fs.existsSync(VALIDATION_STRATEGY_FILE)) {
const content = fs.readFileSync(VALIDATION_STRATEGY_FILE, 'utf8');
return JSON.parse(content);
}
} catch (err) {
console.warn(`Warning: Could not read validation strategy: ${err.message}`);
}
return null;
}
/**
* Save cache statistics for reporting
* @param {Object} stats - Cache statistics to save
*/
export function saveCacheStats(stats) {
try {
fs.writeFileSync(CACHE_STATS_FILE, JSON.stringify(stats, null, 2));
} catch (err) {
console.warn(`Warning: Could not save cache stats: ${err.message}`);
}
}
/**
* Save validation strategy for reporting
* @param {Object} strategy - Validation strategy to save
*/
export function saveValidationStrategy(strategy) {
try {
fs.writeFileSync(
VALIDATION_STRATEGY_FILE,
JSON.stringify(strategy, null, 2)
);
} catch (err) {
console.warn(`Warning: Could not save validation strategy: ${err.message}`);
}
}
/**
* Formats and displays the broken links report to the console
* @param {Array} brokenLinksReport - The report data to display
* @returns {number} The total number of broken links found
*/
export function displayBrokenLinksReport(brokenLinksReport = null) {
// If no report provided, read from file
if (!brokenLinksReport) {
brokenLinksReport = readBrokenLinksReport();
}
// Read cache statistics and validation strategy
const cacheStats = readCacheStats();
const validationStrategy = readValidationStrategy();
// Display cache performance first
if (cacheStats) {
console.log('\n📊 Link Validation Cache Performance:');
console.log('=======================================');
console.log(`Cache hit rate: ${cacheStats.hitRate}%`);
console.log(`Cache hits: ${cacheStats.cacheHits}`);
console.log(`Cache misses: ${cacheStats.cacheMisses}`);
console.log(`Total validations: ${cacheStats.totalValidations || cacheStats.cacheHits + cacheStats.cacheMisses}`);
console.log(`New entries stored: ${cacheStats.newEntriesStored || 0}`);
if (cacheStats.cleanups > 0) {
console.log(`Expired entries cleaned: ${cacheStats.cleanups}`);
}
if (cacheStats.totalValidations > 0) {
const message = cacheStats.cacheHits > 0
? `✨ Cache optimization saved ${cacheStats.cacheHits} link validations`
: '🔄 No cache hits - all links were validated fresh';
console.log(message);
}
if (validationStrategy) {
console.log(`Files analyzed: ${validationStrategy.total}`);
console.log(
`Links needing validation: ${validationStrategy.newLinks.length}`
);
}
console.log(''); // Add spacing after cache stats
}
// Check both the report and first broken link file to determine if we have broken links
const firstBrokenLink = readFirstBrokenLink();
// Only report "no broken links" if both checks pass
if (
(!brokenLinksReport || brokenLinksReport.length === 0) &&
!firstBrokenLink
) {
console.log('\n✅ No broken links detected in the validation report');
return 0;
}
// Special case: check if the single broken link file could be missing from the report
if (
firstBrokenLink &&
(!brokenLinksReport || brokenLinksReport.length === 0)
) {
console.error(
'\n⚠ Warning: First broken link record exists but no links in the report.'
);
console.error('This could indicate a reporting issue.');
}
// Load sources mapping
const sourcesMapping = readSourcesMapping();
// Print a prominent header
console.error('\n\n' + '='.repeat(80));
console.error(' 🚨 BROKEN LINKS DETECTED 🚨 ');
console.error('='.repeat(80));
// Show first failing link if available
if (firstBrokenLink) {
console.error('\n🔴 FIRST FAILING LINK:');
console.error(` URL: ${firstBrokenLink.url}`);
console.error(` Status: ${firstBrokenLink.status}`);
console.error(` Type: ${firstBrokenLink.type}`);
console.error(` Page: ${firstBrokenLink.page}`);
if (firstBrokenLink.linkText) {
console.error(
` Link text: "${firstBrokenLink.linkText.substring(0, 50)}${firstBrokenLink.linkText.length > 50 ? '...' : ''}"`
);
}
console.error('-'.repeat(40));
}
let totalBrokenLinks = 0;
brokenLinksReport.forEach((report) => {
console.error(`\n📄 PAGE: ${report.page}`);
// Add source information if available
const source = sourcesMapping[report.page];
if (source) {
console.error(` PAGE CONTENT SOURCE: ${source}`);
}
console.error('-'.repeat(40));
report.links.forEach((link) => {
console.error(`${link.url}`);
console.error(` - Status: ${link.status}`);
console.error(` - Type: ${link.type}`);
if (link.linkText) {
console.error(
` - Link text: "${link.linkText.substring(0, 50)}${link.linkText.length > 50 ? '...' : ''}"`
);
}
console.error('');
totalBrokenLinks++;
});
});
// Print a prominent summary footer
console.error('='.repeat(80));
console.error(`📊 TOTAL BROKEN LINKS FOUND: ${totalBrokenLinks}`);
console.error('='.repeat(80) + '\n');
return totalBrokenLinks;
}
/**
* Reads the first broken link info from the file system
* @returns {Object|null} First broken link data or null if not found
*/
export function readFirstBrokenLink() {
if (!fs.existsSync(FIRST_BROKEN_LINK_FILE)) {
return null;
}
try {
const fileContent = fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8');
// Check if the file is empty or contains whitespace only
if (!fileContent || fileContent.trim() === '') {
return null;
}
// Try to parse the JSON content
try {
return JSON.parse(fileContent);
} catch (parseErr) {
console.error(
`Error parsing first broken link JSON: ${parseErr.message}`
);
return null;
}
} catch (err) {
console.error(`Error reading first broken link: ${err.message}`);
return null;
}
}
/**
* Initialize the broken links report files
* @returns {boolean} True if initialization was successful
*/
export function initializeReport() {
try {
// Create an empty array for the broken links report
fs.writeFileSync(BROKEN_LINKS_FILE, '[]', 'utf8');
// Reset the first broken link file by creating an empty file
// Using empty string as a clear indicator that no broken link has been recorded yet
fs.writeFileSync(FIRST_BROKEN_LINK_FILE, '', 'utf8');
console.debug('🔄 Initialized broken links reporting system');
return true;
} catch (err) {
console.error(`Error initializing broken links report: ${err.message}`);
return false;
}
}

View File

@ -2,34 +2,10 @@
* InfluxData Documentation E2E Test Runner
*
* This script automates running Cypress end-to-end tests for the InfluxData documentation site.
* It handles starting a local Hugo server, mapping content files to their URLs, running Cypress tests,
* It handles starting a local Hugo server, mapping content files to their URLs, and running Cypress tests,
* and reporting broken links.
*
* Usage: node run-e2e-specs.js [file paths...] [--spec test // Display broken links report
const brokenLinksCount = displayBrokenLinksReport();
// Check if we might have special case failures
const hasSpecialCaseFailures =
results &&
results.totalFailed > 0 &&
brokenLinksCount === 0;
if (hasSpecialCaseFailures) {
console.warn(
` Note: Tests failed (${results.totalFailed}) but no broken links were reported. This may be due to special case URLs (like Reddit) that return expected status codes.`
);
}
if (
(results && results.totalFailed && results.totalFailed > 0 && !hasSpecialCaseFailures) ||
brokenLinksCount > 0
) {
console.error(
`⚠️ Tests failed: ${results.totalFailed || 0} test(s) failed, ${brokenLinksCount || 0} broken links found`
);
cypressFailed = true;
exitCode = 1; *
* Example: node run-e2e-specs.js content/influxdb/v2/write-data.md --spec cypress/e2e/content/article-links.cy.js
* Usage: node run-e2e-specs.js [file paths...] [--spec test specs...]
*/
import { spawn } from 'child_process';
@ -39,7 +15,6 @@ import path from 'path';
import cypress from 'cypress';
import net from 'net';
import { Buffer } from 'buffer';
import { displayBrokenLinksReport, initializeReport } from './link-reporter.js';
import {
HUGO_ENVIRONMENT,
HUGO_PORT,
@ -119,7 +94,7 @@ async function main() {
let exitCode = 0;
let hugoStarted = false;
// (Lines 124-126 removed; no replacement needed)
// (Lines 124-126 removed; no replacement needed)
// Add this signal handler to ensure cleanup on unexpected termination
const cleanupAndExit = (code = 1) => {
@ -364,10 +339,6 @@ async function main() {
// 4. Run Cypress tests
let cypressFailed = false;
try {
// Initialize/clear broken links report before running tests
console.log('Initializing broken links report...');
initializeReport();
console.log(`Running Cypress tests for ${urlList.length} URLs...`);
// Add CI-specific configuration
@ -426,19 +397,13 @@ async function main() {
clearInterval(hugoHealthCheckInterval);
}
// Process broken links report
const brokenLinksCount = displayBrokenLinksReport();
// Determine why tests failed
const testFailureCount = results?.totalFailed || 0;
if (testFailureCount > 0 && brokenLinksCount === 0) {
if (testFailureCount > 0) {
console.warn(
` Note: ${testFailureCount} test(s) failed but no broken links were detected in the report.`
);
console.warn(
' This usually indicates test errors unrelated to link validation.'
);
// Provide detailed failure analysis
if (results) {
@ -531,14 +496,8 @@ async function main() {
// but we'll still report other test failures
cypressFailed = true;
exitCode = 1;
} else if (brokenLinksCount > 0) {
console.error(
`⚠️ Tests failed: ${brokenLinksCount} broken link(s) detected`
);
cypressFailed = true;
exitCode = 1;
} else if (results) {
console.log('✅ Tests completed successfully');
console.log('✅ e2e tests completed successfully');
}
} catch (err) {
console.error(`❌ Cypress execution error: ${err.message}`);
@ -609,9 +568,6 @@ async function main() {
console.error(' • Check if test URLs are accessible manually');
console.error(' • Review Cypress screenshots/videos if available');
// Still try to display broken links report if available
displayBrokenLinksReport();
cypressFailed = true;
exitCode = 1;
} finally {

View File

@ -111,16 +111,6 @@ pre-push:
node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/article-links.cy.js" content/example.md
exit $?
# Link validation runs in GitHub actions.
# You can still run it locally for development.
# e2e-links:
# tags: test,links
# glob: 'content/*.{md,html}'
# run: |
# echo "Running link checker for: {staged_files}"
# yarn test:links {staged_files}
# exit $?
# Manage Docker containers
prune-legacy-containers:
priority: 1

View File

@ -55,15 +55,6 @@
"test:codeblocks:v2": "docker compose run --rm --name v2-pytest v2-pytest",
"test:codeblocks:stop-monitors": "./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest && ./test/scripts/monitor-tests.sh stop clustered-pytest",
"test:e2e": "node cypress/support/run-e2e-specs.js",
"test:links": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\"",
"test:links:v1": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{v1,enterprise_influxdb}/**/*.{md,html}",
"test:links:v2": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{cloud,v2}/**/*.{md,html}",
"test:links:v3": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb3/**/*.{md,html}",
"test:links:chronograf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/chronograf/**/*.{md,html}",
"test:links:kapacitor": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/kapacitor/**/*.{md,html}",
"test:links:telegraf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/telegraf/**/*.{md,html}",
"test:links:shared": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/shared/**/*.{md,html}",
"test:links:api-docs": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" /influxdb3/core/api/,/influxdb3/enterprise/api/,/influxdb3/cloud-dedicated/api/,/influxdb3/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/management/,/influxdb3/cloud-dedicated/api/management/",
"test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md",
"audit:cli": "node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js both local",
"audit:cli:3core": "node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js core local",