@mhosaic/feedback 0.19.0 → 0.21.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.
@@ -0,0 +1,1614 @@
1
+ // src/qa-meter/mount.tsx
2
+ import { h as h5, render } from "preact";
3
+ import { useCallback, useEffect as useEffect2, useState as useState3 } from "preact/hooks";
4
+
5
+ // src/qa-meter/i18n.ts
6
+ var QA_STRINGS_FR = {
7
+ "pastille.label": "Suivi des tests",
8
+ "pastille.loading": "Chargement\u2026",
9
+ "pastille.unavailable": "Statuts indisponibles",
10
+ "pastille.unknownPage": "Page inconnue",
11
+ "pastille.unknownPageHint": "Cette route n\u2019est pas encore au plan du site des tests.",
12
+ "panel.freshness": "Statuts g\xE9n\xE9r\xE9s le {date} \xB7 version {version} \xB7 env {env}",
13
+ "panel.noScenarios": "Aucun sc\xE9nario li\xE9 \xE0 cette page.",
14
+ "panel.openSitemap": "Voir le plan du site",
15
+ "panel.sitemapLink": "Plan du site",
16
+ "panel.scenarioCountOne": "{count} sc\xE9nario",
17
+ "panel.scenarioCountMany": "{count} sc\xE9narios",
18
+ "panel.laneAutomated": "Automatis\xE9",
19
+ "panel.laneDev": "D\xE9v",
20
+ "panel.laneClient": "Client",
21
+ "lane.notTested": "Non test\xE9",
22
+ "lane.validatedBy": "Valid\xE9 par {by}",
23
+ "lane.rejectedBy": "Rejet\xE9 par {by}",
24
+ "lane.stale": "Valid\xE9 en v{version}, version actuelle v{current}",
25
+ "lane.clientPending": "Non test\xE9 \u2014 plateforme requise",
26
+ "lane.bot": "Automatis\xE9 (bot)",
27
+ "lane.code": "D\xE9veloppeur (code)",
28
+ "lane.human": "Humain",
29
+ "laneState.ok": "\xC0 jour",
30
+ "laneState.issue": "Probl\xE8me",
31
+ "laneState.none": "Non fait / p\xE9rim\xE9",
32
+ "color.green": "Valid\xE9",
33
+ "color.yellow": "En attente",
34
+ "color.red": "En \xE9chec",
35
+ "color.gray": "Non couvert",
36
+ "automated.passing": "R\xE9ussi",
37
+ "automated.failing": "En \xE9chec",
38
+ "automated.flaky": "Instable",
39
+ "automated.neverRun": "Jamais ex\xE9cut\xE9",
40
+ "source.testgen": "g\xE9n\xE9r\xE9",
41
+ "source.testgenHint": "Sc\xE9nario g\xE9n\xE9r\xE9 automatiquement (testgen)",
42
+ "suite.cases": "{count} cas",
43
+ "case.covered": "auto",
44
+ "case.manual": "manuel",
45
+ "case.blocked": "bloqu\xE9",
46
+ "case.openQuestion": "question produit",
47
+ "grounding.verified": "attendu v\xE9rifi\xE9",
48
+ "grounding.confirmLive": "\xE0 confirmer en live",
49
+ "grounding.productQuestion": "d\xE9cision produit",
50
+ "modal.title": "Plan du site \u2014 suivi des tests",
51
+ "modal.close": "Fermer",
52
+ "modal.tabPages": "Pages",
53
+ "modal.tabScenarios": "Sc\xE9narios",
54
+ "modal.youAreHere": "vous \xEAtes ici",
55
+ "modal.search": "Filtrer les sc\xE9narios\u2026",
56
+ "modal.noMatch": "Aucun sc\xE9nario ne correspond au filtre.",
57
+ "modal.traversed": "Travers\xE9es",
58
+ "modal.appliesTo": "{count} pages",
59
+ "modal.kpiPagesCovered": "Pages avec \u2265 1 sc\xE9nario",
60
+ "modal.kpiScenariosPassing": "Sc\xE9narios passants",
61
+ // New keys (not in donor)
62
+ "panel.noTests": "Aucun test ne couvre encore cette page.",
63
+ "panel.unknownPage": "Page inconnue du registre QA."
64
+ };
65
+ var QA_STRINGS_EN = {
66
+ "pastille.label": "Test tracking",
67
+ "pastille.loading": "Loading\u2026",
68
+ "pastille.unavailable": "Statuses unavailable",
69
+ "pastille.unknownPage": "Unknown page",
70
+ "pastille.unknownPageHint": "This route isn\u2019t in the test sitemap yet.",
71
+ "panel.freshness": "Statuses generated on {date} \xB7 version {version} \xB7 env {env}",
72
+ "panel.noScenarios": "No scenario linked to this page.",
73
+ "panel.openSitemap": "View sitemap",
74
+ "panel.sitemapLink": "Sitemap",
75
+ "panel.scenarioCountOne": "{count} scenario",
76
+ "panel.scenarioCountMany": "{count} scenarios",
77
+ "panel.laneAutomated": "Automated",
78
+ "panel.laneDev": "Dev",
79
+ "panel.laneClient": "Client",
80
+ "lane.notTested": "Not tested",
81
+ "lane.validatedBy": "Validated by {by}",
82
+ "lane.rejectedBy": "Rejected by {by}",
83
+ "lane.stale": "Validated in v{version}, current version v{current}",
84
+ "lane.clientPending": "Not tested \u2014 platform required",
85
+ "lane.bot": "Automated (bot)",
86
+ "lane.code": "Developer (code)",
87
+ "lane.human": "Human",
88
+ "laneState.ok": "Up to date",
89
+ "laneState.issue": "Issue",
90
+ "laneState.none": "Not done / stale",
91
+ "color.green": "Validated",
92
+ "color.yellow": "Pending",
93
+ "color.red": "Failing",
94
+ "color.gray": "Not covered",
95
+ "automated.passing": "Passing",
96
+ "automated.failing": "Failing",
97
+ "automated.flaky": "Flaky",
98
+ "automated.neverRun": "Never run",
99
+ "source.testgen": "generated",
100
+ "source.testgenHint": "Auto-generated scenario (testgen)",
101
+ "suite.cases": "{count} cases",
102
+ "case.covered": "auto",
103
+ "case.manual": "manual",
104
+ "case.blocked": "blocked",
105
+ "case.openQuestion": "product question",
106
+ "grounding.verified": "expected verified",
107
+ "grounding.confirmLive": "confirm live",
108
+ "grounding.productQuestion": "product decision",
109
+ "modal.title": "Sitemap \u2014 test tracking",
110
+ "modal.close": "Close",
111
+ "modal.tabPages": "Pages",
112
+ "modal.tabScenarios": "Scenarios",
113
+ "modal.youAreHere": "you are here",
114
+ "modal.search": "Filter scenarios\u2026",
115
+ "modal.noMatch": "No scenario matches the filter.",
116
+ "modal.traversed": "Traversed",
117
+ "modal.appliesTo": "{count} pages",
118
+ "modal.kpiPagesCovered": "Pages with \u2265 1 scenario",
119
+ "modal.kpiScenariosPassing": "Passing scenarios",
120
+ // New keys (not in donor)
121
+ "panel.noTests": "No tests cover this page yet.",
122
+ "panel.unknownPage": "Page unknown to the QA registry."
123
+ };
124
+ function resolveStrings(locale) {
125
+ return locale === "en" ? QA_STRINGS_EN : QA_STRINGS_FR;
126
+ }
127
+
128
+ // src/qa-meter/resolver.ts
129
+ function normalizePattern(pattern) {
130
+ if (!pattern) return "/";
131
+ let n = pattern.startsWith("/") ? pattern : `/${pattern}`;
132
+ n = n.replace(/\/{2,}/g, "/");
133
+ if (n.length > 1) n = n.replace(/\/+$/, "");
134
+ return n.length > 0 ? n : "/";
135
+ }
136
+ var segments = (p) => p.split("/").filter(Boolean);
137
+ function matchesPattern(pathSegs, patternSegs) {
138
+ let required = patternSegs.length;
139
+ while (required > 0) {
140
+ const last = patternSegs[required - 1];
141
+ if (last !== void 0 && last.startsWith(":") && last.endsWith("?")) required--;
142
+ else break;
143
+ }
144
+ if (pathSegs.length < required || pathSegs.length > patternSegs.length) return false;
145
+ return pathSegs.every((seg, i) => {
146
+ const pat = patternSegs[i];
147
+ return pat !== void 0 && (pat.startsWith(":") || pat === seg);
148
+ });
149
+ }
150
+ function resolvePage(pathname, patterns) {
151
+ const pathSegs = segments(normalizePattern(pathname));
152
+ let best = null;
153
+ let bestScore = -1;
154
+ for (const pattern of patterns) {
155
+ const patSegs = segments(pattern);
156
+ if (!matchesPattern(pathSegs, patSegs)) continue;
157
+ const statics = patSegs.filter((s) => !s.startsWith(":")).length;
158
+ const score = statics * 100 + patSegs.length;
159
+ if (score > bestScore) {
160
+ best = pattern;
161
+ bestScore = score;
162
+ }
163
+ }
164
+ return best;
165
+ }
166
+
167
+ // src/qa-meter/styles.ts
168
+ var QA_METER_STYLES = `
169
+ :host {
170
+ --mqa-z-index: 2147483639;
171
+ --mqa-z-panel: 2147483638;
172
+
173
+ /* Color tokens */
174
+ --mqa-green: #16a34a;
175
+ --mqa-yellow: #eab308;
176
+ --mqa-red: #dc2626;
177
+ --mqa-gray: #9ca3af;
178
+
179
+ /* Surface */
180
+ --mqa-bg: #ffffff;
181
+ --mqa-surface: #f9fafb;
182
+ --mqa-border: #e5e7eb;
183
+ --mqa-text: #111827;
184
+ --mqa-text-muted: #6b7280;
185
+ --mqa-primary: #3b82f6;
186
+ --mqa-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.12);
187
+ --mqa-shadow-xl: 0 24px 56px rgba(0,0,0,0.18), 0 8px 20px rgba(0,0,0,0.10);
188
+
189
+ /* Font */
190
+ --mqa-font: system-ui, -apple-system, sans-serif;
191
+ --mqa-radius: 8px;
192
+
193
+ all: initial;
194
+ font-family: var(--mqa-font);
195
+ color: var(--mqa-text);
196
+ position: fixed;
197
+ z-index: var(--mqa-z-index);
198
+ }
199
+
200
+ @media (prefers-color-scheme: dark) {
201
+ :host {
202
+ --mqa-bg: #1e293b;
203
+ --mqa-surface: #0f172a;
204
+ --mqa-border: #334155;
205
+ --mqa-text: #f8fafc;
206
+ --mqa-text-muted: #94a3b8;
207
+ --mqa-shadow-xl: 0 24px 56px rgba(0,0,0,0.55), 0 8px 20px rgba(0,0,0,0.40);
208
+ }
209
+ }
210
+
211
+ /* \u2500\u2500 Semaphore modifier classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
212
+
213
+ .qa--green { background: var(--mqa-green); }
214
+ .qa--yellow { background: var(--mqa-yellow); }
215
+ .qa--red { background: var(--mqa-red); }
216
+ .qa--gray { background: var(--mqa-gray); }
217
+
218
+ /* Priority modifiers (color only \u2014 components set sizing). */
219
+ .qa-prio--p0 { color: var(--mqa-red); }
220
+ .qa-prio--p1 { color: var(--mqa-yellow); }
221
+ .qa-prio--p2 { color: var(--mqa-green); }
222
+ .qa-prio--p3 { color: var(--mqa-gray); }
223
+
224
+ /* \u2500\u2500 Pastille (FAB circle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
225
+
226
+ .qa-pastille {
227
+ position: fixed;
228
+ right: var(--mqa-right, 1.5rem);
229
+ bottom: var(--mqa-bottom, 1.5rem);
230
+ z-index: var(--mqa-z-index);
231
+ }
232
+
233
+ .qa-pastille__dot {
234
+ width: 48px;
235
+ height: 48px;
236
+ border-radius: 9999px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ cursor: pointer;
241
+ box-shadow: var(--mqa-shadow);
242
+ color: rgba(0, 0, 0, 0.5);
243
+ animation: qa-heartbeat 2.4s ease-in-out infinite;
244
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
245
+ border: none;
246
+ }
247
+
248
+ .qa-pastille__dot:hover {
249
+ transform: scale(1.12);
250
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
251
+ }
252
+
253
+ .qa-pastille__ecg {
254
+ width: 30px;
255
+ height: 15px;
256
+ }
257
+
258
+ /* Secondary size: half-scale pastille for the integrated second-FAB use. */
259
+ :host([data-mqa-size="sm"]) .qa-pastille__dot { width: 24px; height: 24px; }
260
+ :host([data-mqa-size="sm"]) .qa-pastille__ecg { width: 15px; height: 8px; }
261
+
262
+ .qa-pastille__panel {
263
+ position: absolute;
264
+ bottom: calc(100% + 0.5rem);
265
+ right: 0;
266
+ z-index: var(--mqa-z-panel);
267
+ }
268
+
269
+ /* Color overrides: on colored backgrounds the ECG trace needs a dark stroke. */
270
+ .qa-pastille__dot.qa--green,
271
+ .qa-pastille__dot.qa--yellow,
272
+ .qa-pastille__dot.qa--red {
273
+ color: rgba(0, 0, 0, 0.6);
274
+ }
275
+
276
+ .qa-pastille__dot.qa--gray {
277
+ background: rgba(156, 163, 175, 0.35);
278
+ color: rgba(0, 0, 0, 0.45);
279
+ }
280
+
281
+ @keyframes qa-heartbeat {
282
+ 0%, 100% { transform: scale(1); }
283
+ 8% { transform: scale(1.1); }
284
+ 16% { transform: scale(1); }
285
+ 24% { transform: scale(1.06); }
286
+ }
287
+
288
+ .qa-fade-enter-active,
289
+ .qa-fade-leave-active {
290
+ transition: opacity 0.15s ease, transform 0.15s ease;
291
+ }
292
+ .qa-fade-enter-from,
293
+ .qa-fade-leave-to {
294
+ opacity: 0;
295
+ transform: translateY(4px);
296
+ }
297
+
298
+ @media (prefers-reduced-motion: reduce) {
299
+ .qa-pastille__dot { animation: none; transition: none; }
300
+ }
301
+
302
+ /* \u2500\u2500 Test panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
303
+
304
+ .qa-panel {
305
+ background: var(--mqa-bg);
306
+ border-radius: var(--mqa-radius);
307
+ border: 1px solid var(--mqa-border);
308
+ box-shadow: var(--mqa-shadow-xl);
309
+ width: 340px;
310
+ max-height: 60vh;
311
+ overflow: hidden auto;
312
+ font-size: 0.8125rem;
313
+ color: var(--mqa-text);
314
+ }
315
+
316
+ .qa-panel__empty {
317
+ padding: 0.75rem;
318
+ opacity: 0.7;
319
+ }
320
+
321
+ .qa-panel__list,
322
+ .qa-panel__groups {
323
+ margin: 0;
324
+ padding: 0.25rem 0;
325
+ list-style: none;
326
+ }
327
+
328
+ .qa-panel__foot {
329
+ border-top: 1px solid var(--mqa-border);
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: 0.5rem;
334
+ padding: 0.5rem 0.75rem;
335
+ position: sticky;
336
+ bottom: 0;
337
+ background: var(--mqa-bg);
338
+ }
339
+
340
+ .qa-panel__count {
341
+ flex: 1;
342
+ min-width: 0;
343
+ overflow: hidden;
344
+ white-space: nowrap;
345
+ text-overflow: ellipsis;
346
+ font-size: 0.6875rem;
347
+ opacity: 0.65;
348
+ }
349
+
350
+ .qa-panel__link {
351
+ flex: none;
352
+ font-weight: 600;
353
+ cursor: pointer;
354
+ white-space: nowrap;
355
+ color: var(--mqa-primary);
356
+ background: none;
357
+ border: none;
358
+ font: inherit;
359
+ }
360
+
361
+ .qa-panel__link:hover { text-decoration: underline; }
362
+
363
+ /* \u2500\u2500 Suite groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
364
+
365
+ .qa-suite + .qa-suite { border-top: 1px solid var(--mqa-border); }
366
+
367
+ .qa-suite__head {
368
+ width: 100%;
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 0.5rem;
372
+ padding: 0.5rem 0.75rem;
373
+ cursor: pointer;
374
+ text-align: left;
375
+ background: none;
376
+ border: none;
377
+ font: inherit;
378
+ color: inherit;
379
+ }
380
+
381
+ .qa-suite__head:hover { background: var(--mqa-surface); }
382
+
383
+ .qa-suite__chev {
384
+ flex: none;
385
+ font-size: 0.625rem;
386
+ opacity: 0.6;
387
+ transition: transform 0.12s ease;
388
+ }
389
+
390
+ .qa-suite__chev.is-open { transform: rotate(90deg); }
391
+
392
+ .qa-suite__key {
393
+ flex: none;
394
+ width: 1.25rem;
395
+ height: 1.25rem;
396
+ display: inline-flex;
397
+ align-items: center;
398
+ justify-content: center;
399
+ font-size: 0.6875rem;
400
+ font-weight: 700;
401
+ background: var(--mqa-surface);
402
+ border-radius: 4px;
403
+ }
404
+
405
+ .qa-suite__label {
406
+ flex: 1;
407
+ min-width: 0;
408
+ font-weight: 600;
409
+ overflow: hidden;
410
+ text-overflow: ellipsis;
411
+ white-space: nowrap;
412
+ }
413
+
414
+ .qa-suite__count {
415
+ flex: none;
416
+ font-size: 0.6875rem;
417
+ opacity: 0.55;
418
+ }
419
+
420
+ .qa-suite__list {
421
+ background: var(--mqa-surface);
422
+ margin: 0;
423
+ padding: 0 0 0.25rem;
424
+ list-style: none;
425
+ }
426
+
427
+ /* \u2500\u2500 Scenario rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
428
+
429
+ .qa-row {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: space-between;
433
+ gap: 0.75rem;
434
+ padding: 0.5rem 0.75rem;
435
+ }
436
+
437
+ .qa-row + .qa-row { border-top: 1px solid var(--mqa-border); }
438
+
439
+ .qa-row__title {
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 0.375rem;
443
+ min-width: 0;
444
+ }
445
+
446
+ .qa-row__name {
447
+ flex: 1;
448
+ min-width: 0;
449
+ font-weight: 600;
450
+ line-height: 1.25;
451
+ overflow: hidden;
452
+ text-overflow: ellipsis;
453
+ white-space: nowrap;
454
+ }
455
+
456
+ .qa-suite__list .qa-row { padding-left: 1.5rem; }
457
+
458
+ .qa-tag {
459
+ flex: none;
460
+ font-size: 0.625rem;
461
+ padding: 0 0.25rem;
462
+ text-transform: uppercase;
463
+ letter-spacing: 0.02em;
464
+ opacity: 0.8;
465
+ background: var(--mqa-surface);
466
+ border-radius: 9999px;
467
+ }
468
+
469
+ /* \u2500\u2500 Lane icons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
470
+
471
+ .qa-lanes {
472
+ display: inline-flex;
473
+ align-items: center;
474
+ gap: 0.3125rem;
475
+ flex: none;
476
+ padding: 0.1875rem 0.4375rem;
477
+ border-radius: 9999px;
478
+ background: rgba(17, 24, 39, 0.06);
479
+ }
480
+
481
+ .qa-lanes--sm { font-size: 0.875rem; }
482
+ .qa-lanes--md { font-size: 1rem; }
483
+
484
+ .qa-lanes__icon {
485
+ display: inline-flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ color: rgba(17, 24, 39, 0.4);
489
+ }
490
+
491
+ .qa-lanes__svg {
492
+ width: 1em;
493
+ height: 1em;
494
+ display: block;
495
+ }
496
+
497
+ .qa-lanes__icon.qa--green { color: var(--mqa-green); background: none; }
498
+ .qa-lanes__icon.qa--yellow { color: var(--mqa-yellow); background: none; }
499
+ .qa-lanes__icon.qa--gray { color: rgba(17, 24, 39, 0.4); background: none; }
500
+
501
+ /* \u2500\u2500 Sitemap modal backdrop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
502
+
503
+ .qa-modal__backdrop {
504
+ background-color: rgba(17, 24, 39, 0.4);
505
+ backdrop-filter: blur(1px);
506
+ position: fixed;
507
+ inset: 0;
508
+ z-index: var(--mqa-z-index);
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ padding: 1rem;
513
+ }
514
+
515
+ /* \u2500\u2500 Sitemap modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
516
+
517
+ .qa-modal {
518
+ background: var(--mqa-bg);
519
+ border-radius: var(--mqa-radius);
520
+ box-shadow: var(--mqa-shadow-xl);
521
+ border: 1px solid var(--mqa-border);
522
+ width: min(720px, 92vw);
523
+ max-height: 86vh;
524
+ display: flex;
525
+ flex-direction: column;
526
+ overflow: hidden;
527
+ color: var(--mqa-text);
528
+ }
529
+
530
+ .qa-modal__head {
531
+ background: var(--mqa-primary);
532
+ color: #ffffff;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: space-between;
536
+ padding: 0.5rem 0.75rem;
537
+ }
538
+
539
+ .qa-modal__head h3 {
540
+ font-weight: 700;
541
+ font-size: 1rem;
542
+ margin: 0;
543
+ }
544
+
545
+ .qa-modal__close {
546
+ border: 1px solid rgba(255, 255, 255, 0.3);
547
+ background: transparent;
548
+ color: #ffffff;
549
+ width: 24px;
550
+ height: 24px;
551
+ padding: 0;
552
+ border-radius: var(--mqa-radius);
553
+ cursor: pointer;
554
+ display: grid;
555
+ place-items: center;
556
+ }
557
+
558
+ .qa-modal__close:hover { background: var(--mqa-red); }
559
+
560
+ .qa-modal__freshness {
561
+ background: var(--mqa-surface);
562
+ padding: 0.375rem 0.75rem;
563
+ font-size: 0.6875rem;
564
+ opacity: 0.8;
565
+ }
566
+
567
+ .qa-modal__body {
568
+ overflow: auto;
569
+ padding: 0.5rem 0;
570
+ }
571
+
572
+ /* \u2500\u2500 KPI strip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
573
+
574
+ .qa-kpi {
575
+ display: flex;
576
+ gap: 1.5rem;
577
+ padding: 0.625rem 0.75rem;
578
+ }
579
+
580
+ .qa-kpi__item {
581
+ display: flex;
582
+ flex-direction: column;
583
+ }
584
+
585
+ .qa-kpi__value {
586
+ font-size: 1.25rem;
587
+ font-weight: 700;
588
+ line-height: 1;
589
+ }
590
+
591
+ .qa-kpi__label {
592
+ font-size: 0.6875rem;
593
+ opacity: 0.7;
594
+ }
595
+
596
+ /* \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
597
+
598
+ .qa-tabs {
599
+ border-bottom: 1px solid var(--mqa-border);
600
+ display: flex;
601
+ gap: 0.25rem;
602
+ padding: 0 0.5rem;
603
+ }
604
+
605
+ .qa-tabs__tab {
606
+ padding: 0.375rem 0.75rem;
607
+ font-weight: 600;
608
+ font-size: 0.8125rem;
609
+ border-bottom: 2px solid transparent;
610
+ cursor: pointer;
611
+ opacity: 0.6;
612
+ background: none;
613
+ border-left: none;
614
+ border-right: none;
615
+ border-top: none;
616
+ font: inherit;
617
+ color: inherit;
618
+ }
619
+
620
+ .qa-tabs__tab--active {
621
+ color: var(--mqa-primary);
622
+ border-bottom-color: var(--mqa-primary);
623
+ opacity: 1;
624
+ }
625
+
626
+ /* \u2500\u2500 Sitemap page tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
627
+
628
+ .qa-tree {
629
+ margin: 0;
630
+ padding: 0;
631
+ list-style: none;
632
+ font-size: 0.8125rem;
633
+ }
634
+
635
+ .qa-tree__row {
636
+ display: flex;
637
+ align-items: center;
638
+ gap: 0.375rem;
639
+ padding-top: 0.25rem;
640
+ padding-bottom: 0.25rem;
641
+ padding-right: 0.75rem;
642
+ }
643
+
644
+ .qa-tree__row--here { background: var(--mqa-surface); }
645
+
646
+ .qa-tree__caret {
647
+ width: 1rem;
648
+ text-align: center;
649
+ cursor: pointer;
650
+ opacity: 0.6;
651
+ font-size: 0.6875rem;
652
+ background: none;
653
+ border: none;
654
+ font: inherit;
655
+ color: inherit;
656
+ }
657
+
658
+ .qa-tree__caret--leaf { cursor: default; }
659
+
660
+ .qa-tree__title { flex: 1; }
661
+ .qa-tree__title--structural { font-style: italic; opacity: 0.75; }
662
+
663
+ .qa-tree__here {
664
+ color: var(--mqa-primary);
665
+ font-size: 0.625rem;
666
+ font-weight: 700;
667
+ text-transform: uppercase;
668
+ }
669
+
670
+ .qa-tree__count {
671
+ background: var(--mqa-surface);
672
+ border-radius: 9999px;
673
+ font-size: 0.6875rem;
674
+ padding: 0 0.375rem;
675
+ min-width: 1.25rem;
676
+ text-align: center;
677
+ }
678
+
679
+ /* \u2500\u2500 Scenarios tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
680
+
681
+ .qa-scenarios { padding: 0 0.75rem; }
682
+
683
+ .qa-scenarios__search {
684
+ border: 1px solid var(--mqa-border);
685
+ border-radius: var(--mqa-radius);
686
+ width: 100%;
687
+ height: 2rem;
688
+ padding: 0 0.75rem;
689
+ font-size: 0.8125rem;
690
+ margin-bottom: 0.5rem;
691
+ background: var(--mqa-bg);
692
+ color: var(--mqa-text);
693
+ font-family: inherit;
694
+ box-sizing: border-box;
695
+ }
696
+
697
+ .qa-scenarios__empty {
698
+ opacity: 0.7;
699
+ padding: 0.5rem 0;
700
+ }
701
+
702
+ .qa-scenarios__list {
703
+ margin: 0;
704
+ padding: 0;
705
+ list-style: none;
706
+ }
707
+
708
+ .qa-scenarios__item {
709
+ border-top: 1px solid var(--mqa-border);
710
+ padding: 0.5rem 0;
711
+ }
712
+
713
+ .qa-scenarios__line {
714
+ display: flex;
715
+ align-items: center;
716
+ gap: 0.375rem;
717
+ }
718
+
719
+ .qa-scenarios__title {
720
+ flex: 1;
721
+ font-weight: 600;
722
+ font-size: 0.8125rem;
723
+ }
724
+
725
+ .qa-scenarios__pages {
726
+ display: flex;
727
+ flex-wrap: wrap;
728
+ gap: 0.75rem;
729
+ font-size: 0.6875rem;
730
+ opacity: 0.7;
731
+ margin-top: 0.125rem;
732
+ padding-left: 1rem;
733
+ }
734
+
735
+ /* \u2500\u2500 Scenario dot (color indicator) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
736
+
737
+ .qa-dot {
738
+ width: 0.625rem;
739
+ height: 0.625rem;
740
+ border-radius: 9999px;
741
+ flex: none;
742
+ background: rgba(17, 24, 39, 0.6);
743
+ }
744
+
745
+ /* Override modifier background for dot \u2014 dot uses background not text color. */
746
+ .qa-dot.qa--green { background: var(--mqa-green); }
747
+ .qa-dot.qa--yellow { background: var(--mqa-yellow); }
748
+ .qa-dot.qa--red { background: var(--mqa-red); }
749
+ .qa-dot.qa--gray { background: var(--mqa-gray); }
750
+
751
+ /* \u2500\u2500 Suite key badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
752
+
753
+ .qa-suitekey {
754
+ flex: none;
755
+ font-size: 0.625rem;
756
+ font-weight: 700;
757
+ padding: 0 0.3125rem;
758
+ line-height: 1.4;
759
+ background: var(--mqa-surface);
760
+ border-radius: 4px;
761
+ }
762
+ `;
763
+
764
+ // src/qa-meter/Pastille.tsx
765
+ import { h } from "preact";
766
+
767
+ // src/qa-meter/ui.ts
768
+ var QA_COLOR_MODIFIER = {
769
+ green: "qa--green",
770
+ yellow: "qa--yellow",
771
+ red: "qa--red",
772
+ gray: "qa--gray"
773
+ };
774
+ var QA_COLOR_LABEL_KEY = {
775
+ green: "color.green",
776
+ yellow: "color.yellow",
777
+ red: "color.red",
778
+ gray: "color.gray"
779
+ };
780
+ var QA_PRIORITY_MODIFIER = {
781
+ P0: "qa-prio--p0",
782
+ P1: "qa-prio--p1",
783
+ P2: "qa-prio--p2",
784
+ P3: "qa-prio--p3"
785
+ };
786
+ var QA_CASE_STATUS_LABEL_KEY = {
787
+ covered: "case.covered",
788
+ manual: "case.manual",
789
+ blocked: "case.blocked",
790
+ "open-question": "case.openQuestion"
791
+ };
792
+ var QA_GROUNDING_LABEL_KEY = {
793
+ verified: "grounding.verified",
794
+ "confirm-live": "grounding.confirmLive",
795
+ "product-question": "grounding.productQuestion"
796
+ };
797
+ function botTriColor(lane) {
798
+ if (lane.status === "passing") {
799
+ return "green";
800
+ }
801
+ if (lane.status === "failing" || lane.status === "flaky") {
802
+ return "yellow";
803
+ }
804
+ return "gray";
805
+ }
806
+ function humanTriColor(lane) {
807
+ if (lane.rejected) {
808
+ return "yellow";
809
+ }
810
+ if (lane.validated) {
811
+ return lane.stale ? "gray" : "green";
812
+ }
813
+ return "gray";
814
+ }
815
+ function aggregateTri(colors) {
816
+ if (colors.some((c) => c === "yellow")) {
817
+ return "yellow";
818
+ }
819
+ if (colors.length > 0 && colors.every((c) => c === "green")) {
820
+ return "green";
821
+ }
822
+ return "gray";
823
+ }
824
+ function formatQaDate(iso) {
825
+ if (!iso) {
826
+ return "";
827
+ }
828
+ const date = new Date(iso);
829
+ if (Number.isNaN(date.getTime())) {
830
+ return "";
831
+ }
832
+ return new Intl.DateTimeFormat("fr-CA", {
833
+ year: "numeric",
834
+ month: "2-digit",
835
+ day: "2-digit",
836
+ hour: "2-digit",
837
+ minute: "2-digit"
838
+ }).format(date);
839
+ }
840
+ function shortVersion(version) {
841
+ if (!version) {
842
+ return "";
843
+ }
844
+ return version.length > 19 ? `${version.slice(0, 19)}\u2026` : version;
845
+ }
846
+
847
+ // src/qa-meter/Pastille.tsx
848
+ function Pastille({ color, state, strings, onOpenModal, onHover }) {
849
+ const base = strings["pastille.label"];
850
+ let detail = strings[QA_COLOR_LABEL_KEY[color]];
851
+ if (state === "no-tests") {
852
+ detail = strings["panel.noTests"];
853
+ } else if (state === "unknown-page") {
854
+ detail = strings["pastille.unknownPage"];
855
+ }
856
+ const label = `${base} \u2014 ${detail}`;
857
+ return h(
858
+ "div",
859
+ { class: "qa-pastille" },
860
+ h(
861
+ "button",
862
+ {
863
+ type: "button",
864
+ "data-testid": "qa-pastille",
865
+ class: `qa-pastille__dot ${QA_COLOR_MODIFIER[color]}`,
866
+ "aria-label": label,
867
+ title: label,
868
+ onClick: onOpenModal,
869
+ onMouseEnter: onHover,
870
+ onFocus: onHover
871
+ },
872
+ h(
873
+ "svg",
874
+ { class: "qa-pastille__ecg", viewBox: "0 0 24 12", "aria-hidden": "true" },
875
+ h("polyline", {
876
+ points: "1,6 7,6 9,2 12,10 15,4 17,6 23,6",
877
+ fill: "none",
878
+ stroke: "currentColor",
879
+ "stroke-width": "1.5",
880
+ "stroke-linejoin": "round",
881
+ "stroke-linecap": "round"
882
+ })
883
+ )
884
+ )
885
+ );
886
+ }
887
+
888
+ // src/qa-meter/TestPanel.tsx
889
+ import { h as h3 } from "preact";
890
+ import { useState } from "preact/hooks";
891
+
892
+ // src/qa-meter/LaneIcons.tsx
893
+ import { h as h2 } from "preact";
894
+ var TRI_MODIFIER = {
895
+ green: "qa--green",
896
+ yellow: "qa--yellow",
897
+ gray: "qa--gray"
898
+ };
899
+ function LaneIcons({ dev, bot, human, strings }) {
900
+ return h2(
901
+ "span",
902
+ { class: "qa-lanes", "data-testid": "qa-lane-icons" },
903
+ // dev — chevrons </>
904
+ h2(
905
+ "span",
906
+ {
907
+ class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[dev]}`,
908
+ title: strings["lane.code"],
909
+ "aria-label": strings["lane.code"],
910
+ "data-testid": "qa-lane-dev",
911
+ "data-state": dev
912
+ },
913
+ h2(
914
+ "svg",
915
+ { class: "qa-lanes__svg", viewBox: "0 0 24 24", "aria-hidden": "true" },
916
+ h2("path", {
917
+ d: "M8 7l-5 5 5 5M16 7l5 5-5 5",
918
+ fill: "none",
919
+ stroke: "currentColor",
920
+ "stroke-width": "2.2",
921
+ "stroke-linecap": "round",
922
+ "stroke-linejoin": "round"
923
+ })
924
+ )
925
+ ),
926
+ // bot — robot head
927
+ h2(
928
+ "span",
929
+ {
930
+ class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[bot]}`,
931
+ title: strings["lane.bot"],
932
+ "aria-label": strings["lane.bot"],
933
+ "data-testid": "qa-lane-bot",
934
+ "data-state": bot
935
+ },
936
+ h2(
937
+ "svg",
938
+ {
939
+ class: "qa-lanes__svg",
940
+ viewBox: "0 0 24 24",
941
+ "aria-hidden": "true",
942
+ fill: "none",
943
+ stroke: "currentColor",
944
+ "stroke-width": "2",
945
+ "stroke-linecap": "round",
946
+ "stroke-linejoin": "round"
947
+ },
948
+ h2("rect", { x: "4.5", y: "8.5", width: "15", height: "10.5", rx: "2.5" }),
949
+ h2("path", { d: "M12 4.5v4" }),
950
+ h2("circle", { cx: "12", cy: "3.4", r: "1.3", fill: "currentColor", stroke: "none" }),
951
+ h2("path", { d: "M2.5 12.5v3M21.5 12.5v3" }),
952
+ h2("circle", { cx: "9", cy: "13.5", r: "1.2", fill: "currentColor", stroke: "none" }),
953
+ h2("circle", { cx: "15", cy: "13.5", r: "1.2", fill: "currentColor", stroke: "none" })
954
+ )
955
+ ),
956
+ // client — person
957
+ h2(
958
+ "span",
959
+ {
960
+ class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[human]}`,
961
+ title: strings["lane.human"],
962
+ "aria-label": strings["lane.human"],
963
+ "data-testid": "qa-lane-client",
964
+ "data-state": human
965
+ },
966
+ h2(
967
+ "svg",
968
+ {
969
+ class: "qa-lanes__svg",
970
+ viewBox: "0 0 24 24",
971
+ "aria-hidden": "true",
972
+ fill: "none",
973
+ stroke: "currentColor",
974
+ "stroke-width": "2",
975
+ "stroke-linecap": "round",
976
+ "stroke-linejoin": "round"
977
+ },
978
+ h2("circle", { cx: "12", cy: "8", r: "3.6" }),
979
+ h2("path", { d: "M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6" })
980
+ )
981
+ )
982
+ );
983
+ }
984
+
985
+ // src/qa-meter/TestPanel.tsx
986
+ function laneTriColor(scenario) {
987
+ return {
988
+ dev: humanTriColor(scenario.dev),
989
+ bot: botTriColor(scenario.automated),
990
+ human: humanTriColor(scenario.client)
991
+ };
992
+ }
993
+ function groupScenarios(scenarios) {
994
+ const map = /* @__PURE__ */ new Map();
995
+ for (const scenario of scenarios) {
996
+ const key = scenario.group?.key ?? "\xB7";
997
+ const bucket = map.get(key) ?? [];
998
+ bucket.push(scenario);
999
+ map.set(key, bucket);
1000
+ }
1001
+ const keys = [...map.keys()].sort((a, b) => a === "X" ? 1 : b === "X" ? -1 : a.localeCompare(b));
1002
+ return keys.map((key) => {
1003
+ const bucket = map.get(key);
1004
+ return { key, label: bucket[0]?.group?.label ?? "", scenarios: bucket };
1005
+ });
1006
+ }
1007
+ function interpolate(template, params) {
1008
+ return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? ""));
1009
+ }
1010
+ function ScenarioRow({ scenario, strings }) {
1011
+ const lanes = laneTriColor(scenario);
1012
+ const provenance = scenario.automated.provenance;
1013
+ return h3(
1014
+ "li",
1015
+ { class: "qa-row", "data-testid": "qa-scenario-row" },
1016
+ h3(
1017
+ "span",
1018
+ { class: "qa-row__title" },
1019
+ scenario.priority ? h3(
1020
+ "span",
1021
+ { class: `qa-tag ${QA_PRIORITY_MODIFIER[scenario.priority]}`, "data-testid": "qa-priority" },
1022
+ scenario.priority
1023
+ ) : null,
1024
+ h3("span", { class: "qa-row__name" }, scenario.title),
1025
+ scenario.source === "testgen" ? h3("span", { class: "qa-tag", title: strings["source.testgenHint"] }, strings["source.testgen"]) : null,
1026
+ scenario.caseStatus ? h3("span", { class: "qa-tag" }, strings[QA_CASE_STATUS_LABEL_KEY[scenario.caseStatus]]) : null,
1027
+ scenario.grounding ? h3("span", { class: "qa-tag" }, strings[QA_GROUNDING_LABEL_KEY[scenario.grounding]]) : null,
1028
+ provenance ? h3("span", { class: "qa-tag", "data-testid": "qa-provenance" }, provenance) : null
1029
+ ),
1030
+ h3(LaneIcons, { dev: lanes.dev, bot: lanes.bot, human: lanes.human, strings })
1031
+ );
1032
+ }
1033
+ function TestPanel({ page, state, meta, strings }) {
1034
+ const [expanded, setExpanded] = useState(/* @__PURE__ */ new Set());
1035
+ const toggleGroup = (key) => {
1036
+ setExpanded((prev) => {
1037
+ const next = new Set(prev);
1038
+ if (next.has(key)) {
1039
+ next.delete(key);
1040
+ } else {
1041
+ next.add(key);
1042
+ }
1043
+ return next;
1044
+ });
1045
+ };
1046
+ const freshness = interpolate(strings["panel.freshness"], {
1047
+ date: formatQaDate(meta.generated_at),
1048
+ version: shortVersion(meta.app_version),
1049
+ env: meta.environment
1050
+ });
1051
+ let body;
1052
+ if (state === "no-tests") {
1053
+ body = h3("p", { class: "qa-panel__empty" }, strings["panel.noTests"]);
1054
+ } else if (state === "unknown-page") {
1055
+ body = h3("p", { class: "qa-panel__empty" }, strings["panel.unknownPage"]);
1056
+ } else {
1057
+ const scenarios = page?.scenarios ?? [];
1058
+ const hasGroups = scenarios.some((s) => s.group);
1059
+ if (scenarios.length === 0) {
1060
+ body = h3("p", { class: "qa-panel__empty" }, strings["panel.noScenarios"]);
1061
+ } else if (!hasGroups) {
1062
+ body = h3(
1063
+ "ul",
1064
+ { class: "qa-panel__list" },
1065
+ scenarios.map((scenario) => h3(ScenarioRow, { key: scenario.id, scenario, strings }))
1066
+ );
1067
+ } else {
1068
+ const groups = groupScenarios(scenarios);
1069
+ body = h3(
1070
+ "ul",
1071
+ { class: "qa-panel__groups", "data-testid": "qa-suite-groups" },
1072
+ groups.map((group) => {
1073
+ const isOpen = expanded.has(group.key);
1074
+ const agg = {
1075
+ dev: aggregateTri(group.scenarios.map((s) => humanTriColor(s.dev))),
1076
+ bot: aggregateTri(group.scenarios.map((s) => botTriColor(s.automated))),
1077
+ human: aggregateTri(group.scenarios.map((s) => humanTriColor(s.client)))
1078
+ };
1079
+ return h3(
1080
+ "li",
1081
+ { key: group.key, class: "qa-suite", "data-testid": "qa-panel-group" },
1082
+ h3(
1083
+ "button",
1084
+ {
1085
+ type: "button",
1086
+ class: "qa-suite__head",
1087
+ "aria-expanded": isOpen,
1088
+ "data-testid": "qa-suite-group",
1089
+ onClick: () => toggleGroup(group.key)
1090
+ },
1091
+ h3(
1092
+ "span",
1093
+ { class: `qa-suite__chev ${isOpen ? "is-open" : ""}`, "aria-hidden": "true" },
1094
+ "\u25B8"
1095
+ ),
1096
+ h3("span", { class: "qa-suite__key" }, group.key),
1097
+ h3("span", { class: "qa-suite__label" }, group.label),
1098
+ h3(
1099
+ "span",
1100
+ { class: "qa-suite__count" },
1101
+ interpolate(strings["suite.cases"], { count: group.scenarios.length })
1102
+ ),
1103
+ h3(LaneIcons, { dev: agg.dev, bot: agg.bot, human: agg.human, strings })
1104
+ ),
1105
+ isOpen ? h3(
1106
+ "ul",
1107
+ { class: "qa-suite__list" },
1108
+ group.scenarios.map(
1109
+ (scenario) => h3(ScenarioRow, { key: scenario.id, scenario, strings })
1110
+ )
1111
+ ) : null
1112
+ );
1113
+ })
1114
+ );
1115
+ }
1116
+ }
1117
+ return h3(
1118
+ "section",
1119
+ { class: "qa-panel", "data-testid": "qa-panel" },
1120
+ body,
1121
+ h3(
1122
+ "footer",
1123
+ { class: "qa-panel__foot" },
1124
+ h3(
1125
+ "span",
1126
+ { class: "qa-panel__count", title: freshness, "data-testid": "qa-panel-freshness" },
1127
+ freshness
1128
+ )
1129
+ )
1130
+ );
1131
+ }
1132
+
1133
+ // src/qa-meter/SitemapModal.tsx
1134
+ import { h as h4 } from "preact";
1135
+ import { useEffect, useMemo, useState as useState2 } from "preact/hooks";
1136
+ function buildTree(nodes) {
1137
+ const childrenByParent = /* @__PURE__ */ new Map();
1138
+ for (const node of nodes) {
1139
+ const bucket = childrenByParent.get(node.parent_id) ?? [];
1140
+ bucket.push(node);
1141
+ childrenByParent.set(node.parent_id, bucket);
1142
+ }
1143
+ for (const bucket of childrenByParent.values()) {
1144
+ bucket.sort((a, b) => a.order - b.order);
1145
+ }
1146
+ const out = [];
1147
+ const visit = (parentId, depth) => {
1148
+ for (const node of childrenByParent.get(parentId) ?? []) {
1149
+ const children = childrenByParent.get(node.id) ?? [];
1150
+ out.push({ node, depth, hasChildren: children.length > 0 });
1151
+ if (children.length > 0) {
1152
+ visit(node.id, depth + 1);
1153
+ }
1154
+ }
1155
+ };
1156
+ visit(null, 0);
1157
+ return out;
1158
+ }
1159
+ function dedupeScenarios(artifact) {
1160
+ const byKey = /* @__PURE__ */ new Map();
1161
+ for (const page of artifact.pages) {
1162
+ for (const scenario of page.scenarios) {
1163
+ const key = scenario.external_key ?? scenario.id;
1164
+ const entry = byKey.get(key) ?? { scenario, primaries: /* @__PURE__ */ new Set(), traversed: /* @__PURE__ */ new Set() };
1165
+ for (const ref of scenario.pages ?? []) {
1166
+ ;
1167
+ (ref.role === "primary" ? entry.primaries : entry.traversed).add(ref.pattern);
1168
+ }
1169
+ byKey.set(key, entry);
1170
+ }
1171
+ }
1172
+ const rank = (p) => p ? Number(p.slice(1)) : 9;
1173
+ return [...byKey.values()].sort((a, b) => {
1174
+ const ga = a.scenario.group?.key ?? "~";
1175
+ const gb = b.scenario.group?.key ?? "~";
1176
+ if (ga !== gb) {
1177
+ return ga.localeCompare(gb);
1178
+ }
1179
+ return rank(a.scenario.priority) - rank(b.scenario.priority) || a.scenario.title.localeCompare(b.scenario.title);
1180
+ });
1181
+ }
1182
+ function interpolate2(template, params) {
1183
+ return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? ""));
1184
+ }
1185
+ function formatPct(pct) {
1186
+ return `${Math.round(pct)}%`;
1187
+ }
1188
+ function SitemapModal({ artifact, currentPattern, strings, onClose }) {
1189
+ const [activeTab, setActiveTab] = useState2("pages");
1190
+ const [search, setSearch] = useState2("");
1191
+ useEffect(() => {
1192
+ const onKeydown = (event) => {
1193
+ if (event.key === "Escape") {
1194
+ onClose();
1195
+ }
1196
+ };
1197
+ window.addEventListener("keydown", onKeydown);
1198
+ return () => window.removeEventListener("keydown", onKeydown);
1199
+ }, [onClose]);
1200
+ const here = currentPattern ? normalizePattern(currentPattern) : null;
1201
+ const tree = useMemo(() => buildTree(artifact.sitemap), [artifact]);
1202
+ const cases = useMemo(() => dedupeScenarios(artifact), [artifact]);
1203
+ const titleByPattern = useMemo(() => {
1204
+ const map = /* @__PURE__ */ new Map();
1205
+ for (const node of artifact.sitemap) {
1206
+ map.set(node.path_pattern, node.title);
1207
+ }
1208
+ return map;
1209
+ }, [artifact]);
1210
+ const patternTitle = (pattern) => titleByPattern.get(pattern) ?? pattern;
1211
+ const query = search.trim().toLowerCase();
1212
+ const filteredCases = query ? cases.filter(
1213
+ (c) => c.scenario.title.toLowerCase().includes(query) || (c.scenario.group?.label ?? "").toLowerCase().includes(query)
1214
+ ) : cases;
1215
+ const freshness = interpolate2(strings["panel.freshness"], {
1216
+ date: formatQaDate(artifact.generated_at),
1217
+ version: shortVersion(artifact.app_version),
1218
+ env: artifact.environment
1219
+ });
1220
+ return h4(
1221
+ "div",
1222
+ {
1223
+ class: "qa-modal__backdrop",
1224
+ "data-testid": "qa-sitemap-modal",
1225
+ onClick: (e) => {
1226
+ if (e.target === e.currentTarget) onClose();
1227
+ }
1228
+ },
1229
+ h4(
1230
+ "div",
1231
+ { class: "qa-modal", role: "dialog", "aria-modal": "true" },
1232
+ h4(
1233
+ "header",
1234
+ { class: "qa-modal__head" },
1235
+ h4("h3", null, strings["modal.title"]),
1236
+ h4(
1237
+ "button",
1238
+ {
1239
+ type: "button",
1240
+ class: "qa-modal__close",
1241
+ title: strings["modal.close"],
1242
+ "aria-label": strings["modal.close"],
1243
+ "data-testid": "qa-modal-close",
1244
+ onClick: onClose
1245
+ },
1246
+ "\xD7"
1247
+ )
1248
+ ),
1249
+ h4("div", { class: "qa-modal__freshness" }, freshness),
1250
+ // Dual metric (doc 01 D12) — rendered as rounded percentages.
1251
+ h4(
1252
+ "div",
1253
+ { class: "qa-kpi", "data-testid": "qa-kpi" },
1254
+ h4(
1255
+ "div",
1256
+ { class: "qa-kpi__item" },
1257
+ h4("span", { class: "qa-kpi__value" }, formatPct(artifact.metric.pages_with_scenarios_pct)),
1258
+ h4("span", { class: "qa-kpi__label" }, strings["modal.kpiPagesCovered"])
1259
+ ),
1260
+ h4(
1261
+ "div",
1262
+ { class: "qa-kpi__item" },
1263
+ h4("span", { class: "qa-kpi__value" }, formatPct(artifact.metric.scenarios_passing_pct)),
1264
+ h4("span", { class: "qa-kpi__label" }, strings["modal.kpiScenariosPassing"])
1265
+ )
1266
+ ),
1267
+ h4(
1268
+ "nav",
1269
+ { class: "qa-tabs" },
1270
+ h4(
1271
+ "button",
1272
+ {
1273
+ type: "button",
1274
+ class: `qa-tabs__tab ${activeTab === "pages" ? "qa-tabs__tab--active" : ""}`,
1275
+ "data-testid": "qa-tab-pages",
1276
+ onClick: () => setActiveTab("pages")
1277
+ },
1278
+ strings["modal.tabPages"]
1279
+ ),
1280
+ h4(
1281
+ "button",
1282
+ {
1283
+ type: "button",
1284
+ class: `qa-tabs__tab ${activeTab === "scenarios" ? "qa-tabs__tab--active" : ""}`,
1285
+ "data-testid": "qa-tab-scenarios",
1286
+ onClick: () => setActiveTab("scenarios")
1287
+ },
1288
+ strings["modal.tabScenarios"]
1289
+ )
1290
+ ),
1291
+ h4(
1292
+ "div",
1293
+ { class: "qa-modal__body" },
1294
+ activeTab === "pages" ? h4(
1295
+ "ul",
1296
+ { class: "qa-tree" },
1297
+ tree.map((row) => {
1298
+ const isHere = here !== null && row.node.path_pattern === here;
1299
+ return h4(
1300
+ "li",
1301
+ {
1302
+ key: row.node.id,
1303
+ class: `qa-tree__row ${isHere ? "qa-tree__row--here" : ""}`,
1304
+ "data-testid": isHere ? "qa-tree-current" : "qa-sitemap-node",
1305
+ style: { paddingLeft: `${0.5 + row.depth * 1.1}rem` }
1306
+ },
1307
+ h4(
1308
+ "span",
1309
+ {
1310
+ class: `qa-dot ${QA_COLOR_MODIFIER[row.node.color]}`,
1311
+ "aria-hidden": "true"
1312
+ }
1313
+ ),
1314
+ h4(
1315
+ "span",
1316
+ {
1317
+ class: `qa-tree__title ${row.node.structural ? "qa-tree__title--structural" : ""}`
1318
+ },
1319
+ row.node.title || row.node.path_pattern
1320
+ ),
1321
+ isHere ? h4("span", { class: "qa-tree__here" }, ` ${row.node.path_pattern} `) : null,
1322
+ isHere ? h4("span", { class: "qa-tree__here" }, strings["modal.youAreHere"]) : null,
1323
+ h4("span", { class: "qa-tree__count" }, String(row.node.counts.scenarios))
1324
+ );
1325
+ })
1326
+ ) : h4(
1327
+ "div",
1328
+ { class: "qa-scenarios" },
1329
+ h4("input", {
1330
+ type: "search",
1331
+ class: "qa-scenarios__search",
1332
+ placeholder: strings["modal.search"],
1333
+ "data-testid": "qa-scenarios-search",
1334
+ value: search,
1335
+ onInput: (e) => setSearch(e.target.value)
1336
+ }),
1337
+ filteredCases.length === 0 ? h4("p", { class: "qa-scenarios__empty" }, strings["modal.noMatch"]) : h4(
1338
+ "ul",
1339
+ { class: "qa-scenarios__list" },
1340
+ filteredCases.map((entry) => {
1341
+ const scenario = entry.scenario;
1342
+ const lanes = {
1343
+ dev: humanTriColor(scenario.dev),
1344
+ bot: botTriColor(scenario.automated),
1345
+ human: humanTriColor(scenario.client)
1346
+ };
1347
+ const traversed = [...entry.traversed].map(patternTitle);
1348
+ return h4(
1349
+ "li",
1350
+ {
1351
+ key: scenario.external_key ?? scenario.id,
1352
+ class: "qa-scenarios__item",
1353
+ "data-testid": "qa-scenario-row"
1354
+ },
1355
+ h4(
1356
+ "div",
1357
+ { class: "qa-scenarios__line" },
1358
+ h4("span", { class: `qa-dot ${QA_COLOR_MODIFIER[scenario.color]}`, "aria-hidden": "true" }),
1359
+ scenario.group?.key ? h4("span", { class: "qa-suitekey" }, scenario.group.key) : null,
1360
+ h4("span", { class: "qa-scenarios__title" }, scenario.title),
1361
+ scenario.source === "testgen" ? h4("span", { class: "qa-tag" }, strings["source.testgen"]) : null,
1362
+ h4(LaneIcons, { dev: lanes.dev, bot: lanes.bot, human: lanes.human, strings })
1363
+ ),
1364
+ h4(
1365
+ "div",
1366
+ { class: "qa-scenarios__pages" },
1367
+ entry.primaries.size ? h4(
1368
+ "span",
1369
+ { class: "qa-scenarios__primary" },
1370
+ interpolate2(strings["modal.appliesTo"], { count: entry.primaries.size })
1371
+ ) : null,
1372
+ traversed.length ? h4(
1373
+ "span",
1374
+ { class: "qa-scenarios__traversed" },
1375
+ `${strings["modal.traversed"]}: ${traversed.join(", ")}`
1376
+ ) : null
1377
+ )
1378
+ );
1379
+ })
1380
+ )
1381
+ )
1382
+ )
1383
+ )
1384
+ );
1385
+ }
1386
+
1387
+ // src/qa-meter/mount.tsx
1388
+ var LOCATION_CHANGE_EVENT = "mqa:locationchange";
1389
+ var historyPatched = false;
1390
+ var historyRefCount = 0;
1391
+ var originalPushState = null;
1392
+ var originalReplaceState = null;
1393
+ function patchHistory() {
1394
+ historyRefCount++;
1395
+ if (historyPatched) return;
1396
+ historyPatched = true;
1397
+ originalPushState = history.pushState;
1398
+ originalReplaceState = history.replaceState;
1399
+ const fire = () => {
1400
+ window.dispatchEvent(new CustomEvent(LOCATION_CHANGE_EVENT));
1401
+ };
1402
+ history.pushState = function patchedPushState(...args) {
1403
+ const ret = originalPushState.apply(this, args);
1404
+ fire();
1405
+ return ret;
1406
+ };
1407
+ history.replaceState = function patchedReplaceState(...args) {
1408
+ const ret = originalReplaceState.apply(this, args);
1409
+ fire();
1410
+ return ret;
1411
+ };
1412
+ }
1413
+ function unpatchHistory() {
1414
+ if (historyRefCount > 0) historyRefCount--;
1415
+ if (historyRefCount > 0 || !historyPatched) return;
1416
+ historyPatched = false;
1417
+ if (originalPushState) history.pushState = originalPushState;
1418
+ if (originalReplaceState) history.replaceState = originalReplaceState;
1419
+ originalPushState = null;
1420
+ originalReplaceState = null;
1421
+ }
1422
+ var QA_ARTIFACT_FORMAT = 1;
1423
+ function isValidArtifact(value) {
1424
+ return typeof value === "object" && value !== null && value.format === QA_ARTIFACT_FORMAT;
1425
+ }
1426
+ var warnedBadFormat = false;
1427
+ function warnBadFormatOnce() {
1428
+ if (warnedBadFormat) return;
1429
+ warnedBadFormat = true;
1430
+ if (typeof console !== "undefined") {
1431
+ console.warn(
1432
+ "[mqa] QA Meter artifact has an unsupported `format` \u2014 refusing to mount."
1433
+ );
1434
+ }
1435
+ }
1436
+ var NOOP_HANDLE = {
1437
+ open() {
1438
+ },
1439
+ close() {
1440
+ },
1441
+ dispose() {
1442
+ }
1443
+ };
1444
+ function patternsFromArtifact(artifact) {
1445
+ const set = /* @__PURE__ */ new Set();
1446
+ for (const page of artifact.pages) {
1447
+ if (page.page) set.add(page.page.path_pattern);
1448
+ }
1449
+ for (const node of artifact.sitemap) {
1450
+ set.add(node.path_pattern);
1451
+ }
1452
+ return [...set];
1453
+ }
1454
+ var UNKNOWN = { state: "unknown-page", color: "gray", pattern: null, page: null };
1455
+ function resolveCurrent(artifact, pattern) {
1456
+ if (!artifact || !pattern) return UNKNOWN;
1457
+ const page = artifact.pages.find((p) => p.page && p.page.path_pattern === pattern);
1458
+ if (page && page.page) {
1459
+ return { state: "known", color: page.color, pattern, page };
1460
+ }
1461
+ const inSitemap = artifact.sitemap.some((n) => n.path_pattern === pattern);
1462
+ if (inSitemap) {
1463
+ return { state: "no-tests", color: "gray", pattern, page: null };
1464
+ }
1465
+ return { state: "unknown-page", color: "gray", pattern, page: null };
1466
+ }
1467
+ function createQaMeter(options) {
1468
+ const { source } = options;
1469
+ const strings = resolveStrings(options.locale ?? "fr");
1470
+ const inlineArtifact = typeof source === "object" ? source : null;
1471
+ if (inlineArtifact && !isValidArtifact(inlineArtifact)) {
1472
+ warnBadFormatOnce();
1473
+ return NOOP_HANDLE;
1474
+ }
1475
+ let artifact = inlineArtifact;
1476
+ let loadStarted = false;
1477
+ let loadPromise = null;
1478
+ function load() {
1479
+ if (artifact || loadStarted) return;
1480
+ loadStarted = true;
1481
+ const got = typeof source === "function" ? source() : typeof source === "string" ? fetch(source).then((r) => r.json()) : Promise.resolve(source);
1482
+ loadPromise = got.then((value) => {
1483
+ if (isValidArtifact(value)) {
1484
+ artifact = value;
1485
+ forceRender();
1486
+ } else {
1487
+ warnBadFormatOnce();
1488
+ }
1489
+ }).catch(() => {
1490
+ });
1491
+ }
1492
+ const host = document.createElement("div");
1493
+ host.setAttribute("data-mqa-host", "");
1494
+ host.setAttribute("data-mqa-size", options.size ?? "md");
1495
+ if (options.position?.right !== void 0) {
1496
+ host.style.setProperty("--mqa-right", `${options.position.right}px`);
1497
+ }
1498
+ if (options.position?.bottom !== void 0) {
1499
+ host.style.setProperty("--mqa-bottom", `${options.position.bottom}px`);
1500
+ }
1501
+ document.body.appendChild(host);
1502
+ const shadow = host.attachShadow({ mode: "open" });
1503
+ const style = document.createElement("style");
1504
+ style.textContent = QA_METER_STYLES;
1505
+ shadow.appendChild(style);
1506
+ const mountPoint = document.createElement("div");
1507
+ shadow.appendChild(mountPoint);
1508
+ let forceRender = () => {
1509
+ };
1510
+ function getCurrentPattern() {
1511
+ if (options.getCurrentPage) return options.getCurrentPage();
1512
+ if (!artifact) return null;
1513
+ return resolvePage(window.location.pathname, patternsFromArtifact(artifact));
1514
+ }
1515
+ function Container() {
1516
+ const [tick, setTick] = useState3(0);
1517
+ void tick;
1518
+ const bump = useCallback(() => setTick((n) => n + 1), []);
1519
+ forceRender = bump;
1520
+ const resolved = resolveCurrent(artifact, getCurrentPattern());
1521
+ const meta = artifact ? {
1522
+ generated_at: artifact.generated_at,
1523
+ app_version: artifact.app_version,
1524
+ environment: artifact.environment
1525
+ } : { generated_at: "", app_version: "", environment: "" };
1526
+ const onHover = useCallback(() => {
1527
+ load();
1528
+ }, []);
1529
+ const onOpenModal = useCallback(() => {
1530
+ load();
1531
+ uiOpen = true;
1532
+ bump();
1533
+ }, []);
1534
+ const closeModal = useCallback(() => {
1535
+ uiOpen = false;
1536
+ bump();
1537
+ }, []);
1538
+ useEffect2(() => {
1539
+ const onNav = () => bump();
1540
+ window.addEventListener("popstate", onNav);
1541
+ window.addEventListener(LOCATION_CHANGE_EVENT, onNav);
1542
+ return () => {
1543
+ window.removeEventListener("popstate", onNav);
1544
+ window.removeEventListener(LOCATION_CHANGE_EVENT, onNav);
1545
+ };
1546
+ }, []);
1547
+ return h5(
1548
+ "div",
1549
+ {
1550
+ class: "qa-pastille-wrap",
1551
+ onMouseLeave: () => {
1552
+ uiHover = false;
1553
+ bump();
1554
+ }
1555
+ },
1556
+ uiHover ? h5(TestPanel, { page: resolved.page, state: resolved.state, meta, strings }) : null,
1557
+ h5(Pastille, {
1558
+ color: resolved.color,
1559
+ state: resolved.state,
1560
+ strings,
1561
+ onHover: () => {
1562
+ uiHover = true;
1563
+ onHover();
1564
+ bump();
1565
+ },
1566
+ onOpenModal
1567
+ }),
1568
+ uiOpen && artifact ? h5(SitemapModal, {
1569
+ artifact,
1570
+ currentPattern: resolved.pattern,
1571
+ strings,
1572
+ onClose: closeModal
1573
+ }) : null
1574
+ );
1575
+ }
1576
+ let uiOpen = false;
1577
+ let uiHover = false;
1578
+ function mountTree() {
1579
+ render(h5(Container, {}), mountPoint);
1580
+ }
1581
+ mountTree();
1582
+ patchHistory();
1583
+ let disposed = false;
1584
+ return {
1585
+ open() {
1586
+ if (disposed) return;
1587
+ load();
1588
+ uiOpen = true;
1589
+ forceRender();
1590
+ },
1591
+ close() {
1592
+ if (disposed) return;
1593
+ uiOpen = false;
1594
+ forceRender();
1595
+ },
1596
+ dispose() {
1597
+ if (disposed) return;
1598
+ disposed = true;
1599
+ render(null, mountPoint);
1600
+ host.remove();
1601
+ unpatchHistory();
1602
+ void loadPromise;
1603
+ }
1604
+ };
1605
+ }
1606
+
1607
+ // src/qa-meter/index.ts
1608
+ function createQaMeter2(options) {
1609
+ return createQaMeter(options);
1610
+ }
1611
+ export {
1612
+ createQaMeter2 as createQaMeter
1613
+ };
1614
+ //# sourceMappingURL=qa-meter.mjs.map