test contracts
This commit is contained in:
@@ -16,64 +16,79 @@
|
||||
* @UX_STATE: Error -> Inline error with retry preserving filters.
|
||||
* @UX_FEEDBACK: Filter change reloads list immediately.
|
||||
* @UX_RECOVERY: Retry and clear filters actions available.
|
||||
*
|
||||
* @TEST_CONTRACT Page_Reports ->
|
||||
* {
|
||||
* required_props: {},
|
||||
* optional_props: {},
|
||||
* invariants: [
|
||||
* "Loads reports on mount using query filters",
|
||||
* "Reloads reports automatically upon filter changes",
|
||||
* "Loads selected report detail on click"
|
||||
* ]
|
||||
* }
|
||||
* @TEST_FIXTURE init_state -> {}
|
||||
* @TEST_EDGE server_error -> shows error block and retry button
|
||||
* @TEST_EDGE empty_filtered_list -> displays \"No results matching filters\"
|
||||
* @TEST_INVARIANT pagination_and_filtering -> verifies: [init_state]
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { PageHeader } from '$lib/ui';
|
||||
import { getReports, getReportDetail } from '$lib/api/reports.js';
|
||||
import ReportsList from '$lib/components/reports/ReportsList.svelte';
|
||||
import ReportDetailPanel from '$lib/components/reports/ReportDetailPanel.svelte';
|
||||
import { onMount } from "svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import { PageHeader } from "$lib/ui";
|
||||
import { getReports, getReportDetail } from "$lib/api/reports.js";
|
||||
import ReportsList from "$lib/components/reports/ReportsList.svelte";
|
||||
import ReportDetailPanel from "$lib/components/reports/ReportDetailPanel.svelte";
|
||||
|
||||
let loading = true;
|
||||
let error = '';
|
||||
let error = "";
|
||||
let collection = null;
|
||||
let selectedReport = null;
|
||||
let selectedReportDetail = null;
|
||||
|
||||
let taskType = 'all';
|
||||
let status = 'all';
|
||||
let taskType = "all";
|
||||
let status = "all";
|
||||
let page = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
const TASK_TYPE_OPTIONS = [
|
||||
{ value: 'all', label: $t.reports?.all_types },
|
||||
{ value: 'llm_verification', label: 'LLM' },
|
||||
{ value: 'backup', label: $t.nav?.backups },
|
||||
{ value: 'migration', label: $t.nav?.migration },
|
||||
{ value: 'documentation', label: 'Documentation' }
|
||||
{ value: "all", label: $t.reports?.all_types },
|
||||
{ value: "llm_verification", label: "LLM" },
|
||||
{ value: "backup", label: $t.nav?.backups },
|
||||
{ value: "migration", label: $t.nav?.migration },
|
||||
{ value: "documentation", label: "Documentation" },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'all', label: $t.reports?.all_statuses },
|
||||
{ value: 'success', label: 'Success' },
|
||||
{ value: 'failed', label: 'Failed' },
|
||||
{ value: 'in_progress', label: 'In progress' },
|
||||
{ value: 'partial', label: 'Partial' }
|
||||
{ value: "all", label: $t.reports?.all_statuses },
|
||||
{ value: "success", label: "Success" },
|
||||
{ value: "failed", label: "Failed" },
|
||||
{ value: "in_progress", label: "In progress" },
|
||||
{ value: "partial", label: "Partial" },
|
||||
];
|
||||
|
||||
function buildQuery() {
|
||||
return {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
task_types: taskType === 'all' ? [] : [taskType],
|
||||
statuses: status === 'all' ? [] : [status],
|
||||
sort_by: 'updated_at',
|
||||
sort_order: 'desc'
|
||||
task_types: taskType === "all" ? [] : [taskType],
|
||||
statuses: status === "all" ? [] : [status],
|
||||
sort_by: "updated_at",
|
||||
sort_order: "desc",
|
||||
};
|
||||
}
|
||||
|
||||
async function loadReports({ silent = false } = {}) {
|
||||
try {
|
||||
if (!silent) loading = true;
|
||||
error = '';
|
||||
error = "";
|
||||
collection = await getReports(buildQuery());
|
||||
if (!selectedReport && collection?.items?.length) {
|
||||
selectedReport = collection.items[0];
|
||||
selectedReportDetail = await getReportDetail(selectedReport.report_id);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e?.message || 'Failed to load reports';
|
||||
error = e?.message || "Failed to load reports";
|
||||
collection = null;
|
||||
} finally {
|
||||
if (!silent) loading = false;
|
||||
@@ -81,12 +96,12 @@
|
||||
}
|
||||
|
||||
function hasActiveFilters() {
|
||||
return taskType !== 'all' || status !== 'all';
|
||||
return taskType !== "all" || status !== "all";
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
taskType = 'all';
|
||||
status = 'all';
|
||||
taskType = "all";
|
||||
status = "all";
|
||||
page = 1;
|
||||
selectedReport = null;
|
||||
selectedReportDetail = null;
|
||||
@@ -112,7 +127,7 @@
|
||||
|
||||
<div class="mx-auto w-full max-w-7xl space-y-4">
|
||||
<PageHeader
|
||||
title={$t.reports?.title }
|
||||
title={$t.reports?.title}
|
||||
subtitle={() => null}
|
||||
actions={() => null}
|
||||
/>
|
||||
@@ -143,38 +158,52 @@
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||
on:click={() => loadReports()}
|
||||
>
|
||||
{$t.common?.refresh }
|
||||
{$t.common?.refresh}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||
on:click={clearFilters}
|
||||
>
|
||||
{$t.reports?.clear_filters }
|
||||
{$t.reports?.clear_filters}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||
{$t.reports?.loading }
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm"
|
||||
>
|
||||
{$t.reports?.loading}
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 shadow-sm">
|
||||
<div
|
||||
class="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 shadow-sm"
|
||||
>
|
||||
<p>{error}</p>
|
||||
<button class="mt-2 inline-flex items-center justify-center rounded-lg border border-red-300 px-3 py-1 text-sm font-medium text-red-700 transition-colors hover:bg-red-100" on:click={() => loadReports()}>
|
||||
{$t.reports?.retry_load || $t.common?.retry }
|
||||
<button
|
||||
class="mt-2 inline-flex items-center justify-center rounded-lg border border-red-300 px-3 py-1 text-sm font-medium text-red-700 transition-colors hover:bg-red-100"
|
||||
on:click={() => loadReports()}
|
||||
>
|
||||
{$t.reports?.retry_load || $t.common?.retry}
|
||||
</button>
|
||||
</div>
|
||||
{:else if !collection || collection.total === 0}
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||
{$t.reports?.empty }
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm"
|
||||
>
|
||||
{$t.reports?.empty}
|
||||
</div>
|
||||
{:else if collection.items.length === 0 && hasActiveFilters()}
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||
<p>{$t.reports?.filtered_empty }</p>
|
||||
<button class="mt-2 inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50" on:click={clearFilters}>
|
||||
{$t.reports?.clear_filters }
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm"
|
||||
>
|
||||
<p>{$t.reports?.filtered_empty}</p>
|
||||
<button
|
||||
class="mt-2 inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||
on:click={clearFilters}
|
||||
>
|
||||
{$t.reports?.clear_filters}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -191,4 +220,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:UnifiedReportsPage:Component] -->
|
||||
<!-- [/DEF:UnifiedReportsPage:Component] -->
|
||||
|
||||
Reference in New Issue
Block a user