@bloom-perf/yaml-pptr 2.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -14
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/model.d.ts +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.js +2 -1
- package/dist/puppet/interpreter.d.ts +9 -3
- package/dist/puppet/launcher.d.ts +9 -0
- package/dist/schema.json +900 -0
- package/dist/yaml/json-schema.d.ts +6 -0
- package/dist/yaml/parser.d.ts +1 -1
- package/dist/yaml/resolver.d.ts +20 -8
- package/dist/yaml/validator.d.ts +172 -172
- package/package.json +23 -19
package/README.md
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install @bloom-perf/yaml-pptr
|
|
14
|
+
npm install @bloom-perf/yaml-pptr puppeteer
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
`puppeteer` is a peer dependency (`^25.0.0`): your project controls its version.
|
|
18
|
+
|
|
17
19
|
## Quick Start
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
@@ -108,7 +110,93 @@ readYamlAndInterpret(yaml)
|
|
|
108
110
|
### 3. Run it
|
|
109
111
|
|
|
110
112
|
```bash
|
|
111
|
-
npx
|
|
113
|
+
npx tsx src/runner.ts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Options
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
await readYamlAndInterpret(yaml, {
|
|
120
|
+
headless: true, // default: true
|
|
121
|
+
launchOptions: { // extra Puppeteer launch options, per browser
|
|
122
|
+
firefox: { executablePath: '/path/to/firefox' },
|
|
123
|
+
},
|
|
124
|
+
browsers: { chrome: myBrowser }, // reuse already launched browsers (not closed)
|
|
125
|
+
logger: myLogger,
|
|
126
|
+
onPage: async (page, ctx) => { /* page hook */ },
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Option | Default | Description |
|
|
131
|
+
| ------ | ------- | ----------- |
|
|
132
|
+
| `headless` | `true` | Run launched browsers headless |
|
|
133
|
+
| `launchOptions` | `{}` | Puppeteer `LaunchOptions` per browser; `args` are appended to the defaults |
|
|
134
|
+
| `browsers` | `{}` | Pre-launched browsers to use instead of launching new ones |
|
|
135
|
+
| `logger` | console | Custom logger (`debug`, `info`, `warn`, `error`) |
|
|
136
|
+
| `onPage` | - | Callback invoked on every new page with `{ scenarioName, workerIndex, iteration }` |
|
|
137
|
+
|
|
138
|
+
### Firefox
|
|
139
|
+
|
|
140
|
+
Puppeteer drives Firefox through WebDriver BiDi, which requires a recent Firefox. Install the version matching your Puppeteer release with:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx puppeteer browsers install firefox
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To use another Firefox installed on the machine, pass its path with `launchOptions.firefox.executablePath`.
|
|
147
|
+
|
|
148
|
+
### Error handling
|
|
149
|
+
|
|
150
|
+
The configuration is fully checked before anything runs: YAML syntax, schema (an unknown key such as a typo is an error), and environment variables. If it is invalid, `readYamlAndInterpret` rejects with a `ConfigurationError` listing every problem with its location, and no browser page is opened:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
ConfigurationError: Invalid scenario configuration:
|
|
154
|
+
✖ Unrecognized key: "iteration"
|
|
155
|
+
→ at scenarios[0]
|
|
156
|
+
✖ Invalid input: expected int, received number
|
|
157
|
+
→ at scenarios[0].workers
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then every scenario, worker and iteration always runs to completion: a failed iteration does not stop the next iterations nor the other workers, and its page is always closed. If anything failed, `readYamlAndInterpret` then rejects with an `AggregateError` whose `errors` contain:
|
|
161
|
+
|
|
162
|
+
- `WorkerError` — an iteration failed (`scenarioName`, `workerIndex`, `iteration`, original error in `cause`)
|
|
163
|
+
- `BrowserLaunchError` — a browser could not be launched (`browser`, original error in `cause`)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { readYamlAndInterpret, ConfigurationError } from '@bloom-perf/yaml-pptr';
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await readYamlAndInterpret(yaml);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e instanceof ConfigurationError) {
|
|
172
|
+
console.error(e.message); // invalid YAML, unknown key, missing variable...
|
|
173
|
+
} else if (e instanceof AggregateError) {
|
|
174
|
+
for (const err of e.errors) console.error(err.message); // WorkerError / BrowserLaunchError
|
|
175
|
+
}
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Editor Support
|
|
181
|
+
|
|
182
|
+
The package ships a JSON Schema of the YAML configuration (`dist/schema.json`), generated from the very schema used to validate it at runtime. With the [YAML extension for VS Code](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) (or any editor using `yaml-language-server`), you get completion, documentation on hover, and errors (unknown key, wrong type...) while typing.
|
|
183
|
+
|
|
184
|
+
Add this first line to a scenario file:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
# yaml-language-server: $schema=https://unpkg.com/@bloom-perf/yaml-pptr@3/dist/schema.json
|
|
188
|
+
scenarios:
|
|
189
|
+
- name: Login
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
With the package installed, a local path works too (relative to the YAML file), e.g. `$schema=../node_modules/@bloom-perf/yaml-pptr/dist/schema.json`. To apply it to several files, map them in `.vscode/settings.json`:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"yaml.schemas": {
|
|
197
|
+
"https://unpkg.com/@bloom-perf/yaml-pptr@3/dist/schema.json": ["scenarios/*.yaml"]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
112
200
|
```
|
|
113
201
|
|
|
114
202
|
## YAML Reference
|
|
@@ -119,13 +207,37 @@ npx ts-node src/runner.ts
|
|
|
119
207
|
| -------- | ---- | ------- | ----------- |
|
|
120
208
|
| `name` | string | `Scenario N` | Scenario name |
|
|
121
209
|
| `browser` | `chrome` \| `firefox` | `chrome` | Browser to use |
|
|
122
|
-
| `run` | `PARALLEL` \| `SEQUENTIAL` | `PARALLEL` | Worker start strategy |
|
|
123
|
-
| `initialDelaySeconds` | number | `0` |
|
|
124
|
-
| `workers` |
|
|
125
|
-
| `iterations` |
|
|
210
|
+
| `run` | `PARALLEL` \| `SEQUENTIAL` | `PARALLEL` | Worker start strategy: `PARALLEL` starts all workers after `initialDelaySeconds`; `SEQUENTIAL` starts worker *n* after *n* × `initialDelaySeconds` |
|
|
211
|
+
| `initialDelaySeconds` | number | `0` | Start delay, applied once per worker before its first iteration (see `run`) |
|
|
212
|
+
| `workers` | integer ≥ 1 | `1` | Number of parallel workers |
|
|
213
|
+
| `iterations` | integer ≥ 1 | `1` | Repetitions per worker |
|
|
126
214
|
| `location` | string | - | Starting URL |
|
|
127
|
-
| `steps` | array | - | Simplified syntax |
|
|
128
|
-
| `actions` | array | - | Full syntax with options |
|
|
215
|
+
| `steps` | array | - | Simplified syntax (full actions are accepted too) |
|
|
216
|
+
| `actions` | array | - | Full syntax with options, run after `steps` |
|
|
217
|
+
|
|
218
|
+
All the scenarios of a file start at the same time, whatever their browser: each one is a population of users with its own `workers`, `iterations` and start strategy. Unknown keys are rejected (see [Error handling](#error-handling)).
|
|
219
|
+
|
|
220
|
+
### Selectors
|
|
221
|
+
|
|
222
|
+
Every selector (`click`, `input`, `CLICK`, `TYPE`, `WAIT_FOR_SELECTOR`) accepts a CSS selector or any [Puppeteer selector](https://pptr.dev/guides/page-interactions), so XPath, text and ARIA queries need no special syntax:
|
|
223
|
+
|
|
224
|
+
| Kind | Example |
|
|
225
|
+
| ---- | ------- |
|
|
226
|
+
| CSS | `#login-button`, `form .submit` |
|
|
227
|
+
| XPath | `::-p-xpath(//button[@type='submit'])` or `xpath///button[@type='submit']` |
|
|
228
|
+
| Text | `::-p-text(Sign in)` |
|
|
229
|
+
| ARIA | `::-p-aria(Sign in)`, `::-p-aria([name="Sign in"][role="button"])` |
|
|
230
|
+
| Shadow DOM | `my-app >>> button` (any depth), `my-app >>>> button` (direct shadow root) |
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
steps:
|
|
234
|
+
- input:
|
|
235
|
+
selector: "::-p-xpath(//input[@name='email'])"
|
|
236
|
+
text: "${APP_USER}"
|
|
237
|
+
- click: "::-p-text(Sign in)"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`${VAR}` placeholders are resolved in selectors too.
|
|
129
241
|
|
|
130
242
|
### Steps (Simplified Syntax)
|
|
131
243
|
|
|
@@ -164,7 +276,17 @@ npx ts-node src/runner.ts
|
|
|
164
276
|
|
|
165
277
|
### Actions (Full Syntax)
|
|
166
278
|
|
|
167
|
-
Use `actions`
|
|
279
|
+
Use the full syntax when you need advanced options. Full actions can be listed in `actions` (run after `steps`), or written directly among `steps` to mix both syntaxes in the order you need:
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
steps:
|
|
283
|
+
- click: "#open-menu"
|
|
284
|
+
- type: WAIT_FOR_SELECTOR
|
|
285
|
+
selector: ".menu"
|
|
286
|
+
options:
|
|
287
|
+
visible: true
|
|
288
|
+
- click: ".menu .logout"
|
|
289
|
+
```
|
|
168
290
|
|
|
169
291
|
#### GOTO
|
|
170
292
|
|
|
@@ -248,6 +370,8 @@ Use `actions` instead of `steps` when you need advanced options.
|
|
|
248
370
|
script: "document.title"
|
|
249
371
|
```
|
|
250
372
|
|
|
373
|
+
The script is never interpolated with environment variables (injecting values into code would be unsafe).
|
|
374
|
+
|
|
251
375
|
#### SET_VIEWPORT
|
|
252
376
|
|
|
253
377
|
```yaml
|
|
@@ -262,6 +386,8 @@ Use `actions` instead of `steps` when you need advanced options.
|
|
|
262
386
|
- type: PAUSE # Press Enter to continue
|
|
263
387
|
```
|
|
264
388
|
|
|
389
|
+
Meant for debugging in an interactive terminal. When there is none (CI, container, piped input), the PAUSE is ignored with a warning instead of blocking forever.
|
|
390
|
+
|
|
265
391
|
#### CLOSE
|
|
266
392
|
|
|
267
393
|
```yaml
|
|
@@ -270,19 +396,37 @@ Use `actions` instead of `steps` when you need advanced options.
|
|
|
270
396
|
|
|
271
397
|
## Environment Variables
|
|
272
398
|
|
|
273
|
-
|
|
399
|
+
Keep URLs and credentials out of the YAML with `${VAR_NAME}` placeholders. They are resolved before anything runs: a missing variable is a `ConfigurationError`.
|
|
274
400
|
|
|
275
401
|
```yaml
|
|
276
|
-
|
|
402
|
+
scenarios:
|
|
403
|
+
- name: Login
|
|
404
|
+
location: "${BASE_URL}/login"
|
|
405
|
+
steps:
|
|
406
|
+
- input:
|
|
407
|
+
selector: "#username"
|
|
408
|
+
text: "${APP_USER}"
|
|
409
|
+
- input:
|
|
410
|
+
selector: "#password"
|
|
411
|
+
text: "${APP_PASSWORD}"
|
|
412
|
+
- click: "#login-button"
|
|
277
413
|
```
|
|
278
414
|
|
|
279
415
|
```bash
|
|
280
|
-
BASE_URL=https://example.com npx
|
|
416
|
+
BASE_URL=https://example.com APP_USER=alice APP_PASSWORD=... npx tsx runner.ts
|
|
281
417
|
```
|
|
282
418
|
|
|
419
|
+
| Syntax | Where | Notes |
|
|
420
|
+
| ------ | ----- | ----- |
|
|
421
|
+
| `${VAR}` | Anywhere in `location`, `navigate`, GOTO `url`/`referer`, selectors, typed `text`, SCREENSHOT `path` | `$${` produces a literal `${` |
|
|
422
|
+
| `$VAR` | As the whole value of `location`, `navigate`, GOTO `url`, SCREENSHOT `path` | Elsewhere a `$` is kept as is (e.g. a password like `$ecret`) |
|
|
423
|
+
| `$VAR[workerIndex]` | As the whole value of `location`, `navigate` | One value per worker, see below |
|
|
424
|
+
|
|
425
|
+
`EVALUATE` scripts are never interpolated. Typed texts and resolved values are never logged.
|
|
426
|
+
|
|
283
427
|
### Indexed Variables
|
|
284
428
|
|
|
285
|
-
Assign different values to each worker using `$VAR[workerIndex]`:
|
|
429
|
+
Assign different values to each worker using `$VAR[workerIndex]` (comma-separated values). There must be at least one non-empty value per worker, otherwise it is a `ConfigurationError`:
|
|
286
430
|
|
|
287
431
|
```yaml
|
|
288
432
|
scenarios:
|
|
@@ -294,7 +438,7 @@ scenarios:
|
|
|
294
438
|
```
|
|
295
439
|
|
|
296
440
|
```bash
|
|
297
|
-
URLS="https://site1.com,https://site2.com,https://site3.com" npx
|
|
441
|
+
URLS="https://site1.com,https://site2.com,https://site3.com" npx tsx runner.ts
|
|
298
442
|
```
|
|
299
443
|
|
|
300
444
|
Worker 0 gets `site1.com`, worker 1 gets `site2.com`, etc.
|
|
@@ -324,6 +468,9 @@ npm test
|
|
|
324
468
|
# Run tests with coverage
|
|
325
469
|
npm run test:coverage
|
|
326
470
|
|
|
471
|
+
# Type check
|
|
472
|
+
npm run typecheck
|
|
473
|
+
|
|
327
474
|
# Build
|
|
328
475
|
npm run build
|
|
329
476
|
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised before anything runs when the configuration is invalid: YAML syntax,
|
|
3
|
+
* schema validation (unknown key, wrong type...) or environment variable
|
|
4
|
+
* resolution (missing variable, not enough per-worker values...).
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConfigurationError extends Error {
|
|
7
|
+
constructor(message: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Raised when an iteration of a scenario worker fails. Carries the scenario
|
|
11
|
+
* name, the worker index and the iteration so the caller knows exactly where
|
|
12
|
+
* the failure happened.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WorkerError extends Error {
|
|
15
|
+
readonly scenarioName: string;
|
|
16
|
+
readonly workerIndex: number;
|
|
17
|
+
readonly iteration: number;
|
|
18
|
+
constructor(scenarioName: string, workerIndex: number, iteration: number, cause: unknown);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Raised when a browser could not be launched (missing binary, bad options...).
|
|
22
|
+
*/
|
|
23
|
+
export declare class BrowserLaunchError extends Error {
|
|
24
|
+
readonly browser: string;
|
|
25
|
+
constructor(browser: string, cause: unknown);
|
|
26
|
+
}
|
package/dist/core/model.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface CloseAction extends BaseAction {
|
|
|
103
103
|
}
|
|
104
104
|
export interface EvaluateAction extends BaseAction {
|
|
105
105
|
actionType: ActionType.Evaluate;
|
|
106
|
-
script: string
|
|
106
|
+
script: string;
|
|
107
107
|
}
|
|
108
108
|
export interface SetViewportAction extends BaseAction {
|
|
109
109
|
actionType: ActionType.SetViewport;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import * as p from 'puppeteer';
|
|
2
|
-
import { SupportedBrowser, OnPageCallback } from './core/model';
|
|
3
|
-
import { YpLogger } from './core/logger';
|
|
4
|
-
export type { ScenarioContext, OnPageCallback } from './core/model';
|
|
2
|
+
import { SupportedBrowser, OnPageCallback } from './core/model.js';
|
|
3
|
+
import { YpLogger } from './core/logger.js';
|
|
4
|
+
export type { ScenarioContext, OnPageCallback } from './core/model.js';
|
|
5
|
+
export { WorkerError, BrowserLaunchError, ConfigurationError } from './core/errors.js';
|
|
5
6
|
export type ReadYamlOptions = {
|
|
7
|
+
/** Already launched browsers to use instead of launching new ones. They are not closed. */
|
|
6
8
|
browsers?: {
|
|
7
9
|
[key in SupportedBrowser]?: p.Browser;
|
|
8
10
|
};
|
|
9
11
|
logger?: YpLogger;
|
|
10
12
|
onPage?: OnPageCallback;
|
|
13
|
+
/** Run launched browsers in headless mode. Defaults to `true`. */
|
|
14
|
+
headless?: boolean;
|
|
15
|
+
/** Extra Puppeteer launch options per browser (e.g. `executablePath`). `args` are appended to the defaults. */
|
|
16
|
+
launchOptions?: {
|
|
17
|
+
[key in SupportedBrowser]?: p.LaunchOptions;
|
|
18
|
+
};
|
|
11
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Parses and validates the YAML, resolves environment variables, then runs
|
|
22
|
+
* every scenario. All scenarios start at the same time (each one with its own
|
|
23
|
+
* workers and start strategy), whatever the browser they target.
|
|
24
|
+
*
|
|
25
|
+
* - An invalid configuration (YAML syntax, unknown key, missing environment
|
|
26
|
+
* variable...) rejects with a `ConfigurationError` before anything runs.
|
|
27
|
+
* - Otherwise all scenarios are always run to completion. If anything failed
|
|
28
|
+
* (browser launch or iteration), the promise rejects with an `AggregateError`
|
|
29
|
+
* whose `errors` are `BrowserLaunchError` / `WorkerError` instances, in
|
|
30
|
+
* scenario order.
|
|
31
|
+
*/
|
|
12
32
|
export declare const readYamlAndInterpret: (yamlContent: string, options?: ReadYamlOptions) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import*as x from"puppeteer";import*as k from"js-yaml";import*as t from"zod";var T=t.enum(["load","domcontentloaded","networkidle0","networkidle2"]),R=t.enum(["left","right","middle"]),P=t.enum(["chrome","firefox"]),F=t.object({timeout:t.number().optional(),waitUntil:t.union([T,T.array()]).optional(),referer:t.string().optional()}).optional(),L=t.object({type:t.literal("WAIT"),milliseconds:t.number()}),B=t.object({button:R.optional(),clickCount:t.number().optional(),delay:t.number().optional()}).optional(),j=t.object({delay:t.number().optional()}).optional(),U=t.object({visible:t.boolean().optional(),hidden:t.boolean().optional(),timeout:t.number().optional()}).optional(),V=t.object({type:t.enum(["png","jpeg"]).optional(),quality:t.number().optional(),fullPage:t.boolean().optional(),clip:t.object({x:t.number(),y:t.number(),width:t.number(),height:t.number()}).optional(),omitBackground:t.boolean().optional(),encoding:t.enum(["base64","binary"]).optional()}).optional(),_=t.object({type:t.literal("GOTO"),url:t.string(),options:F}),G=t.object({type:t.literal("CLICK"),selector:t.string(),options:B}),I=t.object({type:t.literal("TYPE"),selector:t.string(),text:t.string(),options:j}),N=t.object({type:t.literal("WAIT_FOR_SELECTOR"),selector:t.string(),options:U}),M=t.object({type:t.literal("WAIT_FOR_TIMEOUT"),timeout:t.number()}),D=t.object({type:t.literal("PAUSE")}),q=t.object({type:t.literal("SCREENSHOT"),path:t.string(),options:V}),H=t.object({type:t.literal("EVALUATE"),script:t.union([t.string(),t.function()])}),K=t.object({type:t.literal("SET_VIEWPORT"),width:t.number(),height:t.number()}),J=t.object({type:t.literal("CLOSE")}),z=[_,G,I,N,M,D,q,H,K,J,L];if(z.length===0)throw new Error("Le tableau des sch\xE9mas d'actions est vide.");var Q=z,Z=t.discriminatedUnion("type",Q),X=t.object({navigate:t.string()}),Y=t.literal("waitForever"),tt=t.object({wait:t.number()}),et=t.object({click:t.string()}),ot=t.object({input:t.object({selector:t.string(),text:t.string()})}),it=t.union([X,Y,tt,et,ot]),nt=t.object({name:t.string().optional(),browser:P.optional(),run:t.enum(["PARALLEL","SEQUENTIAL"]).optional(),initialDelaySeconds:t.number().nonnegative().optional(),workers:t.number().positive().optional(),iterations:t.number().positive().optional(),location:t.string().optional(),steps:t.array(it).optional(),actions:t.array(Z).optional()}),$=t.object({scenarios:t.array(nt)});var E=i=>{let e=k.load(i);return $.parse(e)};var A={debug(i){console.log(i)},info(i){console.log(i)},warn(i){console.log(i)},error(i){console.error(i)}};var w=class{envVarResolve;logger;constructor(e,n=A){this.envVarResolve=o=>{let r=o.startsWith("$")?o.substring(1):o;return e(r)},this.logger=n}resolve(e){return{scenarios:e.scenarios.map((n,o)=>this.resolveScenario(n,o+1))}}resolveScenario(e,n){this.logger.debug(`Resolving YAML scenario: ${JSON.stringify(e)}`);let o=[];e.location&&o.push(this.createNavigateAction(e.location)),e.steps&&o.push(...e.steps.map(s=>this.resolveStep(s))),e.actions&&o.push(...e.actions.map(s=>this.resolveAction(s)));let r=e.run==="SEQUENTIAL"?{delaySecondsBetweenWorkerInits:e.initialDelaySeconds||0}:{initialDelaySeconds:e.initialDelaySeconds||0};return{name:e.name||`Scenario ${n}`,browser:e.browser||"chrome",run:r,workers:e.workers||1,iterations:e.iterations||1,actions:o}}resolveStep(e){if(e==="waitForever")return this.logger.debug("Resolving step: waitForever"),{actionType:"WaitForever"};if("wait"in e)return this.logger.debug(`Resolving step: wait ${e.wait} seconds`),{actionType:"Wait",milliseconds:e.wait*1e3};if("navigate"in e)return this.logger.debug("Resolving step: navigate"),this.createNavigateAction(e.navigate);if("click"in e)return this.logger.debug(`Resolving step: click "${e.click}"`),{actionType:"CLICK",selector:e.click};if("input"in e)return this.logger.debug(`Resolving step: input into "${e.input.selector}"`),{actionType:"TYPE",selector:e.input.selector,text:e.input.text};throw new Error(`Unknown step format: ${JSON.stringify(e)}`)}resolveAction(e){switch(this.logger.debug(`Resolving action: ${JSON.stringify(e)}`),e.type){case"GOTO":return{actionType:"GOTO",url:this.resolveStringOrEnv(e.url),options:e.options};case"WAIT":return{actionType:"Wait",milliseconds:e.milliseconds*1e3};case"CLICK":return{actionType:"CLICK",selector:e.selector,options:e.options};case"TYPE":return{actionType:"TYPE",selector:e.selector,text:e.text,options:e.options};case"WAIT_FOR_SELECTOR":return{actionType:"WAIT_FOR_SELECTOR",selector:e.selector,options:e.options};case"WAIT_FOR_TIMEOUT":return{actionType:"WAIT_FOR_TIMEOUT",timeout:e.timeout};case"PAUSE":return{actionType:"PAUSE"};case"SCREENSHOT":return{actionType:"SCREENSHOT",path:this.resolveStringOrEnv(e.path),options:e.options};case"EVALUATE":return{actionType:"EVALUATE",script:e.script};case"SET_VIEWPORT":return{actionType:"SET_VIEWPORT",width:e.width,height:e.height};case"CLOSE":return{actionType:"CLOSE"};default:return e}}createNavigateAction(e){return{actionType:"Navigate",location:this.resolveUrlOrEnv(e)}}resolveUrlOrEnv(e){let n="[workerIndex]";if(e.startsWith("$")){if(e.endsWith(n)){this.logger.debug(`Resolving indexed environment variable: "${e}"`);let o=e.substring(1,e.length-n.length);return{workerIndex:this.envVarResolve(o).split(",").map(c=>c.trim())}}return this.logger.debug(`Resolving environment variable: "${e}"`),{url:this.envVarResolve(e.substring(1))}}return this.logger.debug(`Using direct URL: "${e}"`),{url:e}}resolveStringOrEnv(e){return e.startsWith("$")?(this.logger.debug(`Resolving environment variable: "${e}"`),this.envVarResolve(e.substring(1))):e}};function st(i,e){let n=e||"png";return i.endsWith(".png")||i.endsWith(".jpeg")||i.endsWith(".jpg")||i.endsWith(".webp")?i:`${i}.${n}`}var W=async(i,e,n=A,o)=>await at(e.workers,e.name,async r=>{for(let s=0;s<e.iterations;s++){let c=e.run;"delaySecondsBetweenWorkerInits"in c?(n.info(`Scenario "${e.name}" (worker ${r}) > Choosing SEQUENTIAL start with ${c.delaySecondsBetweenWorkerInits} seconds between workers`),await new Promise(u=>setTimeout(u,c.delaySecondsBetweenWorkerInits*r*1e3))):(n.info(`Scenario "${e.name}" (worker ${r}) > Choosing PARALLEL start with ${c.initialDelaySeconds} seconds of initial delay`),await new Promise(u=>setTimeout(u,c.initialDelaySeconds*1e3)));let l=await i.newPage();o&&await o(l,{scenarioName:e.name,workerIndex:r,iteration:s}),await ct(e.name,e.actions,l,r,n),await l.close()}},n),at=async(i,e,n,o=A)=>Promise.all(Array.from({length:i},async(r,s)=>{try{o.info(`Scenario "${e}" (worker ${s}) started.`),await n(s)}catch(c){o.error(`Scenario ${e} (worker ${s}) failed: ${c}`)}})),ct=async(i,e,n,o,r=A)=>{for(let s of e){let{actionType:c}=s;switch(c){case"Navigate":let l=s;"url"in l.location?(r.info(`Scenario "${i}" (worker ${o}) > Navigating to location "${l.location.url}"`),await n.goto(l.location.url)):(r.info(`Scenario "${i}" (worker ${o}) > Navigating to indexed location "${l.location.workerIndex[o]}"`),await n.goto(l.location.workerIndex[o]));break;case"Wait":let u=s;r.info(`Scenario "${i}" (worker ${o}) > Waiting ${u.milliseconds} milliseconds...`),await n.evaluate(y=>new Promise(f=>setTimeout(f,y)),u.milliseconds);break;case"WaitForever":for(r.info(`Scenario "${i}" (worker ${o}) > Waiting forever...`);;)await new Promise(y=>setTimeout(y,3600*1e3));break;case"GOTO":let d=s;r.info(`Scenario "${i}" (worker ${o}) > GOTO "${d.url}"`);let v={timeout:d.options?.timeout,waitUntil:d.options?.waitUntil,referer:d.options?.referer};await n.goto(d.url,v);break;case"CLICK":let a=s;r.info(`Scenario "${i}" (worker ${o}) > CLICK on "${a.selector}"`);let g={button:a.options?.button,clickCount:a.options?.clickCount,delay:a.options?.delay};await n.click(a.selector,g);break;case"TYPE":let S=s;r.info(`Scenario "${i}" (worker ${o}) > TYPE into "${S.selector}"`),await n.type(S.selector,S.text,S.options);break;case"WAIT_FOR_SELECTOR":let m=s;r.info(`Scenario "${i}" (worker ${o}) > WAIT_FOR_SELECTOR "${m.selector}"`),await n.waitForSelector(m.selector,m.options);break;case"WAIT_FOR_TIMEOUT":let h=s;r.info(`Scenario "${i}" (worker ${o}) > WAIT_FOR_TIMEOUT ${h.timeout} milliseconds`),await n.evaluate(y=>new Promise(f=>setTimeout(f,y)),h.timeout);break;case"PAUSE":r.info(`Scenario "${i}" (worker ${o}) > PAUSE, press Enter to continue...`),await new Promise(y=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),y()})});break;case"SCREENSHOT":let p=s;r.info(`Scenario "${i}" (worker ${o}) > SCREENSHOT "${p.path}"`);let O={path:st(p.path,p.options?.type),type:p.options?.type,quality:p.options?.quality,fullPage:p.options?.fullPage,clip:p.options?.clip,omitBackground:p.options?.omitBackground,encoding:p.options?.encoding};await n.screenshot(O);break;case"EVALUATE":let C=s;r.info(`Scenario "${i}" (worker ${o}) > EVALUATE script`),await n.evaluate(C.script);break;case"SET_VIEWPORT":let b=s;r.info(`Scenario "${i}" (worker ${o}) > SET_VIEWPORT width=${b.width}, height=${b.height}`),await n.setViewport({width:b.width,height:b.height});break;case"CLOSE":r.info(`Scenario "${i}" (worker ${o}) > CLOSE page`),await n.close();break;default:throw new Error(`Unhandled action type "${c}" while evaluating scenario "${i}" on worker ${o}`)}}};var re=async(i,e={})=>{let{browsers:n={},logger:o=A,onPage:r}=e,s=E(i),l=new w(a=>{let g=process.env[a];if(o.debug(`Resolving env variable "${a}" with value ***`),g)return g;throw new Error(`Tried to resolve env variable ${a} but was empty.`)},o).resolve(s),u={};for(let a of l.scenarios){let g=a.browser||"chrome";u[g]||(u[g]=[]),u[g]?.push(a)}let d=["chrome","firefox"],v=Object.entries(u).map(async([a,g])=>{if(!g)return;if(!d.includes(a)){console.error(`Navigateur non support\xE9: ${a}. Les navigateurs support\xE9s sont: ${d.join(", ")}`);return}let S=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage","--disable-accelerated-2d-canvas","--disable-gpu","--ignore-certificate-errors"];a==="chrome"&&S.push("--start-maximized"),a==="firefox"&&S.push("--private");let m=n[a],h=!1;m||(m=await x.launch({headless:!1,channel:a,acceptInsecureCerts:!0,args:S}),h=!0);try{for(let p of g)await W(m,p,o,r)}catch(p){console.error(`Erreur lors de l'ex\xE9cution des sc\xE9narios pour ${a}:`,p)}finally{h&&m&&await m.close()}});await Promise.all(v)};export{re as readYamlAndInterpret};
|
|
1
|
+
import*as F from"js-yaml";import*as _ from"zod";import*as t from"zod";var y=t.number().int().nonnegative(),v=t.number().int().positive(),$=t.string().describe("CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."),W=t.string().describe('URL. "$VAR" as the whole value, or ${VAR} placeholders anywhere.'),L=t.enum(["load","domcontentloaded","networkidle0","networkidle2"]),M=t.enum(["left","right","middle"]),D=t.enum(["chrome","firefox"]),Z=t.strictObject({timeout:y.optional().describe("Navigation timeout in milliseconds."),waitUntil:t.union([L,L.array()]).optional().describe("When to consider the navigation finished."),referer:t.string().optional().describe("Referer header. ${VAR} placeholders are resolved.")}).optional(),H=t.strictObject({button:M.optional(),clickCount:v.optional().describe("2 for a double-click."),delay:y.optional().describe("Milliseconds between mousedown and mouseup.")}).optional(),K=t.strictObject({delay:y.optional().describe("Milliseconds between keystrokes.")}).optional(),q=t.strictObject({visible:t.boolean().optional(),hidden:t.boolean().optional(),timeout:y.optional().describe("Timeout in milliseconds.")}).optional(),Q=t.strictObject({type:t.enum(["png","jpeg"]).optional(),quality:t.number().int().min(0).max(100).optional().describe("JPEG quality (0-100)."),fullPage:t.boolean().optional(),clip:t.strictObject({x:t.number(),y:t.number(),width:t.number().positive(),height:t.number().positive()}).optional(),omitBackground:t.boolean().optional(),encoding:t.enum(["base64","binary"]).optional()}).optional(),J=t.strictObject({type:t.literal("GOTO"),url:W,options:Z}).describe("Navigate to a URL, with navigation options."),X=t.strictObject({type:t.literal("CLICK"),selector:$,options:H}).describe("Click on an element."),Y=t.strictObject({type:t.literal("TYPE"),selector:$,text:t.string().describe("Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged."),options:K}).describe("Type text into an element."),ee=t.strictObject({type:t.literal("WAIT_FOR_SELECTOR"),selector:$,options:q}).describe("Wait for an element to appear (or to be visible / hidden)."),te=t.strictObject({type:t.literal("WAIT_FOR_TIMEOUT"),timeout:y.describe("Milliseconds.")}).describe("Wait for a duration in milliseconds."),oe=t.strictObject({type:t.literal("WAIT"),milliseconds:y}).describe("Wait for a duration in milliseconds."),re=t.strictObject({type:t.literal("PAUSE")}).describe("Wait for Enter in an interactive terminal (debugging). Ignored with a warning when there is none."),ie=t.strictObject({type:t.literal("SCREENSHOT"),path:t.string().describe('File path. "$VAR" as the whole value, or ${VAR} placeholders anywhere.'),options:Q}).describe("Take a screenshot."),ne=t.strictObject({type:t.literal("EVALUATE"),script:t.string().describe("JavaScript evaluated in the page. Never interpolated.")}).describe("Evaluate JavaScript in the page."),se=t.strictObject({type:t.literal("SET_VIEWPORT"),width:v,height:v}).describe("Set the page viewport size."),ae=t.strictObject({type:t.literal("CLOSE")}).describe("Close the page."),P=t.discriminatedUnion("type",[J,X,Y,ee,te,oe,re,ie,ne,se,ae]),ce=t.strictObject({navigate:W}).describe('Navigate to a URL ("$VAR[workerIndex]" gives one URL per worker).'),pe=t.literal("waitForever").describe("Keep the page open forever."),le=t.strictObject({wait:t.number().nonnegative().describe("Seconds.")}).describe("Wait for a duration in seconds."),ue=t.strictObject({click:$}).describe("Click on an element."),de=t.strictObject({input:t.strictObject({selector:$,text:t.string().describe("Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged.")})}).describe("Type text into an element."),ge=t.union([ce,pe,le,ue,de,P]),he=t.strictObject({name:t.string().optional().describe('Scenario name (default: "Scenario <n>").'),browser:D.optional().describe("Browser to use (default: chrome)."),run:t.enum(["PARALLEL","SEQUENTIAL"]).optional().describe("Worker start strategy (default: PARALLEL). PARALLEL: all workers start after initialDelaySeconds. SEQUENTIAL: worker n starts after n \xD7 initialDelaySeconds."),initialDelaySeconds:t.number().nonnegative().optional().describe("Start delay, applied once per worker before its first iteration (see run)."),workers:v.optional().describe("Number of parallel workers (default: 1)."),iterations:v.optional().describe("Repetitions per worker (default: 1)."),location:W.optional().describe('Starting URL. "$VAR", ${VAR} placeholders, or "$VAR[workerIndex]" for one URL per worker.'),steps:t.array(ge).optional().describe("Shorthand steps; full actions are accepted too, to mix both syntaxes in order."),actions:t.array(P).optional().describe("Full actions, run after steps.")}).describe("A population of users: its workers run the steps and actions for the given number of iterations."),B=t.strictObject({scenarios:t.array(he).describe("Scenarios, all started at the same time.")});var V=r=>r instanceof Error?r.message:String(r),b=class extends Error{constructor(e,o){super(e,o),this.name="ConfigurationError"}},T=class extends Error{scenarioName;workerIndex;iteration;constructor(e,o,i,n){super(`Scenario "${e}" (worker ${o}, iteration ${i}) failed: ${V(n)}`,{cause:n}),this.name="WorkerError",this.scenarioName=e,this.workerIndex=o,this.iteration=i}},E=class extends Error{browser;constructor(e,o){super(`Failed to launch browser "${e}": ${V(o)}`,{cause:o}),this.name="BrowserLaunchError",this.browser=e}};var I=r=>{let e;try{e=F.load(r)}catch(i){throw new b(`Invalid YAML syntax: ${i instanceof Error?i.message:String(i)}`,{cause:i})}let o=B.safeParse(e);if(!o.success)throw new b(`Invalid scenario configuration:
|
|
2
|
+
${_.prettifyError(o.error)}`,{cause:o.error});return o.data};var f={debug(r){console.log(r)},info(r){console.log(r)},warn(r){console.log(r)},error(r){console.error(r)}};var we=/^[A-Za-z_][A-Za-z0-9_]*$/,be=/^\$([A-Za-z_][A-Za-z0-9_]*)$/,Se=/^\$([A-Za-z_][A-Za-z0-9_]*)\[workerIndex\]$/,fe=/\$\$\{|\$\{([^}]*)\}/g,U='per-worker values ("$VAR[workerIndex]") are only supported as the whole value of location and navigate',O=class{envVarResolve;logger;constructor(e,o=f){this.envVarResolve=e,this.logger=o}resolve(e){return{scenarios:e.scenarios.map((o,i)=>this.resolveScenario(o,i+1))}}resolveScenario(e,o){let i={name:e.name||`Scenario ${o}`,workers:e.workers||1};this.logger.debug(`Resolving scenario "${i.name}" (${e.steps?.length??0} step(s), ${e.actions?.length??0} action(s))`);let n=[];try{e.location&&n.push(this.createNavigateAction(e.location,i));for(let s of e.steps??[])n.push(this.resolveStep(s,i));for(let s of e.actions??[])n.push(this.resolveAction(s))}catch(s){throw new b(`Scenario "${i.name}": ${s instanceof Error?s.message:String(s)}`,{cause:s})}let a=e.run==="SEQUENTIAL"?{delaySecondsBetweenWorkerInits:e.initialDelaySeconds||0}:{initialDelaySeconds:e.initialDelaySeconds||0};return{name:i.name,browser:e.browser||"chrome",run:a,workers:i.workers,iterations:e.iterations||1,actions:n}}resolveStep(e,o){return e==="waitForever"?{actionType:"WaitForever"}:"type"in e?this.resolveAction(e):"wait"in e?{actionType:"Wait",milliseconds:e.wait*1e3}:"navigate"in e?this.createNavigateAction(e.navigate,o):"click"in e?{actionType:"CLICK",selector:this.interpolate(e.click)}:{actionType:"TYPE",selector:this.interpolate(e.input.selector),text:this.interpolate(e.input.text)}}resolveAction(e){switch(this.logger.debug(`Resolving action: ${e.type}`),e.type){case"GOTO":return{actionType:"GOTO",url:this.resolveWholeValueOrInterpolate(e.url),options:e.options&&{...e.options,referer:e.options.referer&&this.interpolate(e.options.referer)}};case"WAIT":return{actionType:"Wait",milliseconds:e.milliseconds};case"CLICK":return{actionType:"CLICK",selector:this.interpolate(e.selector),options:e.options};case"TYPE":return{actionType:"TYPE",selector:this.interpolate(e.selector),text:this.interpolate(e.text),options:e.options};case"WAIT_FOR_SELECTOR":return{actionType:"WAIT_FOR_SELECTOR",selector:this.interpolate(e.selector),options:e.options};case"WAIT_FOR_TIMEOUT":return{actionType:"WAIT_FOR_TIMEOUT",timeout:e.timeout};case"PAUSE":return{actionType:"PAUSE"};case"SCREENSHOT":return{actionType:"SCREENSHOT",path:this.resolveWholeValueOrInterpolate(e.path),options:e.options};case"EVALUATE":return{actionType:"EVALUATE",script:e.script};case"SET_VIEWPORT":return{actionType:"SET_VIEWPORT",width:e.width,height:e.height};case"CLOSE":return{actionType:"CLOSE"};default:return e}}createNavigateAction(e,o){let i=Se.exec(e);if(i){let n=i[1],a=this.env(n).split(",").map(s=>s.trim());if(a.some(s=>s===""))throw new Error(`environment variable "${n}" (used as "${e}") contains an empty value`);if(a.length<o.workers)throw new Error(`environment variable "${n}" (used as "${e}") provides ${a.length} value(s) for ${o.workers} worker(s)`);return{actionType:"Navigate",location:{workerIndex:a}}}return{actionType:"Navigate",location:{url:this.resolveWholeValueOrInterpolate(e)}}}resolveWholeValueOrInterpolate(e){let o=be.exec(e);if(o)return this.env(o[1]);if(/^\$[A-Za-z_]/.test(e))throw new Error(`unsupported environment variable reference "${e}": use "$VAR" for the whole value or "\${VAR}" inside a value`+(e.endsWith("[workerIndex]")?`; ${U}`:""));return this.interpolate(e)}interpolate(e){return e.replace(fe,(o,i)=>{if(o==="$${")return"${";if(i===void 0||!we.test(i))throw new Error(`invalid placeholder "${o}": expected \${VAR_NAME}`+(i?.endsWith("[workerIndex]")?`; ${U}`:""));return this.env(i)})}env(e){return this.logger.debug(`Resolving environment variable "${e}"`),this.envVarResolve(e)}};import{setTimeout as x}from"node:timers/promises";function Ae(r,e){let o=e||"png";return r.endsWith(".png")||r.endsWith(".jpeg")||r.endsWith(".jpg")||r.endsWith(".webp")?r:`${r}.${o}`}var ye=(r,e)=>"delaySecondsBetweenWorkerInits"in r?r.delaySecondsBetweenWorkerInits*e:r.initialDelaySeconds,j=async(r,e,o=f,i)=>{let n=[],a=async s=>{let p=ye(e.run,s),m="delaySecondsBetweenWorkerInits"in e.run?"SEQUENTIAL":"PARALLEL";o.info(`Scenario "${e.name}" (worker ${s}) started (${m}, starting in ${p}s).`),await x(p*1e3);for(let u=0;u<e.iterations;u++)try{await ve(r,e,s,u,o,i)}catch(S){let g=new T(e.name,s,u,S);o.error(g.message),n.push(g)}};if(await Promise.all(Array.from({length:e.workers},(s,p)=>a(p))),n.length>0){n.sort((p,m)=>p.workerIndex-m.workerIndex||p.iteration-m.iteration);let s=e.workers*e.iterations;throw new AggregateError(n,`Scenario "${e.name}": ${n.length}/${s} iteration(s) failed`)}},ve=async(r,e,o,i,n,a)=>{let s;try{s=await r.newPage(),a&&await a(s,{scenarioName:e.name,workerIndex:o,iteration:i}),await $e(e.name,e.actions,s,o,n)}finally{s&&!s.isClosed()&&await s.close().catch(p=>n.warn(`Scenario "${e.name}" (worker ${o}, iteration ${i}) > failed to close page: ${p}`))}},$e=async(r,e,o,i,n=f)=>{for(let a of e){let{actionType:s}=a;switch(s){case"Navigate":let p=a;"url"in p.location?(n.info(`Scenario "${r}" (worker ${i}) > Navigating to location "${p.location.url}"`),await o.goto(p.location.url)):(n.info(`Scenario "${r}" (worker ${i}) > Navigating to indexed location "${p.location.workerIndex[i]}"`),await o.goto(p.location.workerIndex[i]));break;case"Wait":let m=a;n.info(`Scenario "${r}" (worker ${i}) > Waiting ${m.milliseconds} milliseconds...`),await x(m.milliseconds);break;case"WaitForever":for(n.info(`Scenario "${r}" (worker ${i}) > Waiting forever...`);;)await x(3600*1e3);break;case"GOTO":let u=a;n.info(`Scenario "${r}" (worker ${i}) > GOTO "${u.url}"`);let S={timeout:u.options?.timeout,waitUntil:u.options?.waitUntil,referer:u.options?.referer};await o.goto(u.url,S);break;case"CLICK":let g=a;n.info(`Scenario "${r}" (worker ${i}) > CLICK on "${g.selector}"`);let C={button:g.options?.button,clickCount:g.options?.clickCount,delay:g.options?.delay};await o.click(g.selector,C);break;case"TYPE":let w=a;n.info(`Scenario "${r}" (worker ${i}) > TYPE into "${w.selector}"`),await o.type(w.selector,w.text,w.options);break;case"WAIT_FOR_SELECTOR":let c=a;n.info(`Scenario "${r}" (worker ${i}) > WAIT_FOR_SELECTOR "${c.selector}"`),await o.waitForSelector(c.selector,c.options);break;case"WAIT_FOR_TIMEOUT":let d=a;n.info(`Scenario "${r}" (worker ${i}) > WAIT_FOR_TIMEOUT ${d.timeout} milliseconds`),await x(d.timeout);break;case"PAUSE":if(!process.stdin.isTTY){n.warn(`Scenario "${r}" (worker ${i}) > PAUSE ignored: no interactive terminal`);break}n.info(`Scenario "${r}" (worker ${i}) > PAUSE, press Enter to continue...`),await new Promise(A=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),A()})});break;case"SCREENSHOT":let l=a;n.info(`Scenario "${r}" (worker ${i}) > SCREENSHOT "${l.path}"`);let R={path:Ae(l.path,l.options?.type),type:l.options?.type,quality:l.options?.quality,fullPage:l.options?.fullPage,clip:l.options?.clip,omitBackground:l.options?.omitBackground,encoding:l.options?.encoding};await o.screenshot(R);break;case"EVALUATE":let k=a;n.info(`Scenario "${r}" (worker ${i}) > EVALUATE script`),await o.evaluate(k.script);break;case"SET_VIEWPORT":let h=a;n.info(`Scenario "${r}" (worker ${i}) > SET_VIEWPORT width=${h.width}, height=${h.height}`),await o.setViewport({width:h.width,height:h.height});break;case"CLOSE":n.info(`Scenario "${r}" (worker ${i}) > CLOSE page`),await o.close();break;default:throw new Error(`Unhandled action type "${s}" while evaluating scenario "${r}" on worker ${i}`)}}};import*as N from"puppeteer";var Te=(r,e)=>{if(r==="firefox")return[];let o=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage","--disable-accelerated-2d-canvas","--disable-gpu","--ignore-certificate-errors"];return e||o.push("--start-maximized"),o},Ee=(r,e,o={})=>({browser:r,headless:e,acceptInsecureCerts:!0,...o,args:[...Te(r,e),...o.args??[]]}),G=(r,e,o)=>N.launch(Ee(r,e,o));var bt=async(r,e={})=>{let{browsers:o={},logger:i=f,onPage:n,headless:a=!0,launchOptions:s={}}=e,p=I(r),u=new O(c=>{let d=process.env[c];if(d)return d;throw new Error(`environment variable "${c}" is not set (or empty)`)},i).resolve(p),S=new Map;for(let c of u.scenarios){let d=S.get(c.browser)??[];d.push(c),S.set(c.browser,d)}let g=async(c,d)=>{let l=o[c],R=!l;if(!l)try{i.info(`Launching ${c} (headless: ${a})`),l=await G(c,a,s[c])}catch(h){let A=new E(c,h);return i.error(A.message),[A]}let k=l;try{return(await Promise.all(d.map(async A=>{try{return await j(k,A,i,n),[]}catch(z){return z instanceof AggregateError?z.errors:[z]}}))).flat()}finally{R&&await k.close().catch(h=>i.warn(`Failed to close ${c}: ${h}`))}},w=(await Promise.all([...S].map(([c,d])=>g(c,d)))).flat();if(w.length>0)throw new AggregateError(w,`${w.length} error(s) occurred while running scenarios`)};export{E as BrowserLaunchError,b as ConfigurationError,T as WorkerError,bt as readYamlAndInterpret};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import * as p from 'puppeteer';
|
|
2
|
-
import { YpLogger } from '../core/logger';
|
|
3
|
-
import { OnPageCallback, Scenario } from '../core/model';
|
|
4
|
-
|
|
2
|
+
import { YpLogger } from '../core/logger.js';
|
|
3
|
+
import { OnPageCallback, Scenario } from '../core/model.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runs every worker of the scenario, each one running all its iterations.
|
|
6
|
+
* A failing iteration interrupts neither the next iterations of the worker
|
|
7
|
+
* nor the other workers. Once everything is done, the promise rejects with an
|
|
8
|
+
* `AggregateError` of `WorkerError` (one per failed iteration) if anything failed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const evalScenario: (browser: p.Browser, scenario: Scenario, logger?: YpLogger, onPage?: OnPageCallback) => Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as p from 'puppeteer';
|
|
2
|
+
import { SupportedBrowser } from '../core/model.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds Puppeteer launch options for the given browser. The browser is
|
|
5
|
+
* selected with `browser` (not `channel`, which only accepts Chrome release
|
|
6
|
+
* channels). User options override defaults; user `args` are appended.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildLaunchOptions: (browser: SupportedBrowser, headless: boolean, extra?: p.LaunchOptions) => p.LaunchOptions;
|
|
9
|
+
export declare const launchBrowser: (browser: SupportedBrowser, headless: boolean, extra?: p.LaunchOptions) => Promise<p.Browser>;
|