@pygmalionjs/pygmalion 0.5.34 → 0.6.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.
- package/README.md +7 -3
- package/dist-lib/{FrozenRoutePreview-hofdx4eP.js → FrozenRoutePreview-3yddEUJs.js} +2524 -2222
- package/dist-lib/pygmalion.js +6611 -6119
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +1 -1
- package/dist-lib/types/editor/automaticPseudoStates.d.ts +20 -0
- package/dist-lib/types/editor/designImport.d.ts +7 -1
- package/dist-lib/types/editor/heldPseudoStates.d.ts +18 -0
- package/dist-lib/types/editor/interactiveStates.d.ts +26 -1
- package/dist-lib/types/editor/previewBootstrap.d.ts +8 -0
- package/dist-lib/types/editor/shadowPreview.d.ts +6 -0
- package/dist-lib/types/lib.d.ts +13 -8
- package/dist-lib/types/shell/ComponentStateControls.d.ts +1 -5
- package/docs/screen-state-contract.md +150 -0
- package/node/automatic-pseudo-runtime.mjs +376 -0
- package/node/inspect-plugin.mjs +37 -0
- package/node/storyboard-capture-runtime.mjs +15 -0
- package/package.json +3 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotates visible pseudo-state targets in the running capture page.
|
|
3
|
+
*
|
|
4
|
+
* The function is intentionally self-contained: Playwright serializes it into
|
|
5
|
+
* the browser realm. Keep its metadata schema and discovery behavior aligned
|
|
6
|
+
* with src/editor/automaticPseudoStates.ts.
|
|
7
|
+
*/
|
|
8
|
+
export function annotateStoryboardAutomaticPseudoStates() {
|
|
9
|
+
const metadataAttribute = 'data-pygmalion-auto-pseudo';
|
|
10
|
+
const eventAttribute = 'data-pygmalion-pseudo-events';
|
|
11
|
+
const stateOrder = ['hover', 'focus-visible', 'active'];
|
|
12
|
+
const pseudoPattern =
|
|
13
|
+
/:(focus-visible|focus-within|hover|focus|active)(?![a-z0-9_-])/gi;
|
|
14
|
+
const focusableSelector = [
|
|
15
|
+
'button:not(:disabled)',
|
|
16
|
+
'a[href]',
|
|
17
|
+
'input:not(:disabled)',
|
|
18
|
+
'textarea:not(:disabled)',
|
|
19
|
+
'select:not(:disabled)',
|
|
20
|
+
'summary',
|
|
21
|
+
'area[href]',
|
|
22
|
+
'audio[controls]',
|
|
23
|
+
'video[controls]',
|
|
24
|
+
'iframe',
|
|
25
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
26
|
+
'[tabindex]:not(:disabled)',
|
|
27
|
+
].join(',');
|
|
28
|
+
const hoverReactEventProps = [
|
|
29
|
+
'onMouseEnter',
|
|
30
|
+
'onMouseOver',
|
|
31
|
+
'onMouseMove',
|
|
32
|
+
'onPointerEnter',
|
|
33
|
+
'onPointerOver',
|
|
34
|
+
'onPointerMove',
|
|
35
|
+
];
|
|
36
|
+
const focusReactEventProps = ['onFocus', 'onFocusCapture'];
|
|
37
|
+
|
|
38
|
+
const normalizeText = (value) =>
|
|
39
|
+
String(value ?? '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
42
|
+
const compactLabel = (value) => {
|
|
43
|
+
const normalized = normalizeText(value);
|
|
44
|
+
return normalized.length <= 52
|
|
45
|
+
? normalized
|
|
46
|
+
: `${normalized.slice(0, 49).trimEnd()}…`;
|
|
47
|
+
};
|
|
48
|
+
const attributeSelector = (name, value) => {
|
|
49
|
+
const escaped = String(value)
|
|
50
|
+
.replaceAll('\\', '\\\\')
|
|
51
|
+
.replaceAll('"', '\\"')
|
|
52
|
+
.replaceAll('\n', '\\a ')
|
|
53
|
+
.replaceAll('\r', '\\d ');
|
|
54
|
+
return `[${name}="${escaped}"]`;
|
|
55
|
+
};
|
|
56
|
+
const splitSelectorList = (selectorText) => {
|
|
57
|
+
const selectors = [];
|
|
58
|
+
let start = 0;
|
|
59
|
+
let round = 0;
|
|
60
|
+
let square = 0;
|
|
61
|
+
let quote = '';
|
|
62
|
+
for (let index = 0; index < selectorText.length; index += 1) {
|
|
63
|
+
const character = selectorText[index];
|
|
64
|
+
if (quote) {
|
|
65
|
+
if (character === '\\') index += 1;
|
|
66
|
+
else if (character === quote) quote = '';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (character === '"' || character === "'") {
|
|
70
|
+
quote = character;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (character === '(') round += 1;
|
|
74
|
+
else if (character === ')') round = Math.max(0, round - 1);
|
|
75
|
+
else if (character === '[') square += 1;
|
|
76
|
+
else if (character === ']') square = Math.max(0, square - 1);
|
|
77
|
+
else if (character === ',' && round === 0 && square === 0) {
|
|
78
|
+
selectors.push(selectorText.slice(start, index).trim());
|
|
79
|
+
start = index + 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
selectors.push(selectorText.slice(start).trim());
|
|
83
|
+
return selectors.filter(Boolean);
|
|
84
|
+
};
|
|
85
|
+
const visibleElement = (element) => {
|
|
86
|
+
let style;
|
|
87
|
+
try {
|
|
88
|
+
style = getComputedStyle(element);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
style.display === 'none' ||
|
|
94
|
+
style.visibility === 'hidden' ||
|
|
95
|
+
style.visibility === 'collapse'
|
|
96
|
+
) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const rect = element.getBoundingClientRect();
|
|
100
|
+
return rect.width > 0 && rect.height > 0;
|
|
101
|
+
};
|
|
102
|
+
const focusTarget = (element) => {
|
|
103
|
+
try {
|
|
104
|
+
if (element.matches(focusableSelector) && visibleElement(element)) {
|
|
105
|
+
return element;
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
[...element.querySelectorAll(focusableSelector)].find(visibleElement) ??
|
|
109
|
+
null
|
|
110
|
+
);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const normalizedNegatedPseudo = (selector) =>
|
|
116
|
+
selector.replace(
|
|
117
|
+
/:not\(\s*:(focus-visible|focus-within|hover|focus|active)\s*\)/gi,
|
|
118
|
+
(_match, state) => `:${state}`,
|
|
119
|
+
);
|
|
120
|
+
const compoundEndAfter = (selector, start) => {
|
|
121
|
+
let round = 0;
|
|
122
|
+
let square = 0;
|
|
123
|
+
let quote = '';
|
|
124
|
+
for (let index = 0; index < selector.length; index += 1) {
|
|
125
|
+
const character = selector[index];
|
|
126
|
+
if (quote) {
|
|
127
|
+
if (character === '\\') index += 1;
|
|
128
|
+
else if (character === quote) quote = '';
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (character === '"' || character === "'") {
|
|
132
|
+
quote = character;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === '(') round += 1;
|
|
136
|
+
else if (character === ')') round = Math.max(0, round - 1);
|
|
137
|
+
else if (character === '[') square += 1;
|
|
138
|
+
else if (character === ']') square = Math.max(0, square - 1);
|
|
139
|
+
if (
|
|
140
|
+
index >= start &&
|
|
141
|
+
round === 0 &&
|
|
142
|
+
square === 0 &&
|
|
143
|
+
(character === '>' ||
|
|
144
|
+
character === '+' ||
|
|
145
|
+
character === '~' ||
|
|
146
|
+
/\s/.test(character))
|
|
147
|
+
) {
|
|
148
|
+
return index;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return selector.length;
|
|
152
|
+
};
|
|
153
|
+
const pseudoSubjectSelector = (selectorText, pseudo) => {
|
|
154
|
+
const selector = normalizedNegatedPseudo(selectorText);
|
|
155
|
+
pseudoPattern.lastIndex = 0;
|
|
156
|
+
const matches = [...selector.matchAll(pseudoPattern)].filter((match) => {
|
|
157
|
+
const state = match[1].toLowerCase();
|
|
158
|
+
return pseudo === 'focus-visible'
|
|
159
|
+
? state === 'focus' ||
|
|
160
|
+
state === 'focus-visible' ||
|
|
161
|
+
state === 'focus-within'
|
|
162
|
+
: state === pseudo;
|
|
163
|
+
});
|
|
164
|
+
pseudoPattern.lastIndex = 0;
|
|
165
|
+
const match = matches.at(-1);
|
|
166
|
+
if (!match || match.index == null) return null;
|
|
167
|
+
const end = compoundEndAfter(selector, match.index + match[0].length);
|
|
168
|
+
const prefix = selector
|
|
169
|
+
.slice(0, end)
|
|
170
|
+
.replace(pseudoPattern, '')
|
|
171
|
+
.replace(/::[a-z0-9_-]+(?:\([^)]*\))?/gi, '')
|
|
172
|
+
.replace(/:(?:is|where|not)\(\s*(?:,\s*)*\)/gi, '')
|
|
173
|
+
.trim();
|
|
174
|
+
pseudoPattern.lastIndex = 0;
|
|
175
|
+
return prefix || null;
|
|
176
|
+
};
|
|
177
|
+
const activeGroupingRule = (rule) => {
|
|
178
|
+
if (rule.type === CSSRule.MEDIA_RULE) {
|
|
179
|
+
return !rule.conditionText || matchMedia(rule.conditionText).matches;
|
|
180
|
+
}
|
|
181
|
+
if (rule.type === CSSRule.SUPPORTS_RULE) {
|
|
182
|
+
try {
|
|
183
|
+
return !rule.conditionText || CSS.supports(rule.conditionText);
|
|
184
|
+
} catch {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
const found = new Map();
|
|
191
|
+
const add = (element, state) => {
|
|
192
|
+
if (!visibleElement(element)) return;
|
|
193
|
+
const states = found.get(element) ?? new Set();
|
|
194
|
+
states.add(state);
|
|
195
|
+
found.set(element, states);
|
|
196
|
+
};
|
|
197
|
+
const inspectRules = (rules) => {
|
|
198
|
+
for (const rule of [...rules]) {
|
|
199
|
+
const selectorText = rule.selectorText;
|
|
200
|
+
pseudoPattern.lastIndex = 0;
|
|
201
|
+
const carriesPseudo =
|
|
202
|
+
typeof selectorText === 'string' && pseudoPattern.test(selectorText);
|
|
203
|
+
pseudoPattern.lastIndex = 0;
|
|
204
|
+
if (carriesPseudo) {
|
|
205
|
+
for (const selector of splitSelectorList(selectorText)) {
|
|
206
|
+
const rawStates = new Set(
|
|
207
|
+
[...selector.matchAll(pseudoPattern)].map((match) =>
|
|
208
|
+
match[1].toLowerCase(),
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
pseudoPattern.lastIndex = 0;
|
|
212
|
+
const states = [];
|
|
213
|
+
if (rawStates.has('hover')) states.push('hover');
|
|
214
|
+
if (
|
|
215
|
+
rawStates.has('focus') ||
|
|
216
|
+
rawStates.has('focus-visible') ||
|
|
217
|
+
rawStates.has('focus-within')
|
|
218
|
+
) {
|
|
219
|
+
states.push('focus-visible');
|
|
220
|
+
}
|
|
221
|
+
if (rawStates.has('active')) states.push('active');
|
|
222
|
+
for (const state of states) {
|
|
223
|
+
const subjectSelector = pseudoSubjectSelector(selector, state);
|
|
224
|
+
if (!subjectSelector) continue;
|
|
225
|
+
let subjects;
|
|
226
|
+
try {
|
|
227
|
+
subjects = [...document.querySelectorAll(subjectSelector)];
|
|
228
|
+
} catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
for (const subject of subjects) {
|
|
232
|
+
if (state === 'focus-visible') {
|
|
233
|
+
const target = focusTarget(subject);
|
|
234
|
+
if (target) add(target, state);
|
|
235
|
+
} else {
|
|
236
|
+
add(subject, state);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (rule.cssRules && activeGroupingRule(rule)) {
|
|
243
|
+
inspectRules(rule.cssRules);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
for (const sheet of [...document.styleSheets]) {
|
|
248
|
+
try {
|
|
249
|
+
inspectRules(sheet.cssRules);
|
|
250
|
+
} catch {
|
|
251
|
+
// Cross-origin stylesheets cannot be inspected.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const element of document.querySelectorAll('*')) {
|
|
255
|
+
if (!visibleElement(element)) continue;
|
|
256
|
+
const declared = new Set(
|
|
257
|
+
String(element.getAttribute(eventAttribute) ?? '')
|
|
258
|
+
.split(',')
|
|
259
|
+
.map((value) => value.trim()),
|
|
260
|
+
);
|
|
261
|
+
for (const key of Object.getOwnPropertyNames(element)) {
|
|
262
|
+
if (!key.startsWith('__reactProps$')) continue;
|
|
263
|
+
const props = element[key];
|
|
264
|
+
if (!props || typeof props !== 'object') continue;
|
|
265
|
+
if (hoverReactEventProps.some((name) => typeof props[name] === 'function')) {
|
|
266
|
+
declared.add('hover');
|
|
267
|
+
}
|
|
268
|
+
if (focusReactEventProps.some((name) => typeof props[name] === 'function')) {
|
|
269
|
+
declared.add('focus');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const states = found.get(element) ?? new Set();
|
|
273
|
+
if (declared.has('hover')) states.add('hover');
|
|
274
|
+
if (declared.has('focus') && focusTarget(element) === element) {
|
|
275
|
+
states.add('focus-visible');
|
|
276
|
+
}
|
|
277
|
+
if (states.size > 0) found.set(element, states);
|
|
278
|
+
}
|
|
279
|
+
const selectorForElement = (element) => {
|
|
280
|
+
for (const name of [
|
|
281
|
+
'data-testid',
|
|
282
|
+
'data-pygmalion-own-source',
|
|
283
|
+
'data-pygmalion-source',
|
|
284
|
+
'id',
|
|
285
|
+
]) {
|
|
286
|
+
const value = element.getAttribute(name);
|
|
287
|
+
if (!value) continue;
|
|
288
|
+
const selector = attributeSelector(name, value);
|
|
289
|
+
try {
|
|
290
|
+
const matches = [...document.querySelectorAll(selector)];
|
|
291
|
+
const index = matches.indexOf(element);
|
|
292
|
+
if (index >= 0) return { selector, index };
|
|
293
|
+
} catch {
|
|
294
|
+
// Try the next identity source.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const segments = [];
|
|
298
|
+
let current = element;
|
|
299
|
+
while (current && current !== document.body) {
|
|
300
|
+
const parent = current.parentElement;
|
|
301
|
+
if (!parent) return null;
|
|
302
|
+
const tag = current.tagName.toLowerCase();
|
|
303
|
+
const siblings = [...parent.children].filter(
|
|
304
|
+
(candidate) => candidate.tagName === current.tagName,
|
|
305
|
+
);
|
|
306
|
+
segments.unshift(
|
|
307
|
+
`${tag}:nth-of-type(${siblings.indexOf(current) + 1})`,
|
|
308
|
+
);
|
|
309
|
+
current = parent;
|
|
310
|
+
}
|
|
311
|
+
return current && segments.length > 0
|
|
312
|
+
? { selector: `body > ${segments.join(' > ')}`, index: 0 }
|
|
313
|
+
: null;
|
|
314
|
+
};
|
|
315
|
+
const targetLabel = (element) => {
|
|
316
|
+
const descendantLabel = element
|
|
317
|
+
.querySelector('[aria-label]')
|
|
318
|
+
?.getAttribute('aria-label');
|
|
319
|
+
const sourceName = element
|
|
320
|
+
.getAttribute('data-pygmalion-source-style')
|
|
321
|
+
?.split('#')
|
|
322
|
+
.at(-1);
|
|
323
|
+
return compactLabel(
|
|
324
|
+
element.getAttribute('aria-label') ||
|
|
325
|
+
element.getAttribute('title') ||
|
|
326
|
+
descendantLabel ||
|
|
327
|
+
element.getAttribute('data-testid') ||
|
|
328
|
+
normalizeText(element.textContent) ||
|
|
329
|
+
sourceName ||
|
|
330
|
+
element.getAttribute('role') ||
|
|
331
|
+
element.tagName.toLowerCase(),
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
const stableId = (value) => {
|
|
335
|
+
let hash = 0x811c9dc5;
|
|
336
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
337
|
+
hash ^= value.charCodeAt(index);
|
|
338
|
+
hash = Math.imul(hash, 0x01000193);
|
|
339
|
+
}
|
|
340
|
+
return (hash >>> 0).toString(36);
|
|
341
|
+
};
|
|
342
|
+
const pending = [...found].flatMap(([element, states]) => {
|
|
343
|
+
const identity = selectorForElement(element);
|
|
344
|
+
if (!identity) return [];
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
element,
|
|
348
|
+
identity,
|
|
349
|
+
baseLabel: targetLabel(element),
|
|
350
|
+
states: stateOrder.filter((state) => states.has(state)),
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
});
|
|
354
|
+
const totals = new Map();
|
|
355
|
+
for (const entry of pending) {
|
|
356
|
+
totals.set(entry.baseLabel, (totals.get(entry.baseLabel) ?? 0) + 1);
|
|
357
|
+
}
|
|
358
|
+
const seen = new Map();
|
|
359
|
+
return pending.map(({ element, identity, baseLabel, states }) => {
|
|
360
|
+
const ordinal = (seen.get(baseLabel) ?? 0) + 1;
|
|
361
|
+
seen.set(baseLabel, ordinal);
|
|
362
|
+
const total = totals.get(baseLabel) ?? 1;
|
|
363
|
+
const label =
|
|
364
|
+
total > 1 ? `${baseLabel} · ${ordinal}/${total}` : baseLabel;
|
|
365
|
+
const key = `${identity.selector}\u0000${identity.index}`;
|
|
366
|
+
const target = {
|
|
367
|
+
id: `auto-pseudo:${stableId(key)}`,
|
|
368
|
+
label,
|
|
369
|
+
selector: identity.selector,
|
|
370
|
+
index: identity.index,
|
|
371
|
+
states,
|
|
372
|
+
};
|
|
373
|
+
element.setAttribute(metadataAttribute, JSON.stringify(target));
|
|
374
|
+
return target;
|
|
375
|
+
});
|
|
376
|
+
}
|
package/node/inspect-plugin.mjs
CHANGED
|
@@ -45,8 +45,20 @@ const INTERNAL_SOURCE_ATTRIBUTES = new Set([
|
|
|
45
45
|
'data-pygmalion-slot-count',
|
|
46
46
|
'data-pygmalion-own-source',
|
|
47
47
|
'data-pygmalion-own-props',
|
|
48
|
+
'data-pygmalion-pseudo-events',
|
|
48
49
|
]);
|
|
49
50
|
|
|
51
|
+
const HOVER_EVENT_ATTRIBUTES = new Set([
|
|
52
|
+
'onMouseEnter',
|
|
53
|
+
'onMouseOver',
|
|
54
|
+
'onMouseMove',
|
|
55
|
+
'onPointerEnter',
|
|
56
|
+
'onPointerOver',
|
|
57
|
+
'onPointerMove',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const FOCUS_EVENT_ATTRIBUTES = new Set(['onFocus']);
|
|
61
|
+
|
|
50
62
|
function primitiveJsxAttributeValue(attribute, sourceFile) {
|
|
51
63
|
if (!attribute.initializer) return true;
|
|
52
64
|
if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
|
|
@@ -144,6 +156,31 @@ function instrumentJsxInto(code, componentFile, magic) {
|
|
|
144
156
|
: 0;
|
|
145
157
|
injected.push(` data-pygmalion-slot-count="${slotCount}"`);
|
|
146
158
|
}
|
|
159
|
+
if (
|
|
160
|
+
/^[a-z]/.test(elementName) &&
|
|
161
|
+
!authoredAttributes.has('data-pygmalion-pseudo-events')
|
|
162
|
+
) {
|
|
163
|
+
const pseudoEvents = [];
|
|
164
|
+
if (
|
|
165
|
+
[...authoredAttributes].some((name) =>
|
|
166
|
+
HOVER_EVENT_ATTRIBUTES.has(name),
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
pseudoEvents.push('hover');
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
[...authoredAttributes].some((name) =>
|
|
173
|
+
FOCUS_EVENT_ATTRIBUTES.has(name),
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
pseudoEvents.push('focus');
|
|
177
|
+
}
|
|
178
|
+
if (pseudoEvents.length > 0) {
|
|
179
|
+
injected.push(
|
|
180
|
+
` data-pygmalion-pseudo-events="${pseudoEvents.join(',')}"`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
147
184
|
if (injected.length > 0) {
|
|
148
185
|
// Insert as first properties. Afterwards, if there are spread/explicit properties, they follow React's general property priority.
|
|
149
186
|
magic.appendLeft(node.tagName.getEnd(), injected.join(''));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { annotateStoryboardAutomaticPseudoStates } from './automatic-pseudo-runtime.mjs';
|
|
2
3
|
|
|
3
4
|
const DEFAULT_VIEWPORT = Object.freeze({ width: 1280, height: 800 });
|
|
4
5
|
const QA_FAILURE_STAGES = new Set(['interaction', 'assertion']);
|
|
@@ -381,6 +382,13 @@ export async function runStoryboardInteraction(page, interaction) {
|
|
|
381
382
|
await target.click({ timeout: timeoutMs });
|
|
382
383
|
} else if (interaction.action === 'focus') {
|
|
383
384
|
await target.focus({ timeout: timeoutMs });
|
|
385
|
+
} else if (interaction.action === 'hover') {
|
|
386
|
+
await target.hover({ timeout: timeoutMs });
|
|
387
|
+
} else if (interaction.action === 'focus-visible') {
|
|
388
|
+
await target.focus({ timeout: timeoutMs });
|
|
389
|
+
} else if (interaction.action === 'active') {
|
|
390
|
+
await target.hover({ timeout: timeoutMs });
|
|
391
|
+
await page.mouse.down();
|
|
384
392
|
} else if (interaction.action === 'fill') {
|
|
385
393
|
await target.fill(interaction.value ?? '', { timeout: timeoutMs });
|
|
386
394
|
} else if (interaction.action === 'check') {
|
|
@@ -1245,6 +1253,13 @@ async function collectStableEvidence(
|
|
|
1245
1253
|
} catch (error) {
|
|
1246
1254
|
errors.push(new StoryboardCaptureStageError('stabilize', error));
|
|
1247
1255
|
}
|
|
1256
|
+
if (includePreviewSnapshot) {
|
|
1257
|
+
try {
|
|
1258
|
+
await page.evaluate(annotateStoryboardAutomaticPseudoStates);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
errors.push(new StoryboardCaptureStageError('serialize', error));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1248
1263
|
if (includeDomTree) {
|
|
1249
1264
|
try {
|
|
1250
1265
|
evidence.domTree = await page.evaluate(collectStoryboardDomTree);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pygmalionjs/pygmalion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Code-backed DOM design sandbox and visual QA editor",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist-lib",
|
|
35
|
+
"docs/screen-state-contract.md",
|
|
35
36
|
"node/component-branches.mjs",
|
|
37
|
+
"node/automatic-pseudo-runtime.mjs",
|
|
36
38
|
"node/design-session.mjs",
|
|
37
39
|
"node/dev-mirror.mjs",
|
|
38
40
|
"node/dev-view.vite.mjs",
|