chore(ci): Removes old Cypress link checker test code
parent
f5df3cb6f0
commit
a8578bb0af
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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()
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
10
lefthook.yml
10
lefthook.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue