Skip to content

Commit

Permalink
feat: implement REPL interface to debug tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DudaGod committed Dec 19, 2023
1 parent feba91a commit c1201b5
Show file tree
Hide file tree
Showing 15 changed files with 736 additions and 76 deletions.
105 changes: 93 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,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 `switchToRepl` in test code to open repl interface (default: false)
--repl-before-test [type] open repl interface before run test (default: false)
--repl-on-fail [type] open repl interface only on test fail (default: false)
-h, --help output usage information
```

Expand Down Expand Up @@ -1535,6 +1538,96 @@ 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 learn the framework API, but also 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 when 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

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`).
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:

```bash
await browser.getUrl();
// about:blank
```

After the 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 to 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).

### Environment variables

#### HERMIONE_SKIP_BROWSERS
Expand All @@ -1554,18 +1647,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:
Expand Down
10 changes: 9 additions & 1 deletion src/browser/commands/index.js
Original file line number Diff line number Diff line change
@@ -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",
];
23 changes: 3 additions & 20 deletions src/browser/commands/openAndWait.ts
Original file line number Diff line number Diff line change
@@ -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<RegExp | string>;
};
};
}
import type { Browser } from "../types";

interface WaitOpts {
selector?: string | string[];
Expand All @@ -43,7 +26,7 @@ const is: Record<string, (match: Matches) => 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(
Expand All @@ -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<string | void> {
waitNetworkIdle &&= isChrome || isCDP;
Expand Down
77 changes: 77 additions & 0 deletions src/browser/commands/switchToRepl.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
const { publicAPI: session } = browser;

const applyContext = (replServer: repl.REPLServer, ctx: Record<string, unknown> = {}): 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(/(let |const )/g, "var ");

for (const event of lineEvents) {
event(newCmd);
}
});
};

session.addCommand("switchToRepl", async function (ctx: Record<string, unknown> = {}) {
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 to 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(undefined);
});
});
});
};
39 changes: 29 additions & 10 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,40 @@ 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 in repl mode", Boolean, false)
.option("--repl-before-test [type]", "open repl interface before run test", Boolean, false)
.option("--repl-on-fail [type]", "open repl interface only on test fail", 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,
},
});

Expand Down
Loading

0 comments on commit c1201b5

Please sign in to comment.