@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 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 `@chaos-maker/playwright` loads the core UMD bundle into the browser page.
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
- await injectChaos(page, {
23
- network: {
24
- failures: [{ urlPattern: '/api/data', statusCode: 503, probability: 1.0 }]
25
- }
26
- });
27
-
28
- await page.goto('/dashboard');
29
- await expect(page.getByText('Something went wrong')).toBeVisible();
30
-
31
- // Check what chaos was applied
32
- const log = await getChaosLog(page);
33
- expect(log.some(e => e.type === 'network:failure' && e.applied)).toBe(true);
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` Playwright `Page` instance
122
- - `config` `ChaosConfig` object (see [@chaos-maker/core](../core/) for full config reference)
123
- - `opts` optional. `InjectChaosOptions`:
124
- - `tracing?: boolean | 'auto'` emit chaos events into the Playwright trace (see [Debugging with trace](#debugging-with-trace)). Requires `testInfo` when `true`.
125
- - `testInfo?: TestInfo` active Playwright `TestInfo` (supplied automatically by the fixture).
126
- - `traceOptions?: { verbose?: boolean; attachmentName?: string }` tune trace output.
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[]` every chaos check emitted since injection, with `applied: true/false`.
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)` same as `injectChaos(page, config)`
141
- - `chaos.remove()` same as `removeChaos(page)` (also called automatically after each test)
142
- - `chaos.getLog()` same as `getChaosLog(page)`
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 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`.
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 { injectSWChaos, removeSWChaos, getSWChaosLog } from '@chaos-maker/playwright';
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
- network: { failures: [{ urlPattern: '/api/data', statusCode: 503, probability: 1 }] },
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` IIFE bundle for classic SWs (`importScripts('/chaos-maker-sw.js')`).
220
- - `dist/sw.mjs` ESM bundle for module SWs (`import { installChaosSW } from '/chaos-maker-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
+ };