pagean 6.0.8 → 6.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -111
- package/bin/pagean.js +13 -6
- package/bin/pageanrc-lint.js +7 -8
- package/docs/upgrade-guide.md +26 -26
- package/index.js +19 -4
- package/lib/config.js +34 -12
- package/lib/default-config.json +2 -8
- package/lib/external-file-utils.js +6 -3
- package/lib/link-utils.js +23 -19
- package/lib/logger.js +7 -3
- package/lib/reporter.js +4 -2
- package/lib/schema-errors.js +22 -8
- package/lib/test-utils.js +19 -7
- package/lib/tests.js +193 -91
- package/package.json +22 -18
- package/schemas/pageanrc.schema.json +3 -11
package/lib/link-utils.js
CHANGED
|
@@ -45,15 +45,14 @@ const noRetryResponses = new Set([httpResponse.tooManyRequests]);
|
|
|
45
45
|
* @param {string} url The URL to normalize.
|
|
46
46
|
* @returns {string} The normalized URL.
|
|
47
47
|
*/
|
|
48
|
-
const normalizeLink = url =>
|
|
49
|
-
{
|
|
48
|
+
const normalizeLink = (url) =>
|
|
49
|
+
normalizeUrl(url, {
|
|
50
50
|
defaultProtocol: 'https:',
|
|
51
51
|
removeQueryParameters: [],
|
|
52
52
|
stripHash: true,
|
|
53
53
|
stripWWW: false
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
|
|
57
56
|
/**
|
|
58
57
|
* Checks settings to determine if the provided link should be ignored.
|
|
59
58
|
*
|
|
@@ -64,8 +63,8 @@ const normalizeLink = url => normalizeUrl(url,
|
|
|
64
63
|
* @param {string} link The link to check against the ignore list.
|
|
65
64
|
* @returns {boolean} True if the link should be ignored, otherwise false.
|
|
66
65
|
*/
|
|
67
|
-
const ignoreLink = (settings, link) =>
|
|
68
|
-
|
|
66
|
+
const ignoreLink = (settings, link) =>
|
|
67
|
+
settings.ignoredLinks && settings.ignoredLinks.includes(link);
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* Checks a response to an HTTP request, either a response code or explicit error,
|
|
@@ -76,8 +75,9 @@ const ignoreLink = (settings, link) => settings.ignoredLinks && settings.ignored
|
|
|
76
75
|
* @param {(string|number)} response The response to an HTTP request to check for failure.
|
|
77
76
|
* @returns {boolean} True if failed, otherwise false.
|
|
78
77
|
*/
|
|
79
|
-
const isFailedResponse =
|
|
80
|
-
|
|
78
|
+
const isFailedResponse = (response) =>
|
|
79
|
+
Number.isNaN(Number(response.status)) ||
|
|
80
|
+
response.status >= httpResponse.badRequest;
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Checks a Puppeteer page for the element specified in the hash of the provided link.
|
|
@@ -98,7 +98,6 @@ const checkSamePageLink = async (page, link) => {
|
|
|
98
98
|
return element ? httpResponse.ok : `${selector} Not Found`;
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
|
|
102
101
|
/**
|
|
103
102
|
* Checks the provided link for validity by loading in a Puppeteer page.
|
|
104
103
|
*
|
|
@@ -115,8 +114,7 @@ const checkExternalPageLinkBrowser = async (page, link) => {
|
|
|
115
114
|
const response = await testPage.goto(link);
|
|
116
115
|
status = response.status();
|
|
117
116
|
await testPage.close();
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
117
|
+
} catch (error) {
|
|
120
118
|
// Errors are returned in the format: "ENOTFOUND at https://this.url.does.not.exist/",
|
|
121
119
|
// so extract error only and remove URL
|
|
122
120
|
status = error.message.replace(/^(.*) at .*$/, '$1');
|
|
@@ -124,7 +122,6 @@ const checkExternalPageLinkBrowser = async (page, link) => {
|
|
|
124
122
|
return status;
|
|
125
123
|
};
|
|
126
124
|
|
|
127
|
-
|
|
128
125
|
/**
|
|
129
126
|
* Checks the provided link for validity by requesting with axios. If useGet if false,
|
|
130
127
|
* a HEAD request is made for efficiency. If useGet is true, a full GET request is made.
|
|
@@ -142,7 +139,10 @@ const checkExternalPageLink = async (page, link, useGet = false) => {
|
|
|
142
139
|
const userAgent = await page.evaluate('navigator.userAgent');
|
|
143
140
|
|
|
144
141
|
try {
|
|
145
|
-
const options = {
|
|
142
|
+
const options = {
|
|
143
|
+
headers: { 'User-Agent': userAgent },
|
|
144
|
+
timeout: timeoutSeconds * msPerSec
|
|
145
|
+
};
|
|
146
146
|
// Using internal browser property since not exposed
|
|
147
147
|
// eslint-disable-next-line no-underscore-dangle
|
|
148
148
|
if (page.browser()._ignoreHTTPSErrors) {
|
|
@@ -152,12 +152,15 @@ const checkExternalPageLink = async (page, link, useGet = false) => {
|
|
|
152
152
|
const httpMethod = useGet ? axios.get : axios.head;
|
|
153
153
|
const response = await httpMethod(link, options);
|
|
154
154
|
return response.status;
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
155
|
+
} catch (error) {
|
|
157
156
|
// Some servers respond invalid for head request (e.g. a lot of 405 Method
|
|
158
157
|
// Not Allowed), so if a response was returned, is not a response that should
|
|
159
158
|
// skip a retry (e.g. 429), and the request was head then retry with get.
|
|
160
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
error.response &&
|
|
161
|
+
!noRetryResponses.has(error.response.status) &&
|
|
162
|
+
!useGet
|
|
163
|
+
) {
|
|
161
164
|
return checkExternalPageLink(page, link, true);
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -167,7 +170,6 @@ const checkExternalPageLink = async (page, link, useGet = false) => {
|
|
|
167
170
|
}
|
|
168
171
|
};
|
|
169
172
|
|
|
170
|
-
|
|
171
173
|
/**
|
|
172
174
|
* Factory function returning a linkChecker object with a {@link checkLink}
|
|
173
175
|
* function that caches checked link results.
|
|
@@ -205,15 +207,17 @@ const createLinkChecker = () => {
|
|
|
205
207
|
return httpResponse.continue;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
context.testSettings.ignoreDuplicates &&
|
|
212
|
+
checkedLinks.has(link)
|
|
213
|
+
) {
|
|
209
214
|
return checkedLinks.get(link);
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
status = await (context.testSettings.checkWithBrowser
|
|
213
218
|
? checkExternalPageLinkBrowser(context.page, normalizedLink)
|
|
214
219
|
: checkExternalPageLink(context.page, normalizedLink));
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
220
|
+
} catch {
|
|
217
221
|
status = httpResponse.unknownError;
|
|
218
222
|
}
|
|
219
223
|
|
package/lib/logger.js
CHANGED
|
@@ -15,7 +15,7 @@ const testResultSymbols = Object.freeze({
|
|
|
15
15
|
warning: ' ‼'
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
const nullFunction = () => {
|
|
18
|
+
const nullFunction = () => {};
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Factory function that creates an instance of a
|
|
@@ -29,7 +29,9 @@ const nullFunction = () => { };
|
|
|
29
29
|
module.exports = (config) => {
|
|
30
30
|
const cliReporter = {
|
|
31
31
|
// If cli reporter is enabled, set console log function, otherwise null function
|
|
32
|
-
log: config.reporters.includes(reporterTypes.cli)
|
|
32
|
+
log: config.reporters.includes(reporterTypes.cli)
|
|
33
|
+
? consoleLogger.log
|
|
34
|
+
: nullFunction,
|
|
33
35
|
// The logAlways function provides a mechanism to log to the console even
|
|
34
36
|
// when the cli reporter is disabled (e.g. returning the final results)
|
|
35
37
|
logAlways: consoleLogger.log
|
|
@@ -87,7 +89,9 @@ module.exports = (config) => {
|
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
const outputTestSummary = () => {
|
|
90
|
-
cliReporter.logAlways({
|
|
92
|
+
cliReporter.logAlways({
|
|
93
|
+
message: `\n Tests: ${testResults.summary.tests}\n Passed: ${testResults.summary.passed}\n Warning: ${testResults.summary.warning}\n Failed: ${testResults.summary.failed}\n`
|
|
94
|
+
});
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
/**
|
package/lib/reporter.js
CHANGED
|
@@ -19,8 +19,10 @@ handlebars.registerHelper('json', (context) => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
const saveHtmlReport = (results) => {
|
|
22
|
-
const templateFile = path.resolve(
|
|
23
|
-
|
|
22
|
+
const templateFile = path.resolve(
|
|
23
|
+
path.join(__dirname, htmlReportTemplateName)
|
|
24
|
+
);
|
|
25
|
+
const htmlReportTemplate = fs.readFileSync(templateFile, 'utf8');
|
|
24
26
|
const template = handlebars.compile(htmlReportTemplate);
|
|
25
27
|
const htmlReport = template(results);
|
|
26
28
|
fs.writeFileSync(htmlReportFileName, htmlReport);
|
package/lib/schema-errors.js
CHANGED
|
@@ -16,13 +16,22 @@ const getDataKey = (instancePath) => {
|
|
|
16
16
|
// Convert pointer to key be splitting by /, removing first value (since pointer
|
|
17
17
|
// starts with /, so empty), unencoding values, and the re-assembling with different
|
|
18
18
|
// formatting for numeric array indices versus properties.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
return (
|
|
20
|
+
baseKey +
|
|
21
|
+
instancePath
|
|
22
|
+
.split('/')
|
|
23
|
+
.slice(1)
|
|
24
|
+
// eslint-disable-next-line unicorn/no-array-reduce -- reduce for string concatenation
|
|
25
|
+
.reduce((accumulator, currentValue) => {
|
|
26
|
+
const unencodedValue = currentValue
|
|
27
|
+
.replace(/~0/g, '~')
|
|
28
|
+
.replace(/~1/g, '/');
|
|
29
|
+
const encodedValue = Number.isNaN(Number(unencodedValue))
|
|
30
|
+
? `.${unencodedValue}`
|
|
31
|
+
: `[${unencodedValue}]`;
|
|
32
|
+
return `${accumulator}${encodedValue}`;
|
|
33
|
+
}, '')
|
|
34
|
+
);
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
const processErrorParameters = (error) => {
|
|
@@ -54,7 +63,12 @@ const formatErrors = (errors) => {
|
|
|
54
63
|
maxLength = error.dataKey.length;
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
|
-
return errors.map(
|
|
66
|
+
return errors.map(
|
|
67
|
+
(error) =>
|
|
68
|
+
` ${error.dataKey.padEnd(maxLength + margin)}${red(
|
|
69
|
+
error.formattedMessage
|
|
70
|
+
)}`
|
|
71
|
+
);
|
|
58
72
|
};
|
|
59
73
|
|
|
60
74
|
module.exports.formatErrors = formatErrors;
|
package/lib/test-utils.js
CHANGED
|
@@ -14,7 +14,9 @@ const testResultStates = Object.freeze({
|
|
|
14
14
|
|
|
15
15
|
const getTestSettings = (testSettingProperty, urlSettings) => {
|
|
16
16
|
if (urlSettings[testSettingProperty] === undefined) {
|
|
17
|
-
throw new Error(
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Property '${testSettingProperty}' could not be found in test settings`
|
|
19
|
+
);
|
|
18
20
|
}
|
|
19
21
|
return urlSettings[testSettingProperty];
|
|
20
22
|
};
|
|
@@ -31,19 +33,29 @@ const getTestSettings = (testSettingProperty, urlSettings) => {
|
|
|
31
33
|
* @param {string} testSettingProperty The name of the config property
|
|
32
34
|
* with settings for the current test.
|
|
33
35
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const pageanTest = async (
|
|
37
|
+
name,
|
|
38
|
+
testFunction,
|
|
39
|
+
testContext,
|
|
40
|
+
testSettingProperty
|
|
41
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Allow < 10
|
|
42
|
+
) => {
|
|
43
|
+
const testSettings = getTestSettings(
|
|
44
|
+
testSettingProperty,
|
|
45
|
+
testContext.urlSettings
|
|
46
|
+
);
|
|
37
47
|
testContext.testSettings = testSettings;
|
|
38
48
|
if (testSettings.enabled) {
|
|
39
49
|
let results;
|
|
40
50
|
try {
|
|
41
51
|
results = await testFunction(testContext);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
52
|
+
} catch (error) {
|
|
44
53
|
results = { result: testResultStates.failed, data: { error } };
|
|
45
54
|
}
|
|
46
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
results.result === testResultStates.failed &&
|
|
57
|
+
testSettings.failWarn
|
|
58
|
+
) {
|
|
47
59
|
results.result = testResultStates.warning;
|
|
48
60
|
}
|
|
49
61
|
testContext.logger.logTestResults({
|
package/lib/tests.js
CHANGED
|
@@ -20,15 +20,25 @@ const msPerSec = 1000;
|
|
|
20
20
|
* @param {object} context Test execution context.
|
|
21
21
|
*/
|
|
22
22
|
const horizontalScrollbarTest = async (context) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
await pageanTest(
|
|
24
|
+
'should not have a horizontal scrollbar',
|
|
25
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
26
|
+
async (context) => {
|
|
27
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
28
|
+
const scrollbar = await context.page.evaluate(() => {
|
|
29
|
+
document.scrollingElement.scrollLeft = 1;
|
|
30
|
+
return document.scrollingElement.scrollLeft === 1;
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
result:
|
|
34
|
+
scrollbar === false
|
|
35
|
+
? testResultStates.passed
|
|
36
|
+
: testResultStates.failed
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
context,
|
|
40
|
+
'horizontalScrollbarTest'
|
|
41
|
+
);
|
|
32
42
|
};
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -38,14 +48,24 @@ const horizontalScrollbarTest = async (context) => {
|
|
|
38
48
|
* @param {object} context Test execution context.
|
|
39
49
|
*/
|
|
40
50
|
const consoleOutputTest = (context) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
testResult
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
pageanTest(
|
|
52
|
+
'should not have console output',
|
|
53
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
54
|
+
(context) => {
|
|
55
|
+
const testResult = {
|
|
56
|
+
result:
|
|
57
|
+
context.consoleLog.length === 0
|
|
58
|
+
? testResultStates.passed
|
|
59
|
+
: testResultStates.failed
|
|
60
|
+
};
|
|
61
|
+
if (testResult.result === testResultStates.failed) {
|
|
62
|
+
testResult.data = context.consoleLog;
|
|
63
|
+
}
|
|
64
|
+
return testResult;
|
|
65
|
+
},
|
|
66
|
+
context,
|
|
67
|
+
'consoleOutputTest'
|
|
68
|
+
);
|
|
49
69
|
};
|
|
50
70
|
|
|
51
71
|
/**
|
|
@@ -55,17 +75,29 @@ const consoleOutputTest = (context) => {
|
|
|
55
75
|
* @param {object} context Test execution context.
|
|
56
76
|
*/
|
|
57
77
|
const consoleErrorTest = (context) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
pageanTest(
|
|
79
|
+
'should not have console errors',
|
|
80
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
81
|
+
(context) => {
|
|
82
|
+
const browserErrorLog = context.consoleLog.filter(
|
|
83
|
+
// Object property names defined by puppeteer API
|
|
84
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
85
|
+
(log) => log._type === 'error'
|
|
86
|
+
);
|
|
87
|
+
const testResult = {
|
|
88
|
+
result:
|
|
89
|
+
browserErrorLog.length === 0
|
|
90
|
+
? testResultStates.passed
|
|
91
|
+
: testResultStates.failed
|
|
92
|
+
};
|
|
93
|
+
if (testResult.result === testResultStates.failed) {
|
|
94
|
+
testResult.data = browserErrorLog;
|
|
95
|
+
}
|
|
96
|
+
return testResult;
|
|
97
|
+
},
|
|
98
|
+
context,
|
|
99
|
+
'consoleErrorTest'
|
|
100
|
+
);
|
|
69
101
|
};
|
|
70
102
|
|
|
71
103
|
/**
|
|
@@ -75,16 +107,29 @@ const consoleErrorTest = (context) => {
|
|
|
75
107
|
* @param {object} context Test execution context.
|
|
76
108
|
*/
|
|
77
109
|
const renderedHtmlTest = async (context) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
await pageanTest(
|
|
111
|
+
'should have valid rendered HTML',
|
|
112
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
113
|
+
async (context) => {
|
|
114
|
+
const html = await context.page.content();
|
|
115
|
+
const lintResults = HTMLHint.verify(
|
|
116
|
+
html,
|
|
117
|
+
context.urlSettings.htmlHintConfig
|
|
118
|
+
);
|
|
119
|
+
const testResult = {
|
|
120
|
+
result:
|
|
121
|
+
lintResults.length === 0
|
|
122
|
+
? testResultStates.passed
|
|
123
|
+
: testResultStates.failed
|
|
124
|
+
};
|
|
125
|
+
if (testResult.result === testResultStates.failed) {
|
|
126
|
+
testResult.data = lintResults;
|
|
127
|
+
}
|
|
128
|
+
return testResult;
|
|
129
|
+
},
|
|
130
|
+
context,
|
|
131
|
+
'renderedHtmlTest'
|
|
132
|
+
);
|
|
88
133
|
};
|
|
89
134
|
|
|
90
135
|
/**
|
|
@@ -93,23 +138,40 @@ const renderedHtmlTest = async (context) => {
|
|
|
93
138
|
* @static
|
|
94
139
|
* @param {object} context Test execution context.
|
|
95
140
|
*/
|
|
141
|
+
// eslint-disable-next-line max-lines-per-function
|
|
96
142
|
const pageLoadTimeTest = async (context) => {
|
|
97
143
|
const testSettingName = 'pageLoadTimeTest';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
await pageanTest(
|
|
145
|
+
'should load page within timeout',
|
|
146
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
147
|
+
async (context) => {
|
|
148
|
+
const { pageLoadTimeThreshold } = context.testSettings;
|
|
149
|
+
const name = `should load page within ${pageLoadTimeThreshold} sec`;
|
|
150
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
151
|
+
const performanceTiming = JSON.parse(
|
|
152
|
+
await context.page.evaluate(() =>
|
|
153
|
+
JSON.stringify(window.performance)
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
const loadTimeSec =
|
|
157
|
+
(performanceTiming.timing.loadEventEnd -
|
|
158
|
+
performanceTiming.timing.navigationStart) /
|
|
159
|
+
msPerSec;
|
|
160
|
+
const testResult = {
|
|
161
|
+
name,
|
|
162
|
+
result:
|
|
163
|
+
loadTimeSec < pageLoadTimeThreshold
|
|
164
|
+
? testResultStates.passed
|
|
165
|
+
: testResultStates.failed
|
|
166
|
+
};
|
|
167
|
+
if (testResult.result === testResultStates.failed) {
|
|
168
|
+
testResult.data = { pageLoadTime: loadTimeSec };
|
|
169
|
+
}
|
|
170
|
+
return testResult;
|
|
171
|
+
},
|
|
172
|
+
context,
|
|
173
|
+
testSettingName
|
|
174
|
+
);
|
|
113
175
|
};
|
|
114
176
|
|
|
115
177
|
/**
|
|
@@ -119,23 +181,42 @@ const pageLoadTimeTest = async (context) => {
|
|
|
119
181
|
* @static
|
|
120
182
|
* @param {object} context Test execution context.
|
|
121
183
|
*/
|
|
184
|
+
// eslint-disable-next-line max-lines-per-function
|
|
122
185
|
const externalScriptTest = async (context) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
await pageanTest(
|
|
187
|
+
'should not have external scripts',
|
|
188
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
189
|
+
async (context) => {
|
|
190
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
191
|
+
const scripts = await context.page.evaluate(() => {
|
|
192
|
+
return [...document.querySelectorAll('script[src]')].map(
|
|
193
|
+
(s) => s.src
|
|
194
|
+
);
|
|
195
|
+
});
|
|
129
196
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
197
|
+
const pageUrl = context.page.url();
|
|
198
|
+
const externalScripts = scripts.filter((script) =>
|
|
199
|
+
fileUtils.shouldSaveFile(script, pageUrl)
|
|
200
|
+
);
|
|
201
|
+
const scriptResults = await Promise.all(
|
|
202
|
+
externalScripts.map((script) =>
|
|
203
|
+
fileUtils.saveExternalScript(script)
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
const testResult = {
|
|
207
|
+
result:
|
|
208
|
+
scriptResults.length > 0
|
|
209
|
+
? testResultStates.failed
|
|
210
|
+
: testResultStates.passed
|
|
211
|
+
};
|
|
212
|
+
if (testResult.result === testResultStates.failed) {
|
|
213
|
+
testResult.data = scriptResults;
|
|
214
|
+
}
|
|
215
|
+
return testResult;
|
|
216
|
+
},
|
|
217
|
+
context,
|
|
218
|
+
'externalScriptTest'
|
|
219
|
+
);
|
|
139
220
|
};
|
|
140
221
|
|
|
141
222
|
/**
|
|
@@ -144,33 +225,54 @@ const externalScriptTest = async (context) => {
|
|
|
144
225
|
* @static
|
|
145
226
|
* @param {object} context Test execution context.
|
|
146
227
|
*/
|
|
228
|
+
// eslint-disable-next-line max-lines-per-function
|
|
147
229
|
const brokenLinkTest = async (context) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
230
|
+
await pageanTest(
|
|
231
|
+
'should not have broken links',
|
|
232
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
233
|
+
async (context) => {
|
|
234
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
235
|
+
const links = await context.page.evaluate(() => {
|
|
236
|
+
return [...document.querySelectorAll('a[href]')].map(
|
|
237
|
+
(a) => a.href
|
|
238
|
+
);
|
|
239
|
+
});
|
|
154
240
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
241
|
+
// All links are returned from puppeteer as absolute links, so this filters out
|
|
242
|
+
// javascript and other values and leaves only pages to request.
|
|
243
|
+
const httpLinks = links.filter((link) =>
|
|
244
|
+
link.match(/(http(s?)|file):\/\//)
|
|
245
|
+
);
|
|
246
|
+
// Reduce to unique page links so only checked once
|
|
247
|
+
const uniqueHttpLinks = [...new Set(httpLinks)];
|
|
160
248
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
249
|
+
// Check each link includes check against ignored list, and if not checks
|
|
250
|
+
// both links within the page as well as to other pages
|
|
251
|
+
const linkResponses = await Promise.all(
|
|
252
|
+
uniqueHttpLinks.map(async (link) => ({
|
|
253
|
+
href: link,
|
|
254
|
+
status: await context.linkChecker.checkLink(context, link)
|
|
255
|
+
}))
|
|
256
|
+
);
|
|
165
257
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
testResult
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
258
|
+
// Returned results includes status for all links, so filter down to only failed
|
|
259
|
+
const failedLinkResponses = linkResponses.filter((result) =>
|
|
260
|
+
isFailedResponse(result)
|
|
261
|
+
);
|
|
262
|
+
const testResult = {
|
|
263
|
+
result:
|
|
264
|
+
failedLinkResponses.length > 0
|
|
265
|
+
? testResultStates.failed
|
|
266
|
+
: testResultStates.passed
|
|
267
|
+
};
|
|
268
|
+
if (testResult.result === testResultStates.failed) {
|
|
269
|
+
testResult.data = failedLinkResponses;
|
|
270
|
+
}
|
|
271
|
+
return testResult;
|
|
272
|
+
},
|
|
273
|
+
context,
|
|
274
|
+
'brokenLinkTest'
|
|
275
|
+
);
|
|
174
276
|
};
|
|
175
277
|
|
|
176
278
|
module.exports.consoleErrorTest = consoleErrorTest;
|