Skip to content

Commit 279af04

Browse files
committed
add better error handling, documentation, improve type safety, move selectors to selectors.ts
1 parent a697925 commit 279af04

File tree

2 files changed

+35
-18
lines changed

2 files changed

+35
-18
lines changed

src/common/selectors.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,12 @@ export const selectors: Selectors = {
293293
}
294294
}
295295
}
296-
}
296+
}
297+
298+
/**
299+
* Global selectors for link validation and general page elements.
300+
*/
301+
export const linkValidationSelectors = {
302+
allLinksInContent: '#wpbody-content a[href]',
303+
tabLinksInContent: '#wpbody-content a[href*="page=wprocket#"]',
304+
} as const;

src/support/steps/broken-links.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@
33
* This module contains Cucumber step definitions for validating links in WP Rocket settings UI.
44
*
55
* @requires {@link ../../common/custom-world}
6-
* @requires {@link @playwright/test}
76
* @requires {@link @cucumber/cucumber}
7+
* @requires {@link ../../common/selectors}
88
*/
99
import { Then } from '@cucumber/cucumber';
1010
import { ICustomWorld } from '../../common/custom-world';
11+
import { linkValidationSelectors } from '../../common/selectors';
1112

1213
/**
13-
* Executes the step to verify that WP Rocket settings links are not broken.
14+
* Step definition that verifies WP Rocket settings links are not broken by collecting all links from settings tabs
15+
* and checking their HTTP status codes. Fails on client errors (4xx) and logs warnings for server errors (5xx)
16+
* to avoid flakiness from transient backend issues.
1417
*
1518
* @function
1619
* @async
1720
* @param {ICustomWorld} this - The Cucumber world context for the current scenario.
18-
* @return {Promise<void>} - A Promise that resolves when the check is completed.
21+
* @return {Promise<void>} - A Promise that resolves when all links have been validated.
1922
*/
2023
Then('WP Rocket settings links are not broken', async function (this: ICustomWorld) {
2124
const hrefs = new Set<string>();
@@ -24,7 +27,7 @@ Then('WP Rocket settings links are not broken', async function (this: ICustomWor
2427
await this.utils.visitPage('wp-admin/options-general.php?page=wprocket');
2528

2629
const collectLinks = async (): Promise<void> => {
27-
const links = await this.page.$$eval('#wpbody-content a[href]', (elements) =>
30+
const links = await this.page.$$eval(linkValidationSelectors.allLinksInContent, (elements) =>
2831
elements
2932
.map((element) => element.getAttribute('href'))
3033
.filter((href): href is string => Boolean(href))
@@ -39,16 +42,19 @@ Then('WP Rocket settings links are not broken', async function (this: ICustomWor
3942
await collectLinks();
4043

4144
// Collect tab links from within the WP Rocket settings content.
42-
const tabHrefs = await this.page.$$eval('#wpbody-content a[href*="page=wprocket#"]', (links) =>
45+
// Links are collected from each tab separately because different tabs may display different content
46+
// and therefore different links. This ensures comprehensive link coverage across all settings sections.
47+
const tabHrefs = await this.page.$$eval(linkValidationSelectors.tabLinksInContent, (links) =>
4348
links
4449
.map((link) => link.getAttribute('href'))
45-
.filter(Boolean)
50+
.filter((href): href is string => Boolean(href))
4651
);
4752

4853
const tabUrls = Array.from(new Set(tabHrefs)).map((href) => {
49-
return new URL(href as string, this.page.url()).toString();
54+
return new URL(href, this.page.url()).toString();
5055
});
5156

57+
// Visit each tab and collect its links to ensure all links across all settings sections are validated
5258
for (const tabUrl of tabUrls) {
5359
await this.page.goto(tabUrl);
5460
await this.page.waitForLoadState('load');
@@ -97,30 +103,33 @@ Then('WP Rocket settings links are not broken', async function (this: ICustomWor
97103
try {
98104
const response = await this.page.request.get(url, { maxRedirects: 5, timeout: 30000 });
99105

100-
// Only fail on 404 - this indicates a truly broken link
101-
if (response.status() === 404) {
102-
throw new Error(`Link is broken: ${url} returned 404`);
106+
const status = response.status();
107+
108+
// Treat all client errors (4xx) as hard failures, as they usually indicate broken or unauthorized links.
109+
// This includes 401/403, which can reveal authentication/authorization problems, not just 404 "not found".
110+
if (status >= 400 && status < 500) {
111+
throw new Error(`Client error: ${url} returned status ${status}`);
103112
}
104113

105-
// For other non-2xx/3xx status codes (like 5xx), log but don't fail
106-
if (response.status() >= 400) {
114+
// For server errors (5xx), log but don't fail to avoid flakiness from transient backend issues.
115+
if (status >= 500) {
107116
// eslint-disable-next-line no-console
108117
console.warn(
109-
`Warning: ${url} returned status ${response.status()}, but continuing test (only 404s fail)`
118+
`Warning: ${url} returned server error status ${status}, but continuing test to avoid flakiness.`
110119
);
111120
}
112121
} catch (error) {
113122
const message = error instanceof Error ? error.message : String(error);
114123

115-
// If this is our explicit 404 error, re-throw it to fail the test
116-
if (message.includes('returned 404')) {
124+
// If this is a client error (4xx), re-throw it to fail the test.
125+
if (message.startsWith('Client error:')) {
117126
throw error;
118127
}
119128

120-
// For network errors (timeouts, connection issues, etc.), log but don't fail
129+
// For network errors (timeouts, connection issues, etc.), log but don't fail.
121130
// eslint-disable-next-line no-console
122131
console.warn(
123-
`Warning: Network error while requesting ${url}: ${message}. Continuing test (only 404s fail).`
132+
`Warning: Network or non-client error while requesting ${url}: ${message}. Continuing test.`
124133
);
125134
}
126135
}

0 commit comments

Comments
 (0)