From 4f230b76d3e54b70bac36fcf70983a02199c24d1 Mon Sep 17 00:00:00 2001 From: DudaGod Date: Tue, 19 Dec 2023 12:02:36 +0300 Subject: [PATCH] feat: implement REPL interface to debug tests --- README.md | 125 +++- package-lock.json | 676 ++++-------------- package.json | 6 +- src/browser/commands/index.js | 10 +- src/browser/commands/openAndWait.ts | 23 +- src/browser/commands/scrollIntoView.ts | 4 +- src/browser/commands/switchToRepl.ts | 77 ++ src/browser/types.ts | 20 + src/cli/index.js | 44 +- src/config/types.ts | 8 + src/hermione.ts | 50 +- src/reporters/informers/file.js | 5 +- src/test-reader/index.js | 14 + src/test-reader/test-parser.js | 4 +- src/types/index.ts | 2 +- src/utils/worker-process.ts | 4 +- .../runner/test-runner/execution-thread.js | 23 +- test/src/browser/commands/switchToRepl.ts | 221 ++++++ test/src/browser/utils.js | 9 + test/src/cli/index.js | 50 ++ test/src/hermione.js | 57 +- test/src/reporters/flat.js | 3 +- test/src/reporters/utils.js | 4 +- test/src/test-reader/index.js | 54 ++ test/src/utils/page-loader.ts | 2 +- .../runner/test-runner/execution-thread.js | 114 +++ typings/global.d.ts | 5 + 27 files changed, 977 insertions(+), 637 deletions(-) create mode 100644 src/browser/commands/switchToRepl.ts create mode 100644 test/src/browser/commands/switchToRepl.ts diff --git a/README.md b/README.md index 3bc6240ae..17ac330b0 100644 --- a/README.md +++ b/README.md @@ -1481,6 +1481,9 @@ shows the following --update-refs update screenshot references or gather them if they do not exist ("assertView" command) --inspect [inspect] nodejs inspector on [=[host:]port] --inspect-brk [inspect-brk] nodejs inspector with break at the start + --repl [type] run one test, call `browser.switchToRepl` in test code to open repl interface (default: false) + --repl-before-test [type] open repl interface before test run (default: false) + --repl-on-fail [type] open repl interface on test fail only (default: false) -h, --help output usage information ``` @@ -1549,6 +1552,116 @@ hermione_base_url=http://example.com hermione path/to/mytest.js hermione_browsers_firefox_sessions_per_browser=7 hermione path/to/mytest.js ``` +### Debug mode + +In order to understand what is going on in the test step by step, there is a debug mode. You can run tests in this mode using these options: `--inspect` and `--inspect-brk`. The difference between them is that the second one stops before executing the code. + +Example: +``` +hermione path/to/mytest.js --inspect +``` + +**Note**: In the debugging mode, only one worker is started and all tests are performed only in it. +Use this mode with option `sessionsPerBrowser=1` in order to debug tests one at a time. + +### REPL mode + +Hermione provides a [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) implementation that helps you not only to learn the framework API, but also to debug and inspect your tests. In this mode, there is no timeout for the duration of the test (it means that there will be enough time to debug the test). It can be used by specifying the CLI options: + +- `--repl` - in this mode, only one test in one browser should be run, otherwise an error is thrown. REPL interface does not start automatically, so you need to call [switchToRepl](#switchtorepl) command in the test code. Disabled by default; +- `--repl-before-test` - the same as `--repl` option except that REPL interface opens automatically before run test. Disabled by default; +- `--repl-on-fail` - the same as `--repl` option except that REPL interface opens automatically on test fail. Disabled by default. + +#### switchToRepl + +Browser command that stops the test execution and opens REPL interface in order to communicate with browser. For example: + +```js +it('foo', async ({browser}) => { + console.log('before open repl'); + + await browser.switchToRepl(); + + console.log('after open repl'); +}); +``` + +And run it using the command: + +```bash +npx hermione --repl --grep "foo" -b "chrome" +``` + +In this case, we are running only one test in one browser (or you can use `hermione.only.in('chrome')` before `it` + `it.only`). +When executing the test, the text `before open repl` will be displayed in the console first, then test execution stops, REPL interface is opened and waits your commands. So we can write some command in the terminal: + +```js +await browser.getUrl(); +// about:blank +``` + +In the case when you need to execute a block of code, for example: + +```js +for (const item of [...Array(3).keys]) { + await browser.$(`.selector_${item}`).isDisplayed(); +} +``` + +you need to switch to editor mode by running the `.editor` command in REPL and insert the desired a block of code. Then execute it by pressing `Ctrl+D`. +it is worth considering that some of code can be executed without editor mode: +- one-line code like `await browser.getUrl().then(console.log)`; +- few lines of code without using block scope or chaining, for example: + ```js + await browser.url('http://localhost:3000'); + await browser.getUrl(); + // http://localhost:3000 + ``` + +After user closes the server, the test will continue to run (text `after open repl` will be displayed in the console and browser will close). + +Another command features: +- all `const` and `let` declarations called in REPL mode are modified to `var` in runtime. This is done in order to be able to redefine created variables; +- before switching to the REPL mode `process.cwd` is replaced with the path to the folder of the executed test. After exiting from the REPL mode `process.cwd` is restored. This feature allows you to import modules relative to the test correctly; +- ability to pass the context to the REPL interface. For example: + + ```js + it('foo', async ({browser}) => { + const foo = 1; + + await browser.switchToRepl({foo}); + }); + ``` + + And now `foo` variable is available in REPL: + + ```bash + console.log("foo:", foo); + // foo: 1 + ``` + + + +#### Test development in runtime + +For quick test development without restarting the test or the browser, you can run the test in the terminal of IDE with enabled REPL mode: + +```bash +npx hermione --repl-before-test --grep "foo" -b "chrome" +``` + +After that, you need to configure the hotkey in IDE to run the selected one or more lines of code in the terminal. As a result, each new written line can be sent to the terminal using a hotkey and due to this, you can write a test much faster. + +##### How to set up using VSCode + +1. Open `Code` -> `Settings...` -> `Keyboard Shortcuts` and print `run selected text` to search input. After that, you can specify the desired key combination +2. Run hermione in repl mode (examples were above) +3. Select one or mode lines of code and press created hotkey + +##### How to set up using Webstorm + +Ability to run selected text in terminal will be available after this [issue](https://youtrack.jetbrains.com/issue/WEB-49916/Debug-JS-file-selection) will be resolved. + ### Environment variables #### HERMIONE_SKIP_BROWSERS @@ -1568,18 +1681,6 @@ For example, HERMIONE_SETS=desktop,touch hermione ``` -### Debug mode - -In order to understand what is going on in the test step by step, there is a debug mode. You can run tests in this mode using these options: --inspect and --inspect-brk. The difference between them is that the second one stops before executing the code. - -Example: -``` -hermione path/to/mytest.js --inspect -``` - -**Note**: In the debugging mode, only one worker is started and all tests are performed only in it. -Use this mode with option `sessionsPerBrowser=1` in order to debug tests one at a time. - ## Programmatic API With the API, you can use Hermione programmatically in your scripts or build tools. To do this, you must require `hermione` module and create instance: diff --git a/package-lock.json b/package-lock.json index 6def68834..603470f7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@gemini-testing/commander": "2.15.3", + "@types/chalk": "^2.2.0", "@types/mocha": "^10.0.1", "@wdio/globals": "^8.10.7", "@wdio/types": "^8.10.4", @@ -17,7 +18,7 @@ "aliasify": "^1.9.0", "bluebird": "^3.5.1", "browserify": "^13.3.0", - "chalk": "^1.1.1", + "chalk": "^2.4.2", "clear-require": "^1.0.1", "date-fns": "^2.29.3", "debug": "^2.6.9", @@ -35,6 +36,7 @@ "png-validator": "1.1.0", "sharp": "~0.30.7", "sizzle": "^2.3.6", + "strip-ansi": "^6.0.1", "temp": "^0.8.3", "uglifyify": "^3.0.4", "urijs": "^1.19.11", @@ -55,7 +57,7 @@ "@types/chai": "^4.3.4", "@types/chai-as-promised": "^7.1.5", "@types/lodash": "^4.14.191", - "@types/node": "^12.20.28", + "@types/node": "^18.19.3", "@types/proxyquire": "^1.3.28", "@types/sharp": "^0.31.1", "@types/sinon": "^4.3.3", @@ -123,54 +125,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.0.tgz", @@ -448,54 +402,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.0.tgz", @@ -2201,6 +2107,15 @@ "@types/chai": "*" } }, + "node_modules/@types/chalk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz", + "integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==", + "deprecated": "This is a stub types definition for chalk (https://github.com/chalk/chalk). chalk provides its own type definitions, so you don't need @types/chalk installed!", + "dependencies": { + "chalk": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -2268,9 +2183,12 @@ "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==" }, "node_modules/@types/node": { - "version": "12.20.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.28.tgz", - "integrity": "sha512-cBw8gzxUPYX+/5lugXIPksioBSbE42k0fZ39p+4yRzfYjN6++eq9kAPdlY9qm+MXyfbk9EmvCYAYRn380sF46w==" + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -3138,6 +3056,21 @@ "node": "^16.13 || >=18" } }, + "node_modules/@wdio/globals/node_modules/@wdio/logger/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "optional": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@wdio/globals/node_modules/@wdio/protocols": { "version": "8.14.6", "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.14.6.tgz", @@ -3354,27 +3287,6 @@ "node": ">=12" } }, - "node_modules/@wdio/globals/node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wdio/globals/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@wdio/globals/node_modules/compress-commons": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", @@ -4149,21 +4061,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/globals/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "optional": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@wdio/globals/node_modules/tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -4424,27 +4321,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@wdio/globals/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wdio/globals/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@wdio/globals/node_modules/ws": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.0.tgz", @@ -4598,11 +4474,6 @@ "node": ">=12.0.0" } }, - "node_modules/@wdio/utils/node_modules/@types/node": { - "version": "18.15.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.5.tgz", - "integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==" - }, "node_modules/@wdio/utils/node_modules/@wdio/types": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.26.0.tgz", @@ -5780,53 +5651,51 @@ } }, "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/chalk/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/chalk/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/chalk/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "color-name": "1.1.3" } }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "node_modules/chalk/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">=4" } }, "node_modules/character-entities": { @@ -5984,14 +5853,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6048,17 +5909,6 @@ "node": ">=8" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9548,25 +9398,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -14459,47 +14290,6 @@ "node": ">=10" } }, - "node_modules/standard-version/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/standard-version/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/standard-version/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/standard-version/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/standard-version/node_modules/conventional-changelog-conventionalcommits": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", @@ -14596,18 +14386,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/standard-version/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -15361,6 +15139,11 @@ "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unified": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", @@ -16401,14 +16184,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -16443,17 +16218,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -16554,47 +16318,6 @@ "requires": { "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@babel/compat-data": { @@ -16806,47 +16529,6 @@ "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.4.2", "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@babel/parser": { @@ -18109,6 +17791,14 @@ "@types/chai": "*" } }, + "@types/chalk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz", + "integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==", + "requires": { + "chalk": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -18176,9 +17866,12 @@ "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==" }, "@types/node": { - "version": "12.20.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.28.tgz", - "integrity": "sha512-cBw8gzxUPYX+/5lugXIPksioBSbE42k0fZ39p+4yRzfYjN6++eq9kAPdlY9qm+MXyfbk9EmvCYAYRn380sF46w==" + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/normalize-package-data": { "version": "2.4.1", @@ -18769,6 +18462,17 @@ "loglevel": "^1.6.0", "loglevel-plugin-prefix": "^0.8.4", "strip-ansi": "^7.1.0" + }, + "dependencies": { + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "optional": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } } }, "@wdio/protocols": { @@ -18930,23 +18634,6 @@ "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "compress-commons": { @@ -19489,15 +19176,6 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "optional": true }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "optional": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, "tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -19679,23 +19357,6 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "ws": { @@ -19812,11 +19473,6 @@ "p-iteration": "^1.1.8" }, "dependencies": { - "@types/node": { - "version": "18.15.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.5.tgz", - "integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==" - }, "@wdio/types": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.26.0.tgz", @@ -20757,39 +20413,43 @@ } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { - "ansi-regex": "^2.0.0" + "color-name": "1.1.3" } }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -20903,11 +20563,6 @@ "wrap-ansi": "^7.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -20949,14 +20604,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -23601,21 +23248,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - } - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -27337,41 +26969,6 @@ "yargs": "^16.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "conventional-changelog-conventionalcommits": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", @@ -27434,15 +27031,6 @@ "requires": { "p-limit": "^3.0.2" } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -28022,6 +27610,11 @@ "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "unified": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", @@ -28737,11 +28330,6 @@ "yargs-parser": "^20.2.2" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -28767,14 +28355,6 @@ "strip-ansi": "^6.0.1" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 316498fff..a551e1201 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "license": "MIT", "dependencies": { "@gemini-testing/commander": "2.15.3", + "@types/chalk": "^2.2.0", "@types/mocha": "^10.0.1", "@wdio/globals": "^8.10.7", "@wdio/types": "^8.10.4", @@ -51,7 +52,7 @@ "aliasify": "^1.9.0", "bluebird": "^3.5.1", "browserify": "^13.3.0", - "chalk": "^1.1.1", + "chalk": "^2.4.2", "clear-require": "^1.0.1", "date-fns": "^2.29.3", "debug": "^2.6.9", @@ -69,6 +70,7 @@ "png-validator": "1.1.0", "sharp": "~0.30.7", "sizzle": "^2.3.6", + "strip-ansi": "^6.0.1", "temp": "^0.8.3", "uglifyify": "^3.0.4", "urijs": "^1.19.11", @@ -86,7 +88,7 @@ "@types/chai": "^4.3.4", "@types/chai-as-promised": "^7.1.5", "@types/lodash": "^4.14.191", - "@types/node": "^12.20.28", + "@types/node": "^18.19.3", "@types/proxyquire": "^1.3.28", "@types/sharp": "^0.31.1", "@types/sinon": "^4.3.3", diff --git a/src/browser/commands/index.js b/src/browser/commands/index.js index fed12ba06..ae34a4eb1 100644 --- a/src/browser/commands/index.js +++ b/src/browser/commands/index.js @@ -1,3 +1,11 @@ "use strict"; -module.exports = ["assert-view", "getConfig", "getPuppeteer", "setOrientation", "scrollIntoView", "openAndWait"]; +module.exports = [ + "assert-view", + "getConfig", + "getPuppeteer", + "setOrientation", + "scrollIntoView", + "openAndWait", + "switchToRepl", +]; diff --git a/src/browser/commands/openAndWait.ts b/src/browser/commands/openAndWait.ts index b3ac87b4d..9a37e6cfe 100644 --- a/src/browser/commands/openAndWait.ts +++ b/src/browser/commands/openAndWait.ts @@ -1,24 +1,7 @@ import _ from "lodash"; import { Matches } from "webdriverio"; import PageLoader from "../../utils/page-loader"; - -interface Browser { - publicAPI: WebdriverIO.Browser; - config: { - desiredCapabilities: { - browserName: string; - }; - automationProtocol: "webdriver" | "devtools"; - pageLoadTimeout: number; - openAndWaitOpts: { - timeout?: number; - waitNetworkIdle: boolean; - waitNetworkIdleTimeout: number; - failOnNetworkError: boolean; - ignoreNetworkErrorsPatterns: Array; - }; - }; -} +import type { Browser } from "../types"; interface WaitOpts { selector?: string | string[]; @@ -43,7 +26,7 @@ const is: Record boolean> = { export = (browser: Browser): void => { const { publicAPI: session, config } = browser; const { openAndWaitOpts } = config; - const isChrome = config.desiredCapabilities.browserName === "chrome"; + const isChrome = config.desiredCapabilities?.browserName === "chrome"; const isCDP = config.automationProtocol === "devtools"; function openAndWait( @@ -56,7 +39,7 @@ export = (browser: Browser): void => { failOnNetworkError = openAndWaitOpts?.failOnNetworkError, shouldThrowError = shouldThrowErrorDefault, ignoreNetworkErrorsPatterns = openAndWaitOpts?.ignoreNetworkErrorsPatterns, - timeout = openAndWaitOpts?.timeout || config?.pageLoadTimeout, + timeout = openAndWaitOpts?.timeout || config?.pageLoadTimeout || 0, }: WaitOpts = {}, ): Promise { waitNetworkIdle &&= isChrome || isCDP; diff --git a/src/browser/commands/scrollIntoView.ts b/src/browser/commands/scrollIntoView.ts index 389c39326..7146de881 100644 --- a/src/browser/commands/scrollIntoView.ts +++ b/src/browser/commands/scrollIntoView.ts @@ -1,6 +1,4 @@ -type Browser = { - publicAPI: WebdriverIO.Browser; -}; +import type { Browser } from "../types"; // TODO: remove after fix https://github.com/webdriverio/webdriverio/issues/9620 export = async (browser: Browser): Promise => { diff --git a/src/browser/commands/switchToRepl.ts b/src/browser/commands/switchToRepl.ts new file mode 100644 index 000000000..87d3c841e --- /dev/null +++ b/src/browser/commands/switchToRepl.ts @@ -0,0 +1,77 @@ +import repl from "node:repl"; +import path from "node:path"; +import { getEventListeners } from "node:events"; +import chalk from "chalk"; +import RuntimeConfig from "../../config/runtime-config"; +import logger from "../../utils/logger"; +import type { Browser } from "../types"; + +const REPL_LINE_EVENT = "line"; + +export = async (browser: Browser): Promise => { + const { publicAPI: session } = browser; + + const applyContext = (replServer: repl.REPLServer, ctx: Record = {}): void => { + if (!ctx.browser) { + ctx.browser = session; + } + + for (const [key, value] of Object.entries(ctx)) { + Object.defineProperty(replServer.context, key, { + configurable: false, + enumerable: true, + value, + }); + } + }; + + const handleLines = (replServer: repl.REPLServer): void => { + const lineEvents = getEventListeners(replServer, REPL_LINE_EVENT); + replServer.removeAllListeners(REPL_LINE_EVENT); + + replServer.on(REPL_LINE_EVENT, cmd => { + const trimmedCmd = cmd.trim(); + const newCmd = trimmedCmd.replace(/(?<=^|\s|;|\(|\{)(let |const )/g, "var "); + + for (const event of lineEvents) { + event(newCmd); + } + }); + }; + + session.addCommand("switchToRepl", async function (ctx: Record = {}) { + const { replMode } = RuntimeConfig.getInstance(); + const { onReplMode } = browser.state; + + if (!replMode?.enabled) { + throw new Error( + 'Command "switchToRepl" available only in REPL mode, which can be started using cli option: "--repl", "--repl-before-test" or "--repl-on-fail"', + ); + } + + if (onReplMode) { + logger.warn(chalk.yellow("Hermione is already in REPL mode")); + return; + } + + logger.log(chalk.yellow("You have entered REPL mode via terminal")); + + const currCwd = process.cwd(); + const testCwd = path.dirname(session.executionContext.ctx.currentTest.file!); + process.chdir(testCwd); + + const replServer = repl.start({ prompt: "> " }); + browser.applyState({ onReplMode: true }); + + applyContext(replServer, ctx); + handleLines(replServer); + + return new Promise(resolve => { + return replServer.on("exit", () => { + process.chdir(currCwd); + browser.applyState({ onReplMode: false }); + resolve(); + }); + }); + }); +}; diff --git a/src/browser/types.ts b/src/browser/types.ts index 36b5ff8b1..d7e220e36 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -1,4 +1,6 @@ import type { AssertViewCommand, AssertViewElementCommand } from "./commands/types"; +import type { BrowserConfig } from "./../config/browser-config"; +import type { AssertViewResult, RunnerTest, RunnerHook } from "../types"; export interface BrowserMeta { pid: number; @@ -6,6 +8,13 @@ export interface BrowserMeta { [name: string]: unknown; } +export interface Browser { + publicAPI: WebdriverIO.Browser; + config: BrowserConfig; + state: Record; + applyState: (state: Record) => void; +} + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace WebdriverIO { @@ -70,6 +79,17 @@ declare global { * @returns {Promise} value, returned by `stepCb` */ runStep(stepName: string, stepCb: () => Promise | unknown): Promise; + + // TODO: describe executionContext more precisely + executionContext: (RunnerTest | RunnerHook) & { + hermioneCtx: { + assertViewResults: Array; + }; + ctx: { + browser: WebdriverIO.Browser; + currentTest: RunnerTest; + }; + }; } interface Element { diff --git a/src/cli/index.js b/src/cli/index.js index 2d3c20d70..1867e9254 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -59,21 +59,45 @@ exports.run = () => { ) .option("--inspect [inspect]", "nodejs inspector on [=[host:]port]") .option("--inspect-brk [inspect-brk]", "nodejs inspector with break at the start") + .option( + "--repl [type]", + "run one test, call `browser.switchToRepl` in test code to open repl interface", + Boolean, + false, + ) + .option("--repl-before-test [type]", "open repl interface before test run", Boolean, false) + .option("--repl-on-fail [type]", "open repl interface on test fail only", Boolean, false) .arguments("[paths...]") .action(async paths => { try { - await handleRequires(program.require); + const { + reporter: reporters, + browser: browsers, + set: sets, + grep, + updateRefs, + require: requireModules, + inspect, + inspectBrk, + repl, + replBeforeTest, + replOnFail, + } = program; + + await handleRequires(requireModules); const isTestsSuccess = await hermione.run(paths, { - reporters: program.reporter || defaults.reporters, - browsers: program.browser, - sets: program.set, - grep: program.grep, - updateRefs: program.updateRefs, - requireModules: program.require, - inspectMode: (program.inspect || program.inspectBrk) && { - inspect: program.inspect, - inspectBrk: program.inspectBrk, + reporters: reporters || defaults.reporters, + browsers, + sets, + grep, + updateRefs, + requireModules, + inspectMode: (inspect || inspectBrk) && { inspect, inspectBrk }, + replMode: { + enabled: repl || replBeforeTest || replOnFail, + beforeTest: replBeforeTest, + onFail: replOnFail, }, }); diff --git a/src/config/types.ts b/src/config/types.ts index b8fdbb863..98cf39c12 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -120,6 +120,14 @@ export interface CommonConfig { system: SystemConfig; headless: boolean | null; isolation: boolean; + + openAndWaitOpts: { + timeout?: number; + waitNetworkIdle: boolean; + waitNetworkIdleTimeout: number; + failOnNetworkError: boolean; + ignoreNetworkErrorsPatterns: Array; + }; } export interface SetsConfig { diff --git a/src/hermione.ts b/src/hermione.ts index d5318e43c..cadb8e352 100644 --- a/src/hermione.ts +++ b/src/hermione.ts @@ -16,22 +16,24 @@ import { ConfigInput } from "./config/types"; import { MasterEventHandler, Test } from "./types"; interface RunOpts { - browsers?: string[]; - sets?: string[]; - grep?: string; - updateRefs?: boolean; - requireModules?: string[]; - inspectMode?: { + browsers: string[]; + sets: string[]; + grep: RegExp; + updateRefs: boolean; + requireModules: string[]; + inspectMode: { inspect: boolean; inspectBrk: boolean; }; - reporters?: string[]; + reporters: string[]; + replMode: { + enabled: boolean; + beforeTest: boolean; + onFail: boolean; + }; } -interface ReadTestsOpts { - browsers: string[]; - sets: string[]; - grep: string | RegExp; +interface ReadTestsOpts extends Pick { silent: boolean; ignore: string | string[]; } @@ -59,11 +61,24 @@ export class Hermione extends BaseHermione { async run( testPaths: TestCollection | string[], - { browsers, sets, grep, updateRefs, requireModules, inspectMode, reporters = [] }: Partial = {}, + { + browsers, + sets, + grep, + updateRefs, + requireModules, + inspectMode, + replMode, + reporters = [], + }: Partial = {}, ): Promise { validateUnknownBrowsers(browsers, _.keys(this._config.browsers)); - RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode }); + RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode, replMode }); + + if (replMode?.enabled) { + this._config.system.mochaOpts.timeout = 0; + } const runner = MainRunner.create(this._config, this._interceptors); this.runner = runner; @@ -78,7 +93,10 @@ export class Hermione extends BaseHermione { await this._init(); runner.init(); - await runner.run(await this._readTests(testPaths, { browsers, sets, grep }), RunnerStats.create(this)); + await runner.run( + await this._readTests(testPaths, { browsers, sets, grep, replMode }), + RunnerStats.create(this), + ); return !this.isFailed(); } @@ -96,7 +114,7 @@ export class Hermione extends BaseHermione { async readTests( testPaths: string[], - { browsers, sets, grep, silent, ignore }: Partial = {}, + { browsers, sets, grep, silent, ignore, replMode }: Partial = {}, ): Promise { const testReader = TestReader.create(this._config); @@ -109,7 +127,7 @@ export class Hermione extends BaseHermione { ]); } - const specs = await testReader.read({ paths: testPaths, browsers, ignore, sets, grep }); + const specs = await testReader.read({ paths: testPaths, browsers, ignore, sets, grep, replMode }); const collection = TestCollection.create(specs); collection.getBrowsers().forEach(bro => { diff --git a/src/reporters/informers/file.js b/src/reporters/informers/file.js index e5da00078..cb971337b 100644 --- a/src/reporters/informers/file.js +++ b/src/reporters/informers/file.js @@ -1,5 +1,6 @@ const fs = require("fs"); -const chalk = require("chalk"); +const stripAnsi = require("strip-ansi"); + const BaseInformer = require("./base"); const logger = require("../../utils/logger"); @@ -34,6 +35,6 @@ module.exports = class FileInformer extends BaseInformer { } _prepareMsg(msg) { - return typeof msg === "object" ? JSON.stringify(msg) : chalk.stripColor(msg); + return typeof msg === "object" ? JSON.stringify(msg) : stripAnsi(msg); } }; diff --git a/src/test-reader/index.js b/src/test-reader/index.js index 96d59b4e0..00a73f306 100644 --- a/src/test-reader/index.js +++ b/src/test-reader/index.js @@ -47,6 +47,20 @@ module.exports = class TestReader extends EventEmitter { function validateTests(testsByBro, options) { const tests = _.flatten(Object.values(testsByBro)); + + if (options.replMode?.enabled) { + const testsToRun = tests.filter(test => !test.disabled && !test.pending); + const browsersToRun = _.uniq(testsToRun.map(test => test.browserId)); + + if (testsToRun.length !== 1) { + throw new Error( + `In repl mode only 1 test in 1 browser should be run, but found ${testsToRun.length} tests` + + `${testsToRun.length === 0 ? ". " : ` that run in ${browsersToRun.join(", ")} browsers. `}` + + `Try to specify cli-options: "--grep" and "--browser" or use "hermione.only.in" in the test file.`, + ); + } + } + if (!_.isEmpty(tests) && tests.some(test => !test.silentSkip)) { return; } diff --git a/src/test-reader/test-parser.js b/src/test-reader/test-parser.js index 1cad5ef3f..ec0f42528 100644 --- a/src/test-reader/test-parser.js +++ b/src/test-reader/test-parser.js @@ -45,7 +45,7 @@ class TestParser extends EventEmitter { this.#applyInstructionsEvents(eventBus); this.#passthroughFileEvents(eventBus, global.hermione); - this.#clearRequireCach(files); + this.#clearRequireCache(files); const rand = Math.random(); const esmDecorator = f => f + `?rand=${rand}`; @@ -78,7 +78,7 @@ class TestParser extends EventEmitter { passthroughEvent_(MasterEvents.AFTER_FILE_READ); } - #clearRequireCach(files) { + #clearRequireCache(files) { files.forEach(filename => { if (path.extname(filename) !== ".mjs") { clearRequire(path.resolve(filename)); diff --git a/src/types/index.ts b/src/types/index.ts index aa4e2f1bc..64047126b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,7 +14,7 @@ import { WorkerProcess } from "../utils/worker-process"; import { BaseHermione } from "../base-hermione"; import { CoordBounds, LooksSameOptions } from "looks-same"; -export { Suite as RunnerSuite, Test as RunnerTest } from "mocha"; +export { Suite as RunnerSuite, Test as RunnerTest, Hook as RunnerHook } from "mocha"; export type { Browser as WdioBrowser } from "webdriverio"; diff --git a/src/utils/worker-process.ts b/src/utils/worker-process.ts index eda7b2264..f66724b54 100644 --- a/src/utils/worker-process.ts +++ b/src/utils/worker-process.ts @@ -1,5 +1,7 @@ import { ChildProcess } from "child_process"; +type Serializable = string | object | number | boolean | bigint; + export class WorkerProcess { protected process: ChildProcess; @@ -11,7 +13,7 @@ export class WorkerProcess { this.process = process; } - send(message: unknown): boolean { + send(message: Serializable): boolean { if (!this.process.connected) { return false; } diff --git a/src/worker/runner/test-runner/execution-thread.js b/src/worker/runner/test-runner/execution-thread.js index 48a6c8ca0..cf7814caf 100644 --- a/src/worker/runner/test-runner/execution-thread.js +++ b/src/worker/runner/test-runner/execution-thread.js @@ -1,6 +1,8 @@ "use strict"; const Promise = require("bluebird"); +const RuntimeConfig = require("../../../config/runtime-config"); +const logger = require("../../../utils/logger"); module.exports = class ExecutionThread { static create(...args) { @@ -14,6 +16,9 @@ module.exports = class ExecutionThread { browser: browser.publicAPI, currentTest: test, }; + + this._runtimeConfig = RuntimeConfig.getInstance(); + this._isReplBeforeTestOpened = false; } async run(runnable) { @@ -35,7 +40,14 @@ module.exports = class ExecutionThread { } } - _call(runnable) { + async _call(runnable) { + const { replMode } = this._runtimeConfig; + + if (replMode?.beforeTest && !this._isReplBeforeTestOpened) { + await this._ctx.browser.switchToRepl(); + this._isReplBeforeTestOpened = true; + } + let fnPromise = Promise.method(runnable.fn).call(this._ctx, this._ctx); if (runnable.timeout) { @@ -44,7 +56,14 @@ module.exports = class ExecutionThread { } return fnPromise - .tapCatch(e => this._screenshooter.extendWithScreenshot(e)) + .tapCatch(async e => { + if (replMode?.onFail) { + logger.log("Caught error:", e); + await this._ctx.browser.switchToRepl(); + } + + return this._screenshooter.extendWithScreenshot(e); + }) .finally(async () => { if (this._hermioneCtx.assertViewResults && this._hermioneCtx.assertViewResults.hasFails()) { await this._screenshooter.captureScreenshotOnAssertViewFail(); diff --git a/test/src/browser/commands/switchToRepl.ts b/test/src/browser/commands/switchToRepl.ts new file mode 100644 index 000000000..13809f7f8 --- /dev/null +++ b/test/src/browser/commands/switchToRepl.ts @@ -0,0 +1,221 @@ +import repl, { type REPLServer } from "node:repl"; +import { EventEmitter } from "node:events"; +import webdriverio from "webdriverio"; +import chalk from "chalk"; +import sinon, { type SinonStub, type SinonSpy } from "sinon"; + +import RuntimeConfig from "src/config/runtime-config"; +import clientBridge from "src/browser/client-bridge"; +import logger from "src/utils/logger"; +import { mkExistingBrowser_ as mkBrowser_, mkSessionStub_ } from "../utils"; + +import type ExistingBrowser from "src/browser/existing-browser"; + +describe('"switchToRepl" command', () => { + const sandbox = sinon.sandbox.create(); + + const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { + (webdriverio.attach as SinonStub).resolves(session); + + return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: {} }); + }; + + const mkReplServer_ = (): REPLServer => { + const replServer = new EventEmitter() as REPLServer; + (replServer.context as unknown) = {}; + + sandbox.stub(repl, "start").returns(replServer); + + return replServer; + }; + + const switchToRepl_ = async ({ + session = mkSessionStub_(), + replServer = mkReplServer_(), + ctx = {}, + }): Promise => { + const promise = session.switchToRepl(ctx); + + replServer.emit("exit"); + await promise; + }; + + beforeEach(() => { + sandbox.stub(webdriverio, "attach"); + sandbox.stub(clientBridge, "build").resolves(); + sandbox.stub(RuntimeConfig, "getInstance").returns({ replMode: { enabled: false } }); + sandbox.stub(logger, "warn"); + sandbox.stub(logger, "log"); + sandbox.stub(process, "chdir"); + }); + + afterEach(() => sandbox.restore()); + + it("should add command", async () => { + const session = mkSessionStub_(); + + await initBrowser_({ session }); + + assert.calledWith(session.addCommand, "switchToRepl", sinon.match.func); + }); + + it("should throw error if command is not called in repl mode", async () => { + (RuntimeConfig.getInstance as SinonStub).returns({ replMode: { enabled: false } }); + const session = mkSessionStub_(); + + await initBrowser_({ session }); + + try { + await session.switchToRepl(); + } catch (e) { + assert.match((e as Error).message, /Command "switchToRepl" available only in REPL mode/); + } + }); + + describe("in REPL mode", async () => { + beforeEach(() => { + (RuntimeConfig.getInstance as SinonStub).returns({ replMode: { enabled: true } }); + }); + + it("should inform that user entered to repl server before run it", async () => { + const session = mkSessionStub_(); + + await initBrowser_({ session }); + await switchToRepl_({ session }); + + assert.callOrder( + (logger.log as SinonStub).withArgs(chalk.yellow("You have entered REPL mode via terminal")), + repl.start as SinonStub, + ); + }); + + it("should change cwd to test directory before run repl server", async () => { + const session = mkSessionStub_(); + session.executionContext.ctx.currentTest.file = "/root/project/dir/file.hermione.js"; + + await initBrowser_({ session }); + await switchToRepl_({ session }); + + assert.callOrder((process.chdir as SinonStub).withArgs("/root/project/dir"), repl.start as SinonStub); + }); + + it("should change cwd to its original value on close repl server", async () => { + const session = mkSessionStub_(); + session.executionContext.ctx.currentTest.file = "/root/project/dir/file.hermione.js"; + const currCwd = process.cwd(); + const onExit = sandbox.spy(); + + const replServer = mkReplServer_(); + replServer.on("exit", onExit); + + await initBrowser_({ session }); + const promise = session.switchToRepl(); + + replServer.emit("exit"); + await promise; + + assert.callOrder(onExit, (process.chdir as SinonStub).withArgs(currCwd)); + }); + + it("should add browser instance to repl context by default", async () => { + const session = mkSessionStub_(); + const replServer = mkReplServer_(); + + await initBrowser_({ session }); + await switchToRepl_({ session, replServer }); + + assert.deepEqual(replServer.context.browser, session); + }); + + it("should not be able to overwrite browser instance in repl context", async () => { + const session = mkSessionStub_(); + const replServer = mkReplServer_(); + + await initBrowser_({ session }); + await switchToRepl_({ session, replServer }); + + try { + replServer.context.browser = "foo"; + } catch (err) { + assert.match((err as Error).message, "Cannot assign to read only property 'browser'"); + } + }); + + it("should add passed user context to repl server", async () => { + const session = mkSessionStub_(); + const replServer = mkReplServer_(); + + await initBrowser_({ session }); + await switchToRepl_({ session, replServer, ctx: { foo: "bar" } }); + + assert.equal(replServer.context.foo, "bar"); + }); + + it("should not create new repl server if old one is already used", async () => { + const replServer = mkReplServer_(); + const session = mkSessionStub_(); + + await initBrowser_({ session }); + const promise1 = session.switchToRepl(); + const promise2 = session.switchToRepl(); + + replServer.emit("exit"); + await Promise.all([promise1, promise2]); + + assert.calledOnce(repl.start as SinonStub); + assert.calledOnceWith(logger.warn, chalk.yellow("Hermione is already in REPL mode")); + }); + + ["const", "let"].forEach(decl => { + describe(`"${decl}" declaration to var in order to reassign`, () => { + let replServer: REPLServer; + let onLine: SinonSpy; + + beforeEach(async () => { + replServer = mkReplServer_(); + onLine = sandbox.spy(); + replServer.on("line", onLine); + + const session = mkSessionStub_(); + + await initBrowser_({ session }); + await switchToRepl_({ session, replServer }); + }); + + describe("should modify", () => { + it("with spaces before declaration", () => { + replServer.emit("line", ` ${decl} foo = 1`); + + assert.calledWith(onLine.firstCall, "var foo = 1"); + }); + + it("with few declarations one by one", () => { + replServer.emit("line", `${decl} foo = 1; ${decl} bar = 2;${decl} qux = 3`); + + assert.calledWith(onLine.firstCall, "var foo = 1; var bar = 2;var qux = 3"); + }); + + it("with declaration in cycle", () => { + replServer.emit("line", `for (${decl} item of items) {${decl} a = 1}`); + + assert.calledWith(onLine.firstCall, "for (var item of items) {var a = 1}"); + }); + }); + + describe("should not modify", () => { + it("with declaration as a string", () => { + replServer.emit("line", `"${decl} " + '${decl} '`); + + assert.calledWith(onLine.firstCall, `"${decl} " + '${decl} '`); + }); + + it("with declaration as variable name", () => { + replServer.emit("line", `var zzz${decl} = 1`); + + assert.calledWith(onLine.firstCall, `var zzz${decl} = 1`); + }); + }); + }); + }); + }); +}); diff --git a/test/src/browser/utils.js b/test/src/browser/utils.js index 53919c789..f787e937a 100644 --- a/test/src/browser/utils.js +++ b/test/src/browser/utils.js @@ -84,6 +84,13 @@ exports.mkSessionStub_ = () => { session.options = {}; session.capabilities = {}; session.commandList = []; + session.executionContext = { + ctx: { + currentTest: { + file: "/default", + }, + }, + }; session.deleteSession = sinon.stub().named("end").resolves(); session.url = sinon.stub().named("url").resolves(); @@ -103,6 +110,8 @@ exports.mkSessionStub_ = () => { session.switchToWindow = sinon.stub().named("switchToWindow").resolves(); session.findElements = sinon.stub().named("findElements").resolves([]); session.switchToFrame = sinon.stub().named("switchToFrame").resolves(); + session.switchToParentFrame = sinon.stub().named("switchToParentFrame").resolves(); + session.switchToRepl = sinon.stub().named("switchToRepl").resolves(); session.addCommand = sinon.stub().callsFake((name, command, isElement) => { const target = isElement ? wdioElement : session; diff --git a/test/src/cli/index.js b/test/src/cli/index.js index 541099e26..e21f28925 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -230,4 +230,54 @@ describe("cli", () => { assert.calledWithMatch(Hermione.prototype.run, any, { inspectMode: { inspectBrk: "9229" } }); }); + + describe("repl mode", () => { + it("should be disabled by default", async () => { + await run_(); + + assert.calledWithMatch(Hermione.prototype.run, any, { + replMode: { + enabled: false, + beforeTest: false, + onFail: false, + }, + }); + }); + + it('should be enabled when specify "repl" flag', async () => { + await run_("--repl"); + + assert.calledWithMatch(Hermione.prototype.run, any, { + replMode: { + enabled: true, + beforeTest: false, + onFail: false, + }, + }); + }); + + it('should be enabled when specify "beforeTest" flag', async () => { + await run_("--repl-before-test"); + + assert.calledWithMatch(Hermione.prototype.run, any, { + replMode: { + enabled: true, + beforeTest: true, + onFail: false, + }, + }); + }); + + it('should be enabled when specify "onFail" flag', async () => { + await run_("--repl-on-fail"); + + assert.calledWithMatch(Hermione.prototype.run, any, { + replMode: { + enabled: true, + beforeTest: false, + onFail: true, + }, + }); + }); + }); }); diff --git a/test/src/hermione.js b/test/src/hermione.js index 7ff4b4159..4f90360ef 100644 --- a/test/src/hermione.js +++ b/test/src/hermione.js @@ -176,20 +176,48 @@ describe("hermione", () => { ); }); - it("should init runtime config", () => { + it("should init runtime config", async () => { mkRunnerStub_(); - return runHermione([], { updateRefs: true, requireModules: ["foo"], inspectMode: { inspect: true } }).then( - () => { - assert.calledOnce(RuntimeConfig.getInstance); - assert.calledOnceWith(RuntimeConfig.getInstance.lastCall.returnValue.extend, { - updateRefs: true, - requireModules: ["foo"], - inspectMode: { inspect: true }, - }); - assert.callOrder(RuntimeConfig.getInstance, Runner.create); + await runHermione([], { + updateRefs: true, + requireModules: ["foo"], + inspectMode: { + inspect: true, }, - ); + replMode: { + enabled: true, + }, + }); + + assert.calledOnce(RuntimeConfig.getInstance); + assert.calledOnceWith(RuntimeConfig.getInstance.lastCall.returnValue.extend, { + updateRefs: true, + requireModules: ["foo"], + inspectMode: { inspect: true }, + replMode: { enabled: true }, + }); + assert.callOrder(RuntimeConfig.getInstance, Runner.create); + }); + + describe("repl mode", () => { + it("should not reset test timeout to 0 if run not in repl", async () => { + mkRunnerStub_(); + const hermione = mkHermione_({ system: { mochaOpts: { timeout: 100500 } } }); + + await hermione.run([], { replMode: { enabled: false } }); + + assert.equal(hermione.config.system.mochaOpts.timeout, 100500); + }); + + it("should reset test timeout to 0 if run in repl", async () => { + mkRunnerStub_(); + const hermione = mkHermione_({ system: { mochaOpts: { timeout: 100500 } } }); + + await hermione.run([], { replMode: { enabled: true } }); + + assert.equal(hermione.config.system.mochaOpts.timeout, 0); + }); }); describe("INIT", () => { @@ -270,12 +298,13 @@ describe("hermione", () => { const browsers = ["bro1", "bro2"]; const grep = "baz.*"; const sets = ["set1", "set2"]; + const replMode = { enabled: false }; sandbox.spy(Hermione.prototype, "readTests"); - await runHermione(testPaths, { browsers, grep, sets }); + await runHermione(testPaths, { browsers, grep, sets, replMode }); - assert.calledOnceWith(Hermione.prototype.readTests, testPaths, { browsers, grep, sets }); + assert.calledOnceWith(Hermione.prototype.readTests, testPaths, { browsers, grep, sets, replMode }); }); it("should accept test collection as first parameter", async () => { @@ -550,6 +579,7 @@ describe("hermione", () => { ignore: "baz/qux", sets: ["s1", "s2"], grep: "grep", + replMode: { enabled: false }, }); assert.calledOnceWith(TestReader.prototype.read, { @@ -558,6 +588,7 @@ describe("hermione", () => { ignore: "baz/qux", sets: ["s1", "s2"], grep: "grep", + replMode: { enabled: false }, }); }); diff --git a/test/src/reporters/flat.js b/test/src/reporters/flat.js index 6d7ca04d4..96825dc85 100644 --- a/test/src/reporters/flat.js +++ b/test/src/reporters/flat.js @@ -1,6 +1,7 @@ "use strict"; const chalk = require("chalk"); +const stripAnsi = require("strip-ansi"); const path = require("path"); const { EventEmitter } = require("events"); const proxyquire = require("proxyquire"); @@ -90,7 +91,7 @@ describe("Flat reporter", () => { retries: 2, }); - const deserealizedResult = chalk.stripColor(informer.log.firstCall.args[0]); + const deserealizedResult = stripAnsi(informer.log.firstCall.args[0]); assert.equal(deserealizedResult, "Total: 5 Passed: 2 Failed: 2 Skipped: 1 Retries: 2"); }); diff --git a/test/src/reporters/utils.js b/test/src/reporters/utils.js index c9881229e..4fdbeef31 100644 --- a/test/src/reporters/utils.js +++ b/test/src/reporters/utils.js @@ -1,6 +1,6 @@ "use strict"; -const chalk = require("chalk"); +const stripAnsi = require("strip-ansi"); const _ = require("lodash"); exports.mkTestStub_ = opts => { @@ -14,5 +14,5 @@ exports.mkTestStub_ = opts => { }; exports.getDeserializedResult = log => { - return chalk.stripColor(log).substr(2); // remove first symbol (icon) + return stripAnsi(log).substr(2); // remove first symbol (icon) }; diff --git a/test/src/test-reader/index.js b/test/src/test-reader/index.js index aae170a06..be67867d2 100644 --- a/test/src/test-reader/index.js +++ b/test/src/test-reader/index.js @@ -289,5 +289,59 @@ describe("test-reader", () => { } }); }); + + describe("repl mode", () => { + it("should not throw error if there are only one test that should be run", async () => { + TestParser.prototype.parse.returns([ + { title: "test1", browserId: "yabro", pending: false, disabled: false }, + { title: "test2", browserId: "yabro", pending: true, disabled: false }, + { title: "test3", browserId: "yabro", pending: true, disabled: true }, + ]); + + await assert.isFulfilled(readTests_({ opts: { replMode: { enabled: true } } })); + }); + + it("should throw error if tests not found", async () => { + TestParser.prototype.parse.returns([]); + + try { + await readTests_({ opts: { replMode: { enabled: true } } }); + } catch (e) { + assert.match(e.message, /In repl mode only 1 test in 1 browser should be run, but found 0 tests\./); + } + }); + + it("should throw error if found few tests in one browser", async () => { + TestParser.prototype.parse.returns([ + { title: "test1", browserId: "yabro", pending: false, disabled: false }, + { title: "test2", browserId: "yabro", pending: false, disabled: false }, + ]); + + try { + await readTests_({ opts: { replMode: { enabled: true } } }); + } catch (e) { + assert.match( + e.message, + /In repl mode only 1 test in 1 browser should be run, but found 2 tests that run in yabro browsers\./, + ); + } + }); + + it("should throw error if found one test in few browsers", async () => { + TestParser.prototype.parse.returns([ + { title: "test1", browserId: "yabro", pending: false, disabled: false }, + { title: "test1", browserId: "broya", pending: false, disabled: false }, + ]); + + try { + await readTests_({ opts: { replMode: { enabled: true } } }); + } catch (e) { + assert.match( + e.message, + /In repl mode only 1 test in 1 browser should be run, but found 2 tests that run in yabro, broya browsers\./, + ); + } + }); + }); }); }); diff --git a/test/src/utils/page-loader.ts b/test/src/utils/page-loader.ts index 4232de275..a5e88cc0c 100644 --- a/test/src/utils/page-loader.ts +++ b/test/src/utils/page-loader.ts @@ -16,7 +16,7 @@ type MockStub = { }; const waitUntilMock = (condition: () => boolean, opts?: WaitForOptions): Promise => { - let rejectingTimeout: number; + let rejectingTimeout: NodeJS.Timeout; let destroyed = false; return new Promise((resolve, reject) => { diff --git a/test/src/worker/runner/test-runner/execution-thread.js b/test/src/worker/runner/test-runner/execution-thread.js index 3b3206d61..193eb18a1 100644 --- a/test/src/worker/runner/test-runner/execution-thread.js +++ b/test/src/worker/runner/test-runner/execution-thread.js @@ -7,6 +7,8 @@ const AssertViewResults = require("src/browser/commands/assert-view/assert-view- const ExecutionThread = require("src/worker/runner/test-runner/execution-thread"); const OneTimeScreenshooter = require("src/worker/runner/test-runner/one-time-screenshooter"); const { Test } = require("src/test-reader/test-object"); +const RuntimeConfig = require("src/config/runtime-config"); +const logger = require("src/utils/logger"); describe("worker/runner/test-runner/execution-thread", () => { const sandbox = sinon.sandbox.create(); @@ -31,6 +33,7 @@ describe("worker/runner/test-runner/execution-thread", () => { config, publicAPI: Object.create({ getCommandHistory: sinon.stub().resolves([]), + switchToRepl: sinon.stub().resolves(), }), }; }; @@ -47,6 +50,8 @@ describe("worker/runner/test-runner/execution-thread", () => { beforeEach(() => { sandbox.stub(OneTimeScreenshooter.prototype, "extendWithScreenshot").callsFake(e => Promise.resolve(e)); sandbox.stub(OneTimeScreenshooter.prototype, "captureScreenshotOnAssertViewFail").resolves(); + sandbox.stub(RuntimeConfig, "getInstance").returns({ replMode: { onFail: false } }); + sandbox.stub(logger, "log"); }); afterEach(() => sandbox.restore()); @@ -306,5 +311,114 @@ describe("worker/runner/test-runner/execution-thread", () => { await assert.isRejected(executionThread.run(runnable), /foo/); }); }); + + describe("REPL mode", () => { + describe("beforeTest", () => { + it("should do nothing if flag is not specified", async () => { + RuntimeConfig.getInstance.returns({ replMode: { beforeTest: false } }); + + const browser = mkBrowser_(); + const runnable = mkRunnable_({ fn: () => Promise.resolve() }); + + await mkExecutionThread_({ browser }).run(runnable); + + assert.notCalled(browser.publicAPI.switchToRepl); + }); + + describe("if flag is specified", () => { + beforeEach(() => { + RuntimeConfig.getInstance.returns({ replMode: { beforeTest: true } }); + }); + + it("should switch to REPL before execute runnable", async () => { + const browser = mkBrowser_(); + const onRunnable = sandbox.stub().named("runnable"); + const runnable = mkRunnable_({ fn: onRunnable }); + + await mkExecutionThread_({ browser }).run(runnable); + + assert.callOrder(browser.publicAPI.switchToRepl, onRunnable); + }); + + it("should switch to REPL only once for one execution thread", async () => { + const browser = mkBrowser_(); + const runnable1 = mkRunnable_({ fn: () => Promise.resolve() }); + const runnable2 = mkRunnable_({ fn: () => Promise.resolve() }); + const executionThread = mkExecutionThread_({ browser }); + + await executionThread.run(runnable1); + await executionThread.run(runnable2); + + await assert.calledOnce(browser.publicAPI.switchToRepl); + }); + + it("should switch to REPL for each new execution thread", async () => { + const browser = mkBrowser_(); + const runnable1 = mkRunnable_({ fn: () => Promise.resolve() }); + const runnable2 = mkRunnable_({ fn: () => Promise.resolve() }); + const executionThread1 = mkExecutionThread_({ browser }); + const executionThread2 = mkExecutionThread_({ browser }); + + await executionThread1.run(runnable1); + await executionThread2.run(runnable2); + + await assert.calledTwice(browser.publicAPI.switchToRepl); + }); + }); + }); + + describe("onFail", () => { + it("should do nothing if flag is not specified", async () => { + RuntimeConfig.getInstance.returns({ replMode: { onFail: false } }); + + const browser = mkBrowser_(); + const runnable = mkRunnable_({ + fn: () => Promise.reject(new Error()), + }); + + await mkExecutionThread_({ browser }) + .run(runnable) + .catch(() => {}); + + await assert.notCalled(browser.publicAPI.switchToRepl); + }); + + describe("if flag is specified", () => { + beforeEach(() => { + RuntimeConfig.getInstance.returns({ replMode: { onFail: true } }); + }); + + it("should switch to REPL on error", async () => { + const browser = mkBrowser_(); + const runnable = mkRunnable_({ + fn: () => Promise.reject(new Error()), + }); + + await mkExecutionThread_({ browser }) + .run(runnable) + .catch(() => {}); + + await assert.calledOnce(browser.publicAPI.switchToRepl); + }); + + it("should print error before swith to REPL", async () => { + const browser = mkBrowser_(); + const err = new Error(); + const runnable = mkRunnable_({ + fn: () => Promise.reject(err), + }); + + await mkExecutionThread_({ browser }) + .run(runnable) + .catch(() => {}); + + await assert.callOrder( + logger.log.withArgs("Caught error:", err), + browser.publicAPI.switchToRepl, + ); + }); + }); + }); + }); }); }); diff --git a/typings/global.d.ts b/typings/global.d.ts index 05161856b..6fe5974d2 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -28,3 +28,8 @@ interface SuiteDefinition { only: (title: string, fn: (this: Mocha.Suite) => void) => Mocha.Suite; skip: (title: string, fn: (this: Mocha.Suite) => void) => Mocha.Suite; } + +declare namespace globalThis { + // eslint-disable-next-line no-var + var expect: ExpectWebdriverIO.Expect; +}