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/config.js CHANGED
@@ -6,13 +6,15 @@
6
6
  * @module config
7
7
  */
8
8
 
9
- const fs = require('fs');
10
- const path = require('path');
11
- const Ajv = require('ajv').default;
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const Ajv = require('ajv/dist/2019');
12
+ const draft7MetaSchema = require('ajv/dist/refs/json-schema-draft-07.json');
12
13
  const ajvErrors = require('ajv-errors');
13
14
  const protocolify = require('protocolify');
14
15
 
15
16
  const { normalizeLink } = require('./link-utils');
17
+ const { getUrlsFromSitemap } = require('./sitemap');
16
18
 
17
19
  const defaultConfig = require('./default-config.json');
18
20
  const defaultHtmlHintConfigFilename = './.htmlhintrc';
@@ -43,6 +45,8 @@ const processAllTestSettings = (settings) => {
43
45
  for (const key of Object.keys(settings)) {
44
46
  processedSettings[key] = processTestSetting(settings[key]);
45
47
  }
48
+ /* eslint-disable-next-line consistent-return -- return undefined
49
+ if no settings */
46
50
  return processedSettings;
47
51
  };
48
52
 
@@ -70,45 +74,75 @@ const consolidateTestSettings = (
70
74
  return processedSettings;
71
75
  };
72
76
 
73
- const processUrls = (config) => {
77
+ const addSitemapUrls = (config, sitemapUrls) => {
78
+ if (config.urls) {
79
+ for (const url of config.urls) {
80
+ const { rawUrl } = getUrl(url);
81
+ const index = sitemapUrls.indexOf(rawUrl);
82
+ index > -1 && sitemapUrls.splice(index, 1);
83
+ }
84
+ config.urls.push(...sitemapUrls);
85
+ } else {
86
+ config.urls = sitemapUrls;
87
+ }
88
+ };
89
+
90
+ const processUrls = async (config) => {
74
91
  const processedConfigSettings = processAllTestSettings(config.settings);
92
+ if (config.sitemap) {
93
+ const sitemapUrls = await getUrlsFromSitemap(config.sitemap);
94
+ addSitemapUrls(config, sitemapUrls);
95
+ }
75
96
  return config.urls.map((testUrl) => {
76
97
  const { rawUrl, urlSettings } = getUrl(testUrl);
77
98
  return {
78
- url: protocolify(rawUrl),
79
99
  rawUrl,
80
100
  settings: consolidateTestSettings(
81
101
  defaultConfig.settings,
82
102
  processedConfigSettings,
83
103
  processAllTestSettings(urlSettings)
84
- )
104
+ ),
105
+ url: protocolify(rawUrl)
85
106
  };
86
107
  });
87
108
  };
88
109
 
89
110
  const getHtmlHintConfig = (htmlHintConfigFilename) => {
111
+ // Allow users to specify a custom htmlhintrc file.
112
+ // nosemgrep: eslint.detect-non-literal-fs-filename
90
113
  if (!fs.existsSync(htmlHintConfigFilename)) {
91
114
  return;
92
115
  }
116
+ /* eslint-disable consistent-return -- return undefined if no settings */
117
+ // Allow users to specify a custom htmlhintrc file.
118
+ // nosemgrep: eslint.detect-non-literal-fs-filename
93
119
  return JSON.parse(fs.readFileSync(htmlHintConfigFilename, 'utf8'));
120
+ /* eslint-enable consistent-return -- return undefined if no settings */
94
121
  };
95
122
 
96
123
  const validateConfigSchema = (config) => {
97
124
  const schema = JSON.parse(
125
+ // All values hardcoded.
126
+ // nosemgrep: eslint.detect-non-literal-fs-filename
98
127
  fs.readFileSync(
99
128
  path.join(__dirname, '../', 'schemas', 'pageanrc.schema.json')
100
129
  )
101
130
  );
131
+ // Allow allErrors to lint the entire config file, although users could
132
+ // ReDoS themselves.
133
+ // nosemgrep: ajv-allerrors-true
102
134
  const ajv = new Ajv({ allErrors: true });
135
+ ajv.addMetaSchema(draft7MetaSchema);
103
136
  ajvErrors(ajv);
104
137
  const validate = ajv.compile(schema);
105
138
  const isValid = validate(config);
106
- return { isValid, errors: validate.errors || [] };
139
+ return { errors: validate.errors || [], isValid };
107
140
  };
108
141
 
109
- const getConfigFromFile = (configFileName) => {
110
- return JSON.parse(fs.readFileSync(configFileName, 'utf8'));
111
- };
142
+ const getConfigFromFile = (configFileName) =>
143
+ // Allow users to specify config filename.
144
+ // nosemgrep: eslint.detect-non-literal-fs-filename
145
+ JSON.parse(fs.readFileSync(configFileName, 'utf8'));
112
146
 
113
147
  /**
114
148
  * Loads config from file and returns consolidated config with
@@ -116,10 +150,12 @@ const getConfigFromFile = (configFileName) => {
116
150
  *
117
151
  * @param {string} configFileName Pagean configuration file name.
118
152
  * @returns {object} Consolidated Pagean configuration.
153
+ * @async
119
154
  * @throws {TypeError} Throws if config file has an invalid schema.
120
155
  * @static
156
+ * @public
121
157
  */
122
- const processConfig = (configFileName) => {
158
+ const processConfig = async (configFileName) => {
123
159
  const config = getConfigFromFile(configFileName);
124
160
  const { isValid } = validateConfigSchema(config);
125
161
  if (!isValid) {
@@ -128,13 +164,16 @@ const processConfig = (configFileName) => {
128
164
  );
129
165
  }
130
166
  return {
131
- project: config.project || '',
132
- puppeteerLaunchOptions: config.puppeteerLaunchOptions,
133
- urls: processUrls(config),
134
167
  htmlHintConfig: getHtmlHintConfig(
135
168
  config.htmlhintrc || defaultHtmlHintConfigFilename
136
169
  ),
137
- reporters: config.reporters || defaultConfig.reporters
170
+ project: config.project || '',
171
+ puppeteerLaunchOptions: {
172
+ ...defaultConfig.puppeteerLaunchOptions,
173
+ ...config.puppeteerLaunchOptions
174
+ },
175
+ reporters: config.reporters || defaultConfig.reporters,
176
+ urls: await processUrls(config)
138
177
  };
139
178
  };
140
179
 
@@ -32,6 +32,9 @@
32
32
  "ignoreDuplicates": true
33
33
  }
34
34
  },
35
+ "puppeteerLaunchOptions": {
36
+ "headless": "new"
37
+ },
35
38
  "reporters": ["cli", "html", "json"],
36
39
  "urls": ["ignored, required to validate schema"]
37
40
  }
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * @module external-file-utils
7
7
  */
8
- const fs = require('fs');
9
- const path = require('path');
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
10
 
11
11
  const axios = require('axios');
12
12
 
@@ -48,10 +48,16 @@ const saveExternalScript = async (script) => {
48
48
  scriptUrl.hostname,
49
49
  scriptUrl.pathname
50
50
  );
51
+ // Path generated above from URL, not from user input.
52
+ // nosemgrep: eslint.detect-non-literal-fs-filename
51
53
  if (!fs.existsSync(pathName)) {
52
54
  // Axios will throw for any error response
53
55
  const response = await axios.get(script);
56
+ // Path generated above from URL, not from user input.
57
+ // nosemgrep: eslint.detect-non-literal-fs-filename
54
58
  fs.mkdirSync(path.dirname(pathName), { recursive: true });
59
+ // Path generated above from URL, not from user input.
60
+ // nosemgrep: eslint.detect-non-literal-fs-filename
55
61
  fs.writeFileSync(pathName, response.data);
56
62
  }
57
63
  result.localFile = pathName;
package/lib/link-utils.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * @module link-utils
7
7
  */
8
8
 
9
- const https = require('https');
9
+ const https = require('node:https');
10
10
  const axios = require('axios');
11
11
  const normalizeUrl = require('normalize-url');
12
12
  const cssesc = require('cssesc');
@@ -24,6 +24,7 @@ const timeoutSeconds = 120;
24
24
  const httpResponse = Object.freeze({
25
25
  continue: 100,
26
26
  ok: 200,
27
+ // eslint-disable-next-line sort-keys -- order by response code
27
28
  badRequest: 400,
28
29
  notFound: 404,
29
30
  tooManyRequests: 429,
@@ -52,7 +53,6 @@ const normalizeLink = (url) =>
52
53
  stripHash: true,
53
54
  stripWWW: false
54
55
  });
55
- /* eslint-enable jsdoc/require-description-complete-sentence */
56
56
 
57
57
  /**
58
58
  * Checks settings to determine if the provided link should be ignored.
@@ -99,7 +99,6 @@ const checkSamePageLink = async (page, link) => {
99
99
  isIdentifier: true
100
100
  })}`;
101
101
  const element = await page.$(escapedSelector);
102
- // const element = await page.$(selector);
103
102
  return element ? httpResponse.ok : `${selector} Not Found`;
104
103
  };
105
104
 
@@ -122,7 +121,7 @@ const checkExternalPageLinkBrowser = async (page, link) => {
122
121
  } catch (error) {
123
122
  // Errors are returned in the format: "ENOTFOUND at https://this.url.does.not.exist/",
124
123
  // so extract error only and remove URL
125
- status = error.message.replace(/^(.*) at .*$/, '$1');
124
+ status = error.message.replace(/^(?<message>.*) at .*$/, '$1');
126
125
  }
127
126
  return status;
128
127
  };
@@ -149,15 +148,16 @@ const checkExternalPageLink = async (page, link, useGet = false) => {
149
148
 
150
149
  try {
151
150
  const options = {
151
+ decompress: false,
152
152
  headers: { 'User-Agent': userAgent },
153
- timeout: timeoutSeconds * msPerSec,
153
+ timeout: timeoutSeconds * msPerSec
154
154
  // Axios can generate an error trying to decompress an empty compressed
155
155
  // HEAD response, see https://github.com/axios/axios/issues/5102.
156
156
  // The response body is also not used, so more efficient to skip.
157
- decompress: false
158
157
  };
159
158
  // Using internal browser property since not exposed
160
- // eslint-disable-next-line no-underscore-dangle
159
+ /* eslint-disable-next-line no-underscore-dangle -- require to match
160
+ Puppeteer API */
161
161
  if (page.browser()._ignoreHTTPSErrors) {
162
162
  const agent = new https.Agent({ rejectUnauthorized: false });
163
163
  options.httpsAgent = agent;
@@ -184,7 +184,7 @@ const checkExternalPageLink = async (page, link, useGet = false) => {
184
184
  };
185
185
 
186
186
  /**
187
- * Factory function returning a linkChecker object with a {@link checkLink}
187
+ * Factory function returning a linkChecker object with a checkLink
188
188
  * function that caches checked link results.
189
189
  *
190
190
  * @returns {object} Link checker object.
@@ -205,7 +205,7 @@ const createLinkChecker = () => {
205
205
  * @returns {(string|number)} The link status (HTTP response code or error).
206
206
  * @public
207
207
  */
208
- // eslint-disable-next-line sonarjs/cognitive-complexity
208
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- allow < 10
209
209
  checkLink: async (context, link) => {
210
210
  let status = httpResponse.unknownError;
211
211
  try {
package/lib/logger.js CHANGED
@@ -10,8 +10,8 @@ const consoleLogger = require('ci-logger');
10
10
  const { reporterTypes } = require('../lib/reporter');
11
11
 
12
12
  const testResultSymbols = Object.freeze({
13
- passed: ' √',
14
13
  failed: ' ×',
14
+ passed: ' √',
15
15
  warning: ' ‼'
16
16
  });
17
17
 
@@ -25,7 +25,7 @@ const nullFunction = () => {};
25
25
  * @returns {object} A logger object.
26
26
  * @static
27
27
  */
28
- // eslint-disable-next-line max-lines-per-function
28
+ // eslint-disable-next-line max-lines-per-function -- factory function with state
29
29
  module.exports = (config) => {
30
30
  const cliReporter = {
31
31
  // If cli reporter is enabled, set console log function, otherwise null function
@@ -38,15 +38,15 @@ module.exports = (config) => {
38
38
  };
39
39
 
40
40
  const testResults = {
41
- project: config.project,
42
41
  executionStart: new Date(),
42
+ project: config.project,
43
+ results: [],
43
44
  summary: {
44
- tests: 0,
45
+ failed: 0,
45
46
  passed: 0,
46
- warning: 0,
47
- failed: 0
48
- },
49
- results: []
47
+ tests: 0,
48
+ warning: 0
49
+ }
50
50
  };
51
51
 
52
52
  let currentUrlTests;
@@ -59,8 +59,8 @@ module.exports = (config) => {
59
59
  */
60
60
  const startUrlTests = (url) => {
61
61
  const urlTestResults = {
62
- url,
63
- tests: []
62
+ tests: [],
63
+ url
64
64
  };
65
65
  testResults.results.push(urlTestResults);
66
66
  currentUrlTests = urlTestResults;
@@ -72,7 +72,7 @@ module.exports = (config) => {
72
72
  * by the last call to StartUrlTests).
73
73
  *
74
74
  * @instance
75
- * @param {object} testResult The test results.
75
+ * @param {object} testResult The test results object.
76
76
  */
77
77
  const logTestResults = (testResult) => {
78
78
  if (testResult) {
@@ -81,8 +81,8 @@ module.exports = (config) => {
81
81
  testResults.summary[testResult.result]++;
82
82
 
83
83
  cliReporter.log({
84
- message: `${testResult.name} (${testResult.result})`,
85
84
  isResult: true,
85
+ message: `${testResult.name} (${testResult.result})`,
86
86
  resultPrefix: testResultSymbols[testResult.result]
87
87
  });
88
88
  }
@@ -107,8 +107,8 @@ module.exports = (config) => {
107
107
  };
108
108
 
109
109
  return {
110
- startUrlTests,
110
+ getTestResults,
111
111
  logTestResults,
112
- getTestResults
112
+ startUrlTests
113
113
  };
114
114
  };
@@ -4,25 +4,40 @@
4
4
  <head>
5
5
  <title>Pagean Results</title>
6
6
  <style>
7
+ :root {
8
+ /* Default colors */
9
+ --color-text-default: #333333;
10
+ --color-background-data-hover: rgba(255 255 255 / 10%);
11
+ --color-background-summary: #f4f4f4;
12
+ --color-border-summary: #333333;
13
+
14
+ /* Test result colors */
15
+ --color-background-passed: #dff2bf;
16
+ --color-text-passed: #44760f;
17
+ --color-background-failed: #ffbaba;
18
+ --color-text-failed: #ad000c;
19
+ --color-background-warning: #fdec96;
20
+ --color-text-warning: #7e6902;
21
+ }
22
+
7
23
  html {
8
24
  margin: 0;
9
25
  padding: 0;
10
26
  }
11
27
 
12
28
  body {
13
- color: #333333;
14
- font-family: Arial, Helvetica, sans-serif;
29
+ color: var(--color-text-default);
30
+ font-family: system-ui, sans-serif;
15
31
  font-size: 0.85rem;
16
32
  margin: auto;
17
- max-width: 1000px;
33
+ max-inline-size: 1000px;
18
34
  padding: 1rem;
19
35
  }
20
36
 
21
37
  ul {
22
- margin: 0;
23
38
  margin-block: 0;
24
39
  margin-inline: 0;
25
- padding: 0;
40
+ padding-block: 0;
26
41
  padding-inline: 0;
27
42
  }
28
43
 
@@ -31,7 +46,6 @@
31
46
  }
32
47
 
33
48
  h1 {
34
- margin: 1rem 0 0.75rem;
35
49
  margin-block: 0;
36
50
  margin-inline: 0;
37
51
  }
@@ -48,62 +62,68 @@
48
62
  .project,
49
63
  .started {
50
64
  margin: 0;
51
- padding-bottom: 0.5rem;
65
+ padding-block-end: 0.5rem;
52
66
  }
53
67
 
54
68
  .test-summary {
55
69
  display: flex;
56
70
  flex-direction: row;
57
- margin-bottom: 2rem;
58
- max-width: 50%;
71
+ margin-block-end: 2rem;
72
+ max-inline-size: 50%;
59
73
  }
60
74
 
61
75
  .summary {
62
- background-color: #f4f4f4;
63
- border: 1px solid #333333;
76
+ background-color: var(--color-background-summary);
77
+ border: 1px solid var(--color-border-summary);
64
78
  display: inline-block;
65
79
  flex: 1 1 10%;
66
- margin: 0 0.25rem;
67
- padding: 0.25rem 0.5rem;
80
+ margin-block: 0;
81
+ margin-inline: 0.25rem;
82
+ padding-block: 0.25rem;
83
+ padding-inline: 0.5rem;
68
84
  }
69
85
 
70
86
  .summary:first-of-type {
71
- margin-left: 0;
87
+ margin-inline-start: 0;
72
88
  }
73
89
 
74
90
  .summary:last-of-type {
75
- margin-right: 0;
91
+ margin-inline-end: 0;
76
92
  }
77
93
 
78
94
  .passed {
79
- background-color: #dff2bf;
80
- border: 1px solid #44760f;
81
- color: #44760f;
95
+ background-color: var(--color-background-passed);
96
+ border: 1px solid var(--color-text-passed);
97
+ color: var(--color-text-passed);
82
98
  }
83
99
 
84
100
  .failed {
85
- background-color: #ffbaba;
86
- border: 1px solid #ad000c;
87
- color: #ad000c;
101
+ background-color: var(--color-background-failed);
102
+ border: 1px solid var(--color-text-failed);
103
+ color: var(--color-text-failed);
88
104
  }
89
105
 
90
106
  .warning {
91
- background-color: #fdec96;
92
- border: 1px solid #7e6902;
93
- color: #7e6902;
107
+ background-color: var(--color-background-warning);
108
+ border: 1px solid var(--color-text-warning);
109
+ color: var(--color-text-warning);
94
110
  }
95
111
 
96
112
  .test-results h2 {
97
- margin-block-end: 0;
98
- margin-block-start: 1rem;
113
+ margin-block: 1rem 0;
99
114
  }
100
115
 
101
- details summary .name::after {
102
- content: " ⏵";
116
+ details summary .name::after,
117
+ details[open] summary .name::after {
118
+ content: "+";
119
+ font-family: monospace;
120
+ font-weight: bold;
121
+ opacity: 0.8;
122
+ padding-inline-start: 0.5rem;
103
123
  }
104
124
 
105
125
  details[open] summary .name::after {
106
- content: "";
126
+ content: "";
107
127
  }
108
128
 
109
129
  summary {
@@ -116,8 +136,10 @@
116
136
  }
117
137
 
118
138
  li.test {
119
- margin: 0.25rem 0;
120
- padding: 0.5rem 1rem;
139
+ margin-block: 0.25rem;
140
+ margin-inline: 0;
141
+ padding-block: 0.5rem;
142
+ padding-inline: 1rem;
121
143
  }
122
144
 
123
145
  .test .header {
@@ -131,20 +153,20 @@
131
153
 
132
154
  .test .header .result {
133
155
  flex: 1 1 25%;
134
- text-align: right;
156
+ text-align: end;
135
157
  }
136
158
 
137
159
  .test .data,
138
160
  .test .time,
139
161
  .test .error {
140
- padding-left: 10px;
162
+ padding-inline-start: 0.75rem;
141
163
  }
142
164
 
143
165
  .test .data h3 {
144
166
  font-size: inherit;
145
167
  font-weight: normal;
146
- margin: 0.25rem 0 0;
147
168
  margin-block: 0;
169
+ margin-inline: 0;
148
170
  }
149
171
 
150
172
  .test .data li {
@@ -154,18 +176,20 @@
154
176
  }
155
177
 
156
178
  .test .data li:hover {
157
- background-color: rgba(255 255 255 / 10%);
179
+ background-color: var(--color-background-data-hover);
158
180
  border: 1px dotted;
159
- border-right: 4px solid;
181
+ border-inline-end: 4px solid;
160
182
  padding: calc(0.25rem - 1px);
161
183
  }
162
184
 
163
185
  .test .data .pre {
164
- margin: 0 0 0.25rem;
186
+ margin-block: 0 0.25rem;
187
+ margin-inline: 0;
165
188
  }
166
189
 
167
190
  .test .time {
168
- margin: 0.25rem 0 0;
191
+ margin-block: 0.25rem 0;
192
+ margin-inline: 0;
169
193
  }
170
194
  </style>
171
195
  </head>
@@ -192,7 +216,7 @@
192
216
  <li class="test {{result}}">
193
217
  {{#if data}}
194
218
  <details>
195
- <summary class="header">
219
+ <summary class="header" title="Show/hide details">
196
220
  <div class="name">{{name}}</div>
197
221
  <div class="result">{{result}}</div>
198
222
  </summary>
package/lib/reporter.js CHANGED
@@ -5,23 +5,25 @@
5
5
  *
6
6
  * @module reporter
7
7
  */
8
- const fs = require('fs');
9
- const path = require('path');
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
10
  const handlebars = require('handlebars');
11
11
 
12
12
  const htmlReportTemplateName = 'report-template.handlebars';
13
13
  const htmlReportFileName = './pagean-results.html';
14
14
  const jsonReportFileName = './pagean-results.json';
15
15
 
16
- handlebars.registerHelper('json', (context) => {
17
- // eslint-disable-next-line no-magic-numbers -- count
18
- return JSON.stringify(context, undefined, 2);
19
- });
16
+ handlebars.registerHelper('json', (context) =>
17
+ // eslint-disable-next-line no-magic-numbers -- no-magic-numbers - count
18
+ JSON.stringify(context, undefined, 2)
19
+ );
20
20
 
21
21
  const saveHtmlReport = (results) => {
22
22
  const templateFile = path.resolve(
23
23
  path.join(__dirname, htmlReportTemplateName)
24
24
  );
25
+ // Path hardcoded above, not from user input.
26
+ // nosemgrep: eslint.detect-non-literal-fs-filename
25
27
  const htmlReportTemplate = fs.readFileSync(templateFile, 'utf8');
26
28
  const template = handlebars.compile(htmlReportTemplate);
27
29
  const htmlReport = template(results);
@@ -24,8 +24,8 @@ const getDataKey = (instancePath) => {
24
24
  // eslint-disable-next-line unicorn/no-array-reduce -- reduce for string concatenation
25
25
  .reduce((accumulator, currentValue) => {
26
26
  const unencodedValue = currentValue
27
- .replace(/~0/g, '~')
28
- .replace(/~1/g, '/');
27
+ .replaceAll('~0', '~')
28
+ .replaceAll('~1', '/');
29
29
  const encodedValue = Number.isNaN(Number(unencodedValue))
30
30
  ? `.${unencodedValue}`
31
31
  : `[${unencodedValue}]`;