@@ -6,8 +6,8 @@ describe('Article', () => {
66 . split ( ',' )
77 . filter ( ( s ) => s . trim ( ) !== '' )
88 : [ ] ;
9- let validationStrategy = null ;
10- let shouldSkipAllTests = false ; // Flag to skip tests when all files are cached
9+
10+ // Cache will be checked during test execution at the URL level
1111
1212 // Always use HEAD for downloads to avoid timeouts
1313 const useHeadForDownloads = true ;
@@ -16,6 +16,42 @@ describe('Article', () => {
1616 before ( ( ) => {
1717 // Initialize the broken links report
1818 cy . task ( 'initializeBrokenLinksReport' ) ;
19+
20+ // Clean up expired cache entries
21+ cy . task ( 'cleanupCache' ) . then ( ( cleaned ) => {
22+ if ( cleaned > 0 ) {
23+ cy . log ( `🧹 Cleaned up ${ cleaned } expired cache entries` ) ;
24+ }
25+ } ) ;
26+ } ) ;
27+
28+ // Display cache statistics after all tests complete
29+ after ( ( ) => {
30+ cy . task ( 'getCacheStats' ) . then ( ( stats ) => {
31+ cy . log ( '📊 Link Validation Cache Statistics:' ) ;
32+ cy . log ( ` • Cache hits: ${ stats . hits } ` ) ;
33+ cy . log ( ` • Cache misses: ${ stats . misses } ` ) ;
34+ cy . log ( ` • New entries stored: ${ stats . stores } ` ) ;
35+ cy . log ( ` • Hit rate: ${ stats . hitRate } ` ) ;
36+ cy . log ( ` • Total validations: ${ stats . total } ` ) ;
37+
38+ if ( stats . total > 0 ) {
39+ const message = stats . hits > 0
40+ ? `✨ Cache optimization saved ${ stats . hits } link validations`
41+ : '🔄 No cache hits - all links were validated fresh' ;
42+ cy . log ( message ) ;
43+ }
44+
45+ // Save cache statistics for the reporter to display
46+ cy . task ( 'saveCacheStatsForReporter' , {
47+ hitRate : parseFloat ( stats . hitRate . replace ( '%' , '' ) ) ,
48+ cacheHits : stats . hits ,
49+ cacheMisses : stats . misses ,
50+ totalValidations : stats . total ,
51+ newEntriesStored : stats . stores ,
52+ cleanups : stats . cleanups
53+ } ) ;
54+ } ) ;
1955 } ) ;
2056
2157 // Helper function to identify download links
@@ -57,8 +93,45 @@ describe('Article', () => {
5793 return hasDownloadExtension || isFromDownloadDomain ;
5894 }
5995
60- // Helper function to make appropriate request based on link type
96+ // Helper function for handling failed links
97+ function handleFailedLink ( url , status , type , redirectChain = '' , linkText = '' , pageUrl = '' ) {
98+ // Report the broken link
99+ cy . task ( 'reportBrokenLink' , {
100+ url : url + redirectChain ,
101+ status,
102+ type,
103+ linkText,
104+ page : pageUrl ,
105+ } ) ;
106+
107+ // Throw error for broken links
108+ throw new Error (
109+ `BROKEN ${ type . toUpperCase ( ) } LINK: ${ url } (status: ${ status } )${ redirectChain } on ${ pageUrl } `
110+ ) ;
111+ }
112+
113+ // Helper function to test a link with cache integration
61114 function testLink ( href , linkText = '' , pageUrl ) {
115+ // Check cache first
116+ return cy . task ( 'isLinkCached' , href ) . then ( ( isCached ) => {
117+ if ( isCached ) {
118+ cy . log ( `✅ Cache hit: ${ href } ` ) ;
119+ return cy . task ( 'getLinkCache' , href ) . then ( ( cachedResult ) => {
120+ if ( cachedResult && cachedResult . result && cachedResult . result . status >= 400 ) {
121+ // Cached result shows this link is broken
122+ handleFailedLink ( href , cachedResult . result . status , cachedResult . result . type || 'cached' , '' , linkText , pageUrl ) ;
123+ }
124+ // For successful cached results, just return - no further action needed
125+ } ) ;
126+ } else {
127+ // Not cached, perform actual validation
128+ return performLinkValidation ( href , linkText , pageUrl ) ;
129+ }
130+ } ) ;
131+ }
132+
133+ // Helper function to perform actual link validation and cache the result
134+ function performLinkValidation ( href , linkText = '' , pageUrl ) {
62135 // Common request options for both methods
63136 const requestOptions = {
64137 failOnStatusCode : true ,
@@ -68,196 +141,78 @@ describe('Article', () => {
68141 retryOnStatusCodeFailure : true , // Retry on 5xx errors
69142 } ;
70143
71- function handleFailedLink ( url , status , type , redirectChain = '' ) {
72- // Report the broken link
73- cy . task ( 'reportBrokenLink' , {
74- url : url + redirectChain ,
75- status,
76- type,
77- linkText,
78- page : pageUrl ,
79- } ) ;
80-
81- // Throw error for broken links
82- throw new Error (
83- `BROKEN ${ type . toUpperCase ( ) } LINK: ${ url } (status: ${ status } )${ redirectChain } on ${ pageUrl } `
84- ) ;
85- }
86144
87145 if ( useHeadForDownloads && isDownloadLink ( href ) ) {
88146 cy . log ( `** Testing download link with HEAD: ${ href } **` ) ;
89- cy . request ( {
147+ return cy . request ( {
90148 method : 'HEAD' ,
91149 url : href ,
92150 ...requestOptions ,
93151 } ) . then ( ( response ) => {
152+ // Prepare result for caching
153+ const result = {
154+ status : response . status ,
155+ type : 'download' ,
156+ timestamp : new Date ( ) . toISOString ( )
157+ } ;
158+
94159 // Check final status after following any redirects
95160 if ( response . status >= 400 ) {
96- // Build redirect info string if available
97161 const redirectInfo =
98162 response . redirects && response . redirects . length > 0
99163 ? ` (redirected to: ${ response . redirects . join ( ' -> ' ) } )`
100164 : '' ;
101-
102- handleFailedLink ( href , response . status , 'download' , redirectInfo ) ;
165+
166+ // Cache the failed result
167+ cy . task ( 'setLinkCache' , { url : href , result } ) ;
168+ handleFailedLink ( href , response . status , 'download' , redirectInfo , linkText , pageUrl ) ;
169+ } else {
170+ // Cache the successful result
171+ cy . task ( 'setLinkCache' , { url : href , result } ) ;
103172 }
104173 } ) ;
105174 } else {
106175 cy . log ( `** Testing link: ${ href } **` ) ;
107- cy . log ( JSON . stringify ( requestOptions ) ) ;
108- cy . request ( {
176+ return cy . request ( {
109177 url : href ,
110178 ...requestOptions ,
111179 } ) . then ( ( response ) => {
112- // Check final status after following any redirects
180+ // Prepare result for caching
181+ const result = {
182+ status : response . status ,
183+ type : 'regular' ,
184+ timestamp : new Date ( ) . toISOString ( )
185+ } ;
186+
113187 if ( response . status >= 400 ) {
114- // Build redirect info string if available
115188 const redirectInfo =
116189 response . redirects && response . redirects . length > 0
117190 ? ` (redirected to: ${ response . redirects . join ( ' -> ' ) } )`
118191 : '' ;
119-
120- handleFailedLink ( href , response . status , 'regular' , redirectInfo ) ;
192+
193+ // Cache the failed result
194+ cy . task ( 'setLinkCache' , { url : href , result } ) ;
195+ handleFailedLink ( href , response . status , 'regular' , redirectInfo , linkText , pageUrl ) ;
196+ } else {
197+ // Cache the successful result
198+ cy . task ( 'setLinkCache' , { url : href , result } ) ;
121199 }
122200 } ) ;
123201 }
124202 }
125203
126- // Test implementation for subjects
127- // Add debugging information about test subjects
204+ // Test setup validation
128205 it ( 'Test Setup Validation' , function ( ) {
129- cy . log ( `📋 Initial Test Configuration:` ) ;
130- cy . log ( ` • Initial test subjects count: ${ subjects . length } ` ) ;
131-
132- // Get source file paths for incremental validation
133- const testSubjectsData = Cypress . env ( 'test_subjects_data' ) ;
134- let sourceFilePaths = subjects ; // fallback to subjects if no data available
135-
136- if ( testSubjectsData ) {
137- try {
138- const urlToSourceData = JSON . parse ( testSubjectsData ) ;
139- // Extract source file paths from the structured data
140- sourceFilePaths = urlToSourceData . map ( ( item ) => item . source ) ;
141- cy . log ( ` • Source files to analyze: ${ sourceFilePaths . length } ` ) ;
142- } catch ( e ) {
143- cy . log (
144- '⚠️ Could not parse test_subjects_data, using subjects as fallback'
145- ) ;
146- sourceFilePaths = subjects ;
147- }
148- }
149-
150- // Only run incremental validation if we have source file paths
151- if ( sourceFilePaths . length > 0 ) {
152- cy . log ( '🔄 Running incremental validation analysis...' ) ;
153- cy . log (
154- ` • Analyzing ${ sourceFilePaths . length } files: ${ sourceFilePaths . join ( ', ' ) } `
155- ) ;
156-
157- // Run incremental validation with proper error handling
158- cy . task ( 'runIncrementalValidation' , sourceFilePaths ) . then ( ( results ) => {
159- if ( ! results ) {
160- cy . log ( '⚠️ No results returned from incremental validation' ) ;
161- cy . log (
162- '🔄 Falling back to test all provided subjects without cache optimization'
163- ) ;
164- return ;
165- }
166-
167- // Check if results have expected structure
168- if ( ! results . validationStrategy || ! results . cacheStats ) {
169- cy . log ( '⚠️ Incremental validation results missing expected fields' ) ;
170- cy . log ( ` • Results: ${ JSON . stringify ( results ) } ` ) ;
171- cy . log (
172- '🔄 Falling back to test all provided subjects without cache optimization'
173- ) ;
174- return ;
175- }
176-
177- validationStrategy = results . validationStrategy ;
178-
179- // Save cache statistics and validation strategy for reporting
180- cy . task ( 'saveCacheStatistics' , results . cacheStats ) ;
181- cy . task ( 'saveValidationStrategy' , validationStrategy ) ;
182-
183- // Update subjects to only test files that need validation
184- if ( results . filesToValidate && results . filesToValidate . length > 0 ) {
185- // Convert file paths to URLs using shared utility via Cypress task
186- const urlPromises = results . filesToValidate . map ( ( file ) =>
187- cy . task ( 'filePathToUrl' , file . filePath )
188- ) ;
189-
190- cy . wrap ( Promise . all ( urlPromises ) ) . then ( ( urls ) => {
191- subjects = urls ;
192-
193- cy . log (
194- `📊 Cache Analysis: ${ results . cacheStats . hitRate } % hit rate`
195- ) ;
196- cy . log (
197- `🔄 Testing ${ subjects . length } pages (${ results . cacheStats . cacheHits } cached)`
198- ) ;
199- cy . log ( '✅ Incremental validation completed - ready to test' ) ;
200- } ) ;
201- } else {
202- // All files are cached, no validation needed
203- shouldSkipAllTests = true ; // Set flag to skip all tests
204- cy . log ( '✨ All files cached - will skip all validation tests' ) ;
205- cy . log (
206- `📊 Cache hit rate: ${ results . cacheStats . hitRate } % (${ results . cacheStats . cacheHits } /${ results . cacheStats . totalFiles } files cached)`
207- ) ;
208- cy . log ( '🎯 No new validation needed - this is the expected outcome' ) ;
209- cy . log ( '⏭️ All link validation tests will be skipped' ) ;
210- }
211- } ) ;
212- } else {
213- cy . log ( '⚠️ No source file paths available, using all provided subjects' ) ;
214-
215- // Set a simple validation strategy when no source data is available
216- validationStrategy = {
217- noSourceData : true ,
218- unchanged : [ ] ,
219- changed : [ ] ,
220- total : subjects . length ,
221- } ;
222-
223- cy . log (
224- `📋 Testing ${ subjects . length } pages without incremental validation`
225- ) ;
226- }
227-
228- // Check for truly problematic scenarios
229- if ( ! validationStrategy && subjects . length === 0 ) {
230- const testSubjectsData = Cypress . env ( 'test_subjects_data' ) ;
231- if (
232- ! testSubjectsData ||
233- testSubjectsData === '' ||
234- testSubjectsData === '[]'
235- ) {
236- cy . log ( '❌ Critical setup issue detected:' ) ;
237- cy . log ( ' • No validation strategy' ) ;
238- cy . log ( ' • No test subjects' ) ;
239- cy . log ( ' • No test subjects data' ) ;
240- cy . log ( ' This indicates a fundamental configuration problem' ) ;
241-
242- // Only fail in this truly problematic case
243- throw new Error (
244- 'Critical test setup failure: No strategy, subjects, or data available'
245- ) ;
246- }
247- }
248-
249- // Always pass if we get to this point - the setup is valid
250- cy . log ( '✅ Test setup validation completed successfully' ) ;
206+ cy . log ( `📋 Test Configuration:` ) ;
207+ cy . log ( ` • Test subjects: ${ subjects . length } ` ) ;
208+ cy . log ( ` • Cache: URL-level caching with 30-day TTL` ) ;
209+ cy . log ( ` • Link validation: Internal, anchor, and allowed external links` ) ;
210+
211+ cy . log ( '✅ Test setup validation completed' ) ;
251212 } ) ;
252213
253214 subjects . forEach ( ( subject ) => {
254215 it ( `${ subject } has valid internal links` , function ( ) {
255- // Skip test if all files are cached
256- if ( shouldSkipAllTests ) {
257- cy . log ( '✅ All files cached - skipping internal links test' ) ;
258- this . skip ( ) ;
259- return ;
260- }
261216
262217 // Add error handling for page visit failures
263218 cy . visit ( `${ subject } ` , { timeout : 20000 } ) . then ( ( ) => {
@@ -291,12 +246,6 @@ describe('Article', () => {
291246 } ) ;
292247
293248 it ( `${ subject } has valid anchor links` , function ( ) {
294- // Skip test if all files are cached
295- if ( shouldSkipAllTests ) {
296- cy . log ( '✅ All files cached - skipping anchor links test' ) ;
297- this . skip ( ) ;
298- return ;
299- }
300249
301250 cy . visit ( `${ subject } ` ) . then ( ( ) => {
302251 cy . log ( `✅ Successfully loaded page for anchor testing: ${ subject } ` ) ;
@@ -351,12 +300,6 @@ describe('Article', () => {
351300 } ) ;
352301
353302 it ( `${ subject } has valid external links` , function ( ) {
354- // Skip test if all files are cached
355- if ( shouldSkipAllTests ) {
356- cy . log ( '✅ All files cached - skipping external links test' ) ;
357- this . skip ( ) ;
358- return ;
359- }
360303
361304 // Check if we should skip external links entirely
362305 if ( Cypress . env ( 'skipExternalLinks' ) === true ) {
0 commit comments