waygraph 0.13.3 → 0.13.4

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 CHANGED
@@ -2942,9 +2942,73 @@ async function clickPulseAt(page, x, y, tone) {
2942
2942
  .catch(() => {});
2943
2943
  }
2944
2944
 
2945
+ /**
2946
+ * Fit + center the OS browser window on the device viewport so mobile/tablet
2947
+ * is not stuck top-left inside a maximized frame (Dan 0.13.4).
2948
+ * Desktop clear restores maximized.
2949
+ */
2950
+ async function fitWindowToDeviceViewport(page, viewport, opts) {
2951
+ const maximize = !!(opts && opts.maximize);
2952
+ let client;
2953
+ try {
2954
+ client = await page.context().newCDPSession(page);
2955
+ const { windowId } = await client.send("Browser.getWindowForTarget");
2956
+ if (maximize) {
2957
+ await client.send("Browser.setWindowBounds", {
2958
+ windowId,
2959
+ bounds: { windowState: "maximized" },
2960
+ });
2961
+ return;
2962
+ }
2963
+ if (!viewport || !viewport.width || !viewport.height) return;
2964
+ const metrics = await page
2965
+ .evaluate(() => {
2966
+ const chromeW = Math.max(0, (window.outerWidth || 0) - (window.innerWidth || 0));
2967
+ const chromeH = Math.max(0, (window.outerHeight || 0) - (window.innerHeight || 0));
2968
+ return {
2969
+ chromeW: Number.isFinite(chromeW) ? chromeW : 0,
2970
+ // First paint after maximize often reports 0 chrome - use a floor.
2971
+ chromeH: chromeH > 20 ? chromeH : 88,
2972
+ screenW: window.screen.availWidth || 1920,
2973
+ screenH: window.screen.availHeight || 1080,
2974
+ };
2975
+ })
2976
+ .catch(() => ({ chromeW: 0, chromeH: 88, screenW: 1920, screenH: 1080 }));
2977
+ // Leave maximized before setting pixel bounds (Chromium ignores size while max).
2978
+ await client.send("Browser.setWindowBounds", {
2979
+ windowId,
2980
+ bounds: { windowState: "normal" },
2981
+ });
2982
+ const winW = Math.min(
2983
+ metrics.screenW,
2984
+ Math.max(320, Math.floor(viewport.width + metrics.chromeW)),
2985
+ );
2986
+ const winH = Math.min(
2987
+ metrics.screenH,
2988
+ Math.max(320, Math.floor(viewport.height + metrics.chromeH)),
2989
+ );
2990
+ const left = Math.max(0, Math.floor((metrics.screenW - winW) / 2));
2991
+ const top = Math.max(0, Math.floor((metrics.screenH - winH) / 2));
2992
+ await client.send("Browser.setWindowBounds", {
2993
+ windowId,
2994
+ bounds: {
2995
+ windowState: "normal",
2996
+ left,
2997
+ top,
2998
+ width: winW,
2999
+ height: winH,
3000
+ },
3001
+ });
3002
+ } catch {
3003
+ /* headless / no window - ignore */
3004
+ } finally {
3005
+ if (client) await client.detach().catch(() => {});
3006
+ }
3007
+ }
3008
+
2945
3009
  /**
2946
3010
  * Apply authored device fixture to the live page (0.13+).
2947
- * Seamless viewport lerp + toast on set/clear (quiet chip recreate on keep).
3011
+ * Seamless viewport lerp + centered window + toast on set/clear.
2948
3012
  */
2949
3013
  async function applyDeviceToPage(page, device, sync) {
2950
3014
  const mode = sync || (device ? "set" : "keep");
@@ -2973,6 +3037,8 @@ async function applyDeviceToPage(page, device, sync) {
2973
3037
  from = null;
2974
3038
  }
2975
3039
  const announce = mode === "set" || mode === "clear";
3040
+ const toDesktop =
3041
+ mode === "clear" || (d.preset === "desktop" && !d.touchMode);
2976
3042
  const shouldAnimate =
2977
3043
  announce &&
2978
3044
  from &&
@@ -2980,6 +3046,14 @@ async function applyDeviceToPage(page, device, sync) {
2980
3046
  from.height > 0 &&
2981
3047
  (from.width !== target.width || from.height !== target.height);
2982
3048
 
3049
+ // Unmaximize + center early so the lerp does not sit in the corner of a
3050
+ // maximized frame. Desktop clear maximizes at the end instead.
3051
+ if (!toDesktop) {
3052
+ await fitWindowToDeviceViewport(page, from && from.width ? from : target, {
3053
+ maximize: false,
3054
+ });
3055
+ }
3056
+
2983
3057
  try {
2984
3058
  if (shouldAnimate) {
2985
3059
  const steps = 14;
@@ -2988,10 +3062,14 @@ async function applyDeviceToPage(page, device, sync) {
2988
3062
  for (let i = 1; i <= steps; i++) {
2989
3063
  const t = i / steps;
2990
3064
  const e = t * t * (3 - 2 * t); // smoothstep
2991
- await realSet({
3065
+ const mid = {
2992
3066
  width: Math.round(from.width + (target.width - from.width) * e),
2993
3067
  height: Math.round(from.height + (target.height - from.height) * e),
2994
- });
3068
+ };
3069
+ await realSet(mid);
3070
+ if (!toDesktop && (i === 1 || i === steps || i % 3 === 0)) {
3071
+ await fitWindowToDeviceViewport(page, mid, { maximize: false });
3072
+ }
2995
3073
  if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
2996
3074
  }
2997
3075
  } else {
@@ -3001,6 +3079,12 @@ async function applyDeviceToPage(page, device, sync) {
3001
3079
  /* maximized / null viewport hosts may reject - toast still updates */
3002
3080
  }
3003
3081
 
3082
+ if (toDesktop) {
3083
+ await fitWindowToDeviceViewport(page, target, { maximize: true });
3084
+ } else {
3085
+ await fitWindowToDeviceViewport(page, target, { maximize: false });
3086
+ }
3087
+
3004
3088
  // Fire toast near the end of the lerp so it reads as one seamless beat.
3005
3089
  await page
3006
3090
  .evaluate(
@@ -39,6 +39,8 @@ npx playwright install chromium
39
39
  | Coverage JSON + min gate (10%) | `npm run traverse:coverage` |
40
40
  | Shop demo stepper | `npm run demo:step` |
41
41
  | Shop demo autoplay | `npm run demo:autoplay` |
42
+ | **Device showcase** (mobile/tablet + portrait/landscape) | `npm run demo:device` |
43
+ | Device showcase + `.webm` | `npm run demo:device:video` |
42
44
  | **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
43
45
  | Cart bulk stepper | `npm run demo:bulk:step` |
44
46
  | List flows | `npm run flows` |
@@ -25,6 +25,8 @@
25
25
  "prove:add-to-cart-menu": "node scripts/prove-add-to-cart-menu.mjs",
26
26
  "prove": "npm run prove:login-branch && npm run prove:graph && npm run prove:add-to-cart-menu && npm test",
27
27
  "record": "waygraph run --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --video --no-step",
28
+ "demo:device": "waygraph demo --blocks deviceShowcaseFlow --auto-next --title \"Device showcase\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
29
+ "demo:device:video": "waygraph demo --blocks deviceShowcaseFlow --auto-next --video ./.tmp/device-showcase --title \"Device showcase\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
28
30
  "demo:bulk": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
29
31
  "demo:bulk:auto-next": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next --title \"Cart bulk demo\"",
30
32
  "demo:bulk:video": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-play-video --title \"Cart bulk demo\"",
@@ -17,7 +17,9 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
17
17
  click: ".shopping_cart_link",
18
18
  verify: [Trait.url({ pathname: "/cart.html" })],
19
19
  stubBefore: (ctx: StubCtx) => {
20
- ctx.title("Open cart");
20
+ ctx.title("Open cart · landscape");
21
+ // Device showcase: rotate tablet to landscape before the cart tap.
22
+ ctx.landscape();
21
23
  ctx.todoId("saucedemo-cart");
22
24
  ctx.todos([
23
25
  { id: "cart-find", text: "Find the product" },
@@ -38,11 +40,14 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
38
40
  zoomOut: false,
39
41
  followMouse: false,
40
42
  weight: "bold",
43
+ gesture: "tap",
41
44
  },
42
45
  });
43
46
  },
44
47
  stubAfter: (ctx: StubCtx) => {
45
- ctx.banner("Your cart");
48
+ ctx.banner("Your cart · desktop");
49
+ // Showcase finale: back to desktop.
50
+ ctx.clearDevice();
46
51
  ctx.zoomOut(false);
47
52
  ctx.ring("items", {
48
53
  selector: InventorySel.cartItem,
@@ -36,9 +36,10 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
36
36
  ],
37
37
  // Demo: zoom product -> add (stay zoomed), then badge after act.
38
38
  stubBefore: (ctx: StubCtx) => {
39
- ctx.title("Pick a product");
40
- // Back to desktop after mobile login (explicit clear - not omit/remain).
41
- ctx.clearDevice();
39
+ ctx.title("Pick a product · tablet");
40
+ // Device showcase: tablet portrait (toast + lerp from mobile landscape).
41
+ ctx.device("tablet");
42
+ ctx.portrait();
42
43
  ctx.todoId("saucedemo-cart");
43
44
  ctx.todos([
44
45
  { id: "cart-find", text: "Find the product" },
@@ -50,9 +50,10 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
50
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
- ctx.title("Signing in");
54
- // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
53
+ ctx.title("Signing in · mobile");
54
+ // 0.13.3: mobile portrait + touch; tap Login.
55
55
  ctx.device("mobile");
56
+ ctx.portrait();
56
57
  ctx.todoId("saucedemo-login");
57
58
  ctx.todos([
58
59
  { id: "login-user", text: "Enter username" },
@@ -109,7 +110,9 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
109
110
  });
110
111
  return;
111
112
  }
112
- ctx.banner("Inventory");
113
+ ctx.banner("Inventory · landscape");
114
+ // Rotate mobile to landscape on the product shelf (toast + lerp).
115
+ ctx.landscape();
113
116
  ctx.todoId("saucedemo-login");
114
117
  ctx.todos([
115
118
  { id: "login-user", text: "Enter username" },
@@ -0,0 +1,28 @@
1
+ import { Engine, start, end, withTitle } from "waygraph";
2
+ import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
+ import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
4
+ import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
5
+ import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
6
+
7
+ const engine = new Engine();
8
+
9
+ /**
10
+ * Short device/orientation showcase (0.13.3+):
11
+ * login mobile portrait -> land on inventory landscape ->
12
+ * add-to-cart tablet portrait -> open cart tablet landscape -> desktop
13
+ *
14
+ * Run:
15
+ * npm run demo:device
16
+ * npm run demo:device:video
17
+ */
18
+ export const deviceShowcaseFlow = withTitle(
19
+ engine.defineFlow([
20
+ start,
21
+ NavLoginBlock,
22
+ SubmitLoginForFlow,
23
+ AddToCartBlock,
24
+ NavCartBlock,
25
+ end,
26
+ ]),
27
+ "Device showcase",
28
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import { defineNavClickBlock, Trait } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { CartPage } from "../../../states/checkout.states.js";
3
4
  import { InventorySel } from "../inventory/methods/inventory-items.js";
4
5
 
@@ -15,8 +16,10 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
15
16
  checkpoint: "CartPage",
16
17
  click: ".shopping_cart_link",
17
18
  verify: [Trait.url({ pathname: "/cart.html" })],
18
- stubBefore: (ctx) => {
19
- ctx.title("Open cart");
19
+ stubBefore: (ctx: StubCtx) => {
20
+ ctx.title("Open cart · landscape");
21
+ // Device showcase: rotate tablet to landscape before the cart tap.
22
+ ctx.landscape();
20
23
  ctx.todoId("saucedemo-cart");
21
24
  ctx.todos([
22
25
  { id: "cart-find", text: "Find the product" },
@@ -37,11 +40,14 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
37
40
  zoomOut: false,
38
41
  followMouse: false,
39
42
  weight: "bold",
43
+ gesture: "tap",
40
44
  },
41
45
  });
42
46
  },
43
- stubAfter: (ctx) => {
44
- ctx.banner("Your cart");
47
+ stubAfter: (ctx: StubCtx) => {
48
+ ctx.banner("Your cart · desktop");
49
+ // Showcase finale: back to desktop.
50
+ ctx.clearDevice();
45
51
  ctx.zoomOut(false);
46
52
  ctx.ring("items", {
47
53
  selector: InventorySel.cartItem,
@@ -1,4 +1,5 @@
1
1
  import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { ItemInCart } from "../../../../states/checkout.states.js";
3
4
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
5
  import { collectAddableItems, InventorySel } from "./inventory-items.js";
@@ -34,10 +35,11 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
34
35
  },
35
36
  ],
36
37
  // Demo: zoom product -> add (stay zoomed), then badge after act.
37
- stubBefore: (ctx) => {
38
- ctx.title("Pick a product");
39
- // Back to desktop after mobile login (explicit clear - not omit/remain).
40
- ctx.clearDevice();
38
+ stubBefore: (ctx: StubCtx) => {
39
+ ctx.title("Pick a product · tablet");
40
+ // Device showcase: tablet portrait (toast + lerp from mobile landscape).
41
+ ctx.device("tablet");
42
+ ctx.portrait();
41
43
  ctx.todoId("saucedemo-cart");
42
44
  ctx.todos([
43
45
  { id: "cart-find", text: "Find the product" },
@@ -68,7 +70,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
68
70
  },
69
71
  });
70
72
  },
71
- stubAfter: (ctx) => {
73
+ stubAfter: (ctx: StubCtx) => {
72
74
  ctx.banner("In the cart");
73
75
  ctx.todoId("saucedemo-cart");
74
76
  ctx.todos([
@@ -50,9 +50,10 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
50
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
- ctx.title("Signing in");
54
- // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
53
+ ctx.title("Signing in · mobile");
54
+ // 0.13.3: mobile portrait + touch; tap Login.
55
55
  ctx.device("mobile");
56
+ ctx.portrait();
56
57
  ctx.todoId("saucedemo-login");
57
58
  ctx.todos([
58
59
  { id: "login-user", text: "Enter username" },
@@ -109,7 +110,9 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
109
110
  });
110
111
  return;
111
112
  }
112
- ctx.banner("Inventory");
113
+ ctx.banner("Inventory · landscape");
114
+ // Rotate mobile to landscape on the product shelf (toast + lerp).
115
+ ctx.landscape();
113
116
  ctx.todoId("saucedemo-login");
114
117
  ctx.todos([
115
118
  { id: "login-user", text: "Enter username" },
@@ -0,0 +1,28 @@
1
+ import { Engine, start, end, withTitle } from "waygraph";
2
+ import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
+ import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
4
+ import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
5
+ import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
6
+
7
+ const engine = new Engine();
8
+
9
+ /**
10
+ * Short device/orientation showcase (0.13.3+):
11
+ * login mobile portrait -> land on inventory landscape ->
12
+ * add-to-cart tablet portrait -> open cart tablet landscape -> desktop
13
+ *
14
+ * Run:
15
+ * npm run demo:device
16
+ * npm run demo:device:video
17
+ */
18
+ export const deviceShowcaseFlow = withTitle(
19
+ engine.defineFlow([
20
+ start,
21
+ NavLoginBlock,
22
+ SubmitLoginForFlow,
23
+ AddToCartBlock,
24
+ NavCartBlock,
25
+ end,
26
+ ]),
27
+ "Device showcase",
28
+ );