@specific.dev/spectest 0.28.3 → 0.29.0

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 (2) hide show
  1. package/dist/browser.js +81 -0
  2. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -550,6 +550,87 @@ const RRWEB_BOOTSTRAP = `
550
550
  }, true);
551
551
  } catch (e) { log("init FAIL " + (e && e.message)); }
552
552
  })();
553
+ // ── Record form resets as synthetic Input events ───────────────────
554
+ // rrweb learns that a control's value changed through exactly two
555
+ // channels: the \`input\`/\`change\` events, and setters it hooks on the
556
+ // HTMLInput/TextArea/SelectElement prototypes. A **form reset** uses
557
+ // neither — the browser clears the controls internally — so the
558
+ // recording keeps the last typed value forever while the real page
559
+ // shows an empty field. Measured, not assumed: \`el.value = x\` IS
560
+ // captured (even through React's own per-node \`value\` accessor, since
561
+ // that delegates to the prototype descriptor we hooked first), and
562
+ // \`form.reset()\` is NOT. The shape is everywhere in chat/prompt UIs —
563
+ // shadcn/ai-elements' PromptInput resets the form on submit, as does
564
+ // React 19's automatic \`<form action>\` reset — where it reads as
565
+ // "clicking send didn't clear the box".
566
+ //
567
+ // The \`reset\` event is the hook: it fires (cancelable) BEFORE the
568
+ // controls are reset, for both \`form.reset()\` and a user-clicked
569
+ // \`type="reset"\` button. Snapshot the values there, diff on the next
570
+ // task, and push an Input event per control that actually moved —
571
+ // byte-identical in shape to what rrweb emits for a real input, so
572
+ // the replayer applies it the same way. A prevented or no-op reset
573
+ // changes nothing and therefore emits nothing. We never touch the
574
+ // page's DOM to provoke a recording (\`el.value = el.value\` would
575
+ // work, but it sets the control's dirty-value flag and would change
576
+ // how the app's own later attribute writes render).
577
+ (function () {
578
+ if (window.__spectestFormReset) return;
579
+ window.__spectestFormReset = true;
580
+ var INPUT_TAGS = ["INPUT", "TEXTAREA", "SELECT"];
581
+ function snap(form) {
582
+ var out = [], els;
583
+ try { els = form.elements; } catch (e) { return out; }
584
+ if (!els) return out;
585
+ for (var i = 0; i < els.length; i++) {
586
+ var el = els[i];
587
+ if (!el || !el.tagName) continue;
588
+ if (INPUT_TAGS.indexOf(String(el.tagName).toUpperCase()) < 0) continue;
589
+ out.push({ el: el, value: el.value, checked: !!el.checked });
590
+ }
591
+ return out;
592
+ }
593
+ function emitChanged(before) {
594
+ var mirror = rec && rec.mirror;
595
+ if (!mirror || typeof mirror.getId !== "function") return;
596
+ for (var i = 0; i < before.length; i++) {
597
+ var el = before[i].el;
598
+ if (el.value === before[i].value && !!el.checked === before[i].checked) continue;
599
+ if (el.classList && el.classList.contains("rr-ignore")) continue;
600
+ var id = mirror.getId(el);
601
+ if (!(id > 0)) continue;
602
+ var type = el.type ? String(el.type).toLowerCase() : "";
603
+ var text = String(el.value);
604
+ // rrweb masks password inputs by default (maskInputOptions
605
+ // \`{ password: true }\`, default maskInputFn = same-length '*').
606
+ if (type === "password") {
607
+ var masked = "";
608
+ for (var k = 0; k < text.length; k++) masked += "*";
609
+ text = masked;
610
+ }
611
+ window.__spectestRrwebEvents.push({
612
+ type: 3,
613
+ data: {
614
+ source: 5,
615
+ text: text,
616
+ isChecked: (type === "radio" || type === "checkbox") ? !!el.checked : false,
617
+ id: id,
618
+ },
619
+ timestamp: Date.now(),
620
+ });
621
+ }
622
+ }
623
+ try {
624
+ document.addEventListener("reset", function (ev) {
625
+ var form = ev.target;
626
+ if (!form || !form.tagName || String(form.tagName).toUpperCase() !== "FORM") return;
627
+ var before = snap(form);
628
+ if (!before.length) return;
629
+ // The controls are reset after dispatch returns — diff then.
630
+ setTimeout(function () { emitChanged(before); }, 0);
631
+ }, true);
632
+ } catch (e) { /* recording continues without reset capture. */ }
633
+ })();
553
634
  })();
554
635
  `;
555
636
  // The `;` between parts is load-bearing: a vendored bundle that ends
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.28.3",
3
+ "version": "0.29.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",