pagean 8.0.4 → 10.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/lib/sitemap.js ADDED
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const axios = require('axios');
5
+ const { parseStringPromise } = require('xml2js');
6
+
7
+ /**
8
+ * Gets a sitemap, via file path or URL, and returns the string contents.
9
+ *
10
+ * @param {string} url The URL or file path to the sitemap.
11
+ * @returns {string} The string contents of the sitemap.
12
+ * @async
13
+ * @throws {Error} Throws if the sitemap cannot be retrieved.
14
+ * @static
15
+ * @private
16
+ */
17
+ const getSitemap = async (url) => {
18
+ try {
19
+ if (url.startsWith('http')) {
20
+ const response = await axios.get(url);
21
+ return response.data;
22
+ }
23
+ // Allow users to specify a local sitemap filename.
24
+ // nosemgrep: eslint.detect-non-literal-fs-filename
25
+ return fs.readFileSync(url, 'utf8');
26
+ } catch {
27
+ throw new Error(`Error retrieving sitemap "${url}"`);
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Parses a sitemap string and returns an array of URLs.
33
+ *
34
+ * @param {string} sitemapXml The string contents of the sitemap.
35
+ * @returns {string[]} The URLs from the sitemap.
36
+ * @async
37
+ * @static
38
+ * @private
39
+ */
40
+ const parseSitemap = async (sitemapXml) => {
41
+ const sitemapData = await parseStringPromise(sitemapXml);
42
+ if (!sitemapData.urlset.url) {
43
+ return [];
44
+ }
45
+ return sitemapData.urlset.url.map((url) => url.loc[0]);
46
+ };
47
+
48
+ /**
49
+ * Removes excluded URLs from the list of URLs.
50
+ *
51
+ * @param {string[]} urls The URLs from the sitemap.
52
+ * @param {string[]} exclude The URLs to exclude (regular expressions).
53
+ * @returns {string[]} The non-excluded URLs from the sitemap.
54
+ * @static
55
+ * @private
56
+ */
57
+ const removeExcludedUrls = (urls, exclude = []) => {
58
+ // Allow URLs to be excluded by regular expression.
59
+ // nosemgrep: eslint.detect-non-literal-regexp
60
+ const excludeRegex = exclude.map((url) => new RegExp(url));
61
+ return urls.filter((url) => {
62
+ for (const excludeUrlRegex of excludeRegex) {
63
+ if (excludeUrlRegex.test(url)) {
64
+ return false;
65
+ }
66
+ }
67
+ return true;
68
+ });
69
+ };
70
+
71
+ /**
72
+ * Replaces the "find" string with the "replace" string in each of the URLs.
73
+ *
74
+ * @param {string[]} urls The URLs to process.
75
+ * @param {string} [find] The string to find in the URLs.
76
+ * @param {string} [replace] The string to replace in the URLs.
77
+ * @returns {string[]} The processed URLs.
78
+ * @async
79
+ * @static
80
+ * @private
81
+ */
82
+ const replaceUrlStrings = (urls, find, replace) => {
83
+ if (!find || !replace) {
84
+ return urls;
85
+ }
86
+ return urls.map((url) => url.replace(find, replace));
87
+ };
88
+
89
+ /**
90
+ * Retrieves a list of URLs from a sitemap, via a local file path or URL.
91
+ * Options are available to exclude URLs from the results, and to find and
92
+ * replace strings in the URLs (primarily intended for isolated testing,
93
+ * for example change "https://somewhere.test/" to "http://localhost:3000/").
94
+ *
95
+ * @param {object} options The sitemap processing options.
96
+ * @param {string} options.url The URL or file path to the sitemap.
97
+ * @param {string} [options.find] The string to find in the URLs.
98
+ * @param {string} [options.replace] The string to replace in the URLs.
99
+ * @param {string[]} [options.exclude] The URLs to exclude (regular expressions).
100
+ * @returns {string[]} The processed URLs from the sitemap.
101
+ * @async
102
+ * @throws {Error} Throws if the sitemap cannot be processed.
103
+ * @static
104
+ * @public
105
+ */
106
+ const getUrlsFromSitemap = async ({ url, find, replace, exclude } = {}) => {
107
+ const sitemapXml = await getSitemap(url);
108
+ try {
109
+ const rawUrls = await parseSitemap(sitemapXml);
110
+ return replaceUrlStrings(
111
+ removeExcludedUrls(rawUrls, exclude),
112
+ find,
113
+ replace
114
+ );
115
+ } catch {
116
+ throw new Error(`Error processing sitemap "${url}"`);
117
+ }
118
+ };
119
+
120
+ module.exports.getUrlsFromSitemap = getUrlsFromSitemap;
package/lib/test-utils.js CHANGED
@@ -7,9 +7,9 @@
7
7
  */
8
8
 
9
9
  const testResultStates = Object.freeze({
10
+ failed: 'failed',
10
11
  passed: 'passed',
11
- warning: 'warning',
12
- failed: 'failed'
12
+ warning: 'warning'
13
13
  });
14
14
 
15
15
  const getTestSettings = (testSettingProperty, urlSettings) => {
@@ -26,7 +26,7 @@ const getTestSettings = (testSettingProperty, urlSettings) => {
26
26
  * test-specific settings from configuration, passing the test context,
27
27
  * and logging results.
28
28
  *
29
- * @param {string} name The name of the test.
29
+ * @param {string} name The name of the test being run.
30
30
  * @param {Function} testFunction The test function to be executed.
31
31
  * @param {object} testContext The test execution context.
32
32
  * @param {string} testSettingProperty The name of the config property
@@ -50,7 +50,7 @@ const pageanTest = async (
50
50
  try {
51
51
  results = await testFunction(testContext);
52
52
  } catch (error) {
53
- results = { result: testResultStates.failed, data: { error } };
53
+ results = { data: { error }, result: testResultStates.failed };
54
54
  }
55
55
  if (
56
56
  results.result === testResultStates.failed &&
package/lib/tests.js CHANGED
@@ -27,7 +27,10 @@ const horizontalScrollbarTest = async (context) => {
27
27
  // istanbul ignore next: injects script causing puppeteer error, see #48
28
28
  const scrollbar = await context.page.evaluate(() => {
29
29
  document.scrollingElement.scrollLeft = 1;
30
- return document.scrollingElement.scrollLeft === 1;
30
+ // The scrollLeft value may not end up exactly 1. But, any
31
+ // value greater than 0 indicates the presence of a horizontal
32
+ // scrollbar.
33
+ return document.scrollingElement.scrollLeft > 0;
31
34
  });
32
35
  return {
33
36
  result:
@@ -136,7 +139,7 @@ const renderedHtmlTest = async (context) => {
136
139
  * @param {object} context Test execution context.
137
140
  * @static
138
141
  */
139
- // eslint-disable-next-line max-lines-per-function
142
+ // eslint-disable-next-line max-lines-per-function -- exceeded due to formatting
140
143
  const pageLoadTimeTest = async (context) => {
141
144
  const testSettingName = 'pageLoadTimeTest';
142
145
  await pageanTest(
@@ -179,18 +182,16 @@ const pageLoadTimeTest = async (context) => {
179
182
  * @param {object} context Test execution context.
180
183
  * @static
181
184
  */
182
- // eslint-disable-next-line max-lines-per-function
185
+ // eslint-disable-next-line max-lines-per-function -- exceeded due to formatting
183
186
  const externalScriptTest = async (context) => {
184
187
  await pageanTest(
185
188
  'should not have external scripts',
186
189
  // eslint-disable-next-line no-shadow -- less intuitive
187
190
  async (context) => {
188
191
  // 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
- });
192
+ const scripts = await context.page.evaluate(() =>
193
+ [...document.querySelectorAll('script[src]')].map((s) => s.src)
194
+ );
194
195
 
195
196
  const pageUrl = context.page.url();
196
197
  const externalScripts = scripts.filter((script) =>
@@ -223,23 +224,21 @@ const externalScriptTest = async (context) => {
223
224
  * @param {object} context Test execution context.
224
225
  * @static
225
226
  */
226
- // eslint-disable-next-line max-lines-per-function
227
+ // eslint-disable-next-line max-lines-per-function -- exceeded due to formatting
227
228
  const brokenLinkTest = async (context) => {
228
229
  await pageanTest(
229
230
  'should not have broken links',
230
231
  // eslint-disable-next-line no-shadow -- less intuitive
231
232
  async (context) => {
232
233
  // 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
- });
234
+ const links = await context.page.evaluate(() =>
235
+ [...document.querySelectorAll('a[href]')].map((a) => a.href)
236
+ );
238
237
 
239
238
  // All links are returned from puppeteer as absolute links, so this filters out
240
239
  // javascript and other values and leaves only pages to request.
241
240
  const httpLinks = links.filter((link) =>
242
- link.match(/(http(s?)|file):\/\//)
241
+ link.match(/(?<protocol>https?|file):\/\//)
243
242
  );
244
243
  // Reduce to unique page links so only checked once
245
244
  const uniqueHttpLinks = [...new Set(httpLinks)];
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "pagean",
3
- "version": "8.0.4",
3
+ "version": "10.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
- "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",
13
- "lint-css": "stylelint ./lib/report-template.handlebars",
14
- "lint-html": "htmlhint ./lib/report-template.handlebars",
15
- "lint-js": "eslint \"**/*.js\"",
16
- "lint-md": "markdownlint **/*.md --ignore node_modules",
17
- "prettier-check": "prettier --check .",
18
- "prettier-fix": "prettier --write .",
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",
13
+ "lint:css": "stylelint ./lib/report-template.handlebars",
14
+ "lint:html": "htmlhint ./lib/report-template.handlebars",
15
+ "lint:js": "eslint .",
16
+ "lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#Archive\"",
17
+ "prettier:check": "prettier --check .",
18
+ "prettier:fix": "prettier --write .",
19
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 static-server.pageanrc.json && npm run start-lint ./lib/default-config.json",
20
+ "start:lint": "node ./bin/pageanrc-lint.js",
21
+ "start:lint-all": "npm run start:lint && npm run start:lint static-server.pageanrc.json && npm run start:lint ./lib/default-config.json",
22
22
  "test": "jest --ci --config jest.config.json"
23
23
  },
24
24
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "author": "Aaron Goldenthal <npm@aarongoldenthal.com>",
38
38
  "license": "MIT",
39
39
  "engines": {
40
- "node": "^14.15.0 || ^16.13.0 || >=18.0.0"
40
+ "node": "^18.12.0 || >=20.0.0"
41
41
  },
42
42
  "files": [
43
43
  "index.js",
@@ -51,29 +51,30 @@
51
51
  },
52
52
  "homepage": "https://gitlab.com/gitlab-ci-utils/pagean",
53
53
  "devDependencies": {
54
- "@aarongoldenthal/eslint-config-standard": "^20.0.0",
55
- "@aarongoldenthal/stylelint-config-standard": "^13.0.1",
56
- "bin-tester": "^3.0.1",
57
- "eslint": "^8.34.0",
58
- "jest": "^29.4.3",
59
- "jest-junit": "^15.0.0",
60
- "markdownlint-cli": "^0.33.0",
61
- "prettier": "^2.8.4",
54
+ "@aarongoldenthal/eslint-config-standard": "^25.0.0",
55
+ "@aarongoldenthal/stylelint-config-standard": "^17.0.0",
56
+ "bin-tester": "^5.0.0",
57
+ "eslint": "^8.56.0",
58
+ "jest": "^29.7.0",
59
+ "jest-junit": "^16.0.0",
60
+ "markdownlint-cli2": "^0.12.1",
61
+ "prettier": "^3.2.4",
62
62
  "strip-ansi": "^6.0.1",
63
- "stylelint": "^15.2.0"
63
+ "stylelint": "^16.2.0"
64
64
  },
65
65
  "dependencies": {
66
66
  "ajv": "^8.12.0",
67
67
  "ajv-errors": "^3.0.0",
68
- "axios": "^1.3.3",
69
- "ci-logger": "^5.1.1",
70
- "commander": "^10.0.0",
68
+ "axios": "^1.6.7",
69
+ "ci-logger": "^6.0.0",
70
+ "commander": "^11.1.0",
71
71
  "cssesc": "^3.0.0",
72
- "handlebars": "^4.7.7",
72
+ "handlebars": "^4.7.8",
73
73
  "htmlhint": "^1.1.4",
74
74
  "kleur": "^4.1.5",
75
75
  "normalize-url": "^6.1.0",
76
76
  "protocolify": "^3.0.0",
77
- "puppeteer": "^19.7.1"
77
+ "puppeteer": "^21.9.0",
78
+ "xml2js": "^0.6.2"
78
79
  }
79
80
  }
@@ -30,6 +30,48 @@
30
30
  "description": "The global test settings to apply to all tests, unless overridden for a specific URL.",
31
31
  "$ref": "#/definitions/allTestSettings"
32
32
  },
33
+ "sitemap": {
34
+ "description": "The sitemap to test",
35
+ "type": "object",
36
+ "properties": {
37
+ "url": {
38
+ "description": "The URL of the sitemap to test",
39
+ "type": "string"
40
+ },
41
+ "find": {
42
+ "description": "The string or regex pattern to find in sitemap URLs",
43
+ "type": "string"
44
+ },
45
+ "replace": {
46
+ "description": "The replacement string in sitemap URLs",
47
+ "type": "string"
48
+ },
49
+ "exclude": {
50
+ "description": "An array of strings or regex patterns to exclude from the sitemap",
51
+ "type": "array",
52
+ "items": {
53
+ "type": "string"
54
+ },
55
+ "uniqueItems": true,
56
+ "minItems": 1,
57
+ "maxItems": 100
58
+ }
59
+ },
60
+ "allOf": [
61
+ { "required": ["url"], "errorMessage": "must provide 'url'" },
62
+ {
63
+ "dependentRequired": {
64
+ "find": ["replace"],
65
+ "replace": ["find"]
66
+ },
67
+ "errorMessage": "must use 'find' and 'replace' together"
68
+ }
69
+ ],
70
+ "additionalProperties": {
71
+ "not": true,
72
+ "errorMessage": "must NOT contain additional properties: ${0#}"
73
+ }
74
+ },
33
75
  "urls": {
34
76
  "description": "The collection of URLs to test, with any URL-specific test settings.",
35
77
  "type": "array",
@@ -71,7 +113,16 @@
71
113
  "not": true,
72
114
  "errorMessage": "must NOT contain additional properties: ${0#}"
73
115
  },
74
- "required": ["urls"],
116
+ "anyOf": [
117
+ {
118
+ "required": ["sitemap"],
119
+ "errorMessage": "must have either property 'sitemap' or 'urls' (or both)"
120
+ },
121
+ {
122
+ "required": ["urls"],
123
+ "errorMessage": "must have either property 'sitemap' or 'urls' (or both)"
124
+ }
125
+ ],
75
126
  "definitions": {
76
127
  "detailedSetting": {
77
128
  "description": "The complete set of test-specific settings for most tests.",