@saywright/studio 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.
Files changed (42) hide show
  1. package/dist/bridge.js +80 -32
  2. package/dist/cli.mjs +5193 -4848
  3. package/dist/client/assets/CommandPalette-BM1BSUxc.js +1 -0
  4. package/dist/client/assets/CommandPalette-BM1BSUxc.js.br +0 -0
  5. package/dist/client/assets/CommandPalette-BM1BSUxc.js.gz +0 -0
  6. package/dist/client/assets/{HelpDialog-CnCVSSNP.js → HelpDialog-Bc6yjiVi.js} +1 -1
  7. package/dist/client/assets/HelpDialog-Bc6yjiVi.js.br +0 -0
  8. package/dist/client/assets/HelpDialog-Bc6yjiVi.js.gz +0 -0
  9. package/dist/client/assets/{ReportView-BwBy4JIG.js → ReportView-DKcggbSt.js} +1 -1
  10. package/dist/client/assets/{ReportView-BwBy4JIG.js.br → ReportView-DKcggbSt.js.br} +0 -0
  11. package/dist/client/assets/ReportView-DKcggbSt.js.gz +0 -0
  12. package/dist/client/assets/{SettingsDialog-CS5w9CxJ.js → SettingsDialog-CIpvauuY.js} +1 -1
  13. package/dist/client/assets/SettingsDialog-CIpvauuY.js.br +0 -0
  14. package/dist/client/assets/SettingsDialog-CIpvauuY.js.gz +0 -0
  15. package/dist/client/assets/{arrow-left-BpsxETMA.js → arrow-left-5LUh_bW3.js} +1 -1
  16. package/dist/client/assets/brand-BcIB43Uo.js +57 -0
  17. package/dist/client/assets/brand-BcIB43Uo.js.br +0 -0
  18. package/dist/client/assets/brand-BcIB43Uo.js.gz +0 -0
  19. package/dist/client/assets/{footprints-Bc2dzReV.js → footprints-CfMi7MiN.js} +1 -1
  20. package/dist/client/assets/index-D1ooswLS.js +19 -0
  21. package/dist/client/assets/index-D1ooswLS.js.br +0 -0
  22. package/dist/client/assets/index-D1ooswLS.js.gz +0 -0
  23. package/dist/client/assets/{refresh-cw-DocKnJID.js → refresh-cw-BM1RSPTL.js} +1 -1
  24. package/dist/client/index.html +2 -2
  25. package/dist/client/index.html.br +0 -0
  26. package/dist/client/index.html.gz +0 -0
  27. package/dist/sample/package.json +1 -1
  28. package/package.json +1 -1
  29. package/dist/client/assets/CommandPalette-VEQNxJ4K.js +0 -1
  30. package/dist/client/assets/CommandPalette-VEQNxJ4K.js.br +0 -0
  31. package/dist/client/assets/CommandPalette-VEQNxJ4K.js.gz +0 -0
  32. package/dist/client/assets/HelpDialog-CnCVSSNP.js.br +0 -0
  33. package/dist/client/assets/HelpDialog-CnCVSSNP.js.gz +0 -0
  34. package/dist/client/assets/ReportView-BwBy4JIG.js.gz +0 -0
  35. package/dist/client/assets/SettingsDialog-CS5w9CxJ.js.br +0 -0
  36. package/dist/client/assets/SettingsDialog-CS5w9CxJ.js.gz +0 -0
  37. package/dist/client/assets/brand-BSL9G2NW.js +0 -57
  38. package/dist/client/assets/brand-BSL9G2NW.js.br +0 -0
  39. package/dist/client/assets/brand-BSL9G2NW.js.gz +0 -0
  40. package/dist/client/assets/index-C3xqQUPf.js +0 -19
  41. package/dist/client/assets/index-C3xqQUPf.js.br +0 -0
  42. package/dist/client/assets/index-C3xqQUPf.js.gz +0 -0
package/dist/bridge.js CHANGED
@@ -1021,6 +1021,7 @@ function contentName(el) {
1021
1021
  }
1022
1022
  function visibleText(el) {
1023
1023
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return collapse(isSecretField(el) ? el.placeholder : el.value || el.placeholder, 80);
1024
+ if (el instanceof HTMLSelectElement) return collapse(Array.from(el.selectedOptions, (o) => o.label || o.text).join(", "), 80);
1024
1025
  return collapse(el.innerText ?? el.textContent, 80);
1025
1026
  }
1026
1027
  function shortLabel(el) {
@@ -1196,6 +1197,48 @@ function plainElement(el) {
1196
1197
  return cur;
1197
1198
  }
1198
1199
  //#endregion
1200
+ //#region ../../packages/vite/src/bridge/own.ts
1201
+ /**
1202
+ * Saywright's own work on the app's page (reading the screen, building
1203
+ * locators, answering the studio): timed, so a slow moment it caused is never
1204
+ * reported as the app's.
1205
+ */
1206
+ var spans = [];
1207
+ /** Run some of our own work, remembering when it ran if it took long enough to matter. */
1208
+ function own(fn) {
1209
+ const start = performance.now();
1210
+ try {
1211
+ return fn();
1212
+ } finally {
1213
+ const end = performance.now();
1214
+ if (end - start >= 8) {
1215
+ spans.push([start, end]);
1216
+ if (spans.length > 64) spans.splice(0, spans.length - 64);
1217
+ }
1218
+ }
1219
+ }
1220
+ /** How much of [start, end] (performance.now() times) was our own work. */
1221
+ function ownTime(start, end) {
1222
+ let ms = 0;
1223
+ for (const [a, b] of spans) ms += Math.max(0, Math.min(b, end) - Math.max(a, start));
1224
+ return ms;
1225
+ }
1226
+ /** When a replay or Autopilot last acted on the page (a click, typing, a key): the times of the last few. */
1227
+ var acted = [];
1228
+ /** A step a replay or Autopilot did, now. */
1229
+ function scriptedInput() {
1230
+ acted.push(performance.now());
1231
+ if (acted.length > 16) acted.shift();
1232
+ }
1233
+ /**
1234
+ * Whether a replay or Autopilot acted in the half second before `at` (a performance.now() time): as the browser
1235
+ * leaves out what moves right after a person's input (a menu opening where it was clicked), what moves right after a
1236
+ * scripted step is that step's doing, not the page shifting under the reader.
1237
+ */
1238
+ function scriptedBefore(at, ms = 500) {
1239
+ return acted.some((t) => t <= at && at - t <= ms);
1240
+ }
1241
+ //#endregion
1199
1242
  //#region ../../packages/vite/src/bridge/actions.ts
1200
1243
  var CLICKABLE$2 = "button, a[href], [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [role=\"checkbox\"], [role=\"radio\"], input, select, textarea, label, summary, [tabindex]";
1201
1244
  /** The element a click at (x, y) should activate. */
@@ -1208,6 +1251,7 @@ var lastField = null;
1208
1251
  function clickAt(x, y) {
1209
1252
  const el = clickTargetAt(x, y);
1210
1253
  if (!el) return null;
1254
+ scriptedInput();
1211
1255
  if (el instanceof HTMLElement) {
1212
1256
  if (isField(el) && !(el instanceof HTMLSelectElement) && ![
1213
1257
  "checkbox",
@@ -1235,6 +1279,7 @@ function setNativeValue(el, value) {
1235
1279
  el.dispatchEvent(new Event("change", { bubbles: true }));
1236
1280
  }
1237
1281
  function typeText(text, submit) {
1282
+ scriptedInput();
1238
1283
  const active = document.activeElement;
1239
1284
  const target = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? active : lastField && lastField.isConnected ? lastField : null;
1240
1285
  if (!target) return null;
@@ -2508,6 +2553,7 @@ var mouse = (type, x, y) => new MouseEvent(type, {
2508
2553
  */
2509
2554
  function act(el, step) {
2510
2555
  if (unusable(el)) throw new Error("disabled");
2556
+ scriptedInput();
2511
2557
  const r = el.getBoundingClientRect();
2512
2558
  const x = r.left + r.width / 2;
2513
2559
  const y = r.top + r.height / 2;
@@ -2811,33 +2857,6 @@ var Recorder = class {
2811
2857
  }
2812
2858
  };
2813
2859
  //#endregion
2814
- //#region ../../packages/vite/src/bridge/own.ts
2815
- /**
2816
- * Saywright's own work on the app's page (reading the screen, building
2817
- * locators, answering the studio): timed, so a slow moment it caused is never
2818
- * reported as the app's.
2819
- */
2820
- var spans = [];
2821
- /** Run some of our own work, remembering when it ran if it took long enough to matter. */
2822
- function own(fn) {
2823
- const start = performance.now();
2824
- try {
2825
- return fn();
2826
- } finally {
2827
- const end = performance.now();
2828
- if (end - start >= 8) {
2829
- spans.push([start, end]);
2830
- if (spans.length > 64) spans.splice(0, spans.length - 64);
2831
- }
2832
- }
2833
- }
2834
- /** How much of [start, end] (performance.now() times) was our own work. */
2835
- function ownTime(start, end) {
2836
- let ms = 0;
2837
- for (const [a, b] of spans) ms += Math.max(0, Math.min(b, end) - Math.max(a, start));
2838
- return ms;
2839
- }
2840
- //#endregion
2841
2860
  //#region ../../packages/vite/src/bridge/proxy.ts
2842
2861
  /**
2843
2862
  * When the studio shows a site through its proxy, the site's own absolute
@@ -3422,6 +3441,8 @@ var RequestTally = class {
3422
3441
  */
3423
3442
  var ACTIVE_KEY = "saywright.session";
3424
3443
  var PENDING_KEY = "saywright.session.pending";
3444
+ /** What counts as another target near a small one, as the Experts count it (a box's own label is part of it). */
3445
+ var TARGETS = "a[href], button, input:not([type=hidden]), select, textarea, summary, [role=\"button\"], [role=\"link\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"tab\"], [role=\"menuitem\"], [tabindex]:not([tabindex=\"-1\"])";
3425
3446
  var INTERACTIVE = "button, a[href], input, select, textarea, summary, label, [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"option\"], [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])";
3426
3447
  /** Requests that are the dev server or the studio talking, not the app. */
3427
3448
  var PLUMBING_URL = /\/@vite\/|\/@fs\/|\/@id\/|\/@react-refresh|\/node_modules\/|__saywright|\.hot-update\.|\/_next\/webpack-hmr|\/__nextjs|sockjs-node|\/api\/sessions\/|\/api\/speech\//;
@@ -3531,6 +3552,20 @@ function shortUrl(url) {
3531
3552
  return url;
3532
3553
  }
3533
3554
  }
3555
+ /**
3556
+ * Whether something else to click is within 12 px of a small target's centre, where a 24 px circle on it would reach
3557
+ * (WCAG 2.5.8): looked for around it, so it costs the same on a page of any size.
3558
+ */
3559
+ function crowded(el, r) {
3560
+ const cx = r.left + r.width / 2;
3561
+ const cy = r.top + r.height / 2;
3562
+ for (const radius of [6, 11.5]) for (let k = 0; k < 16; k++) {
3563
+ const a = k / 16 * 2 * Math.PI;
3564
+ const other = document.elementFromPoint(cx + radius * Math.cos(a), cy + radius * Math.sin(a))?.closest(TARGETS);
3565
+ if (other && other !== el && !el.contains(other) && !other.contains(el)) return true;
3566
+ }
3567
+ return false;
3568
+ }
3534
3569
  var SessionRecorder = class {
3535
3570
  onStats;
3536
3571
  onMoment;
@@ -3769,6 +3804,7 @@ var SessionRecorder = class {
3769
3804
  this.fibers.set(t, fiber);
3770
3805
  }
3771
3806
  if (tag === "a" && getComputedStyle(el).display === "inline") t.inline = true;
3807
+ if ((r.width < 24 || r.height < 24) && r.width > 0 && r.height > 0 && !crowded(el, r)) t.spaced = true;
3772
3808
  return t;
3773
3809
  }
3774
3810
  interactive(el) {
@@ -4364,7 +4400,7 @@ var SessionRecorder = class {
4364
4400
  });
4365
4401
  observe("layout-shift", (es) => {
4366
4402
  for (const e of es) {
4367
- if (e.hadRecentInput || e.startTime < this.since) continue;
4403
+ if (e.hadRecentInput || scriptedBefore(e.startTime) || e.startTime < this.since) continue;
4368
4404
  this.cls += e.value;
4369
4405
  if (e.value < .02) continue;
4370
4406
  const node = e.sources?.find((s) => s.node instanceof Element)?.node;
@@ -4868,7 +4904,17 @@ var ScreenCapture = class {
4868
4904
  * the studio and by the recorder inside the app's own tab.
4869
4905
  */
4870
4906
  var PENDING_AUDIO = "saywright.session.audio";
4871
- /** How long starting waits for the browser's “share this tab” question to be answered. */
4907
+ /** After `ms`, once the page has drawn again (a page not drawing, in the background, doesn't hold it up). */
4908
+ function painted(ms) {
4909
+ return new Promise((resolve) => setTimeout(() => {
4910
+ if (typeof requestAnimationFrame !== "function") return resolve();
4911
+ const done = setTimeout(resolve, 100);
4912
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4913
+ clearTimeout(done);
4914
+ resolve();
4915
+ }));
4916
+ }, ms));
4917
+ }
4872
4918
  var SCREEN_WAIT_MS = 3e4;
4873
4919
  function toBase64(buf) {
4874
4920
  const bytes = new Uint8Array(buf);
@@ -5356,6 +5402,7 @@ var SessionClient = class {
5356
5402
  id,
5357
5403
  countdown: null
5358
5404
  });
5405
+ if (this.screen && opts.countdown) await painted(220);
5359
5406
  this.screen?.record(opts.video);
5360
5407
  try {
5361
5408
  await opts.onCreated?.(id, this.key);
@@ -6267,9 +6314,10 @@ function facts() {
6267
6314
  label: fieldLabel(f),
6268
6315
  type: f.type || f.tagName.toLowerCase(),
6269
6316
  autocomplete: f.getAttribute("autocomplete"),
6270
- required: f.required
6317
+ required: f.required,
6318
+ ...f.getAttribute("inputmode") ? { inputmode: f.getAttribute("inputmode") } : {}
6271
6319
  })),
6272
- buttons: Array.from(document.querySelectorAll("button, [role=\"button\"], input[type=submit]")).filter(shown).map((b) => collapse(b.innerText || b.getAttribute("aria-label") || b.value || "", 40)).filter(Boolean).slice(0, 30),
6320
+ buttons: Array.from(document.querySelectorAll("button, [role=\"button\"], input[type=submit]")).filter(shown).map((b) => collapse(accessibleName(b) || b.value || "", 40) || "(no name)").slice(0, 30),
6273
6321
  lcp,
6274
6322
  cls: watching ? Math.round(cls * 1e3) / 1e3 : null,
6275
6323
  readability: readability(text),
@@ -6311,7 +6359,7 @@ function linkTargets() {
6311
6359
  const out = [];
6312
6360
  for (const a of Array.from(document.querySelectorAll("a[href]")).filter(shown)) {
6313
6361
  const link = {
6314
- text: collapse(a.innerText || a.getAttribute("aria-label") || "", 40),
6362
+ text: collapse(accessibleName(a), 40) || "(no name)",
6315
6363
  href: redactUrl(a.getAttribute("href") ?? "").slice(0, 120)
6316
6364
  };
6317
6365
  if (!out.some((l) => l.text === link.text && l.href === link.href)) out.push(link);