@pygmalionjs/pygmalion 0.2.18 → 0.2.19

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 CHANGED
@@ -154,6 +154,26 @@ unapplied edits, `switchSource` refuses rather than leave the canvas on a
154
154
  revision those edits were never written against — apply or revert them first, or
155
155
  pass `{ discardEdits: true }`.
156
156
 
157
+ ### Screens that never stop moving
158
+
159
+ A capture waits for the document to hold still. A screen with an elapsed clock, a
160
+ level meter, or a marquee never does, so it fails on `stabilize` no matter how
161
+ long it waits. Declare those regions and their mutations stop counting as churn:
162
+
163
+ ```ts
164
+ captureStoryboardCase({
165
+ // ...
166
+ stability: {
167
+ volatileSelectors: ['[data-testid="elapsed-time"]', '[data-live-meter]'],
168
+ },
169
+ });
170
+ ```
171
+
172
+ The wait itself samples every 100ms and needs three identical samples, up to a
173
+ 4s ceiling — wide enough for a screen that repaints on a one-second beat.
174
+ `attempts`, `requiredStableSamples`, `intervalMs`, and `minimumWaitMs` are
175
+ adjustable for anything slower.
176
+
157
177
  ## Register screens and scenarios
158
178
 
159
179
  The host owns application-specific routes, fixtures, authentication, mock data,
@@ -651,22 +651,43 @@ export function collectStoryboardDomTree() {
651
651
  * This function is self-contained because Playwright serializes it into the
652
652
  * page.
653
653
  */
654
- export function storyboardDocumentStabilitySignature() {
654
+ export function storyboardDocumentStabilitySignature(volatileSelectors = []) {
655
655
  const runtime = window;
656
656
  const trackerKey = '__PYGMALION_STORYBOARD_STABILITY__';
657
+ const selectors = Array.isArray(volatileSelectors) ? volatileSelectors : [];
658
+ const selectorKey = selectors.join(',');
657
659
  let tracker = runtime[trackerKey];
658
- if (!tracker || tracker.document !== document) {
660
+ if (!tracker || tracker.document !== document || tracker.selectorKey !== selectorKey) {
659
661
  tracker?.observer?.disconnect?.();
660
662
  tracker = {
661
663
  document,
664
+ selectorKey,
662
665
  observer: null,
663
666
  revision: 0,
664
667
  };
668
+ // A screen can hold something that never stops changing — an elapsed clock, a
669
+ // level meter, a marquee. Counting those makes the document look unstable
670
+ // forever, so a caller can declare them and have their mutations ignored.
671
+ const isVolatile = (node) => {
672
+ if (!selectorKey) return false;
673
+ const element =
674
+ node && node.nodeType === 1 ? node : (node && node.parentElement) || null;
675
+ if (!element || typeof element.closest !== 'function') return false;
676
+ try {
677
+ return element.closest(selectorKey) != null;
678
+ } catch {
679
+ return false;
680
+ }
681
+ };
665
682
  if (
666
683
  document.documentElement &&
667
684
  typeof runtime.MutationObserver === 'function'
668
685
  ) {
669
- tracker.observer = new runtime.MutationObserver(() => {
686
+ tracker.observer = new runtime.MutationObserver((records) => {
687
+ const batch = Array.isArray(records) ? records : [];
688
+ if (batch.length > 0 && batch.every((record) => isVolatile(record.target))) {
689
+ return;
690
+ }
670
691
  tracker.revision += 1;
671
692
  });
672
693
  tracker.observer.observe(document.documentElement, {
@@ -724,10 +745,13 @@ export function storyboardDocumentStabilitySignature() {
724
745
  export async function waitForStableStoryboardDocument(
725
746
  page,
726
747
  {
727
- attempts = 20,
748
+ // A screen that repaints on a one-second beat needs a window wider than that
749
+ // beat to show a quiet stretch, so the ceiling is 4s rather than 2s.
750
+ attempts = 40,
728
751
  requiredStableSamples = 3,
729
752
  intervalMs = 100,
730
753
  minimumWaitMs = 400,
754
+ volatileSelectors = [],
731
755
  } = {},
732
756
  ) {
733
757
  await page.evaluate(async () => {
@@ -737,7 +761,10 @@ export async function waitForStableStoryboardDocument(
737
761
  let stable = 0;
738
762
  let elapsedMs = 0;
739
763
  for (let attempt = 0; attempt < attempts; attempt += 1) {
740
- const current = await page.evaluate(storyboardDocumentStabilitySignature);
764
+ const current = await page.evaluate(
765
+ storyboardDocumentStabilitySignature,
766
+ volatileSelectors,
767
+ );
741
768
  if (current === previous) stable += 1;
742
769
  else stable = 0;
743
770
  previous = current;
@@ -890,13 +917,14 @@ async function collectStableEvidence(
890
917
  includePreviewSnapshot,
891
918
  previewBaseToken,
892
919
  screenshotOptions,
920
+ stability = {},
893
921
  },
894
922
  ) {
895
923
  const evidence = {};
896
924
  const errors = [];
897
925
  try {
898
926
  await page.addStyleTag({ content: FROZEN_STYLE });
899
- const stable = await waitForStableStoryboardDocument(page);
927
+ const stable = await waitForStableStoryboardDocument(page, stability);
900
928
  if (!stable) {
901
929
  throw new Error(
902
930
  'The rendered document did not reach a stable DOM and overlay state.',
@@ -986,6 +1014,7 @@ export async function captureStoryboardCase({
986
1014
  contextOptions = {},
987
1015
  navigationOptions = {},
988
1016
  screenshotOptions = {},
1017
+ stability = {},
989
1018
  } = {}) {
990
1019
  if (!browser || typeof browser.newContext !== 'function') {
991
1020
  throw new TypeError(
@@ -1081,7 +1110,7 @@ export async function captureStoryboardCase({
1081
1110
  }
1082
1111
  await atCaptureStage('stabilize', async () => {
1083
1112
  await page.addStyleTag({ content: FROZEN_STYLE });
1084
- const stable = await waitForStableStoryboardDocument(page);
1113
+ const stable = await waitForStableStoryboardDocument(page, stability);
1085
1114
  if (!stable) {
1086
1115
  throw new Error(
1087
1116
  'The rendered document did not reach a stable DOM and overlay state.',
@@ -1104,6 +1133,7 @@ export async function captureStoryboardCase({
1104
1133
  includePreviewSnapshot,
1105
1134
  previewBaseToken,
1106
1135
  screenshotOptions,
1136
+ stability,
1107
1137
  });
1108
1138
  evidence = collected.evidence;
1109
1139
  evidenceErrors.push(...collected.errors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
package/storyboard.d.ts CHANGED
@@ -66,6 +66,21 @@ export interface StoryboardCaptureOptions<
66
66
  contextOptions?: Record<string, unknown>;
67
67
  navigationOptions?: Record<string, unknown>;
68
68
  screenshotOptions?: Record<string, unknown>;
69
+ /** How long to wait for a still document, and what may keep moving. */
70
+ stability?: StoryboardStabilityOptions;
71
+ }
72
+
73
+ export interface StoryboardStabilityOptions {
74
+ /** Samples before giving up. Default 40 at 100ms, so a 4s ceiling. */
75
+ attempts?: number;
76
+ requiredStableSamples?: number;
77
+ intervalMs?: number;
78
+ minimumWaitMs?: number;
79
+ /**
80
+ * Regions that are expected to keep changing — an elapsed clock, a level
81
+ * meter, a marquee. Mutations confined to them do not count as churn.
82
+ */
83
+ volatileSelectors?: readonly string[];
69
84
  }
70
85
 
71
86
  export declare const STORYBOARD_CAPTURE_STATUSES: Readonly<{
@@ -132,14 +147,11 @@ export declare function assertFinalStoryboardState(
132
147
  ): Promise<void>;
133
148
  export declare function waitForStableStoryboardDocument(
134
149
  page: unknown,
135
- options?: {
136
- attempts?: number;
137
- requiredStableSamples?: number;
138
- intervalMs?: number;
139
- minimumWaitMs?: number;
140
- },
150
+ options?: StoryboardStabilityOptions,
141
151
  ): Promise<boolean>;
142
- export declare function storyboardDocumentStabilitySignature(): string;
152
+ export declare function storyboardDocumentStabilitySignature(
153
+ volatileSelectors?: readonly string[],
154
+ ): string;
143
155
  export declare function storyboardCaptureCandidateTexts(
144
156
  element: Element,
145
157
  ): string[];