@chaos-maker/playwright 0.1.0 → 0.2.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
@@ -92,12 +92,16 @@ test('checkout handles combined chaos', async ({ page }) => {
92
92
 
93
93
  ## API
94
94
 
95
- ### `injectChaos(page, config)`
95
+ ### `injectChaos(page, config, opts?)`
96
96
 
97
97
  Inject chaos into a Playwright page. **Call before `page.goto()`** to ensure all network requests are intercepted from the start.
98
98
 
99
99
  - `page` — Playwright `Page` instance
100
100
  - `config` — `ChaosConfig` object (see [@chaos-maker/core](../core/) for full config reference)
101
+ - `opts` — optional. `InjectChaosOptions`:
102
+ - `tracing?: boolean | 'auto'` — emit chaos events into the Playwright trace (see [Debugging with trace](#debugging-with-trace)). Requires `testInfo` when `true`.
103
+ - `testInfo?: TestInfo` — active Playwright `TestInfo` (supplied automatically by the fixture).
104
+ - `traceOptions?: { verbose?: boolean; attachmentName?: string }` — tune trace output.
101
105
 
102
106
  ### `removeChaos(page)`
103
107
 
@@ -115,6 +119,54 @@ Available when importing `test` from `@chaos-maker/playwright/fixture`:
115
119
  - `chaos.remove()` — same as `removeChaos(page)` (also called automatically after each test)
116
120
  - `chaos.getLog()` — same as `getChaosLog(page)`
117
121
 
122
+ ## Debugging with trace
123
+
124
+ 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`.
125
+
126
+ ```ts
127
+ // playwright.config.ts
128
+ export default defineConfig({
129
+ use: {
130
+ trace: 'on-first-retry', // or 'on' / 'retain-on-failure'
131
+ },
132
+ });
133
+ ```
134
+
135
+ ```ts
136
+ import { test, expect } from '@chaos-maker/playwright/fixture';
137
+
138
+ test('flaky checkout', async ({ page, chaos }) => {
139
+ await chaos.inject({
140
+ network: {
141
+ failures: [{ urlPattern: '/api/pay', statusCode: 503, probability: 1.0 }],
142
+ },
143
+ });
144
+ await page.goto('/checkout');
145
+ await page.click('#pay');
146
+ await expect(page.getByText('Order placed')).toBeVisible(); // fails
147
+ });
148
+ ```
149
+
150
+ On failure, open the trace (`pnpm exec playwright show-trace ...`). You'll see a step like:
151
+
152
+ > `chaos:network:failure /api/pay → 503`
153
+
154
+ …alongside the `page.click` and the failing assertion. The `chaos-log.json` attachment contains the full event stream plus the PRNG seed for exact replay.
155
+
156
+ **Tracing is auto-enabled** by the fixture whenever your project's `use.trace` is anything other than `'off'`. Opt out per-call with `chaos.inject(config, { tracing: false })`.
157
+
158
+ **Direct API users** must supply `testInfo` explicitly:
159
+
160
+ ```ts
161
+ import { injectChaos } from '@chaos-maker/playwright';
162
+ import { test } from '@playwright/test';
163
+
164
+ test('with direct API', async ({ page }, testInfo) => {
165
+ await injectChaos(page, config, { tracing: true, testInfo });
166
+ // ...
167
+ });
168
+ ```
169
+
118
170
  ## License
119
171
 
120
172
  [MIT](../../LICENSE)
@@ -0,0 +1,193 @@
1
+ // src/index.ts
2
+ import { resolve, dirname } from "path";
3
+ import { createRequire } from "module";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // src/trace.ts
7
+ import { test } from "@playwright/test";
8
+ var CHAOS_BINDING = "__chaosMakerReport";
9
+ function formatStepTitle(event) {
10
+ const prefix = `chaos:${event.type}`;
11
+ const d = event.detail ?? {};
12
+ const parts = [];
13
+ const subject = d.url ?? d.selector;
14
+ if (subject) parts.push(truncate(subject, 48));
15
+ const outcome = formatOutcome(event);
16
+ if (outcome) parts.push(`\u2192 ${outcome}`);
17
+ if (!event.applied) parts.push("(skipped)");
18
+ return parts.length > 0 ? `${prefix} ${parts.join(" ")}` : prefix;
19
+ }
20
+ function formatOutcome(event) {
21
+ const d = event.detail ?? {};
22
+ switch (event.type) {
23
+ case "network:failure":
24
+ return d.statusCode != null ? String(d.statusCode) : null;
25
+ case "network:latency":
26
+ return d.delayMs != null ? `+${d.delayMs}ms` : null;
27
+ case "network:abort":
28
+ return "abort";
29
+ case "network:corruption":
30
+ return d.strategy ?? "corrupted";
31
+ case "network:cors":
32
+ return "cors-block";
33
+ case "ui:assault":
34
+ return d.action ?? null;
35
+ case "websocket:drop":
36
+ return d.direction ? `drop ${d.direction}` : "drop";
37
+ case "websocket:delay":
38
+ return d.delayMs != null ? `delay ${d.direction ?? ""} +${d.delayMs}ms` : "delay";
39
+ case "websocket:corrupt":
40
+ return d.strategy ?? "corrupt";
41
+ case "websocket:close":
42
+ return d.closeCode != null ? `close ${d.closeCode}` : "close";
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+ function truncate(s, max) {
48
+ if (s.length <= max) return s;
49
+ return `\u2026${s.slice(-(max - 1))}`;
50
+ }
51
+ function shouldEmitStep(event, verbose) {
52
+ if (event.applied) return true;
53
+ return verbose;
54
+ }
55
+ async function createTraceReporter(page, testInfo, opts = {}) {
56
+ const verbose = opts.verbose ?? false;
57
+ const attachmentName = opts.attachmentName ?? "chaos-log.json";
58
+ const events = [];
59
+ const handler = (_source, event) => {
60
+ events.push(event);
61
+ if (!shouldEmitStep(event, verbose)) return;
62
+ const title = formatStepTitle(event);
63
+ test.step(title, async () => {
64
+ }).catch(() => {
65
+ });
66
+ };
67
+ await page.exposeBinding(CHAOS_BINDING, handler);
68
+ await page.addInitScript((bindingName) => {
69
+ const win = globalThis;
70
+ const attach = () => {
71
+ const utils = win.chaosUtils;
72
+ if (!utils || !utils.instance) return false;
73
+ if (utils.__chaosMakerTraceBound === utils.instance) return true;
74
+ utils.__chaosMakerTraceBound = utils.instance;
75
+ utils.instance.on("*", (event) => {
76
+ try {
77
+ if (typeof win[bindingName] === "function") {
78
+ win[bindingName](event);
79
+ }
80
+ } catch {
81
+ }
82
+ });
83
+ return true;
84
+ };
85
+ if (attach()) return;
86
+ const intervalId = setInterval(() => {
87
+ if (attach()) clearInterval(intervalId);
88
+ }, 10);
89
+ setTimeout(() => clearInterval(intervalId), 5e3);
90
+ }, CHAOS_BINDING);
91
+ return {
92
+ events,
93
+ dispose: async (seed = null) => {
94
+ const payload = {
95
+ seed,
96
+ eventCount: events.length,
97
+ events
98
+ };
99
+ try {
100
+ await testInfo.attach(attachmentName, {
101
+ body: Buffer.from(JSON.stringify(payload, null, 2), "utf-8"),
102
+ contentType: "application/json"
103
+ });
104
+ } catch {
105
+ }
106
+ }
107
+ };
108
+ }
109
+
110
+ // src/index.ts
111
+ var cachedUmdPath = null;
112
+ function getCoreUmdPath() {
113
+ if (cachedUmdPath) return cachedUmdPath;
114
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
115
+ const req = createRequire(resolve(currentDir, "package.json"));
116
+ const coreEntry = req.resolve("@chaos-maker/core");
117
+ const coreDistDir = dirname(coreEntry);
118
+ cachedUmdPath = resolve(coreDistDir, "chaos-maker.umd.js");
119
+ return cachedUmdPath;
120
+ }
121
+ var TRACE_HANDLE_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceHandle");
122
+ async function injectChaos(page, config, opts = {}) {
123
+ const umdPath = getCoreUmdPath();
124
+ const tracingEnabled = resolveTracing(opts);
125
+ if (tracingEnabled) {
126
+ if (!opts.testInfo) {
127
+ throw new Error(
128
+ "[chaos-maker] tracing requires a `testInfo` in InjectChaosOptions. Use the fixture (`@chaos-maker/playwright/fixture`) or pass testInfo explicitly."
129
+ );
130
+ }
131
+ const existing = page[TRACE_HANDLE_KEY];
132
+ if (!existing) {
133
+ const handle = await createTraceReporter(page, opts.testInfo, opts.traceOptions);
134
+ page[TRACE_HANDLE_KEY] = handle;
135
+ }
136
+ }
137
+ await page.addInitScript((cfg) => {
138
+ const win = globalThis;
139
+ win.__CHAOS_CONFIG__ = cfg;
140
+ }, config);
141
+ await page.addInitScript({ path: umdPath });
142
+ }
143
+ function resolveTracing(opts) {
144
+ if (opts.tracing === true) return true;
145
+ if (opts.tracing === false || opts.tracing === void 0) return false;
146
+ return false;
147
+ }
148
+ async function removeChaos(page) {
149
+ const handle = page[TRACE_HANDLE_KEY];
150
+ let seed = null;
151
+ if (handle) {
152
+ try {
153
+ seed = await getChaosSeed(page);
154
+ } catch {
155
+ }
156
+ }
157
+ await page.evaluate(() => {
158
+ const win = globalThis;
159
+ if (win.chaosUtils) {
160
+ win.chaosUtils.stop();
161
+ }
162
+ }).catch(() => {
163
+ });
164
+ if (handle) {
165
+ await handle.dispose(seed);
166
+ delete page[TRACE_HANDLE_KEY];
167
+ }
168
+ }
169
+ async function getChaosLog(page) {
170
+ return page.evaluate(() => {
171
+ const win = globalThis;
172
+ if (win.chaosUtils) {
173
+ return win.chaosUtils.getLog();
174
+ }
175
+ return [];
176
+ });
177
+ }
178
+ async function getChaosSeed(page) {
179
+ return page.evaluate(() => {
180
+ const win = globalThis;
181
+ if (win.chaosUtils) {
182
+ return win.chaosUtils.getSeed();
183
+ }
184
+ return null;
185
+ });
186
+ }
187
+
188
+ export {
189
+ injectChaos,
190
+ removeChaos,
191
+ getChaosLog,
192
+ getChaosSeed
193
+ };
package/dist/fixture.cjs CHANGED
@@ -20,16 +20,122 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/fixture.ts
21
21
  var fixture_exports = {};
22
22
  __export(fixture_exports, {
23
- expect: () => import_test2.expect,
24
- test: () => test
23
+ expect: () => import_test3.expect,
24
+ test: () => test2
25
25
  });
26
26
  module.exports = __toCommonJS(fixture_exports);
27
- var import_test = require("@playwright/test");
27
+ var import_test2 = require("@playwright/test");
28
28
 
29
29
  // src/index.ts
30
30
  var import_path = require("path");
31
31
  var import_module = require("module");
32
32
  var import_url = require("url");
33
+
34
+ // src/trace.ts
35
+ var import_test = require("@playwright/test");
36
+ var CHAOS_BINDING = "__chaosMakerReport";
37
+ function formatStepTitle(event) {
38
+ const prefix = `chaos:${event.type}`;
39
+ const d = event.detail ?? {};
40
+ const parts = [];
41
+ const subject = d.url ?? d.selector;
42
+ if (subject) parts.push(truncate(subject, 48));
43
+ const outcome = formatOutcome(event);
44
+ if (outcome) parts.push(`\u2192 ${outcome}`);
45
+ if (!event.applied) parts.push("(skipped)");
46
+ return parts.length > 0 ? `${prefix} ${parts.join(" ")}` : prefix;
47
+ }
48
+ function formatOutcome(event) {
49
+ const d = event.detail ?? {};
50
+ switch (event.type) {
51
+ case "network:failure":
52
+ return d.statusCode != null ? String(d.statusCode) : null;
53
+ case "network:latency":
54
+ return d.delayMs != null ? `+${d.delayMs}ms` : null;
55
+ case "network:abort":
56
+ return "abort";
57
+ case "network:corruption":
58
+ return d.strategy ?? "corrupted";
59
+ case "network:cors":
60
+ return "cors-block";
61
+ case "ui:assault":
62
+ return d.action ?? null;
63
+ case "websocket:drop":
64
+ return d.direction ? `drop ${d.direction}` : "drop";
65
+ case "websocket:delay":
66
+ return d.delayMs != null ? `delay ${d.direction ?? ""} +${d.delayMs}ms` : "delay";
67
+ case "websocket:corrupt":
68
+ return d.strategy ?? "corrupt";
69
+ case "websocket:close":
70
+ return d.closeCode != null ? `close ${d.closeCode}` : "close";
71
+ default:
72
+ return null;
73
+ }
74
+ }
75
+ function truncate(s, max) {
76
+ if (s.length <= max) return s;
77
+ return `\u2026${s.slice(-(max - 1))}`;
78
+ }
79
+ function shouldEmitStep(event, verbose) {
80
+ if (event.applied) return true;
81
+ return verbose;
82
+ }
83
+ async function createTraceReporter(page, testInfo, opts = {}) {
84
+ const verbose = opts.verbose ?? false;
85
+ const attachmentName = opts.attachmentName ?? "chaos-log.json";
86
+ const events = [];
87
+ const handler = (_source, event) => {
88
+ events.push(event);
89
+ if (!shouldEmitStep(event, verbose)) return;
90
+ const title = formatStepTitle(event);
91
+ import_test.test.step(title, async () => {
92
+ }).catch(() => {
93
+ });
94
+ };
95
+ await page.exposeBinding(CHAOS_BINDING, handler);
96
+ await page.addInitScript((bindingName) => {
97
+ const win = globalThis;
98
+ const attach = () => {
99
+ const utils = win.chaosUtils;
100
+ if (!utils || !utils.instance) return false;
101
+ if (utils.__chaosMakerTraceBound === utils.instance) return true;
102
+ utils.__chaosMakerTraceBound = utils.instance;
103
+ utils.instance.on("*", (event) => {
104
+ try {
105
+ if (typeof win[bindingName] === "function") {
106
+ win[bindingName](event);
107
+ }
108
+ } catch {
109
+ }
110
+ });
111
+ return true;
112
+ };
113
+ if (attach()) return;
114
+ const intervalId = setInterval(() => {
115
+ if (attach()) clearInterval(intervalId);
116
+ }, 10);
117
+ setTimeout(() => clearInterval(intervalId), 5e3);
118
+ }, CHAOS_BINDING);
119
+ return {
120
+ events,
121
+ dispose: async (seed = null) => {
122
+ const payload = {
123
+ seed,
124
+ eventCount: events.length,
125
+ events
126
+ };
127
+ try {
128
+ await testInfo.attach(attachmentName, {
129
+ body: Buffer.from(JSON.stringify(payload, null, 2), "utf-8"),
130
+ contentType: "application/json"
131
+ });
132
+ } catch {
133
+ }
134
+ }
135
+ };
136
+ }
137
+
138
+ // src/index.ts
33
139
  var import_meta = {};
34
140
  var cachedUmdPath = null;
35
141
  function getCoreUmdPath() {
@@ -41,21 +147,53 @@ function getCoreUmdPath() {
41
147
  cachedUmdPath = (0, import_path.resolve)(coreDistDir, "chaos-maker.umd.js");
42
148
  return cachedUmdPath;
43
149
  }
44
- async function injectChaos(page, config) {
150
+ var TRACE_HANDLE_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceHandle");
151
+ async function injectChaos(page, config, opts = {}) {
45
152
  const umdPath = getCoreUmdPath();
153
+ const tracingEnabled = resolveTracing(opts);
154
+ if (tracingEnabled) {
155
+ if (!opts.testInfo) {
156
+ throw new Error(
157
+ "[chaos-maker] tracing requires a `testInfo` in InjectChaosOptions. Use the fixture (`@chaos-maker/playwright/fixture`) or pass testInfo explicitly."
158
+ );
159
+ }
160
+ const existing = page[TRACE_HANDLE_KEY];
161
+ if (!existing) {
162
+ const handle = await createTraceReporter(page, opts.testInfo, opts.traceOptions);
163
+ page[TRACE_HANDLE_KEY] = handle;
164
+ }
165
+ }
46
166
  await page.addInitScript((cfg) => {
47
167
  const win = globalThis;
48
168
  win.__CHAOS_CONFIG__ = cfg;
49
169
  }, config);
50
170
  await page.addInitScript({ path: umdPath });
51
171
  }
172
+ function resolveTracing(opts) {
173
+ if (opts.tracing === true) return true;
174
+ if (opts.tracing === false || opts.tracing === void 0) return false;
175
+ return false;
176
+ }
52
177
  async function removeChaos(page) {
178
+ const handle = page[TRACE_HANDLE_KEY];
179
+ let seed = null;
180
+ if (handle) {
181
+ try {
182
+ seed = await getChaosSeed(page);
183
+ } catch {
184
+ }
185
+ }
53
186
  await page.evaluate(() => {
54
187
  const win = globalThis;
55
188
  if (win.chaosUtils) {
56
189
  win.chaosUtils.stop();
57
190
  }
191
+ }).catch(() => {
58
192
  });
193
+ if (handle) {
194
+ await handle.dispose(seed);
195
+ delete page[TRACE_HANDLE_KEY];
196
+ }
59
197
  }
60
198
  async function getChaosLog(page) {
61
199
  return page.evaluate(() => {
@@ -66,15 +204,45 @@ async function getChaosLog(page) {
66
204
  return [];
67
205
  });
68
206
  }
207
+ async function getChaosSeed(page) {
208
+ return page.evaluate(() => {
209
+ const win = globalThis;
210
+ if (win.chaosUtils) {
211
+ return win.chaosUtils.getSeed();
212
+ }
213
+ return null;
214
+ });
215
+ }
69
216
 
70
217
  // src/fixture.ts
71
- var import_test2 = require("@playwright/test");
72
- var test = import_test.test.extend({
73
- chaos: async ({ page }, use) => {
218
+ var import_test3 = require("@playwright/test");
219
+ function shouldAutoTrace(testInfo) {
220
+ const trace = testInfo.project.use?.trace;
221
+ if (trace == null) return false;
222
+ if (typeof trace === "string") return trace !== "off";
223
+ if (typeof trace === "object" && trace !== null && "mode" in trace) {
224
+ return trace.mode !== "off";
225
+ }
226
+ return false;
227
+ }
228
+ var test2 = import_test2.test.extend({
229
+ chaos: async ({ page }, use, testInfo) => {
230
+ const autoTrace = shouldAutoTrace(testInfo);
74
231
  const fixture = {
75
- inject: (config) => injectChaos(page, config),
232
+ inject: (config, opts = {}) => {
233
+ let tracing = opts.tracing;
234
+ if (tracing === void 0 || tracing === "auto") {
235
+ tracing = autoTrace;
236
+ }
237
+ return injectChaos(page, config, {
238
+ ...opts,
239
+ tracing,
240
+ testInfo: opts.testInfo ?? testInfo
241
+ });
242
+ },
76
243
  remove: () => removeChaos(page),
77
- getLog: () => getChaosLog(page)
244
+ getLog: () => getChaosLog(page),
245
+ getSeed: () => getChaosSeed(page)
78
246
  };
79
247
  await use(fixture);
80
248
  await removeChaos(page);
@@ -2,15 +2,20 @@ import * as _playwright_test from '@playwright/test';
2
2
  export { expect } from '@playwright/test';
3
3
  import { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
4
4
  export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
5
+ import { InjectChaosOptions } from './index.cjs';
5
6
 
6
7
  interface ChaosFixture {
7
- inject: (config: ChaosConfig) => Promise<void>;
8
+ inject: (config: ChaosConfig, opts?: InjectChaosOptions) => Promise<void>;
8
9
  remove: () => Promise<void>;
9
10
  getLog: () => Promise<ChaosEvent[]>;
11
+ getSeed: () => Promise<number | null>;
10
12
  }
11
13
  /**
12
14
  * Extended Playwright test with a `chaos` fixture.
13
15
  *
16
+ * Tracing is auto-enabled when the project's `use.trace` config is not `'off'`.
17
+ * Override with `chaos.inject(config, { tracing: false })` to opt out.
18
+ *
14
19
  * @example
15
20
  * ```ts
16
21
  * import { test, expect } from '@chaos-maker/playwright/fixture';
@@ -31,4 +36,4 @@ declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArg
31
36
  chaos: ChaosFixture;
32
37
  }, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
33
38
 
34
- export { type ChaosFixture, test };
39
+ export { type ChaosFixture, InjectChaosOptions, test };
package/dist/fixture.d.ts CHANGED
@@ -2,15 +2,20 @@ import * as _playwright_test from '@playwright/test';
2
2
  export { expect } from '@playwright/test';
3
3
  import { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
4
4
  export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
5
+ import { InjectChaosOptions } from './index.js';
5
6
 
6
7
  interface ChaosFixture {
7
- inject: (config: ChaosConfig) => Promise<void>;
8
+ inject: (config: ChaosConfig, opts?: InjectChaosOptions) => Promise<void>;
8
9
  remove: () => Promise<void>;
9
10
  getLog: () => Promise<ChaosEvent[]>;
11
+ getSeed: () => Promise<number | null>;
10
12
  }
11
13
  /**
12
14
  * Extended Playwright test with a `chaos` fixture.
13
15
  *
16
+ * Tracing is auto-enabled when the project's `use.trace` config is not `'off'`.
17
+ * Override with `chaos.inject(config, { tracing: false })` to opt out.
18
+ *
14
19
  * @example
15
20
  * ```ts
16
21
  * import { test, expect } from '@chaos-maker/playwright/fixture';
@@ -31,4 +36,4 @@ declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArg
31
36
  chaos: ChaosFixture;
32
37
  }, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
33
38
 
34
- export { type ChaosFixture, test };
39
+ export { type ChaosFixture, InjectChaosOptions, test };
package/dist/fixture.js CHANGED
@@ -1,18 +1,40 @@
1
1
  import {
2
2
  getChaosLog,
3
+ getChaosSeed,
3
4
  injectChaos,
4
5
  removeChaos
5
- } from "./chunk-BZSMGQHL.js";
6
+ } from "./chunk-XVP3BFFM.js";
6
7
 
7
8
  // src/fixture.ts
8
9
  import { test as base } from "@playwright/test";
9
10
  import { expect } from "@playwright/test";
11
+ function shouldAutoTrace(testInfo) {
12
+ const trace = testInfo.project.use?.trace;
13
+ if (trace == null) return false;
14
+ if (typeof trace === "string") return trace !== "off";
15
+ if (typeof trace === "object" && trace !== null && "mode" in trace) {
16
+ return trace.mode !== "off";
17
+ }
18
+ return false;
19
+ }
10
20
  var test = base.extend({
11
- chaos: async ({ page }, use) => {
21
+ chaos: async ({ page }, use, testInfo) => {
22
+ const autoTrace = shouldAutoTrace(testInfo);
12
23
  const fixture = {
13
- inject: (config) => injectChaos(page, config),
24
+ inject: (config, opts = {}) => {
25
+ let tracing = opts.tracing;
26
+ if (tracing === void 0 || tracing === "auto") {
27
+ tracing = autoTrace;
28
+ }
29
+ return injectChaos(page, config, {
30
+ ...opts,
31
+ tracing,
32
+ testInfo: opts.testInfo ?? testInfo
33
+ });
34
+ },
14
35
  remove: () => removeChaos(page),
15
- getLog: () => getChaosLog(page)
36
+ getLog: () => getChaosLog(page),
37
+ getSeed: () => getChaosSeed(page)
16
38
  };
17
39
  await use(fixture);
18
40
  await removeChaos(page);
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  getChaosLog: () => getChaosLog,
24
+ getChaosSeed: () => getChaosSeed,
24
25
  injectChaos: () => injectChaos,
25
26
  removeChaos: () => removeChaos
26
27
  });
@@ -28,6 +29,112 @@ module.exports = __toCommonJS(index_exports);
28
29
  var import_path = require("path");
29
30
  var import_module = require("module");
30
31
  var import_url = require("url");
32
+
33
+ // src/trace.ts
34
+ var import_test = require("@playwright/test");
35
+ var CHAOS_BINDING = "__chaosMakerReport";
36
+ function formatStepTitle(event) {
37
+ const prefix = `chaos:${event.type}`;
38
+ const d = event.detail ?? {};
39
+ const parts = [];
40
+ const subject = d.url ?? d.selector;
41
+ if (subject) parts.push(truncate(subject, 48));
42
+ const outcome = formatOutcome(event);
43
+ if (outcome) parts.push(`\u2192 ${outcome}`);
44
+ if (!event.applied) parts.push("(skipped)");
45
+ return parts.length > 0 ? `${prefix} ${parts.join(" ")}` : prefix;
46
+ }
47
+ function formatOutcome(event) {
48
+ const d = event.detail ?? {};
49
+ switch (event.type) {
50
+ case "network:failure":
51
+ return d.statusCode != null ? String(d.statusCode) : null;
52
+ case "network:latency":
53
+ return d.delayMs != null ? `+${d.delayMs}ms` : null;
54
+ case "network:abort":
55
+ return "abort";
56
+ case "network:corruption":
57
+ return d.strategy ?? "corrupted";
58
+ case "network:cors":
59
+ return "cors-block";
60
+ case "ui:assault":
61
+ return d.action ?? null;
62
+ case "websocket:drop":
63
+ return d.direction ? `drop ${d.direction}` : "drop";
64
+ case "websocket:delay":
65
+ return d.delayMs != null ? `delay ${d.direction ?? ""} +${d.delayMs}ms` : "delay";
66
+ case "websocket:corrupt":
67
+ return d.strategy ?? "corrupt";
68
+ case "websocket:close":
69
+ return d.closeCode != null ? `close ${d.closeCode}` : "close";
70
+ default:
71
+ return null;
72
+ }
73
+ }
74
+ function truncate(s, max) {
75
+ if (s.length <= max) return s;
76
+ return `\u2026${s.slice(-(max - 1))}`;
77
+ }
78
+ function shouldEmitStep(event, verbose) {
79
+ if (event.applied) return true;
80
+ return verbose;
81
+ }
82
+ async function createTraceReporter(page, testInfo, opts = {}) {
83
+ const verbose = opts.verbose ?? false;
84
+ const attachmentName = opts.attachmentName ?? "chaos-log.json";
85
+ const events = [];
86
+ const handler = (_source, event) => {
87
+ events.push(event);
88
+ if (!shouldEmitStep(event, verbose)) return;
89
+ const title = formatStepTitle(event);
90
+ import_test.test.step(title, async () => {
91
+ }).catch(() => {
92
+ });
93
+ };
94
+ await page.exposeBinding(CHAOS_BINDING, handler);
95
+ await page.addInitScript((bindingName) => {
96
+ const win = globalThis;
97
+ const attach = () => {
98
+ const utils = win.chaosUtils;
99
+ if (!utils || !utils.instance) return false;
100
+ if (utils.__chaosMakerTraceBound === utils.instance) return true;
101
+ utils.__chaosMakerTraceBound = utils.instance;
102
+ utils.instance.on("*", (event) => {
103
+ try {
104
+ if (typeof win[bindingName] === "function") {
105
+ win[bindingName](event);
106
+ }
107
+ } catch {
108
+ }
109
+ });
110
+ return true;
111
+ };
112
+ if (attach()) return;
113
+ const intervalId = setInterval(() => {
114
+ if (attach()) clearInterval(intervalId);
115
+ }, 10);
116
+ setTimeout(() => clearInterval(intervalId), 5e3);
117
+ }, CHAOS_BINDING);
118
+ return {
119
+ events,
120
+ dispose: async (seed = null) => {
121
+ const payload = {
122
+ seed,
123
+ eventCount: events.length,
124
+ events
125
+ };
126
+ try {
127
+ await testInfo.attach(attachmentName, {
128
+ body: Buffer.from(JSON.stringify(payload, null, 2), "utf-8"),
129
+ contentType: "application/json"
130
+ });
131
+ } catch {
132
+ }
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/index.ts
31
138
  var import_meta = {};
32
139
  var cachedUmdPath = null;
33
140
  function getCoreUmdPath() {
@@ -39,21 +146,53 @@ function getCoreUmdPath() {
39
146
  cachedUmdPath = (0, import_path.resolve)(coreDistDir, "chaos-maker.umd.js");
40
147
  return cachedUmdPath;
41
148
  }
42
- async function injectChaos(page, config) {
149
+ var TRACE_HANDLE_KEY = /* @__PURE__ */ Symbol.for("chaos-maker.playwright.traceHandle");
150
+ async function injectChaos(page, config, opts = {}) {
43
151
  const umdPath = getCoreUmdPath();
152
+ const tracingEnabled = resolveTracing(opts);
153
+ if (tracingEnabled) {
154
+ if (!opts.testInfo) {
155
+ throw new Error(
156
+ "[chaos-maker] tracing requires a `testInfo` in InjectChaosOptions. Use the fixture (`@chaos-maker/playwright/fixture`) or pass testInfo explicitly."
157
+ );
158
+ }
159
+ const existing = page[TRACE_HANDLE_KEY];
160
+ if (!existing) {
161
+ const handle = await createTraceReporter(page, opts.testInfo, opts.traceOptions);
162
+ page[TRACE_HANDLE_KEY] = handle;
163
+ }
164
+ }
44
165
  await page.addInitScript((cfg) => {
45
166
  const win = globalThis;
46
167
  win.__CHAOS_CONFIG__ = cfg;
47
168
  }, config);
48
169
  await page.addInitScript({ path: umdPath });
49
170
  }
171
+ function resolveTracing(opts) {
172
+ if (opts.tracing === true) return true;
173
+ if (opts.tracing === false || opts.tracing === void 0) return false;
174
+ return false;
175
+ }
50
176
  async function removeChaos(page) {
177
+ const handle = page[TRACE_HANDLE_KEY];
178
+ let seed = null;
179
+ if (handle) {
180
+ try {
181
+ seed = await getChaosSeed(page);
182
+ } catch {
183
+ }
184
+ }
51
185
  await page.evaluate(() => {
52
186
  const win = globalThis;
53
187
  if (win.chaosUtils) {
54
188
  win.chaosUtils.stop();
55
189
  }
190
+ }).catch(() => {
56
191
  });
192
+ if (handle) {
193
+ await handle.dispose(seed);
194
+ delete page[TRACE_HANDLE_KEY];
195
+ }
57
196
  }
58
197
  async function getChaosLog(page) {
59
198
  return page.evaluate(() => {
@@ -64,9 +203,19 @@ async function getChaosLog(page) {
64
203
  return [];
65
204
  });
66
205
  }
206
+ async function getChaosSeed(page) {
207
+ return page.evaluate(() => {
208
+ const win = globalThis;
209
+ if (win.chaosUtils) {
210
+ return win.chaosUtils.getSeed();
211
+ }
212
+ return null;
213
+ });
214
+ }
67
215
  // Annotate the CommonJS export names for ESM import in node:
68
216
  0 && (module.exports = {
69
217
  getChaosLog,
218
+ getChaosSeed,
70
219
  injectChaos,
71
220
  removeChaos
72
221
  });
package/dist/index.d.cts CHANGED
@@ -1,7 +1,45 @@
1
- import { Page } from '@playwright/test';
1
+ import { TestInfo, Page } from '@playwright/test';
2
2
  import { ChaosEvent, ChaosConfig } from '@chaos-maker/core';
3
- export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
3
+ export { ChaosConfig, ChaosEvent, WebSocketCloseConfig, WebSocketConfig, WebSocketCorruptConfig, WebSocketCorruptionStrategy, WebSocketDelayConfig, WebSocketDirection, WebSocketDropConfig } from '@chaos-maker/core';
4
4
 
5
+ /**
6
+ * Shape of the JSON attachment written to `testInfo.attachments` on teardown.
7
+ */
8
+ interface ChaosTraceAttachment {
9
+ seed: number | null;
10
+ eventCount: number;
11
+ events: ChaosEvent[];
12
+ }
13
+ interface TraceReporterOptions {
14
+ /** Emit `test.step` for `applied:false` diagnostic events too. Default false. */
15
+ verbose?: boolean;
16
+ /** Attachment name. Default `chaos-log.json`. */
17
+ attachmentName?: string;
18
+ }
19
+
20
+ /**
21
+ * Options for `injectChaos`. Most callers can omit this entirely; defaults
22
+ * preserve backward compatibility with the v0.1.x signature.
23
+ */
24
+ interface InjectChaosOptions {
25
+ /**
26
+ * Emit chaos events into the Playwright trace as `test.step` entries and
27
+ * attach the full event log on test end.
28
+ *
29
+ * - `true` — always on. Requires `testInfo`.
30
+ * - `false` (default for direct `injectChaos()` calls) — off.
31
+ * - `'auto'` (default for the fixture) — on when Playwright tracing is
32
+ * enabled in the project config; no-op otherwise.
33
+ */
34
+ tracing?: boolean | 'auto';
35
+ /**
36
+ * Active Playwright `TestInfo`, required when `tracing` is truthy.
37
+ * The fixture supplies this automatically.
38
+ */
39
+ testInfo?: TestInfo;
40
+ /** Pass through to the trace reporter. */
41
+ traceOptions?: TraceReporterOptions;
42
+ }
5
43
  /**
6
44
  * Inject chaos into a Playwright page. Call before `page.goto()` to ensure
7
45
  * all network requests are intercepted from the start.
@@ -20,7 +58,7 @@ export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
20
58
  * });
21
59
  * ```
22
60
  */
23
- declare function injectChaos(page: Page, config: ChaosConfig): Promise<void>;
61
+ declare function injectChaos(page: Page, config: ChaosConfig, opts?: InjectChaosOptions): Promise<void>;
24
62
  /**
25
63
  * Remove chaos from a Playwright page. Restores original fetch/XHR/DOM behavior.
26
64
  */
@@ -30,5 +68,10 @@ declare function removeChaos(page: Page): Promise<void>;
30
68
  * Returns all events emitted since chaos was injected.
31
69
  */
32
70
  declare function getChaosLog(page: Page): Promise<ChaosEvent[]>;
71
+ /**
72
+ * Retrieve the PRNG seed from a Playwright page.
73
+ * Log this value on test failure to replay exact chaos decisions.
74
+ */
75
+ declare function getChaosSeed(page: Page): Promise<number | null>;
33
76
 
34
- export { getChaosLog, injectChaos, removeChaos };
77
+ export { type ChaosTraceAttachment, type InjectChaosOptions, type TraceReporterOptions, getChaosLog, getChaosSeed, injectChaos, removeChaos };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,45 @@
1
- import { Page } from '@playwright/test';
1
+ import { TestInfo, Page } from '@playwright/test';
2
2
  import { ChaosEvent, ChaosConfig } from '@chaos-maker/core';
3
- export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
3
+ export { ChaosConfig, ChaosEvent, WebSocketCloseConfig, WebSocketConfig, WebSocketCorruptConfig, WebSocketCorruptionStrategy, WebSocketDelayConfig, WebSocketDirection, WebSocketDropConfig } from '@chaos-maker/core';
4
4
 
5
+ /**
6
+ * Shape of the JSON attachment written to `testInfo.attachments` on teardown.
7
+ */
8
+ interface ChaosTraceAttachment {
9
+ seed: number | null;
10
+ eventCount: number;
11
+ events: ChaosEvent[];
12
+ }
13
+ interface TraceReporterOptions {
14
+ /** Emit `test.step` for `applied:false` diagnostic events too. Default false. */
15
+ verbose?: boolean;
16
+ /** Attachment name. Default `chaos-log.json`. */
17
+ attachmentName?: string;
18
+ }
19
+
20
+ /**
21
+ * Options for `injectChaos`. Most callers can omit this entirely; defaults
22
+ * preserve backward compatibility with the v0.1.x signature.
23
+ */
24
+ interface InjectChaosOptions {
25
+ /**
26
+ * Emit chaos events into the Playwright trace as `test.step` entries and
27
+ * attach the full event log on test end.
28
+ *
29
+ * - `true` — always on. Requires `testInfo`.
30
+ * - `false` (default for direct `injectChaos()` calls) — off.
31
+ * - `'auto'` (default for the fixture) — on when Playwright tracing is
32
+ * enabled in the project config; no-op otherwise.
33
+ */
34
+ tracing?: boolean | 'auto';
35
+ /**
36
+ * Active Playwright `TestInfo`, required when `tracing` is truthy.
37
+ * The fixture supplies this automatically.
38
+ */
39
+ testInfo?: TestInfo;
40
+ /** Pass through to the trace reporter. */
41
+ traceOptions?: TraceReporterOptions;
42
+ }
5
43
  /**
6
44
  * Inject chaos into a Playwright page. Call before `page.goto()` to ensure
7
45
  * all network requests are intercepted from the start.
@@ -20,7 +58,7 @@ export { ChaosConfig, ChaosEvent } from '@chaos-maker/core';
20
58
  * });
21
59
  * ```
22
60
  */
23
- declare function injectChaos(page: Page, config: ChaosConfig): Promise<void>;
61
+ declare function injectChaos(page: Page, config: ChaosConfig, opts?: InjectChaosOptions): Promise<void>;
24
62
  /**
25
63
  * Remove chaos from a Playwright page. Restores original fetch/XHR/DOM behavior.
26
64
  */
@@ -30,5 +68,10 @@ declare function removeChaos(page: Page): Promise<void>;
30
68
  * Returns all events emitted since chaos was injected.
31
69
  */
32
70
  declare function getChaosLog(page: Page): Promise<ChaosEvent[]>;
71
+ /**
72
+ * Retrieve the PRNG seed from a Playwright page.
73
+ * Log this value on test failure to replay exact chaos decisions.
74
+ */
75
+ declare function getChaosSeed(page: Page): Promise<number | null>;
33
76
 
34
- export { getChaosLog, injectChaos, removeChaos };
77
+ export { type ChaosTraceAttachment, type InjectChaosOptions, type TraceReporterOptions, getChaosLog, getChaosSeed, injectChaos, removeChaos };
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  getChaosLog,
3
+ getChaosSeed,
3
4
  injectChaos,
4
5
  removeChaos
5
- } from "./chunk-BZSMGQHL.js";
6
+ } from "./chunk-XVP3BFFM.js";
6
7
  export {
7
8
  getChaosLog,
9
+ getChaosSeed,
8
10
  injectChaos,
9
11
  removeChaos
10
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaos-maker/playwright",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Playwright adapter for @chaos-maker/core — one-line chaos injection in E2E tests",
6
6
  "keywords": [
@@ -49,7 +49,7 @@
49
49
  "dist"
50
50
  ],
51
51
  "dependencies": {
52
- "@chaos-maker/core": "0.1.0"
52
+ "@chaos-maker/core": "0.2.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@playwright/test": ">=1.40.0"
@@ -61,12 +61,13 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@playwright/test": "^1.45.0",
64
- "tsup": "^8.0.0",
64
+ "tsup": "^8.5.1",
65
65
  "typescript": "^5.4.5",
66
- "vitest": "^1.6.0"
66
+ "vitest": "^3.2.4"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsup",
70
- "test": "vitest"
70
+ "test": "vitest run",
71
+ "test:watch": "vitest"
71
72
  }
72
73
  }
@@ -1,45 +0,0 @@
1
- // src/index.ts
2
- import { resolve, dirname } from "path";
3
- import { createRequire } from "module";
4
- import { fileURLToPath } from "url";
5
- var cachedUmdPath = null;
6
- function getCoreUmdPath() {
7
- if (cachedUmdPath) return cachedUmdPath;
8
- const currentDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
9
- const req = createRequire(resolve(currentDir, "package.json"));
10
- const coreEntry = req.resolve("@chaos-maker/core");
11
- const coreDistDir = dirname(coreEntry);
12
- cachedUmdPath = resolve(coreDistDir, "chaos-maker.umd.js");
13
- return cachedUmdPath;
14
- }
15
- async function injectChaos(page, config) {
16
- const umdPath = getCoreUmdPath();
17
- await page.addInitScript((cfg) => {
18
- const win = globalThis;
19
- win.__CHAOS_CONFIG__ = cfg;
20
- }, config);
21
- await page.addInitScript({ path: umdPath });
22
- }
23
- async function removeChaos(page) {
24
- await page.evaluate(() => {
25
- const win = globalThis;
26
- if (win.chaosUtils) {
27
- win.chaosUtils.stop();
28
- }
29
- });
30
- }
31
- async function getChaosLog(page) {
32
- return page.evaluate(() => {
33
- const win = globalThis;
34
- if (win.chaosUtils) {
35
- return win.chaosUtils.getLog();
36
- }
37
- return [];
38
- });
39
- }
40
-
41
- export {
42
- injectChaos,
43
- removeChaos,
44
- getChaosLog
45
- };