@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 +20 -0
- package/dist/{chunk-3X36PFTS.cjs → chunk-665R4N7R.cjs} +157 -3
- package/dist/chunk-665R4N7R.cjs.map +1 -0
- package/dist/{chunk-3TXDODCO.js → chunk-I2PQGZU7.js} +158 -5
- package/dist/chunk-I2PQGZU7.js.map +1 -0
- package/dist/index.cjs +29 -25
- package/dist/index.d.cts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +1 -1
- package/dist/test.cjs +2 -2
- package/dist/test.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-3TXDODCO.js.map +0 -1
- package/dist/chunk-3X36PFTS.cjs.map +0 -1
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/
|
|
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
|
-
|
|
2082
|
-
//# sourceMappingURL=chunk-
|
|
2234
|
+
exports.replayTimeline = replayTimeline;
|
|
2235
|
+
//# sourceMappingURL=chunk-665R4N7R.cjs.map
|
|
2236
|
+
//# sourceMappingURL=chunk-665R4N7R.cjs.map
|