tuffgal 0.1.0-alpha.8 → 0.2.0-alpha.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/CHANGELOG.md +140 -1
- package/README.md +82 -4
- package/dist/cli.d.ts +50 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +19 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/config.d.ts +121 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +114 -9
- package/dist/config.js.map +1 -1
- package/dist/coverage/screens.d.ts +11 -6
- package/dist/coverage/screens.d.ts.map +1 -1
- package/dist/coverage/screens.js +12 -7
- package/dist/coverage/screens.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/reporter/assets/report.css +330 -50
- package/dist/reporter/assets/report.js +361 -55
- package/dist/reporter/template.d.ts +8 -1
- package/dist/reporter/template.d.ts.map +1 -1
- package/dist/reporter/template.js +447 -78
- package/dist/reporter/template.js.map +1 -1
- package/dist/reporter/writeReport.d.ts +2 -2
- package/dist/reporter/writeReport.d.ts.map +1 -1
- package/dist/reporter/writeReport.js +3 -3
- package/dist/reporter/writeReport.js.map +1 -1
- package/dist/runner/approve.d.ts +34 -9
- package/dist/runner/approve.d.ts.map +1 -1
- package/dist/runner/approve.js +54 -10
- package/dist/runner/approve.js.map +1 -1
- package/dist/runner/approveFrom.d.ts +58 -0
- package/dist/runner/approveFrom.d.ts.map +1 -0
- package/dist/runner/approveFrom.js +384 -0
- package/dist/runner/approveFrom.js.map +1 -0
- package/dist/runner/breakpointPasses.d.ts +39 -0
- package/dist/runner/breakpointPasses.d.ts.map +1 -0
- package/dist/runner/breakpointPasses.js +107 -0
- package/dist/runner/breakpointPasses.js.map +1 -0
- package/dist/runner/bridges/database.d.ts +4 -3
- package/dist/runner/bridges/database.d.ts.map +1 -1
- package/dist/runner/bridges/database.js +4 -3
- package/dist/runner/bridges/database.js.map +1 -1
- package/dist/runner/exitCode.d.ts +34 -0
- package/dist/runner/exitCode.d.ts.map +1 -0
- package/dist/runner/exitCode.js +41 -0
- package/dist/runner/exitCode.js.map +1 -0
- package/dist/runner/manifest.d.ts +149 -0
- package/dist/runner/manifest.d.ts.map +1 -0
- package/dist/runner/manifest.js +229 -0
- package/dist/runner/manifest.js.map +1 -0
- package/dist/runner/mode.d.ts +41 -0
- package/dist/runner/mode.d.ts.map +1 -0
- package/dist/runner/mode.js +43 -0
- package/dist/runner/mode.js.map +1 -0
- package/dist/runner/orphanScan.d.ts +68 -0
- package/dist/runner/orphanScan.d.ts.map +1 -0
- package/dist/runner/orphanScan.js +150 -0
- package/dist/runner/orphanScan.js.map +1 -0
- package/dist/runner/run.d.ts +85 -1
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +267 -41
- package/dist/runner/run.js.map +1 -1
- package/dist/runner/runAction.d.ts +22 -0
- package/dist/runner/runAction.d.ts.map +1 -1
- package/dist/runner/runAction.js +146 -24
- package/dist/runner/runAction.js.map +1 -1
- package/dist/runner/runStory.d.ts +40 -2
- package/dist/runner/runStory.d.ts.map +1 -1
- package/dist/runner/runStory.js +195 -62
- package/dist/runner/runStory.js.map +1 -1
- package/dist/runner/storyFilter.d.ts +8 -0
- package/dist/runner/storyFilter.d.ts.map +1 -1
- package/dist/runner/storyFilter.js +11 -0
- package/dist/runner/storyFilter.js.map +1 -1
- package/dist/schema/action.d.ts +11 -11
- package/dist/schema/result.d.ts +124 -6
- package/dist/schema/result.d.ts.map +1 -1
- package/dist/schema/result.js.map +1 -1
- package/dist/schema/story.d.ts +15 -4
- package/dist/schema/story.d.ts.map +1 -1
- package/dist/schema/story.js +35 -11
- package/dist/schema/story.js.map +1 -1
- package/dist/screenshots/baselineStore.d.ts +111 -5
- package/dist/screenshots/baselineStore.d.ts.map +1 -1
- package/dist/screenshots/baselineStore.js +132 -20
- package/dist/screenshots/baselineStore.js.map +1 -1
- package/dist/screenshots/capture.d.ts +10 -5
- package/dist/screenshots/capture.d.ts.map +1 -1
- package/dist/screenshots/capture.js +31 -18
- package/dist/screenshots/capture.js.map +1 -1
- package/package.json +1 -1
|
@@ -59,6 +59,20 @@
|
|
|
59
59
|
var statusRegion = createStatusRegion('.story-filter-status');
|
|
60
60
|
var bulkRegion = createStatusRegion('.bulk-toggle-status');
|
|
61
61
|
|
|
62
|
+
// The visible label word for a filter button, used to echo the active filter
|
|
63
|
+
// in the bulk-toggle announcement (e.g. "Expanded passed in 3 stories"). The
|
|
64
|
+
// button's `data-filter` carries the matcher TOKEN ("pass"); the visible
|
|
65
|
+
// `.indicator` span carries the human LABEL ("passed"). The "all" total maps to
|
|
66
|
+
// "all" regardless of its visible "stories" label. A missing button → "all".
|
|
67
|
+
function filterLabel(button) {
|
|
68
|
+
if (!button) return 'all';
|
|
69
|
+
if (button.getAttribute('data-filter') === 'all') return 'all';
|
|
70
|
+
var indicator = button.querySelector('.indicator');
|
|
71
|
+
return indicator
|
|
72
|
+
? indicator.textContent.trim()
|
|
73
|
+
: button.getAttribute('data-filter');
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
// Reveals the matching <div.shot-panel> for the radio the user picks. The
|
|
63
77
|
// radio group already handles keyboard navigation natively (arrow keys move
|
|
64
78
|
// the selection across the visible labels); this script just translates the
|
|
@@ -79,17 +93,20 @@
|
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
var defaultTab = container.getAttribute('data-default-tab');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
initialRadio
|
|
91
|
-
|
|
96
|
+
function reset() {
|
|
97
|
+
var initialRadio =
|
|
98
|
+
radios.find(function (radio) {
|
|
99
|
+
return radio.value === defaultTab && !radio.disabled;
|
|
100
|
+
}) ||
|
|
101
|
+
radios.find(function (radio) {
|
|
102
|
+
return !radio.disabled;
|
|
103
|
+
});
|
|
104
|
+
if (initialRadio) {
|
|
105
|
+
initialRadio.checked = true;
|
|
106
|
+
activate(initialRadio.value);
|
|
107
|
+
}
|
|
92
108
|
}
|
|
109
|
+
reset();
|
|
93
110
|
|
|
94
111
|
radios.forEach(function (radio) {
|
|
95
112
|
radio.addEventListener('change', function () {
|
|
@@ -97,6 +114,21 @@
|
|
|
97
114
|
activate(radio.value);
|
|
98
115
|
});
|
|
99
116
|
});
|
|
117
|
+
|
|
118
|
+
// Selection lives in the DOM, so a collapse only HIDES it — a prior
|
|
119
|
+
// Baseline/Diff pick would otherwise still be checked on the next expand.
|
|
120
|
+
// Reset on COLLAPSE, not expand: the `toggle` event fires AFTER the state
|
|
121
|
+
// change, so resetting on open flashes a visible baseline→actual jump.
|
|
122
|
+
// Resetting on close does the swap while the panel is hidden, so the next
|
|
123
|
+
// expand is already on the default. <details> fires `toggle` on user clicks
|
|
124
|
+
// AND the bulk-toggle's programmatic `open` change, so this covers
|
|
125
|
+
// Collapse-all too. First-ever open is covered by the initial reset() above.
|
|
126
|
+
var details = container.closest && container.closest('details.shots');
|
|
127
|
+
if (details) {
|
|
128
|
+
details.addEventListener('toggle', function () {
|
|
129
|
+
if (!details.open) reset();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
document.querySelectorAll('.shot-radio').forEach(function (container) {
|
|
@@ -104,65 +136,262 @@
|
|
|
104
136
|
});
|
|
105
137
|
})();
|
|
106
138
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// the
|
|
110
|
-
//
|
|
111
|
-
//
|
|
139
|
+
// Interactive screenshot viewer (rendered when the report is built with
|
|
140
|
+
// interactiveMode). Each action shows ONE shared <img>. A native radio group
|
|
141
|
+
// is the committed-state source of truth — keyboard, touch, and AT operate it
|
|
142
|
+
// exactly like the radio-tab viewer. On top of that, the mouse gesture is a
|
|
143
|
+
// STATELESS visual preview layered on the same <img>:
|
|
144
|
+
// hover (mouseenter/mousemove) → baseline src
|
|
145
|
+
// press (mousedown) → diff src (no-op when there is no diff)
|
|
146
|
+
// release/leave (mouseup/mouseleave) → revert to the checked radio's variant
|
|
147
|
+
// The preview ONLY rewrites img.src. It never changes the checked radio, the
|
|
148
|
+
// img alt, any ARIA attribute, or any live region — so hovering announces
|
|
149
|
+
// nothing by construction. The mouse listeners live on the .shot-stage wrapper
|
|
150
|
+
// rather than the <img>, keeping the image itself handler-free and
|
|
151
|
+
// non-focusable. Non-interactive reports have no .shot-interactive nodes, so
|
|
152
|
+
// this block is a no-op there (and setupShots is a no-op on interactive ones).
|
|
112
153
|
(function () {
|
|
113
|
-
var
|
|
114
|
-
|
|
154
|
+
var VARIANT_LABELS = {
|
|
155
|
+
baseline: 'Baseline',
|
|
156
|
+
actual: 'Actual',
|
|
157
|
+
diff: 'Diff',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function setupInteractiveShots(fieldset) {
|
|
161
|
+
var root = fieldset.parentElement;
|
|
162
|
+
if (!root) return;
|
|
163
|
+
var stage = root.querySelector('.shot-stage');
|
|
164
|
+
var image = root.querySelector('.shot-image');
|
|
165
|
+
if (!stage || !image) return;
|
|
166
|
+
var captionVariant = fieldset.querySelector('.shot-caption-variant');
|
|
167
|
+
var radios = Array.prototype.slice.call(
|
|
168
|
+
fieldset.querySelectorAll('input[type="radio"]'),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Map each AVAILABLE variant to its src, read off the shared <img>'s
|
|
172
|
+
// data-src-* attributes. Absent variants (e.g. diff on a clean pass) have
|
|
173
|
+
// no attribute, so they never enter the map and every lookup is a no-op.
|
|
174
|
+
var sources = {};
|
|
175
|
+
['baseline', 'actual', 'diff'].forEach(function (variant) {
|
|
176
|
+
var src = image.getAttribute('data-src-' + variant);
|
|
177
|
+
if (src !== null) sources[variant] = src;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// `committed` mirrors the checked radio — the ONLY state the keyboard /
|
|
181
|
+
// touch / AT path mutates. Mouse preview reverts here on release/leave.
|
|
182
|
+
var committed = null;
|
|
183
|
+
var pressed = false;
|
|
184
|
+
|
|
185
|
+
function commit(variant) {
|
|
186
|
+
if (!(variant in sources)) return;
|
|
187
|
+
committed = variant;
|
|
188
|
+
image.src = sources[variant];
|
|
189
|
+
if (captionVariant) {
|
|
190
|
+
captionVariant.textContent = VARIANT_LABELS[variant] || variant;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Show a variant WITHOUT committing (mouse preview). No-op when the variant
|
|
195
|
+
// has no src, so press-with-no-diff leaves the displayed image untouched.
|
|
196
|
+
function preview(variant) {
|
|
197
|
+
if (!(variant in sources)) return;
|
|
198
|
+
image.src = sources[variant];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function revert() {
|
|
202
|
+
if (committed && committed in sources) {
|
|
203
|
+
image.src = sources[committed];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
var initial =
|
|
208
|
+
radios.find(function (radio) {
|
|
209
|
+
return radio.checked;
|
|
210
|
+
}) || radios[0];
|
|
211
|
+
if (initial) commit(initial.value);
|
|
212
|
+
|
|
213
|
+
radios.forEach(function (radio) {
|
|
214
|
+
radio.addEventListener('change', function () {
|
|
215
|
+
commit(radio.value);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
function hoverPreview() {
|
|
220
|
+
// While not pressed, hovering previews the baseline for an in-place
|
|
221
|
+
// compare. The pressed guard keeps a held mousedown pinned to diff even
|
|
222
|
+
// as the pointer moves across the image.
|
|
223
|
+
if (!pressed) preview('baseline');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
stage.addEventListener('mouseenter', hoverPreview);
|
|
227
|
+
stage.addEventListener('mousemove', hoverPreview);
|
|
228
|
+
stage.addEventListener('mousedown', function () {
|
|
229
|
+
pressed = true;
|
|
230
|
+
preview('diff');
|
|
231
|
+
});
|
|
232
|
+
stage.addEventListener('mouseup', function () {
|
|
233
|
+
pressed = false;
|
|
234
|
+
revert();
|
|
235
|
+
});
|
|
236
|
+
stage.addEventListener('mouseleave', function () {
|
|
237
|
+
pressed = false;
|
|
238
|
+
revert();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
document
|
|
243
|
+
.querySelectorAll('.shot-interactive')
|
|
244
|
+
.forEach(function (fieldset) {
|
|
245
|
+
setupInteractiveShots(fieldset);
|
|
246
|
+
});
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
// Status filter for the stories list. Each status total in the summary row is
|
|
250
|
+
// a native <button aria-pressed> single-select filter; clicking one toggles
|
|
251
|
+
// `hidden` on every <li.story> whose `data-status` doesn't match its
|
|
252
|
+
// `data-filter` token ("all" clears the filter). Exactly one button is pressed
|
|
253
|
+
// at all times (default: the "all/stories" button). Re-clicking the active
|
|
254
|
+
// non-"all" filter reverts to "all". Live-region updates are debounced by
|
|
255
|
+
// ~150ms; the visual hide/show is applied immediately.
|
|
256
|
+
(function () {
|
|
257
|
+
var filterButtons = Array.prototype.slice.call(
|
|
258
|
+
document.querySelectorAll('.summary-filter'),
|
|
259
|
+
);
|
|
260
|
+
if (filterButtons.length === 0) return;
|
|
115
261
|
var list = document.querySelector('.stories');
|
|
116
262
|
var empty = document.querySelector('.stories-empty');
|
|
117
263
|
if (!list || !empty) return;
|
|
118
264
|
|
|
119
265
|
var stories = Array.prototype.slice.call(list.querySelectorAll('.story'));
|
|
120
|
-
var radios = Array.prototype.slice.call(
|
|
121
|
-
fieldset.querySelectorAll('input[type="radio"]'),
|
|
122
|
-
);
|
|
123
266
|
var total = stories.length;
|
|
267
|
+
var allButton =
|
|
268
|
+
filterButtons.find(function (button) {
|
|
269
|
+
return button.getAttribute('data-filter') === 'all';
|
|
270
|
+
}) || filterButtons[0];
|
|
124
271
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// WCAG 2.5.3 Label in Name) and no live-region write (the filter
|
|
130
|
-
// announcement already covers the context change).
|
|
272
|
+
// Cached for the zero-match disable + focus-rescue logic in apply(). The
|
|
273
|
+
// scope word is now static markup ("screenshots" visible, " all screenshots"
|
|
274
|
+
// sr-only), so the buttons are never relabelled per filter — the filter's own
|
|
275
|
+
// live-region announcement already covers the context change.
|
|
131
276
|
var expandButton = document.querySelector('[data-bulk-toggle="expand"]');
|
|
132
277
|
var collapseButton = document.querySelector(
|
|
133
278
|
'[data-bulk-toggle="collapse"]',
|
|
134
279
|
);
|
|
135
280
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
// Maintain the single-pressed invariant: exactly one filter button carries
|
|
282
|
+
// aria-pressed="true".
|
|
283
|
+
function setPressed(active) {
|
|
284
|
+
filterButtons.forEach(function (button) {
|
|
285
|
+
button.setAttribute(
|
|
286
|
+
'aria-pressed',
|
|
287
|
+
button === active ? 'true' : 'false',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function show(el) {
|
|
293
|
+
el.hidden = false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// True when `container` holds at least one `.action` that is not hidden.
|
|
297
|
+
// Drives whether a breakpoint group / actions list earns its place under an
|
|
298
|
+
// active filter.
|
|
299
|
+
function hasVisibleAction(container) {
|
|
300
|
+
return Array.prototype.some.call(
|
|
301
|
+
container.querySelectorAll('.action'),
|
|
302
|
+
function (action) {
|
|
303
|
+
return !action.hidden;
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Apply the active filter top-down: story → breakpoint group → action. A
|
|
309
|
+
// non-"all" filter does more than hide whole stories — inside a matching
|
|
310
|
+
// story it also prunes the actions (and the now-empty breakpoint groups /
|
|
311
|
+
// actions lists) that don't match, so e.g. the "changed" view shows ONLY the
|
|
312
|
+
// changed rows of a changed story, never the pass rows that happen to share
|
|
313
|
+
// it. Expand-all then opens only those surviving rows.
|
|
314
|
+
function applyToStory(story, value) {
|
|
315
|
+
var actions = Array.prototype.slice.call(
|
|
316
|
+
story.querySelectorAll('.action'),
|
|
317
|
+
);
|
|
318
|
+
var groups = Array.prototype.slice.call(
|
|
319
|
+
story.querySelectorAll('.breakpoint-group'),
|
|
320
|
+
);
|
|
321
|
+
var lists = Array.prototype.slice.call(
|
|
322
|
+
story.querySelectorAll('ol.actions'),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Story visibility keeps the worst-wins rollup semantics: a story shows
|
|
326
|
+
// only when its own status matches (or the filter is "all").
|
|
327
|
+
var storyMatches =
|
|
328
|
+
value === 'all' || story.getAttribute('data-status') === value;
|
|
329
|
+
if (!storyMatches) {
|
|
330
|
+
story.hidden = true;
|
|
331
|
+
// Clear inner pruning so switching back to a matching filter — or to
|
|
332
|
+
// "all" — starts from a clean slate rather than inheriting stale hides.
|
|
333
|
+
actions.forEach(show);
|
|
334
|
+
groups.forEach(show);
|
|
335
|
+
lists.forEach(show);
|
|
336
|
+
return false;
|
|
140
337
|
}
|
|
141
|
-
|
|
142
|
-
|
|
338
|
+
|
|
339
|
+
if (value === 'all') {
|
|
340
|
+
story.hidden = false;
|
|
341
|
+
actions.forEach(show);
|
|
342
|
+
groups.forEach(show);
|
|
343
|
+
lists.forEach(show);
|
|
344
|
+
return true;
|
|
143
345
|
}
|
|
346
|
+
|
|
347
|
+
// Matching story under a specific filter: compare each action's
|
|
348
|
+
// data-status against the button's data-filter TOKEN ("pass"), not its
|
|
349
|
+
// visible label ("passed"), so the "passed" filter matches the `pass`
|
|
350
|
+
// actions instead of silently emptying every story.
|
|
351
|
+
actions.forEach(function (action) {
|
|
352
|
+
action.hidden = action.getAttribute('data-status') !== value;
|
|
353
|
+
});
|
|
354
|
+
groups.forEach(function (group) {
|
|
355
|
+
group.hidden = !hasVisibleAction(group);
|
|
356
|
+
});
|
|
357
|
+
lists.forEach(function (listEl) {
|
|
358
|
+
listEl.hidden = !hasVisibleAction(listEl);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// A worst-wins rollup guarantees at least one matching action, but guard
|
|
362
|
+
// the invariant: a story that pruned to nothing is hidden and uncounted
|
|
363
|
+
// so the "N of M" announcement stays truthful.
|
|
364
|
+
var storyVisible = actions.some(function (action) {
|
|
365
|
+
return !action.hidden;
|
|
366
|
+
});
|
|
367
|
+
story.hidden = !storyVisible;
|
|
368
|
+
return storyVisible;
|
|
144
369
|
}
|
|
145
370
|
|
|
146
|
-
function apply(
|
|
147
|
-
var value = radio.value;
|
|
148
|
-
var name = radio.getAttribute('data-filter-name') || value;
|
|
149
|
-
relabelBulkToggle(name);
|
|
371
|
+
function apply(value, trigger) {
|
|
150
372
|
var visible = 0;
|
|
151
373
|
stories.forEach(function (story) {
|
|
152
|
-
|
|
153
|
-
value === 'all' || story.getAttribute('data-status') === value;
|
|
154
|
-
story.hidden = !match;
|
|
155
|
-
if (match) visible += 1;
|
|
374
|
+
if (applyToStory(story, value)) visible += 1;
|
|
156
375
|
});
|
|
157
376
|
var hasNone = visible === 0;
|
|
158
377
|
list.hidden = hasNone;
|
|
159
378
|
empty.hidden = !hasNone;
|
|
160
379
|
|
|
380
|
+
// Disable-while-focused guard: when a zero-match filter is about to
|
|
381
|
+
// disable a bulk-toggle button that currently holds focus, move focus to
|
|
382
|
+
// the triggering filter control FIRST, so focus is never stranded on a
|
|
383
|
+
// disabled (unfocusable) button.
|
|
384
|
+
if (
|
|
385
|
+
hasNone &&
|
|
386
|
+
trigger &&
|
|
387
|
+
(document.activeElement === expandButton ||
|
|
388
|
+
document.activeElement === collapseButton)
|
|
389
|
+
) {
|
|
390
|
+
trigger.focus();
|
|
391
|
+
}
|
|
161
392
|
// Disable both bulk-toggle buttons when the active filter matches zero
|
|
162
393
|
// stories — there is nothing to expand/collapse. Runs on EVERY apply()
|
|
163
394
|
// call (including "all"), so selecting a matching filter re-enables them.
|
|
164
|
-
// `disabled` on a standalone <button> removes it from the tab order
|
|
165
|
-
// without trapping focus (focus stays on the radio that triggered apply).
|
|
166
395
|
if (expandButton) expandButton.disabled = hasNone;
|
|
167
396
|
if (collapseButton) collapseButton.disabled = hasNone;
|
|
168
397
|
|
|
@@ -172,11 +401,37 @@
|
|
|
172
401
|
: 'Showing ' + visible + ' of ' + total + ' stories';
|
|
173
402
|
|
|
174
403
|
statusRegion.writeDebounced(message, 150);
|
|
404
|
+
|
|
405
|
+
// Post-apply focus-loss guard: if filtering left focus on nothing, the
|
|
406
|
+
// body, or inside a now-[hidden] subtree, return it to the control that
|
|
407
|
+
// triggered this apply so keyboard users keep their place.
|
|
408
|
+
var active = document.activeElement;
|
|
409
|
+
if (
|
|
410
|
+
trigger &&
|
|
411
|
+
(!active ||
|
|
412
|
+
active === document.body ||
|
|
413
|
+
(active.closest && active.closest('[hidden]')))
|
|
414
|
+
) {
|
|
415
|
+
trigger.focus();
|
|
416
|
+
}
|
|
175
417
|
}
|
|
176
418
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
419
|
+
filterButtons.forEach(function (button) {
|
|
420
|
+
button.addEventListener('click', function () {
|
|
421
|
+
var token = button.getAttribute('data-filter');
|
|
422
|
+
var isActive = button.getAttribute('aria-pressed') === 'true';
|
|
423
|
+
// Re-clicking the active, non-"all" filter reverts to "all". Focus stays
|
|
424
|
+
// on the clicked button — it is in the summary row and never hidden by
|
|
425
|
+
// its own action.
|
|
426
|
+
if (isActive && token !== 'all') {
|
|
427
|
+
setPressed(allButton);
|
|
428
|
+
apply('all', button);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// Re-clicking the already-active "all" button is a no-op.
|
|
432
|
+
if (isActive) return;
|
|
433
|
+
setPressed(button);
|
|
434
|
+
apply(token, button);
|
|
180
435
|
});
|
|
181
436
|
});
|
|
182
437
|
})();
|
|
@@ -210,12 +465,30 @@
|
|
|
210
465
|
|
|
211
466
|
function apply(mode) {
|
|
212
467
|
var shouldOpen = mode === 'expand';
|
|
468
|
+
// Collapse-all focus rescue: if focus sits inside a disclosure that is
|
|
469
|
+
// about to collapse, move it to that disclosure's <summary> first — the
|
|
470
|
+
// summary stays visible after collapse, so focus is never dropped into a
|
|
471
|
+
// hidden subtree. Expand-all reveals content, so it needs no rescue.
|
|
472
|
+
if (!shouldOpen) {
|
|
473
|
+
var active = document.activeElement;
|
|
474
|
+
var openDetails =
|
|
475
|
+
active &&
|
|
476
|
+
active.closest &&
|
|
477
|
+
active.closest('details.shots[open]');
|
|
478
|
+
if (openDetails) {
|
|
479
|
+
var summary = openDetails.querySelector('summary');
|
|
480
|
+
if (summary) summary.focus();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
213
483
|
var visibleStories = Array.prototype.slice.call(
|
|
214
484
|
document.querySelectorAll('.story:not([hidden])'),
|
|
215
485
|
);
|
|
216
486
|
visibleStories.forEach(function (story) {
|
|
487
|
+
// Only the rows surviving the active filter — `.action:not([hidden])` —
|
|
488
|
+
// get toggled, so "Expand all" under e.g. the changed filter opens just
|
|
489
|
+
// the changed screenshots, not the pass rows sharing the same story.
|
|
217
490
|
var panels = Array.prototype.slice.call(
|
|
218
|
-
story.querySelectorAll('details.shots'),
|
|
491
|
+
story.querySelectorAll('.action:not([hidden]) details.shots'),
|
|
219
492
|
);
|
|
220
493
|
panels.forEach(function (panel) {
|
|
221
494
|
panel.open = shouldOpen;
|
|
@@ -227,15 +500,14 @@
|
|
|
227
500
|
// keeps the logic simple.
|
|
228
501
|
var count = visibleStories.length;
|
|
229
502
|
var verb = shouldOpen ? 'Expanded' : 'Collapsed';
|
|
230
|
-
// Echo the active filter scope so the announcement
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'all';
|
|
503
|
+
// Echo the active filter scope so the announcement carries context for
|
|
504
|
+
// what was toggled (e.g. "Expanded passed in 3 stories"). The button
|
|
505
|
+
// label itself is static ("Expand all screenshots"); only this live
|
|
506
|
+
// announcement reflects the filter. Read the pressed filter button at
|
|
507
|
+
// click time via the shared filterLabel helper. No pressed button falls
|
|
508
|
+
// back to "all".
|
|
509
|
+
var pressed = document.querySelector('.summary-filter[aria-pressed="true"]');
|
|
510
|
+
var name = filterLabel(pressed);
|
|
239
511
|
bulkRegion.write(verb + ' ' + name + ' in ' + count + ' stories');
|
|
240
512
|
}
|
|
241
513
|
|
|
@@ -245,4 +517,38 @@
|
|
|
245
517
|
});
|
|
246
518
|
});
|
|
247
519
|
})();
|
|
520
|
+
|
|
521
|
+
// Scroll a user-opened screenshot disclosure to the top of the viewport so the
|
|
522
|
+
// tall screenshot below the summary row is immediately visible. Fires ONLY on a
|
|
523
|
+
// user single-open:
|
|
524
|
+
// - The bulk-toggle sets `details.open` directly, which fires `toggle` but NO
|
|
525
|
+
// `click`, so a summary `click` listener never catches a bulk op (no
|
|
526
|
+
// scroll-thrash to the last-opened panel).
|
|
527
|
+
// - Native <summary> activation by mouse, Enter, or Space all dispatch one
|
|
528
|
+
// `click`, so a single listener covers every input mode.
|
|
529
|
+
// - The open-state flip is the click's DEFAULT action, run AFTER this handler,
|
|
530
|
+
// so `details.open` here is the PRE-toggle state. `if (open) return` means
|
|
531
|
+
// this click is closing the panel: never scroll on close.
|
|
532
|
+
// - Deferred to rAF so it runs after the flip + content layout. scrollIntoView
|
|
533
|
+
// does not move focus, so focus stays on the <summary>, which sits flush at
|
|
534
|
+
// the viewport top with its focus indicator visible.
|
|
535
|
+
(function () {
|
|
536
|
+
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
537
|
+
document.addEventListener('click', function (event) {
|
|
538
|
+
var target = event.target;
|
|
539
|
+
var summary = target && target.closest ? target.closest('summary') : null;
|
|
540
|
+
if (!summary) return;
|
|
541
|
+
var details = summary.parentElement;
|
|
542
|
+
if (!details || !details.matches('details.shots')) return;
|
|
543
|
+
// Pre-toggle state: open === "this click will close it" → do not scroll.
|
|
544
|
+
if (details.open) return;
|
|
545
|
+
requestAnimationFrame(function () {
|
|
546
|
+
details.scrollIntoView({
|
|
547
|
+
behavior: reduce.matches ? 'instant' : 'smooth',
|
|
548
|
+
block: 'start',
|
|
549
|
+
inline: 'nearest',
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
})();
|
|
248
554
|
})();
|
|
@@ -3,6 +3,13 @@ import type { RunResult } from '../schema/result.ts';
|
|
|
3
3
|
* Static HTML report. Rocks a terminal, dev-tool aesthetic. Dark mode by
|
|
4
4
|
* default, monospace font for data, sharp borders, and minimal but
|
|
5
5
|
* evocative icons + neon colors to indicate statuses.
|
|
6
|
+
*
|
|
7
|
+
* `<main>` reading order: an optional environment-mismatch banner (first, only
|
|
8
|
+
* when `environment.mismatch`), then the summary, the stories, and an optional
|
|
9
|
+
* deleted (orphaned-baseline) section last. The banner and deleted section are
|
|
10
|
+
* peer `<h2>` landmarks; the two supplementary per-action notes (candidate /
|
|
11
|
+
* a11y-drift) contribute no heading levels, so the outline stays
|
|
12
|
+
* H1 → summary → stories → deleted.
|
|
6
13
|
*/
|
|
7
|
-
export declare function renderReport(result: RunResult, reportDir: string): string;
|
|
14
|
+
export declare function renderReport(result: RunResult, reportDir: string, interactiveMode?: boolean): string;
|
|
8
15
|
//# sourceMappingURL=template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/reporter/template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAkB7B
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/reporter/template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAkB7B;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,MAAM,EACjB,eAAe,UAAQ,GACtB,MAAM,CA+BR"}
|