@olomonhelpdesk2/helpdesk 0.1.15 → 0.1.17
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/helpdesk-capture.mjs +127 -32
- package/package.json +1 -1
package/helpdesk-capture.mjs
CHANGED
|
@@ -1561,6 +1561,8 @@ function readStep(value, at) {
|
|
|
1561
1561
|
if (!route || !isPathLike(route)) return `step ${at} names no screen`;
|
|
1562
1562
|
if (role !== void 0 && (typeof role !== "string" || !CONTROL_ROLES.includes(role))) return `step ${at} names no control role a screen lists`;
|
|
1563
1563
|
if (!name || name.length > MAX_TEXT2) return `step ${at} names no control`;
|
|
1564
|
+
const scope = control["scope"] === void 0 ? void 0 : cleanText(control["scope"]);
|
|
1565
|
+
if (scope !== void 0 && (!scope || scope.length > MAX_TEXT2)) return `step ${at} names a region of the screen with no name`;
|
|
1564
1566
|
if (typeof action !== "string" || !CLIP_ACTIONS.includes(action)) return `step ${at} says neither click, fill, select, check nor auto`;
|
|
1565
1567
|
if (!says || says.length > MAX_TEXT2) return `step ${at} has no sentence for a reader`;
|
|
1566
1568
|
if (role === void 0 && action !== "auto") return `step ${at} says how to act on a control without saying what kind it is`;
|
|
@@ -1569,12 +1571,14 @@ function readStep(value, at) {
|
|
|
1569
1571
|
if (typed && !valued) return `step ${at} has nothing to ${action}`;
|
|
1570
1572
|
if (action !== "auto" && !typed && given !== void 0) return `step ${at} gives a value to ${action}, which takes none`;
|
|
1571
1573
|
if (action === "auto" && given !== void 0 && !valued) return `step ${at} has a value nothing can type`;
|
|
1574
|
+
if (step["follows"] !== void 0 && step["follows"] !== true) return `step ${at} says neither that it follows the step before it nor that it does not`;
|
|
1572
1575
|
return {
|
|
1573
1576
|
route,
|
|
1574
|
-
control: { ...role === void 0 ? {} : { role }, name },
|
|
1577
|
+
control: { ...role === void 0 ? {} : { role }, name, ...scope ? { scope } : {} },
|
|
1575
1578
|
action,
|
|
1576
1579
|
...valued && (typed || action === "auto") ? { value: given } : {},
|
|
1577
|
-
says
|
|
1580
|
+
says,
|
|
1581
|
+
...step["follows"] === true ? { follows: true } : {}
|
|
1578
1582
|
};
|
|
1579
1583
|
}
|
|
1580
1584
|
function readSteps(raw) {
|
|
@@ -1633,6 +1637,10 @@ function readClipScripts(raw, limit = MAX_SEEDED_SCRIPTS) {
|
|
|
1633
1637
|
refuse(`it opens at ${visit || "nowhere"}, which is not ${first.route}`);
|
|
1634
1638
|
continue;
|
|
1635
1639
|
}
|
|
1640
|
+
if (first.follows) {
|
|
1641
|
+
refuse("its first step follows no step before it, and is the screen it opens at");
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1636
1644
|
if (lands !== void 0 && !isPathLike(lands)) {
|
|
1637
1645
|
refuse("it names no screen to land on");
|
|
1638
1646
|
continue;
|
|
@@ -1641,14 +1649,57 @@ function readClipScripts(raw, limit = MAX_SEEDED_SCRIPTS) {
|
|
|
1641
1649
|
}
|
|
1642
1650
|
return { scripts, refused };
|
|
1643
1651
|
}
|
|
1652
|
+
function comparable(text2) {
|
|
1653
|
+
return cleanText(text2).toLowerCase().replace(/(…|\.\.\.)$/, "").trim();
|
|
1654
|
+
}
|
|
1655
|
+
var CHROME_ROLES = ["navigation", "complementary", "banner", "contentinfo"];
|
|
1656
|
+
var IN_CHROME = /\b(sidebar|side bar|menu|navigation|nav bar|top bar|header)\b/i;
|
|
1657
|
+
function within(manifest, control, regionIds) {
|
|
1658
|
+
const regions = new Map(manifest.regions.map((region) => [region.id, region]));
|
|
1659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1660
|
+
for (let id = control.region; id && !seen.has(id); id = regions.get(id)?.parent) {
|
|
1661
|
+
seen.add(id);
|
|
1662
|
+
if (regionIds.has(id)) return true;
|
|
1663
|
+
}
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
function namesControl(shown, named) {
|
|
1667
|
+
const have = comparable(shown);
|
|
1668
|
+
const want = comparable(named);
|
|
1669
|
+
return Boolean(want) && (have === want || have.startsWith(`${want} `));
|
|
1670
|
+
}
|
|
1671
|
+
function inChrome(manifest, control) {
|
|
1672
|
+
const regions = new Map(manifest.regions.map((region) => [region.id, region]));
|
|
1673
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1674
|
+
for (let id = control.region; id && !seen.has(id); id = regions.get(id)?.parent) {
|
|
1675
|
+
seen.add(id);
|
|
1676
|
+
if (CHROME_ROLES.includes(regions.get(id)?.role ?? "")) return true;
|
|
1677
|
+
}
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1644
1680
|
function resolveStep(manifest, step) {
|
|
1645
|
-
const name =
|
|
1681
|
+
const name = comparable(step.control.name);
|
|
1646
1682
|
const where = manifest.screen.path;
|
|
1647
1683
|
const role = step.control.role;
|
|
1648
1684
|
const what = role ?? "control";
|
|
1649
|
-
const
|
|
1685
|
+
const candidates = manifest.controls.filter((control) => !role || control.role === role);
|
|
1686
|
+
let matches = candidates.filter((control) => comparable(control.name) === name);
|
|
1687
|
+
if (!role && !matches.length) matches = candidates.filter((control) => namesControl(control.name, step.control.name));
|
|
1688
|
+
const scope = step.control.scope;
|
|
1689
|
+
if (scope && matches.length > 1) {
|
|
1690
|
+
const regions = new Set(manifest.regions.filter((region) => region.id === scope || comparable(region.label) === comparable(scope)).map((region) => region.id));
|
|
1691
|
+
const scoped = matches.filter((control) => within(manifest, control, regions));
|
|
1692
|
+
if (scoped.length) matches = scoped;
|
|
1693
|
+
}
|
|
1694
|
+
if (!role && matches.length > 1) {
|
|
1695
|
+
const chrome = IN_CHROME.test(step.says);
|
|
1696
|
+
const meant = matches.filter((control) => inChrome(manifest, control) === chrome);
|
|
1697
|
+
if (meant.length === 1) matches = meant;
|
|
1698
|
+
}
|
|
1650
1699
|
if (matches.length === 0) return { ok: false, because: `no ${what} "${step.control.name}" on ${where}` };
|
|
1651
|
-
if (matches.length > 1)
|
|
1700
|
+
if (matches.length > 1) {
|
|
1701
|
+
return { ok: false, because: `${matches.length} of ${what} "${step.control.name}" on ${where}${scope ? ` in ${scope}` : ""}, so the step does not say which` };
|
|
1702
|
+
}
|
|
1652
1703
|
if (matches[0].state?.disabled) return { ok: false, because: `${what} "${step.control.name}" on ${where} is disabled` };
|
|
1653
1704
|
return { ok: true, control: matches[0] };
|
|
1654
1705
|
}
|
|
@@ -1663,6 +1714,7 @@ function clipFingerprint(script, touched) {
|
|
|
1663
1714
|
route: step.route,
|
|
1664
1715
|
role: step.control.role ?? null,
|
|
1665
1716
|
name: step.control.name,
|
|
1717
|
+
...step.control.scope ? { scope: step.control.scope } : {},
|
|
1666
1718
|
action: step.action,
|
|
1667
1719
|
value: step.value ?? null,
|
|
1668
1720
|
says: step.says
|
|
@@ -3130,6 +3182,11 @@ async function playwrightBrowser(options = {}) {
|
|
|
3130
3182
|
return `${url.pathname}${url.search}`;
|
|
3131
3183
|
},
|
|
3132
3184
|
async pointer(action) {
|
|
3185
|
+
if (action.kind === "scroll") {
|
|
3186
|
+
await page.mouse.move(action.at.x, action.at.y);
|
|
3187
|
+
await page.mouse.wheel(0, action.by);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3133
3190
|
if (action.kind === "select") {
|
|
3134
3191
|
const handle = await page.evaluateHandle(({ x, y }) => document.elementFromPoint(x, y), action.at);
|
|
3135
3192
|
const element = handle.asElement();
|
|
@@ -3389,7 +3446,13 @@ var TYPING_FRAMES = 12;
|
|
|
3389
3446
|
var SETTLE_MS = 3e3;
|
|
3390
3447
|
var STEP_WAIT_MS = 4e3;
|
|
3391
3448
|
var CLOSING_MS = 1200;
|
|
3449
|
+
var SCROLL_TO = 0.45;
|
|
3450
|
+
var WHOLE_SHIFT = 1e5;
|
|
3392
3451
|
var START = { x: Math.round(CLIP_VIEWPORT.width / 2), y: Math.round(CLIP_VIEWPORT.height * 0.66) };
|
|
3452
|
+
function wholePage(raw) {
|
|
3453
|
+
const down = (item) => ({ ...item, box: { ...item.box, y: item.box.y + WHOLE_SHIFT } });
|
|
3454
|
+
return { ...raw, viewport: { width: raw.viewport.width, height: 2 * WHOLE_SHIFT }, regions: raw.regions.map(down), controls: raw.controls.map(down) };
|
|
3455
|
+
}
|
|
3393
3456
|
function chunksOf(text2, size) {
|
|
3394
3457
|
return Array.from({ length: Math.ceil(text2.length / size) }, (_, index) => text2.slice(index * size, (index + 1) * size));
|
|
3395
3458
|
}
|
|
@@ -3453,17 +3516,14 @@ async function recordClip(options, script, touched) {
|
|
|
3453
3516
|
return true;
|
|
3454
3517
|
};
|
|
3455
3518
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
}),
|
|
3465
|
-
step
|
|
3466
|
-
);
|
|
3519
|
+
const manifestOf = (raw, step, where) => buildLayoutManifest(raw, {
|
|
3520
|
+
shotId: script.id,
|
|
3521
|
+
route: step.route,
|
|
3522
|
+
path: where.split("?")[0],
|
|
3523
|
+
variant: "desktop",
|
|
3524
|
+
image: { width: raw.viewport.width, height: raw.viewport.height },
|
|
3525
|
+
origin: new URL(options.baseUrl).origin
|
|
3526
|
+
});
|
|
3467
3527
|
const steady = async () => {
|
|
3468
3528
|
let previous = await page.screenshot(cover);
|
|
3469
3529
|
for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
|
|
@@ -3473,6 +3533,35 @@ async function recordClip(options, script, touched) {
|
|
|
3473
3533
|
previous = next;
|
|
3474
3534
|
}
|
|
3475
3535
|
};
|
|
3536
|
+
let resting = START;
|
|
3537
|
+
const find = async (step, where, filming) => {
|
|
3538
|
+
const raw = await layout(cover);
|
|
3539
|
+
const shown = resolveStep(manifestOf(raw, step, where), step);
|
|
3540
|
+
if (shown.ok) return shown;
|
|
3541
|
+
const whole = resolveStep(manifestOf(wholePage(raw), step, where), step);
|
|
3542
|
+
if (!whole.ok) return shown;
|
|
3543
|
+
const box = whole.control.box;
|
|
3544
|
+
const by = Math.round(box.y - WHOLE_SHIFT + box.height / 2 - CLIP_VIEWPORT.height * SCROLL_TO);
|
|
3545
|
+
const wheel = { x: Math.round(Math.min(Math.max(box.x + box.width / 2, 1), CLIP_VIEWPORT.width - 1)), y: Math.round(CLIP_VIEWPORT.height / 2) };
|
|
3546
|
+
if (Math.abs(by) < 2) return shown;
|
|
3547
|
+
if (filming) {
|
|
3548
|
+
for (const point of cursorPath(resting, wheel, MOVE_FRAMES)) {
|
|
3549
|
+
await cursor(point);
|
|
3550
|
+
await shoot(FRAME_MS);
|
|
3551
|
+
}
|
|
3552
|
+
resting = wheel;
|
|
3553
|
+
}
|
|
3554
|
+
await pointer({ kind: "scroll", at: wheel, by });
|
|
3555
|
+
if (filming) {
|
|
3556
|
+
await cursor(resting);
|
|
3557
|
+
for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
|
|
3558
|
+
if (!await film()) break;
|
|
3559
|
+
}
|
|
3560
|
+
} else {
|
|
3561
|
+
await steady();
|
|
3562
|
+
}
|
|
3563
|
+
return resolveStep(manifestOf(await layout(cover), step, where), step);
|
|
3564
|
+
};
|
|
3476
3565
|
const reach = async (route) => {
|
|
3477
3566
|
let where = await location2();
|
|
3478
3567
|
for (let waited = 0; !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
@@ -3485,13 +3574,19 @@ async function recordClip(options, script, touched) {
|
|
|
3485
3574
|
}
|
|
3486
3575
|
return where;
|
|
3487
3576
|
};
|
|
3577
|
+
const settled = async () => {
|
|
3578
|
+
await page.quiet?.(SETTLE_MS);
|
|
3579
|
+
await steady();
|
|
3580
|
+
return location2();
|
|
3581
|
+
};
|
|
3582
|
+
const performedOn = (step, where) => onScreen(step.route, where) ? step.route : [...script.before ?? [], ...script.steps].map((each) => each.route).find((route) => onScreen(route, where)) ?? where.split("?")[0];
|
|
3488
3583
|
const lead = async (step) => {
|
|
3489
|
-
const where = await reach(step.route);
|
|
3490
|
-
if (!onScreen(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
|
|
3491
|
-
let resolution = await
|
|
3584
|
+
const where = step.follows ? await settled() : await reach(step.route);
|
|
3585
|
+
if (!step.follows && !onScreen(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
|
|
3586
|
+
let resolution = await find(step, where, false);
|
|
3492
3587
|
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3493
3588
|
await pause(FRAME_MS);
|
|
3494
|
-
resolution = await
|
|
3589
|
+
resolution = await find(step, where, false);
|
|
3495
3590
|
}
|
|
3496
3591
|
if (!resolution.ok) return resolution.because;
|
|
3497
3592
|
const target = centreOf(resolution.control.box);
|
|
@@ -3518,40 +3613,40 @@ async function recordClip(options, script, touched) {
|
|
|
3518
3613
|
const problem = await lead(step);
|
|
3519
3614
|
if (problem) return fail(`before the recording, step ${index + 1}: ${problem}`);
|
|
3520
3615
|
}
|
|
3521
|
-
const
|
|
3522
|
-
|
|
3616
|
+
const first = script.steps[0];
|
|
3617
|
+
const starting = !script.before?.length ? opened : first.follows ? await settled() : await reach(first.route);
|
|
3618
|
+
if (!first.follows && !onScreen(first.route, starting)) return fail(`once the steps before it were done, the browser is on ${starting}, not ${first.route}`);
|
|
3523
3619
|
const before = await judge(starting);
|
|
3524
3620
|
if (before) return refuse(before);
|
|
3525
|
-
let resting = START;
|
|
3526
3621
|
let poster;
|
|
3527
3622
|
const steps = [];
|
|
3528
|
-
const arrive = async (route) => {
|
|
3623
|
+
const arrive = async (route, follows = false) => {
|
|
3529
3624
|
let where = await location2();
|
|
3530
|
-
for (let waited = 0; !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3625
|
+
for (let waited = 0; !follows && !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3531
3626
|
await cursor(resting);
|
|
3532
3627
|
await film();
|
|
3533
3628
|
where = await location2();
|
|
3534
3629
|
}
|
|
3535
|
-
if (!onScreen(route, where)) return where;
|
|
3630
|
+
if (!follows && !onScreen(route, where)) return where;
|
|
3536
3631
|
await page.quiet?.(SETTLE_MS);
|
|
3537
3632
|
await cursor(resting);
|
|
3538
3633
|
for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
|
|
3539
3634
|
if (!await film()) break;
|
|
3540
3635
|
}
|
|
3541
|
-
return where;
|
|
3636
|
+
return follows ? location2() : where;
|
|
3542
3637
|
};
|
|
3543
3638
|
await cursor(resting);
|
|
3544
3639
|
checked.add(await shoot(OPENING_MS));
|
|
3545
3640
|
for (const [index, step] of script.steps.entries()) {
|
|
3546
|
-
const where = await arrive(step.route);
|
|
3547
|
-
if (!onScreen(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
|
|
3641
|
+
const where = await arrive(step.route, step.follows === true);
|
|
3642
|
+
if (!step.follows && !onScreen(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
|
|
3548
3643
|
const arrived = await judge(where);
|
|
3549
3644
|
if (arrived) return refuse(arrived);
|
|
3550
|
-
let resolution = await
|
|
3645
|
+
let resolution = await find(step, where, true);
|
|
3551
3646
|
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3552
3647
|
await cursor(resting);
|
|
3553
3648
|
await film();
|
|
3554
|
-
resolution = await
|
|
3649
|
+
resolution = await find(step, where, true);
|
|
3555
3650
|
}
|
|
3556
3651
|
if (!resolution.ok) return fail(`step ${index + 1}: ${resolution.because}`);
|
|
3557
3652
|
const target = centreOf(resolution.control.box);
|
|
@@ -3592,7 +3687,7 @@ async function recordClip(options, script, touched) {
|
|
|
3592
3687
|
const after = await judge(now);
|
|
3593
3688
|
if (after) return refuse(after);
|
|
3594
3689
|
steps.push({
|
|
3595
|
-
route: step
|
|
3690
|
+
route: performedOn(step, where),
|
|
3596
3691
|
role: resolution.control.role,
|
|
3597
3692
|
name: resolution.control.name,
|
|
3598
3693
|
action,
|