ui5-test-runner 6.0.0-beta.5 → 6.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.
Files changed (130) hide show
  1. package/README.md +20 -29
  2. package/dist/Command.js +52 -0
  3. package/dist/Npm.js +154 -74
  4. package/dist/browsers/RecoverableBrowserError.js +6 -0
  5. package/dist/browsers/chromeArguments.js +5 -0
  6. package/dist/browsers/chromeOptions.js +29 -0
  7. package/dist/browsers/consoleMessage.js +23 -0
  8. package/dist/browsers/factory.js +168 -3
  9. package/dist/browsers/networkResponse.js +15 -0
  10. package/dist/browsers/playwright.js +104 -0
  11. package/dist/browsers/puppeteer.js +54 -88
  12. package/dist/browsers/seleniumWebdriver.js +124 -0
  13. package/dist/browsers/webdriverio.js +85 -0
  14. package/dist/cli.js +18 -12
  15. package/dist/configuration/CommandLine.js +46 -26
  16. package/dist/configuration/ConfigurationValidator.js +132 -18
  17. package/dist/configuration/OptionValidationError.js +50 -2
  18. package/dist/configuration/derivations.js +8 -0
  19. package/dist/configuration/options.js +380 -23
  20. package/dist/configuration/validations.js +135 -0
  21. package/dist/configuration/validators/boolean.js +1 -1
  22. package/dist/configuration/validators/enumeration.js +7 -0
  23. package/dist/configuration/validators/fsEntry.js +12 -12
  24. package/dist/configuration/validators/index.js +8 -2
  25. package/dist/configuration/validators/integer.js +7 -1
  26. package/dist/configuration/validators/json.js +23 -0
  27. package/dist/configuration/validators/libraryMapping.js +28 -0
  28. package/dist/configuration/validators/percent.js +2 -2
  29. package/dist/configuration/validators/regexp.js +2 -2
  30. package/dist/configuration/validators/secret.js +8 -0
  31. package/dist/configuration/validators/string.js +1 -1
  32. package/dist/configuration/validators/timeout.js +2 -2
  33. package/dist/configuration/validators/url.js +3 -3
  34. package/dist/end.js +58 -0
  35. package/dist/if.js +22 -0
  36. package/dist/modes/Modes.js +2 -1
  37. package/dist/modes/batch/BatchItem.js +1 -0
  38. package/dist/modes/batch/batchTask.js +159 -0
  39. package/dist/modes/batch/index.js +98 -0
  40. package/dist/modes/batch/output.js +22 -0
  41. package/dist/modes/batch/report.js +45 -0
  42. package/dist/modes/batch/resolve.js +105 -0
  43. package/dist/modes/dumpConfig.js +6 -0
  44. package/dist/modes/execute.js +6 -5
  45. package/dist/modes/help.js +86 -2
  46. package/dist/modes/log/LogReader.js +3 -2
  47. package/dist/modes/log/LogStorage.js +31 -27
  48. package/dist/modes/log/index.js +38 -6
  49. package/dist/modes/log/{REserve.js → reserve.js} +25 -2
  50. package/dist/modes/mcp/IDocument.js +2 -0
  51. package/dist/modes/mcp/frontmatter.js +142 -0
  52. package/dist/modes/mcp/index.js +49 -0
  53. package/dist/modes/mcp/knowledgeBase.js +104 -0
  54. package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
  55. package/dist/modes/mcp/reserve.js +175 -0
  56. package/dist/modes/mcp/tools/getTopic.js +46 -0
  57. package/dist/modes/mcp/tools/index.js +6 -0
  58. package/dist/modes/mcp/tools/listTopics.js +14 -0
  59. package/dist/modes/test/PageContext.js +1 -0
  60. package/dist/modes/test/Server.js +99 -0
  61. package/dist/modes/test/browser.js +31 -17
  62. package/dist/modes/test/browserConfig.js +17 -0
  63. package/dist/modes/test/coverage/collect.js +38 -0
  64. package/dist/modes/test/coverage/index.js +3 -0
  65. package/dist/modes/test/coverage/instrument.js +70 -0
  66. package/dist/modes/test/coverage/nyc.js +15 -0
  67. package/dist/modes/test/coverage/report.js +71 -0
  68. package/dist/modes/test/coverage/settings.js +53 -0
  69. package/dist/modes/test/index.js +94 -34
  70. package/dist/modes/test/pageTask.js +293 -98
  71. package/dist/modes/test/report.js +20 -3
  72. package/dist/modes/test/reserve.js +76 -0
  73. package/dist/modes/test/screenshot.js +49 -0
  74. package/dist/platform/Crypto.js +9 -0
  75. package/dist/platform/Exit.js +33 -24
  76. package/dist/platform/FileSystem.js +6 -2
  77. package/dist/platform/Host.js +9 -1
  78. package/dist/platform/Http.js +54 -29
  79. package/dist/platform/Module.js +2 -1
  80. package/dist/platform/Path.js +5 -1
  81. package/dist/platform/Process.js +67 -13
  82. package/dist/platform/Terminal.js +12 -8
  83. package/dist/platform/Thread.js +1 -1
  84. package/dist/platform/assert.js +13 -12
  85. package/dist/platform/environment.js +9 -18
  86. package/dist/platform/index.js +1 -0
  87. package/dist/platform/logger/CompressionContext.js +117 -0
  88. package/dist/platform/logger/allCompressed.js +8 -7
  89. package/dist/platform/logger/compress.js +29 -129
  90. package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
  91. package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
  92. package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
  93. package/dist/platform/logger/output/factory.js +5 -1
  94. package/dist/platform/logger/output.js +24 -6
  95. package/dist/platform/logger/toInternalLogAttributes.js +10 -11
  96. package/dist/platform/logger/traceFileName.js +1 -0
  97. package/dist/platform/logger.js +52 -21
  98. package/dist/platform/mock.js +110 -19
  99. package/dist/platform/version.js +1 -2
  100. package/dist/platform/workerBootstrap.js +1 -1
  101. package/dist/reports/initReportBuilder.js +18 -0
  102. package/dist/reports/junit.js +64 -0
  103. package/dist/reports/saveReport.js +10 -0
  104. package/dist/reports/textReport.js +92 -0
  105. package/dist/reserveLogger.js +22 -0
  106. package/dist/sendToParentProcess.js +6 -0
  107. package/dist/start.js +79 -0
  108. package/dist/types/ISecretValue.js +1 -0
  109. package/dist/types/SecretValue.js +15 -0
  110. package/dist/ui/agent.js +4 -2
  111. package/dist/ui/html-report.js +2 -2
  112. package/dist/ui/lib.js +1 -1
  113. package/dist/ui/log-viewer.js +1 -1
  114. package/dist/utils/node/Folder.js +2 -4
  115. package/dist/utils/node/FramedStreamReader.js +4 -5
  116. package/dist/utils/node/FramedStreamWriter.js +6 -5
  117. package/dist/utils/node/anonymize.js +11 -0
  118. package/dist/utils/node/timeout.js +11 -0
  119. package/dist/utils/shared/ProgressBar.js +1 -2
  120. package/dist/utils/shared/TestReportBuilder.js +11 -7
  121. package/dist/utils/shared/host.js +11 -0
  122. package/dist/utils/shared/object.js +12 -3
  123. package/dist/utils/shared/schema.js +20 -0
  124. package/dist/utils/shared/string.js +1 -0
  125. package/dist/utils/shared/toIError.js +3 -7
  126. package/dist/utils/shared/version.js +52 -0
  127. package/package.json +2 -78
  128. package/dist/configuration/validators/browser.js +0 -11
  129. package/dist/modes/test/REserve.js +0 -31
  130. package/dist/modes/test/server.js +0 -109
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # UI5 Test runner 6️⃣ β
1
+ # UI5 Test runner
2
2
 
3
3
  [![Node.js CI](https://github.com/ArnaudBuchholz/ui5-test-runner/actions/workflows/node.js.yml/badge.svg)](https://github.com/ArnaudBuchholz/ui5-test-runner/actions/workflows/node.js.yml)
4
4
  [![Package Quality](https://npm.packagequality.com/shield/ui5-test-runner.svg)](https://packagequality.com/#?package=ui5-test-runner)
@@ -6,48 +6,39 @@
6
6
  [![ui5-test-runner](https://badge.fury.io/js/ui5-test-runner.svg)](https://www.npmjs.org/package/ui5-test-runner)
7
7
  [![PackagePhobia](https://img.shields.io/badge/%F0%9F%93%A6package-phobia-lightgrey)](https://packagephobia.com/result?p=ui5-test-runner)
8
8
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
- [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield)
10
- [![Documentation](https://img.shields.io/badge/-%F0%9F%93%9Adocumentation-blueviolet)](https://arnaudbuchholz.github.io/ui5-test-runner/)
9
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield&issueType=license)
10
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield&issueType=security)
11
+ [![Documentation](https://img.shields.io/badge/-%F0%9F%93%9Adocumentation-blueviolet)](https://github.com/ArnaudBuchholz/ui5-test-runner/tree/main/docs#-documentation)
11
12
 
12
- A self-sufficient test runner for UI5 applications enabling parallel execution of tests.
13
13
 
14
- > To put it in a nutshell, some UI5 applications have so many tests that when you run them in a browser, it ends up **crashing**. The main reason is **memory consumption** : the browser process goes up to 2 GB and it blows up. JavaScript is based on garbage collecting but it needs time to operate and the stress caused by executing the tests as well as the use of iframes do not let enough bandwidth for the browser to free up the memory.
14
+ A self-sufficient test runner for [UI5 applications](https://ui5.sap.com/) enabling parallel execution of tests.
15
15
 
16
- > This tool is designed and built as a **substitute** of the [UI5 karma runner](https://github.com/SAP/karma-ui5). It executes all the tests in **parallel** thanks to several browser instances *(which also **reduces the total execution time**)*.
16
+ > To put it in a nutshell, some [UI5 applications](https://ui5.sap.com/) have so many tests that when you run them in a browser, it ends up **crashing**. The main reason is **memory consumption** : the browser process goes up to 2 GB and it blows up. JavaScript is based on garbage collecting but it needs time to operate and the stress caused by executing the tests as well as the use of iframes do not let enough bandwidth for the browser to free up the memory.
17
17
 
18
- ## 📚 [Documentation](https://arnaudbuchholz.github.io/ui5-test-runner/)
18
+ > This tool is designed and built as a **substitute** of the [UI5 karma runner](https://github.com/SAP/karma-ui5). It executes all the tests in **parallel** thanks to several browser tabs *(which also **reduces the total execution time**)*.
19
19
 
20
20
  ## 💿 How to install
21
21
 
22
- * Works with [Node.js](https://nodejs.org/en/download/) >= 24
22
+ * Works with [Node.js](https://nodejs.org/en/download/) >= 24.11.1
23
23
  * Local installation
24
- * `npm install --save-dev ui5-test-runner`
24
+ * `npm install --save-dev ui5-test-runner@6`
25
25
  * Trigger either with `npx ui5-test-runner` or through an npm script invoking `ui5-test-runner`
26
26
  * Global installation
27
- * `npm install --global ui5-test-runner`
27
+ * `npm install --global ui5-test-runner@6`
28
28
  * Trigger with `ui5-test-runner`
29
29
 
30
- **NOTE** : additional packages might be needed during the execution (`puppeteer`, `selenium-webdriver`, `nyc`...) . If they are found installed **locally** in the tested project, they are used. Otherwise, they are installed **globally**.
30
+ **NOTE** : additional packages might be needed during the execution (`puppeteer`, `selenium-webdriver`, `nyc`...) . If they are found installed **locally** in the tested project, they are used. Otherwise, they are installed **on demand**.
31
31
 
32
- ## ⚖️ License
33
- [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_large)
32
+ ## 🤖 AI-powered documentation
34
33
 
35
- ## ⚠️ Breaking changes
34
+ Use `npx ui5-test-runner@6 --mcp` to start an MCP server exposing the latest indexed documentation to your AI client.
36
35
 
37
- | Version | Reason |
38
- |-|-|
39
- | **6**.0.0 | • Drop support of Node.js < 24 |
40
- | **5**.0.0 | • Some coverage reports now includes **all** files, leading to a potential decrease of coverage |
41
- | **4**.0.0 | • Drop support of Node.js 16 |
42
- | **3**.0.0 | • Drop support of Node.js 14 |
43
- | **2**.0.0 | • Command line **parameters** as well as configuration file **syntax** have changed |
44
- || • Dependencies are installed **on demand** |
45
- || • Browser instantiation command evolved in an **incompatible way** (see [documentation](https://arnaudbuchholz.github.io/ui5-test-runner/browser.html)) |
46
- || • Output is different (report, traces) |
36
+ ## 🧭 Links
37
+
38
+ * 📚 [Documentation](https://github.com/ArnaudBuchholz/ui5-test-runner/tree/main/docs#-documentation)
39
+
40
+ * ⚠️ [Breaking changes](https://github.com/ArnaudBuchholz/ui5-test-runner/blob/main/docs/v6_breaking_changes.md)
41
+
42
+ * ✒ [Contributors](https://github.com/ArnaudBuchholz/ui5-test-runner/graphs/contributors?all=1)
47
43
 
48
- ## ✒ Contributors
49
44
 
50
- * [Marian Zeis](https://github.com/marianfoo): Documentation page revamp [PR #54](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/54)
51
- * [Raj Singh](https://github.com/rajxsingh): Basic HTTP Authentication in Puppeteer [PR #71](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/71)
52
- * [Andreas Kunz](https://github.com/akudev): Improved documentation for TypeScript testing and reviewed `nyc` settings handling [PR #110](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/110)
53
- * [Juri Zeisler](https://github.com/juriz889): Added screenshots to the JUnit XML Report [PR #158](https://github.com/ArnaudBuchholz/ui5-test-runner/pull/158)
@@ -0,0 +1,52 @@
1
+ import { assert } from './platform/index.js';
2
+ import { Npm } from './Npm.js';
3
+ const QUOTED_AND_UNQUOTED_TOKENS = /"[^"]*"|'[^']*'|[^ ]+/g;
4
+ export const Command = {
5
+ split(command) {
6
+ return command
7
+ .matchAll(QUOTED_AND_UNQUOTED_TOKENS)
8
+ .map(([value]) => {
9
+ return (value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))
10
+ ? value.slice(1, -1)
11
+ : value;
12
+ })
13
+ .toArray();
14
+ },
15
+ substitute(configuration, value, extras = {}) {
16
+ return value.replaceAll(/\{\{(\w+)\}\}/g, (_, optionName) => {
17
+ if (optionName in extras) {
18
+ return extras[optionName];
19
+ }
20
+ assert(optionName in configuration, `Invalid command line substitution parameter: ${optionName}`);
21
+ return configuration[optionName]?.toString() ?? '';
22
+ });
23
+ },
24
+ async parse(configuration, command, extras = {}) {
25
+ let executable = 'node';
26
+ const tokens = Command.split(command);
27
+ const environment = {};
28
+ while (tokens.length > 0 && /^\w+=/.test(tokens[0])) {
29
+ const [key, ...rest] = tokens.shift().split('=');
30
+ environment[key] = Command.substitute(configuration, rest.join('='), extras);
31
+ }
32
+ const [commandSpecifier, ...parameters] = tokens;
33
+ assert(!!commandSpecifier);
34
+ if (commandSpecifier === 'npm') {
35
+ parameters.unshift(await Npm.getCliPath());
36
+ }
37
+ else if (commandSpecifier !== 'node') {
38
+ const scripts = await Npm.listPackageScriptNames(configuration.cwd);
39
+ if (scripts.includes(commandSpecifier)) {
40
+ parameters.unshift(await Npm.getCliPath(), 'run', commandSpecifier);
41
+ }
42
+ else {
43
+ executable = commandSpecifier;
44
+ }
45
+ }
46
+ return [
47
+ executable,
48
+ parameters.map((parameter) => Command.substitute(configuration, parameter, extras)),
49
+ environment
50
+ ];
51
+ }
52
+ };
package/dist/Npm.js CHANGED
@@ -1,21 +1,14 @@
1
- import { logger, FileSystem, Http, Module, Path, Process, Url } from './platform/index.js';
1
+ import { assert, logger, FileSystem, Http, Module, Path, Process, Url } from './platform/index.js';
2
2
  import { memoize } from './utils/shared/memoize.js';
3
3
  export const getNpmCliPath = async () => {
4
4
  const npmChildProcess = Process.spawn('npm', [], {
5
5
  shell: true
6
6
  });
7
7
  await npmChildProcess.closed;
8
- const error = new Error('Unable to initialize NPM');
9
8
  const match = /^npm@([^ ]+) (.*)$/gm.exec(npmChildProcess.stdout);
10
- if (!match) {
11
- logger.fatal({ source: 'npm', message: 'Unable to match NPM output', error });
12
- throw error;
13
- }
9
+ assert(match !== null, 'unable to match NPM output');
14
10
  const [, semver, path] = match;
15
- if (!semver || !path) {
16
- logger.fatal({ source: 'npm', message: 'Failed to parse NPM output', error, data: { semver, path } });
17
- throw error;
18
- }
11
+ assert(!!semver && !!path, 'failed to parse NPM output');
19
12
  logger.debug({ source: 'npm', message: `npm@${semver} ${path}` });
20
13
  return Path.join(path, 'bin/npm-cli.js');
21
14
  };
@@ -26,76 +19,87 @@ const npm = async (...arguments_) => {
26
19
  detached: true
27
20
  });
28
21
  };
22
+ const validateRoot = async (proc, label) => {
23
+ if (proc.code !== 0) {
24
+ logger.fatal({ source: 'npm', message: `npm root ${label} failed with code ${proc.code}` });
25
+ }
26
+ const value = proc.stdout.trim();
27
+ if (value === '' || !Path.isAbsolute(value)) {
28
+ logger.fatal({ source: 'npm', message: `npm root ${label} returned an invalid path: "${value}"` });
29
+ }
30
+ try {
31
+ await FileSystem.access(value, FileSystem.constants.R_OK);
32
+ }
33
+ catch {
34
+ logger.fatal({ source: 'npm', message: `npm root ${label} directory is not accessible: "${value}"` });
35
+ }
36
+ return value;
37
+ };
29
38
  const getRoots = memoize(async () => {
30
39
  const localRootProcess = await npm('root');
31
40
  const globalRootProcess = await npm('root', '--global');
32
41
  await Promise.all([localRootProcess.closed, globalRootProcess.closed]);
33
- const local = localRootProcess.stdout.trim();
34
- const global = globalRootProcess.stdout.trim();
35
- logger.debug({ source: 'npm', message: 'Roots', data: { local, global } });
36
- return {
37
- local: localRootProcess.stdout.trim(),
38
- global: globalRootProcess.stdout.trim()
39
- };
42
+ const local = await validateRoot(localRootProcess, '(local)');
43
+ const global = await validateRoot(globalRootProcess, '(global)');
44
+ logger.debug({ source: 'npm', message: 'roots', data: { local, global } });
45
+ return { local, global };
40
46
  });
41
- const buildInstallPlan = (strategy, moduleName, globalRoot, prefix) => {
47
+ const buildInstallPlan = (strategy, moduleName, options) => {
48
+ const { globalRoot, prefix, allowScripts, minReleaseAge } = options;
49
+ const noScripts = allowScripts ? [] : ['--ignore-scripts'];
50
+ const releaseAge = minReleaseAge > 0 ? [`--min-release-age=${minReleaseAge}`] : [];
51
+ const extraFlags = [...noScripts, ...releaseAge];
42
52
  if (strategy === 'global') {
43
- return { installArguments: ['install', '-g', moduleName], reimportPath: globalRoot };
53
+ return { installArguments: ['install', '-g', ...extraFlags, moduleName], reimportPath: globalRoot };
44
54
  }
45
- if (strategy === 'prefix') {
46
- return {
47
- installArguments: ['install', '--prefix', prefix, '--no-save', moduleName],
55
+ return strategy === 'prefix'
56
+ ? {
57
+ installArguments: ['install', '--prefix', prefix, '--no-save', ...extraFlags, moduleName],
48
58
  reimportPath: Path.join(prefix, 'node_modules')
49
- };
50
- }
51
- return { installArguments: ['install', '--no-save', moduleName], reimportPath: undefined };
59
+ }
60
+ : { installArguments: ['install', '--no-save', ...extraFlags, moduleName], reimportPath: undefined };
52
61
  };
53
62
  export class Npm {
54
63
  static dynamicImport(specifier) {
55
64
  return import(specifier);
56
65
  }
57
- static async tryImportFromPath(configuration, moduleName, nodeModulesPath) {
66
+ static tryResolvePackageDir(moduleName, nodeModulesPath) {
58
67
  try {
59
- const require = Module.createRequire(Url.pathToFileURL(Path.join(configuration.cwd, 'package.json')).href);
60
- const resolved = require.resolve(moduleName, { paths: [nodeModulesPath] });
61
- return await this.dynamicImport(Url.pathToFileURL(resolved).href);
68
+ const packageJsonPath = Module.findPackageJSON(moduleName, `${nodeModulesPath}/`);
69
+ return packageJsonPath === undefined ? undefined : Path.dirname(packageJsonPath);
62
70
  }
63
71
  catch {
64
72
  return undefined;
65
73
  }
66
74
  }
67
- static async getLatestVersion(moduleName) {
75
+ static async tryImportFromPath(configuration, moduleName, nodeModulesPath) {
68
76
  try {
69
- const response = await Http.get(`https://registry.npmjs.org/${moduleName}/latest`);
70
- const { version } = JSON.parse(response);
71
- return version;
77
+ const require = Module.createRequire(`${Url.pathToFileURL(configuration.cwd).href}/`);
78
+ const resolved = require.resolve(moduleName, { paths: [nodeModulesPath] });
79
+ return await this.dynamicImport(Url.pathToFileURL(resolved).href);
72
80
  }
73
- catch (error) {
74
- throw new Error(`Unable to fetch latest version of ${moduleName} from NPM registry`, {
75
- cause: error
76
- });
81
+ catch {
82
+ return undefined;
77
83
  }
78
84
  }
79
- static async checkIfLatestVersion(moduleName, isLocal) {
80
- try {
81
- const { local, global } = await getRoots();
82
- const { version: installedVersion } = JSON.parse(await FileSystem.readFile(Path.join(isLocal ? local : global, moduleName, 'package.json'), 'utf8'));
83
- logger.info({ source: 'npm', message: `Installed version of ${moduleName} is ${installedVersion}` });
84
- const latestVersion = await Npm.getLatestVersion(moduleName);
85
- if (latestVersion !== installedVersion) {
86
- logger.warn({ source: 'npm', message: `[PKGVRS] Latest version of ${moduleName} is ${latestVersion}` });
85
+ static async tryImportFromRoots(configuration, moduleName, roots) {
86
+ for (const { path, label, checkVersion } of roots) {
87
+ const result = await this.tryImportFromPath(configuration, moduleName, path);
88
+ if (result !== undefined) {
89
+ logger.debug({ source: 'npm', message: `Module ${moduleName} ${label}` });
90
+ if (checkVersion)
91
+ void this.checkIfLatestVersion(configuration, moduleName);
92
+ return result;
87
93
  }
88
94
  }
89
- catch (error) {
90
- logger.error({ source: 'npm', message: 'Failed in checkIfLatestVersion', error });
91
- }
95
+ return undefined;
92
96
  }
93
97
  static async import(configuration, moduleName) {
94
- logger.debug({ source: 'npm', message: `Npm.import(${moduleName})` });
98
+ logger.debug({ source: 'npm', message: `Importing module: ${moduleName}` });
95
99
  try {
96
100
  const module = await this.dynamicImport(moduleName);
97
101
  logger.debug({ source: 'npm', message: `Module ${moduleName} found locally` });
98
- void Npm.checkIfLatestVersion(moduleName, true);
102
+ void this.checkIfLatestVersion(configuration, moduleName);
99
103
  return module;
100
104
  }
101
105
  catch (error) {
@@ -103,37 +107,38 @@ export class Npm {
103
107
  if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
104
108
  throw error;
105
109
  }
106
- logger.warn({ source: 'npm', message: `Module ${moduleName} not found locally` });
107
- }
108
- const { global: globalRoot } = await getRoots();
109
- const fromGlobal = await this.tryImportFromPath(configuration, moduleName, globalRoot);
110
- if (fromGlobal !== undefined) {
111
- logger.debug({ source: 'npm', message: `Module ${moduleName} found globally` });
112
- void Npm.checkIfLatestVersion(moduleName, false);
113
- return fromGlobal;
114
- }
115
- if (configuration.alternateNpmPath) {
116
- const fromAlternate = await this.tryImportFromPath(configuration, moduleName, configuration.alternateNpmPath);
117
- if (fromAlternate !== undefined) {
118
- logger.debug({ source: 'npm', message: `Module ${moduleName} found in alternateNpmPath` });
119
- return fromAlternate;
120
- }
121
- }
122
- if (configuration.npmInstallPrefix) {
123
- const fromPrefix = await this.tryImportFromPath(configuration, moduleName, Path.join(configuration.npmInstallPrefix, 'node_modules'));
124
- if (fromPrefix !== undefined) {
125
- logger.debug({ source: 'npm', message: `Module ${moduleName} found in npmInstallPrefix` });
126
- return fromPrefix;
110
+ if (moduleName === this.packageNameOf(moduleName)) {
111
+ logger.warn({ source: 'npm', message: `Module ${moduleName} not found locally` });
127
112
  }
128
113
  }
114
+ const { local: localRoot, global: globalRoot } = await getRoots();
115
+ const extraRoots = [
116
+ ...(configuration.alternateNpmPath
117
+ ? [{ path: configuration.alternateNpmPath, label: 'found in alternateNpmPath' }]
118
+ : []),
119
+ ...(configuration.npmInstallPrefix
120
+ ? [{ path: Path.join(configuration.npmInstallPrefix, 'node_modules'), label: 'found in npmInstallPrefix' }]
121
+ : [])
122
+ ];
123
+ const found = await this.tryImportFromRoots(configuration, moduleName, [
124
+ { path: localRoot, label: 'found in local node_modules', checkVersion: true },
125
+ { path: globalRoot, label: 'found globally', checkVersion: true },
126
+ ...extraRoots
127
+ ]);
128
+ if (found !== undefined)
129
+ return found;
129
130
  if (configuration.noNpmInstall) {
130
131
  const message = `Module ${moduleName} not found and noNpmInstall is set`;
131
132
  logger.fatal({ source: 'npm', message });
132
- throw new Error(message);
133
133
  }
134
134
  const strategy = configuration.npmInstall;
135
135
  logger.info({ source: 'npm', message: `Installing ${moduleName} (strategy: ${strategy})` });
136
- const { installArguments, reimportPath } = buildInstallPlan(strategy, moduleName, globalRoot, configuration.npmInstallPrefix ?? '');
136
+ const { installArguments, reimportPath } = buildInstallPlan(strategy, moduleName, {
137
+ globalRoot,
138
+ prefix: configuration.npmInstallPrefix ?? '',
139
+ allowScripts: !!configuration.npmAllowInstallScripts,
140
+ minReleaseAge: configuration.npmInstallMinReleaseAge
141
+ });
137
142
  const installProcess = await npm(...installArguments);
138
143
  await installProcess.closed;
139
144
  if (reimportPath !== undefined) {
@@ -141,10 +146,85 @@ export class Npm {
141
146
  if (result === undefined) {
142
147
  const message = `Module ${moduleName} could not be loaded after install`;
143
148
  logger.fatal({ source: 'npm', message });
144
- throw new Error(message);
145
149
  }
146
150
  return result;
147
151
  }
148
152
  return await this.dynamicImport(moduleName);
149
153
  }
154
+ static async resolvePackageDir(configuration, moduleName) {
155
+ const { local, global: globalRoot } = await getRoots();
156
+ const fromLocal = this.tryResolvePackageDir(moduleName, local);
157
+ if (fromLocal)
158
+ return fromLocal;
159
+ const fromGlobal = this.tryResolvePackageDir(moduleName, globalRoot);
160
+ if (fromGlobal)
161
+ return fromGlobal;
162
+ if (configuration.alternateNpmPath) {
163
+ const fromAlternate = this.tryResolvePackageDir(moduleName, configuration.alternateNpmPath);
164
+ if (fromAlternate)
165
+ return fromAlternate;
166
+ }
167
+ if (configuration.npmInstallPrefix) {
168
+ const fromPrefix = this.tryResolvePackageDir(moduleName, Path.join(configuration.npmInstallPrefix, 'node_modules'));
169
+ if (fromPrefix)
170
+ return fromPrefix;
171
+ }
172
+ throw new Error(`Cannot locate package directory for ${moduleName}`);
173
+ }
174
+ static packageNameOf(moduleName) {
175
+ if (moduleName.startsWith('@')) {
176
+ const [scope, package_] = moduleName.split('/', 2);
177
+ return `${scope}/${package_}`;
178
+ }
179
+ return moduleName.split('/', 1)[0];
180
+ }
181
+ static async getLatestVersion(packageName) {
182
+ try {
183
+ const response = await Http.getAsText(`https://registry.npmjs.org/${packageName}/latest`);
184
+ const { version } = JSON.parse(response);
185
+ return version;
186
+ }
187
+ catch (error) {
188
+ throw new Error(`Unable to fetch latest version of ${packageName} from NPM registry`, {
189
+ cause: error
190
+ });
191
+ }
192
+ }
193
+ static _checkedPackages = new Set();
194
+ static async checkIfLatestVersion(configuration, moduleName) {
195
+ const packageName = this.packageNameOf(moduleName);
196
+ if (this._checkedPackages.has(packageName)) {
197
+ return;
198
+ }
199
+ this._checkedPackages.add(packageName);
200
+ try {
201
+ const packageDirectory = await this.resolvePackageDir(configuration, packageName);
202
+ const { version: installedVersion } = JSON.parse(await FileSystem.readFile(Path.join(packageDirectory, 'package.json'), 'utf8'));
203
+ logger.info({ source: 'npm', message: `Installed version of ${packageName} is ${installedVersion}` });
204
+ const latestVersion = await this.getLatestVersion(packageName);
205
+ if (latestVersion !== installedVersion) {
206
+ logger.warn({ source: 'npm', message: `Latest version of ${packageName} is ${latestVersion}` });
207
+ }
208
+ }
209
+ catch (error) {
210
+ logger.error({ source: 'npm', message: 'failed in checkIfLatestVersion', error });
211
+ }
212
+ }
213
+ static async getCliPath() {
214
+ return await memoizedNpmCliPath();
215
+ }
216
+ static async listPackageScriptNames(cwd) {
217
+ try {
218
+ const packageJsonPath = Module.findPackageJSON(`${Url.pathToFileURL(cwd).href}/`);
219
+ if (!packageJsonPath) {
220
+ return [];
221
+ }
222
+ const content = JSON.parse(await FileSystem.readFile(packageJsonPath, 'utf8'));
223
+ return Object.keys(content?.scripts ?? {});
224
+ }
225
+ catch (error) {
226
+ logger.error({ source: 'npm', message: 'failed to list package script names', error, data: { cwd } });
227
+ return [];
228
+ }
229
+ }
150
230
  }
@@ -0,0 +1,6 @@
1
+ export class RecoverableBrowserError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'RecoverableBrowserError';
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ import { Host } from '../platform/index.js';
2
+ export const getExtraChromeArguments = () => {
3
+ const raw = Host.env['UI5TR_CHROME_ARGS'];
4
+ return raw === undefined ? [] : raw.split(/\s+/).filter((argument) => argument.length > 0);
5
+ };
@@ -0,0 +1,29 @@
1
+ import { getExtraChromeArguments } from './chromeArguments.js';
2
+ export const DEFAULT_CHROME_ARGS = [
3
+ '--disable-background-timer-throttling',
4
+ '--disable-backgrounding-occluded-windows',
5
+ '--disable-renderer-backgrounding'
6
+ ];
7
+ export const buildChromeLaunchOptions = (options, extraArguments = [], shouldIncludeEnvironmentArguments = true) => {
8
+ const rawArguments = options?.['args'];
9
+ const userArguments = Array.isArray(rawArguments)
10
+ ? rawArguments.filter((a) => typeof a === 'string')
11
+ : [];
12
+ const rawIgnoreDefaultArguments = options?.['ignoreDefaultArgs'];
13
+ let ignoreDefaultArguments;
14
+ if (rawIgnoreDefaultArguments === true) {
15
+ ignoreDefaultArguments = true;
16
+ }
17
+ else if (Array.isArray(rawIgnoreDefaultArguments)) {
18
+ ignoreDefaultArguments = rawIgnoreDefaultArguments.filter((argument) => typeof argument === 'string');
19
+ }
20
+ return {
21
+ args: [
22
+ ...DEFAULT_CHROME_ARGS,
23
+ ...extraArguments,
24
+ ...userArguments,
25
+ ...(shouldIncludeEnvironmentArguments ? getExtraChromeArguments() : [])
26
+ ],
27
+ ...(ignoreDefaultArguments !== undefined && { ignoreDefaultArgs: ignoreDefaultArguments })
28
+ };
29
+ };
@@ -0,0 +1,23 @@
1
+ import { logger } from '../platform/index.js';
2
+ import { agentLogPrefix } from '../types/AgentState.js';
3
+ const LOG_TYPES = {
4
+ error: 'error',
5
+ warn: 'warn',
6
+ assert: 'warn',
7
+ debug: 'debug'
8
+ };
9
+ export const handleConsoleMessage = (type, text, pageId) => {
10
+ const normalizedType = type === 'warning' ? 'warn' : type;
11
+ const logType = LOG_TYPES[normalizedType] ?? 'info';
12
+ let source;
13
+ let message;
14
+ if (text.startsWith(agentLogPrefix)) {
15
+ source = 'browser/agent';
16
+ message = text.slice(agentLogPrefix.length);
17
+ }
18
+ else {
19
+ source = 'browser/console';
20
+ message = text;
21
+ }
22
+ logger[logType]({ source, message, pageId, data: { type: normalizedType } });
23
+ };