@@ -6,8 +6,8 @@ describe('Article', () => {
6
6
. split ( ',' )
7
7
. filter ( ( s ) => s . trim ( ) !== '' )
8
8
: [ ] ;
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
11
11
12
12
// Always use HEAD for downloads to avoid timeouts
13
13
const useHeadForDownloads = true ;
@@ -16,6 +16,42 @@ describe('Article', () => {
16
16
before ( ( ) => {
17
17
// Initialize the broken links report
18
18
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
+ } ) ;
19
55
} ) ;
20
56
21
57
// Helper function to identify download links
@@ -57,8 +93,45 @@ describe('Article', () => {
57
93
return hasDownloadExtension || isFromDownloadDomain ;
58
94
}
59
95
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
61
114
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 ) {
62
135
// Common request options for both methods
63
136
const requestOptions = {
64
137
failOnStatusCode : true ,
@@ -68,196 +141,78 @@ describe('Article', () => {
68
141
retryOnStatusCodeFailure : true , // Retry on 5xx errors
69
142
} ;
70
143
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
- }
86
144
87
145
if ( useHeadForDownloads && isDownloadLink ( href ) ) {
88
146
cy . log ( `** Testing download link with HEAD: ${ href } **` ) ;
89
- cy . request ( {
147
+ return cy . request ( {
90
148
method : 'HEAD' ,
91
149
url : href ,
92
150
...requestOptions ,
93
151
} ) . then ( ( response ) => {
152
+ // Prepare result for caching
153
+ const result = {
154
+ status : response . status ,
155
+ type : 'download' ,
156
+ timestamp : new Date ( ) . toISOString ( )
157
+ } ;
158
+
94
159
// Check final status after following any redirects
95
160
if ( response . status >= 400 ) {
96
- // Build redirect info string if available
97
161
const redirectInfo =
98
162
response . redirects && response . redirects . length > 0
99
163
? ` (redirected to: ${ response . redirects . join ( ' -> ' ) } )`
100
164
: '' ;
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 } ) ;
103
172
}
104
173
} ) ;
105
174
} else {
106
175
cy . log ( `** Testing link: ${ href } **` ) ;
107
- cy . log ( JSON . stringify ( requestOptions ) ) ;
108
- cy . request ( {
176
+ return cy . request ( {
109
177
url : href ,
110
178
...requestOptions ,
111
179
} ) . 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
+
113
187
if ( response . status >= 400 ) {
114
- // Build redirect info string if available
115
188
const redirectInfo =
116
189
response . redirects && response . redirects . length > 0
117
190
? ` (redirected to: ${ response . redirects . join ( ' -> ' ) } )`
118
191
: '' ;
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 } ) ;
121
199
}
122
200
} ) ;
123
201
}
124
202
}
125
203
126
- // Test implementation for subjects
127
- // Add debugging information about test subjects
204
+ // Test setup validation
128
205
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' ) ;
251
212
} ) ;
252
213
253
214
subjects . forEach ( ( subject ) => {
254
215
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
- }
261
216
262
217
// Add error handling for page visit failures
263
218
cy . visit ( `${ subject } ` , { timeout : 20000 } ) . then ( ( ) => {
@@ -291,12 +246,6 @@ describe('Article', () => {
291
246
} ) ;
292
247
293
248
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
- }
300
249
301
250
cy . visit ( `${ subject } ` ) . then ( ( ) => {
302
251
cy . log ( `✅ Successfully loaded page for anchor testing: ${ subject } ` ) ;
@@ -351,12 +300,6 @@ describe('Article', () => {
351
300
} ) ;
352
301
353
302
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
- }
360
303
361
304
// Check if we should skip external links entirely
362
305
if ( Cypress . env ( 'skipExternalLinks' ) === true ) {
0 commit comments