@cat-factory/app 0.137.2 → 0.138.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.
- package/app/components/followUp/FollowUpWindow.vue +174 -200
- package/app/components/forkDecision/ForkDecisionWindow.vue +250 -275
- package/app/components/gates/GateResultView.vue +309 -333
- package/app/components/humanTest/HumanTestWindow.vue +212 -236
- package/app/components/media/ArtifactLightbox.vue +20 -19
- package/app/components/panels/GenericStructuredResultView.vue +60 -88
- package/app/components/ralph/RalphLoopResultView.vue +150 -171
- package/app/components/testing/TestReportWindow.vue +505 -559
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +224 -262
- package/i18n/locales/de.json +0 -1
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/es.json +0 -1
- package/i18n/locales/fr.json +0 -1
- package/i18n/locales/he.json +0 -1
- package/i18n/locales/it.json +0 -1
- package/i18n/locales/ja.json +0 -1
- package/i18n/locales/pl.json +0 -1
- package/i18n/locales/tr.json +0 -1
- package/i18n/locales/uk.json +0 -1
- package/package.json +1 -1
- package/app/composables/useFocusTrap.ts +0 -72
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
14
14
|
import type { TestConcern, TestOutcome, TestReport, TestScreenshot } from '~/types/domain'
|
|
15
15
|
import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
16
|
-
import { useFocusTrap } from '~/composables/useFocusTrap'
|
|
17
16
|
import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
|
|
18
|
-
import
|
|
17
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
19
18
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
20
19
|
import StepContainerStatus from '~/components/panels/StepContainerStatus.vue'
|
|
21
20
|
import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
|
|
@@ -30,10 +29,17 @@ const { t, d, n } = useI18n()
|
|
|
30
29
|
const blobs = useArtifactBlobs()
|
|
31
30
|
onUnmounted(() => blobs.revokeAll())
|
|
32
31
|
|
|
33
|
-
// Shared seam contract (open/blockId/close
|
|
34
|
-
//
|
|
35
|
-
|
|
32
|
+
// Shared seam contract (open/blockId/close). No `onOpen` loader: this window reads its report
|
|
33
|
+
// straight off the execution step, so there's nothing to fetch on open. `manageEscape: false`
|
|
34
|
+
// — `ResultWindowShell` owns Escape (and the focus trap + scroll lock + stacking) via the
|
|
35
|
+
// shared overlay behaviour; the nested lightbox layers above it on the same stack.
|
|
36
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('tester', {
|
|
37
|
+
manageEscape: false,
|
|
38
|
+
})
|
|
36
39
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
40
|
+
const headerTitle = computed(
|
|
41
|
+
() => `${t('testing.title')}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
42
|
+
)
|
|
37
43
|
|
|
38
44
|
const instance = computed(() =>
|
|
39
45
|
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
@@ -262,13 +268,6 @@ function openShot(artifactId: string) {
|
|
|
262
268
|
lightboxOpen.value = true
|
|
263
269
|
}
|
|
264
270
|
|
|
265
|
-
// Focus management for the modal panel; hands the Tab trap off to the lightbox while it's open.
|
|
266
|
-
const dialogRoot = ref<HTMLElement | null>(null)
|
|
267
|
-
useFocusTrap(
|
|
268
|
-
dialogRoot,
|
|
269
|
-
computed(() => open.value && !lightboxOpen.value),
|
|
270
|
-
)
|
|
271
|
-
|
|
272
271
|
const sortedConcerns = computed<TestConcern[]>(() => {
|
|
273
272
|
const r = report.value
|
|
274
273
|
if (!r) return []
|
|
@@ -309,508 +308,419 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
309
308
|
</script>
|
|
310
309
|
|
|
311
310
|
<template>
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
311
|
+
<ResultWindowShell
|
|
312
|
+
:open="open"
|
|
313
|
+
icon="i-lucide-flask-conical"
|
|
314
|
+
icon-class="bg-amber-500/15 text-amber-300"
|
|
315
|
+
:title="headerTitle"
|
|
316
|
+
:subtitle="t('testing.subtitle')"
|
|
317
|
+
:step-ref="{ instanceId, stepIndex }"
|
|
318
|
+
width="5xl"
|
|
319
|
+
testid="tester-report-window"
|
|
320
|
+
@close="close"
|
|
321
|
+
>
|
|
322
|
+
<template #header-extras>
|
|
323
|
+
<UBadge
|
|
324
|
+
v-if="report"
|
|
325
|
+
:color="report.greenlight ? 'success' : 'warning'"
|
|
326
|
+
variant="subtle"
|
|
327
|
+
size="sm"
|
|
326
328
|
>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
v-if="report"
|
|
344
|
-
:color="report.greenlight ? 'success' : 'warning'"
|
|
345
|
-
variant="subtle"
|
|
346
|
-
size="sm"
|
|
347
|
-
>
|
|
348
|
-
{{ report.greenlight ? t('testing.badge.greenlit') : t('testing.badge.needsFixes') }}
|
|
349
|
-
</UBadge>
|
|
350
|
-
<span
|
|
351
|
-
v-if="testState && testState.attempts > 0"
|
|
352
|
-
class="text-[11px] text-slate-400"
|
|
353
|
-
:title="t('testing.fixerAttempts')"
|
|
354
|
-
>
|
|
355
|
-
{{
|
|
356
|
-
t('testing.fixCount', { attempts: testState.attempts, max: testState.maxAttempts })
|
|
357
|
-
}}
|
|
358
|
-
<template v-if="testState.phase === 'fixing'">
|
|
359
|
-
{{ t('testing.fixingSuffix') }}</template
|
|
360
|
-
>
|
|
361
|
-
</span>
|
|
362
|
-
<StepRestartControl
|
|
363
|
-
:instance-id="instanceId"
|
|
364
|
-
:step-index="stepIndex"
|
|
365
|
-
@restarted="close"
|
|
366
|
-
/>
|
|
367
|
-
<button
|
|
368
|
-
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
369
|
-
@click="close"
|
|
370
|
-
>
|
|
371
|
-
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
372
|
-
</button>
|
|
373
|
-
</header>
|
|
374
|
-
|
|
375
|
-
<div class="flex min-h-0 flex-1">
|
|
376
|
-
<!-- Main: infrastructure observability + scenarios → outcomes → concerns tree -->
|
|
377
|
-
<div class="min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
|
|
378
|
-
<!-- Infrastructure: container lifecycle (where + what it's doing), the
|
|
329
|
+
{{ report.greenlight ? t('testing.badge.greenlit') : t('testing.badge.needsFixes') }}
|
|
330
|
+
</UBadge>
|
|
331
|
+
<span
|
|
332
|
+
v-if="testState && testState.attempts > 0"
|
|
333
|
+
class="text-[11px] text-slate-400"
|
|
334
|
+
:title="t('testing.fixerAttempts')"
|
|
335
|
+
>
|
|
336
|
+
{{ t('testing.fixCount', { attempts: testState.attempts, max: testState.maxAttempts }) }}
|
|
337
|
+
<template v-if="testState.phase === 'fixing'"> {{ t('testing.fixingSuffix') }}</template>
|
|
338
|
+
</span>
|
|
339
|
+
</template>
|
|
340
|
+
|
|
341
|
+
<div class="flex min-h-0 flex-1">
|
|
342
|
+
<!-- Main: infrastructure observability + scenarios → outcomes → concerns tree -->
|
|
343
|
+
<div class="min-w-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
|
|
344
|
+
<!-- Infrastructure: container lifecycle (where + what it's doing), the
|
|
379
345
|
ephemeral environment, and the run's infra attempts + logs — parity with
|
|
380
346
|
the Coder's step detail. Shown even before a report lands, so the infra
|
|
381
347
|
spin-up is visible WHILE the Tester is still standing it up. -->
|
|
382
|
-
|
|
348
|
+
<!-- Only when there's genuine infrastructure to show — a container or an ephemeral
|
|
383
349
|
environment. A no-infra tester (no container, no env) has no infra attempts
|
|
384
350
|
either, so we don't render an empty header + a log toggle over nothing. -->
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
351
|
+
<section
|
|
352
|
+
v-if="step && (step.container || stepEnvironment || infraSetup)"
|
|
353
|
+
data-testid="tester-infrastructure"
|
|
354
|
+
class="space-y-3"
|
|
355
|
+
>
|
|
356
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
357
|
+
{{ t('testing.infrastructure') }}
|
|
358
|
+
</h3>
|
|
359
|
+
<StepContainerStatus :step="step" :run-failed="runFailed" />
|
|
360
|
+
<EnvironmentStatusPanel v-if="stepEnvironment" :environment="stepEnvironment" />
|
|
361
|
+
|
|
362
|
+
<!-- In-container docker-compose dependency stand-up (local-infra tester): the
|
|
397
363
|
outcome + the captured `docker compose up` logs. This is the stand-up that
|
|
398
364
|
runs INSIDE the container, so its output isn't in the provisioning drawer
|
|
399
365
|
below — it's the highest-signal artifact when local infra fails to start. -->
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
366
|
+
<div
|
|
367
|
+
v-if="infraSetup"
|
|
368
|
+
data-testid="tester-infra-setup"
|
|
369
|
+
class="rounded-lg border px-3 py-2"
|
|
370
|
+
:class="
|
|
371
|
+
infraSetup.started
|
|
372
|
+
? 'border-slate-800 bg-slate-900/60'
|
|
373
|
+
: 'border-rose-500/40 bg-rose-500/10'
|
|
374
|
+
"
|
|
375
|
+
>
|
|
376
|
+
<div class="flex items-center gap-2">
|
|
377
|
+
<UIcon
|
|
378
|
+
:name="infraSetup.started ? 'i-lucide-container' : 'i-lucide-circle-x'"
|
|
379
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
380
|
+
:class="infraSetup.started ? 'text-emerald-400' : 'text-rose-400'"
|
|
381
|
+
/>
|
|
382
|
+
<span class="text-[13px] font-medium text-slate-200">
|
|
383
|
+
{{ infraSetup.started ? t('testing.standup.up') : t('testing.standup.failed') }}
|
|
384
|
+
</span>
|
|
385
|
+
<span v-if="infraSetup.durationMs != null" class="ms-auto text-[11px] text-slate-500">
|
|
386
|
+
{{
|
|
387
|
+
t('testing.standup.took', {
|
|
388
|
+
seconds: n(infraSetup.durationMs / 1000, 'decimal'),
|
|
389
|
+
})
|
|
390
|
+
}}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<p v-if="infraSetup.composePath" class="mt-1 font-mono text-[11px] text-slate-500">
|
|
394
|
+
{{ infraSetup.composePath }}
|
|
395
|
+
</p>
|
|
396
|
+
<p
|
|
397
|
+
v-if="infraSetup.error"
|
|
398
|
+
class="mt-1 text-[12px] leading-snug text-rose-300"
|
|
399
|
+
data-testid="tester-infra-setup-error"
|
|
400
|
+
>
|
|
401
|
+
{{ infraSetup.error }}
|
|
402
|
+
</p>
|
|
403
|
+
<template v-if="infraSetup.logs">
|
|
404
|
+
<UButton
|
|
405
|
+
:icon="showInfraSetupLogs ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
406
|
+
variant="ghost"
|
|
407
|
+
size="xs"
|
|
408
|
+
class="mt-1.5"
|
|
409
|
+
data-testid="tester-infra-setup-logs-toggle"
|
|
410
|
+
@click="
|
|
411
|
+
() => {
|
|
412
|
+
showInfraSetupLogs = !showInfraSetupLogs
|
|
413
|
+
}
|
|
408
414
|
"
|
|
409
415
|
>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
>
|
|
423
|
-
{{
|
|
424
|
-
t('testing.standup.took', {
|
|
425
|
-
seconds: n(infraSetup.durationMs / 1000, 'decimal'),
|
|
426
|
-
})
|
|
427
|
-
}}
|
|
428
|
-
</span>
|
|
429
|
-
</div>
|
|
430
|
-
<p v-if="infraSetup.composePath" class="mt-1 font-mono text-[11px] text-slate-500">
|
|
431
|
-
{{ infraSetup.composePath }}
|
|
432
|
-
</p>
|
|
433
|
-
<p
|
|
434
|
-
v-if="infraSetup.error"
|
|
435
|
-
class="mt-1 text-[12px] leading-snug text-rose-300"
|
|
436
|
-
data-testid="tester-infra-setup-error"
|
|
437
|
-
>
|
|
438
|
-
{{ infraSetup.error }}
|
|
439
|
-
</p>
|
|
440
|
-
<template v-if="infraSetup.logs">
|
|
441
|
-
<UButton
|
|
442
|
-
:icon="showInfraSetupLogs ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
443
|
-
variant="ghost"
|
|
444
|
-
size="xs"
|
|
445
|
-
class="mt-1.5"
|
|
446
|
-
data-testid="tester-infra-setup-logs-toggle"
|
|
447
|
-
@click="
|
|
448
|
-
() => {
|
|
449
|
-
showInfraSetupLogs = !showInfraSetupLogs
|
|
450
|
-
}
|
|
451
|
-
"
|
|
452
|
-
>
|
|
453
|
-
{{
|
|
454
|
-
showInfraSetupLogs
|
|
455
|
-
? t('testing.standup.hideLogs')
|
|
456
|
-
: t('testing.standup.showLogs')
|
|
457
|
-
}}
|
|
458
|
-
</UButton>
|
|
459
|
-
<pre
|
|
460
|
-
v-if="showInfraSetupLogs"
|
|
461
|
-
data-testid="tester-infra-setup-logs"
|
|
462
|
-
class="mt-2 max-h-64 overflow-auto rounded bg-slate-950/70 p-2 font-mono text-[11px] leading-relaxed text-slate-300"
|
|
463
|
-
>{{ infraSetup.logs }}</pre
|
|
464
|
-
>
|
|
465
|
-
</template>
|
|
466
|
-
</div>
|
|
416
|
+
{{
|
|
417
|
+
showInfraSetupLogs ? t('testing.standup.hideLogs') : t('testing.standup.showLogs')
|
|
418
|
+
}}
|
|
419
|
+
</UButton>
|
|
420
|
+
<pre
|
|
421
|
+
v-if="showInfraSetupLogs"
|
|
422
|
+
data-testid="tester-infra-setup-logs"
|
|
423
|
+
class="mt-2 max-h-64 overflow-auto rounded bg-slate-950/70 p-2 font-mono text-[11px] leading-relaxed text-slate-300"
|
|
424
|
+
>{{ infraSetup.logs }}</pre
|
|
425
|
+
>
|
|
426
|
+
</template>
|
|
427
|
+
</div>
|
|
467
428
|
|
|
468
|
-
|
|
429
|
+
<!-- Explicit confirmation that every piece of the tester's infrastructure is up
|
|
469
430
|
(container running, the ephemeral environment ready, any in-container
|
|
470
431
|
dependency stand-up done) and the agent is now starting its work — so the
|
|
471
432
|
details don't jump silently from "provisioning" into a blank working state. -->
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
433
|
+
<div
|
|
434
|
+
v-if="infraReady"
|
|
435
|
+
data-testid="tester-env-ready"
|
|
436
|
+
class="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-[13px] text-emerald-200"
|
|
437
|
+
>
|
|
438
|
+
<UIcon name="i-lucide-rocket" class="h-4 w-4 shrink-0 text-emerald-400" />
|
|
439
|
+
<span>{{ t('testing.readyBanner') }}</span>
|
|
440
|
+
</div>
|
|
480
441
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
442
|
+
<div v-if="executionId">
|
|
443
|
+
<UButton
|
|
444
|
+
:icon="showProvisioning ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
445
|
+
variant="ghost"
|
|
446
|
+
size="xs"
|
|
447
|
+
data-testid="tester-infra-attempts-toggle"
|
|
448
|
+
@click="
|
|
449
|
+
() => {
|
|
450
|
+
showProvisioning = !showProvisioning
|
|
451
|
+
}
|
|
452
|
+
"
|
|
453
|
+
>
|
|
454
|
+
{{
|
|
455
|
+
showProvisioning
|
|
456
|
+
? t('panels.stepDetail.hideInfraAttempts')
|
|
457
|
+
: t('panels.stepDetail.infraAttempts')
|
|
458
|
+
}}
|
|
459
|
+
</UButton>
|
|
460
|
+
<ProvisioningLogsDrawer
|
|
461
|
+
v-if="showProvisioning"
|
|
462
|
+
class="mt-2"
|
|
463
|
+
:execution-id="executionId"
|
|
464
|
+
:live="runLive"
|
|
465
|
+
/>
|
|
466
|
+
</div>
|
|
467
|
+
</section>
|
|
507
468
|
|
|
508
|
-
|
|
469
|
+
<!-- Fixer timeline: one inspectable entry per fixer round (what it was handed and
|
|
509
470
|
how it ended), so the otherwise-opaque fixer sub-jobs have a surface — the
|
|
510
471
|
analogue of the polling gate's attempt history. -->
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
472
|
+
<section v-if="fixerAttempts.length" data-testid="tester-fixer-attempts" class="space-y-2">
|
|
473
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
474
|
+
{{ t('testing.fixerAttempts') }}
|
|
475
|
+
</h3>
|
|
476
|
+
<ol class="space-y-2">
|
|
477
|
+
<li
|
|
478
|
+
v-for="a in fixerAttempts"
|
|
479
|
+
:key="a.attempt"
|
|
480
|
+
data-testid="tester-fixer-attempt"
|
|
481
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
515
482
|
>
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
:class="SEVERITY_META[c.severity].chip"
|
|
554
|
-
>{{ SEVERITY_LABELS[c.severity] }}</span
|
|
555
|
-
>
|
|
556
|
-
<span class="truncate">{{ c.title }}</span>
|
|
557
|
-
</li>
|
|
558
|
-
</ul>
|
|
559
|
-
</div>
|
|
560
|
-
</li>
|
|
561
|
-
</ol>
|
|
562
|
-
</section>
|
|
483
|
+
<AttemptEntryHeader
|
|
484
|
+
:label="t('testing.fixerTimeline.attempt', { n: a.attempt })"
|
|
485
|
+
:outcome="a.outcome"
|
|
486
|
+
:outcome-label="
|
|
487
|
+
a.outcome === 'completed'
|
|
488
|
+
? t('testing.fixerTimeline.completed')
|
|
489
|
+
: t('testing.fixerTimeline.failed')
|
|
490
|
+
"
|
|
491
|
+
:at="a.at"
|
|
492
|
+
:icon="a.outcome === 'completed' ? 'i-lucide-wrench' : 'i-lucide-circle-x'"
|
|
493
|
+
:icon-class="a.outcome === 'completed' ? 'text-amber-300' : 'text-rose-400'"
|
|
494
|
+
/>
|
|
495
|
+
<p v-if="a.summary" class="mt-1 text-[12px] leading-snug text-slate-400">
|
|
496
|
+
{{ a.summary }}
|
|
497
|
+
</p>
|
|
498
|
+
<div v-if="a.concerns && a.concerns.length" class="mt-1.5">
|
|
499
|
+
<p class="text-[11px] text-slate-500">
|
|
500
|
+
{{ t('testing.fixerTimeline.addressed') }}
|
|
501
|
+
</p>
|
|
502
|
+
<ul class="mt-1 space-y-0.5">
|
|
503
|
+
<li
|
|
504
|
+
v-for="(c, ci) in a.concerns"
|
|
505
|
+
:key="`fa${a.attempt}-c${ci}`"
|
|
506
|
+
class="flex items-center gap-1.5 text-[12px] text-slate-300"
|
|
507
|
+
>
|
|
508
|
+
<span
|
|
509
|
+
class="rounded px-1 text-[10px] uppercase"
|
|
510
|
+
:class="SEVERITY_META[c.severity].chip"
|
|
511
|
+
>{{ SEVERITY_LABELS[c.severity] }}</span
|
|
512
|
+
>
|
|
513
|
+
<span class="truncate">{{ c.title }}</span>
|
|
514
|
+
</li>
|
|
515
|
+
</ul>
|
|
516
|
+
</div>
|
|
517
|
+
</li>
|
|
518
|
+
</ol>
|
|
519
|
+
</section>
|
|
563
520
|
|
|
564
|
-
|
|
521
|
+
<!-- Test quality-control companion: the coverage audit(s) the QC reviewer ran on
|
|
565
522
|
the report before the greenlight/fixer decision. Each verdict says whether the
|
|
566
523
|
report adequately covered what the task needed tested, with the gaps that
|
|
567
524
|
looped the Tester for a focused additional pass. -->
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
525
|
+
<section
|
|
526
|
+
v-if="quality && qualityVerdicts.length"
|
|
527
|
+
data-testid="tester-quality"
|
|
528
|
+
class="space-y-2"
|
|
529
|
+
>
|
|
530
|
+
<div class="flex items-center gap-2">
|
|
531
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
532
|
+
{{ t('testing.quality.heading') }}
|
|
533
|
+
</h3>
|
|
534
|
+
<span
|
|
535
|
+
v-if="quality.attempts"
|
|
536
|
+
class="text-[11px] text-slate-400"
|
|
537
|
+
:title="t('testing.quality.reruns')"
|
|
538
|
+
>
|
|
539
|
+
{{
|
|
540
|
+
t('testing.quality.rerunCount', {
|
|
541
|
+
attempts: quality.attempts,
|
|
542
|
+
max: quality.maxAttempts,
|
|
543
|
+
})
|
|
544
|
+
}}
|
|
545
|
+
</span>
|
|
546
|
+
<UBadge
|
|
547
|
+
v-if="quality.exceeded"
|
|
548
|
+
color="warning"
|
|
549
|
+
variant="subtle"
|
|
550
|
+
size="sm"
|
|
551
|
+
data-testid="tester-quality-exceeded"
|
|
552
|
+
>
|
|
553
|
+
{{ t('testing.quality.exceeded') }}
|
|
554
|
+
</UBadge>
|
|
555
|
+
</div>
|
|
556
|
+
<ol class="space-y-2">
|
|
557
|
+
<li
|
|
558
|
+
v-for="(vd, vi) in qualityVerdicts"
|
|
559
|
+
:key="`qc${vi}`"
|
|
560
|
+
data-testid="tester-quality-verdict"
|
|
561
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
572
562
|
>
|
|
573
563
|
<div class="flex items-center gap-2">
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
:title="t('testing.quality.reruns')"
|
|
581
|
-
>
|
|
564
|
+
<UIcon
|
|
565
|
+
:name="vd.adequate ? 'i-lucide-shield-check' : 'i-lucide-shield-alert'"
|
|
566
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
567
|
+
:class="vd.adequate ? 'text-emerald-400' : 'text-amber-300'"
|
|
568
|
+
/>
|
|
569
|
+
<span class="text-[13px] font-medium text-slate-200">
|
|
582
570
|
{{
|
|
583
|
-
t('testing.quality.
|
|
584
|
-
attempts: quality.attempts,
|
|
585
|
-
max: quality.maxAttempts,
|
|
586
|
-
})
|
|
571
|
+
vd.adequate ? t('testing.quality.adequate') : t('testing.quality.inadequate')
|
|
587
572
|
}}
|
|
588
573
|
</span>
|
|
589
|
-
<
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
>
|
|
596
|
-
{{ t('testing.quality.exceeded') }}
|
|
597
|
-
</UBadge>
|
|
574
|
+
<span v-if="vd.model" class="ms-auto font-mono text-[10px] text-slate-500">{{
|
|
575
|
+
vd.model
|
|
576
|
+
}}</span>
|
|
577
|
+
<span class="text-[11px] text-slate-500" :class="{ 'ms-auto': !vd.model }">{{
|
|
578
|
+
d(new Date(vd.at), 'short')
|
|
579
|
+
}}</span>
|
|
598
580
|
</div>
|
|
599
|
-
<
|
|
600
|
-
|
|
601
|
-
v-for="(vd, vi) in qualityVerdicts"
|
|
602
|
-
:key="`qc${vi}`"
|
|
603
|
-
data-testid="tester-quality-verdict"
|
|
604
|
-
class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
605
|
-
>
|
|
606
|
-
<div class="flex items-center gap-2">
|
|
607
|
-
<UIcon
|
|
608
|
-
:name="vd.adequate ? 'i-lucide-shield-check' : 'i-lucide-shield-alert'"
|
|
609
|
-
class="h-3.5 w-3.5 shrink-0"
|
|
610
|
-
:class="vd.adequate ? 'text-emerald-400' : 'text-amber-300'"
|
|
611
|
-
/>
|
|
612
|
-
<span class="text-[13px] font-medium text-slate-200">
|
|
613
|
-
{{
|
|
614
|
-
vd.adequate
|
|
615
|
-
? t('testing.quality.adequate')
|
|
616
|
-
: t('testing.quality.inadequate')
|
|
617
|
-
}}
|
|
618
|
-
</span>
|
|
619
|
-
<span v-if="vd.model" class="ms-auto font-mono text-[10px] text-slate-500">{{
|
|
620
|
-
vd.model
|
|
621
|
-
}}</span>
|
|
622
|
-
<span class="text-[11px] text-slate-500" :class="{ 'ms-auto': !vd.model }">{{
|
|
623
|
-
d(new Date(vd.at), 'short')
|
|
624
|
-
}}</span>
|
|
625
|
-
</div>
|
|
626
|
-
<p v-if="vd.feedback" class="mt-1 text-[12px] leading-snug text-slate-400">
|
|
627
|
-
{{ vd.feedback }}
|
|
628
|
-
</p>
|
|
629
|
-
<div v-if="vd.gaps.length" class="mt-1.5">
|
|
630
|
-
<p class="text-[11px] text-slate-500">{{ t('testing.quality.gaps') }}</p>
|
|
631
|
-
<ul class="mt-1 space-y-0.5">
|
|
632
|
-
<li
|
|
633
|
-
v-for="(gap, gi) in vd.gaps"
|
|
634
|
-
:key="`qc${vi}-g${gi}`"
|
|
635
|
-
class="flex items-start gap-1.5 text-[12px] text-slate-300"
|
|
636
|
-
>
|
|
637
|
-
<UIcon
|
|
638
|
-
name="i-lucide-dot"
|
|
639
|
-
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-400"
|
|
640
|
-
/>
|
|
641
|
-
<span>{{ gap }}</span>
|
|
642
|
-
</li>
|
|
643
|
-
</ul>
|
|
644
|
-
</div>
|
|
645
|
-
</li>
|
|
646
|
-
</ol>
|
|
647
|
-
</section>
|
|
648
|
-
|
|
649
|
-
<div
|
|
650
|
-
v-if="!report"
|
|
651
|
-
class="flex flex-col items-center justify-center gap-2 py-12 text-center text-slate-400"
|
|
652
|
-
>
|
|
653
|
-
<UIcon name="i-lucide-flask-conical" class="h-8 w-8 opacity-40" />
|
|
654
|
-
<p class="text-sm">{{ t('testing.empty.title') }}</p>
|
|
655
|
-
<p class="max-w-sm text-[11px] text-slate-500">
|
|
656
|
-
{{ t('testing.empty.hint') }}
|
|
581
|
+
<p v-if="vd.feedback" class="mt-1 text-[12px] leading-snug text-slate-400">
|
|
582
|
+
{{ vd.feedback }}
|
|
657
583
|
</p>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
667
|
-
{{ t('testing.scenariosOutcomes') }}
|
|
668
|
-
</h3>
|
|
669
|
-
<ul class="space-y-2">
|
|
670
|
-
<li
|
|
671
|
-
v-for="g in groups"
|
|
672
|
-
:key="g.key"
|
|
673
|
-
class="overflow-hidden rounded-lg border border-slate-800 bg-slate-900/60"
|
|
674
|
-
>
|
|
675
|
-
<button
|
|
676
|
-
class="flex w-full items-center gap-2 px-3 py-2 text-start hover:bg-slate-800/40"
|
|
677
|
-
@click="toggle(g.key)"
|
|
584
|
+
<div v-if="vd.gaps.length" class="mt-1.5">
|
|
585
|
+
<p class="text-[11px] text-slate-500">{{ t('testing.quality.gaps') }}</p>
|
|
586
|
+
<ul class="mt-1 space-y-0.5">
|
|
587
|
+
<li
|
|
588
|
+
v-for="(gap, gi) in vd.gaps"
|
|
589
|
+
:key="`qc${vi}-g${gi}`"
|
|
590
|
+
class="flex items-start gap-1.5 text-[12px] text-slate-300"
|
|
678
591
|
>
|
|
679
|
-
<UIcon
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
name="i-lucide-camera"
|
|
699
|
-
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
700
|
-
:title="
|
|
701
|
-
t(
|
|
702
|
-
'testing.screenshotCount',
|
|
703
|
-
{ count: g.screenshots.length },
|
|
704
|
-
g.screenshots.length,
|
|
705
|
-
)
|
|
706
|
-
"
|
|
707
|
-
/>
|
|
708
|
-
<span class="shrink-0 text-[11px] text-slate-500">
|
|
709
|
-
{{ t('testing.checkCount', { count: g.outcomes.length }, g.outcomes.length) }}
|
|
710
|
-
<template v-if="g.concerns.length">
|
|
711
|
-
·
|
|
712
|
-
{{
|
|
713
|
-
t('testing.concernCount', { count: g.concerns.length }, g.concerns.length)
|
|
714
|
-
}}
|
|
715
|
-
</template>
|
|
716
|
-
</span>
|
|
717
|
-
</button>
|
|
592
|
+
<UIcon name="i-lucide-dot" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-400" />
|
|
593
|
+
<span>{{ gap }}</span>
|
|
594
|
+
</li>
|
|
595
|
+
</ul>
|
|
596
|
+
</div>
|
|
597
|
+
</li>
|
|
598
|
+
</ol>
|
|
599
|
+
</section>
|
|
600
|
+
|
|
601
|
+
<div
|
|
602
|
+
v-if="!report"
|
|
603
|
+
class="flex flex-col items-center justify-center gap-2 py-12 text-center text-slate-400"
|
|
604
|
+
>
|
|
605
|
+
<UIcon name="i-lucide-flask-conical" class="h-8 w-8 opacity-40" />
|
|
606
|
+
<p class="text-sm">{{ t('testing.empty.title') }}</p>
|
|
607
|
+
<p class="max-w-sm text-[11px] text-slate-500">
|
|
608
|
+
{{ t('testing.empty.hint') }}
|
|
609
|
+
</p>
|
|
610
|
+
</div>
|
|
718
611
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
612
|
+
<template v-else>
|
|
613
|
+
<!-- Summary -->
|
|
614
|
+
<p v-if="report.summary" class="mb-4 text-[13px] leading-relaxed text-slate-300">
|
|
615
|
+
{{ report.summary }}
|
|
616
|
+
</p>
|
|
617
|
+
|
|
618
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
619
|
+
{{ t('testing.scenariosOutcomes') }}
|
|
620
|
+
</h3>
|
|
621
|
+
<ul class="space-y-2">
|
|
622
|
+
<li
|
|
623
|
+
v-for="g in groups"
|
|
624
|
+
:key="g.key"
|
|
625
|
+
class="overflow-hidden rounded-lg border border-slate-800 bg-slate-900/60"
|
|
626
|
+
>
|
|
627
|
+
<button
|
|
628
|
+
class="flex w-full items-center gap-2 px-3 py-2 text-start hover:bg-slate-800/40"
|
|
629
|
+
@click="toggle(g.key)"
|
|
630
|
+
>
|
|
631
|
+
<UIcon
|
|
632
|
+
:name="collapsed.has(g.key) ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'"
|
|
633
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
634
|
+
/>
|
|
635
|
+
<UIcon
|
|
636
|
+
:name="GROUP_STATUS_META[g.status].icon"
|
|
637
|
+
class="h-4 w-4 shrink-0"
|
|
638
|
+
:class="GROUP_STATUS_META[g.status].text"
|
|
639
|
+
/>
|
|
640
|
+
<span
|
|
641
|
+
class="min-w-0 flex-1 truncate text-[13px]"
|
|
642
|
+
:class="g.other ? 'text-slate-400' : 'font-medium text-slate-200'"
|
|
643
|
+
>
|
|
644
|
+
{{ g.title }}
|
|
645
|
+
</span>
|
|
646
|
+
<UIcon
|
|
647
|
+
v-if="g.screenshots.length"
|
|
648
|
+
name="i-lucide-camera"
|
|
649
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
650
|
+
:title="
|
|
651
|
+
t(
|
|
652
|
+
'testing.screenshotCount',
|
|
653
|
+
{ count: g.screenshots.length },
|
|
654
|
+
g.screenshots.length,
|
|
655
|
+
)
|
|
656
|
+
"
|
|
657
|
+
/>
|
|
658
|
+
<span class="shrink-0 text-[11px] text-slate-500">
|
|
659
|
+
{{ t('testing.checkCount', { count: g.outcomes.length }, g.outcomes.length) }}
|
|
660
|
+
<template v-if="g.concerns.length">
|
|
661
|
+
·
|
|
662
|
+
{{ t('testing.concernCount', { count: g.concerns.length }, g.concerns.length) }}
|
|
663
|
+
</template>
|
|
664
|
+
</span>
|
|
665
|
+
</button>
|
|
666
|
+
|
|
667
|
+
<div v-if="!collapsed.has(g.key)" class="space-y-1 px-3 pb-3 ps-9">
|
|
668
|
+
<!-- Outcomes -->
|
|
669
|
+
<div
|
|
670
|
+
v-for="(o, oi) in g.outcomes"
|
|
671
|
+
:key="`o${oi}`"
|
|
672
|
+
class="flex items-start gap-2 py-0.5"
|
|
673
|
+
>
|
|
674
|
+
<UIcon
|
|
675
|
+
:name="STATUS_META[o.status].icon"
|
|
676
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0"
|
|
677
|
+
:class="STATUS_META[o.status].text"
|
|
678
|
+
/>
|
|
679
|
+
<div class="min-w-0">
|
|
680
|
+
<span class="text-[13px] text-slate-200">{{ o.name }}</span>
|
|
681
|
+
<p v-if="o.detail" class="text-[12px] leading-snug text-slate-400">
|
|
682
|
+
{{ o.detail }}
|
|
740
683
|
</p>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
<p v-if="!g.outcomes.length" class="py-0.5 text-[12px] italic text-slate-500">
|
|
687
|
+
{{ t('testing.noDiscreteCheck') }}
|
|
688
|
+
</p>
|
|
741
689
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
>
|
|
760
|
-
{{ SEVERITY_LABELS[c.severity] }}
|
|
761
|
-
</span>
|
|
762
|
-
</div>
|
|
763
|
-
<p v-if="c.detail" class="text-[12px] leading-snug text-slate-400">
|
|
764
|
-
{{ c.detail }}
|
|
765
|
-
</p>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
|
|
769
|
-
<!-- Screenshots captured for this scenario -->
|
|
770
|
-
<div v-if="g.screenshots.length" class="mt-2 flex flex-wrap gap-2">
|
|
771
|
-
<button
|
|
772
|
-
v-for="(s, si) in g.screenshots"
|
|
773
|
-
:key="`shot${si}`"
|
|
774
|
-
class="group relative h-20 w-28 shrink-0 overflow-hidden rounded border border-slate-800 bg-slate-950/60 hover:border-slate-600"
|
|
775
|
-
:title="s.view"
|
|
776
|
-
@click="openShot(s.artifactId)"
|
|
690
|
+
<!-- Concerns linked to this scenario -->
|
|
691
|
+
<div
|
|
692
|
+
v-for="(c, ci) in g.concerns"
|
|
693
|
+
:key="`c${ci}`"
|
|
694
|
+
class="mt-1 flex items-start gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-2 py-1.5"
|
|
695
|
+
>
|
|
696
|
+
<UIcon
|
|
697
|
+
name="i-lucide-alert-triangle"
|
|
698
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0"
|
|
699
|
+
:class="SEVERITY_META[c.severity].text"
|
|
700
|
+
/>
|
|
701
|
+
<div class="min-w-0">
|
|
702
|
+
<div class="flex items-center gap-1.5">
|
|
703
|
+
<span class="text-[12px] font-medium text-slate-200">{{ c.title }}</span>
|
|
704
|
+
<span
|
|
705
|
+
class="rounded px-1 text-[10px] uppercase"
|
|
706
|
+
:class="SEVERITY_META[c.severity].chip"
|
|
777
707
|
>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
:src="blobs.urlFor(s.artifactId)"
|
|
781
|
-
:alt="t('testing.screenshotAlt', { view: s.view })"
|
|
782
|
-
class="h-full w-full object-cover object-top"
|
|
783
|
-
/>
|
|
784
|
-
<span
|
|
785
|
-
v-else
|
|
786
|
-
class="flex h-full w-full items-center justify-center text-[10px] text-slate-600"
|
|
787
|
-
>
|
|
788
|
-
{{
|
|
789
|
-
blobs.statusFor(s.artifactId) === 'error'
|
|
790
|
-
? t('testing.shot.failed')
|
|
791
|
-
: t('testing.shot.loading')
|
|
792
|
-
}}
|
|
793
|
-
</span>
|
|
794
|
-
<span
|
|
795
|
-
class="absolute inset-x-0 bottom-0 truncate bg-slate-950/80 px-1 py-0.5 text-[9px] text-slate-300"
|
|
796
|
-
>{{ s.view }}</span
|
|
797
|
-
>
|
|
798
|
-
</button>
|
|
708
|
+
{{ SEVERITY_LABELS[c.severity] }}
|
|
709
|
+
</span>
|
|
799
710
|
</div>
|
|
711
|
+
<p v-if="c.detail" class="text-[12px] leading-snug text-slate-400">
|
|
712
|
+
{{ c.detail }}
|
|
713
|
+
</p>
|
|
800
714
|
</div>
|
|
801
|
-
</
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
<section v-if="ungroupedScreenshots.length" class="mt-5">
|
|
806
|
-
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
807
|
-
{{ t('testing.screenshots') }}
|
|
808
|
-
</h3>
|
|
809
|
-
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<!-- Screenshots captured for this scenario -->
|
|
718
|
+
<div v-if="g.screenshots.length" class="mt-2 flex flex-wrap gap-2">
|
|
810
719
|
<button
|
|
811
|
-
v-for="(s, si) in
|
|
812
|
-
:key="`
|
|
813
|
-
|
|
720
|
+
v-for="(s, si) in g.screenshots"
|
|
721
|
+
:key="`shot${si}`"
|
|
722
|
+
data-testid="tester-screenshot"
|
|
723
|
+
class="group relative h-20 w-28 shrink-0 overflow-hidden rounded border border-slate-800 bg-slate-950/60 hover:border-slate-600"
|
|
814
724
|
:title="s.view"
|
|
815
725
|
@click="openShot(s.artifactId)"
|
|
816
726
|
>
|
|
@@ -822,110 +732,146 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
822
732
|
/>
|
|
823
733
|
<span
|
|
824
734
|
v-else
|
|
825
|
-
class="flex h-full w-full items-center justify-center text-[
|
|
735
|
+
class="flex h-full w-full items-center justify-center text-[10px] text-slate-600"
|
|
826
736
|
>
|
|
827
737
|
{{
|
|
828
738
|
blobs.statusFor(s.artifactId) === 'error'
|
|
829
|
-
? t('testing.shot.
|
|
739
|
+
? t('testing.shot.failed')
|
|
830
740
|
: t('testing.shot.loading')
|
|
831
741
|
}}
|
|
832
742
|
</span>
|
|
833
743
|
<span
|
|
834
|
-
class="absolute inset-x-0 bottom-0 truncate bg-slate-950/80 px-1
|
|
744
|
+
class="absolute inset-x-0 bottom-0 truncate bg-slate-950/80 px-1 py-0.5 text-[9px] text-slate-300"
|
|
835
745
|
>{{ s.view }}</span
|
|
836
746
|
>
|
|
837
747
|
</button>
|
|
838
748
|
</div>
|
|
839
|
-
</
|
|
840
|
-
</
|
|
841
|
-
</
|
|
842
|
-
|
|
843
|
-
<!--
|
|
844
|
-
<
|
|
845
|
-
class="
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
749
|
+
</div>
|
|
750
|
+
</li>
|
|
751
|
+
</ul>
|
|
752
|
+
|
|
753
|
+
<!-- Standalone gallery: any captures not mapped to a scenario above -->
|
|
754
|
+
<section v-if="ungroupedScreenshots.length" class="mt-5">
|
|
755
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
756
|
+
{{ t('testing.screenshots') }}
|
|
757
|
+
</h3>
|
|
758
|
+
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
759
|
+
<button
|
|
760
|
+
v-for="(s, si) in ungroupedScreenshots"
|
|
761
|
+
:key="`gal${si}`"
|
|
762
|
+
class="group relative aspect-video overflow-hidden rounded-lg border border-slate-800 bg-slate-950/60 hover:border-slate-600"
|
|
763
|
+
:title="s.view"
|
|
764
|
+
@click="openShot(s.artifactId)"
|
|
765
|
+
>
|
|
766
|
+
<img
|
|
767
|
+
v-if="blobs.urlFor(s.artifactId)"
|
|
768
|
+
:src="blobs.urlFor(s.artifactId)"
|
|
769
|
+
:alt="t('testing.screenshotAlt', { view: s.view })"
|
|
770
|
+
class="h-full w-full object-cover object-top"
|
|
856
771
|
/>
|
|
857
|
-
<span
|
|
772
|
+
<span
|
|
773
|
+
v-else
|
|
774
|
+
class="flex h-full w-full items-center justify-center text-[11px] text-slate-600"
|
|
775
|
+
>
|
|
858
776
|
{{
|
|
859
|
-
|
|
777
|
+
blobs.statusFor(s.artifactId) === 'error'
|
|
778
|
+
? t('testing.shot.failedToLoad')
|
|
779
|
+
: t('testing.shot.loading')
|
|
860
780
|
}}
|
|
861
781
|
</span>
|
|
862
|
-
|
|
782
|
+
<span
|
|
783
|
+
class="absolute inset-x-0 bottom-0 truncate bg-slate-950/80 px-1.5 py-0.5 text-[10px] text-slate-300"
|
|
784
|
+
>{{ s.view }}</span
|
|
785
|
+
>
|
|
786
|
+
</button>
|
|
863
787
|
</div>
|
|
788
|
+
</section>
|
|
789
|
+
</template>
|
|
790
|
+
</div>
|
|
864
791
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
<dd class="text-amber-300">
|
|
885
|
-
{{ counts.concerns
|
|
886
|
-
}}<template v-if="counts.blocking">
|
|
887
|
-
{{
|
|
888
|
-
t('testing.outcomes.blocking', { count: counts.blocking }, counts.blocking)
|
|
889
|
-
}}</template
|
|
890
|
-
>
|
|
891
|
-
</dd>
|
|
892
|
-
</div>
|
|
893
|
-
</dl>
|
|
894
|
-
</div>
|
|
792
|
+
<!-- Sidebar: metadata -->
|
|
793
|
+
<aside
|
|
794
|
+
class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
795
|
+
>
|
|
796
|
+
<div v-if="report">
|
|
797
|
+
<h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
798
|
+
{{ t('testing.verdict.heading') }}
|
|
799
|
+
</h4>
|
|
800
|
+
<div class="flex items-center gap-2 text-[13px]">
|
|
801
|
+
<UIcon
|
|
802
|
+
:name="report.greenlight ? 'i-lucide-circle-check' : 'i-lucide-circle-x'"
|
|
803
|
+
class="h-4 w-4"
|
|
804
|
+
:class="report.greenlight ? 'text-emerald-400' : 'text-rose-400'"
|
|
805
|
+
/>
|
|
806
|
+
<span :class="report.greenlight ? 'text-emerald-300' : 'text-rose-300'">
|
|
807
|
+
{{ report.greenlight ? t('testing.verdict.safe') : t('testing.verdict.withheld') }}
|
|
808
|
+
</span>
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
895
811
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
812
|
+
<div v-if="report">
|
|
813
|
+
<h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
814
|
+
{{ t('testing.outcomes.heading') }}
|
|
815
|
+
</h4>
|
|
816
|
+
<dl class="space-y-1 text-[12px]">
|
|
817
|
+
<div class="flex items-center justify-between">
|
|
818
|
+
<dt class="text-slate-400">{{ t('testing.outcomes.passed') }}</dt>
|
|
819
|
+
<dd class="text-emerald-300">{{ counts.passed }}</dd>
|
|
901
820
|
</div>
|
|
821
|
+
<div class="flex items-center justify-between">
|
|
822
|
+
<dt class="text-slate-400">{{ t('testing.outcomes.failed') }}</dt>
|
|
823
|
+
<dd class="text-rose-300">{{ counts.failed }}</dd>
|
|
824
|
+
</div>
|
|
825
|
+
<div class="flex items-center justify-between">
|
|
826
|
+
<dt class="text-slate-400">{{ t('testing.outcomes.skipped') }}</dt>
|
|
827
|
+
<dd class="text-slate-300">{{ counts.skipped }}</dd>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="flex items-center justify-between border-t border-slate-800 pt-1">
|
|
830
|
+
<dt class="text-slate-400">{{ t('testing.outcomes.concerns') }}</dt>
|
|
831
|
+
<dd class="text-amber-300">
|
|
832
|
+
{{ counts.concerns
|
|
833
|
+
}}<template v-if="counts.blocking">
|
|
834
|
+
{{
|
|
835
|
+
t('testing.outcomes.blocking', { count: counts.blocking }, counts.blocking)
|
|
836
|
+
}}</template
|
|
837
|
+
>
|
|
838
|
+
</dd>
|
|
839
|
+
</div>
|
|
840
|
+
</dl>
|
|
841
|
+
</div>
|
|
902
842
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
:instance-id="instanceId ?? undefined"
|
|
909
|
-
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
910
|
-
:total-steps="instance?.steps.length"
|
|
911
|
-
:run-failed="instance?.status === 'failed'"
|
|
912
|
-
:failure-at="instance?.failure?.occurredAt"
|
|
913
|
-
/>
|
|
914
|
-
|
|
915
|
-
<p class="mt-auto text-[10px] leading-relaxed text-slate-600">
|
|
916
|
-
{{ t('testing.footer') }}
|
|
917
|
-
</p>
|
|
918
|
-
</aside>
|
|
843
|
+
<div v-if="report?.environment">
|
|
844
|
+
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
845
|
+
{{ t('testing.environment') }}
|
|
846
|
+
</h4>
|
|
847
|
+
<p class="text-[12px] capitalize text-slate-300">{{ report.environment }}</p>
|
|
919
848
|
</div>
|
|
920
|
-
</div>
|
|
921
|
-
</div>
|
|
922
849
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
850
|
+
<!-- Shared run metadata + embedded observability (model, run id, timing,
|
|
851
|
+
model-activity rollup) — identical to the gate and agent step detail. -->
|
|
852
|
+
<StepRunMeta
|
|
853
|
+
v-if="step"
|
|
854
|
+
:step="step"
|
|
855
|
+
:instance-id="instanceId ?? undefined"
|
|
856
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
857
|
+
:total-steps="instance?.steps.length"
|
|
858
|
+
:run-failed="instance?.status === 'failed'"
|
|
859
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
860
|
+
/>
|
|
861
|
+
|
|
862
|
+
<p class="mt-auto text-[10px] leading-relaxed text-slate-600">
|
|
863
|
+
{{ t('testing.footer') }}
|
|
864
|
+
</p>
|
|
865
|
+
</aside>
|
|
866
|
+
</div>
|
|
867
|
+
</ResultWindowShell>
|
|
868
|
+
|
|
869
|
+
<!-- Shared zoom/pan viewer for the captured screenshots — a sibling overlay that layers
|
|
870
|
+
above this window on the shared modal stack while open. -->
|
|
871
|
+
<ArtifactLightbox
|
|
872
|
+
v-model:open="lightboxOpen"
|
|
873
|
+
v-model:index="lightboxIndex"
|
|
874
|
+
:items="lightboxItems"
|
|
875
|
+
:blobs="blobs"
|
|
876
|
+
/>
|
|
931
877
|
</template>
|