ui5-test-runner 6.0.0-beta.4 → 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.
- package/README.md +20 -29
- package/dist/Command.js +52 -0
- package/dist/Npm.js +193 -43
- package/dist/browsers/RecoverableBrowserError.js +6 -0
- package/dist/browsers/chromeArguments.js +5 -0
- package/dist/browsers/chromeOptions.js +29 -0
- package/dist/browsers/consoleMessage.js +23 -0
- package/dist/browsers/factory.js +169 -4
- package/dist/browsers/networkResponse.js +15 -0
- package/dist/browsers/playwright.js +104 -0
- package/dist/browsers/puppeteer.js +55 -89
- package/dist/browsers/seleniumWebdriver.js +124 -0
- package/dist/browsers/webdriverio.js +85 -0
- package/dist/cli.js +18 -12
- package/dist/configuration/CommandLine.js +46 -26
- package/dist/configuration/ConfigurationValidator.js +132 -18
- package/dist/configuration/OptionValidationError.js +50 -2
- package/dist/configuration/derivations.js +8 -0
- package/dist/configuration/options.js +392 -23
- package/dist/configuration/validations.js +135 -0
- package/dist/configuration/validators/boolean.js +1 -1
- package/dist/configuration/validators/enumeration.js +7 -0
- package/dist/configuration/validators/fsEntry.js +12 -12
- package/dist/configuration/validators/index.js +8 -2
- package/dist/configuration/validators/integer.js +7 -1
- package/dist/configuration/validators/json.js +23 -0
- package/dist/configuration/validators/libraryMapping.js +28 -0
- package/dist/configuration/validators/percent.js +2 -2
- package/dist/configuration/validators/regexp.js +2 -2
- package/dist/configuration/validators/secret.js +8 -0
- package/dist/configuration/validators/string.js +1 -1
- package/dist/configuration/validators/timeout.js +2 -2
- package/dist/configuration/validators/url.js +3 -3
- package/dist/end.js +58 -0
- package/dist/if.js +22 -0
- package/dist/modes/Modes.js +2 -1
- package/dist/modes/batch/BatchItem.js +1 -0
- package/dist/modes/batch/batchTask.js +159 -0
- package/dist/modes/batch/index.js +98 -0
- package/dist/modes/batch/output.js +22 -0
- package/dist/modes/batch/report.js +45 -0
- package/dist/modes/batch/resolve.js +105 -0
- package/dist/modes/dumpConfig.js +6 -0
- package/dist/modes/execute.js +6 -5
- package/dist/modes/help.js +86 -2
- package/dist/modes/log/LogReader.js +3 -2
- package/dist/modes/log/LogStorage.js +31 -27
- package/dist/modes/log/index.js +39 -7
- package/dist/modes/log/{REserve.js → reserve.js} +25 -2
- package/dist/modes/mcp/IDocument.js +2 -0
- package/dist/modes/mcp/frontmatter.js +142 -0
- package/dist/modes/mcp/index.js +49 -0
- package/dist/modes/mcp/knowledgeBase.js +104 -0
- package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
- package/dist/modes/mcp/reserve.js +175 -0
- package/dist/modes/mcp/tools/getTopic.js +46 -0
- package/dist/modes/mcp/tools/index.js +6 -0
- package/dist/modes/mcp/tools/listTopics.js +14 -0
- package/dist/modes/test/PageContext.js +1 -0
- package/dist/modes/test/Server.js +99 -0
- package/dist/modes/test/browser.js +31 -17
- package/dist/modes/test/browserConfig.js +17 -0
- package/dist/modes/test/coverage/collect.js +38 -0
- package/dist/modes/test/coverage/index.js +3 -0
- package/dist/modes/test/coverage/instrument.js +70 -0
- package/dist/modes/test/coverage/nyc.js +15 -0
- package/dist/modes/test/coverage/report.js +71 -0
- package/dist/modes/test/coverage/settings.js +53 -0
- package/dist/modes/test/index.js +94 -34
- package/dist/modes/test/pageTask.js +293 -98
- package/dist/modes/test/report.js +20 -3
- package/dist/modes/test/reserve.js +76 -0
- package/dist/modes/test/screenshot.js +49 -0
- package/dist/platform/Crypto.js +9 -0
- package/dist/platform/Exit.js +33 -24
- package/dist/platform/FileSystem.js +6 -2
- package/dist/platform/Host.js +9 -1
- package/dist/platform/Http.js +54 -29
- package/dist/platform/Module.js +5 -0
- package/dist/platform/Path.js +5 -1
- package/dist/platform/Process.js +67 -13
- package/dist/platform/Terminal.js +12 -8
- package/dist/platform/Thread.js +1 -1
- package/dist/platform/Url.js +4 -0
- package/dist/platform/assert.js +13 -12
- package/dist/platform/environment.js +9 -18
- package/dist/platform/index.js +3 -0
- package/dist/platform/logger/CompressionContext.js +117 -0
- package/dist/platform/logger/allCompressed.js +8 -7
- package/dist/platform/logger/compress.js +29 -129
- package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
- package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
- package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
- package/dist/platform/logger/output/factory.js +5 -1
- package/dist/platform/logger/output.js +24 -6
- package/dist/platform/logger/toInternalLogAttributes.js +10 -11
- package/dist/platform/logger/traceFileName.js +1 -0
- package/dist/platform/logger.js +52 -21
- package/dist/platform/mock.js +124 -27
- package/dist/platform/version.js +1 -2
- package/dist/platform/workerBootstrap.js +1 -1
- package/dist/reports/initReportBuilder.js +18 -0
- package/dist/reports/junit.js +64 -0
- package/dist/reports/saveReport.js +10 -0
- package/dist/reports/textReport.js +92 -0
- package/dist/reserveLogger.js +22 -0
- package/dist/sendToParentProcess.js +6 -0
- package/dist/start.js +79 -0
- package/dist/types/ISecretValue.js +1 -0
- package/dist/types/SecretValue.js +15 -0
- package/dist/ui/agent.js +4 -2
- package/dist/ui/html-report.js +2 -2
- package/dist/ui/lib.js +1 -1
- package/dist/ui/log-viewer.js +1 -1
- package/dist/utils/node/Folder.js +2 -4
- package/dist/utils/node/FramedStreamReader.js +4 -5
- package/dist/utils/node/FramedStreamWriter.js +6 -5
- package/dist/utils/node/anonymize.js +11 -0
- package/dist/utils/node/timeout.js +11 -0
- package/dist/utils/shared/ProgressBar.js +1 -2
- package/dist/utils/shared/TestReportBuilder.js +11 -7
- package/dist/utils/shared/host.js +11 -0
- package/dist/utils/shared/object.js +12 -3
- package/dist/utils/shared/schema.js +20 -0
- package/dist/utils/shared/string.js +1 -0
- package/dist/utils/shared/toIError.js +3 -7
- package/dist/utils/shared/version.js +52 -0
- package/package.json +2 -78
- package/dist/configuration/validators/browser.js +0 -11
- package/dist/modes/test/REserve.js +0 -31
- package/dist/modes/test/server.js +0 -109
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# UI5 Test runner
|
|
1
|
+
# UI5 Test runner
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ArnaudBuchholz/ui5-test-runner/actions/workflows/node.js.yml)
|
|
4
4
|
[](https://packagequality.com/#?package=ui5-test-runner)
|
|
@@ -6,48 +6,39 @@
|
|
|
6
6
|
[](https://www.npmjs.org/package/ui5-test-runner)
|
|
7
7
|
[](https://packagephobia.com/result?p=ui5-test-runner)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield)
|
|
10
|
-
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield&issueType=license)
|
|
10
|
+
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_shield&issueType=security)
|
|
11
|
+
[](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
|
-
|
|
14
|
+
A self-sufficient test runner for [UI5 applications](https://ui5.sap.com/) enabling parallel execution of tests.
|
|
15
15
|
|
|
16
|
-
>
|
|
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
|
-
|
|
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 **
|
|
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
|
-
##
|
|
33
|
-
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_large)
|
|
32
|
+
## 🤖 AI-powered documentation
|
|
34
33
|
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
package/dist/Command.js
ADDED
|
@@ -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,80 +1,230 @@
|
|
|
1
|
-
import { logger, FileSystem, Http, Path, Process } 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
|
-
const getNpmCliPath =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
15
|
+
const memoizedNpmCliPath = memoize(getNpmCliPath);
|
|
22
16
|
const npm = async (...arguments_) => {
|
|
23
|
-
const npmCliPath = await
|
|
17
|
+
const npmCliPath = await memoizedNpmCliPath();
|
|
24
18
|
return Process.spawn('node', [npmCliPath, ...arguments_], {
|
|
25
19
|
detached: true
|
|
26
20
|
});
|
|
27
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
|
+
};
|
|
28
38
|
const getRoots = memoize(async () => {
|
|
29
39
|
const localRootProcess = await npm('root');
|
|
30
40
|
const globalRootProcess = await npm('root', '--global');
|
|
31
41
|
await Promise.all([localRootProcess.closed, globalRootProcess.closed]);
|
|
32
|
-
const local = localRootProcess
|
|
33
|
-
const global = globalRootProcess
|
|
34
|
-
logger.debug({ source: 'npm', message: '
|
|
35
|
-
return {
|
|
36
|
-
local: localRootProcess.stdout.trim(),
|
|
37
|
-
global: globalRootProcess.stdout.trim()
|
|
38
|
-
};
|
|
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 };
|
|
39
46
|
});
|
|
40
|
-
|
|
41
|
-
|
|
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];
|
|
52
|
+
if (strategy === 'global') {
|
|
53
|
+
return { installArguments: ['install', '-g', ...extraFlags, moduleName], reimportPath: globalRoot };
|
|
54
|
+
}
|
|
55
|
+
return strategy === 'prefix'
|
|
56
|
+
? {
|
|
57
|
+
installArguments: ['install', '--prefix', prefix, '--no-save', ...extraFlags, moduleName],
|
|
58
|
+
reimportPath: Path.join(prefix, 'node_modules')
|
|
59
|
+
}
|
|
60
|
+
: { installArguments: ['install', '--no-save', ...extraFlags, moduleName], reimportPath: undefined };
|
|
61
|
+
};
|
|
62
|
+
export class Npm {
|
|
63
|
+
static dynamicImport(specifier) {
|
|
64
|
+
return import(specifier);
|
|
65
|
+
}
|
|
66
|
+
static tryResolvePackageDir(moduleName, nodeModulesPath) {
|
|
42
67
|
try {
|
|
43
|
-
const
|
|
68
|
+
const packageJsonPath = Module.findPackageJSON(moduleName, `${nodeModulesPath}/`);
|
|
69
|
+
return packageJsonPath === undefined ? undefined : Path.dirname(packageJsonPath);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
static async tryImportFromPath(configuration, moduleName, nodeModulesPath) {
|
|
76
|
+
try {
|
|
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);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
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;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
static async import(configuration, moduleName) {
|
|
98
|
+
logger.debug({ source: 'npm', message: `Importing module: ${moduleName}` });
|
|
99
|
+
try {
|
|
100
|
+
const module = await this.dynamicImport(moduleName);
|
|
101
|
+
logger.debug({ source: 'npm', message: `Module ${moduleName} found locally` });
|
|
102
|
+
void this.checkIfLatestVersion(configuration, moduleName);
|
|
103
|
+
return module;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const code = error.code;
|
|
107
|
+
if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
if (moduleName === this.packageNameOf(moduleName)) {
|
|
111
|
+
logger.warn({ source: 'npm', message: `Module ${moduleName} not found locally` });
|
|
112
|
+
}
|
|
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;
|
|
130
|
+
if (configuration.noNpmInstall) {
|
|
131
|
+
const message = `Module ${moduleName} not found and noNpmInstall is set`;
|
|
132
|
+
logger.fatal({ source: 'npm', message });
|
|
133
|
+
}
|
|
134
|
+
const strategy = configuration.npmInstall;
|
|
135
|
+
logger.info({ source: 'npm', message: `Installing ${moduleName} (strategy: ${strategy})` });
|
|
136
|
+
const { installArguments, reimportPath } = buildInstallPlan(strategy, moduleName, {
|
|
137
|
+
globalRoot,
|
|
138
|
+
prefix: configuration.npmInstallPrefix ?? '',
|
|
139
|
+
allowScripts: !!configuration.npmAllowInstallScripts,
|
|
140
|
+
minReleaseAge: configuration.npmInstallMinReleaseAge
|
|
141
|
+
});
|
|
142
|
+
const installProcess = await npm(...installArguments);
|
|
143
|
+
await installProcess.closed;
|
|
144
|
+
if (reimportPath !== undefined) {
|
|
145
|
+
const result = await this.tryImportFromPath(configuration, moduleName, reimportPath);
|
|
146
|
+
if (result === undefined) {
|
|
147
|
+
const message = `Module ${moduleName} could not be loaded after install`;
|
|
148
|
+
logger.fatal({ source: 'npm', message });
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
return await this.dynamicImport(moduleName);
|
|
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`);
|
|
44
184
|
const { version } = JSON.parse(response);
|
|
45
185
|
return version;
|
|
46
186
|
}
|
|
47
187
|
catch (error) {
|
|
48
|
-
throw new Error(`Unable to fetch latest version of ${
|
|
188
|
+
throw new Error(`Unable to fetch latest version of ${packageName} from NPM registry`, {
|
|
49
189
|
cause: error
|
|
50
190
|
});
|
|
51
191
|
}
|
|
52
|
-
}
|
|
53
|
-
|
|
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);
|
|
54
200
|
try {
|
|
55
|
-
const
|
|
56
|
-
const { version: installedVersion } = JSON.parse(await FileSystem.readFile(Path.join(
|
|
57
|
-
logger.info({ source: 'npm', message: `Installed version of ${
|
|
58
|
-
const latestVersion = await
|
|
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);
|
|
59
205
|
if (latestVersion !== installedVersion) {
|
|
60
|
-
logger.warn({ source: 'npm', message: `
|
|
206
|
+
logger.warn({ source: 'npm', message: `Latest version of ${packageName} is ${latestVersion}` });
|
|
61
207
|
}
|
|
62
208
|
}
|
|
63
209
|
catch (error) {
|
|
64
|
-
logger.error({ source: 'npm', message: '
|
|
210
|
+
logger.error({ source: 'npm', message: 'failed in checkIfLatestVersion', error });
|
|
65
211
|
}
|
|
66
|
-
}
|
|
67
|
-
async
|
|
68
|
-
|
|
212
|
+
}
|
|
213
|
+
static async getCliPath() {
|
|
214
|
+
return await memoizedNpmCliPath();
|
|
215
|
+
}
|
|
216
|
+
static async listPackageScriptNames(cwd) {
|
|
69
217
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 ?? {});
|
|
74
224
|
}
|
|
75
|
-
catch {
|
|
76
|
-
logger.
|
|
225
|
+
catch (error) {
|
|
226
|
+
logger.error({ source: 'npm', message: 'failed to list package script names', error, data: { cwd } });
|
|
227
|
+
return [];
|
|
77
228
|
}
|
|
78
|
-
throw new Error('Not implemented');
|
|
79
229
|
}
|
|
80
|
-
}
|
|
230
|
+
}
|
|
@@ -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
|
+
};
|
package/dist/browsers/factory.js
CHANGED
|
@@ -1,9 +1,174 @@
|
|
|
1
|
-
import { factory as puppeteerFactory } from './puppeteer.js';
|
|
1
|
+
import { factory as puppeteerFactory, descriptor as puppeteerDescriptor } from './puppeteer.js';
|
|
2
|
+
import { factory as playwrightFactory, descriptor as playwrightDescriptor } from './playwright.js';
|
|
3
|
+
import { factory as webdriverioFactory, descriptor as webdriverioDescriptor } from './webdriverio.js';
|
|
4
|
+
import { factory as seleniumWebdriverFactory, descriptor as seleniumWebdriverDescriptor } from './seleniumWebdriver.js';
|
|
5
|
+
import { Exit, logger } from '../platform/index.js';
|
|
6
|
+
import { RecoverableBrowserError } from './RecoverableBrowserError.js';
|
|
7
|
+
const DRIVERS = ['puppeteer', 'playwright', 'webdriverio', 'selenium-webdriver'];
|
|
8
|
+
export const isDriver = (value) => DRIVERS.includes(value);
|
|
2
9
|
const factories = {
|
|
3
|
-
puppeteer: puppeteerFactory
|
|
10
|
+
puppeteer: puppeteerFactory,
|
|
11
|
+
playwright: playwrightFactory,
|
|
12
|
+
webdriverio: webdriverioFactory,
|
|
13
|
+
'selenium-webdriver': seleniumWebdriverFactory
|
|
14
|
+
};
|
|
15
|
+
const descriptors = {
|
|
16
|
+
puppeteer: puppeteerDescriptor,
|
|
17
|
+
playwright: playwrightDescriptor,
|
|
18
|
+
webdriverio: webdriverioDescriptor,
|
|
19
|
+
'selenium-webdriver': seleniumWebdriverDescriptor
|
|
20
|
+
};
|
|
21
|
+
export const getDescriptor = (driver) => descriptors[driver];
|
|
22
|
+
let _instanceCount = 0;
|
|
23
|
+
const _activeInstances = new Set();
|
|
24
|
+
const shutdown = async (browser, inner) => {
|
|
25
|
+
logger.debug({ source: browser, message: 'shutdown' });
|
|
26
|
+
try {
|
|
27
|
+
await inner.shutdown();
|
|
28
|
+
logger.debug({ source: browser, message: 'shutdown completed' });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.warn({
|
|
32
|
+
source: browser,
|
|
33
|
+
message: 'shutdown failed',
|
|
34
|
+
error
|
|
35
|
+
});
|
|
36
|
+
}
|
|
4
37
|
};
|
|
5
38
|
export const BrowserFactory = {
|
|
6
|
-
async build(browser) {
|
|
7
|
-
|
|
39
|
+
async build(configuration, browser) {
|
|
40
|
+
if (_activeInstances.has(browser)) {
|
|
41
|
+
logger.fatal({ source: browser, message: 'build failed: already has an active instance' });
|
|
42
|
+
}
|
|
43
|
+
_activeInstances.add(browser);
|
|
44
|
+
const abortController = new AbortController();
|
|
45
|
+
let inner;
|
|
46
|
+
try {
|
|
47
|
+
logger.debug({ source: browser, message: 'build' });
|
|
48
|
+
inner = await factories[browser](configuration, abortController.signal);
|
|
49
|
+
logger.debug({ source: browser, message: 'build completed' });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
_activeInstances.delete(browser);
|
|
53
|
+
logger.fatal({ source: browser, message: 'build failed', error });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
const _openWindows = new Set();
|
|
57
|
+
const closeAllWindows = async () => {
|
|
58
|
+
await Promise.all([..._openWindows].map(async (w) => {
|
|
59
|
+
try {
|
|
60
|
+
await w.close();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
let task;
|
|
67
|
+
return {
|
|
68
|
+
async setup(settings) {
|
|
69
|
+
task = Exit.registerAsyncTask({
|
|
70
|
+
name: `${browser}#${++_instanceCount}`,
|
|
71
|
+
async stop() {
|
|
72
|
+
abortController.abort();
|
|
73
|
+
await closeAllWindows();
|
|
74
|
+
await shutdown(browser, inner);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
logger.debug({ source: browser, message: 'setup', data: settings });
|
|
78
|
+
try {
|
|
79
|
+
const capabilities = await inner.setup(settings);
|
|
80
|
+
logger.debug({ source: browser, message: 'setup completed', data: capabilities });
|
|
81
|
+
return capabilities;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
task[Symbol.dispose]();
|
|
85
|
+
_activeInstances.delete(browser);
|
|
86
|
+
logger.fatal({
|
|
87
|
+
source: browser,
|
|
88
|
+
message: 'setup failed',
|
|
89
|
+
error
|
|
90
|
+
});
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async newWindow(settings) {
|
|
95
|
+
const { pageId } = settings;
|
|
96
|
+
logger.debug({ source: browser, message: 'newWindow', pageId, data: settings });
|
|
97
|
+
try {
|
|
98
|
+
const innerWindow = await inner.newWindow(settings);
|
|
99
|
+
logger.debug({ source: browser, message: 'newWindow completed', pageId });
|
|
100
|
+
const wrapped = wrapWindow(innerWindow, browser, pageId, () => _openWindows.delete(wrapped));
|
|
101
|
+
_openWindows.add(wrapped);
|
|
102
|
+
return wrapped;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const logMethod = error instanceof RecoverableBrowserError ? 'error' : 'fatal';
|
|
106
|
+
logger[logMethod]({
|
|
107
|
+
source: browser,
|
|
108
|
+
message: 'newWindow failed',
|
|
109
|
+
error
|
|
110
|
+
});
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async shutdown() {
|
|
115
|
+
await closeAllWindows();
|
|
116
|
+
await shutdown(browser, inner);
|
|
117
|
+
task?.[Symbol.dispose]();
|
|
118
|
+
_activeInstances.delete(browser);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
8
121
|
}
|
|
9
122
|
};
|
|
123
|
+
const wrapWindow = (innerWindow, source, pageId, onClosed) => {
|
|
124
|
+
let isClosed = false;
|
|
125
|
+
return {
|
|
126
|
+
async eval(script) {
|
|
127
|
+
logger.debug({ source, message: 'eval', pageId, data: { script } });
|
|
128
|
+
try {
|
|
129
|
+
if (isClosed) {
|
|
130
|
+
throw new Error('window closed');
|
|
131
|
+
}
|
|
132
|
+
const result = await innerWindow.eval(script);
|
|
133
|
+
logger.debug({ source, message: 'eval completed', pageId, data: { result } });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
logger.error({ source, message: 'eval failed', pageId, error });
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async screenshot(path) {
|
|
142
|
+
logger.debug({ source, message: 'screenshot', pageId, data: { path } });
|
|
143
|
+
try {
|
|
144
|
+
if (isClosed) {
|
|
145
|
+
throw new Error('window closed');
|
|
146
|
+
}
|
|
147
|
+
await innerWindow.screenshot(path);
|
|
148
|
+
logger.debug({ source, message: 'screenshot completed', pageId, data: { path } });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger.error({ source, message: 'screenshot failed', pageId, error, data: { path } });
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async close() {
|
|
156
|
+
if (isClosed) {
|
|
157
|
+
logger.warn({ source, message: 'closing an already closed window, ignored', pageId });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
isClosed = true;
|
|
161
|
+
logger.debug({ source, message: 'window close', pageId });
|
|
162
|
+
try {
|
|
163
|
+
await innerWindow.close();
|
|
164
|
+
logger.debug({ source, message: 'window closed', pageId });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.error({ source, message: 'window close failed', error });
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
onClosed();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
};
|