waygraph 0.12.41 → 0.12.43
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 -1
- package/dist/auto-explore-run.js +49 -0
- package/dist/cli.js +670 -821
- package/dist/engine.d.ts +8 -8
- package/dist/engine.js +8 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/overlay-beacon.d.ts +44 -0
- package/dist/overlay-beacon.js +85 -0
- package/dist/step-overlay.d.ts +15 -0
- package/dist/step-overlay.js +11 -0
- package/examples/saucedemo/package-lock.json +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +16 -0
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +11 -7
- package/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +16 -0
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +11 -7
package/dist/engine.d.ts
CHANGED
|
@@ -292,15 +292,15 @@ export declare function withBlockPace<In extends Checkpoint<string>, Out extends
|
|
|
292
292
|
* {@link withTitle}.
|
|
293
293
|
*
|
|
294
294
|
* By itself this changes nothing about how the Flow runs - it still throws
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
295
|
+
* Marks a Flow whose last step is an intentional product failure (demo overlay).
|
|
296
|
+
* Step mode shows stubOnError rings + an amber "expected outcome" panel when:
|
|
297
|
+
* - the last block **throws**, or
|
|
298
|
+
* - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
|
|
299
|
+
* after submit-login branched instead of throwing).
|
|
300
|
+
* Headless `run()` is unchanged - branching success returns the checkpoint.
|
|
301
301
|
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
302
|
-
* engine.defineFlow([start, NavLoginBlock,
|
|
303
|
-
* "locked_out_user
|
|
302
|
+
* engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
|
|
303
|
+
* "locked_out_user stays on LoginPage with the error banner - working as intended.",
|
|
304
304
|
* );
|
|
305
305
|
*/
|
|
306
306
|
export declare function withExpectedFailure<Out extends Checkpoint<string>>(flow: Flow<Out>, reason: string): Flow<Out>;
|
package/dist/engine.js
CHANGED
|
@@ -405,15 +405,15 @@ export function withBlockPace(block, pace) {
|
|
|
405
405
|
* {@link withTitle}.
|
|
406
406
|
*
|
|
407
407
|
* By itself this changes nothing about how the Flow runs - it still throws
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
408
|
+
* Marks a Flow whose last step is an intentional product failure (demo overlay).
|
|
409
|
+
* Step mode shows stubOnError rings + an amber "expected outcome" panel when:
|
|
410
|
+
* - the last block **throws**, or
|
|
411
|
+
* - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
|
|
412
|
+
* after submit-login branched instead of throwing).
|
|
413
|
+
* Headless `run()` is unchanged - branching success returns the checkpoint.
|
|
414
414
|
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
415
|
-
* engine.defineFlow([start, NavLoginBlock,
|
|
416
|
-
* "locked_out_user
|
|
415
|
+
* engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
|
|
416
|
+
* "locked_out_user stays on LoginPage with the error banner - working as intended.",
|
|
417
417
|
* );
|
|
418
418
|
*/
|
|
419
419
|
export function withExpectedFailure(flow, reason) {
|
package/dist/index.d.ts
CHANGED
|
@@ -16,3 +16,5 @@ export { runTraverse } from "./traverse-run.js";
|
|
|
16
16
|
export type { TraverseOptions, TraverseSessionMode, TraverseWorkerResult, } from "./traverse-run.js";
|
|
17
17
|
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
18
18
|
export type { TraverseCoverageReport } from "./traverse-coverage.js";
|
|
19
|
+
export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
|
|
20
|
+
export type { WgModalKind, WgBeaconRow } from "./overlay-beacon.js";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp,
|
|
|
8
8
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
9
9
|
export { runTraverse } from "./traverse-run.js";
|
|
10
10
|
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
11
|
+
export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
import type { Page } from "@playwright/test";
|
|
13
|
+
export declare const WG_UI_SEL = "[data-wg-ui=\"1\"]";
|
|
14
|
+
export declare const WG_READY_PANEL_SEL = "[data-wg-modal=\"panel\"][data-wg-ready=\"1\"]";
|
|
15
|
+
export declare const WG_READY_AUTO_SEL = "[data-wg-modal=\"auto-panel\"][data-wg-ready=\"1\"]";
|
|
16
|
+
export declare const WG_READY_BANNER_SEL = "[data-wg-modal=\"banner\"][data-wg-ready=\"1\"]";
|
|
17
|
+
export type WgModalKind = "panel" | "banner" | "auto-panel";
|
|
18
|
+
export type WgBeaconRow = {
|
|
19
|
+
id: string | null;
|
|
20
|
+
modal: string | null;
|
|
21
|
+
ready: boolean;
|
|
22
|
+
phase: string | null;
|
|
23
|
+
step: string | null;
|
|
24
|
+
block: string | null;
|
|
25
|
+
collapsed: boolean;
|
|
26
|
+
opacity: string;
|
|
27
|
+
textLen: number;
|
|
28
|
+
w: number;
|
|
29
|
+
h: number;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
33
|
+
export declare function readWgOverlayBeaconInPage(): WgBeaconRow[];
|
|
34
|
+
export declare function readWgOverlayBeacon(page: Page): Promise<WgBeaconRow[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
37
|
+
* Use after demo/auto paints its first overlay.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertWgOverlayReady(page: Page, opts?: {
|
|
40
|
+
modal?: WgModalKind;
|
|
41
|
+
minTextLen?: number;
|
|
42
|
+
/** When true, collapsed mini chrome is allowed (still needs visible box + text). */
|
|
43
|
+
allowCollapsed?: boolean;
|
|
44
|
+
}): Promise<WgBeaconRow>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
export const WG_UI_SEL = '[data-wg-ui="1"]';
|
|
13
|
+
export const WG_READY_PANEL_SEL = '[data-wg-modal="panel"][data-wg-ready="1"]';
|
|
14
|
+
export const WG_READY_AUTO_SEL = '[data-wg-modal="auto-panel"][data-wg-ready="1"]';
|
|
15
|
+
export const WG_READY_BANNER_SEL = '[data-wg-modal="banner"][data-wg-ready="1"]';
|
|
16
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
17
|
+
export function readWgOverlayBeaconInPage() {
|
|
18
|
+
const w = globalThis;
|
|
19
|
+
if (typeof w.__wgOverlayBeacon === "function")
|
|
20
|
+
return w.__wgOverlayBeacon();
|
|
21
|
+
return Array.from(document.querySelectorAll(WG_UI_SEL)).map((el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
const st = getComputedStyle(el);
|
|
24
|
+
return {
|
|
25
|
+
id: el.id || null,
|
|
26
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
27
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
28
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
29
|
+
step: el.getAttribute("data-wg-step"),
|
|
30
|
+
block: el.getAttribute("data-wg-block"),
|
|
31
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
32
|
+
opacity: st.opacity,
|
|
33
|
+
textLen: (el.textContent || "").trim().length,
|
|
34
|
+
w: Math.round(r.width),
|
|
35
|
+
h: Math.round(r.height),
|
|
36
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function readWgOverlayBeacon(page) {
|
|
41
|
+
return page.evaluate(() => {
|
|
42
|
+
const w = window;
|
|
43
|
+
if (typeof w.__wgOverlayBeacon === "function") {
|
|
44
|
+
return w.__wgOverlayBeacon();
|
|
45
|
+
}
|
|
46
|
+
return Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
|
|
47
|
+
const r = el.getBoundingClientRect();
|
|
48
|
+
const st = getComputedStyle(el);
|
|
49
|
+
return {
|
|
50
|
+
id: el.id || null,
|
|
51
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
52
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
53
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
54
|
+
step: el.getAttribute("data-wg-step"),
|
|
55
|
+
block: el.getAttribute("data-wg-block"),
|
|
56
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
57
|
+
opacity: st.opacity,
|
|
58
|
+
textLen: (el.textContent || "").trim().length,
|
|
59
|
+
w: Math.round(r.width),
|
|
60
|
+
h: Math.round(r.height),
|
|
61
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
68
|
+
* Use after demo/auto paints its first overlay.
|
|
69
|
+
*/
|
|
70
|
+
export async function assertWgOverlayReady(page, opts) {
|
|
71
|
+
const modal = opts?.modal ?? "panel";
|
|
72
|
+
const minText = opts?.minTextLen ?? 12;
|
|
73
|
+
const rows = await readWgOverlayBeacon(page);
|
|
74
|
+
const hit = rows.find((r) => r.modal === modal &&
|
|
75
|
+
r.ready &&
|
|
76
|
+
r.visible &&
|
|
77
|
+
r.textLen >= minText &&
|
|
78
|
+
(opts?.allowCollapsed || !r.collapsed || r.textLen >= minText));
|
|
79
|
+
if (!hit) {
|
|
80
|
+
throw new Error(`waygraph overlay NOT ready (modal=${modal}, minTextLen=${minText}). ` +
|
|
81
|
+
`beacons=${JSON.stringify(rows)}. ` +
|
|
82
|
+
`Do not claim stepper/modal works.`);
|
|
83
|
+
}
|
|
84
|
+
return hit;
|
|
85
|
+
}
|
package/dist/step-overlay.d.ts
CHANGED
|
@@ -18,5 +18,20 @@ declare global {
|
|
|
18
18
|
__wgHideRing?: () => void;
|
|
19
19
|
__wgPendingNavClickLabel?: string;
|
|
20
20
|
__wgWirePanelChrome?: (panel: HTMLElement, storageKey: string, chromeTitle?: string) => void;
|
|
21
|
+
__wgStampModal?: (el: Element, kind: string, meta?: Record<string, unknown>) => void;
|
|
22
|
+
__wgOverlayBeacon?: () => Array<{
|
|
23
|
+
id: string | null;
|
|
24
|
+
modal: string | null;
|
|
25
|
+
ready: boolean;
|
|
26
|
+
phase: string | null;
|
|
27
|
+
step: string | null;
|
|
28
|
+
block: string | null;
|
|
29
|
+
collapsed: boolean;
|
|
30
|
+
opacity: string;
|
|
31
|
+
textLen: number;
|
|
32
|
+
w: number;
|
|
33
|
+
h: number;
|
|
34
|
+
visible: boolean;
|
|
35
|
+
}>;
|
|
21
36
|
}
|
|
22
37
|
}
|
package/dist/step-overlay.js
CHANGED
|
@@ -146,6 +146,17 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
146
146
|
banner.innerHTML =
|
|
147
147
|
"<span class='wg-banner-tag'>waygraph auto</span><span>" + title + "</span>";
|
|
148
148
|
document.documentElement.appendChild(banner);
|
|
149
|
+
banner.setAttribute("data-wg-ui", "1");
|
|
150
|
+
banner.setAttribute("data-wg-modal", "banner");
|
|
151
|
+
banner.setAttribute("data-wg-ready", "1");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const existing = document.getElementById("wg-banner");
|
|
155
|
+
if (existing) {
|
|
156
|
+
existing.setAttribute("data-wg-ui", "1");
|
|
157
|
+
existing.setAttribute("data-wg-modal", "banner");
|
|
158
|
+
existing.setAttribute("data-wg-ready", "1");
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
if (showBanner && !document.title.startsWith("[waygraph] ")) {
|
|
151
162
|
document.title = "[waygraph] " + document.title;
|
|
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
78
78
|
});
|
|
79
79
|
},
|
|
80
80
|
stubAfter: (ctx) => {
|
|
81
|
+
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
82
|
+
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
83
|
+
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
84
|
+
ctx.title("Login blocked");
|
|
85
|
+
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
86
|
+
ctx.todoIndex(2);
|
|
87
|
+
ctx.ring("error", {
|
|
88
|
+
selector: '[data-test="error"]',
|
|
89
|
+
label: "Login error banner",
|
|
90
|
+
tone: "danger",
|
|
91
|
+
focus: true,
|
|
92
|
+
color: "#f87171",
|
|
93
|
+
duration: true,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
81
97
|
ctx.banner("Inventory");
|
|
82
98
|
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
83
99
|
ctx.todoIndex(2);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
|
|
1
|
+
import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
3
|
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
4
4
|
|
|
@@ -16,10 +16,14 @@ const engine = new Engine();
|
|
|
16
16
|
// genuinely fresh cookies/storage, or the previous user's session would
|
|
17
17
|
// still be live and this login attempt wouldn't mean anything.
|
|
18
18
|
//
|
|
19
|
-
// submit-login
|
|
20
|
-
// resolve -> LoginPage, verify confirms the error banner
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// submit-login branches: observe sees no /inventory.html navigation,
|
|
20
|
+
// resolve -> LoginPage, verify confirms the error banner. Demo still wraps
|
|
21
|
+
// withExpectedFailure so step 9/9 shows stubOnError rings + amber
|
|
22
|
+
// "expected outcome" panel (branching no longer throws).
|
|
23
|
+
export const viewerBlockedFlow = withExpectedFailure(
|
|
24
|
+
withTitle(
|
|
25
|
+
withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
|
|
26
|
+
"Viewer: Blocked Login Attempt",
|
|
27
|
+
),
|
|
28
|
+
"locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
|
|
25
29
|
);
|
package/package.json
CHANGED
|
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
78
78
|
});
|
|
79
79
|
},
|
|
80
80
|
stubAfter: (ctx) => {
|
|
81
|
+
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
82
|
+
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
83
|
+
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
84
|
+
ctx.title("Login blocked");
|
|
85
|
+
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
86
|
+
ctx.todoIndex(2);
|
|
87
|
+
ctx.ring("error", {
|
|
88
|
+
selector: '[data-test="error"]',
|
|
89
|
+
label: "Login error banner",
|
|
90
|
+
tone: "danger",
|
|
91
|
+
focus: true,
|
|
92
|
+
color: "#f87171",
|
|
93
|
+
duration: true,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
81
97
|
ctx.banner("Inventory");
|
|
82
98
|
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
83
99
|
ctx.todoIndex(2);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
|
|
1
|
+
import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
3
|
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
4
4
|
|
|
@@ -16,10 +16,14 @@ const engine = new Engine();
|
|
|
16
16
|
// genuinely fresh cookies/storage, or the previous user's session would
|
|
17
17
|
// still be live and this login attempt wouldn't mean anything.
|
|
18
18
|
//
|
|
19
|
-
// submit-login
|
|
20
|
-
// resolve -> LoginPage, verify confirms the error banner
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// submit-login branches: observe sees no /inventory.html navigation,
|
|
20
|
+
// resolve -> LoginPage, verify confirms the error banner. Demo still wraps
|
|
21
|
+
// withExpectedFailure so step 9/9 shows stubOnError rings + amber
|
|
22
|
+
// "expected outcome" panel (branching no longer throws).
|
|
23
|
+
export const viewerBlockedFlow = withExpectedFailure(
|
|
24
|
+
withTitle(
|
|
25
|
+
withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
|
|
26
|
+
"Viewer: Blocked Login Attempt",
|
|
27
|
+
),
|
|
28
|
+
"locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
|
|
25
29
|
);
|