codeceptjs 4.2.0-beta.4 → 4.2.1-beta.1
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/bin/codecept.js +11 -0
- package/docs/agents.md +26 -1
- package/docs/ai.md +57 -0
- package/docs/alternative-browsers.md +175 -148
- package/docs/assertions.md +74 -0
- package/docs/bootstrap.md +2 -0
- package/docs/commands.md +45 -0
- package/docs/configuration.md +37 -0
- package/docs/debugging.md +1 -1
- package/docs/helpers/ApiDataFactory.md +20 -20
- package/docs/helpers/Appium.md +1615 -670
- package/docs/helpers/Decision.md +146 -0
- package/docs/helpers/GraphQL.md +31 -31
- package/docs/helpers/GraphQLDataFactory.md +20 -20
- package/docs/helpers/Kitesurf.md +1808 -25
- package/docs/helpers/Lightpanda.md +139 -0
- package/docs/helpers/Obscura.md +1799 -74
- package/docs/helpers/Playwright.md +490 -428
- package/docs/helpers/Puppeteer.md +317 -255
- package/docs/helpers/REST.md +43 -43
- package/docs/helpers/WebDriver.md +237 -175
- package/docs/installation.md +16 -0
- package/docs/mcp.md +7 -1
- package/docs/parallel.md +35 -1
- package/docs/plugins/pause.md +2 -1
- package/docs/plugins/sitemap.md +48 -0
- package/docs/plugins.md +4 -0
- package/lib/ai.js +81 -2
- package/lib/codecept.js +32 -2
- package/lib/command/init.js +22 -0
- package/lib/command/initAgent.js +78 -0
- package/lib/command/run-multiple.js +3 -0
- package/lib/command/run-workers.js +10 -3
- package/lib/command/run.js +3 -2
- package/lib/command/utils.js +15 -0
- package/lib/command/workers/runTests.js +10 -10
- package/lib/container.js +4 -2
- package/lib/effects.js +19 -1
- package/lib/helper/Appium.js +23 -0
- package/lib/helper/CDPBrowser.js +387 -1338
- package/lib/helper/Decision.js +256 -0
- package/lib/helper/Kitesurf.js +3 -21
- package/lib/helper/Lightpanda.js +137 -0
- package/lib/helper/Obscura.js +21 -238
- package/lib/helper/Playwright.js +50 -4
- package/lib/helper/Puppeteer.js +62 -8
- package/lib/helper/WebDriver.js +45 -1
- package/lib/helper/extras/clipboard.js +31 -0
- package/lib/listener/result.js +4 -0
- package/lib/mocha/bdd.js +19 -1
- package/lib/plugin/junitReporter.js +4 -1
- package/lib/plugin/pause.js +4 -1
- package/lib/plugin/sitemap.js +228 -0
- package/lib/recorder.js +19 -18
- package/lib/result.js +16 -0
- package/lib/step/base.js +12 -0
- package/lib/utils.js +2 -10
- package/lib/webServer.js +193 -0
- package/lib/workers.js +118 -31
- package/package.json +4 -2
- package/typings/index.d.ts +60 -7
- package/typings/promiseBasedTypes.d.ts +985 -814
- package/typings/types.d.ts +1084 -815
- package/docs/helpers/CDPBrowser.md +0 -2138
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import Helper from '@codeceptjs/helper'
|
|
5
|
+
import Container from '../container.js'
|
|
6
|
+
import Config from '../config.js'
|
|
7
|
+
import store from '../store.js'
|
|
8
|
+
import output from '../output.js'
|
|
9
|
+
import AssertionFailedError from '../assert/error.js'
|
|
10
|
+
import { compactAriaSnapshot } from '../aria.js'
|
|
11
|
+
import { minifyHtml } from '../html.js'
|
|
12
|
+
import { pickActingHelper } from '../utils/trace.js'
|
|
13
|
+
import { DecisionAI, DecisionConnectionError } from '../ai.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Asserts statements about the current page with a decision model.
|
|
17
|
+
*
|
|
18
|
+
* Decision models (like [Jev](https://openrouter.ai/typesafe/jev-1.13) or [Clef](https://openrouter.ai/cloudflare/clef))
|
|
19
|
+
* do not generate text. They read the page state and return the probability that a statement is true.
|
|
20
|
+
* A statement passes when its probability reaches the configured `confidence`.
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* I.decide('top level navigation is available');
|
|
24
|
+
* I.decide([
|
|
25
|
+
* 'checkout form has all required fields',
|
|
26
|
+
* 'success message is shown',
|
|
27
|
+
* 'submit button enabled',
|
|
28
|
+
* 'cancel button present',
|
|
29
|
+
* ]);
|
|
30
|
+
* I.decideVisually('sidebar is shown');
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The model receives the page URL, title and ARIA snapshot.
|
|
34
|
+
* Helpers without ARIA snapshots (anything but Playwright) send minified HTML instead.
|
|
35
|
+
* `decideVisually` also sends a screenshot, so it requires a model with image input.
|
|
36
|
+
*
|
|
37
|
+
* This helper must be enabled together with a web helper (Playwright, Puppeteer, WebDriver).
|
|
38
|
+
*
|
|
39
|
+
* ## Configuration
|
|
40
|
+
*
|
|
41
|
+
* Decision model is created with the OpenRouter provider from `@openrouter/ai-sdk-provider`
|
|
42
|
+
* and configured in the `ai.decisionModel` section of the config:
|
|
43
|
+
*
|
|
44
|
+
* ```js
|
|
45
|
+
* import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
46
|
+
*
|
|
47
|
+
* const openrouter = createOpenRouter({
|
|
48
|
+
* apiKey: process.env.OPENROUTER_API_KEY,
|
|
49
|
+
* })
|
|
50
|
+
*
|
|
51
|
+
* export default {
|
|
52
|
+
* ai: {
|
|
53
|
+
* decisionModel: {
|
|
54
|
+
* model: openrouter.evaluationModel('typesafe/jev-1.13'),
|
|
55
|
+
* confidence: 0.8,
|
|
56
|
+
* },
|
|
57
|
+
* },
|
|
58
|
+
* helpers: {
|
|
59
|
+
* Playwright: { url: 'http://localhost' },
|
|
60
|
+
* Decision: {},
|
|
61
|
+
* },
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* * `model` - decision model used by `decide`.
|
|
66
|
+
* * `confidence` (default: `0.7`) - minimal probability, between 0 and 1, for a statement to pass.
|
|
67
|
+
* * `timeout` (default: `15000`) - request timeout in ms.
|
|
68
|
+
* * `maxLength` (default: `12000`) - maximal length of ARIA snapshot or HTML sent to the model.
|
|
69
|
+
*
|
|
70
|
+
* `decideVisually` uses a separate decision model with image input, configured as `visualModel`:
|
|
71
|
+
*
|
|
72
|
+
* ```js
|
|
73
|
+
* decisionModel: {
|
|
74
|
+
* model: openrouter.evaluationModel('typesafe/jev-1.13'),
|
|
75
|
+
* visualModel: openrouter.evaluationModel('cloudflare/clef'),
|
|
76
|
+
* },
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* The helper has one option:
|
|
80
|
+
*
|
|
81
|
+
* * `mode` (default: `assert`) - how decisions are executed:
|
|
82
|
+
* * `assert` - request the model and fail the step when a statement is not confirmed.
|
|
83
|
+
* * `report` - request the model but never fail, even on API errors. Results and errors are added to the step as a comment.
|
|
84
|
+
* * `skip` - do not request the model, every decision passes.
|
|
85
|
+
*
|
|
86
|
+
* ```js
|
|
87
|
+
* Decision: { mode: process.env.CI ? 'assert' : 'skip' }
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* Decisions work without the `--ai` flag.
|
|
91
|
+
* Browse all decision models at [OpenRouter](https://openrouter.ai/models?output_modalities=decisions).
|
|
92
|
+
*
|
|
93
|
+
* ## Methods
|
|
94
|
+
*/
|
|
95
|
+
class Decision extends Helper {
|
|
96
|
+
constructor(config = {}) {
|
|
97
|
+
super(config)
|
|
98
|
+
const modes = ['assert', 'report', 'skip']
|
|
99
|
+
this.mode = config.mode || 'assert'
|
|
100
|
+
if (!modes.includes(this.mode)) throw new Error(`Unknown Decision helper mode "${this.mode}", use one of: ${modes.join(', ')}`)
|
|
101
|
+
this.decisionAI = new DecisionAI(Config.get('ai', {}).decisionModel)
|
|
102
|
+
this.options = this.decisionAI.config
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get isAsserting() {
|
|
106
|
+
return this.mode === 'assert'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get isReporting() {
|
|
110
|
+
return this.mode === 'report'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get isSkipping() {
|
|
114
|
+
return this.mode === 'skip'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Asserts that a statement (or each of statements) is true for the current page.
|
|
119
|
+
* The decision model receives page URL, title and ARIA snapshot.
|
|
120
|
+
* Multiple statements are checked in a single request and all of them must pass.
|
|
121
|
+
*
|
|
122
|
+
* ```js
|
|
123
|
+
* I.decide('user is logged in');
|
|
124
|
+
* I.decide(['submit button enabled', 'cancel button present']);
|
|
125
|
+
* const probability = await I.decide('cart is empty');
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @param {string|string[]} statements statement or list of statements to verify.
|
|
129
|
+
* @returns {Promise<number|number[]|undefined>} probability of each statement, `undefined` in `skip` mode.
|
|
130
|
+
*/
|
|
131
|
+
async decide(statements) {
|
|
132
|
+
if (this.isSkipping) return this._skip()
|
|
133
|
+
return this._run(async () => {
|
|
134
|
+
const state = await this._grabState()
|
|
135
|
+
return this._assert(statements, 'model', state)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Asserts that a statement (or each of statements) is true for the current page using a screenshot.
|
|
141
|
+
* The decision model receives page URL, title, ARIA snapshot and a screenshot.
|
|
142
|
+
* Requires `visualModel` with image input. Experimental.
|
|
143
|
+
*
|
|
144
|
+
* ```js
|
|
145
|
+
* I.decideVisually('sidebar is shown');
|
|
146
|
+
* I.decideVisually(['logo is in the header', 'page uses dark theme']);
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @param {string|string[]} statements statement or list of statements to verify.
|
|
150
|
+
* @returns {Promise<number|number[]|undefined>} probability of each statement, `undefined` in `skip` mode.
|
|
151
|
+
*/
|
|
152
|
+
async decideVisually(statements) {
|
|
153
|
+
if (this.isSkipping) return this._skip()
|
|
154
|
+
return this._run(async () => {
|
|
155
|
+
const state = await this._grabState()
|
|
156
|
+
const screenshot = await this._grabScreenshot()
|
|
157
|
+
const content = [
|
|
158
|
+
{ type: 'text', text: JSON.stringify(state) },
|
|
159
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${screenshot}` } },
|
|
160
|
+
]
|
|
161
|
+
return this._assert(statements, 'visualModel', content)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_skip() {
|
|
166
|
+
if (store.currentStep) store.currentStep.comment = ' (skipped)'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _run(fn) {
|
|
170
|
+
try {
|
|
171
|
+
return await fn()
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (this.isReporting) {
|
|
174
|
+
if (store.currentStep) store.currentStep.comment = ` (error: ${err.message})`
|
|
175
|
+
output.say(` ✖ ${err.message}`, 'yellow')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (!(err instanceof DecisionConnectionError)) err.isTerminal = true
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async _assert(statements, option, state) {
|
|
184
|
+
const list = [statements].flat()
|
|
185
|
+
if (!list.length) throw new Error('No statements to decide')
|
|
186
|
+
|
|
187
|
+
const probabilities = await this.decisionAI.decide(option, state, list)
|
|
188
|
+
|
|
189
|
+
const failed = []
|
|
190
|
+
const results = list.map((statement, i) => {
|
|
191
|
+
const passed = probabilities[i] >= this.options.confidence
|
|
192
|
+
const result = `${passed ? '✔' : '✖'} ${statement} (${formatProbability(probabilities[i])})`
|
|
193
|
+
this.debugSection('Decision', result)
|
|
194
|
+
if (!passed) failed.push(`"${statement}" (${formatProbability(probabilities[i])})`)
|
|
195
|
+
return result
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (this.isReporting) {
|
|
199
|
+
if (store.currentStep) store.currentStep.comment = `\n${results.join('\n')}`
|
|
200
|
+
results.forEach(result => output.say(` ${result}`, result.startsWith('✔') ? 'green' : 'yellow'))
|
|
201
|
+
return Array.isArray(statements) ? probabilities : probabilities[0]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (failed.length) {
|
|
205
|
+
const err = new AssertionFailedError(
|
|
206
|
+
{ statements: failed.join(', '), confidence: formatProbability(this.options.confidence) },
|
|
207
|
+
'expected page to satisfy {{statements}} with confidence of {{confidence}}',
|
|
208
|
+
)
|
|
209
|
+
err.showDiff = false
|
|
210
|
+
err.message = err.cliMessage()
|
|
211
|
+
throw err
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return Array.isArray(statements) ? probabilities : probabilities[0]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async _grabState() {
|
|
218
|
+
const helper = this._actingHelper()
|
|
219
|
+
const state = {
|
|
220
|
+
url: await helper.grabCurrentUrl(),
|
|
221
|
+
title: await helper.grabTitle(),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (helper.grabAriaSnapshot) {
|
|
225
|
+
state.aria = compactAriaSnapshot(await helper.grabAriaSnapshot()).slice(0, this.options.maxLength)
|
|
226
|
+
return state
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
state.html = (await minifyHtml(await helper.grabSource())).slice(0, this.options.maxLength)
|
|
230
|
+
return state
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async _grabScreenshot() {
|
|
234
|
+
const helper = this._actingHelper()
|
|
235
|
+
const file = path.join(store.outputDir, `decision_${Date.now()}.png`)
|
|
236
|
+
await helper.saveScreenshot(file)
|
|
237
|
+
if (!fs.existsSync(file)) throw new Error('Could not take a screenshot for visual decision')
|
|
238
|
+
try {
|
|
239
|
+
return fs.readFileSync(file).toString('base64')
|
|
240
|
+
} finally {
|
|
241
|
+
fs.rmSync(file, { force: true })
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_actingHelper() {
|
|
246
|
+
const helper = pickActingHelper(Container.helpers())
|
|
247
|
+
if (!helper) throw new Error('Decision helper requires a web helper: Playwright, Puppeteer or WebDriver')
|
|
248
|
+
return helper
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatProbability(probability) {
|
|
253
|
+
return `${Math.round(probability * 100)}%`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default Decision
|
package/lib/helper/Kitesurf.js
CHANGED
|
@@ -4,8 +4,7 @@ import CDPBrowser from './CDPBrowser.js'
|
|
|
4
4
|
/**
|
|
5
5
|
* ## Configuration
|
|
6
6
|
*
|
|
7
|
-
* This helper should be configured in codecept.conf.js
|
|
8
|
-
* accepts (see its config table), plus:
|
|
7
|
+
* This helper should be configured in codecept.conf.js
|
|
9
8
|
*
|
|
10
9
|
* @typedef KitesurfConfig
|
|
11
10
|
* @type {object}
|
|
@@ -14,8 +13,8 @@ import CDPBrowser from './CDPBrowser.js'
|
|
|
14
13
|
* @prop {string} [apiToken] - Cloudflare API token; defaults to CF_API_TOKEN env var
|
|
15
14
|
* @prop {number} [keepAlive=240000] - session keep-alive time in milliseconds
|
|
16
15
|
* @prop {string} [apiBase=https://api.cloudflare.com/client/v4] - Cloudflare API base URL
|
|
17
|
-
* @prop {string} [input=cdp] -
|
|
18
|
-
* @prop {object} [capabilities] -
|
|
16
|
+
* @prop {string} [input=cdp] - how actions are dispatched: `cdp` (real mouse and keyboard events) or `synthetic` (DOM events).
|
|
17
|
+
* @prop {object} [capabilities] - browser capabilities; Kitesurf sets `{ layout: 'real', screenshot: true }`.
|
|
19
18
|
*/
|
|
20
19
|
const config = {}
|
|
21
20
|
|
|
@@ -90,14 +89,6 @@ class Kitesurf extends CDPBrowser {
|
|
|
90
89
|
this.cloudSessionId = null
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
/**
|
|
94
|
-
* Acquires a Kitesurf browser session from the Cloudflare Browser Run API and resolves it to
|
|
95
|
-
* the `wss://` debugger URL `CDPConnection` connects to. Overrides `CDPBrowser._resolveEndpoint`,
|
|
96
|
-
* which resolves a fixed local endpoint instead of provisioning a cloud session per test.
|
|
97
|
-
*
|
|
98
|
-
* @returns {Promise<string>} a `wss://` debugger URL ready to be passed to `CDPConnection`.
|
|
99
|
-
* @protected
|
|
100
|
-
*/
|
|
101
92
|
async _resolveEndpoint() {
|
|
102
93
|
if (!this.options.accountId || !this.options.apiToken) {
|
|
103
94
|
throw new Error('Kitesurf requires accountId and apiToken (or CF_ACCOUNT_ID / CF_API_TOKEN env vars)')
|
|
@@ -113,15 +104,6 @@ class Kitesurf extends CDPBrowser {
|
|
|
113
104
|
return data.webSocketDebuggerUrl
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
/**
|
|
117
|
-
* Closes the target as `CDPBrowser._finishTest` does, then releases the cloud session acquired
|
|
118
|
-
* in `_resolveEndpoint` via the Cloudflare API so it does not linger for the full `keepAlive`
|
|
119
|
-
* window. The release runs in a `finally` so a rejection while closing the CDP connection still
|
|
120
|
-
* frees the cloud session instead of leaving the browser alive until `keepAlive` expires; the
|
|
121
|
-
* session id is cleared before the request, so a repeated call never releases it twice.
|
|
122
|
-
*
|
|
123
|
-
* @protected
|
|
124
|
-
*/
|
|
125
107
|
async _finishTest() {
|
|
126
108
|
try {
|
|
127
109
|
await super._finishTest()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import CDPBrowser from './CDPBrowser.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ## Configuration
|
|
5
|
+
*
|
|
6
|
+
* This helper should be configured in codecept.conf.js
|
|
7
|
+
*
|
|
8
|
+
* @typedef LightpandaConfig
|
|
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 `lightpanda` executable, used in SELF-MANAGED mode
|
|
14
|
+
* (`endpoint` unset). Checked before `LIGHTPANDA_PATH` and `PATH`.
|
|
15
|
+
* @prop {number} [port] - port `lightpanda serve` listens on, in SELF-MANAGED mode. When unset, a
|
|
16
|
+
* free port is picked automatically, which is what makes `run-workers` collision-free — every
|
|
17
|
+
* worker gets its own instance on its own port with zero config.
|
|
18
|
+
* @prop {number} [serverStartTimeout=15000] - milliseconds to wait for a spawned `lightpanda serve`
|
|
19
|
+
* to answer `/json/version` before `_connect` gives up.
|
|
20
|
+
*/
|
|
21
|
+
const config = {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lightpanda drives [Lightpanda](https://lightpanda.io), a headless browser written from scratch
|
|
25
|
+
* for automation, exposed over the Chrome DevTools Protocol. It executes real JavaScript in V8 and
|
|
26
|
+
* implements the DOM and Web APIs, but has no graphical rendering engine: nothing is ever painted,
|
|
27
|
+
* so there are no screenshots.
|
|
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 Lightpanda requires and manages the
|
|
31
|
+
* `lightpanda serve` process lifecycle, the same way Playwright manages its own browser process.
|
|
32
|
+
*
|
|
33
|
+
* > Lightpanda 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 `LIGHTPANDA_PATH` environment variable, then `lightpanda` on `PATH`. The
|
|
42
|
+
* helper spawns `lightpanda serve --host 127.0.0.1 --port <port>` (`port` from the config, or a
|
|
43
|
+
* free port picked automatically), waits for it to answer, connects, and kills it in
|
|
44
|
+
* `_finishTest`.
|
|
45
|
+
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
46
|
+
* answers `http://127.0.0.1:9222/json/version` (e.g. `lightpanda serve` started by hand, or the
|
|
47
|
+
* `lightpanda/browser` Docker image). The helper attaches to it and never kills it. If neither a
|
|
48
|
+
* binary nor a running server on :9222 can be found, the helper throws a loud, actionable error.
|
|
49
|
+
*
|
|
50
|
+
* ## Install
|
|
51
|
+
*
|
|
52
|
+
* Follow the [official installation instructions](https://lightpanda.io/docs/open-source/installation),
|
|
53
|
+
* then put `lightpanda` on your `PATH`, or point `binaryPath`/`LIGHTPANDA_PATH` at it. The helper
|
|
54
|
+
* then launches and tears it down automatically.
|
|
55
|
+
*
|
|
56
|
+
* Lightpanda sends usage telemetry by default. This helper always spawns it with
|
|
57
|
+
* `LIGHTPANDA_DISABLE_TELEMETRY=true`; set that variable yourself when you start the server by hand.
|
|
58
|
+
*
|
|
59
|
+
* Lightpanda is licensed under AGPL-3.0. It runs as a separate process that CodeceptJS talks to
|
|
60
|
+
* over a WebSocket, so it does not change the license of CodeceptJS or of your tests.
|
|
61
|
+
*
|
|
62
|
+
* ## Config presets
|
|
63
|
+
*
|
|
64
|
+
* These are set automatically and only need overriding for unusual setups:
|
|
65
|
+
*
|
|
66
|
+
* | option | value | why |
|
|
67
|
+
* | --- | --- | --- |
|
|
68
|
+
* | `input` | `synthetic` | nothing is painted, so coordinates are not hit-tested; `click` always takes the `forceClick` path |
|
|
69
|
+
* | `xpathPolyfill` | `auto` | probed per binary/page |
|
|
70
|
+
* | `capabilities.layout` | `real` | Lightpanda computes visibility-related styles and element boxes without painting, which is enough for `seeElement` and the `waitForVisible` family; it is pinned because the runtime layout probe cannot run on Lightpanda's `about:blank` |
|
|
71
|
+
* | `capabilities.screenshot` | `false` | there is no rendering engine |
|
|
72
|
+
*
|
|
73
|
+
* ## Limitations
|
|
74
|
+
*
|
|
75
|
+
* - No screenshots and no screencast: `saveScreenshot` throws.
|
|
76
|
+
* - No frames or popups.
|
|
77
|
+
* - No scrolling: scroll positions always stay at 0.
|
|
78
|
+
* - Computed styles cover what visibility checks need (`display`, `visibility`, `opacity`,
|
|
79
|
+
* `pointer-events`); `grabCssPropertyFrom` and `seeCssPropertiesOnElements` are unreliable for
|
|
80
|
+
* other properties.
|
|
81
|
+
* - No coordinate-based input (`clickXY`).
|
|
82
|
+
* - No clipboard access.
|
|
83
|
+
* - Some rich text editors (TinyMCE, Trix, Monaco) never finish initialising.
|
|
84
|
+
*
|
|
85
|
+
* <!-- configuration -->
|
|
86
|
+
*
|
|
87
|
+
* ## Example
|
|
88
|
+
*
|
|
89
|
+
* ```js
|
|
90
|
+
* // inside codecept.conf.js — SELF-LAUNCH mode (recommended): the helper finds/starts/stops
|
|
91
|
+
* // lightpanda serve on its own, on a free port. Ideal for run-workers: every worker gets its
|
|
92
|
+
* // own instance with no config.
|
|
93
|
+
* {
|
|
94
|
+
* helpers: {
|
|
95
|
+
* Lightpanda: {
|
|
96
|
+
* url: 'http://localhost',
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* ```js
|
|
103
|
+
* // ATTACH mode — connect to a Lightpanda instance you manage yourself (remote host, container, etc.)
|
|
104
|
+
* {
|
|
105
|
+
* helpers: {
|
|
106
|
+
* Lightpanda: {
|
|
107
|
+
* url: 'http://localhost',
|
|
108
|
+
* endpoint: 'http://127.0.0.1:9222',
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* ## Methods
|
|
115
|
+
*/
|
|
116
|
+
class Lightpanda extends CDPBrowser {
|
|
117
|
+
/**
|
|
118
|
+
* @param {LightpandaConfig} config
|
|
119
|
+
*/
|
|
120
|
+
constructor(config) {
|
|
121
|
+
super({
|
|
122
|
+
input: 'synthetic',
|
|
123
|
+
xpathPolyfill: 'auto',
|
|
124
|
+
...config,
|
|
125
|
+
capabilities: { layout: 'real', screenshot: false, ...((config && config.capabilities) || {}) },
|
|
126
|
+
})
|
|
127
|
+
this._manageServer(config, {
|
|
128
|
+
name: 'lightpanda',
|
|
129
|
+
envVar: 'LIGHTPANDA_PATH',
|
|
130
|
+
args: port => ['serve', '--host', '127.0.0.1', '--port', String(port)],
|
|
131
|
+
env: { LIGHTPANDA_DISABLE_TELEMETRY: 'true' },
|
|
132
|
+
releases: 'https://github.com/lightpanda-io/browser/releases',
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default Lightpanda
|