Project: workspace-doctolib · Branch: HEAD · Mode: ` · **Model:**claude-sonnet-4-6`
Stats: 1 user messages, 7 tool calls, tools used: Read.
Conversation
Turn 1 — User
Analyze test coverage quality and completeness for the NhrDocumentCategoryFilter component changes.
Test files to review: 1. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.test.tsx 2. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/factories/filters/createNhrDocumentCategoryFilterConfig.test.tsx
Source files under test: 1. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.tsx 2. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.hooks.ts 3. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/factories/filters/createNhrDocumentCategoryFilterConfig.tsx
Context: This implements a custom multi-select dropdown with: - open/close via click and keyboard (Escape) - close on click-outside via pointerdown on document - checkbox toggle for multi-selection - clear button - live search with diacritic normalization for French/German text - option.disabled support
Testing rules location: /Users/USER/workspace/doctolib/treatment-plan/.claude/rules/frontend/react/test/testing.md and testing-patterns.md
Please identify any critical gaps in test coverage.
Turn 1 — Assistant
I'll analyze the test coverage by reading all relevant files in parallel.
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.test.tsx
Tool results:
→ result (ok): 1 import { setup } from '@doctolib/test-helpers'
2 import { screen } from '@testing-library/react'
3 import { afterEach, describe, expect, it, vi } from 'vitest'
4
5 import { NhrDocumentCategoryFilter } from './NhrDocumentCategoryFilter'
6
7 const sampleOptions = [
8 { value: 'A', label: 'Report A' },
9 { value: 'B', label: 'Report B' },
10 { value: 'C', label: 'Report C' },
11 ]
12
13 describe('NhrDocumentCategoryFilter', () => {
14 afterEach(() => vi.clearAllMocks())
15
16 const
…(truncated, 8852 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/factories/filters/createNhrDocumentCategoryFilterConfig.test.tsx
Tool results:
→ result (ok): 1 import { setup } from '@doctolib/test-helpers'
2 import { screen } from '@testing-library/react'
3 import { describe, expect, it, vi } from 'vitest'
4
5 import { mockPatientInfo } from '../../../test-helpers'
6 import { createNhrDocumentCategoryFilterConfig } from './createNhrDocumentCategoryFilterConfig'
7
8 const ctx = { patient: mockPatientInfo }
9
10 const sampleOptions = [
11 { value: 'A', label: 'Report A' },
12 { value: 'B', label: 'Report B' },
13 { value: 'C', label: 'Report
…(truncated, 3984 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.tsx
Tool results:
→ result (ok): 1 import { ChevronDownRegularIcon } from '@doctolib/design-system-icons/ChevronDownRegularIcon'
2 import { Checkbox } from '@doctolib/design-system/Checkbox/v2'
3 import { IconButton } from '@doctolib/design-system/IconButton/v2'
4 import type { Option } from '@doctolib/design-system/Option'
5 import { useCallback, useId, useRef, useState } from 'react'
6
7 import { useNhrDocumentCategoryClose } from './NhrDocumentCategoryFilter.hooks'
8
9 const normalize = (s: string) => s.normalize('NFD').re
…(truncated, 3978 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentCategoryFilter/NhrDocumentCategoryFilter.hooks.ts
Tool results:
→ result (ok): 1 import { type RefObject, useEffect } from 'react'
2
3 export const useNhrDocumentCategoryClose = (
4 containerRef: RefObject<HTMLDivElement | null>,
5 onClose: () => void,
6 ) => {
7 useEffect(() => {
8 const handlePointerDown = (e: PointerEvent) => {
9 if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
10 onClose()
11 }
12 }
13
14 const handleKeyDown = (e: KeyboardEvent) => {
15 if (e.key === 'Escape') onClose()
16
…(truncated, 344 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/factories/filters/createNhrDocumentCategoryFilterConfig.tsx
Tool results:
→ result (ok): 1 import type { Option } from '@doctolib/design-system/Option'
2
3 import { NhrDocumentCategoryFilter } from '../../../components/NhrDocumentList/NhrDocumentCategoryFilter'
4 import type { FilterConfig } from '../../../types'
5
6 export const createNhrDocumentCategoryFilterConfig = ({
7 options = [],
8 label = 'Document Category',
9 placeholder,
10 }: {
11 options?: Option[]
12 label?: string
13 placeholder?: string
14 } = {}): FilterConfig => ({
15 key: 'typeCodes',
16 render:
…(truncated, 631 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/.claude/rules/frontend/react/test/testing.md
Tool results:
→ result (ok): 1 ---
2 paths:
3 - "packages//*.test.ts"
4 - "packages//.test.tsx"
5 - "packages//.spec.ts"
6 - "packages//*.spec.tsx"
7 - "packages//vitest.config.ts"
8 - "packages//tests//"
9 ---
10
11 # Frontend Testing Conventions
12
13 ## Framework & Tools
14
15 - Test runner: Vitest
16 - Component testing: React Testing Library
17 - HTTP mocking: MSW (Mock Service Worker)
18 - User interactions: @testing-library/user-event
19 - Storybook tests*: `@story
…(truncated, 3262 more chars)
Now let me also read the testing-patterns file to get the full picture:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/.claude/rules/frontend/react/test/testing-patterns.md(1–30)
Tool results:
→ result (ok): 1 ---
2 paths:
3 - "packages//*.test.ts"
4 - "packages//.test.tsx"
5 - "packages//.spec.ts"
6 - "packages//*.spec.tsx"
7 - "packages//vitest.config.ts"
8 - "packages//tests//*"
9 ---
10
11 # Frontend Testing Patterns
12
13 Advanced testing patterns for the TPF frontend. For core conventions (MSW, fixtures, Vitest config), see testing.md.
14
15 ## Component Testing
16
17 ### Basic Pattern
18
19 ```typescript
20 import { describe, it, expect, vi, afterEach } from
…(truncated, 337 more chars)
I now have all the information needed for a thorough analysis. Let me compile the findings.
Summary
The test suite is well-structured and covers most of the component's happy-path and interaction surface. Both test files follow project conventions (RTL with setup(), vi.fn() mocks, behavioral assertions, no snapshot testing, no CSS selectors). The NhrDocumentCategoryFilter.test.tsx is thorough for the UI layer and the createNhrDocumentCategoryFilterConfig.test.tsx correctly validates the filter factory integration.
However, several important behavioral contracts are either untested or only partially tested, and the hook that owns two distinct behaviors (click-outside via pointerdown, and Escape closing from anywhere) has no dedicated tests. The option.disabled feature has zero test coverage. Below is the full gap analysis.
Critical Gaps
1. option.disabled support is completely untested — Criticality: 9/10
The source renders isDisabled={option.disabled ?? false} on each Checkbox. There is no test verifying that a disabled option cannot be toggled. A future refactor that accidentally removed the isDisabled prop or inverted the boolean would silently break the contract. This is production-facing behavior.
What to add:
it('should not call onToggle when a disabled option is clicked', async () => {
const onToggle = vi.fn()
const { user } = renderComponent({
onToggle,
options: [{ value: 'A', label: 'Report A', disabled: true }],
})
await user.click(screen.getByRole('combobox', { name: 'Category' }))
await user.click(screen.getByRole('checkbox', { name: 'Report A' }))
expect(onToggle).not.toHaveBeenCalled()
})
it('should render a disabled option as disabled', async () => {
const { user } = renderComponent({
options: [{ value: 'A', label: 'Report A', disabled: true }],
})
await user.click(screen.getByRole('combobox', { name: 'Category' }))
expect(screen.getByRole('checkbox', { name: 'Report A' })).toBeDisabled()
})
Regression this catches: removing isDisabled, flipping the default fallback from false to true, or passing the prop with the wrong name.
2. Diacritic normalization (French/German text) is not tested — Criticality: 8/10
The normalize function is documented as a core feature. Searching "medecin" should surface "médecin"; searching "uble" should surface "über". Neither the component test nor the factory test covers this path. The entire NFD normalization branch is dead from a test perspective.
What to add in NhrDocumentCategoryFilter.test.tsx:
it('should match options regardless of diacritics in the query', async () => {
const { user } = setup(
<NhrDocumentCategoryFilter
label="Category"
values={[]}
options={[
{ value: 'A', label: 'Médecin traitant' },
{ value: 'B', label: 'Uber Arzt' },
]}
onToggle={vi.fn()}
onClear={vi.fn()}
/>
)
await user.click(screen.getByRole('combobox', { name: 'Category' }))
await user.type(screen.getByRole('combobox', { name: 'Category' }), 'medecin')
expect(screen.getByRole('checkbox', { name: 'Médecin traitant' })).toBeInTheDocument()
expect(screen.queryByRole('checkbox', { name: 'Uber Arzt' })).not.toBeInTheDocument()
})
it('should match options when the label contains diacritics and the query does not', async () => {
const { user } = setup(
<NhrDocumentCategoryFilter
label="Category"
values={[]}
options={[{ value: 'A', label: 'über den Dächern' }]}
onToggle={vi.fn()}
onClear={vi.fn()}
/>
)
await user.click(screen.getByRole('combobox', { name: 'Category' }))
await user.type(screen.getByRole('combobox', { name: 'Category' }), 'uber den dachern')
expect(screen.getByRole('checkbox', { name: 'über den Dächern' })).toBeInTheDocument()
})
Regression this catches: changing the normalize implementation, removing the NFD step, or accidentally applying normalization only to one side of the comparison.
3. useNhrDocumentCategoryClose hook has no isolated tests — Criticality: 7/10
The hook owns two distinct behaviors that are only incidentally covered through component tests:
- The pointerdown listener fires on any pointer interaction outside the container (not just click). The existing "close on click-outside" test uses user.click, which produces pointerdown as a side-effect, but it would not catch a regression that switched the listener to click instead of pointerdown.
- The keydown listener fires on any focused element in the document, not just the component — but the current tests only press Escape when the trigger or a checkbox is focused. A case where focus is entirely outside the component (e.g., focus on document.body) is untested.
- Listener cleanup (the removeEventListener teardown) is never exercised.
What to add in a new NhrDocumentCategoryFilter.hooks.test.ts or inline in the component test:
it('should close dropdown when pointerdown fires outside even without a click', async () => {
const { user } = renderComponent()
await user.click(screen.getByRole('combobox', { name: 'Category' }))
expect(screen.getByRole('checkbox', { name: 'Report A' })).toBeInTheDocument()
// Fire a raw pointerdown on body without the full click sequence
fireEvent.pointerDown(document.body)
expect(screen.queryByRole('checkbox', { name: 'Report A' })).not.toBeInTheDocument()
})
it('should close dropdown when Escape is pressed while focus is on an unrelated element', async () => {
const { user } = renderComponent()
await user.click(screen.getByRole('combobox', { name: 'Category' }))
document.body.focus()
await user.keyboard('{Escape}')
expect(screen.queryByRole('checkbox', { name: 'Report A' })).not.toBeInTheDocument()
})
Regression this catches: switching pointerdown to click (would miss drag-to-close), or scoping the Escape listener to only the container instead of the document.
Important Improvements
4. The trigger input does not toggle closed on second click — Criticality: 6/10
handleInputClick only sets isOpen when !isOpen. Clicking the trigger a second time while the dropdown is open does nothing (focus stays on the already-open input). This is a deliberate UX choice, but the test suite does not assert it. A future change that adds a toggle could inadvertently break the "stay open while typing" contract.
What to add:
it('should not close the dropdown when the trigger is clicked again while already open', async () => {
const { user } = renderComponent()
await user.click(screen.getByRole('combobox', { name: 'Category' }))
await user.click(screen.getByRole('combobox', { name: 'Category' }))
expect(screen.getByRole('checkbox', { name: 'Report A' })).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: 'Category' })).toHaveAttribute('aria-expanded', 'true')
})
5. searchQuery is reset when dropdown is closed — Criticality: 6/10
The close() callback calls both setIsOpen(false) and setSearchQuery(''). The component tests for closing (Escape, click-outside, clear button) never verify that reopening the dropdown shows all options again rather than a stale filtered list.
What to add:
it('should show all options when reopened after being closed with an active search', async () => {
const { user } = renderComponent()
await user.click(screen.getByRole('combobox', { name: 'Category' }))
await user.type(screen.getByRole('combobox', { name: 'Category' }), 'Report A')
expect(screen.queryByRole('checkbox', { name: 'Report B' })).not.toBeInTheDocument()
await user.keyboard('{Escape}')
await user.click(screen.getByRole('combobox', { name: 'Category' }))
expect(screen.getByRole('checkbox', { name: 'Report B' })).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Report C' })).toBeInTheDocument()
})
Regression thi …(truncated, 5110 more chars)