waygraph 0.2.0 → 0.2.1
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/dist/cli.js +181 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -603,53 +603,190 @@ function extractVerifyHighlights(block, resultTag) {
|
|
|
603
603
|
* highlight then slowly input." One-time patch (idempotent - guarded so a
|
|
604
604
|
* multi-step chain doesn't re-wrap an already-wrapped fill).
|
|
605
605
|
*/
|
|
606
|
-
function
|
|
606
|
+
async function showRing(page, box, label) {
|
|
607
|
+
await page
|
|
608
|
+
.evaluate(
|
|
609
|
+
({ box, label }) => {
|
|
610
|
+
const ring = document.getElementById("wg-ring");
|
|
611
|
+
if (!ring) return;
|
|
612
|
+
ring.style.left = box.x - 6 + "px";
|
|
613
|
+
ring.style.top = box.y - 6 + "px";
|
|
614
|
+
ring.style.width = box.width + 12 + "px";
|
|
615
|
+
ring.style.height = box.height + 12 + "px";
|
|
616
|
+
ring.setAttribute("data-label", label);
|
|
617
|
+
ring.style.opacity = "1";
|
|
618
|
+
},
|
|
619
|
+
{ box, label },
|
|
620
|
+
)
|
|
621
|
+
.catch(() => {});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function hideRing(page) {
|
|
625
|
+
await page
|
|
626
|
+
.evaluate(() => {
|
|
627
|
+
const ring = document.getElementById("wg-ring");
|
|
628
|
+
if (ring) ring.style.opacity = "0";
|
|
629
|
+
})
|
|
630
|
+
.catch(() => {});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
634
|
+
// Playwright's own slowMo ALREADY pauses after every single low-level
|
|
635
|
+
// action it dispatches - and pressSequentially() fires one such action
|
|
636
|
+
// PER CHARACTER. Also giving pressSequentially its own fixed delay
|
|
637
|
+
// double-paces every keystroke (45ms + slowMo's own ~350ms, per
|
|
638
|
+
// character) - an ordinary 22-character email alone stretched past 8
|
|
639
|
+
// seconds. When slowMo is already doing the pacing, add none of our own;
|
|
640
|
+
// only fall back to a small typing delay when slowMo is off entirely.
|
|
641
|
+
const typeDelay = slowMo ? 0 : 30;
|
|
642
|
+
// Click is a single action, not per-character, so it doesn't compound
|
|
643
|
+
// the same way - but slowMo still adds its own pause around the actual
|
|
644
|
+
// click, so trim our own explicit "pop" pauses when it's already active
|
|
645
|
+
// rather than stacking a full 1.2s on top of that.
|
|
646
|
+
const clickPrePop = slowMo ? 300 : 700;
|
|
647
|
+
const clickPostPop = slowMo ? 200 : 500;
|
|
648
|
+
// Locator.fill()/click() only ever see a raw call, no context of where
|
|
649
|
+
// the value came from. Patching mem.get() to remember the most recently
|
|
650
|
+
// read key's name (Blocks read-then-immediately-fill, e.g. const { email
|
|
651
|
+
// } = mem.get(LoginInput.key); ...fill(email)) lets the fill patch below
|
|
652
|
+
// label the ring with the real key, not a generic "writing from mem".
|
|
653
|
+
// Patches this ONE mem instance only, not MemPage's shared prototype -
|
|
654
|
+
// there's exactly one mem per chain run.
|
|
655
|
+
const memTrack = { lastKeyName: null, at: 0 };
|
|
656
|
+
const originalGet = mem.get.bind(mem);
|
|
657
|
+
mem.get = (key) => {
|
|
658
|
+
memTrack.lastKeyName = key && key.name;
|
|
659
|
+
memTrack.at = Date.now();
|
|
660
|
+
return originalGet(key);
|
|
661
|
+
};
|
|
662
|
+
|
|
607
663
|
const proto = Object.getPrototypeOf(page.locator("html"));
|
|
608
|
-
|
|
609
|
-
proto.__wgFillPatched
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
await new Promise((res) => setTimeout(res, 200));
|
|
664
|
+
|
|
665
|
+
if (!proto.__wgFillPatched) {
|
|
666
|
+
proto.__wgFillPatched = true;
|
|
667
|
+
const originalFill = proto.fill;
|
|
668
|
+
proto.fill = async function (value, options) {
|
|
669
|
+
try {
|
|
670
|
+
await installOverlay(page);
|
|
671
|
+
const box = await this.boundingBox();
|
|
672
|
+
if (box) {
|
|
673
|
+
// Only trust the "last mem.get()" as THIS fill's source if it
|
|
674
|
+
// happened recently - a stale read from several actions ago is
|
|
675
|
+
// more likely unrelated than actually describing this field.
|
|
676
|
+
const label = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
|
|
677
|
+
? "from mem: " + memTrack.lastKeyName
|
|
678
|
+
: "writing from mem";
|
|
679
|
+
await showRing(page, box, label);
|
|
680
|
+
await new Promise((res) => setTimeout(res, 200));
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// best-effort - element not visible/attached yet is not this
|
|
684
|
+
// instrumentation's problem, the real fill below still runs
|
|
630
685
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
686
|
+
let result;
|
|
687
|
+
try {
|
|
688
|
+
// NOT this.clear() - Locator.clear() is itself implemented as
|
|
689
|
+
// fill(""), and since fill is patched on the shared prototype,
|
|
690
|
+
// that call would resolve back to THIS same patched function and
|
|
691
|
+
// recurse forever. Call the real original fill directly instead.
|
|
692
|
+
await originalFill.call(this, "", { timeout: options && options.timeout });
|
|
693
|
+
result = await this.pressSequentially(String(value), { delay: typeDelay, timeout: options && options.timeout });
|
|
694
|
+
} catch {
|
|
695
|
+
// pressSequentially unsupported on this element (e.g. a
|
|
696
|
+
// contenteditable div, or a locator .fill() genuinely needs to
|
|
697
|
+
// handle specially) - fall back to the real, unpatched fill.
|
|
698
|
+
result = await originalFill.call(this, value, options);
|
|
699
|
+
}
|
|
700
|
+
// Fade the ring back out once this field is actually done, instead
|
|
701
|
+
// of leaving it lit until the next Block's own before-panel clears
|
|
702
|
+
// it - it was sticking around through the whole rest of the step.
|
|
703
|
+
await hideRing(page);
|
|
704
|
+
return result;
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!proto.__wgClickPatched) {
|
|
709
|
+
proto.__wgClickPatched = true;
|
|
710
|
+
const originalClick = proto.click;
|
|
711
|
+
proto.click = async function (options) {
|
|
712
|
+
try {
|
|
713
|
+
await installOverlay(page);
|
|
714
|
+
const box = await this.boundingBox();
|
|
715
|
+
if (box) {
|
|
716
|
+
let label = "click";
|
|
717
|
+
try {
|
|
718
|
+
const text = (await this.textContent())?.trim();
|
|
719
|
+
if (text && text.length > 0 && text.length <= 30) label = text;
|
|
720
|
+
} catch {
|
|
721
|
+
// element has no simple text (an icon button, say) - generic label is fine
|
|
722
|
+
}
|
|
723
|
+
await showRing(page, box, label);
|
|
724
|
+
// "pop for a few seconds" - Dan's own phrase, matching the
|
|
725
|
+
// zsign demo-engine's ring-before-click pattern in
|
|
726
|
+
// services/help-center-clip-engine's video-pipeline.
|
|
727
|
+
await new Promise((res) => setTimeout(res, clickPrePop));
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
// best-effort - the real click below still runs either way
|
|
731
|
+
}
|
|
732
|
+
const result = await originalClick.call(this, options);
|
|
733
|
+
await new Promise((res) => setTimeout(res, clickPostPop));
|
|
734
|
+
await hideRing(page);
|
|
735
|
+
return result;
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Caps any Locator.waitFor()/page.waitForTimeout() timeout during step
|
|
740
|
+
// mode - a Block polling for something that will NEVER happen (e.g. "is
|
|
741
|
+
// there an unverified-account banner" on an account that IS verified)
|
|
742
|
+
// has no choice but to wait out its own hardcoded timeout in full before
|
|
743
|
+
// concluding "no". Only the WAIT gets capped, never the real outcome -
|
|
744
|
+
// if the thing genuinely appears at 800ms into a 5000ms wait, waitFor
|
|
745
|
+
// still resolves at 800ms same as always; this only shortens the "it's
|
|
746
|
+
// just never going to happen" case. Plain setTimeout()-based sleep()
|
|
747
|
+
// helpers some Blocks use for their own pacing are NOT Playwright calls
|
|
748
|
+
// at all and can't be touched this way - a real, disclosed limit, not
|
|
749
|
+
// silently ignored.
|
|
750
|
+
// Conservative on purpose: this exact codebase has documented real
|
|
751
|
+
// flakiness around slow-but-legitimate hydration waits (see
|
|
752
|
+
// login.block.ts's own waitForActionable comment). A too-aggressive cap
|
|
753
|
+
// would trade "one annoying 5s dead wait" for "logins that sometimes
|
|
754
|
+
// fail outright" - worse. 3000ms still meaningfully shortens the
|
|
755
|
+
// "waiting for something that will never happen" case without giving a
|
|
756
|
+
// real, slow-but-genuine wait much less room than before.
|
|
757
|
+
const WAIT_CAP_MS = 3000;
|
|
758
|
+
if (!proto.__wgWaitForPatched) {
|
|
759
|
+
proto.__wgWaitForPatched = true;
|
|
760
|
+
const originalWaitFor = proto.waitFor;
|
|
761
|
+
proto.waitFor = function (options) {
|
|
762
|
+
const capped = { ...(options || {}), timeout: Math.min((options && options.timeout) || 30000, WAIT_CAP_MS) };
|
|
763
|
+
return originalWaitFor.call(this, capped);
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const pageProto = Object.getPrototypeOf(page);
|
|
767
|
+
if (!pageProto.__wgWaitForTimeoutPatched) {
|
|
768
|
+
pageProto.__wgWaitForTimeoutPatched = true;
|
|
769
|
+
const originalWaitForTimeout = pageProto.waitForTimeout;
|
|
770
|
+
pageProto.waitForTimeout = function (ms) {
|
|
771
|
+
return originalWaitForTimeout.call(this, Math.min(ms, WAIT_CAP_MS));
|
|
772
|
+
};
|
|
773
|
+
}
|
|
649
774
|
}
|
|
650
775
|
|
|
651
|
-
async function runStepMode(engine, start, end, context, page, mem, resolved) {
|
|
652
|
-
|
|
776
|
+
async function runStepMode(engine, start, end, context, page, mem, resolved, slowMo) {
|
|
777
|
+
instrumentInteractionHighlighting(page, mem, slowMo);
|
|
778
|
+
// Some real Blocks (e.g. zsign-all's login.block.ts) call
|
|
779
|
+
// page.setViewportSize({ width: 1280, height: 720 }) inside their own
|
|
780
|
+
// act() - a hardcoded override for THEIR OWN testing consistency, with no
|
|
781
|
+
// idea an interactive human session is watching. Restoring the size
|
|
782
|
+
// AFTER each step (the first attempt at this) was still just repairing
|
|
783
|
+
// damage after the fact, on a delay, per-step - not the real fix. The
|
|
784
|
+
// real fix is to stop the override from ever landing at all: no-op
|
|
785
|
+
// setViewportSize entirely for the duration of step mode. Blocks that
|
|
786
|
+
// call it don't need it to actually do anything here - their own
|
|
787
|
+
// selectors/layouts still work fine at whatever size the real window
|
|
788
|
+
// already is.
|
|
789
|
+
page.setViewportSize = async () => {};
|
|
653
790
|
let resolveNext = null;
|
|
654
791
|
await page.exposeFunction("__wgNext", (edits) => {
|
|
655
792
|
if (resolveNext) {
|
|
@@ -789,7 +926,7 @@ async function main() {
|
|
|
789
926
|
if (baseURL) {
|
|
790
927
|
await page.goto(baseURL).catch(() => {});
|
|
791
928
|
}
|
|
792
|
-
result = await runStepMode(engine, start, end, context, page, mem, resolved);
|
|
929
|
+
result = await runStepMode(engine, start, end, context, page, mem, resolved, slowMo);
|
|
793
930
|
} else {
|
|
794
931
|
const blocks = resolved.map((r) => r.block);
|
|
795
932
|
const chained = blocks.reduce((a, b) => connect(a, b));
|