waygraph 0.15.51 → 0.15.52

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.
Files changed (130) hide show
  1. package/dist/analysis/graph.d.ts +1 -1
  2. package/dist/analysis/graph.js +10 -15
  3. package/dist/analysis/practices-check.d.ts +0 -1
  4. package/dist/analysis/practices-check.js +0 -13
  5. package/dist/auto-session/session.d.ts +1 -1
  6. package/dist/auto-session/session.js +13 -8
  7. package/dist/blocks-select.d.ts +7 -4
  8. package/dist/blocks-select.js +0 -14
  9. package/dist/browser.d.ts +27 -0
  10. package/dist/browser.js +62 -0
  11. package/dist/cli/run-chain.js +4 -0
  12. package/dist/cli/usage.js +14 -1
  13. package/dist/cli.js +10 -1
  14. package/dist/commands/agent-dive-scaffold.d.ts +5 -0
  15. package/dist/commands/agent-dive-scaffold.js +7 -0
  16. package/dist/commands/extensions.d.ts +1 -0
  17. package/dist/commands/extensions.js +22 -0
  18. package/dist/commands/init.js +42 -4
  19. package/dist/commands/test.js +16 -2
  20. package/dist/commands/try.js +2 -1
  21. package/dist/commands/upgrade.d.ts +6 -0
  22. package/dist/commands/upgrade.js +107 -0
  23. package/dist/config.d.ts +47 -3
  24. package/dist/config.js +45 -12
  25. package/dist/core/block.d.ts +12 -0
  26. package/dist/core/block.js +36 -0
  27. package/dist/core/blocks/nav.d.ts +11 -0
  28. package/dist/core/blocks/nav.js +8 -0
  29. package/dist/core/config.d.ts +5 -0
  30. package/dist/core/engine.d.ts +17 -16
  31. package/dist/core/engine.js +3 -3
  32. package/dist/core/flow.d.ts +17 -0
  33. package/dist/core/flow.js +44 -21
  34. package/dist/core/map-builder.d.ts +3 -3
  35. package/dist/core/map-builder.js +3 -3
  36. package/dist/env-file.d.ts +7 -7
  37. package/dist/env-file.js +29 -16
  38. package/dist/extensions.d.ts +21 -0
  39. package/dist/extensions.js +178 -0
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +3 -2
  42. package/dist/pilot-overlay/overlay-fixtures.d.ts +0 -7
  43. package/dist/pilot-overlay/overlay-fixtures.js +0 -45
  44. package/dist/playwright-config.d.ts +4 -0
  45. package/dist/playwright-config.js +15 -0
  46. package/dist/playwright.config.d.ts +2 -0
  47. package/dist/playwright.config.js +6 -0
  48. package/dist/runner/demo-log.js +1 -7
  49. package/dist/runner/inpage/core.d.ts +1 -1
  50. package/dist/runner/inpage/core.js +8 -7
  51. package/dist/runner/main.js +18 -10
  52. package/dist/runner/rings.js +0 -49
  53. package/dist/session/auto-explore-run.js +9 -5
  54. package/dist/session/auto-explore.js +6 -4
  55. package/dist/session/step-overlay.js +3 -1
  56. package/dist/traverse/run.js +3 -1
  57. package/dist/ui/components/ring.js +1 -0
  58. package/dist/ui/overlay.css +1 -0
  59. package/dist/ui/shadow.d.ts +1 -3
  60. package/dist/ui/shadow.js +0 -4
  61. package/dist/ui/tokens.d.ts +0 -11
  62. package/dist/ui/tokens.js +0 -5
  63. package/dist/video-dir.d.ts +8 -0
  64. package/dist/video-dir.js +60 -0
  65. package/dist/webserver.js +5 -1
  66. package/package.json +1 -1
  67. package/templates/scaffold/README.md +4 -2
  68. package/templates/scaffold/STRUCTURE.md +5 -2
  69. package/templates/scaffold/config/waygraph.config.ts +26 -0
  70. package/templates/scaffold/package.json +1 -1
  71. package/templates/scaffold/src/flows/README.md +5 -5
  72. package/templates/scaffold/src/flows/example.flow.ts +31 -17
  73. package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +35 -0
  74. package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +23 -0
  75. package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +33 -0
  76. package/templates/scaffold/src/flows/mail-verify.flow.ts +36 -75
  77. package/templates/scaffold/src/flows/shop.flow.ts +41 -65
  78. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +0 -9
  79. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +0 -5
  80. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +1 -1
  81. package/templates/scaffold/src/map/(app_base)/home/_methods/add-item.effect.block.ts +3 -19
  82. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +2 -25
  83. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +2 -19
  84. package/templates/scaffold/src/map/(app_base)/home/_methods/clear-cart.method.block.ts +1 -17
  85. package/templates/scaffold/src/map/(app_base)/home/_methods/remove-item.effect.block.ts +1 -10
  86. package/templates/scaffold/src/map/(app_base)/home/_nav.block.ts +1 -12
  87. package/templates/scaffold/src/map/(app_base)/home/_page.block.ts +0 -9
  88. package/templates/scaffold/src/map/(app_base)/home/_sel.ts +3 -1
  89. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +1 -23
  90. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-body.method.block.ts +21 -0
  91. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-header.method.block.ts +19 -0
  92. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-status.method.block.ts +20 -0
  93. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.ts +18 -0
  94. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-body.method.block.ts +21 -0
  95. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-headers.method.block.ts +21 -0
  96. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/send-request.method.block.ts +15 -0
  97. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-bearer-token.method.block.ts +20 -0
  98. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-headers.method.block.ts +23 -0
  99. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-json-body.method.block.ts +36 -0
  100. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-method.method.block.ts +17 -0
  101. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-query-params.method.block.ts +22 -0
  102. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-url.method.block.ts +18 -0
  103. package/templates/scaffold/src/map/(external)/hoppscotch/_nav.block.ts +11 -0
  104. package/templates/scaffold/src/map/(external)/hoppscotch/_page.block.ts +36 -0
  105. package/templates/scaffold/src/map/(external)/hoppscotch/_sel.ts +27 -0
  106. package/templates/scaffold/src/map/(external)/hoppscotch/settings/_nav.block.ts +10 -0
  107. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +2 -27
  108. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +2 -21
  109. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +1 -19
  110. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -11
  111. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +1 -9
  112. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +0 -11
  113. package/templates/scaffold/src/states/demo.mem-keys.ts +20 -19
  114. package/templates/scaffold/src/states/demo.mem-stubs.ts +4 -0
  115. package/templates/scaffold/src/states/demo.states.ts +5 -1
  116. package/templates/scaffold/tests/example.spec.ts +2 -3
  117. package/templates/scaffold/tests/hoppscotch.spec.ts +97 -0
  118. package/templates/scaffold/tests/mail-verify.spec.ts +2 -11
  119. package/templates/scaffold/tsconfig.json +1 -1
  120. package/templates/skills/waygraph-convention.skill.md +32 -3
  121. package/templates/skills/waygraph-pilot.skill.md +4 -5
  122. package/dist/browser-path.d.ts +0 -5
  123. package/dist/browser-path.js +0 -18
  124. package/dist/runner/overlay-present.d.ts +0 -8
  125. package/dist/runner/overlay-present.js +0 -57
  126. package/dist/ui/components/banner.d.ts +0 -2
  127. package/dist/ui/components/banner.js +0 -10
  128. package/dist/ui/components/cursor.d.ts +0 -2
  129. package/dist/ui/components/cursor.js +0 -19
  130. package/templates/scaffold/playwright.config.ts +0 -12
@@ -72,7 +72,14 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
72
72
  ring.style.width = width + "px";
73
73
  ring.style.height = height + "px";
74
74
  ring.style.opacity = "1";
75
- ringLabel.textContent = label || "";
75
+ if (labelId === "wg-ring-label") {
76
+ // The demo ring's caption is drawn from data-label (CSS ::after), not DOM text, so the page's own
77
+ // text selectors (getByText, text=...) never match waygraph's overlay twice.
78
+ ringLabel.dataset.label = label || "";
79
+ ringLabel.textContent = "";
80
+ } else {
81
+ ringLabel.textContent = label || "";
82
+ }
76
83
  ringLabel.style.opacity = "1";
77
84
  const lw = ringLabel.offsetWidth;
78
85
  const lh = ringLabel.offsetHeight;
@@ -104,12 +111,6 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
104
111
  ringLabel.style.top = labelTop + "px";
105
112
  return [ring, ringLabel];
106
113
  };
107
- window.__wgHideRingAt = (ringId, labelId) => {
108
- const ring = __wgById(ringId);
109
- const label = __wgById(labelId);
110
- if (ring) ring.style.opacity = "0";
111
- if (label) label.style.opacity = "0";
112
- };
113
114
  window.__wgEnsureRingEl("wg-ring", "wg-ring-label");
114
115
  if (!__wgById("wg-cursor")) {
115
116
  const cursor = document.createElement("div");
@@ -10,7 +10,8 @@ import { connect } from "../types.js";
10
10
  import { MemPage } from "../mem-page.js";
11
11
  import { Engine, start, end, chainFlow, isFastForwardBlock, branchRoutes, runBranchRegression } from "../core.js";
12
12
  import { withWebServer } from "../webserver.js";
13
- import { chromiumExecutablePath } from "../browser-path.js";
13
+ import { resolveVideoDir } from "../video-dir.js";
14
+ import { chromiumExecutablePath, extensionPathsFromEnv, launchContext } from "../browser.js";
14
15
 
15
16
  async function runMain() {
16
17
  const projectDir = process.argv[2];
@@ -252,7 +253,7 @@ async function runMain() {
252
253
  const videoEnv = process.env.WAYGRAPH_VIDEO;
253
254
  let videoDir = null;
254
255
  if (videoEnv) {
255
- videoDir = videoEnv === "1" ? join(projectDir, ".waygraph-videos") : videoEnv;
256
+ videoDir = videoEnv === "1" ? await resolveVideoDir(projectDir) : videoEnv;
256
257
  mkdirSync(videoDir, { recursive: true });
257
258
  }
258
259
  /** Fixed 16:9 capture for --video (demo QA and headless run). Step+video no longer maximizes. */
@@ -290,12 +291,6 @@ async function runMain() {
290
291
  const executablePath = chromiumExecutablePath();
291
292
  // Maximized for interactive step, and for fixed video viewport so the
292
293
  // locked record size can actually fit (otherwise .webm gets grey pad).
293
- const browser = await chromium.launch({
294
- headless: !headed,
295
- slowMo,
296
- args: step || fixedVideoViewport ? ["--start-maximized"] : [],
297
- ...(executablePath ? { executablePath } : {}),
298
- });
299
294
  const contextOpts = fixedVideoViewport
300
295
  ? {
301
296
  baseURL,
@@ -313,7 +308,17 @@ async function runMain() {
313
308
  ? { dir: videoDir, size: fixedVideoViewport }
314
309
  : { dir: videoDir };
315
310
  }
316
- const context = await browser.newContext(contextOpts);
311
+ const { context, close: closeBrowser } = await launchContext({
312
+ launcher: chromium,
313
+ launch: {
314
+ headless: !headed,
315
+ slowMo,
316
+ args: step || fixedVideoViewport ? ["--start-maximized"] : [],
317
+ ...(executablePath ? { executablePath } : {}),
318
+ },
319
+ context: contextOpts,
320
+ extensions: extensionPathsFromEnv(),
321
+ });
317
322
  let pageForVideo = null;
318
323
  try {
319
324
  if (step) {
@@ -369,7 +374,7 @@ async function runMain() {
369
374
  if (savedVideo) {
370
375
  console.log("waygraph: video saved to " + savedVideo);
371
376
  }
372
- await browser.close();
377
+ await closeBrowser();
373
378
  }
374
379
  } else {
375
380
  if (chainFlows && chainFlows.length > 0) {
@@ -421,6 +426,9 @@ async function runAllBranchesMode(chainFlows, mem, baseURL, headed, engine) {
421
426
  process.exitCode = 1;
422
427
  return;
423
428
  }
429
+ if (extensionPathsFromEnv().length > 0) {
430
+ console.warn("waygraph run --all-branches: extensions are not loaded (branches run in cloned contexts)");
431
+ }
424
432
  const { chromium } = await import("@playwright/test");
425
433
  const browser = await chromium.launch({ headless: !headed });
426
434
  const cloneSession = process.env.WAYGRAPH_SHARED_SESSION !== "1";
@@ -381,55 +381,6 @@ export async function cycleHighlightRings(page, highlights, gatesFast, opts) {
381
381
  }
382
382
 
383
383
 
384
- /**
385
- * Paint a NAMED ring instance (ui/css/ring.css's shared `.wg-ring-el`/`.wg-ring-el-label`) at a
386
- * known viewport box - the primitive both the demo/auto singleton (`showRing`, always "wg-ring")
387
- * and Pilot's several simultaneous fixture rings (their own ids) paint through, so there is one
388
- * ring-rendering implementation, not two. No collision/follow/focus - those are `#wg-ring`'s own
389
- * extras (see `showRing`/`inpage/core.js`'s `__wgPositionRing`), meaningless for an independent
390
- * fixture ring that isn't "the current action."
391
- */
392
- export async function showRingAt(page, ringId, labelId, box, label, tone, style) {
393
- const size = normalizeHighlightSize(style && style.size);
394
- const weight = normalizeHighlightWeight(style && style.weight);
395
- const color = style && style.color ? String(style.color) : "";
396
- await page
397
- .evaluate(
398
- ({ ringId, labelId, box, label, tone, size, weight, color }) => {
399
- if (window.__wgPaintRingAt) {
400
- window.__wgPaintRingAt(ringId, labelId, box, label, tone, { size, weight, color });
401
- }
402
- },
403
- { ringId, labelId, box, label, tone: tone || "planned", size, weight, color },
404
- )
405
- .catch(() => {});
406
- }
407
-
408
- /** Hide (not remove) a named ring instance painted via {@link showRingAt}. */
409
- export async function hideRingAt(page, ringId, labelId) {
410
- await page
411
- .evaluate(
412
- ({ ringId, labelId }) => {
413
- if (window.__wgHideRingAt) window.__wgHideRingAt(ringId, labelId);
414
- },
415
- { ringId, labelId },
416
- )
417
- .catch(() => {});
418
- }
419
-
420
- /** Remove a named ring instance entirely (Pilot's fixture rings are short-lived, not reused). */
421
- export async function removeRingAt(page, ringId, labelId) {
422
- await page
423
- .evaluate(
424
- ({ ringId, labelId }) => {
425
- __wgById(ringId)?.remove();
426
- __wgById(labelId)?.remove();
427
- },
428
- { ringId, labelId },
429
- )
430
- .catch(() => {});
431
- }
432
-
433
384
  export async function showRing(page, box, label, tone, style) {
434
385
  const size = normalizeHighlightSize(style && style.size);
435
386
  const weight = normalizeHighlightWeight(style && style.weight);
@@ -5,9 +5,9 @@ import { Engine, start, end, locate } from "../core.js";
5
5
  import { MemPage } from "../mem-page.js";
6
6
  import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
7
7
  import { installDemoChrome, instrumentInteractionHighlighting } from "./step-overlay.js";
8
- import { resolveBaseUrl } from "../config.js";
8
+ import { configuredExtensions, resolveBaseUrl } from "../config.js";
9
9
  import { withWebServer } from "../webserver.js";
10
- import { chromiumExecutablePath } from "../browser-path.js";
10
+ import { chromiumExecutablePath, launchContext } from "../browser.js";
11
11
  /** See `../config.js`'s own `resolveBaseUrl` - re-exported so existing importers of this module
12
12
  * don't need to change their import path. */
13
13
  export { resolveBaseUrl };
@@ -587,13 +587,17 @@ async function runAutoExploreCore(projectDir, options = {}) {
587
587
  launchOpts.slowMo = 250;
588
588
  if (executablePath)
589
589
  launchOpts.executablePath = executablePath;
590
- const browser = await chromium.launch(launchOpts);
591
590
  const contextOpts = cli
592
591
  ? { viewport: { width: 1280, height: 720 } }
593
592
  : { viewport: null };
594
593
  if (baseURL)
595
594
  contextOpts.baseURL = baseURL;
596
- const context = await browser.newContext(contextOpts);
595
+ const { close: closeBrowser, context } = await launchContext({
596
+ launcher: chromium,
597
+ launch: launchOpts,
598
+ context: contextOpts,
599
+ extensions: await configuredExtensions(projectDir),
600
+ });
597
601
  let page = await context.newPage();
598
602
  if (startUrl) {
599
603
  await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
@@ -692,6 +696,6 @@ async function runAutoExploreCore(projectDir, options = {}) {
692
696
  finally {
693
697
  cliRl?.close();
694
698
  await context.close();
695
- await browser.close();
699
+ await closeBrowser();
696
700
  }
697
701
  }
@@ -169,7 +169,7 @@ export function exploreChoices(graph, here) {
169
169
  const out = [];
170
170
  for (const edge of graph.edges) {
171
171
  if (here === null) {
172
- if (edge.kind !== "nav")
172
+ if (edge.kind !== "nav" || edge.from !== "*")
173
173
  continue;
174
174
  }
175
175
  else if (edge.from !== here && edge.from !== "*") {
@@ -283,8 +283,9 @@ export async function buildExploreMenu(page, graph, library, here) {
283
283
  continue;
284
284
  if (here === null) {
285
285
  // Bootstrap only: URL deep-links as "Start here". Click-nav needs a live
286
- // control so it cannot seed an unknown page.
287
- if (!isUrlNav(entry))
286
+ // control so it cannot seed an unknown page, and a Nav with `from` needs
287
+ // that state first (e.g. signed in) so it cannot start one either.
288
+ if (!isUrlNav(entry) || edge.from !== "*")
288
289
  continue;
289
290
  push(navigate, edge);
290
291
  continue;
@@ -295,7 +296,8 @@ export async function buildExploreMenu(page, graph, library, here) {
295
296
  // Graph still marks NavBlocks from:"*" for pathfinding/traverse; auto menu
296
297
  // only offers click-nav whose control is visible on this page (navRunnable).
297
298
  // Otherwise LoginForm lists every app route + mailpit as if legal moves.
298
- if (isUrlNav(entry))
299
+ // A url Nav that declares `from` is only an edge from those pages, so it IS a real move here.
300
+ if (isUrlNav(entry) && edge.from === "*")
299
301
  continue;
300
302
  if (isBackEdge(edge) && (edge.from === here || edge.from === "*")) {
301
303
  push(back, edge);
@@ -86,7 +86,9 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
86
86
  ring.style.width = box.width + pad * 2 + "px";
87
87
  ring.style.height = box.height + pad * 2 + "px";
88
88
  ring.style.opacity = "1";
89
- ringLabel.textContent = label;
89
+ // Drawn from data-label (CSS ::after), not DOM text, so the page's own text selectors never match it.
90
+ ringLabel.dataset.label = label;
91
+ ringLabel.textContent = "";
90
92
  ringLabel.style.left = box.x + "px";
91
93
  ringLabel.style.top = box.y + box.height + 8 + "px";
92
94
  ringLabel.style.opacity = "1";
@@ -5,7 +5,7 @@ import { defaultMemValueForKey } from "../session/auto-explore-run.js";
5
5
  import { resolveBaseUrl } from "../config.js";
6
6
  import { EdgeLeaseCoordinator } from "./lease.js";
7
7
  import { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, writeCoverageReport, } from "./coverage.js";
8
- import { chromiumExecutablePath } from "../browser-path.js";
8
+ import { chromiumExecutablePath } from "../browser.js";
9
9
  function seedMem(library, mem, dataJson) {
10
10
  const raw = dataJson ?? process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
11
11
  let envParsed = {};
@@ -253,6 +253,8 @@ export async function runTraverse(projectDir, options = {}) {
253
253
  const allEdgeKeys = collectGraphEdgeKeys(graph.edges);
254
254
  const leases = new EdgeLeaseCoordinator({ projectDir, persist: parallel > 1 });
255
255
  const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
256
+ if (process.env.WAYGRAPH_EXTENSIONS)
257
+ console.error("waygraph traverse: extensions are not loaded (workers use plain contexts)");
256
258
  const { chromium } = await import("@playwright/test");
257
259
  const executablePath = chromiumExecutablePath();
258
260
  const launchOpts = {
@@ -20,6 +20,7 @@ export function ringCss() {
20
20
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
21
21
  `max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:${p.labelBg};color:${p.labelFg};` +
22
22
  "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
23
+ "#wg-ring-label::after{content:attr(data-label);}" +
23
24
  toneRules("#wg-ring-label", (t) => `background:${t.labelBg};color:${t.labelFg};`) +
24
25
  // Gray = a real Playwright-driven instruction: mark it with a robot.
25
26
  `#wg-ring-label[data-tone=auto]::before{${ROBOT}}` +
@@ -86,6 +86,7 @@
86
86
  font: 600 12px/1.35 var(--wg-font); transition: opacity .3s ease;
87
87
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,.25);
88
88
  }
89
+ #wg-ring-label::after { content: attr(data-label); }
89
90
  .wg-ring-el-label[data-tone=planned] { --wg-tone-bg: var(--wg-tone-planned-bg); --wg-tone-fg: var(--wg-tone-planned-fg); }
90
91
  .wg-ring-el-label[data-tone=auto] { --wg-tone-bg: var(--wg-tone-auto-bg); --wg-tone-fg: var(--wg-tone-auto-fg); }
91
92
  .wg-ring-el-label[data-tone=info] { --wg-tone-bg: var(--wg-tone-info-bg); --wg-tone-fg: var(--wg-tone-info-fg); }
@@ -7,7 +7,7 @@
7
7
  * lookups check the shadow root first and fall back to the light DOM (the device stage shell and
8
8
  * host-page markers such as `[data-wg-zoomed]` intentionally stay in the light DOM).
9
9
  */
10
- import type { BrowserContext, Page } from "@playwright/test";
10
+ import type { Page } from "@playwright/test";
11
11
  /**
12
12
  * `overlaySheet` is the ONE real-CSS bundle (`ui/stylesheet.ts` -> `dist/ui/overlay.css`, built on
13
13
  * Open Props - see `src/ui/css/README.md`) every surface shares; passed in from Node since in-page
@@ -16,5 +16,3 @@ import type { BrowserContext, Page } from "@playwright/test";
16
16
  export declare function installShadowRoot(overlaySheet?: string): void;
17
17
  /** Idempotent; call before any overlay install on a page. Installs the shared stylesheet bundle. */
18
18
  export declare function ensureShadowRoot(page: Page): Promise<void>;
19
- /** Also define the helpers on every future document (pilot overlay persists across navigations). */
20
- export declare function installShadowRootEverywhere(context: BrowserContext): Promise<void>;
package/dist/ui/shadow.js CHANGED
@@ -44,7 +44,3 @@ export function installShadowRoot(overlaySheet) {
44
44
  export async function ensureShadowRoot(page) {
45
45
  await page.evaluate(installShadowRoot, getOverlaySheet()).catch(() => { });
46
46
  }
47
- /** Also define the helpers on every future document (pilot overlay persists across navigations). */
48
- export async function installShadowRootEverywhere(context) {
49
- await context.addInitScript(installShadowRoot, getOverlaySheet());
50
- }
@@ -18,14 +18,3 @@ export interface Tone {
18
18
  export declare const TONES: Record<ToneName, Tone>;
19
19
  /** Dark surface shared by panels, banner, docks, HUD chips. */
20
20
  export declare const SURFACE = "rgba(20,10,40,.94)";
21
- export declare const ACCENT = "#7C3AED";
22
- /** Secondary accent used on the dark surface (section titles, kinds). */
23
- export declare const ACCENT_SOFT = "#c9a6ff";
24
- /** Stacking order: one scale instead of magic numbers. */
25
- export declare const Z: {
26
- readonly top: 2147483647;
27
- readonly ring: 2147483646;
28
- readonly fx: 2147483645;
29
- readonly focus: 2147483644;
30
- readonly dock: 2147483000;
31
- };
package/dist/ui/tokens.js CHANGED
@@ -14,8 +14,3 @@ export const TONES = {
14
14
  };
15
15
  /** Dark surface shared by panels, banner, docks, HUD chips. */
16
16
  export const SURFACE = "rgba(20,10,40,.94)";
17
- export const ACCENT = "#7C3AED";
18
- /** Secondary accent used on the dark surface (section titles, kinds). */
19
- export const ACCENT_SOFT = "#c9a6ff";
20
- /** Stacking order: one scale instead of magic numbers. */
21
- export const Z = { top: 2147483647, ring: 2147483646, fx: 2147483645, focus: 2147483644, dock: 2147483000 };
@@ -0,0 +1,8 @@
1
+ /** The user's Downloads folder, or undefined when there is none (a server, a container). */
2
+ export declare function downloadsDir(): string | undefined;
3
+ /**
4
+ * Where `--video` recordings go: `WAYGRAPH_VIDEO_DIR`, else `video.dir` from the config (absolute, `~/…`, or relative to
5
+ * the project), else `<Downloads>/waygraph-videos/<project>`. With no usable Downloads folder, or on CI, it is
6
+ * `<project>/.waygraph-videos`. An explicit `--video <dir>` never reaches this.
7
+ */
8
+ export declare function resolveVideoDir(projectDir: string): Promise<string>;
@@ -0,0 +1,60 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, isAbsolute, join, resolve } from "node:path";
4
+ import { loadWaygraphConfig } from "./config.js";
5
+ /** The user's Downloads folder, or undefined when there is none (a server, a container). */
6
+ export function downloadsDir() {
7
+ const home = homedir();
8
+ let dir = process.env.XDG_DOWNLOAD_DIR;
9
+ if (!dir && process.platform === "linux") {
10
+ try {
11
+ const line = readFileSync(join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "user-dirs.dirs"), "utf-8")
12
+ .split("\n")
13
+ .find((l) => l.startsWith("XDG_DOWNLOAD_DIR="));
14
+ dir = line?.slice("XDG_DOWNLOAD_DIR=".length).replace(/^"|"$/g, "").replace("$HOME", home);
15
+ }
16
+ catch {
17
+ /* no user-dirs file: use the conventional name */
18
+ }
19
+ }
20
+ dir ??= join(home, "Downloads");
21
+ return existsSync(dir) ? dir : undefined;
22
+ }
23
+ const expandHome = (p) => (p === "~" || p.startsWith("~/") ? join(homedir(), p.slice(1)) : p);
24
+ /** True if a file can be created in `dir` (macOS asks permission for Downloads the first time; a denial surfaces here). */
25
+ function writable(dir) {
26
+ try {
27
+ mkdirSync(dir, { recursive: true });
28
+ const probe = join(dir, `.waygraph-probe-${process.pid}`);
29
+ writeFileSync(probe, "");
30
+ rmSync(probe);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * Where `--video` recordings go: `WAYGRAPH_VIDEO_DIR`, else `video.dir` from the config (absolute, `~/…`, or relative to
39
+ * the project), else `<Downloads>/waygraph-videos/<project>`. With no usable Downloads folder, or on CI, it is
40
+ * `<project>/.waygraph-videos`. An explicit `--video <dir>` never reaches this.
41
+ */
42
+ export async function resolveVideoDir(projectDir) {
43
+ const fallback = join(projectDir, ".waygraph-videos");
44
+ const configured = process.env.WAYGRAPH_VIDEO_DIR || (await loadWaygraphConfig(projectDir))?.video?.dir;
45
+ if (configured) {
46
+ const dir = isAbsolute(expandHome(configured)) ? expandHome(configured) : resolve(projectDir, configured);
47
+ if (writable(dir))
48
+ return dir;
49
+ console.error(`waygraph: cannot write videos to ${dir} - using ${fallback}`);
50
+ return fallback;
51
+ }
52
+ const downloads = process.env.CI ? undefined : downloadsDir();
53
+ if (!downloads)
54
+ return fallback;
55
+ const dir = join(downloads, "waygraph-videos", basename(resolve(projectDir)));
56
+ if (writable(dir))
57
+ return dir;
58
+ console.error(`waygraph: cannot write to ${downloads} (denied?) - saving videos in ${fallback}`);
59
+ return fallback;
60
+ }
package/dist/webserver.js CHANGED
@@ -1,9 +1,10 @@
1
- // Starts/stops a project's own playwright.config `webServer` around `waygraph run`/`demo`/`auto`,
1
+ // Starts/stops a project's `webServer` (waygraph.config, or a legacy playwright.config) around `waygraph run`/`demo`/`auto`,
2
2
  // the same block `waygraph test` already honors.
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { spawn } from "node:child_process";
7
+ import { loadWaygraphConfig } from "./config.js";
7
8
  const CONFIG_FILES = [
8
9
  "playwright.config.ts",
9
10
  "playwright.config.mts",
@@ -12,6 +13,9 @@ const CONFIG_FILES = [
12
13
  "playwright.config.cjs",
13
14
  ];
14
15
  async function loadWebServerConfigs(projectDir) {
16
+ const fromWaygraph = (await loadWaygraphConfig(projectDir))?.webServer;
17
+ if (fromWaygraph)
18
+ return Array.isArray(fromWaygraph) ? fromWaygraph : [fromWaygraph];
15
19
  for (const name of CONFIG_FILES) {
16
20
  const p = join(projectDir, name);
17
21
  if (!existsSync(p))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.51",
3
+ "version": "0.15.52",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -25,8 +25,10 @@ one Block per action, flows in `src/flows/`.
25
25
  ## Point it at your real app
26
26
 
27
27
  Change each `_nav.block.ts`'s `url` from the local `file://` path to a real one, and set a real
28
- `baseUrl` if your Nav Blocks use relative paths (`waygraph.config.ts`, a `.env`, or
29
- `package.json`'s `"waygraph"` field - see the main docs' "Project config" section). Keep each
28
+ `baseUrl` if your Nav Blocks use relative paths (`config/waygraph.config.ts`, a `.env`, or
29
+ `package.json`'s `"waygraph"` field - see the main docs' "Project config" section). That file also
30
+ holds the test settings (`testDir`, `webServer`, `playwright: { workers, timeout, ... }`), so the
31
+ project needs no `playwright.config.ts`. Keep each
30
32
  `src/map/` folder matching the URL it represents (`waygraph map` enforces this).
31
33
 
32
34
  Docs: https://deviate-dv8.github.io/waygraph/
@@ -7,6 +7,8 @@ Next.js route groups) and never appear in a URL or Checkpoint name.
7
7
  ```text
8
8
  .
9
9
  ├── package.json
10
+ ├── .agents/skills/ # waygraph skills for coding agents
11
+ ├── config/waygraph.config.ts # baseUrl, tests, extensions, webServer - no playwright.config
10
12
  ├── src/
11
13
  │ ├── map/
12
14
  │ │ ├── (app_base)/ # your app (opened via file:// here)
@@ -22,7 +24,8 @@ Next.js route groups) and never appear in a URL or Checkpoint name.
22
24
  │ │ │ │ ├── assert-hello.method.block.ts # + stubs + YAP slide
23
25
  │ │ │ │ └── assert-item-added.method.block.ts
24
26
  │ │ │ └── docs/ # a second page, same shape
25
- │ │ └── (external)/mailpit/ # a different origin (mail catcher)
27
+ │ │ ├── (external)/mailpit/ # a different origin (mail catcher)
28
+ │ │ └── (external)/hoppscotch/ # API client, REST client, needs the hoppscotch extension
26
29
  │ ├── states/ # Checkpoint types + mem keys
27
30
  │ └── flows/ # example / shop / mail-verify
28
31
  └── tests/example.spec.ts
@@ -39,7 +42,7 @@ page folder uses, so any agent or teammate knows where to look without asking.
39
42
  | Add an action on a page | one file in that page's `_methods/` |
40
43
  | Verify a state (button disabled, error shown) | `defineAssertBlock` + `Trait.disabled` / `Trait.enabled` |
41
44
  | Wire steps together | `src/flows/*.flow.ts` |
42
- | Give a flow fake data | `registerMemStub(key, fake)` + `withMemStub(flow)` (docs: docs/REFERENCE.md, "memStub") |
45
+ | Give a flow fake data | `registerMemStub(key, fake)` + `memStub: true` in the flow's options (docs: docs/REFERENCE.md, "memStub") |
43
46
  | Explore by hand | `npm run auto` (headed) or `npm run auto:cli` |
44
47
  | Check your work | `npm run check`, `npm run graph`, `waygraph map` |
45
48
 
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from "waygraph";
2
+
3
+ export default defineConfig(({ env }) => ({
4
+ // Where the app under test lives. BASE_URL comes from .env / .env.<mode>: waygraph test --env staging
5
+ baseUrl: env.BASE_URL ?? "__APP_BASE_URL__",
6
+
7
+ // ── waygraph: uncomment what you need ─────────────────────────────────────────────────────────
8
+ // extensions: ["hoppscotch"], // browser extensions to load (a waygraph feature, not Playwright's)
9
+ // video: { // recordings made with `waygraph run|demo --video`
10
+ // dir: "~/Downloads/waygraph-videos", // where they are saved (default: Downloads/waygraph-videos/<project>)
11
+ // viewport: "1920x1080", // recording size: demo defaults to 1920x1080, run to 1280x720
12
+ // },
13
+ // slowMo: 250, // ms to pause before each browser action, so a run is watchable
14
+ // headed: true, // show the browser window (demo and auto already do)
15
+ // demo: { autoNext: true, fast: false }, // advance demo steps without clicking Next; blitz fast-forward steps
16
+ // webServer: { command: "npm start", port: 3000 }, // started around test / run / demo / auto
17
+
18
+ // ── test runner (Playwright) ──────────────────────────────────────────────────────────────────
19
+ // testDir: "tests",
20
+ // playwright: { // any Playwright test option, merged over waygraph's defaults
21
+ // workers: 1,
22
+ // retries: 1,
23
+ // timeout: 60_000,
24
+ // use: { video: "on", trace: "on", screenshot: "only-on-failure", viewport: { width: 1280, height: 720 } },
25
+ // },
26
+ }));
@@ -21,7 +21,7 @@
21
21
  "run:shop": "waygraph run --blocks shopFlow"
22
22
  },
23
23
  "dependencies": {
24
- "waygraph": "^0.12.20"
24
+ "waygraph": "latest"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@playwright/test": "^1.55.0",
@@ -7,12 +7,12 @@ all point at an export here by name (`waygraph run --blocks shopFlow`).
7
7
  `.branch()`) over hand-composing raw Blocks - it checks each step's Block kind at runtime and
8
8
  the Checkpoint chain at compile time, so a wrong step is a build/test failure, not a
9
9
  mid-run surprise.
10
- - `withTitle`/`withHighlightFixtures`/`withSessionReset`/etc. are non-destructive wrappers -
11
- `someFlow.withTitle("...")` doesn't mutate `someFlow`, it returns a new one. Compose them
12
- around the finished `.end()` result, not inside the Map chain itself.
10
+ - Flow settings go in `.end({ title, memStub, sessionReset })`. No nested `withTitle(withMemStub(...))`.
11
+ - Demo highlights are ctx stubs chained on the Block where the flow uses it:
12
+ `AddItemBlock.stubAfter((ctx) => { ctx.ring("cart", { selector, label }); })`.
13
13
  - One flow, one scenario. A flow that needs a genuinely different starting session
14
- (re-entering the login page mid-chain) wraps that segment in `withSessionReset`, rather than
15
- hand-clearing cookies inside a Block.
14
+ (re-entering the login page mid-chain) sets `sessionReset: true`, rather than hand-clearing
15
+ cookies inside a Block.
16
16
 
17
17
  New flow → new file here, named for the scenario (`checkout.flow.ts`), not for the Blocks it
18
18
  happens to use.
@@ -1,4 +1,6 @@
1
- import { Engine, start, end, withTitle, withHighlightFixtures } from "waygraph";
1
+ import { Engine } from "waygraph";
2
+ import config from "../../config/waygraph.config.js";
3
+ import { DemoSel } from "../map/(app_base)/home/_sel.js";
2
4
  import { NavHomeBlock } from "../map/(app_base)/home/_nav.block.js";
3
5
  import { AssertHelloBlock } from "../map/(app_base)/home/_methods/assert-hello.method.block.js";
4
6
  import { NavDocsBlock } from "../map/(app_base)/docs/_nav.block.js";
@@ -6,24 +8,36 @@ import { NavDocsBlock } from "../map/(app_base)/docs/_nav.block.js";
6
8
  import "../map/(app_base)/home/_page.block.js";
7
9
  import "../map/(app_base)/docs/_page.block.js";
8
10
 
9
- const engine = new Engine();
11
+ const engine = new Engine(config);
10
12
 
11
13
  /** Smoke: nav + assert (no mem), then nav to the sibling docs/ page. Green offline. */
12
- export const exampleFlow = withHighlightFixtures(
13
- withTitle(
14
- engine.defineFlow([start, NavHomeBlock, AssertHelloBlock, NavDocsBlock, end]),
15
- "Scaffold: Hello Waygraph",
16
- ),
17
- {
18
- "assert-hello": {
19
- stubAfter: {
20
- title: {
14
+ export const exampleFlow = engine
15
+ .map()
16
+ .start()
17
+ .gotoPage(
18
+ NavHomeBlock.stubAfter((ctx) => {
19
+ ctx.ring("title", { selector: DemoSel.title, label: "Home heading", duration: true });
20
+ }).stubOnError((ctx) => {
21
+ ctx.ring("title", { selector: DemoSel.title, label: "Home heading at failure" });
22
+ }),
23
+ )
24
+ .assert(
25
+ AssertHelloBlock.stubBefore((ctx) => {
26
+ ctx.ring("title", { selector: DemoSel.title, label: "Heading about to check" });
27
+ })
28
+ .stubAfter((ctx) => {
29
+ ctx.ring("title", {
30
+ selector: DemoSel.title,
21
31
  label: "AC · Hello visible",
22
- detail: "Replace this fixture copy with your ticket lines.",
32
+ detail: "Replace this copy with your ticket lines.",
23
33
  tag: "AC",
24
34
  duration: true,
25
- },
26
- },
27
- },
28
- },
29
- );
35
+ });
36
+ ctx.ring("sub", { selector: DemoSel.sub, label: "Subcopy", duration: 1500 });
37
+ })
38
+ .stubOnError((ctx) => {
39
+ ctx.ring("title", { selector: DemoSel.title, label: "Heading at failure", tag: "FAIL" });
40
+ }),
41
+ )
42
+ .gotoPage(NavDocsBlock)
43
+ .end({ title: "Scaffold: Hello Waygraph" });
@@ -0,0 +1,35 @@
1
+ import { Engine } from "waygraph";
2
+ import config from "../../config/waygraph.config.js";
3
+ import { NavHoppscotchSettingsBlock } from "../map/(external)/hoppscotch/settings/_nav.block.js";
4
+ import { PickExtensionInterceptorBlock } from "../map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.js";
5
+ import { NavHoppscotchRestBlock } from "../map/(external)/hoppscotch/_nav.block.js";
6
+ import { SetQueryParamsBlock } from "../map/(external)/hoppscotch/_methods/set-query-params.method.block.js";
7
+ import { SetUrlBlock } from "../map/(external)/hoppscotch/_methods/set-url.method.block.js";
8
+ import { SetBearerTokenBlock } from "../map/(external)/hoppscotch/_methods/set-bearer-token.method.block.js";
9
+ import { SendRequestBlock } from "../map/(external)/hoppscotch/_methods/send-request.method.block.js";
10
+ import { AssertStatusBlock } from "../map/(external)/hoppscotch/_methods/assert-status.method.block.js";
11
+ import { ReadResponseHeadersBlock } from "../map/(external)/hoppscotch/_methods/read-response-headers.method.block.js";
12
+ import { AssertResponseHeaderBlock } from "../map/(external)/hoppscotch/_methods/assert-response-header.method.block.js";
13
+ import { ReadResponseBodyBlock } from "../map/(external)/hoppscotch/_methods/read-response-body.method.block.js";
14
+ import { AssertResponseBodyBlock } from "../map/(external)/hoppscotch/_methods/assert-response-body.method.block.js";
15
+ import "../map/(external)/hoppscotch/_page.block.js";
16
+
17
+ const engine = new Engine(config);
18
+
19
+ /** GET with query params and a bearer token, check the status, that the API received them, and a response header. Needs `extensions: ["hoppscotch"]`. */
20
+ export const hoppscotchAuthFlow = engine
21
+ .map()
22
+ .start()
23
+ .gotoExternal(NavHoppscotchSettingsBlock)
24
+ .method(PickExtensionInterceptorBlock)
25
+ .gotoExternal(NavHoppscotchRestBlock)
26
+ .method(SetUrlBlock)
27
+ .method(SetQueryParamsBlock)
28
+ .method(SetBearerTokenBlock)
29
+ .method(SendRequestBlock)
30
+ .assert(AssertStatusBlock)
31
+ .method(ReadResponseBodyBlock)
32
+ .assert(AssertResponseBodyBlock)
33
+ .method(ReadResponseHeadersBlock)
34
+ .assert(AssertResponseHeaderBlock)
35
+ .end({ title: "Scaffold: Hoppscotch GET with query params and bearer token" });