sitespeed.io 36.4.1 → 37.0.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
1
 
2
2
  # CHANGELOG - sitespeed.io (we use [semantic versioning](https://semver.org))
3
3
 
4
+ ## 37.0.1 - 2025-03-05
5
+ ### Fixed
6
+ * There was a bug in the InfluxDB plugin and loading the cli parameters failed [#4463](https://github.com/sitespeedio/sitespeed.io/pull/4463).
7
+
8
+
9
+ ## 37.0.0 - 2025-03-05
10
+ ### Breaking change
11
+ * If you are a InfluxDB user the InfluxDB functionality been moved to a standalone plugin [plugin-influxdb](https://github.com/sitespeedio/plugin-influxdb). This means if that sitespeed.io using NodeJS and the default Docker container will not include the InfluxDB plugin. The +1 container will still include the plugin but you need to add it with `--plugins.add @sitespeed.io/plugin-influxdb` for it to be able to run.
12
+
13
+ The InfluxDB plugin has never gotten the love it deserves, moving it out, it means that you easier can do your own modification and get the data the way that you need.
14
+
15
+ ### Added
16
+ * Support for getting CLI options from plugins as long as you use `--help --plugins.add`. This is start to separate CLI options from the monsterous CLI file and instead have them in each plugin [#4450](https://github.com/sitespeedio/sitespeed.io/pull/4450), [#4452](https://github.com/sitespeedio/sitespeed.io/pull/4452) [#4455](https://github.com/sitespeedio/sitespeed.io/pull/4455).
17
+ * You can now set the exact minimum log level using `--logLevel`[#4459](https://github.com/sitespeedio/sitespeed.io/pull/4459).
18
+
19
+ ### Fixed
20
+ * Replace the junit-report-builder package [#4448](https://github.com/sitespeedio/sitespeed.io/pull/4448).
21
+ * Remove Tape dependencies [#4447](https://github.com/sitespeedio/sitespeed.io/pull/4447).
22
+ * Catch if the browser fails to open a broken page [#4457](https://github.com/sitespeedio/sitespeed.io/pull/4457).
23
+
24
+
4
25
  ## 36.4.1 - 2025-02-17
5
26
  ### Fixed
6
27
  * The Docker container for last release was never push. The reason is that our GitHub action that do the release automatically was upgraded to Ubuntu 24 and there its a problem building containers for ARM. With this release we use Ubuntu 22 instead.
package/HELP.md CHANGED
@@ -2,7 +2,7 @@
2
2
  We want to make sitespeed.io one of the best web performance tool in the world and we hope you can help us!
3
3
 
4
4
  ## Developers
5
- We love to have more people involved in improving sitespeed.io. We are constantly working on adding more documentation and trying to write more information in the issues so its easier to help out. If there's an [issue](https://github.com/sitespeedio/sitespeed.io/issues) that you want to take on, ping the the issue and we can help you get started. You can also [join our Slack channel](https://join.slack.com/t/sitespeedio/shared_invite/zt-296jzr7qs-d6DId2KpEnMPJSQ8_R~WFw) if you need help!
5
+ We love to have more people involved in improving sitespeed.io. We are constantly working on adding more documentation and trying to write more information in the issues so its easier to help out. If there's an [issue](https://github.com/sitespeedio/sitespeed.io/issues) that you want to take on, ping the the issue and we can help you get started. You can also [join our Slack channel](https://join.slack.com/t/sitespeedio/shared_invite/zt-296jzr7qs-d6DId2KpEnMPJSQ8_R~WFw) if you need help! You can start by reading the [developer documentation](https://www.sitespeed.io/documentation/sitespeed.io/developers/).
6
6
 
7
7
  ## Designers
8
8
  As a designer there's a lot you can do: You can help us improve the HTML result pages. Maybe we should restructure the metrics ? Or could the header/footer look better? You could also have look at [https://www.sitespeed.io](https://www.sitespeed.io/) where we have all the documentation. You can pretty much help us with everything, no one in the core team got design skills :)
@@ -10,13 +10,6 @@ As a designer there's a lot you can do: You can help us improve the HTML result
10
10
  ## Documentation
11
11
  Documentation is fun and it is the core of making sitespeed.io easy to use. We got a [special documentation tag for issues](https://github.com/sitespeedio/sitespeed.io/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) that you can use to find where we know we lack documentation. Fixing spelling mistakes is great. Or rewrite parts that you think is too complicated. You can find what you need to send a PR to the documentation [here](https://github.com/sitespeedio/sitespeed.io/tree/main/docs).
12
12
 
13
- ## Tests
14
- We lack unit tests. You can read about [our testing pipeline](https://www.sitespeed.io/releasing-with-confidence/) that works good for us but more unit tests are always good. A good start is adding support for the code in the [support library](https://github.com/sitespeedio/sitespeed.io/tree/main/lib/support).
15
-
16
13
 
17
14
  ## Companies
18
- Do you use sitespeed.io in your everyday work? Then we have a perfect proposition for you! Have a hack day with focus on sitespeed.io for your team and contribute back. Pick one the things in the **Help wanted** section or make your plugin Open Source and tell us about it. Or maybe there's a something you think is missing? Create it. Contribute back.
19
-
20
- # Help wanted
21
- You can help us:
22
- * Help us improve the documentation. We love your feedback and help [https://github.com/sitespeedio/sitespeed.io/tree/main/docs](https://github.com/sitespeedio/sitespeed.io/tree/main/docs).
15
+ Do you use sitespeed.io in your everyday work? Then go to the [sponsor page](https://www.sitespeed.io/sponsor/).
package/lib/cli/cli.js CHANGED
@@ -18,6 +18,7 @@ import { config as slackConfig } from '../plugins/slack/index.js';
18
18
  import { config as htmlConfig } from '../plugins/html/index.js';
19
19
  import { messageTypes as matrixMessageTypes } from '../plugins/matrix/index.js';
20
20
  import { findUpSync } from '../support/fileUtil.js';
21
+ import { registerPluginOptions } from './pluginOptions.js';
21
22
 
22
23
  const metricList = Object.keys(friendlynames);
23
24
  const require = createRequire(import.meta.url);
@@ -25,6 +26,13 @@ const version = require('../../package.json').version;
25
26
 
26
27
  const configFiles = ['.sitespeed.io.json'];
27
28
 
29
+ const addedPlugins = yargs(hideBin(process.argv))
30
+ .option('plugins.add', { type: 'array' })
31
+ .help(false)
32
+ .version(false)
33
+ .parse();
34
+ const globalPluginsToAdd = addedPlugins.plugins?.add || [];
35
+
28
36
  function fixAndroidArgs(args) {
29
37
  return args.map(arg => (arg === '--android' ? '--android.enabled' : arg));
30
38
  }
@@ -220,7 +228,7 @@ function validateInput(argv) {
220
228
  export async function parseCommandLine() {
221
229
  const fixedArgs = fixAndroidArgs(hideBin(process.argv));
222
230
  const yargsInstance = yargs(fixedArgs);
223
- let parsed = yargsInstance
231
+ yargsInstance
224
232
  .parserConfiguration({
225
233
  'camel-case-expansion': false,
226
234
  'deep-merge-config': true
@@ -421,7 +429,7 @@ export async function parseCommandLine() {
421
429
  })
422
430
  .option('browsertime.script', {
423
431
  describe:
424
- 'Add custom Javascript that collect metrics and run after the page has finished loading. Note that --script can be passed multiple times if you want to collect multiple metrics. The metrics will automatically be pushed to the summary/detailed summary and each individual page + sent to Graphite/InfluxDB.',
432
+ 'Add custom Javascript that collect metrics and run after the page has finished loading. Note that --script can be passed multiple times if you want to collect multiple metrics. The metrics will automatically be pushed to the summary/detailed summary and each individual page + sent to Graphite',
425
433
  alias: ['script'],
426
434
  group: 'Browser'
427
435
  })
@@ -513,7 +521,7 @@ export async function parseCommandLine() {
513
521
  .option('browsertime.scriptInput.visualElements', {
514
522
  alias: ['scriptInput.visualElements'],
515
523
  describe:
516
- 'Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector . Add multiple instances to measure multiple elements. Visual Metrics will use these elements and calculate when they are visible and fully rendered.',
524
+ 'Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector . If you want to measure multiple elements, use a configuration file with an array for the input. Visual Metrics will use these elements and calculate when they are visible and fully rendered.',
517
525
  group: 'Browser'
518
526
  })
519
527
  .option('browsertime.scriptInput.longTask', {
@@ -1160,7 +1168,6 @@ export async function parseCommandLine() {
1160
1168
  describe: 'Ignore robots.txt rules of the crawled domain.',
1161
1169
  group: 'Crawler'
1162
1170
  })
1163
-
1164
1171
  .option('scp.host', {
1165
1172
  describe: 'The host.',
1166
1173
  group: 'scp'
@@ -1347,80 +1354,6 @@ export async function parseCommandLine() {
1347
1354
  'Define which messages to send to Graphite. By default we do not send data per run, but you can change that by adding run as one of the options',
1348
1355
  group: 'Graphite'
1349
1356
  })
1350
-
1351
- .option('influxdb.protocol', {
1352
- describe: 'The protocol used to store connect to the InfluxDB host.',
1353
- default: 'http',
1354
- group: 'InfluxDB'
1355
- })
1356
- .option('influxdb.host', {
1357
- describe: 'The InfluxDB host used to store captured metrics.',
1358
- group: 'InfluxDB'
1359
- })
1360
- .option('influxdb.port', {
1361
- default: 8086,
1362
- describe: 'The InfluxDB port used to store captured metrics.',
1363
- group: 'InfluxDB'
1364
- })
1365
- .option('influxdb.username', {
1366
- describe:
1367
- 'The InfluxDB username for your InfluxDB instance (only for InfluxDB v1)',
1368
- group: 'InfluxDB'
1369
- })
1370
- .option('influxdb.password', {
1371
- describe:
1372
- 'The InfluxDB password for your InfluxDB instance (only for InfluxDB v1).',
1373
- group: 'InfluxDB'
1374
- })
1375
- .option('influxdb.organisation', {
1376
- describe:
1377
- 'The InfluxDB organisation for your InfluxDB instance (only for InfluxDB v2)',
1378
- group: 'InfluxDB'
1379
- })
1380
- .option('influxdb.token', {
1381
- describe:
1382
- 'The InfluxDB token for your InfluxDB instance (only for InfluxDB v2)',
1383
- group: 'InfluxDB'
1384
- })
1385
- .option('influxdb.version', {
1386
- default: 1,
1387
- describe: 'The InfluxDB version of your InfluxDB instance.',
1388
- type: 'integer',
1389
- group: 'InfluxDB'
1390
- })
1391
- .option('influxdb.database', {
1392
- default: 'sitespeed',
1393
- describe: 'The database name used to store captured metrics.',
1394
- group: 'InfluxDB'
1395
- })
1396
- .option('influxdb.tags', {
1397
- default: 'category=default',
1398
- describe:
1399
- 'A comma separated list of tags and values added to each metric',
1400
- group: 'InfluxDB'
1401
- })
1402
- .option('influxdb.includeQueryParams', {
1403
- default: false,
1404
- describe:
1405
- 'Whether to include query parameters from the URL in the InfluxDB keys or not',
1406
- type: 'boolean',
1407
- group: 'InfluxDB'
1408
- })
1409
- .option('influxdb.groupSeparator', {
1410
- default: '_',
1411
- describe:
1412
- 'Choose which character that will separate a group/domain. Default is underscore, set it to a dot if you wanna keep the original domain name.',
1413
- group: 'InfluxDB'
1414
- })
1415
- .option('influxdb.annotationScreenshot', {
1416
- default: false,
1417
- type: 'boolean',
1418
- describe:
1419
- 'Include screenshot (from Browsertime) in the annotation. You need to specify a --resultBaseURL for this to work.',
1420
- group: 'InfluxDB'
1421
- });
1422
-
1423
- parsed
1424
1357
  /** Plugins */
1425
1358
  .option('plugins.list', {
1426
1359
  describe: 'List all configured plugins in the log.',
@@ -1526,13 +1459,7 @@ export async function parseCommandLine() {
1526
1459
  describe: 'The max size of the screenshot (width and height).',
1527
1460
  default: browsertimeConfig.screenshotParams.maxSize,
1528
1461
  group: 'Screenshot'
1529
- });
1530
- /**
1531
- InfluxDB cli option
1532
- */
1533
-
1534
- parsed
1535
- // Metrics
1462
+ })
1536
1463
  .option('metrics.list', {
1537
1464
  describe: 'List all possible metrics in the data folder (metrics.txt).',
1538
1465
  type: 'boolean',
@@ -1877,9 +1804,7 @@ export async function parseCommandLine() {
1877
1804
  describe:
1878
1805
  'Instead of using the local copy of the hosting database, you can use the latest version through the Green Web Foundation API. This means sitespeed.io will make HTTP GET to the the hosting info.',
1879
1806
  group: 'Sustainable'
1880
- });
1881
-
1882
- parsed
1807
+ })
1883
1808
  .option('api.key', {
1884
1809
  describe: 'The API key to use.',
1885
1810
  group: 'API'
@@ -1930,9 +1855,7 @@ export async function parseCommandLine() {
1930
1855
  .option('api.json', {
1931
1856
  describe: 'Output the result as JSON.',
1932
1857
  group: 'API'
1933
- });
1934
-
1935
- parsed
1858
+ })
1936
1859
  .option('compare.id', {
1937
1860
  type: 'string',
1938
1861
  describe:
@@ -1993,8 +1916,7 @@ export async function parseCommandLine() {
1993
1916
  'Selects the method for calculating the Mann-Whitney U test. auto automatically selects between exact and asymptotic based on sample size, exact uses the exact distribution of U, and symptotic uses a normal approximation.',
1994
1917
  default: 'auto',
1995
1918
  group: 'compare'
1996
- });
1997
- parsed
1919
+ })
1998
1920
  .option('mobile', {
1999
1921
  describe:
2000
1922
  'Access pages as mobile a fake mobile device. Set UA and width/height. For Chrome it will use device Moto G4.',
@@ -2028,12 +1950,12 @@ export async function parseCommandLine() {
2028
1950
  })
2029
1951
  .option('urlAlias', {
2030
1952
  describe:
2031
- 'Use an alias for the URL (if you feed URLs from a file you can instead have the alias in the file). You need to pass on the same amount of alias as URLs. The alias is used as the name of the URL on the HTML report and in Graphite/InfluxDB. Pass on multiple --urlAlias for multiple alias/URLs. This will override alias in a file.',
1953
+ 'Use an alias for the URL (if you feed URLs from a file you can instead have the alias in the file). You need to pass on the same amount of alias as URLs. The alias is used as the name of the URL on the HTML report and in Graphite. Pass on multiple --urlAlias for multiple alias/URLs. This will override alias in a file.',
2032
1954
  type: 'string'
2033
1955
  })
2034
1956
  .option('groupAlias', {
2035
1957
  describe:
2036
- 'Use an alias for the group/domain. You need to pass on the same amount of alias as URLs. The alias is used as the name of the group in Graphite/InfluxDB. Pass on multiple --groupAlias for multiple alias/groups. This do not work for scripting at the moment.',
1958
+ 'Use an alias for the group/domain. You need to pass on the same amount of alias as URLs. The alias is used as the name of the group in Graphite. Pass on multiple --groupAlias for multiple alias/groups. This do not work for scripting at the moment.',
2037
1959
  type: 'string'
2038
1960
  })
2039
1961
  .option('utc', {
@@ -2056,6 +1978,11 @@ export async function parseCommandLine() {
2056
1978
  .option('name', {
2057
1979
  describe: 'Give your test a name.'
2058
1980
  })
1981
+ .option('logLevel', {
1982
+ type: 'string',
1983
+ choices: ['trace', 'verbose', 'debug', 'info', 'warning', 'error'],
1984
+ describe: 'Manually set the min log level'
1985
+ })
2059
1986
  .option('open', {
2060
1987
  alias: ['o', 'view'],
2061
1988
  describe:
@@ -2124,8 +2051,12 @@ export async function parseCommandLine() {
2124
2051
  }
2125
2052
  return plugins;
2126
2053
  }
2127
- })
2128
- // .describe('browser', 'Specify browser')
2054
+ });
2055
+ // .describe('browser', 'Specify browser')
2056
+
2057
+ await registerPluginOptions(yargsInstance, globalPluginsToAdd);
2058
+
2059
+ let parsed = yargsInstance
2129
2060
  .wrap(yargsInstance.terminalWidth())
2130
2061
  // .check(validateInput)
2131
2062
  .epilog(
@@ -0,0 +1,34 @@
1
+ import { importGlobalSilent } from 'import-global';
2
+
3
+ /**
4
+ * Dynamically load and register CLI options from plugins.
5
+ *
6
+ * @param {import('yargs').Argv} yargsInstance - The yargs instance to extend.
7
+ * @param {string[]} plugins - Array of plugin module names.
8
+ * @returns {Promise<void>}
9
+ */
10
+ export async function registerPluginOptions(yargsInstance, plugins) {
11
+ for (const pluginName of plugins) {
12
+ try {
13
+ // Dynamically import the plugin
14
+ let { default: plugin } = await importGlobalSilent(pluginName);
15
+ // If the plugin exports a function to get CLI options, merge them
16
+ if (plugin && typeof plugin.getCliOptions === 'function') {
17
+ const options = plugin.getCliOptions();
18
+ yargsInstance.options(options);
19
+ } else {
20
+ try {
21
+ const { default: plugin } = await import(pluginName);
22
+ if (plugin && typeof plugin.getCliOptions === 'function') {
23
+ const options = plugin.getCliOptions();
24
+ yargsInstance.options(options);
25
+ }
26
+ } catch {
27
+ // Swallow this silent
28
+ }
29
+ }
30
+ } catch {
31
+ // Swallow this silent
32
+ }
33
+ }
34
+ }
@@ -2,6 +2,7 @@ import { configureLog } from '@sitespeed.io/log';
2
2
 
3
3
  export function configure(options = {}) {
4
4
  configureLog({
5
+ level: options.logLevel ?? undefined,
5
6
  verbose: options.verbose ?? 0,
6
7
  silent: options.silent ?? false
7
8
  });
@@ -81,12 +81,18 @@ export async function loadPlugins(pluginNames, options, context, queue) {
81
81
  plugins.push(p);
82
82
  } catch (error) {
83
83
  // try global
84
- let { default: plugin } = await importGlobalSilent(name);
85
- if (plugin) {
86
- let p = new plugin(options, context, queue);
87
- plugins.push(p);
88
- } else {
89
- console.error("Couldn't load plugin %s: %s", name, error_);
84
+ try {
85
+ let { default: plugin } = await importGlobalSilent(name);
86
+ if (plugin) {
87
+ let p = new plugin(options, context, queue);
88
+ plugins.push(p);
89
+ } else {
90
+ console.error("Couldn't load plugin %s: %s", name, error_);
91
+ // if it fails here, let it fail hard
92
+ throw error;
93
+ }
94
+ } catch {
95
+ console.error("Couldn't find/load plugin %s", name);
90
96
  // if it fails here, let it fail hard
91
97
  throw error;
92
98
  }
@@ -3,6 +3,8 @@ import { parse } from 'node:url';
3
3
  import { default as _merge } from 'lodash.merge';
4
4
 
5
5
  import { getLogger } from '@sitespeed.io/log';
6
+ import { configureLogging } from 'browsertime';
7
+
6
8
  const log = getLogger('plugin.browsertime');
7
9
 
8
10
  import dayjs from 'dayjs';
@@ -88,10 +90,10 @@ export default class BrowsertimePlugin extends SitespeedioPlugin {
88
90
  'browsertime.run'
89
91
  );
90
92
  this.axeAggregatorTotal = new AxeAggregator(this.options);
93
+ configureLogging(options);
91
94
  }
95
+
92
96
  async processMessage(message) {
93
- const { configureLogging } = await import('browsertime');
94
- configureLogging(this.options);
95
97
  const options = this.options;
96
98
  switch (message.type) {
97
99
  // When sistespeed.io starts, a setup messages is posted on the queue
@@ -1,69 +1,95 @@
1
1
  import path from 'node:path';
2
- import { parse } from 'node:url';
3
-
4
- import jrp from 'junit-report-builder';
5
-
2
+ import fs from 'node:fs';
3
+ import merge from 'lodash.merge';
6
4
  import { getLogger } from '@sitespeed.io/log';
5
+
7
6
  const log = getLogger('sitespeedio.plugin.budget');
8
7
 
9
- import merge from 'lodash.merge';
8
+ /**
9
+ * Escapes XML special characters.
10
+ *
11
+ * @param {string} str - The text to escape.
12
+ * @returns {string} The escaped text.
13
+ */
14
+ function xmlEscape(str) {
15
+ return String(str)
16
+ .replaceAll('&', '&amp;')
17
+ .replaceAll('<', '&lt;')
18
+ .replaceAll('>', '&gt;')
19
+ .replaceAll('"', '&quot;')
20
+ .replaceAll("'", '&apos;');
21
+ }
22
+
23
+ /**
24
+ * Wraps a string in a CDATA block.
25
+ *
26
+ * @param {string} str - The string to wrap.
27
+ * @returns {string} The CDATA-wrapped string.
28
+ */
29
+ function cdata(str) {
30
+ return `<![CDATA[${str}]]>`;
31
+ }
10
32
 
33
+ /**
34
+ * Writes a JUnit XML report mimicking the original output.
35
+ *
36
+ * @param {object} results - Object containing `failing` and `working` results.
37
+ * @param {string} dir - Directory where `junit.xml` will be written.
38
+ * @param {object} options - Options (expects `options.budget.friendlyName`).
39
+ */
11
40
  export function writeJunit(results, dir, options) {
12
- // lets have one suite per URL
13
- const urls = Object.keys(merge({}, results.failing, results.working));
41
+ const failing = results.failing || {};
42
+ const working = results.working || {};
43
+ const urls = Object.keys(merge({}, failing, working));
14
44
 
15
- for (const url of urls) {
16
- // The URL can be an alias
17
- let name = url;
18
- if (url.startsWith('http')) {
19
- const parsedUrl = parse(url);
20
- name = url.startsWith('http') ? url : url;
21
- parsedUrl.hostname.replaceAll('.', '_') +
22
- '.' +
23
- parsedUrl.path.replaceAll('.', '_').replaceAll('/', '_');
24
- }
45
+ let totalTests = 0;
46
+ let totalFailures = 0;
47
+ let suitesXml = '';
25
48
 
26
- const suite = jrp
27
- .testSuite()
28
- .name(options.budget.friendlyName || 'sitespeed.io' + '.' + name);
49
+ for (const url of urls) {
50
+ const suiteName = `${options.budget.friendlyName || 'sitespeed.io'}.${url}`;
51
+ let suiteTests = 0;
52
+ let suiteFailures = 0;
53
+ let testCasesXml = '';
29
54
 
30
- if (results.failing[url]) {
31
- for (const result of results.failing[url]) {
32
- suite
33
- .testCase()
34
- .className(name)
35
- .name(result.type + '.' + result.metric)
36
- .failure(
37
- result.metric + ' is ' + result.friendlyValue ||
38
- result.value +
39
- ' and limit ' +
40
- result.limitType +
41
- ' ' +
42
- result.friendlyLimit ||
43
- result.limit + ' ' + url
44
- );
55
+ if (failing[url]) {
56
+ for (const result of failing[url]) {
57
+ suiteTests++;
58
+ totalTests++;
59
+ suiteFailures++;
60
+ totalFailures++;
61
+ const testCaseName = `${result.type}.${result.metric}`;
62
+ const failureMessage = `${result.metric} is ${result.friendlyValue || result.value}`;
63
+ testCasesXml += ` <testcase classname="${xmlEscape(url)}" name="${xmlEscape(testCaseName)}">\n`;
64
+ testCasesXml += ` <failure message="${xmlEscape(failureMessage)}"/>\n`;
65
+ testCasesXml += ` </testcase>\n`;
45
66
  }
46
67
  }
47
68
 
48
- if (results.working[url]) {
49
- for (const result of results.working[url]) {
50
- suite
51
- .testCase()
52
- .className(name)
53
- .name(result.type + '.' + result.metric)
54
- .standardOutput(
55
- result.metric + ' is ' + result.friendlyValue ||
56
- result.value +
57
- ' and limit ' +
58
- result.limitType +
59
- ' ' +
60
- result.friendlyLimit ||
61
- result.limit + ' ' + url
62
- );
69
+ if (working[url]) {
70
+ for (const result of working[url]) {
71
+ suiteTests++;
72
+ totalTests++;
73
+ const testCaseName = `${result.type}.${result.metric}`;
74
+ const systemOutMessage = `${result.metric} is ${result.friendlyValue || result.value}`;
75
+ testCasesXml += ` <testcase classname="${xmlEscape(url)}" name="${xmlEscape(testCaseName)}">\n`;
76
+ testCasesXml += ` <system-out>${cdata(systemOutMessage)}</system-out>\n`;
77
+ testCasesXml += ` </testcase>\n`;
63
78
  }
64
79
  }
80
+
81
+ suitesXml += ` <testsuite name="${xmlEscape(suiteName)}" tests="${suiteTests}" failures="${suiteFailures}" errors="0" skipped="0">\n`;
82
+ suitesXml += testCasesXml;
83
+ suitesXml += ` </testsuite>\n`;
65
84
  }
85
+
86
+ const xml =
87
+ `<?xml version="1.0" encoding="UTF-8"?>\n` +
88
+ `<testsuites tests="${totalTests}" failures="${totalFailures}" errors="0" skipped="0">\n` +
89
+ suitesXml +
90
+ `</testsuites>\n`;
91
+
66
92
  const file = path.join(dir, 'junit.xml');
67
93
  log.info('Write junit budget to %s', path.resolve(file));
68
- jrp.writeTo(file);
94
+ fs.writeFileSync(file, xml);
69
95
  }
@@ -1,36 +1,44 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { EOL } from 'node:os';
4
- import tap from 'tape';
5
4
  import { getLogger } from '@sitespeed.io/log';
5
+
6
6
  const log = getLogger('sitespeedio.plugin.budget');
7
7
 
8
8
  export function writeTap(results, dir) {
9
9
  const file = path.join(dir, 'budget.tap');
10
10
  log.info('Write budget to %s', path.resolve(file));
11
- const tapOutput = fs.createWriteStream(file);
12
- tap.createStream().pipe(tapOutput);
13
11
 
14
- for (const resultType of Object.keys(results)) {
15
- const urls = Object.keys(results.failing);
12
+ const lines = [];
13
+ lines.push('TAP version 13');
14
+ let testCount = 0;
16
15
 
16
+ // Iterate over each result group (e.g. "passing" and "failing")
17
+ for (const resultType of Object.keys(results)) {
18
+ const group = results[resultType];
19
+ if (!group) {
20
+ continue;
21
+ }
22
+ const urls = Object.keys(group);
17
23
  for (const url of urls) {
18
- for (const result of results.failing[url]) {
19
- tap(result.type + '.' + result.metric + ' ' + url, function (t) {
20
- let extra = '';
21
- if (resultType === 'failing') {
22
- extra =
23
- ' limit ' + result.limitType + ' ' + result.friendlyLimit ||
24
- result.limit + EOL;
25
- }
26
- t.ok(
27
- resultType === 'failing' ? false : true,
28
- result.type + '.' + result.metric + ' ' + result.friendlyValue ||
29
- result.value + ' ' + extra + ' ' + url
30
- );
31
- t.end();
32
- });
24
+ for (const result of group[url]) {
25
+ testCount += 1;
26
+ const testTitle = `${result.type}.${result.metric} ${url}`;
27
+ let extra = '';
28
+ if (resultType === 'failing') {
29
+ extra = ` limit ${result.limitType} ${result.friendlyLimit || result.limit}`;
30
+ }
31
+ const valueDisplay = result.friendlyValue || result.value;
32
+
33
+ lines.push(`# ${testTitle}`);
34
+ const status = resultType === 'failing' ? 'not ok' : 'ok';
35
+ lines.push(
36
+ `${status} ${testCount} ${testTitle} ${valueDisplay}${extra ? ` ${extra}` : ''}`
37
+ );
33
38
  }
34
39
  }
35
40
  }
41
+
42
+ lines.push(`1..${testCount}`);
43
+ fs.writeFileSync(file, lines.join(EOL) + EOL);
36
44
  }
@@ -41,9 +41,7 @@ export function send(
41
41
  options,
42
42
  group,
43
43
  url,
44
- tsdbType === 'graphite'
45
- ? options.graphite.includeQueryParams
46
- : options.influxdb.includeQueryParams,
44
+ options.graphite.includeQueryParams,
47
45
  alias
48
46
  ).split('.');
49
47
 
@@ -128,15 +128,17 @@ export default class PageXrayPlugin extends SitespeedioPlugin {
128
128
  );
129
129
  }
130
130
  } else {
131
- pageSummary[0].statistics = this.pageXrayAggregator.summarizePerUrl(
132
- message.url
133
- );
134
- queue.postMessage(
135
- make('pagexray.pageSummary', pageSummary[0], {
136
- url: message.url,
137
- group // TODO get the group from the URL?
138
- })
139
- );
131
+ // Check that we actually have one tested page
132
+ if (pageSummary.length > 0) {
133
+ pageSummary[0].statistics =
134
+ this.pageXrayAggregator.summarizePerUrl(message.url);
135
+ queue.postMessage(
136
+ make('pagexray.pageSummary', pageSummary[0], {
137
+ url: message.url,
138
+ group // TODO get the group from the URL?
139
+ })
140
+ );
141
+ }
140
142
  let iteration = 1;
141
143
  for (let summary of pageSummary) {
142
144
  queue.postMessage(
@@ -140,7 +140,8 @@ export class PageXrayAggregator {
140
140
  }
141
141
  }
142
142
  summarizePerUrl(url) {
143
- return this.summarizePerObject(this.urls[url]);
143
+ // If we do not have a tested working page there's nothing to summarize
144
+ return this.urls[url] ? this.summarizePerObject(this.urls[url]) : {};
144
145
  }
145
146
  summarize() {
146
147
  if (Object.keys(this.stats).length === 0) {