codeceptjs 4.2.1 → 4.2.2
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 +2 -0
- package/docs/alternative-browsers.md +56 -1
- package/docs/helpers/Lightpanda.md +1836 -8
- package/docs/helpers/Moli.md +1956 -0
- package/docs/installation.md +18 -0
- package/docs/lint.md +111 -0
- package/docs/mobile.md +1 -0
- package/docs/mobilewright.md +166 -0
- package/lib/command/run-multiple.js +7 -2
- package/lib/container.js +1 -1
- package/lib/helper/Decision.js +1 -1
- package/lib/helper/Moli.js +125 -0
- package/lib/helper/WebDriver.js +3 -6
- package/lib/mocha/cli.js +2 -2
- package/lib/mocha/index.js +1 -2
- package/lib/mocha/suite.js +1 -1
- package/lib/mocha/test.js +1 -2
- package/lib/mocha/ui.js +3 -3
- package/lib/plugin/pause.js +1 -1
- package/package.json +31 -34
- package/typings/promiseBasedTypes.d.ts +106 -0
- package/typings/types.d.ts +106 -0
package/docs/installation.md
CHANGED
|
@@ -115,6 +115,22 @@ export const config = {
|
|
|
115
115
|
|
|
116
116
|
See [Alternative Browser Engines](/alternative-browsers) for running an existing Playwright suite on Lightpanda.
|
|
117
117
|
|
|
118
|
+
## Moli (experimental)
|
|
119
|
+
|
|
120
|
+
Moli is a headless browser for automation that runs your app's JavaScript in V8 and skips layout and painting. Install it following the [official instructions](https://github.com/lexmount/moli#direct-installation), then enable the helper:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
export const config = {
|
|
124
|
+
helpers: {
|
|
125
|
+
Moli: {
|
|
126
|
+
url: 'http://localhost:3000',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
See [Alternative Browser Engines](/alternative-browsers) for running an existing Playwright suite on Moli.
|
|
133
|
+
|
|
118
134
|
## Appium (mobile)
|
|
119
135
|
|
|
120
136
|
Native iOS and Android testing. Appium speaks the WebDriver protocol, so CodeceptJS drives it through `webdriverio`:
|
|
@@ -134,6 +150,8 @@ appium
|
|
|
134
150
|
|
|
135
151
|
You'll also need the Android SDK and an emulator or device, or Xcode and a simulator or device (or a device-cloud account). Set `platformName`, `deviceName`, and `app` in the helper config — see [Mobile Testing](/mobile).
|
|
136
152
|
|
|
153
|
+
For mobile tests without an Appium server, see the experimental [Mobilewright helper](/mobilewright): it drives devices directly via adb/mobilecli and reads the accessibility tree.
|
|
154
|
+
|
|
137
155
|
## API testing
|
|
138
156
|
|
|
139
157
|
REST and GraphQL tests need no browser — the helpers are built in (they use `axios`, already a dependency):
|
package/docs/lint.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /lint
|
|
3
|
+
title: Linting
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Linting
|
|
7
|
+
|
|
8
|
+
CodeceptJS tests have well-known smell patterns: fixed sleeps instead of waits, `if` branches that make tests non-deterministic, raw `fetch()` calls that bypass test steps and reports, plain-text passwords printed into logs, missing `await` on grabbers, and leftover `pause()` or `.only` from debugging sessions.
|
|
9
|
+
|
|
10
|
+
[`@codeceptjs/lint`](https://github.com/codeceptjs/lint) catches all of them statically. It reads tests, page objects and helpers as an AST — CodeceptJS itself doesn't need to be installed and no browser is started.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @codeceptjs/lint # files from codecept.conf.js in the current directory
|
|
16
|
+
npx @codeceptjs/lint tests/ pages/login.js # explicit paths
|
|
17
|
+
npx @codeceptjs/lint -r no-pause -r no-only # only these rules (or --rules no-pause,no-only)
|
|
18
|
+
npx @codeceptjs/lint rules # list all rules
|
|
19
|
+
npx @codeceptjs/lint --json # findings as JSON
|
|
20
|
+
npx @codeceptjs/lint -c e2e/codecept.conf.js # config in another directory
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Each finding is printed on one line:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
checkout_test.js:4:3 error no-fixed-wait I.wait() sleeps unconditionally. Wait for a condition: I.waitForElement / I.waitForText / I.see
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Exit codes: `0` — no errors (warnings allowed), `1` — errors found, `2` — bad input or a file that can't be parsed.
|
|
30
|
+
|
|
31
|
+
Without paths, the linter reads `codecept.conf.js` (or `.ts`, `.cjs`, `.mjs`) and lints files matched by `tests`, local files from `include` (page objects, steps file) and helpers loaded with `require`.
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
| Rule | Default | Detects |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `no-fixed-wait` | error | `I.wait(5)` with a number. Use `I.waitForElement`, `I.waitForText`, `I.see` |
|
|
38
|
+
| `no-sleep` | error | `setTimeout`, including `window.setTimeout`, `await new Promise(r => setTimeout(r, ms))` and `timers/promises`, outside helpers |
|
|
39
|
+
| `no-if-in-test` | error | `if` inside a Scenario or hook. Use `tryTo()` from `codeceptjs/effects` |
|
|
40
|
+
| `no-fetch` | error | `fetch()` outside helpers. Use the REST helper: `I.sendGetRequest`, `I.sendPostRequest` |
|
|
41
|
+
| `no-console` | warn | `console.*` outside helpers. Use `I.say()` |
|
|
42
|
+
| `no-only` | error on CI, warn locally | `Scenario.only`, `Feature.only`, `Data(...).only` |
|
|
43
|
+
| `no-pause` | error on CI, warn locally | `pause()` |
|
|
44
|
+
| `no-skip` | warn | `Scenario.skip`, `Feature.skip`, `xScenario`, `xFeature` |
|
|
45
|
+
| `secret-credentials` | error | `I.fillField` on a password, token, secret or API key field, or such a `process.env.*` passed to `I.*`, without `secret()` |
|
|
46
|
+
| `await-grab` | error | `I.grab*()` assigned or used without `await` |
|
|
47
|
+
| `no-actor-in-helper` | error | `I` inside a class extending `Helper`. Use `this.helpers[...]` |
|
|
48
|
+
| `raw-browser-in-test` | warn | `I.use*To`, `I.executeScript` in a Scenario. Move it into a helper or page object |
|
|
49
|
+
|
|
50
|
+
"On CI" means the `CI` environment variable is set, which every CI provider does. Locally `pause()` and `.only` stay warnings, so debugging stubs don't fail the lint.
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
Set rule levels in a `lint` section of `codecept.conf.js`:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
export const config = {
|
|
58
|
+
tests: './*_test.js',
|
|
59
|
+
lint: {
|
|
60
|
+
rules: { 'no-if-in-test': 'warn', 'raw-browser-in-test': 'off' },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Levels are `error`, `warn` or `off`.
|
|
66
|
+
|
|
67
|
+
To allow a single case, suppress it inline — the rule id is required:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
I.wait(1) // codeceptjs-lint-disable-line no-fixed-wait
|
|
71
|
+
|
|
72
|
+
// codeceptjs-lint-disable-next-line no-fixed-wait
|
|
73
|
+
I.wait(1)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
TypeScript is read with Node's type stripping (Node 22.13+). Files that use `enum` or `namespace` are reported as parse errors. A TypeScript config that imports `.js` paths of `.ts` files (the tsx/esbuild convention) can't be loaded by plain Node; if the project has `tsx` installed, the linter registers its loader automatically, otherwise pass paths explicitly.
|
|
77
|
+
|
|
78
|
+
## CI
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- run: npx @codeceptjs/lint
|
|
82
|
+
- run: npx codeceptjs run
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Blocking Bad Edits by an AI Agent
|
|
86
|
+
|
|
87
|
+
The linter can check an agent's edit before it lands. Add a `PreToolUse` hook to `.claude/settings.json`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"hooks": {
|
|
92
|
+
"PreToolUse": [
|
|
93
|
+
{
|
|
94
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
95
|
+
"hooks": [{ "type": "command", "command": "npx @codeceptjs/lint --hook claude" }]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The hook checks only files the config lints. It builds the file as it would look after the edit, and blocks the edit only when it adds a new error. Errors already in the file and warnings never block, so an agent can still add a `pause()` stub or touch a legacy test. If the hook fails, the edit is allowed. See [Agentic Testing](/agents/) for the full agent toolkit.
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import Linter from '@codeceptjs/lint'
|
|
108
|
+
|
|
109
|
+
const linter = await Linter.load('codecept.conf.js')
|
|
110
|
+
for (const file of linter.files()) console.log(linter.lintFile(file))
|
|
111
|
+
```
|
package/docs/mobile.md
CHANGED
|
@@ -231,6 +231,7 @@ Accessibility id is recommended to use for locating element, as it rarely change
|
|
|
231
231
|
* For [React Native for Android see our special guide](mobile-react-native-locators.md).
|
|
232
232
|
|
|
233
233
|
> If you test React Native application, consider using [Detox helper](/detox) for faster tests.
|
|
234
|
+
> If you want mobile tests without an Appium server at all, consider the experimental [Mobilewright helper](/mobilewright).
|
|
234
235
|
|
|
235
236
|
Add `~` prefix to search for element by its accessibility id:
|
|
236
237
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /mobilewright
|
|
3
|
+
title: Mobile Testing with Mobilewright
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> Warning! Mobilewright support in CodeceptJS is experimental. Please try it and help us to test it and improve it. [See Mobilewright helper repository](https://github.com/codeceptjs/mobilewright-helper).
|
|
7
|
+
|
|
8
|
+
[Appium](/mobile) is the default choice for mobile testing with CodeceptJS, but it requires a running Appium server and slow UI-dump based automation. [Mobilewright](https://mobilewright.dev) takes a different approach: it talks to simulators, emulators and real devices directly via adb (Android) and mobilecli (iOS), and reads the accessibility tree instead of taking UI snapshots.
|
|
9
|
+
|
|
10
|
+
**Pros** (vs Appium):
|
|
11
|
+
|
|
12
|
+
* no Appium server, no WebDriver — a device and `adb` is all you need
|
|
13
|
+
* fast startup: a full suite of 4 scenarios runs in about a minute, ~7-10s per test including app installation
|
|
14
|
+
* Playwright-like locators: `{ role: 'button', name: 'Sign In' }`, test ids, accessibility labels, and human-style "label next to the field" matching
|
|
15
|
+
* same locator conventions as the Appium helper, so most tests move between the two helpers unchanged
|
|
16
|
+
* interactive [pause](/basics#pause), auto-retries and other CodeceptJS features work out of the box
|
|
17
|
+
|
|
18
|
+
**Cons**:
|
|
19
|
+
|
|
20
|
+
* experimental (v0.1.x), the API may change
|
|
21
|
+
* no XPath or CSS locators — only accessibility-based ones
|
|
22
|
+
* text on screen ≠ text in the accessibility tree: some visible messages can't be asserted by text (common for React Native error labels)
|
|
23
|
+
* iOS + React Native: taps may not reach RN controls (known mobilecli issue); Android works
|
|
24
|
+
|
|
25
|
+
CodeceptJS allows you to try different options and choose the one which works best for you. If Mobilewright covers your app, tests run dramatically faster than with Appium.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Requirements: Node.js 22.12+, CodeceptJS 4, a booted simulator or emulator, or a connected device.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npm i @codeceptjs/mobilewright --save-dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Check your environment with one command:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npx mobilewright doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It reports Node.js, mobilecli versions, connected devices and common misconfigurations. List available devices:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
npx mobilewright devices
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
exports.config = {
|
|
51
|
+
tests: './*_test.js',
|
|
52
|
+
output: './output',
|
|
53
|
+
helpers: {
|
|
54
|
+
Mobilewright: {
|
|
55
|
+
require: '@codeceptjs/mobilewright',
|
|
56
|
+
platform: 'android', // or 'ios'
|
|
57
|
+
app: 'com.saucelabs.mydemoapp.rn', // package name (Android) or bundle id (iOS)
|
|
58
|
+
installApps: './MyDemoApp.apk', // installed before tests
|
|
59
|
+
actionTimeout: 10000,
|
|
60
|
+
waitForTimeout: 10000,
|
|
61
|
+
animations: 'off', // disables system animations on device
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
plugins: {
|
|
65
|
+
screenshot: { enabled: true },
|
|
66
|
+
retryFailedStep: { enabled: true },
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Option | Default | Description |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `platform` | required | `ios` or `android` |
|
|
74
|
+
| `app` | — | bundle id (iOS) or package name (Android) of the app under test |
|
|
75
|
+
| `installApps` | — | path or list of paths to `.apk`, `.app` or `.ipa` files installed before tests |
|
|
76
|
+
| `deviceId` | first booted device | UDID or serial of the device |
|
|
77
|
+
| `deviceName` | — | string or RegExp matched against device names |
|
|
78
|
+
| `url` | — | URL of a running mobilecli server; when set, the helper does not start one |
|
|
79
|
+
| `restart` | `'app'` | `'app'` relaunches the app before every test, `'session'` launches it once, `false` never launches it |
|
|
80
|
+
| `animations` | unchanged | `'off'` disables system animations |
|
|
81
|
+
|
|
82
|
+
The helper starts and stops a mobilecli server automatically; with `restart: 'app'` (default) the app is relaunched before every test, so no `Before()` hook is needed.
|
|
83
|
+
|
|
84
|
+
## Locators
|
|
85
|
+
|
|
86
|
+
Mobilewright reads the accessibility tree. Locators follow the [Appium helper](/mobile) conventions:
|
|
87
|
+
|
|
88
|
+
| Locator | Finds |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `'~login'` | test id, then accessibility label |
|
|
91
|
+
| `'#login'` | test id; on Android also matches `com.app:id/login` |
|
|
92
|
+
| `{ role: 'button', name: 'Sign In' }` | element by role and accessible name |
|
|
93
|
+
| `{ label }`, `{ text }`, `{ placeholder }`, `{ testId }` | the matching attribute; `text` accepts a RegExp |
|
|
94
|
+
| `{ android: '#a', ios: '~b' }` | a different locator per platform |
|
|
95
|
+
|
|
96
|
+
For `fillField` a plain name matches a text field by its accessibility label, its placeholder, or **the text label displayed next to the field** — the way a human reads a form. XPath and CSS are not supported and fail with an error suggesting a supported locator.
|
|
97
|
+
|
|
98
|
+
## A Test
|
|
99
|
+
|
|
100
|
+
A test against the [Sauce Labs My Demo App](https://github.com/saucelabs/my-demo-app-rn) (React Native):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { secret } from 'codeceptjs'
|
|
104
|
+
|
|
105
|
+
Feature('Sauce Labs Store')
|
|
106
|
+
|
|
107
|
+
Scenario('Open a product and add it to cart', ({ I }) => {
|
|
108
|
+
I.waitForText('Products', 30)
|
|
109
|
+
I.click('Sauce Labs Backpack') // click by visible text
|
|
110
|
+
I.waitForElement('~Add To Cart button', 15)
|
|
111
|
+
I.click('~Add To Cart button') // ~ = accessibility id / label
|
|
112
|
+
I.see('1', '~cart badge')
|
|
113
|
+
I.saveElementScreenshot('~cart badge', 'cart-badge.png') // element screenshot
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
Scenario('Deep link and fill fields by label', ({ I }) => {
|
|
117
|
+
I.waitForText('Products', 30)
|
|
118
|
+
I.amOnPage('mydemoapprn://login') // deep link
|
|
119
|
+
I.fillField('Username', 'bob@example.com') // found by the label next to the field
|
|
120
|
+
I.fillField('Password', secret('10203040')) // secret() keeps the password out of logs
|
|
121
|
+
I.click('~Login button')
|
|
122
|
+
I.waitForText('Products', 15)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
Scenario('Rotate the screen', ({ I }) => {
|
|
126
|
+
I.setOrientation('LANDSCAPE')
|
|
127
|
+
I.seeOrientationIs('LANDSCAPE')
|
|
128
|
+
I.setOrientation('PORTRAIT')
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run tests as usual:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
npx codeceptjs run --steps
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Debugging
|
|
139
|
+
|
|
140
|
+
* [Interactive pause](/basics#pause) works: drop `pause()` into a test and try steps and locators by hand.
|
|
141
|
+
* `DEBUG='mw:*' npx codeceptjs run` prints the whole device dialogue over WebSocket.
|
|
142
|
+
* `await I.grabSource()` returns the accessibility tree as JSON — the main tool for debugging locators: it shows every label, testId and element type on the current screen.
|
|
143
|
+
* `npx mobilewright screenshot -o screen.png` takes a device screenshot without any test.
|
|
144
|
+
* `npx mobilewright codegen` records taps on the device and generates a test; `npx mobilewright inspect` opens a live element inspector in the browser.
|
|
145
|
+
|
|
146
|
+
## Appium vs Mobilewright
|
|
147
|
+
|
|
148
|
+
| | Appium | Mobilewright |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| Server | Appium server + WebDriver required | none (adb / mobilecli direct) |
|
|
151
|
+
| Automation basis | UI dumps | accessibility tree |
|
|
152
|
+
| Speed | slow startup, minutes per suite | ~7-10s per test incl. app install |
|
|
153
|
+
| Locators | XPath, CSS, accessibility ids | accessibility ids, roles, labels, text |
|
|
154
|
+
| App instrumentation | none needed | none needed |
|
|
155
|
+
| Maturity | industry standard, huge ecosystem | experimental (v0.1.x) |
|
|
156
|
+
| Cloud (SauceLabs, BrowserStack) | yes | not yet |
|
|
157
|
+
|
|
158
|
+
Both helpers share locator conventions, so most tests move between them with little change — start with Mobilewright for fast local runs and keep an Appium job for cloud testing and XPath-heavy suites.
|
|
159
|
+
|
|
160
|
+
## Known Pitfalls
|
|
161
|
+
|
|
162
|
+
1. **Text on screen ≠ text in the accessibility tree.** Error messages may be visible on a screenshot but missing from the tree — text assertions on such elements fail. Check with `I.grabSource()` before asserting by text.
|
|
163
|
+
2. **`clearField` on React Native fields is unstable**; the helper falls back to backspaces, but masked values (passwords) report the wrong length. Filling a password field twice is a flakiness candidate.
|
|
164
|
+
3. **React Native components may not expose testIds** — on such apps `#id` locators don't match; `~` locators fall back to labels and usually work.
|
|
165
|
+
4. **App state persists between tests**: `restart` relaunches the app but does not clear its data.
|
|
166
|
+
5. **iOS + React Native**: taps may not reach RN controls (known mobilecli issue); Android works.
|
|
@@ -75,10 +75,10 @@ export default async function (selectedRuns, options) {
|
|
|
75
75
|
if (options.config) {
|
|
76
76
|
// update paths to config path
|
|
77
77
|
if (config.tests) {
|
|
78
|
-
config.tests =
|
|
78
|
+
config.tests = resolvePatterns(testRoot, config.tests)
|
|
79
79
|
}
|
|
80
80
|
if (config.gherkin && config.gherkin.features) {
|
|
81
|
-
config.gherkin.features =
|
|
81
|
+
config.gherkin.features = resolvePatterns(testRoot, config.gherkin.features)
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -117,6 +117,11 @@ export default async function (selectedRuns, options) {
|
|
|
117
117
|
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export function resolvePatterns(testRoot, patterns) {
|
|
121
|
+
const resolve = pattern => path.resolve(testRoot, pattern).split(path.sep).join(path.posix.sep)
|
|
122
|
+
return Array.isArray(patterns) ? patterns.map(resolve) : resolve(patterns)
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
function executeRun(runName, runConfig) {
|
|
121
126
|
// clone config
|
|
122
127
|
let overriddenConfig = { ...config }
|
package/lib/container.js
CHANGED
|
@@ -61,7 +61,7 @@ class Container {
|
|
|
61
61
|
*
|
|
62
62
|
*/
|
|
63
63
|
static get STANDARD_ACTING_HELPERS() {
|
|
64
|
-
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium', 'Obscura', 'Lightpanda']
|
|
64
|
+
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium', 'Obscura', 'Lightpanda', 'Moli']
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Create container with all required helpers and support objects
|
package/lib/helper/Decision.js
CHANGED
|
@@ -244,7 +244,7 @@ class Decision extends Helper {
|
|
|
244
244
|
|
|
245
245
|
_actingHelper() {
|
|
246
246
|
const helper = pickActingHelper(Container.helpers())
|
|
247
|
-
if (!helper) throw new Error('Decision helper requires a web helper: Playwright, Puppeteer, WebDriver, Obscura or
|
|
247
|
+
if (!helper) throw new Error('Decision helper requires a web helper: Playwright, Puppeteer, WebDriver, Obscura, Lightpanda or Moli')
|
|
248
248
|
return helper
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import CDPBrowser from './CDPBrowser.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ## Configuration
|
|
5
|
+
*
|
|
6
|
+
* This helper should be configured in codecept.conf.js
|
|
7
|
+
*
|
|
8
|
+
* @typedef MoliConfig
|
|
9
|
+
* @type {object}
|
|
10
|
+
* @prop {string} [endpoint] - explicit CDP endpoint. Setting this switches the helper to ATTACH
|
|
11
|
+
* mode: it only connects, and never spawns or kills a process, no matter what else is configured.
|
|
12
|
+
* Leave it unset for SELF-MANAGED mode (see below).
|
|
13
|
+
* @prop {string} [binaryPath] - path to the `moli` executable, used in SELF-MANAGED mode
|
|
14
|
+
* (`endpoint` unset). Checked before `MOLI_PATH` and `PATH`.
|
|
15
|
+
* @prop {number} [port] - port `moli serve` listens on, in SELF-MANAGED mode. When unset, a free
|
|
16
|
+
* port is picked automatically, which is what makes `run-workers` collision-free — every worker
|
|
17
|
+
* gets its own instance on its own port with zero config.
|
|
18
|
+
* @prop {number} [serverStartTimeout=15000] - milliseconds to wait for a spawned `moli serve` to
|
|
19
|
+
* answer `/json/version` before `_connect` gives up.
|
|
20
|
+
*/
|
|
21
|
+
const config = {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Moli drives [Moli](https://github.com/lexmount/moli), an open-source headless browser written
|
|
25
|
+
* in Rust for automation, exposed over the Chrome DevTools Protocol. It runs real JavaScript in V8
|
|
26
|
+
* and computes styles with Servo's Stylo, but by default skips layout and painting: element boxes
|
|
27
|
+
* use deterministic placeholder geometry, so there are no screenshots and no coordinate input.
|
|
28
|
+
*
|
|
29
|
+
* This helper is a thin `CDPBrowser` subclass: it changes nothing about how locating or acting on
|
|
30
|
+
* elements works, it only pins the config presets Moli requires and manages the `moli serve`
|
|
31
|
+
* process lifecycle, the same way Playwright manages its own browser process.
|
|
32
|
+
*
|
|
33
|
+
* > Moli support is experimental. Pin the browser version in CI and keep a
|
|
34
|
+
* > Playwright/WebDriver job for browser-compatibility coverage.
|
|
35
|
+
*
|
|
36
|
+
* ## Modes
|
|
37
|
+
*
|
|
38
|
+
* - **ATTACH** — `endpoint` is set explicitly in the config. The helper only connects to it; it
|
|
39
|
+
* never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
|
|
40
|
+
* - **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
|
|
41
|
+
* the config, then the `MOLI_PATH` environment variable, then `moli` on `PATH`. The helper
|
|
42
|
+
* spawns `moli serve --host 127.0.0.1 --port <port>` (`port` from the config, or a free port
|
|
43
|
+
* picked automatically), waits for it to answer, connects, and kills it in `_finishTest`.
|
|
44
|
+
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
45
|
+
* answers `http://127.0.0.1:9222/json/version` (e.g. `moli serve` started by hand). The helper
|
|
46
|
+
* attaches to it and never kills it. If neither a binary nor a running server on :9222 can be
|
|
47
|
+
* found, the helper throws a loud, actionable error.
|
|
48
|
+
*
|
|
49
|
+
* ## Install
|
|
50
|
+
*
|
|
51
|
+
* Follow the [official installation instructions](https://github.com/lexmount/moli#direct-installation),
|
|
52
|
+
* then put `moli` on your `PATH`, or point `binaryPath`/`MOLI_PATH` at it. The helper then
|
|
53
|
+
* launches and tears it down automatically.
|
|
54
|
+
*
|
|
55
|
+
* ## Config presets
|
|
56
|
+
*
|
|
57
|
+
* These are set automatically and only need overriding for unusual setups:
|
|
58
|
+
*
|
|
59
|
+
* | option | value | why |
|
|
60
|
+
* | --- | --- | --- |
|
|
61
|
+
* | `input` | `synthetic` | without layout Moli rejects coordinate mouse input, so `click` always takes the `forceClick` path |
|
|
62
|
+
* | `xpathPolyfill` | `auto` | probed per binary/page |
|
|
63
|
+
* | `capabilities.layout` | `real` | Moli computes real styles and gives displayed elements a non-empty placeholder box, which is enough for `seeElement` and the `waitForVisible` family; it is pinned because the runtime probe would also enable coordinate input |
|
|
64
|
+
* | `capabilities.screenshot` | `false` | screenshots require `moli serve --layout`, which this helper does not use: its layout is a snapshot that goes stale when the page changes |
|
|
65
|
+
*
|
|
66
|
+
* ## Limitations
|
|
67
|
+
*
|
|
68
|
+
* - No screenshots and no screencast: `saveScreenshot` and `saveElementScreenshot` do nothing (a debug message is printed).
|
|
69
|
+
* - No coordinate-based input (`clickXY`).
|
|
70
|
+
* - No frames or popups.
|
|
71
|
+
* - Uploaded files are sent as `application/octet-stream` regardless of their extension.
|
|
72
|
+
* - The default stylesheet does not color links: `a` computes `color: rgb(0, 0, 0)`.
|
|
73
|
+
*
|
|
74
|
+
* <!-- configuration -->
|
|
75
|
+
*
|
|
76
|
+
* ## Example
|
|
77
|
+
*
|
|
78
|
+
* ```js
|
|
79
|
+
* // inside codecept.conf.js — SELF-LAUNCH mode (recommended): the helper finds/starts/stops
|
|
80
|
+
* // moli serve on its own, on a free port. Ideal for run-workers: every worker gets its own
|
|
81
|
+
* // instance with no config.
|
|
82
|
+
* {
|
|
83
|
+
* helpers: {
|
|
84
|
+
* Moli: {
|
|
85
|
+
* url: 'http://localhost',
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* ```js
|
|
92
|
+
* // ATTACH mode — connect to a Moli instance you manage yourself (remote host, container, etc.)
|
|
93
|
+
* {
|
|
94
|
+
* helpers: {
|
|
95
|
+
* Moli: {
|
|
96
|
+
* url: 'http://localhost',
|
|
97
|
+
* endpoint: 'http://127.0.0.1:9222',
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* ## Methods
|
|
104
|
+
*/
|
|
105
|
+
class Moli extends CDPBrowser {
|
|
106
|
+
/**
|
|
107
|
+
* @param {MoliConfig} config
|
|
108
|
+
*/
|
|
109
|
+
constructor(config) {
|
|
110
|
+
super({
|
|
111
|
+
input: 'synthetic',
|
|
112
|
+
xpathPolyfill: 'auto',
|
|
113
|
+
...config,
|
|
114
|
+
capabilities: { layout: 'real', screenshot: false, ...((config && config.capabilities) || {}) },
|
|
115
|
+
})
|
|
116
|
+
this._manageServer(config, {
|
|
117
|
+
name: 'moli',
|
|
118
|
+
envVar: 'MOLI_PATH',
|
|
119
|
+
args: port => ['serve', '--host', '127.0.0.1', '--port', String(port)],
|
|
120
|
+
releases: 'https://github.com/lexmount/moli/releases',
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default Moli
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1079,7 +1079,6 @@ class WebDriver extends Helper {
|
|
|
1079
1079
|
*
|
|
1080
1080
|
*/
|
|
1081
1081
|
async click(locator, context = null) {
|
|
1082
|
-
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
1083
1082
|
const locateFn = prepareLocateFn.call(this, context)
|
|
1084
1083
|
|
|
1085
1084
|
const res = await findClickable.call(this, locator, locateFn)
|
|
@@ -1090,7 +1089,7 @@ class WebDriver extends Helper {
|
|
|
1090
1089
|
}
|
|
1091
1090
|
const elem = selectElement(res, locator, this)
|
|
1092
1091
|
highlightActiveElement.call(this, elem)
|
|
1093
|
-
return this.browser
|
|
1092
|
+
return this.browser.elementClick(getElementId(elem))
|
|
1094
1093
|
}
|
|
1095
1094
|
|
|
1096
1095
|
/**
|
|
@@ -1390,7 +1389,6 @@ class WebDriver extends Helper {
|
|
|
1390
1389
|
* {{> checkOption }}
|
|
1391
1390
|
*/
|
|
1392
1391
|
async checkOption(field, context = null) {
|
|
1393
|
-
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
1394
1392
|
const locateFn = prepareLocateFn.call(this, context)
|
|
1395
1393
|
|
|
1396
1394
|
const res = await findCheckable.call(this, field, locateFn)
|
|
@@ -1403,7 +1401,7 @@ class WebDriver extends Helper {
|
|
|
1403
1401
|
const isSelected = await isElementChecked(this.browser, elementId)
|
|
1404
1402
|
|
|
1405
1403
|
if (isSelected) return Promise.resolve(true)
|
|
1406
|
-
return this.browser
|
|
1404
|
+
return this.browser.elementClick(elementId)
|
|
1407
1405
|
}
|
|
1408
1406
|
|
|
1409
1407
|
/**
|
|
@@ -1411,7 +1409,6 @@ class WebDriver extends Helper {
|
|
|
1411
1409
|
* {{> uncheckOption }}
|
|
1412
1410
|
*/
|
|
1413
1411
|
async uncheckOption(field, context = null) {
|
|
1414
|
-
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
1415
1412
|
const locateFn = prepareLocateFn.call(this, context)
|
|
1416
1413
|
|
|
1417
1414
|
const res = await findCheckable.call(this, field, locateFn)
|
|
@@ -1424,7 +1421,7 @@ class WebDriver extends Helper {
|
|
|
1424
1421
|
const isSelected = await isElementChecked(this.browser, elementId)
|
|
1425
1422
|
|
|
1426
1423
|
if (!isSelected) return Promise.resolve(true)
|
|
1427
|
-
return this.browser
|
|
1424
|
+
return this.browser.elementClick(elementId)
|
|
1428
1425
|
}
|
|
1429
1426
|
|
|
1430
1427
|
/**
|
package/lib/mocha/cli.js
CHANGED
package/lib/mocha/index.js
CHANGED
package/lib/mocha/suite.js
CHANGED
package/lib/mocha/test.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import Test from 'mocha
|
|
2
|
-
import Suite from 'mocha/lib/suite.js'
|
|
1
|
+
import { Suite, Test } from 'mocha'
|
|
3
2
|
import { genTestId, serializeError, clearString, relativeDir } from '../utils.js'
|
|
4
3
|
import Step from '../step/base.js'
|
|
5
4
|
import { enhanceMochaSuite } from './suite.js'
|
package/lib/mocha/ui.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createTest } from './test.js'
|
|
|
7
7
|
import { createSuite } from './suite.js'
|
|
8
8
|
import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } from './hooks.js'
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
|
-
import
|
|
10
|
+
import { createCommon } from 'mocha/lib/interfaces/common.js'
|
|
11
11
|
import container from '../container.js'
|
|
12
12
|
import store from '../store.js'
|
|
13
13
|
|
|
@@ -49,7 +49,7 @@ export default function (suite) {
|
|
|
49
49
|
let afterEachHooksAreLoaded
|
|
50
50
|
|
|
51
51
|
suite.on('pre-require', (context, file, mocha) => {
|
|
52
|
-
const cmn =
|
|
52
|
+
const cmn = createCommon(suites, context, mocha)
|
|
53
53
|
|
|
54
54
|
const addScenario = function (title, opts = {}, fn) {
|
|
55
55
|
const suite = suites[0]
|
|
@@ -74,7 +74,7 @@ export default function (suite) {
|
|
|
74
74
|
context.BeforeAll = cmn.before
|
|
75
75
|
context.AfterAll = cmn.after
|
|
76
76
|
|
|
77
|
-
context.run = mocha.options.delay &&
|
|
77
|
+
context.run = mocha.options.delay && cmn.runWithSuite(suite)
|
|
78
78
|
/**
|
|
79
79
|
* Describe a "suite" with the given `title`
|
|
80
80
|
* and callback `fn` containing nested suites
|
package/lib/plugin/pause.js
CHANGED
|
@@ -109,7 +109,7 @@ function initUrlMode(pattern) {
|
|
|
109
109
|
const helper = getBrowserHelper()
|
|
110
110
|
|
|
111
111
|
if (!helper) {
|
|
112
|
-
output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium, Obscura, Lightpanda)')
|
|
112
|
+
output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium, Obscura, Lightpanda, Moli)')
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
115
|
|