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

454 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/// <reference types="cypress" />
describe('Article', () => {
let subjects = Cypress.env('test_subjects').split(',');
let validationStrategy = null;
// 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');
// Get source file paths for incremental validation
const testSubjectsData = Cypress.env('test_subjects_data');
let sourceFilePaths = subjects; // fallback to subjects if no data available
if (testSubjectsData) {
try {
const urlToSourceData = JSON.parse(testSubjectsData);
// Extract source file paths from the structured data
sourceFilePaths = urlToSourceData.map((item) => item.source);
} catch (e) {
console.warn(
'Could not parse test_subjects_data, using subjects as fallback'
);
}
}
// Run incremental validation analysis with source file paths
cy.task('runIncrementalValidation', sourceFilePaths)
.then((results) => {
validationStrategy = results.validationStrategy;
// Save cache statistics and validation strategy for reporting
cy.task('saveCacheStatistics', results.cacheStats);
cy.task('saveValidationStrategy', validationStrategy);
// Update subjects to only test files that need validation
if (results.filesToValidate.length > 0) {
// Convert file paths to URLs using shared utility via Cypress task
const urlPromises = results.filesToValidate.map((file) =>
cy.task('filePathToUrl', file.filePath)
);
cy.wrap(Promise.all(urlPromises)).then((urls) => {
subjects = urls;
cy.log(
`📊 Cache Analysis: ${results.cacheStats.hitRate}% hit rate`
);
cy.log(
`🔄 Testing ${subjects.length} pages (${results.cacheStats.cacheHits} cached)`
);
});
} else {
// All files are cached, no validation needed
subjects = [];
cy.log('✨ All files cached - skipping validation');
}
})
.catch((error) => {
cy.log('❌ Error during incremental validation task: ' + error.message);
// Provide more debugging information for validation failures
cy.log('🔍 Validation Error Details:');
cy.log(` • Error Type: ${error.name || 'Unknown'}`);
cy.log(` • Error Message: ${error.message}`);
if (error.stack) {
const stackLines = error.stack.split('\n').slice(0, 3);
cy.log(` • Stack Trace: ${stackLines.join(' -> ')}`);
}
cy.log(
'💡 This error occurred during cache analysis or file validation setup'
);
cy.log(' Check that all files exist and are readable');
// Instead of failing completely, fall back to testing all provided subjects
cy.log(
'🔄 Falling back to test all provided subjects without cache optimization'
);
// Reset validation strategy to indicate fallback mode
validationStrategy = {
fallback: true,
error: error.message,
unchanged: [],
changed: sourceFilePaths.map((filePath) => ({
filePath,
error: 'fallback',
})),
total: sourceFilePaths.length,
};
// Keep original subjects for testing (should come from test_subjects env var)
cy.log(`📋 Testing ${subjects.length} pages in fallback mode`);
});
});
// 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 to make appropriate request based on link type
function testLink(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
};
function handleFailedLink(url, status, type, redirectChain = '') {
// 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}`
);
}
if (useHeadForDownloads && isDownloadLink(href)) {
cy.log(`** Testing download link with HEAD: ${href} **`);
cy.request({
method: 'HEAD',
url: href,
...requestOptions,
}).then((response) => {
// Check final status after following any redirects
if (response.status >= 400) {
// Build redirect info string if available
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
handleFailedLink(href, response.status, 'download', redirectInfo);
}
});
} else {
cy.log(`** Testing link: ${href} **`);
cy.log(JSON.stringify(requestOptions));
cy.request({
url: href,
...requestOptions,
}).then((response) => {
// Check final status after following any redirects
if (response.status >= 400) {
// Build redirect info string if available
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
handleFailedLink(href, response.status, 'regular', redirectInfo);
}
});
}
}
// Test implementation for subjects
// Add debugging information about test subjects
it('Test Setup Validation', function () {
cy.log(`📋 Test Configuration:`);
cy.log(` • Test subjects count: ${subjects.length}`);
cy.log(` • Validation strategy: ${validationStrategy || 'Not set'}`);
// Check if we're in fallback mode due to cache system issues
if (validationStrategy && validationStrategy.fallback) {
cy.log('⚠️ Running in fallback mode due to cache system error');
cy.log(` • Error: ${validationStrategy.error}`);
cy.log(' • All files will be tested without cache optimization');
// Ensure we have subjects to test in fallback mode
expect(subjects.length).to.be.greaterThan(
0,
'Should have test subjects in fallback mode'
);
} else if (subjects.length === 0) {
cy.log('⚠️ No test subjects found - analyzing cause:');
cy.log(' • All files were cached and skipped');
cy.log(' • No files matched the test criteria');
cy.log(' • File mapping failed during setup');
// Don't fail if this is expected (cache hit scenario)
const testSubjectsData = Cypress.env('test_subjects_data');
if (testSubjectsData) {
cy.log('✅ Cache optimization active - this is expected');
cy.log(' Test subjects data is available, all files cached');
} else {
cy.log('❌ No test subjects data available - potential setup issue');
// Only fail if we have no data and no subjects - indicates a real problem
expect(testSubjectsData).to.not.be.empty(
'Should have test subjects data when no subjects to test'
);
}
} else {
cy.log(`✅ Ready to test ${subjects.length} pages`);
subjects.slice(0, 5).forEach((subject) => cy.log(`${subject}`));
if (subjects.length > 5) {
cy.log(` ... and ${subjects.length - 5} more pages`);
}
}
// Always pass if we get to this point - the setup is valid
cy.log('✅ Test setup validation completed successfully');
});
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}`);
})
.catch((error) => {
cy.log(`❌ Failed to load page: ${subject}`);
cy.log(` • Error: ${error.message}`);
cy.log('💡 This could indicate:');
cy.log(' • Hugo server not running or crashed');
cy.log(' • Invalid URL or routing issue');
cy.log(' • Network connectivity problems');
throw error; // Re-throw to fail the test properly
});
// 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
}
});
})
.catch((error) => {
cy.log(`❌ Error finding article content on ${subject}`);
cy.log(` • Error: ${error.message}`);
cy.log('💡 This could indicate:');
cy.log(' • Page structure changed (missing article/.api-content)');
cy.log(' • Page failed to render properly');
cy.log(' • JavaScript errors preventing DOM updates');
throw error;
});
});
it(`${subject} has valid anchor links`, function () {
cy.visit(`${subject}`)
.then(() => {
cy.log(`✅ Successfully loaded page for anchor testing: ${subject}`);
})
.catch((error) => {
cy.log(`❌ Failed to load page for anchor testing: ${subject}`);
cy.log(` • Error: ${error.message}`);
throw error;
});
// 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}`
);
})
.catch((error) => {
cy.log(
`❌ Failed to load page for external link testing: ${subject}`
);
cy.log(` • Error: ${error.message}`);
throw error;
});
// 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;
}
});
})
.catch((error) => {
cy.log(`❌ Error processing external links on ${subject}`);
cy.log(` • Error: ${error.message}`);
throw error;
});
});
});
});