diff --git a/cypress.config.js b/cypress.config.js index d7ffed8fc..5148f60ec 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -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 { diff --git a/cypress/e2e/content/article-links.cy.js b/cypress/e2e/content/article-links.cy.js deleted file mode 100644 index 0ce8d4677..000000000 --- a/cypress/e2e/content/article-links.cy.js +++ /dev/null @@ -1,370 +0,0 @@ -/// - -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; - } - }); - }); - }); - }); -}); diff --git a/cypress/e2e/content/example.cy.js b/cypress/e2e/content/example.cy.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/cypress/support/link-cache.js b/cypress/support/link-cache.js deleted file mode 100644 index 1a54a6e41..000000000 --- a/cypress/support/link-cache.js +++ /dev/null @@ -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() - }; -}; \ No newline at end of file diff --git a/cypress/support/link-reporter.js b/cypress/support/link-reporter.js deleted file mode 100644 index fa514c7ef..000000000 --- a/cypress/support/link-reporter.js +++ /dev/null @@ -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; - } -} diff --git a/cypress/support/run-e2e-specs.js b/cypress/support/run-e2e-specs.js index d39dfb4a2..71f1616fa 100644 --- a/cypress/support/run-e2e-specs.js +++ b/cypress/support/run-e2e-specs.js @@ -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 { diff --git a/lefthook.yml b/lefthook.yml index 68face524..67db3a771 100644 --- a/lefthook.yml +++ b/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 diff --git a/package.json b/package.json index 4dfb14b81..fc09f72a5 100644 --- a/package.json +++ b/package.json @@ -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",