@pygmalionjs/pygmalion 0.5.17 → 0.5.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.
@@ -704,6 +704,11 @@ export function storyboardDocumentStabilitySignature(volatileSelectors = []) {
704
704
  selectorKey,
705
705
  observer: null,
706
706
  revision: 0,
707
+ // What kept changing, by element, so a screen that never settles can say
708
+ // which part of it would not stop — the alternative is a capture that
709
+ // reports "unstable" and a person guessing which widget to declare
710
+ // volatile. Bounded: a screen has a handful of movers, not thousands.
711
+ churn: new Map(),
707
712
  };
708
713
  // A screen can hold something that never stops changing — an elapsed clock, a
709
714
  // level meter, a marquee. Counting those makes the document look unstable
@@ -719,6 +724,35 @@ export function storyboardDocumentStabilitySignature(volatileSelectors = []) {
719
724
  return false;
720
725
  }
721
726
  };
727
+ const describe = (node) => {
728
+ const element =
729
+ node && node.nodeType === 1 ? node : (node && node.parentElement) || null;
730
+ if (!element) return '(text)';
731
+ const tag = String(element.tagName || '').toLowerCase();
732
+ const id = element.id ? `#${element.id}` : '';
733
+ const classes = String(element.getAttribute?.('class') ?? '')
734
+ .split(/\s+/)
735
+ .filter(Boolean)
736
+ .slice(0, 2)
737
+ .map((name) => `.${name}`)
738
+ .join('');
739
+ const testId = element.getAttribute?.('data-testid');
740
+ return `${tag}${id}${classes}${testId ? `[data-testid=${testId}]` : ''}`;
741
+ };
742
+ const countChurn = (records) => {
743
+ for (const record of records) {
744
+ if (isVolatile(record.target)) continue;
745
+ const key = describe(record.target);
746
+ const known = tracker.churn.get(key);
747
+ if (known != null) {
748
+ tracker.churn.set(key, known + 1);
749
+ } else if (tracker.churn.size < 64) {
750
+ tracker.churn.set(key, 1);
751
+ } else {
752
+ tracker.churn.set('(other)', (tracker.churn.get('(other)') ?? 0) + 1);
753
+ }
754
+ }
755
+ };
722
756
  if (
723
757
  document.documentElement &&
724
758
  typeof runtime.MutationObserver === 'function'
@@ -728,6 +762,7 @@ export function storyboardDocumentStabilitySignature(volatileSelectors = []) {
728
762
  if (batch.length > 0 && batch.every((record) => isVolatile(record.target))) {
729
763
  return;
730
764
  }
765
+ countChurn(batch);
731
766
  tracker.revision += 1;
732
767
  });
733
768
  tracker.observer.observe(document.documentElement, {
@@ -782,6 +817,47 @@ export function storyboardDocumentStabilitySignature(volatileSelectors = []) {
782
817
  });
783
818
  }
784
819
 
820
+ /**
821
+ * What kept the document from settling, most active first.
822
+ *
823
+ * Runs inside the page: the stability tracker keeps a bounded count of
824
+ * mutation targets that were not declared volatile, and this reads it back so
825
+ * a failed wait can name the mover instead of only saying "unstable".
826
+ */
827
+ export function storyboardDocumentChurnReport(limit = 6) {
828
+ const tracker = window['__PYGMALION_STORYBOARD_STABILITY__'];
829
+ const churn = tracker?.churn;
830
+ if (!churn || typeof churn.entries !== 'function') return [];
831
+ return [...churn.entries()]
832
+ .sort((left, right) => right[1] - left[1])
833
+ .slice(0, Math.max(0, limit))
834
+ .map(([target, count]) => ({ target, count }));
835
+ }
836
+
837
+ /** One line naming what a screen kept changing, for a stability failure. */
838
+ export function formatStoryboardChurn(report) {
839
+ if (!Array.isArray(report) || report.length === 0) return '';
840
+ const parts = report
841
+ .filter((entry) => entry && typeof entry.target === 'string')
842
+ .map((entry) => `${entry.target} ×${Number(entry.count) || 0}`);
843
+ return parts.length ? ` Still changing: ${parts.join(', ')}.` : '';
844
+ }
845
+
846
+ async function describeStoryboardChurn(page) {
847
+ try {
848
+ const report = await page.evaluate(storyboardDocumentChurnReport, 6);
849
+ return formatStoryboardChurn(report);
850
+ } catch {
851
+ return '';
852
+ }
853
+ }
854
+
855
+ function unstableDocumentError(churn) {
856
+ return new Error(
857
+ `The rendered document did not reach a stable DOM and overlay state.${churn}`,
858
+ );
859
+ }
860
+
785
861
  export async function waitForStableStoryboardDocument(
786
862
  page,
787
863
  {
@@ -1148,17 +1224,23 @@ async function collectStableEvidence(
1148
1224
  screenshotOptions,
1149
1225
  screenshotScale = STORYBOARD_CAPTURE_SCREENSHOT_SCALE,
1150
1226
  stability = {},
1227
+ // The stabilize stage already froze the page and saw it hold still. Waiting
1228
+ // again here measured as the single largest cost of a capture (two rounds
1229
+ // of the same wait, ~40% of the whole), and a document that was still a
1230
+ // moment ago is not made stiller by asking twice.
1231
+ frozenStyleApplied = false,
1232
+ alreadyStable = false,
1151
1233
  },
1152
1234
  ) {
1153
1235
  const evidence = {};
1154
1236
  const errors = [];
1155
1237
  try {
1156
- await page.addStyleTag({ content: FROZEN_STYLE });
1157
- const stable = await waitForStableStoryboardDocument(page, stability);
1158
- if (!stable) {
1159
- throw new Error(
1160
- 'The rendered document did not reach a stable DOM and overlay state.',
1161
- );
1238
+ if (!frozenStyleApplied) await page.addStyleTag({ content: FROZEN_STYLE });
1239
+ if (!alreadyStable) {
1240
+ const stable = await waitForStableStoryboardDocument(page, stability);
1241
+ if (!stable) {
1242
+ throw unstableDocumentError(await describeStoryboardChurn(page));
1243
+ }
1162
1244
  }
1163
1245
  } catch (error) {
1164
1246
  errors.push(new StoryboardCaptureStageError('stabilize', error));
@@ -1306,6 +1388,10 @@ export async function captureStoryboardCase({
1306
1388
  let primaryError = null;
1307
1389
  let evidence = {};
1308
1390
  const evidenceErrors = [];
1391
+ // Set by the stabilize stage; evidence collection reuses both instead of
1392
+ // freezing and waiting a second time.
1393
+ let frozenStyleApplied = Boolean(session?.frozen);
1394
+ let stabilized = false;
1309
1395
  try {
1310
1396
  throwIfAborted(signal);
1311
1397
  if (!warm) {
@@ -1394,12 +1480,12 @@ export async function captureStoryboardCase({
1394
1480
  await page.addStyleTag({ content: FROZEN_STYLE });
1395
1481
  if (session) session.frozen = true;
1396
1482
  }
1483
+ frozenStyleApplied = true;
1397
1484
  const stable = await waitForStableStoryboardDocument(page, stability);
1398
1485
  if (!stable) {
1399
- throw new Error(
1400
- 'The rendered document did not reach a stable DOM and overlay state.',
1401
- );
1486
+ throw unstableDocumentError(await describeStoryboardChurn(page));
1402
1487
  }
1488
+ stabilized = true;
1403
1489
  });
1404
1490
  await atCaptureStage('assertion', async () => {
1405
1491
  await assertFinalStoryboardState(page, screenCase.assertions ?? []);
@@ -1420,6 +1506,8 @@ export async function captureStoryboardCase({
1420
1506
  screenshotOptions,
1421
1507
  screenshotScale: resolveScreenshotScale(contextOptions),
1422
1508
  stability,
1509
+ frozenStyleApplied,
1510
+ alreadyStable: stabilized,
1423
1511
  });
1424
1512
  evidence = collected.evidence;
1425
1513
  evidenceErrors.push(...collected.errors);
package/node/vite.mjs CHANGED
@@ -258,6 +258,10 @@ export function createPygmalionVitePlugins(config) {
258
258
  endpoint: project.preview.artifactEndpoint,
259
259
  generateArtifact,
260
260
  acquireLease: (label) => devMirror?.acquireLease(label) ?? null,
261
+ requestRuntime: () => devMirror?.requestRuntime?.(),
262
+ // No mirror plugin means no checkout to prepare, so generation is
263
+ // always allowed to proceed as it did before.
264
+ runtimePrepared: () => devMirror?.runtimePrepared?.() ?? true,
261
265
  }),
262
266
  );
263
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -1,31 +0,0 @@
1
- import type { StoryboardGraphViewModel } from '../editor/storyboardGraphView';
2
- export interface StoryboardFrameGeometry {
3
- frameId: string;
4
- x: number;
5
- y: number;
6
- width: number;
7
- height: number;
8
- }
9
- export interface StoryboardConnectionPath {
10
- id: string;
11
- sourceFrameId: string;
12
- targetFrameId: string;
13
- path: string;
14
- }
15
- export type StoryboardConnectionMode = 'story' | 'focus' | 'overview';
16
- export interface StoryboardConnectionPathOptions {
17
- mode?: StoryboardConnectionMode;
18
- activeFrameId?: string | null;
19
- }
20
- /**
21
- * Projects graph transitions into world-space paths. The calculation is kept
22
- * independent from camera pan and zoom because the SVG lives inside the same
23
- * transformed canvas layer as the frames.
24
- */
25
- export declare function createStoryboardConnectionPaths(model: StoryboardGraphViewModel, frames: readonly StoryboardFrameGeometry[], options?: StoryboardConnectionPathOptions): StoryboardConnectionPath[];
26
- export declare function StoryboardConnections({ model, frames, mode, activeFrameId, }: {
27
- model: StoryboardGraphViewModel;
28
- frames: readonly StoryboardFrameGeometry[];
29
- mode?: StoryboardConnectionMode;
30
- activeFrameId?: string | null;
31
- }): import("react").JSX.Element | null;
@@ -1,3 +0,0 @@
1
- export declare function AssetsPanel({ onFrameFocus, }: {
2
- onFrameFocus?: (id: string) => void;
3
- }): import("react").JSX.Element;