@chaos-maker/playwright 0.4.0 → 0.6.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 +159 -28
- package/dist/chunk-ZJZRSTXG.js +324 -0
- package/dist/fixture.cjs +150 -81
- package/dist/fixture.d.cts +5 -1
- package/dist/fixture.d.ts +5 -1
- package/dist/fixture.js +10 -2
- package/dist/index.cjs +163 -87
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.js +19 -3
- package/package.json +3 -3
- package/dist/chunk-KHKPTES5.js +0 -256
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Playwright adapter for [`@chaos-maker/core`](../core/). One-line chaos injection
|
|
|
8
8
|
npm install @chaos-maker/core @chaos-maker/playwright
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Both packages are required
|
|
11
|
+
Both packages are required - `@chaos-maker/playwright` loads the core UMD bundle into the browser page.
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
@@ -19,21 +19,27 @@ import { test, expect } from '@playwright/test';
|
|
|
19
19
|
import { injectChaos, removeChaos, getChaosLog } from '@chaos-maker/playwright';
|
|
20
20
|
|
|
21
21
|
test('shows error when API fails', async ({ page }) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
try {
|
|
23
|
+
await injectChaos(page, {
|
|
24
|
+
network: {
|
|
25
|
+
failures: [{ urlPattern: '/api/data', statusCode: 503, probability: 1.0 }]
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await page.goto('/dashboard');
|
|
30
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
31
|
+
|
|
32
|
+
// Check what chaos was applied
|
|
33
|
+
const log = await getChaosLog(page);
|
|
34
|
+
expect(log.some(e => e.type === 'network:failure' && e.applied)).toBe(true);
|
|
35
|
+
} finally {
|
|
36
|
+
await removeChaos(page);
|
|
37
|
+
}
|
|
34
38
|
});
|
|
35
39
|
```
|
|
36
40
|
|
|
41
|
+
For direct API calls, use `try` / `finally` when a test can fail before explicit cleanup. `removeChaos(page)` restores the current document and is safe during teardown. Playwright `addInitScript()` entries stay registered on the `Page`, so a later `page.reload()` or `page.goto()` on the same reused page can run prior chaos init scripts again. Prefer the fixture, Playwright's default fresh page per test, or a new page/context when reload isolation matters.
|
|
42
|
+
|
|
37
43
|
### Test Fixture
|
|
38
44
|
|
|
39
45
|
For automatic cleanup, use the built-in fixture:
|
|
@@ -59,6 +65,27 @@ test('handles slow network', async ({ page, chaos }) => {
|
|
|
59
65
|
|
|
60
66
|
### With Presets
|
|
61
67
|
|
|
68
|
+
Drop a built-in preset by name with the declarative `presets` field:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await injectChaos(page, { presets: ['slow-api'] });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Register your own bundle inline via `customPresets`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
await injectChaos(page, {
|
|
78
|
+
customPresets: {
|
|
79
|
+
'team-flow': {
|
|
80
|
+
network: { failures: [{ urlPattern: '/checkout', statusCode: 503, probability: 1 }] },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
presets: ['team-flow'],
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The legacy spread style still works for migration:
|
|
88
|
+
|
|
62
89
|
```ts
|
|
63
90
|
import { test, expect } from '@playwright/test';
|
|
64
91
|
import { presets } from '@chaos-maker/core';
|
|
@@ -90,6 +117,49 @@ test('checkout handles combined chaos', async ({ page }) => {
|
|
|
90
117
|
});
|
|
91
118
|
```
|
|
92
119
|
|
|
120
|
+
### Rule Groups
|
|
121
|
+
|
|
122
|
+
Use Rule Groups to toggle a set of rules during a test without reinjecting chaos.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { test, expect } from '@playwright/test';
|
|
126
|
+
import { ChaosConfigBuilder } from '@chaos-maker/core';
|
|
127
|
+
import {
|
|
128
|
+
injectChaos,
|
|
129
|
+
enableGroup,
|
|
130
|
+
disableGroup,
|
|
131
|
+
enableSWGroup,
|
|
132
|
+
disableSWGroup,
|
|
133
|
+
} from '@chaos-maker/playwright';
|
|
134
|
+
|
|
135
|
+
test('toggles checkout chaos', async ({ page }) => {
|
|
136
|
+
const config = new ChaosConfigBuilder()
|
|
137
|
+
.defineGroup('payments', { enabled: false })
|
|
138
|
+
.inGroup('payments')
|
|
139
|
+
.failRequests('/api/pay', 503, 1)
|
|
140
|
+
.build();
|
|
141
|
+
|
|
142
|
+
await injectChaos(page, config);
|
|
143
|
+
await page.goto('/checkout');
|
|
144
|
+
|
|
145
|
+
await enableGroup(page, 'payments');
|
|
146
|
+
await expect(page.getByText('Try again')).toBeVisible();
|
|
147
|
+
|
|
148
|
+
await disableGroup(page, 'payments');
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
With the fixture, the same helpers are available as `chaos.enableGroup(name)` and `chaos.disableGroup(name)`.
|
|
153
|
+
|
|
154
|
+
For Service Worker rules, use the SW helpers after `injectSWChaos`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
await enableSWGroup(page, 'payments');
|
|
158
|
+
await disableSWGroup(page, 'payments');
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Browser-side `enableGroup` and `disableGroup` affect page rules from `injectChaos`. `enableSWGroup` and `disableSWGroup` affect Service Worker rules from `injectSWChaos`.
|
|
162
|
+
|
|
93
163
|
### SSE and GraphQL
|
|
94
164
|
|
|
95
165
|
```ts
|
|
@@ -118,32 +188,65 @@ SSE chaos and GraphQL operation matching use the same pre-navigation `injectChao
|
|
|
118
188
|
|
|
119
189
|
Inject chaos into a Playwright page. **Call before `page.goto()`** to ensure all network requests are intercepted from the start.
|
|
120
190
|
|
|
121
|
-
- `page`
|
|
122
|
-
- `config`
|
|
123
|
-
- `opts`
|
|
124
|
-
- `tracing?: boolean | 'auto'`
|
|
125
|
-
- `testInfo?: TestInfo`
|
|
126
|
-
- `traceOptions?: { verbose?: boolean; attachmentName?: string }`
|
|
191
|
+
- `page` - Playwright `Page` instance
|
|
192
|
+
- `config` - `ChaosConfig` object (see [@chaos-maker/core](../core/) for full config reference)
|
|
193
|
+
- `opts` - optional. `InjectChaosOptions`:
|
|
194
|
+
- `tracing?: boolean | 'auto'` - emit chaos events into the Playwright trace (see [Debugging with trace](#debugging-with-trace)). Requires `testInfo` when `true`.
|
|
195
|
+
- `testInfo?: TestInfo` - active Playwright `TestInfo` (supplied automatically by the fixture).
|
|
196
|
+
- `traceOptions?: { verbose?: boolean; attachmentName?: string }` - tune trace output.
|
|
127
197
|
|
|
128
198
|
### `removeChaos(page)`
|
|
129
199
|
|
|
130
200
|
Stop chaos and restore original `fetch`/`XHR`/DOM behavior.
|
|
131
201
|
|
|
202
|
+
This restores the active document. It does not remove Playwright `addInitScript()` registrations from a reused `Page`, because Playwright does not expose a removal API for them.
|
|
203
|
+
|
|
132
204
|
### `getChaosLog(page)`
|
|
133
205
|
|
|
134
|
-
Retrieve the chaos event log from the page. Returns `ChaosEvent[]`
|
|
206
|
+
Retrieve the chaos event log from the page. Returns `ChaosEvent[]` - every chaos check emitted since injection, with `applied: true/false`.
|
|
207
|
+
|
|
208
|
+
### `enableGroup(page, name)` / `disableGroup(page, name)`
|
|
209
|
+
|
|
210
|
+
Toggle a browser-side Rule Group at runtime.
|
|
211
|
+
|
|
212
|
+
### `enableSWGroup(page, name, opts?)` / `disableSWGroup(page, name, opts?)`
|
|
213
|
+
|
|
214
|
+
Toggle a Service Worker Rule Group at runtime. Pass `opts.timeoutMs` to override the Service Worker acknowledgement timeout.
|
|
135
215
|
|
|
136
216
|
### Fixture: `chaos`
|
|
137
217
|
|
|
138
218
|
Available when importing `test` from `@chaos-maker/playwright/fixture`:
|
|
139
219
|
|
|
140
|
-
- `chaos.inject(config)`
|
|
141
|
-
- `chaos.remove()`
|
|
142
|
-
- `chaos.getLog()`
|
|
220
|
+
- `chaos.inject(config)` - same as `injectChaos(page, config)`
|
|
221
|
+
- `chaos.remove()` - same as `removeChaos(page)` (also called automatically after each test)
|
|
222
|
+
- `chaos.getLog()` - same as `getChaosLog(page)`
|
|
223
|
+
- `chaos.enableGroup(name)` - same as `enableGroup(page, name)`
|
|
224
|
+
- `chaos.disableGroup(name)` - same as `disableGroup(page, name)`
|
|
225
|
+
- `chaos.enableSWGroup(name, opts?)` - same as `enableSWGroup(page, name, opts)`
|
|
226
|
+
- `chaos.disableSWGroup(name, opts?)` - same as `disableSWGroup(page, name, opts)`
|
|
227
|
+
|
|
228
|
+
## Validation
|
|
229
|
+
|
|
230
|
+
`injectChaos` validates the config from Node BEFORE any page touch. A malformed config throws `ChaosConfigError` synchronously from the test runner - your test fails before navigation, not in the browser console. `ChaosConfigError.issues` is a structured `ValidationIssue[]` with `path`, `code`, `ruleType`, and optional `expected` / `received`. See the [Rule Validation concept page](https://chaos-maker-dev.github.io/chaos-maker/concepts/validation/).
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { injectChaos, ChaosConfigError } from '@chaos-maker/playwright';
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await injectChaos(page, config, {
|
|
237
|
+
validation: { unknownFields: 'warn' },
|
|
238
|
+
});
|
|
239
|
+
} catch (e) {
|
|
240
|
+
if (e instanceof ChaosConfigError) {
|
|
241
|
+
for (const issue of e.issues) console.error(issue.path, issue.code, issue.message);
|
|
242
|
+
}
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
143
246
|
|
|
144
247
|
## Debugging with trace
|
|
145
248
|
|
|
146
|
-
When a chaos test fails, the Playwright trace viewer is the first place to look. Enable tracing in your Playwright config and use the fixture
|
|
249
|
+
When a chaos test fails, the Playwright trace viewer is the first place to look. Enable tracing in your Playwright config and use the fixture - every applied chaos decision appears inline in the trace action timeline as a `chaos:<type>` step, and the full event log is attached as `chaos-log.json`.
|
|
147
250
|
|
|
148
251
|
```ts
|
|
149
252
|
// playwright.config.ts
|
|
@@ -189,6 +292,18 @@ test('with direct API', async ({ page }, testInfo) => {
|
|
|
189
292
|
});
|
|
190
293
|
```
|
|
191
294
|
|
|
295
|
+
## Leak diagnostics
|
|
296
|
+
|
|
297
|
+
Enable `debug: true` on the chaos config to surface leaked-runtime diagnostics in the event log. Filter `getChaosLog(page)` for `type === 'debug'` events with `detail.reason` covering double-patched globals, stale wrapper handles, orphaned observers, or active-instance conflicts. See [`@chaos-maker/core`](../core/README.md#leak-diagnostics) for the full reason list.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
await injectChaos(page, { debug: true, network: { /* ... */ } });
|
|
301
|
+
await page.goto('/');
|
|
302
|
+
const issues = (await getChaosLog(page)).filter(
|
|
303
|
+
(e) => e.type === 'debug' && /already-patched|stale|orphaned|active-instance-conflict/.test(String(e.detail.reason ?? '')),
|
|
304
|
+
);
|
|
305
|
+
```
|
|
306
|
+
|
|
192
307
|
## Service Worker chaos
|
|
193
308
|
|
|
194
309
|
Intercept SW-originated fetches. Requires one line in your service-worker script.
|
|
@@ -199,25 +314,41 @@ importScripts('/chaos-maker-sw.js');
|
|
|
199
314
|
```
|
|
200
315
|
|
|
201
316
|
```ts
|
|
202
|
-
import {
|
|
317
|
+
import {
|
|
318
|
+
injectSWChaos,
|
|
319
|
+
removeSWChaos,
|
|
320
|
+
getSWChaosLog,
|
|
321
|
+
getSWChaosLogFromSW,
|
|
322
|
+
enableSWGroup,
|
|
323
|
+
disableSWGroup,
|
|
324
|
+
} from '@chaos-maker/playwright';
|
|
203
325
|
|
|
204
326
|
test('SW-fetched /api returns 503', async ({ page }) => {
|
|
205
327
|
await page.goto('/app-with-sw/');
|
|
206
328
|
// wait for controller after your app's SW registration
|
|
207
329
|
await injectSWChaos(page, {
|
|
208
|
-
|
|
330
|
+
groups: [{ name: 'payments', enabled: false }],
|
|
331
|
+
network: {
|
|
332
|
+
failures: [{ urlPattern: '/api/data', statusCode: 503, probability: 1, group: 'payments' }],
|
|
333
|
+
},
|
|
209
334
|
seed: 1,
|
|
210
335
|
});
|
|
336
|
+
await enableSWGroup(page, 'payments');
|
|
211
337
|
await page.click('#trigger');
|
|
212
338
|
const log = await getSWChaosLog(page);
|
|
213
339
|
expect(log.some(e => e.type === 'network:failure' && e.applied)).toBe(true);
|
|
340
|
+
await disableSWGroup(page, 'payments');
|
|
214
341
|
await removeSWChaos(page);
|
|
215
342
|
});
|
|
216
343
|
```
|
|
217
344
|
|
|
345
|
+
Use `getSWChaosLog(page)` for the page-buffered event log. This is the default assertion surface because it reflects events broadcast from the Service Worker to the page. Use `getSWChaosLogFromSW(page)` when you need a direct pull from the Service Worker's in-memory log, such as debugging a missed page-side broadcast.
|
|
346
|
+
|
|
347
|
+
`removeSWChaos(page)` stops the worker engine and clears both the page-buffered and worker-side logs. For full browser isolation between tests, unregister the app's Service Worker or use a fresh browser context.
|
|
348
|
+
|
|
218
349
|
Two artifacts ship in `@chaos-maker/core`:
|
|
219
|
-
- `dist/sw.js`
|
|
220
|
-
- `dist/sw.mjs`
|
|
350
|
+
- `dist/sw.js` - IIFE bundle for classic SWs (`importScripts('/chaos-maker-sw.js')`).
|
|
351
|
+
- `dist/sw.mjs` - ESM bundle for module SWs (`import { installChaosSW } from '/chaos-maker-sw.mjs'`).
|
|
221
352
|
|
|
222
353
|
Serve whichever your SW type uses at a URL reachable from the service-worker scope.
|
|
223
354
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { serializeForTransport, validateChaosConfig as validateChaosConfig2 } from "@chaos-maker/core";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/trace.ts
|
|
8
|
+
import { test } from "@playwright/test";
|
|
9
|
+
import { formatStepTitle, shouldEmitStep } from "@chaos-maker/core";
|
|
10
|
+
import { formatStepTitle as formatStepTitle2, shouldEmitStep as shouldEmitStep2 } from "@chaos-maker/core";
|
|
11
|
+
var CHAOS_BINDING = "__chaosMakerReport";
|
|
12
|
+
var TRACE_HANDLE_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceHandle");
|
|
13
|
+
var TRACE_BINDING_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceBinding");
|
|
14
|
+
async function createTraceReporter(page, testInfo, opts = {}) {
|
|
15
|
+
const existing = page[TRACE_HANDLE_KEY];
|
|
16
|
+
if (existing) return existing;
|
|
17
|
+
const verbose = opts.verbose ?? false;
|
|
18
|
+
const attachmentName = opts.attachmentName ?? "chaos-log.json";
|
|
19
|
+
const events = [];
|
|
20
|
+
const handler = (_source, event) => {
|
|
21
|
+
events.push(event);
|
|
22
|
+
if (!shouldEmitStep(event, verbose)) return;
|
|
23
|
+
const title = formatStepTitle(event);
|
|
24
|
+
test.step(title, async () => {
|
|
25
|
+
}).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
let state = page[TRACE_BINDING_KEY];
|
|
29
|
+
if (!state) {
|
|
30
|
+
state = { handler };
|
|
31
|
+
page[TRACE_BINDING_KEY] = state;
|
|
32
|
+
await page.exposeBinding(CHAOS_BINDING, (source, event) => {
|
|
33
|
+
state.handler(source, event);
|
|
34
|
+
});
|
|
35
|
+
await page.addInitScript((bindingName) => {
|
|
36
|
+
const win = globalThis;
|
|
37
|
+
const attach = () => {
|
|
38
|
+
const utils = win.chaosUtils;
|
|
39
|
+
if (!utils || !utils.instance) return false;
|
|
40
|
+
if (utils.__chaosMakerTraceBound === utils.instance) return true;
|
|
41
|
+
utils.__chaosMakerTraceBound = utils.instance;
|
|
42
|
+
utils.instance.on("*", (event) => {
|
|
43
|
+
try {
|
|
44
|
+
if (typeof win[bindingName] === "function") {
|
|
45
|
+
win[bindingName](event);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
if (attach()) return;
|
|
53
|
+
const intervalId = setInterval(() => {
|
|
54
|
+
if (attach()) clearInterval(intervalId);
|
|
55
|
+
}, 10);
|
|
56
|
+
setTimeout(() => clearInterval(intervalId), 5e3);
|
|
57
|
+
}, CHAOS_BINDING);
|
|
58
|
+
} else {
|
|
59
|
+
state.handler = handler;
|
|
60
|
+
}
|
|
61
|
+
const handle = {
|
|
62
|
+
events,
|
|
63
|
+
dispose: async (seed = null) => {
|
|
64
|
+
const payload = {
|
|
65
|
+
seed,
|
|
66
|
+
eventCount: events.length,
|
|
67
|
+
events
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
await testInfo.attach(attachmentName, {
|
|
71
|
+
body: Buffer.from(JSON.stringify(payload, null, 2), "utf-8"),
|
|
72
|
+
contentType: "application/json"
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
if (page[TRACE_HANDLE_KEY] === handle) {
|
|
77
|
+
delete page[TRACE_HANDLE_KEY];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
page[TRACE_HANDLE_KEY] = handle;
|
|
82
|
+
return handle;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/index.ts
|
|
86
|
+
import { Logger } from "@chaos-maker/core";
|
|
87
|
+
import { validateChaosConfig as validateChaosConfig3, ChaosConfigError, formatSeedReproduction } from "@chaos-maker/core";
|
|
88
|
+
|
|
89
|
+
// src/sw.ts
|
|
90
|
+
import { validateChaosConfig, SW_BRIDGE_SOURCE } from "@chaos-maker/core";
|
|
91
|
+
var BRIDGE_INIT_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.sw.bridgeInit");
|
|
92
|
+
var DEFAULT_SW_TOGGLE_TIMEOUT = 2e3;
|
|
93
|
+
async function ensurePageBridge(page) {
|
|
94
|
+
if (!page[BRIDGE_INIT_KEY]) {
|
|
95
|
+
await page.addInitScript({ content: SW_BRIDGE_SOURCE });
|
|
96
|
+
page[BRIDGE_INIT_KEY] = true;
|
|
97
|
+
}
|
|
98
|
+
await page.evaluate(SW_BRIDGE_SOURCE).catch(() => {
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function injectSWChaos(page, config, opts = {}) {
|
|
102
|
+
const validated = validateChaosConfig(config, opts.validation);
|
|
103
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
104
|
+
await ensurePageBridge(page);
|
|
105
|
+
const result = await page.evaluate(
|
|
106
|
+
async ({ cfg, timeoutMs: timeoutMs2 }) => {
|
|
107
|
+
const bridge = globalThis.__chaosMakerSWBridge;
|
|
108
|
+
if (!bridge) throw new Error("[chaos-maker] SW bridge missing from page \u2014 ensurePageBridge failed");
|
|
109
|
+
return await bridge.apply(cfg, timeoutMs2);
|
|
110
|
+
},
|
|
111
|
+
{ cfg: validated, timeoutMs }
|
|
112
|
+
);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
async function removeSWChaos(page, opts = {}) {
|
|
116
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
117
|
+
await page.evaluate(
|
|
118
|
+
async ({ timeoutMs: timeoutMs2 }) => {
|
|
119
|
+
const bridge = globalThis.__chaosMakerSWBridge;
|
|
120
|
+
if (!bridge) return;
|
|
121
|
+
try {
|
|
122
|
+
await bridge.stop(timeoutMs2);
|
|
123
|
+
} finally {
|
|
124
|
+
bridge.clearLocalLog();
|
|
125
|
+
await bridge.clearRemoteLog?.(timeoutMs2).catch(() => void 0);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ timeoutMs }
|
|
129
|
+
).catch(() => {
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function enableSWGroup(page, name, opts = {}) {
|
|
133
|
+
if (typeof name !== "string") {
|
|
134
|
+
throw new Error("[chaos-maker] group name must be a string");
|
|
135
|
+
}
|
|
136
|
+
const nameNorm = name.trim();
|
|
137
|
+
if (!nameNorm) {
|
|
138
|
+
throw new Error("[chaos-maker] group name cannot be empty");
|
|
139
|
+
}
|
|
140
|
+
await toggleSWGroup(page, nameNorm, true, opts);
|
|
141
|
+
}
|
|
142
|
+
async function disableSWGroup(page, name, opts = {}) {
|
|
143
|
+
if (typeof name !== "string") {
|
|
144
|
+
throw new Error("[chaos-maker] group name must be a string");
|
|
145
|
+
}
|
|
146
|
+
const nameNorm = name.trim();
|
|
147
|
+
if (!nameNorm) {
|
|
148
|
+
throw new Error("[chaos-maker] group name cannot be empty");
|
|
149
|
+
}
|
|
150
|
+
await toggleSWGroup(page, nameNorm, false, opts);
|
|
151
|
+
}
|
|
152
|
+
async function toggleSWGroup(page, name, enabled, opts) {
|
|
153
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_SW_TOGGLE_TIMEOUT;
|
|
154
|
+
await ensurePageBridge(page);
|
|
155
|
+
await page.evaluate(
|
|
156
|
+
async ({ n, e, t }) => {
|
|
157
|
+
const bridge = globalThis.__chaosMakerSWBridge;
|
|
158
|
+
if (!bridge) throw new Error("[chaos-maker] SW bridge missing \u2014 ensurePageBridge failed");
|
|
159
|
+
await bridge.toggleGroup(n, e, t);
|
|
160
|
+
},
|
|
161
|
+
{ n: name, e: enabled, t: timeoutMs }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
async function getSWChaosLog(page) {
|
|
165
|
+
return page.evaluate(() => {
|
|
166
|
+
const bridge = globalThis.__chaosMakerSWBridge;
|
|
167
|
+
if (!bridge) return [];
|
|
168
|
+
return bridge.getLocalLog();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function getSWChaosLogFromSW(page, opts = {}) {
|
|
172
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
173
|
+
return page.evaluate(
|
|
174
|
+
async ({ timeoutMs: timeoutMs2 }) => {
|
|
175
|
+
const bridge = globalThis.__chaosMakerSWBridge;
|
|
176
|
+
if (!bridge) return [];
|
|
177
|
+
return bridge.getRemoteLog(timeoutMs2);
|
|
178
|
+
},
|
|
179
|
+
{ timeoutMs }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/index.ts
|
|
184
|
+
var cachedUmdPath = null;
|
|
185
|
+
function getCoreUmdPath() {
|
|
186
|
+
if (cachedUmdPath) return cachedUmdPath;
|
|
187
|
+
const currentDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
|
|
188
|
+
const req = createRequire(resolve(currentDir, "package.json"));
|
|
189
|
+
const coreEntry = req.resolve("@chaos-maker/core");
|
|
190
|
+
const coreDistDir = dirname(coreEntry);
|
|
191
|
+
cachedUmdPath = resolve(coreDistDir, "chaos-maker.umd.js");
|
|
192
|
+
return cachedUmdPath;
|
|
193
|
+
}
|
|
194
|
+
var TRACE_HANDLE_KEY2 = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceHandle");
|
|
195
|
+
async function injectChaos(page, config, opts = {}) {
|
|
196
|
+
const validated = validateChaosConfig2(config, opts.validation);
|
|
197
|
+
const umdPath = getCoreUmdPath();
|
|
198
|
+
const tracingEnabled = resolveTracing(opts);
|
|
199
|
+
if (tracingEnabled) {
|
|
200
|
+
if (!opts.testInfo) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"[chaos-maker] tracing requires a `testInfo` in InjectChaosOptions. Use the fixture (`@chaos-maker/playwright/fixture`) or pass testInfo explicitly."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const existing = page[TRACE_HANDLE_KEY2];
|
|
206
|
+
if (!existing) {
|
|
207
|
+
const handle = await createTraceReporter(page, opts.testInfo, opts.traceOptions);
|
|
208
|
+
page[TRACE_HANDLE_KEY2] = handle;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const serialized = serializeForTransport(validated);
|
|
212
|
+
await page.addInitScript((cfg) => {
|
|
213
|
+
const win = globalThis;
|
|
214
|
+
win.__CHAOS_CONFIG__ = cfg;
|
|
215
|
+
}, serialized);
|
|
216
|
+
await page.addInitScript({ path: umdPath });
|
|
217
|
+
}
|
|
218
|
+
function resolveTracing(opts) {
|
|
219
|
+
if (opts.tracing === true) return true;
|
|
220
|
+
if (opts.tracing === false || opts.tracing === void 0) return false;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
async function removeChaos(page) {
|
|
224
|
+
const handle = page[TRACE_HANDLE_KEY2];
|
|
225
|
+
let seed = null;
|
|
226
|
+
if (handle) {
|
|
227
|
+
try {
|
|
228
|
+
seed = await getChaosSeed(page);
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
await page.evaluate(() => {
|
|
233
|
+
const win = globalThis;
|
|
234
|
+
if (win.chaosUtils) {
|
|
235
|
+
win.chaosUtils.stop();
|
|
236
|
+
}
|
|
237
|
+
}).catch(() => {
|
|
238
|
+
});
|
|
239
|
+
if (handle) {
|
|
240
|
+
await handle.dispose(seed);
|
|
241
|
+
delete page[TRACE_HANDLE_KEY2];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function getChaosLog(page) {
|
|
245
|
+
return page.evaluate(() => {
|
|
246
|
+
const win = globalThis;
|
|
247
|
+
if (win.chaosUtils) {
|
|
248
|
+
return win.chaosUtils.getLog();
|
|
249
|
+
}
|
|
250
|
+
return [];
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async function enableGroup(page, name) {
|
|
254
|
+
if (typeof name !== "string") {
|
|
255
|
+
throw new Error("[chaos-maker] group name must be a string");
|
|
256
|
+
}
|
|
257
|
+
const nameNorm = name.trim();
|
|
258
|
+
if (!nameNorm) {
|
|
259
|
+
throw new Error("[chaos-maker] group name cannot be empty");
|
|
260
|
+
}
|
|
261
|
+
await page.evaluate(({ n }) => {
|
|
262
|
+
const utils = globalThis.chaosUtils;
|
|
263
|
+
if (!utils || !utils.instance) {
|
|
264
|
+
throw new Error("[chaos-maker] no chaos instance on page \u2014 call injectChaos first");
|
|
265
|
+
}
|
|
266
|
+
if (typeof utils.enableGroup !== "function") {
|
|
267
|
+
throw new Error("[chaos-maker] enableGroup API unavailable");
|
|
268
|
+
}
|
|
269
|
+
const result = utils.enableGroup(n);
|
|
270
|
+
if (result && result.success === false) {
|
|
271
|
+
throw new Error(`[chaos-maker] enableGroup('${n}') failed: ${result.message}`);
|
|
272
|
+
}
|
|
273
|
+
}, { n: nameNorm });
|
|
274
|
+
}
|
|
275
|
+
async function disableGroup(page, name) {
|
|
276
|
+
if (typeof name !== "string") {
|
|
277
|
+
throw new Error("[chaos-maker] group name must be a string");
|
|
278
|
+
}
|
|
279
|
+
const nameNorm = name.trim();
|
|
280
|
+
if (!nameNorm) {
|
|
281
|
+
throw new Error("[chaos-maker] group name cannot be empty");
|
|
282
|
+
}
|
|
283
|
+
await page.evaluate(({ n }) => {
|
|
284
|
+
const utils = globalThis.chaosUtils;
|
|
285
|
+
if (!utils || !utils.instance) {
|
|
286
|
+
throw new Error("[chaos-maker] no chaos instance on page \u2014 call injectChaos first");
|
|
287
|
+
}
|
|
288
|
+
if (typeof utils.disableGroup !== "function") {
|
|
289
|
+
throw new Error("[chaos-maker] disableGroup API unavailable");
|
|
290
|
+
}
|
|
291
|
+
const result = utils.disableGroup(n);
|
|
292
|
+
if (result && result.success === false) {
|
|
293
|
+
throw new Error(`[chaos-maker] disableGroup('${n}') failed: ${result.message}`);
|
|
294
|
+
}
|
|
295
|
+
}, { n: nameNorm });
|
|
296
|
+
}
|
|
297
|
+
async function getChaosSeed(page) {
|
|
298
|
+
return page.evaluate(() => {
|
|
299
|
+
const win = globalThis;
|
|
300
|
+
if (win.chaosUtils) {
|
|
301
|
+
return win.chaosUtils.getSeed();
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export {
|
|
308
|
+
injectSWChaos,
|
|
309
|
+
removeSWChaos,
|
|
310
|
+
enableSWGroup,
|
|
311
|
+
disableSWGroup,
|
|
312
|
+
getSWChaosLog,
|
|
313
|
+
getSWChaosLogFromSW,
|
|
314
|
+
injectChaos,
|
|
315
|
+
removeChaos,
|
|
316
|
+
getChaosLog,
|
|
317
|
+
enableGroup,
|
|
318
|
+
disableGroup,
|
|
319
|
+
getChaosSeed,
|
|
320
|
+
Logger,
|
|
321
|
+
validateChaosConfig3 as validateChaosConfig,
|
|
322
|
+
ChaosConfigError,
|
|
323
|
+
formatSeedReproduction
|
|
324
|
+
};
|