{{-- Loading overlay while a test case's breakdown is being parsed --}}

Test Cases · {{ $quarter }}

@include('partials.test-cases-export', ['txQuarter' => $quarter, 'txFiles' => $cases->count()])

Uploaded & generated test cases for the quarter, each linked to its actual Jira story. Click a test case to view its breakdown, or generate new ones in the {{-- Flips the hub's tab (its x-data is an ancestor) instead of reloading the page; falls back to the real link if this component is ever rendered standalone. --}} Generator tab. Upload more in Settings → Import Test Cases.

@if ($totals)
≈ {{ $totals['grand_days'] }} d estimated for {{ $quarter }} · {{ $totals['sets'] }} test-case set{{ $totals['sets'] === 1 ? '' : 's' }} QA effort ~{{ $totals['qa_days'] }} d · Dev fix TAT ~{{ $totals['tat_days'] }} d · incl. BA clarification ~{{ $totals['clarif_hours'] }} h
@endif
@if ($canManage && count($selected))
{{ count($selected) }} selected @if (count($selected) >= 2) @endif @if ($selectedGrouped) @endif Click rows to select. Group files them into a folder under each project; Merge combines selected files into one (originals kept); Delete removes them permanently.
@endif
@if ($canManage) @endif @if ($canManage)@endif @forelse ($projectNodes as $project => $node) {{-- Folders first: one collapsible node per group, holding this project's members. --}} @foreach ($node['folders'] as $f) @php $fk = 'f'.$f['folder']->id; @endphp @foreach ($f['cases'] as $c) @include('partials.test-case-row', ['c' => $c, 'inFolder' => true, 'xShow' => "folderOpen('".$fk."')"]) @endforeach @endforeach @foreach ($node['loose'] as $c) @include('partials.test-case-row', ['c' => $c, 'inFolder' => false, 'xShow' => null]) @endforeach @empty @endforelse
Test case Date Est. (hours) Est. (days) ResultsLinked stories
{{ $project }} {{ $node['count'] }}
📁 {{ $f['folder']->name }} {{ count($f['cases']) }} @if ($canManage) @endif
No test cases for {{ $quarter }}. Upload some in Settings → Import Test Cases.
{{-- Group: name the folder --}} @if ($confirmingGroup)
Group {{ count($selected) }} test-case file{{ count($selected) === 1 ? '' : 's' }}

The files stay exactly as they are — this only tucks them into a collapsible folder under their project. Files from different projects each stay under their own.

@endif {{-- Merge: name the merged file --}} @if ($confirmingMerge)
Merge {{ count($selected) }} test-case files

Originals are kept; a new file with this name is created. If all selected share one Jira story, its key is appended automatically.

@endif {{-- Delete confirmation --}} @if ($confirmingDelete)
Delete test cases?

Permanently delete {{ count($selected) }} selected test-case file{{ count($selected) === 1 ? '' : 's' }}? This removes the file{{ count($selected) === 1 ? '' : 's' }} and cannot be undone.

@endif {{-- Breakdown modal --}} @if ($viewing && $viewingCase)
{{ $viewingCase->displayName() }} {{ $viewingCase->projectLabel() ? $viewingCase->projectLabel().' · ' : '' }}{{ $viewingCase->jira_key ?: 'not linked' }} ⬇ Download .xlsx @if ($viewRows !== null && count($viewRows)) @endif
Reading results…
@if ($viewResults && ($viewResults['total'] ?? 0) > 0 && ($viewResults['executed'] ?? 0) > 0) @php $rp = $viewResults['pass_pct']; $rc = $rp >= 80 ? '#16a34a' : ($rp >= 50 ? '#d97706' : '#dc2626'); @endphp
{{ $viewResults['executed_pct'] }}% executed · {{ $viewResults['pass_pct'] }}% pass — {{ $viewResults['passed'] }} pass · {{ $viewResults['failed'] }} fail · {{ $viewResults['blocked'] }} blocked · {{ ($viewResults['pended'] ?? 0) > 0 ? $viewResults['pended'].' pended · ' : '' }}{{ $viewResults['not_run'] }} not run (of {{ $viewResults['in_scope'] ?? $viewResults['total'] }}){{ ($viewResults['na'] ?? 0) > 0 ? ' · '.$viewResults['na'].' N/A excluded' : '' }}
@else

No results yet — Download the file, fill the Status column, then Upload results.

@endif @if ($viewEffort)
@include('partials.effort-card', ['effort' => $viewEffort])
@endif @if ($viewRows === null)

No structured test-case table could be read from this file — use Download to open it.

@else @php $vGroups = collect($viewRows)->groupBy(fn ($r) => trim($r['category'] ?? '')); $vHasCat = $vGroups->keys()->contains(fn ($k) => $k !== ''); $vSmpp = $viewingCase && strtoupper((string) $viewingCase->projectLabel()) === 'SMPP'; $vTwoLevel = $vSmpp && collect($viewRows)->contains(fn ($r) => trim($r['title'] ?? '') !== ''); $vNorm = fn ($r) => \App\Services\TestResultSummary::normalize($r['status'] ?? ''); // "All pass" ignores N/A rows — they're out of scope, exactly as the header's // figures treat them (TestResultSummary: in_scope = total − na). Without this a // fully-passed group with one N/A row never turns green. $vScope = fn ($rows) => collect($rows)->reject(fn ($r) => $vNorm($r) === 'na'); $vAllPass = function ($rows) use ($vScope, $vNorm) { $s = $vScope($rows); // An all-N/A group has nothing to complete, so it isn't "all pass". return $s->isNotEmpty() && $s->every(fn ($r) => $vNorm($r) === 'passed'); }; // Fully-passed groups start collapsed — seed the Alpine cats/titles state (key => false = collapsed). $vCollapsedCats = []; $vCollapsedTitles = []; $ci = 0; foreach ($vGroups as $vCat => $vGroup) { $ckk = 'c'.$ci; // Never auto-collapse the QA-Added (no-category) group — the QA's additions // should always be visible, even if every added row passed. $vCatCollapsed = trim((string) $vCat) !== '' && $vAllPass($vGroup); if ($vCatCollapsed) { $vCollapsedCats[$ckk] = false; } // QA-Added rows have no title either → render them flat (no "(untitled)" sub-bar). $vRowGroups = ($vTwoLevel && trim((string) $vCat) !== '') ? collect($vGroup)->groupBy(fn ($r) => trim($r['title'] ?? '')) : collect(['' => $vGroup]); $ti = 0; foreach ($vRowGroups as $vTRows) { // Only pre-collapse a passed title INSIDE a category that stays open — // there it hides noise next to the failures worth reading. Collapsing // both levels means opening an all-pass category reveals nothing but // more collapsed bars, so the whole category folds on its own instead. if (! $vCatCollapsed && $vAllPass($vTRows)) { $vCollapsedTitles[$ckk.'t'.$ti] = false; } $ti++; } $ci++; } @endphp @php $vRowIx = 0; @endphp @foreach ($vGroups as $cat => $group) @php $ck = 'c'.$loop->index; $vIsQaAdded = trim((string) $cat) === ''; $catFailCount = collect($group)->filter(fn ($r) => $vNorm($r) === 'failed')->count(); $catAllPass = $vAllPass($group); $catScope = $vScope($group)->count(); $catNa = count($group) - $catScope; @endphp @if ($vHasCat) @endif @php $rowGroups = ($vTwoLevel && ! $vIsQaAdded) ? collect($group)->groupBy(fn ($r) => trim($r['title'] ?? '')) : collect(['' => $group]); @endphp @foreach ($rowGroups as $title => $tRows) @php $tk = $ck.'t'.$loop->index; $titleFailCount = collect($tRows)->filter(fn ($r) => $vNorm($r) === 'failed')->count(); $titleAllPass = $vAllPass($tRows); $titleScope = $vScope($tRows)->count(); $titleNa = count($tRows) - $titleScope; @endphp @if ($vTwoLevel && ! $vIsQaAdded) @endif @foreach ($tRows as $r) @php $sk = \App\Services\TestResultSummary::normalize($r['status'] ?? ''); // Stable per-row key (survives result re-uploads): the TC id, else a hash. $rowId = \App\Services\TestCaseReader::rowId($r); $rowBugs = $viewBugLinks[$rowId] ?? []; @endphp @php $vRowIx++; @endphp @endforeach @endforeach @endforeach
IDScenarioPreconditionsStepsTest DataExpected ResultPriorityTypeStatusQA Comments
{{ $vIsQaAdded ? 'QA-Added' : $cat }} {{ count($group) }} @if ($vIsQaAdded)· additional (not in original requirements)@endif @if ($catFailCount)✕ {{ $catFailCount }} failed@elseif ($catAllPass && ! $vIsQaAdded)✓ all pass@endif @if (! empty($viewCatEfforts[$cat]))~{{ $viewCatEfforts[$cat]['days'] }} d@endif
{{ $title !== '' ? $title : '(untitled)' }} {{ count($tRows) }} @if ($titleFailCount)✕ {{ $titleFailCount }} failed@elseif ($titleAllPass)✓ all pass@endif
{{ $r['id'] }} {{ $r['scenario'] }} {{ $r['preconditions'] }} {{ $r['steps'] }} {{ $r['test_data'] }} {{ $r['expected_result'] }} {{ $r['priority'] }} {{ $r['type'] }}
@if (trim($r['status'] ?? '') !== '' && $sk !== 'not_run'){{ $r['status'] }}@else@endif
@if ($rowBugs)
@foreach ($rowBugs as $bugKey) @if ($jiraBase){{ $bugKey }}@else{{ $bugKey }}@endif @endforeach
@endif @if ($sk === 'failed') @endif
{{ $r['qa_comments'] ?? '' }}

{{ count($viewRows) }} test case{{ count($viewRows) === 1 ? '' : 's' }}.

@endif
@endif {{-- Bug picker (opened from a failed row) — lists bugs linked to this file's Jira story. --}} @if ($bugPickerRow !== null)
Link a bug to {{ $bugPickerRow }} @if ($viewingCase?->jira_key)· bugs linked to {{ $viewingCase->jira_key }}@endif
@php $already = $viewBugLinks[$bugPickerRow] ?? []; @endphp @if (empty($bugPickerOptions))

@if ($viewingCase?->jira_key) No bugs are linked to {{ $viewingCase->jira_key }} in Jira. @else This test case isn't linked to a Jira story, so there are no bugs to pick from. @endif

@else
@foreach ($bugPickerOptions as $b) @php $isLinked = in_array($b['key'], $already, true); $linkedTcs = $bugLinkedRows[$b['key']] ?? []; @endphp @endforeach
BugSummary Linked TCsStatus
@unless ($isLinked)@endunless @if ($jiraBase){{ $b['key'] }}@else{{ $b['key'] }}@endif {{ $b['summary'] }} @if ($linkedTcs) @foreach ($linkedTcs as $tc){{ $tc }}@endforeach @else@endif {{ $b['status'] }} @if ($isLinked)linked ✓ @else@endif
@endif
@endif {{-- Story-link picker: the file's PROJECT quarter-target stories, multi-select + search. --}} @if ($storyPickerId !== null && $storyPickerCase) @php // One haystack per option, built once and reused by both the row filter and the // select-all header, so "select all shown" can never disagree with what's on screen. $hay = fn ($s) => mb_strtolower($s['key'].' '.$s['title']); $pickerRows = array_map(fn ($s) => ['key' => $s['key'], 'hay' => $hay($s)], $storyOptions); @endphp
Link stories to {{ \Illuminate\Support\Str::limit($storyPickerCase->displayName(), 40) }} · {{ $storyPickerCase->projectLabel() ?? 'all' }} quarter targets · {{ $storyPickerCase->quarter }}
@if (empty($storyOptions))

No {{ $storyPickerCase->projectLabel() }} quarter-target stories for {{ $storyPickerCase->quarter }}. Upload them in Import Dashboard.

@else
@foreach ($storyOptions as $s) @endforeach
StoryTitleStatus
@if ($jiraBase){{ $s['key'] }}@else{{ $s['key'] }}@endif {{ $s['title'] }} @if ($s['status']){{ $s['status'] }}@endif
@endif
{{-- Total picked, not just the visible ones: a filter hides rows, it doesn't deselect them, so the count has to say what will actually be saved. --}}
@endif