@humanjs/playwright 0.9.0 → 0.10.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
@@ -380,6 +380,26 @@ Generated tests are built to *be tests*: they run `speed: process.env.CI ? 'inst
380
380
 
381
381
  Two more options: `{ steps: true }` groups the actions into `test.step(...)` blocks (a new step per navigation) for collapsible sections in the HTML report and trace; `{ baseUrl: true }` rewrites same-origin `goto`s to relative paths and adds a note to set `use.baseURL` in your `playwright.config.ts` — so the same test runs against local / staging / prod.
382
382
 
383
+ ### Replaying a timeline
384
+
385
+ `replayTimeline(page, timeline, options?)` runs a recorded `Timeline` against a live page, driving it through the same humanized primitives the exported test uses — without spawning a test runner. It runs each event in order, reports per-step status via `onStep`, and **stops at the first failure** (like a real test).
386
+
387
+ ```ts
388
+ import { chromium, replayTimeline } from '@humanjs/playwright';
389
+
390
+ const page = await (await chromium.launch({ headless: false })).newPage();
391
+ const result = await replayTimeline(page, recording.timeline, {
392
+ personality: 'careful',
393
+ onStep: ({ index, type, status }) => console.log(`${index} ${type}: ${status}`),
394
+ });
395
+
396
+ result.status; // 'pass' | 'fail'
397
+ result.failedIndex; // index of the failing step, when failed
398
+ result.steps; // per-step { index, type, status, error? }
399
+ ```
400
+
401
+ `assert` events are evaluated with plain Playwright APIs — there's no `@playwright/test` dependency — so they approximate `expect` (`toBeVisible` via `waitFor`, `toHaveText` via normalized text equality, `toHaveURL` via `page.url()`) without its auto-retry on text/url. Pass an `AbortSignal` via `{ signal }` to cancel a run; it rejects with an `AbortError`. The cursor is on and `speed` is `'human'` by default so the replay is watchable. You own `page`'s lifecycle — `replayTimeline` doesn't close it. This is the engine behind the `@humanjs/generator` dashboard's **Run** button.
402
+
383
403
  By default the actual typed/pasted text is captured into the timeline (and the exported code). Values typed into `input[type="password"]` are always masked; set `captureInputs: false` to record none — exports then emit empty-string placeholders:
384
404
 
385
405
  ```ts
@@ -945,8 +945,17 @@ async function detectKindFromTag(locator) {
945
945
  return void 0;
946
946
  }
947
947
 
948
- // src/recording/codegen.ts
948
+ // src/recording/targets.ts
949
949
  var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
950
+ function parsePointTarget(desc) {
951
+ const match = String(desc ?? "").match(POINT_RE);
952
+ return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
953
+ }
954
+ function resolveMouseTarget(desc) {
955
+ return parsePointTarget(desc) ?? String(desc ?? "");
956
+ }
957
+
958
+ // src/recording/codegen.ts
950
959
  var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
951
960
  var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
952
961
  function q(value) {
@@ -1182,6 +1191,150 @@ ${todo}
1182
1191
  });
1183
1192
  `;
1184
1193
  }
1194
+ function abortError() {
1195
+ const error = new Error("Replay aborted");
1196
+ error.name = "AbortError";
1197
+ return error;
1198
+ }
1199
+ async function replayTimeline(page, timeline, options = {}) {
1200
+ const events = Array.isArray(timeline) ? timeline : timeline.events;
1201
+ const { onStep, signal } = options;
1202
+ if (signal?.aborted) throw abortError();
1203
+ const human = await createHuman(page, {
1204
+ personality: options.personality ?? "careful",
1205
+ speed: options.speed ?? "human",
1206
+ ...options.seed !== void 0 ? { seed: options.seed } : {},
1207
+ ...options.cursor !== void 0 ? { cursor: options.cursor } : {}
1208
+ });
1209
+ const startedAt = Date.now();
1210
+ const steps = [];
1211
+ for (const [index, event] of events.entries()) {
1212
+ if (signal?.aborted) throw abortError();
1213
+ onStep?.({ index, type: event.type, status: "running" });
1214
+ try {
1215
+ await runEvent(human, page, event);
1216
+ } catch (cause) {
1217
+ const error = cause instanceof Error ? cause.message : String(cause);
1218
+ steps.push({ index, type: event.type, status: "fail", error });
1219
+ onStep?.({ index, type: event.type, status: "fail", error });
1220
+ return { status: "fail", steps, failedIndex: index, durationMs: Date.now() - startedAt };
1221
+ }
1222
+ steps.push({ index, type: event.type, status: "pass" });
1223
+ onStep?.({ index, type: event.type, status: "pass" });
1224
+ }
1225
+ return { status: "pass", steps, durationMs: Date.now() - startedAt };
1226
+ }
1227
+ function parseScrollTarget(target) {
1228
+ const value = String(target ?? "natural");
1229
+ const by = value.match(/^by:(-?\d+(?:\.\d+)?)$/);
1230
+ if (by) return { by: Number(by[1]) };
1231
+ const to = value.match(/^to:(-?\d+(?:\.\d+)?)$/);
1232
+ if (to) return { to: Number(to[1]) };
1233
+ return value;
1234
+ }
1235
+ var normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
1236
+ async function runAssert(page, params) {
1237
+ const kind = String(params.kind ?? "visible");
1238
+ if (kind === "url") {
1239
+ const actual = page.url();
1240
+ const expected = String(params.value ?? "");
1241
+ if (actual !== expected) {
1242
+ throw new Error(`expected URL ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
1243
+ }
1244
+ return;
1245
+ }
1246
+ const locator = page.locator(String(params.target ?? "")).first();
1247
+ await locator.waitFor({ state: "visible" });
1248
+ if (kind === "text") {
1249
+ const actual = normalizeText(await locator.textContent());
1250
+ const expected = normalizeText(String(params.value ?? ""));
1251
+ if (actual !== expected) {
1252
+ throw new Error(`expected text ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
1253
+ }
1254
+ }
1255
+ }
1256
+ async function runEvent(human, page, event) {
1257
+ const p = event.params;
1258
+ switch (event.type) {
1259
+ case "goto":
1260
+ await human.goto(String(p.url ?? ""));
1261
+ return;
1262
+ case "click":
1263
+ await human.click(resolveMouseTarget(p.target));
1264
+ return;
1265
+ case "rightClick":
1266
+ await human.rightClick(resolveMouseTarget(p.target));
1267
+ return;
1268
+ case "doubleClick":
1269
+ await human.doubleClick(resolveMouseTarget(p.target));
1270
+ return;
1271
+ case "move":
1272
+ await human.move(resolveMouseTarget(p.target));
1273
+ return;
1274
+ case "hover":
1275
+ await human.hover(String(p.target ?? ""));
1276
+ return;
1277
+ case "drag":
1278
+ await human.drag(resolveMouseTarget(p.from), resolveMouseTarget(p.to));
1279
+ return;
1280
+ case "type":
1281
+ await human.type(String(p.target ?? ""), event.inputValue ?? "");
1282
+ return;
1283
+ case "paste":
1284
+ await human.paste(String(p.target ?? ""), event.inputValue ?? "");
1285
+ return;
1286
+ case "clear":
1287
+ await human.clear(String(p.target ?? ""));
1288
+ return;
1289
+ case "check":
1290
+ await human.check(String(p.target ?? ""));
1291
+ return;
1292
+ case "uncheck":
1293
+ await human.uncheck(String(p.target ?? ""));
1294
+ return;
1295
+ case "selectText":
1296
+ await human.selectText(
1297
+ String(p.target ?? ""),
1298
+ typeof p.text === "string" ? { text: p.text } : void 0
1299
+ );
1300
+ return;
1301
+ case "selectOption":
1302
+ await human.selectOption(String(p.target ?? ""), p.values);
1303
+ return;
1304
+ case "upload":
1305
+ await human.upload(String(p.target ?? ""), p.files);
1306
+ return;
1307
+ case "press":
1308
+ await human.press(String(p.key ?? ""));
1309
+ return;
1310
+ case "scroll":
1311
+ await human.scroll(parseScrollTarget(p.target));
1312
+ return;
1313
+ case "read": {
1314
+ const target = String(p.target ?? "");
1315
+ if (/^\d+ words$/.test(target) || /^text:\d+ chars$/.test(target)) return;
1316
+ await human.read(target);
1317
+ return;
1318
+ }
1319
+ case "sleep":
1320
+ await core.sleep(Number(p.ms) || 0);
1321
+ return;
1322
+ case "reload":
1323
+ await human.reload();
1324
+ return;
1325
+ case "goBack":
1326
+ await human.goBack();
1327
+ return;
1328
+ case "goForward":
1329
+ await human.goForward();
1330
+ return;
1331
+ case "assert":
1332
+ await runAssert(page, p);
1333
+ return;
1334
+ default:
1335
+ return;
1336
+ }
1337
+ }
1185
1338
 
1186
1339
  // src/recording/index.ts
1187
1340
  var pendingFrameCleanups = /* @__PURE__ */ new Set();
@@ -2078,5 +2231,6 @@ exports.createHuman = createHuman;
2078
2231
  exports.generateHumanJS = generateHumanJS;
2079
2232
  exports.generatePlaywrightTest = generatePlaywrightTest;
2080
2233
  exports.installMouseHelper = installMouseHelper;
2081
- //# sourceMappingURL=chunk-3X36PFTS.cjs.map
2082
- //# sourceMappingURL=chunk-3X36PFTS.cjs.map
2234
+ exports.replayTimeline = replayTimeline;
2235
+ //# sourceMappingURL=chunk-665R4N7R.cjs.map
2236
+ //# sourceMappingURL=chunk-665R4N7R.cjs.map