File size: 7,406 Bytes
e4daa3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# Spec 37.1: Foundation Components

**Status**: READY FOR IMPLEMENTATION
**Phase**: 1 of 5
**Depends On**: Spec 37.0 (Project Setup)
**Goal**: TDD implementation of Layout and MetricsPanel components

---

## Deliverables

By the end of this phase, you will have:

1. `Layout` component with header and main content area
2. `MetricsPanel` component displaying segmentation results
3. 100% test coverage for both components
4. Visual verification in browser

---

## Component 1: Layout

### Test First

Create `src/components/__tests__/Layout.test.tsx`:

```typescript
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Layout } from '../Layout'

describe('Layout', () => {
  it('renders header with title', () => {
    render(<Layout>Content</Layout>)

    expect(
      screen.getByRole('heading', { name: /stroke lesion segmentation/i })
    ).toBeInTheDocument()
  })

  it('renders subtitle', () => {
    render(<Layout>Content</Layout>)

    expect(screen.getByText(/deepisles segmentation/i)).toBeInTheDocument()
  })

  it('renders children in main area', () => {
    render(
      <Layout>
        <div data-testid="child">Test Child</div>
      </Layout>
    )

    expect(screen.getByTestId('child')).toBeInTheDocument()
  })

  it('has accessible landmark structure', () => {
    render(<Layout>Content</Layout>)

    expect(screen.getByRole('banner')).toBeInTheDocument()
    expect(screen.getByRole('main')).toBeInTheDocument()
  })

  it('applies dark theme styling', () => {
    render(<Layout>Content</Layout>)

    const container = screen.getByRole('banner').parentElement
    expect(container).toHaveClass('bg-gray-950')
  })
})
```

### Implementation

Create `src/components/Layout.tsx`:

```typescript
import { ReactNode } from 'react'

interface LayoutProps {
  children: ReactNode
}

export function Layout({ children }: LayoutProps) {
  return (
    <div className="min-h-screen bg-gray-950 text-white">
      <header className="border-b border-gray-800 py-4">
        <div className="container mx-auto px-4">
          <h1 className="text-2xl font-bold">Stroke Lesion Segmentation</h1>
          <p className="text-gray-400 text-sm mt-1">
            DeepISLES segmentation on ISLES24 dataset
          </p>
        </div>
      </header>
      <main className="container mx-auto px-4 py-6">{children}</main>
    </div>
  )
}
```

### Verify

```bash
npm test -- Layout
# Expected: 5 tests passing
```

---

## Component 2: MetricsPanel

### Test First

Create `src/components/__tests__/MetricsPanel.test.tsx`:

```typescript
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MetricsPanel } from '../MetricsPanel'

describe('MetricsPanel', () => {
  const defaultMetrics = {
    caseId: 'sub-stroke0001',
    diceScore: 0.847,
    volumeMl: 15.32,
    elapsedSeconds: 12.5,
  }

  it('renders results heading', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    expect(
      screen.getByRole('heading', { name: /results/i })
    ).toBeInTheDocument()
  })

  it('displays case ID', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    expect(screen.getByText('sub-stroke0001')).toBeInTheDocument()
  })

  it('displays dice score with 3 decimal places', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    expect(screen.getByText('0.847')).toBeInTheDocument()
  })

  it('displays volume in mL with 2 decimal places', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    expect(screen.getByText('15.32 mL')).toBeInTheDocument()
  })

  it('displays elapsed time with 1 decimal place', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    expect(screen.getByText('12.5s')).toBeInTheDocument()
  })

  it('hides dice score row when null', () => {
    render(
      <MetricsPanel metrics={{ ...defaultMetrics, diceScore: null }} />
    )

    expect(screen.queryByText(/dice score/i)).not.toBeInTheDocument()
  })

  it('hides volume row when null', () => {
    render(
      <MetricsPanel metrics={{ ...defaultMetrics, volumeMl: null }} />
    )

    expect(screen.queryByText(/volume/i)).not.toBeInTheDocument()
  })

  it('applies card styling', () => {
    render(<MetricsPanel metrics={defaultMetrics} />)

    const panel = screen.getByRole('heading', { name: /results/i }).parentElement
    expect(panel).toHaveClass('bg-gray-800', 'rounded-lg')
  })
})
```

### Implementation

Create `src/components/MetricsPanel.tsx`:

```typescript
interface Metrics {
  caseId: string
  diceScore: number | null
  volumeMl: number | null
  elapsedSeconds: number
}

interface MetricsPanelProps {
  metrics: Metrics
}

export function MetricsPanel({ metrics }: MetricsPanelProps) {
  return (
    <div className="bg-gray-800 rounded-lg p-4 space-y-3">
      <h3 className="font-medium text-lg">Results</h3>

      <div className="grid grid-cols-2 gap-3 text-sm">
        <div>
          <span className="text-gray-400">Case:</span>
          <span className="ml-2 font-mono">{metrics.caseId}</span>
        </div>

        {metrics.diceScore !== null && (
          <div>
            <span className="text-gray-400">Dice Score:</span>
            <span className="ml-2 font-mono text-green-400">
              {metrics.diceScore.toFixed(3)}
            </span>
          </div>
        )}

        {metrics.volumeMl !== null && (
          <div>
            <span className="text-gray-400">Volume:</span>
            <span className="ml-2 font-mono">
              {metrics.volumeMl.toFixed(2)} mL
            </span>
          </div>
        )}

        <div>
          <span className="text-gray-400">Time:</span>
          <span className="ml-2 font-mono">
            {metrics.elapsedSeconds.toFixed(1)}s
          </span>
        </div>
      </div>
    </div>
  )
}
```

### Verify

```bash
npm test -- MetricsPanel
# Expected: 8 tests passing
```

---

## Create Index Export

Create `src/components/index.ts`:

```typescript
export { Layout } from './Layout'
export { MetricsPanel } from './MetricsPanel'
```

---

## Visual Verification

Update `src/App.tsx` to see components:

```typescript
import { Layout } from './components/Layout'
import { MetricsPanel } from './components/MetricsPanel'

const mockMetrics = {
  caseId: 'sub-stroke0001',
  diceScore: 0.847,
  volumeMl: 15.32,
  elapsedSeconds: 12.5,
}

function App() {
  return (
    <Layout>
      <div className="max-w-md">
        <MetricsPanel metrics={mockMetrics} />
      </div>
    </Layout>
  )
}

export default App
```

Run dev server and verify visually:

```bash
npm run dev
# Open http://localhost:5173
```

---

## Verification Checklist

- [ ] `npm test` - All 13+ tests pass
- [ ] `npm run dev` - Components render correctly
- [ ] Header shows "Stroke Lesion Segmentation"
- [ ] MetricsPanel shows all metrics with correct formatting
- [ ] Dark theme applies correctly

---

## File Structure After This Phase

```
frontend/src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ __tests__/
β”‚   β”‚   β”œβ”€β”€ Layout.test.tsx
β”‚   β”‚   └── MetricsPanel.test.tsx
β”‚   β”œβ”€β”€ Layout.tsx
β”‚   β”œβ”€β”€ MetricsPanel.tsx
β”‚   └── index.ts
β”œβ”€β”€ mocks/
β”œβ”€β”€ test/
β”œβ”€β”€ App.tsx (updated)
└── ...
```

---

## Next Phase

Once verification passes, proceed to **Spec 37.2: API Layer**