From 20d7d8153d8cfe1e781be4fe0f7d4132f63f6e51 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sun, 3 May 2026 17:55:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(frontend):=20spli?= =?UTF-8?q?t=20HealthDashboard=20into=20smart=20wrapper=20+=20dumb=20View?= =?UTF-8?q?=20for=20state-based=20stories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback (PR #32 commit, T13 follow-up): HealthDashboard.stories.ts could not demonstrate Loading or Error states because the component used useFetch internally and didn't accept props. Same limitation made unit-testing the rendering branches impossible without mocking the Nuxt fetch layer. Split into 2 files (SRP / DDD modular per code-reviewer skill): - HealthDashboardView.vue (NEW): pure presentational, accepts data/pending/error as props. Adds explicit data-testid="health-loading" + "health-error" so e2e and unit tests can target each branch. - HealthDashboard.vue (REFACTORED): now a thin smart wrapper that calls useFetch('/api/healthz') and forwards data/pending/error to HealthDashboardView. Stories: - HealthDashboardView.stories.ts (NEW): 4 stories — Healthy, Loading, ErrorState, HealthyHighUptime. Reviewers can now see all branches without running the backend. - HealthDashboard.stories.ts: still has the Default story for the wrapper (smoke). Tooling: - shims-vue.d.ts: Vue file module declaration with permissive any-typing for the DefineComponent. Required because Vue 3 strict TS + Storybook propagates prop types poorly through .vue imports otherwise (false-positive TS2353 errors). Backwards compatibility: - pages/index.vue still imports (unchanged). - All existing data-testid attributes preserved (health-dashboard, health-info, health-status). The new health-loading and health-error testids are additive. - The Playwright tests from PR #32 continue to pass without modification. 🤖 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/HealthDashboard.stories.ts | 27 +++---- frontend/components/HealthDashboard.vue | 25 ++---- .../components/HealthDashboardView.stories.ts | 79 +++++++++++++++++++ frontend/components/HealthDashboardView.vue | 30 +++++++ frontend/shims-vue.d.ts | 6 ++ 5 files changed, 133 insertions(+), 34 deletions(-) create mode 100644 frontend/components/HealthDashboardView.stories.ts create mode 100644 frontend/components/HealthDashboardView.vue create mode 100644 frontend/shims-vue.d.ts diff --git a/frontend/components/HealthDashboard.stories.ts b/frontend/components/HealthDashboard.stories.ts index 6980774..56e78d4 100644 --- a/frontend/components/HealthDashboard.stories.ts +++ b/frontend/components/HealthDashboard.stories.ts @@ -5,6 +5,16 @@ const meta: Meta = { title: 'Components/HealthDashboard', component: HealthDashboard, tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Smart wrapper that calls /api/healthz internally and delegates rendering to HealthDashboardView. ' + + 'For state-by-state previews (Healthy, Loading, Error), see ' + + '[HealthDashboardView stories](?path=/docs/components-healthdashboardview--docs).', + }, + }, + }, } export default meta @@ -14,20 +24,3 @@ type Story = StoryObj export const Default: Story = { args: {}, } - -// Documents the loading/error visuals so reviewers see them in Storybook -// without needing a backend down. The component currently doesn't accept overrides -// because it uses useFetch internally - this story is a placeholder for a future -// refactor that exposes data/pending/error as props for testability. -export const DocumentationStub: Story = { - args: {}, - parameters: { - docs: { - description: { - story: - 'Renders the same as Default. The HealthDashboard fetches /api/healthz internally, so loading/error states only appear when the backend is down or slow. ' + - 'Future improvement: accept healthData/pending/error as props for easy state-based stories. Until then, see frontend/docs/e2e/ for screenshots of both healthy and error states.', - }, - }, - }, -} diff --git a/frontend/components/HealthDashboard.vue b/frontend/components/HealthDashboard.vue index 8d42398..858b705 100644 --- a/frontend/components/HealthDashboard.vue +++ b/frontend/components/HealthDashboard.vue @@ -1,22 +1,13 @@ + diff --git a/frontend/components/HealthDashboardView.stories.ts b/frontend/components/HealthDashboardView.stories.ts new file mode 100644 index 0000000..942fa2a --- /dev/null +++ b/frontend/components/HealthDashboardView.stories.ts @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import HealthDashboardView from './HealthDashboardView.vue' + +interface ViewArgs { + data: { + status: string + version: string + uptime_seconds: number + timestamp: string + } | null + pending: boolean + error: { message: string } | null +} + +const meta = { + title: 'Components/HealthDashboardView', + component: HealthDashboardView, + tags: ['autodocs'], + argTypes: { + pending: { control: 'boolean' }, + }, + parameters: { + docs: { + description: { + component: + 'Pure presentational component for the health dashboard. ' + + 'Accepts `data`, `pending`, `error` as props so all 3 states can be ' + + 'previewed in Storybook and asserted in unit tests. The data fetching ' + + 'wrapper is `HealthDashboard.vue`.', + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Healthy: Story = { + args: { + data: { + status: 'healthy', + version: '1.4.0', + uptime_seconds: 3600, + timestamp: '2026-05-03T17:30:00.000Z', + }, + pending: false, + error: null, + }, +} + +export const Loading: Story = { + args: { + data: null, + pending: true, + error: null, + }, +} + +export const ErrorState: Story = { + args: { + data: null, + pending: false, + error: { message: '[GET] "/api/healthz": 502 Bad Gateway (simulated)' }, + }, +} + +export const HealthyHighUptime: Story = { + args: { + data: { + status: 'healthy', + version: '1.5.0-rc1', + uptime_seconds: 86400 * 7, + timestamp: new Date().toISOString(), + }, + pending: false, + error: null, + }, +} diff --git a/frontend/components/HealthDashboardView.vue b/frontend/components/HealthDashboardView.vue new file mode 100644 index 0000000..46741fe --- /dev/null +++ b/frontend/components/HealthDashboardView.vue @@ -0,0 +1,30 @@ + + + diff --git a/frontend/shims-vue.d.ts b/frontend/shims-vue.d.ts new file mode 100644 index 0000000..576404f --- /dev/null +++ b/frontend/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const component: DefineComponent + export default component +}