Skip to content

Stability audit: F0Select #3874

@eliseo-juan

Description

@eliseo-juan

Stability Audit — F0Select

Auto-generated by the stability-audit agent. Four reviewers (Code Review, A11y, Storybook, Test Coverage) analyzed packages/react/src/components/F0Select/.


Findings

BLOCKING

Severity Area File:line Description Suggested fix
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:1 screen imported from @testing-library/react instead of @/testing/test-utils Change to import { screen } from "@/testing/test-utils"
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:3 import "@testing-library/jest-dom/vitest" is forbidden per F0 convention Remove the import entirely
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:62-64 console.log(value) in defaultSelectProps.onChange test helper Replace with vi.fn()
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:97 user.click(...) in openSelect helper is not awaited Add await before user.click(...)
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:102 fireEvent.animationStart inside shared openSelect helper — affects every test Replace with equivalent userEvent call or document why fireEvent is required
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:410 container.querySelector("button[data-testid='clear-button']") — non-accessible DOM query Use screen.getByRole("button", { name: /clear/i })
BLOCKING Code F0Select/__tests__/F0Select.test.tsx:416 await fireEvent.click(clearButton)fireEvent is synchronous, await is a no-op; prefer userEvent Replace with await user.click(clearButton)
BLOCKING Code F0Select/__stories__/F0Select.stories.tsx:316 console.log(...) in the story decorator — runs on every story render Remove the console.log call
BLOCKING Code F0Select/__stories__/F0Select.stories.tsx:544 console.log("searchFn", ...) inside WithSearchBox render function Remove
BLOCKING Code F0Select/__stories__/F0Select.stories.tsx:773,814,858 console.log("selectionStatus", ...) in three onSelectItems callbacks Remove; the decorator already renders selectionStatus on screen
BLOCKING Code F0Select/__stories__/F0Select.stories.tsx:865,880 <F0Select {...(args as any)} />any cast in MultiplePaginatedAsList and AsList render functions Type args correctly; use the story's inferred Args type
BLOCKING Code F0Select/F0Select.tsx:581-583 handleApply declared with useCallback(fn, []) (empty deps) but closes over handleChangeOpenLocal — stale closure Add handleChangeOpenLocal to deps array
BLOCKING A11y F0Select/F0Select.tsx:820-825 Custom trigger path: <SelectTrigger asChild> merges Radix combobox role onto a <div>, which is non-interactive; aria-label on a <div> is ignored by AT Wrap children in a <button> so Radix merges onto a real interactive host
BLOCKING A11y F0Select/F0Select.tsx:879-884 Inner <button> has aria-label={label || placeholder} while InputField already injects the same aria-label onto its child — produces a conflicting label Remove explicit aria-label from the inner <button>; rely on InputField's htmlFor/id linkage
BLOCKING A11y F0Select/components/SelectAll.tsx:71 id="select-all" hardcoded — multiple F0Select instances on the same page produce duplicate id attributes, breaking ARIA associations Use useId() inside SelectAll or accept a scoped id from the parent
BLOCKING A11y F0Select/components/SelectionPreview.tsx:51 aria-label={`Remove ${item.label}`} is hardcoded English — untranslatable Use useI18n() with an interpolated translation key
BLOCKING A11y F0Select/components/SelectItem.tsx:27-29 Decorative icon wrapper <div> lacks aria-hidden="true" — SVG content may corrupt the Radix option accessible name Add aria-hidden="true" to the icon wrapper <div>

SUGGESTION

Severity Area File:line Description Suggested fix
SUGGESTION Code F0Select/F0Select.tsx:179-187 useEffect syncing valuelocalValue suppresses react-hooks/exhaustive-deps Audit intentionally omitted deps and document the invariant, or refactor to derived state
SUGGESTION Code F0Select/F0Select.tsx:228-229 useMemo deps array contains "searchFn" in props && props.searchFn — string literal in deps array is a code smell; React hooks lint tooling may not track it reliably Move the conditional into the memo body; put only reactive values in deps
SUGGESTION Code F0Select/F0Select.tsx:376-410 getDisplayItemsForSelection name implies a factory function, but it holds the computed array Rename to displayItemsForSelection
SUGGESTION Code F0Select/F0Select.tsx:916-923 displayName never set on F0SelectComponent after the generic cast Add F0SelectComponent.displayName = "F0Select" before export
SUGGESTION Code F0Select/components/SelectAll.tsx:71 id="select-all" hardcoded — duplicate IDs on multi-instance pages Accept a scoped id prop or call useId()
SUGGESTION Code F0Select/components/SelectionPreview.tsx:51 aria-label hardcoded English (also a BLOCKING a11y item) Use useI18n()
SUGGESTION Code F0Select/types.ts inputFieldStatus const array is referenced in stories but not re-exported from the component's public surface Re-export from types.ts or the barrel index.tsx
SUGGESTION Code F0Select/__tests__/F0Select.test.tsx:50 defaultSelectProps sets size: "md" but component default is "sm" Change to "sm" or remove the prop
SUGGESTION Code F0Select/__tests__/F0Select.test.tsx:228 it.skip "maintains focus on search input during data loading" has no linked issue Fix and unskip, or add a tracking issue reference
SUGGESTION A11y F0Select/F0Select.tsx:773-812 (asList path) <Label htmlFor={id}> targets useId() id but selectPrimitiveProps does not forward that id to the trigger — label→control link is broken Pass id={id} into selectPrimitiveProps or use aria-labelledby
SUGGESTION A11y F0Select/components/Arrow.tsx transition-transform duration-200 animation has no useReducedMotion() guard Apply duration-0 when useReducedMotion() returns true
SUGGESTION A11y F0Select/components/SelectTopActions.tsx <motion.div> for active-filter chips has no useReducedMotion() guard Set duration: 0, bounce: 0 when useReducedMotion() is true
SUGGESTION A11y F0Select/__tests__/F0Select.test.tsx No axe / accessibility assertions in test suite Add vitest-axe checks or axe-playwright story tests for default, multiple, asList, and custom-trigger paths
SUGGESTION Storybook F0Select/__stories__/F0Select.stories.tsx:224-226 Decorator uses SelectedItemsDetailedStatus<any, any> with eslint-disable Use SelectedItemsDetailedStatus<RecordType, FiltersDefinition> (already imported)
SUGGESTION Storybook F0Select/__stories__/F0Select.stories.tsx No dedicated WithDisabled, WithLoading, or WithReadonly interactive stories (they appear only inside Snapshot) Add standalone stories for each
SUGGESTION Storybook F0Select/__stories__/F0Select.stories.tsx:583-588 LargeList spreads WithSearchBox.args but WithSearchBox sets showSearchBox in its render fn, not args — LargeList does not actually show the search box Add showSearchBox: true to WithSearchBox.args or set it explicitly in LargeList.args
SUGGESTION Storybook F0Select/__stories__/F0Select.stories.tsx:337-348 Only WithDataTestId has a play function, limited to DOM presence assertion; no interaction or keyboard-nav play tests Add play functions for open/select/close, keyboard navigation, and onChange assertion
SUGGESTION Tests Missing: keyboard navigation tests (Space/Enter to open, ArrowUp/Down, Escape) Add await user.keyboard(...) tests
SUGGESTION Tests Missing: multiple={true} selection tests (select, deselect, displayed value) Add a describe block for multiple mode
SUGGESTION Tests Missing: SelectAll checkbox behavior tests Add tests for check-all, uncheck-all, indeterminate state
SUGGESTION Tests Missing: loading={true} state test Assert loading indicator is present
SUGGESTION Tests Missing: asList={true} render mode tests Verify options visible without trigger click
SUGGESTION Tests Missing: source (async/paginated) data loading tests Add tests for initial load, pagination trigger
SUGGESTION Tests Missing: error / status / hint display tests Assert status message and hint text appear
SUGGESTION Tests Missing: actions prop test Pass actions and assert rendered action element
SUGGESTION Tests Missing: onOpenChange callback test Assert called with true on open, false on close
SUGGESTION Tests Missing: readonly prop test Assert trigger is non-interactive when readonly

Ready-to-run agent fix prompt

Fix all BLOCKING stability issues in packages/react/src/components/F0Select/ as identified in the stability audit:

**Tests (`__tests__/F0Select.test.tsx`):**
1. Remove `import "@testing-library/jest-dom/vitest"` (line 3)
2. Move `screen` import to come from `@/testing/test-utils` (line 1)
3. Replace `console.log(value)` in `defaultSelectProps.onChange` with `vi.fn()` (line 62-64)
4. Add `await` before `user.click(...)` in the `openSelect` helper (line 97)
5. Replace `fireEvent.animationStart` with the appropriate userEvent call in `openSelect` (line 102)
6. Replace `container.querySelector("button[data-testid='clear-button']")` with `screen.getByRole("button", { name: /clear/i })` (line 410)
7. Replace `await fireEvent.click(clearButton)` with `await user.click(clearButton)` (line 416)

**Stories (`__stories__/F0Select.stories.tsx`):**
8. Remove `console.log(...)` from the meta decorator (line 316)
9. Remove `console.log("searchFn", ...)` from `WithSearchBox` render fn (line 544)
10. Remove the three `console.log("selectionStatus", ...)` calls in `MultiplePaginated`, `MultiplePaginatedWithPreview`, `MultiplePaginatedAsList` (lines 773, 814, 858)
11. Replace `args as any` in `MultiplePaginatedAsList` and `AsList` render functions (lines 865, 880) with properly typed spreads

**Implementation (`F0Select.tsx`):**
12. Fix the `handleApply` stale closure: add `handleChangeOpenLocal` to the `useCallback` deps array (line 582)

**A11y — Custom trigger (`F0Select.tsx:820-825`):**
13. Wrap `children` in a `<button>` element in the custom trigger path so Radix merges the combobox role onto an interactive host

**A11y — Inner button (`F0Select.tsx:879-884`):**
14. Remove explicit `aria-label` from the inner `<button>` inside `InputField`; rely on InputField's htmlFor/id label association

**A11y — `components/SelectAll.tsx:71`:**
15. Replace `id="select-all"` with `useId()`-generated id to prevent duplicate IDs on multi-instance pages

**A11y — `components/SelectionPreview.tsx:51`:**
16. Replace hardcoded `` `Remove ${item.label}` `` with an `useI18n()` translation key with `{{label}}` interpolation

**A11y — `components/SelectItem.tsx:27-29`:**
17. Add `aria-hidden="true"` to the icon wrapper `<div>` to prevent decorative SVG content from corrupting the Radix option accessible name

After each change, run:
- `pnpm tsc` (type-check)
- `pnpm vitest:ci` (unit tests)
- `pnpm lint` (lint)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions