pagean 6.0.7 → 7.0.0
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 +104 -110
- package/bin/pagean.js +14 -7
- package/bin/pageanrc-lint.js +14 -13
- package/docs/upgrade-guide.md +26 -26
- package/index.js +18 -7
- package/lib/config.js +35 -13
- package/lib/default-config.json +2 -8
- package/lib/{externalFileUtils.js → external-file-utils.js} +7 -4
- package/lib/{linkUtils.js → link-utils.js} +36 -35
- package/lib/logger.js +7 -3
- package/lib/reporter.js +4 -2
- package/lib/{schemaErrors.js → schema-errors.js} +26 -10
- package/lib/{testUtils.js → test-utils.js} +20 -8
- package/lib/tests.js +194 -94
- package/package.json +25 -21
- package/schemas/pageanrc.schema.json +3 -11
package/lib/tests.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const { HTMLHint } = require('htmlhint');
|
|
9
9
|
|
|
10
|
-
const { testResultStates, pageanTest } = require('./
|
|
11
|
-
const fileUtils = require('./
|
|
12
|
-
const { isFailedResponse } = require('./
|
|
10
|
+
const { testResultStates, pageanTest } = require('./test-utils');
|
|
11
|
+
const fileUtils = require('./external-file-utils');
|
|
12
|
+
const { isFailedResponse } = require('./link-utils');
|
|
13
13
|
|
|
14
14
|
const msPerSec = 1000;
|
|
15
15
|
|
|
@@ -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,27 @@ 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
|
-
testResult
|
|
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
|
+
(log) => log.type === 'error'
|
|
84
|
+
);
|
|
85
|
+
const testResult = {
|
|
86
|
+
result:
|
|
87
|
+
browserErrorLog.length === 0
|
|
88
|
+
? testResultStates.passed
|
|
89
|
+
: testResultStates.failed
|
|
90
|
+
};
|
|
91
|
+
if (testResult.result === testResultStates.failed) {
|
|
92
|
+
testResult.data = browserErrorLog;
|
|
93
|
+
}
|
|
94
|
+
return testResult;
|
|
95
|
+
},
|
|
96
|
+
context,
|
|
97
|
+
'consoleErrorTest'
|
|
98
|
+
);
|
|
69
99
|
};
|
|
70
100
|
|
|
71
101
|
/**
|
|
@@ -75,16 +105,29 @@ const consoleErrorTest = (context) => {
|
|
|
75
105
|
* @param {object} context Test execution context.
|
|
76
106
|
*/
|
|
77
107
|
const renderedHtmlTest = async (context) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
await pageanTest(
|
|
109
|
+
'should have valid rendered HTML',
|
|
110
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
111
|
+
async (context) => {
|
|
112
|
+
const html = await context.page.content();
|
|
113
|
+
const lintResults = HTMLHint.verify(
|
|
114
|
+
html,
|
|
115
|
+
context.urlSettings.htmlHintConfig
|
|
116
|
+
);
|
|
117
|
+
const testResult = {
|
|
118
|
+
result:
|
|
119
|
+
lintResults.length === 0
|
|
120
|
+
? testResultStates.passed
|
|
121
|
+
: testResultStates.failed
|
|
122
|
+
};
|
|
123
|
+
if (testResult.result === testResultStates.failed) {
|
|
124
|
+
testResult.data = lintResults;
|
|
125
|
+
}
|
|
126
|
+
return testResult;
|
|
127
|
+
},
|
|
128
|
+
context,
|
|
129
|
+
'renderedHtmlTest'
|
|
130
|
+
);
|
|
88
131
|
};
|
|
89
132
|
|
|
90
133
|
/**
|
|
@@ -93,23 +136,40 @@ const renderedHtmlTest = async (context) => {
|
|
|
93
136
|
* @static
|
|
94
137
|
* @param {object} context Test execution context.
|
|
95
138
|
*/
|
|
139
|
+
// eslint-disable-next-line max-lines-per-function
|
|
96
140
|
const pageLoadTimeTest = async (context) => {
|
|
97
141
|
const testSettingName = 'pageLoadTimeTest';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
await pageanTest(
|
|
143
|
+
'should load page within timeout',
|
|
144
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
145
|
+
async (context) => {
|
|
146
|
+
const { pageLoadTimeThreshold } = context.testSettings;
|
|
147
|
+
const name = `should load page within ${pageLoadTimeThreshold} sec`;
|
|
148
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
149
|
+
const performanceTiming = JSON.parse(
|
|
150
|
+
await context.page.evaluate(() =>
|
|
151
|
+
JSON.stringify(window.performance)
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
const loadTimeSec =
|
|
155
|
+
(performanceTiming.timing.loadEventEnd -
|
|
156
|
+
performanceTiming.timing.navigationStart) /
|
|
157
|
+
msPerSec;
|
|
158
|
+
const testResult = {
|
|
159
|
+
name,
|
|
160
|
+
result:
|
|
161
|
+
loadTimeSec < pageLoadTimeThreshold
|
|
162
|
+
? testResultStates.passed
|
|
163
|
+
: testResultStates.failed
|
|
164
|
+
};
|
|
165
|
+
if (testResult.result === testResultStates.failed) {
|
|
166
|
+
testResult.data = { pageLoadTime: loadTimeSec };
|
|
167
|
+
}
|
|
168
|
+
return testResult;
|
|
169
|
+
},
|
|
170
|
+
context,
|
|
171
|
+
testSettingName
|
|
172
|
+
);
|
|
113
173
|
};
|
|
114
174
|
|
|
115
175
|
/**
|
|
@@ -119,23 +179,42 @@ const pageLoadTimeTest = async (context) => {
|
|
|
119
179
|
* @static
|
|
120
180
|
* @param {object} context Test execution context.
|
|
121
181
|
*/
|
|
182
|
+
// eslint-disable-next-line max-lines-per-function
|
|
122
183
|
const externalScriptTest = async (context) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
await pageanTest(
|
|
185
|
+
'should not have external scripts',
|
|
186
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
187
|
+
async (context) => {
|
|
188
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
189
|
+
const scripts = await context.page.evaluate(() => {
|
|
190
|
+
return [...document.querySelectorAll('script[src]')].map(
|
|
191
|
+
(s) => s.src
|
|
192
|
+
);
|
|
193
|
+
});
|
|
129
194
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
const pageUrl = context.page.url();
|
|
196
|
+
const externalScripts = scripts.filter((script) =>
|
|
197
|
+
fileUtils.shouldSaveFile(script, pageUrl)
|
|
198
|
+
);
|
|
199
|
+
const scriptResults = await Promise.all(
|
|
200
|
+
externalScripts.map((script) =>
|
|
201
|
+
fileUtils.saveExternalScript(script)
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
const testResult = {
|
|
205
|
+
result:
|
|
206
|
+
scriptResults.length > 0
|
|
207
|
+
? testResultStates.failed
|
|
208
|
+
: testResultStates.passed
|
|
209
|
+
};
|
|
210
|
+
if (testResult.result === testResultStates.failed) {
|
|
211
|
+
testResult.data = scriptResults;
|
|
212
|
+
}
|
|
213
|
+
return testResult;
|
|
214
|
+
},
|
|
215
|
+
context,
|
|
216
|
+
'externalScriptTest'
|
|
217
|
+
);
|
|
139
218
|
};
|
|
140
219
|
|
|
141
220
|
/**
|
|
@@ -144,33 +223,54 @@ const externalScriptTest = async (context) => {
|
|
|
144
223
|
* @static
|
|
145
224
|
* @param {object} context Test execution context.
|
|
146
225
|
*/
|
|
226
|
+
// eslint-disable-next-line max-lines-per-function
|
|
147
227
|
const brokenLinkTest = async (context) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
228
|
+
await pageanTest(
|
|
229
|
+
'should not have broken links',
|
|
230
|
+
// eslint-disable-next-line no-shadow -- less intuitive
|
|
231
|
+
async (context) => {
|
|
232
|
+
// istanbul ignore next: injects script causing puppeteer error, see #48
|
|
233
|
+
const links = await context.page.evaluate(() => {
|
|
234
|
+
return [...document.querySelectorAll('a[href]')].map(
|
|
235
|
+
(a) => a.href
|
|
236
|
+
);
|
|
237
|
+
});
|
|
154
238
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
239
|
+
// All links are returned from puppeteer as absolute links, so this filters out
|
|
240
|
+
// javascript and other values and leaves only pages to request.
|
|
241
|
+
const httpLinks = links.filter((link) =>
|
|
242
|
+
link.match(/(http(s?)|file):\/\//)
|
|
243
|
+
);
|
|
244
|
+
// Reduce to unique page links so only checked once
|
|
245
|
+
const uniqueHttpLinks = [...new Set(httpLinks)];
|
|
160
246
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
247
|
+
// Check each link includes check against ignored list, and if not checks
|
|
248
|
+
// both links within the page as well as to other pages
|
|
249
|
+
const linkResponses = await Promise.all(
|
|
250
|
+
uniqueHttpLinks.map(async (link) => ({
|
|
251
|
+
href: link,
|
|
252
|
+
status: await context.linkChecker.checkLink(context, link)
|
|
253
|
+
}))
|
|
254
|
+
);
|
|
165
255
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
testResult
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
256
|
+
// Returned results includes status for all links, so filter down to only failed
|
|
257
|
+
const failedLinkResponses = linkResponses.filter((result) =>
|
|
258
|
+
isFailedResponse(result)
|
|
259
|
+
);
|
|
260
|
+
const testResult = {
|
|
261
|
+
result:
|
|
262
|
+
failedLinkResponses.length > 0
|
|
263
|
+
? testResultStates.failed
|
|
264
|
+
: testResultStates.passed
|
|
265
|
+
};
|
|
266
|
+
if (testResult.result === testResultStates.failed) {
|
|
267
|
+
testResult.data = failedLinkResponses;
|
|
268
|
+
}
|
|
269
|
+
return testResult;
|
|
270
|
+
},
|
|
271
|
+
context,
|
|
272
|
+
'brokenLinkTest'
|
|
273
|
+
);
|
|
174
274
|
};
|
|
175
275
|
|
|
176
276
|
module.exports.consoleErrorTest = consoleErrorTest;
|
package/package.json
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pagean",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Pagean is a web page analysis tool designed to automate tests requiring web pages to be loaded in a browser window (e.g. horizontal scrollbar, console errors)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pagean": "./bin/pagean.js",
|
|
7
7
|
"pageanrc-lint": "./bin/pageanrc-lint.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"test": "jest --ci --config jest.config.json",
|
|
10
|
+
"hooks-pre-commit": "npm run lint && npm run prettier-check",
|
|
11
|
+
"hooks-pre-push": "npm audit --audit-level=high && npm test",
|
|
12
|
+
"lint": "npm run lint-css && npm run lint-html && npm run lint-js && npm run lint-md",
|
|
14
13
|
"lint-css": "stylelint ./lib/report-template.handlebars",
|
|
15
14
|
"lint-html": "htmlhint ./lib/report-template.handlebars",
|
|
16
15
|
"lint-js": "eslint \"**/*.js\"",
|
|
17
16
|
"lint-md": "markdownlint **/*.md --ignore node_modules",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
17
|
+
"prettier-check": "prettier --check .",
|
|
18
|
+
"prettier-fix": "prettier --write .",
|
|
19
|
+
"start": "node ./bin/pagean.js",
|
|
20
|
+
"start-lint": "node ./bin/pageanrc-lint.js",
|
|
21
|
+
"start-lint-all": "npm run start-lint && npm run start-lint -c static-server.pageanrc.json && npm run start-lint -c ./lib/default-config.json",
|
|
22
|
+
"test": "jest --ci --config jest.config.json"
|
|
20
23
|
},
|
|
21
24
|
"repository": {
|
|
22
25
|
"type": "git",
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
"author": "Aaron Goldenthal <npm@aarongoldenthal.com>",
|
|
35
38
|
"license": "MIT",
|
|
36
39
|
"engines": {
|
|
37
|
-
"node": "^
|
|
40
|
+
"node": "^14.15.0 || ^16.13.0 || >=18.0.0"
|
|
38
41
|
},
|
|
39
42
|
"files": [
|
|
40
43
|
"index.js",
|
|
@@ -46,29 +49,30 @@
|
|
|
46
49
|
"bugs": {
|
|
47
50
|
"url": "https://gitlab.com/gitlab-ci-utils/pagean/issues"
|
|
48
51
|
},
|
|
49
|
-
"homepage": "https://gitlab.com/gitlab-ci-utils/pagean
|
|
52
|
+
"homepage": "https://gitlab.com/gitlab-ci-utils/pagean",
|
|
50
53
|
"devDependencies": {
|
|
51
|
-
"@aarongoldenthal/eslint-config-standard": "^
|
|
52
|
-
"@aarongoldenthal/stylelint-config-standard": "^
|
|
54
|
+
"@aarongoldenthal/eslint-config-standard": "^15.0.0",
|
|
55
|
+
"@aarongoldenthal/stylelint-config-standard": "^9.0.0",
|
|
53
56
|
"bin-tester": "^2.0.1",
|
|
54
|
-
"eslint": "^8.
|
|
55
|
-
"jest": "^
|
|
56
|
-
"jest-junit": "^13.
|
|
57
|
+
"eslint": "^8.17.0",
|
|
58
|
+
"jest": "^28.1.1",
|
|
59
|
+
"jest-junit": "^13.2.0",
|
|
57
60
|
"markdownlint-cli": "^0.31.1",
|
|
61
|
+
"prettier": "^2.7.0",
|
|
58
62
|
"strip-ansi": "^6.0.1",
|
|
59
|
-
"stylelint": "^14.
|
|
63
|
+
"stylelint": "^14.9.1"
|
|
60
64
|
},
|
|
61
65
|
"dependencies": {
|
|
62
|
-
"ajv": "^8.
|
|
66
|
+
"ajv": "^8.11.0",
|
|
63
67
|
"ajv-errors": "^3.0.0",
|
|
64
|
-
"axios": "^0.
|
|
65
|
-
"ci-logger": "^
|
|
66
|
-
"commander": "^9.
|
|
68
|
+
"axios": "^0.27.2",
|
|
69
|
+
"ci-logger": "^5.0.0",
|
|
70
|
+
"commander": "^9.3.0",
|
|
67
71
|
"handlebars": "^4.7.7",
|
|
68
|
-
"htmlhint": "^1.1.
|
|
72
|
+
"htmlhint": "^1.1.4",
|
|
69
73
|
"kleur": "^4.1.4",
|
|
70
74
|
"normalize-url": "^6.1.0",
|
|
71
75
|
"protocolify": "^3.0.0",
|
|
72
|
-
"puppeteer": "^
|
|
76
|
+
"puppeteer": "^14.4.0"
|
|
73
77
|
}
|
|
74
78
|
}
|
|
@@ -22,11 +22,7 @@
|
|
|
22
22
|
"description": "Reporters to use when returning the results of the Pagean tests.",
|
|
23
23
|
"type": "array",
|
|
24
24
|
"items": {
|
|
25
|
-
"enum": [
|
|
26
|
-
"cli",
|
|
27
|
-
"html",
|
|
28
|
-
"json"
|
|
29
|
-
]
|
|
25
|
+
"enum": ["cli", "html", "json"]
|
|
30
26
|
},
|
|
31
27
|
"minItems": 1
|
|
32
28
|
},
|
|
@@ -60,9 +56,7 @@
|
|
|
60
56
|
"not": true,
|
|
61
57
|
"errorMessage": "must NOT contain additional properties: ${0#}"
|
|
62
58
|
},
|
|
63
|
-
"required": [
|
|
64
|
-
"url"
|
|
65
|
-
],
|
|
59
|
+
"required": ["url"],
|
|
66
60
|
"errorMessage": {
|
|
67
61
|
"required": "must have required property 'url'"
|
|
68
62
|
}
|
|
@@ -77,9 +71,7 @@
|
|
|
77
71
|
"not": true,
|
|
78
72
|
"errorMessage": "must NOT contain additional properties: ${0#}"
|
|
79
73
|
},
|
|
80
|
-
"required": [
|
|
81
|
-
"urls"
|
|
82
|
-
],
|
|
74
|
+
"required": ["urls"],
|
|
83
75
|
"definitions": {
|
|
84
76
|
"detailedSetting": {
|
|
85
77
|
"description": "The complete set of test-specific settings for most tests.",
|