Skip to content

Commit 0443143

Browse files
author
QuantCode Agent
committed
fix: repair DOM test harness, hook rename, date padding, a11y label and sort closure
- bunfig.toml: preload happy-dom setup from repo root (the 'environment' key is not a real Bun option and was silently ignored, so bare 'bun test' had no DOM). Fixes 9 'document is not defined' failures. - apps/web/src/lib/api.ts: hook renamed useThrottle -> useDebounce in @e2e/utils; fix import and useSearchDebounce re-export. - packages/utils/src/format/date.ts: formatDate used en-AU which hard-pads the day (01/03/2024); switch to en-GB numeric day (1/03/2024). - tsconfig.json: add bun-types to compilerOptions.types to resolve 'Cannot find module bun:test'. - Button.tsx: always emit an accessible name via aria-label for icon-only buttons, with a dev warning when none is supplied (WCAG 4.1.2). - DataTable.tsx: unify sortKey/sortDir into one state object updated via a single functional updater, fixing the stale-closure sort toggle. No test files or dependencies changed. bun test: 13/13 pass; tsc --noEmit clean.
1 parent 7e2198e commit 0443143

6 files changed

Lines changed: 32 additions & 45 deletions

File tree

‎apps/web/src/lib/api.ts‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
/**
22
* API client utilities for the web app.
3-
*
4-
* BUG: imports `useThrottle` from @e2e/utils, but that hook was renamed to
5-
* `useDebounce`. This causes a TypeScript error and a runtime crash.
6-
*
7-
* Fix: change the import to `useDebounce`.
83
*/
94

10-
// BUG: useThrottle no longer exists — was renamed to useDebounce
11-
import { useThrottle } from "@e2e/utils"
5+
import { useDebounce } from "@e2e/utils"
126
import { formatDate, formatAUD } from "@e2e/utils"
137

148
export const BASE_URL = process.env.API_URL ?? "http://localhost:3000"
@@ -28,5 +22,5 @@ export async function fetchPosts() {
2822
// Re-export formatting utilities used throughout the app
2923
export { formatDate, formatAUD }
3024

31-
// Re-export the debounce hook (currently broken import)
32-
export { useThrottle as useSearchDebounce }
25+
// Re-export the debounce hook under the app-facing name
26+
export { useDebounce as useSearchDebounce }

‎bunfig.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[test]
2-
environment = "happy-dom"
2+
preload = ["./packages/ui/test/setup.ts"]

‎packages/ui/src/components/Button/Button.tsx‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@ type Props = {
1717
/**
1818
* Button component.
1919
*
20-
* BUG: When `iconOnly` is true, the button renders without visible text.
21-
* An `aria-label` is required for screen reader accessibility (WCAG 2.1 SC 4.1.2),
22-
* but the component does not enforce or warn about its absence.
23-
*
24-
* The test in Button.test.tsx checks that an icon-only button has an accessible name.
25-
* Fix: throw/warn in development when `iconOnly && !aria-label`, or always render
26-
* the aria-label attribute when iconOnly is true.
20+
* Icon-only buttons render no visible text, so they always emit an `aria-label`
21+
* to guarantee an accessible name (WCAG 2.2 SC 4.1.2 Name, Role, Value).
22+
* A dev-only warning is logged when an icon-only button omits `aria-label`,
23+
* since the fallback ("Button") is not a meaningful description.
2724
*/
2825
export function Button({
2926
children,
@@ -34,13 +31,21 @@ export function Button({
3431
onClick,
3532
"aria-label": ariaLabel,
3633
}: Props) {
34+
const resolvedLabel =
35+
ariaLabel ?? (iconOnly ? (typeof children === "string" ? children : "Button") : undefined)
36+
37+
if (process.env.NODE_ENV !== "production" && iconOnly && !ariaLabel) {
38+
console.warn(
39+
"[Button] `iconOnly` buttons require an explicit `aria-label` to provide an accessible name (WCAG 2.2 SC 4.1.2). Falling back to a generic label.",
40+
)
41+
}
42+
3743
return (
3844
<button
3945
className={`btn btn-${variant}`}
4046
disabled={disabled}
4147
onClick={onClick}
42-
// BUG: aria-label is not applied when iconOnly is true and no ariaLabel is passed
43-
// The component should enforce aria-label for icon-only buttons
48+
aria-label={resolvedLabel}
4449
>
4550
{icon && <span className="btn-icon">{icon}</span>}
4651
{!iconOnly && children}

‎packages/ui/src/components/DataTable/DataTable.tsx‎

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,18 @@ type Props<T extends Record<string, unknown>> = {
1616
/**
1717
* DataTable with client-side sorting.
1818
*
19-
* BUG: The sort handler has a stale closure — it captures `sortDir` at the
20-
* time the handler is created, so toggling sort direction does not work
21-
* correctly after the first click. The second click always sorts in the same
22-
* direction as the first.
23-
*
24-
* Fix: use the functional form of setState — `setSortDir(prev => ...)` —
25-
* so the toggle always reads the current value.
19+
* Sort key and direction are held in a single state object and updated through
20+
* one functional updater, so the toggle decision always reads the current
21+
* (non-stale) state rather than the value captured when the handler was created.
2622
*/
2723
export function DataTable<T extends Record<string, unknown>>({ data, columns }: Props<T>) {
28-
const [sortKey, setSortKey] = useState<keyof T | null>(null)
29-
const [sortDir, setSortDir] = useState<SortDir>("asc")
24+
const [sort, setSort] = useState<{ key: keyof T | null; dir: SortDir }>({ key: null, dir: "asc" })
25+
const { key: sortKey, dir: sortDir } = sort
3026

31-
// BUG: stale closure — sortDir is captured at handler creation time
3227
const handleSort = (key: keyof T) => {
33-
if (sortKey === key) {
34-
setSortDir(sortDir === "asc" ? "desc" : "asc") // BUG: reads stale sortDir
35-
} else {
36-
setSortKey(key)
37-
setSortDir("asc")
38-
}
28+
setSort((prev) =>
29+
prev.key === key ? { key, dir: prev.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" },
30+
)
3931
}
4032

4133
const sorted = sortKey

‎packages/utils/src/format/date.ts‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
/**
22
* Date formatting utilities.
33
*
4-
* BUG: formatDate passes `'en-AU'` as the locale but then uses a US-style
5-
* format string option (`month: 'numeric'` before `day: 'numeric'`), which
6-
* produces MM/DD/YYYY output instead of DD/MM/YYYY for Australian dates.
7-
*
8-
* Fix: use `dateStyle: 'short'` with `'en-AU'` locale, which correctly
9-
* produces DD/MM/YYYY, or explicitly set `day: 'numeric', month: 'numeric', year: 'numeric'`
10-
* and rely on the locale to order them correctly.
4+
* Produces day-first (D/MM/YYYY) output. Note that `en-AU` zero-pads the day
5+
* regardless of `day: "numeric"`, yielding "01/03/2024"; `en-GB` honours the
6+
* numeric day request and yields "1/03/2024" while keeping day-first ordering.
117
*/
128
export function formatDate(date: Date): string {
13-
// BUG: explicit field order overrides locale ordering — produces M/D/YYYY not D/M/YYYY
14-
return new Intl.DateTimeFormat("en-AU", {
15-
month: "numeric",
9+
return new Intl.DateTimeFormat("en-GB", {
1610
day: "numeric",
11+
month: "2-digit",
1712
year: "numeric",
1813
}).format(date)
1914
}

‎tsconfig.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"jsx": "react-jsx",
77
"strict": true,
88
"skipLibCheck": true,
9+
"types": ["bun-types"],
910
"paths": {
1011
"@e2e/ui": ["./packages/ui/src/index.ts"],
1112
"@e2e/utils": ["./packages/utils/src/index.ts"]

0 commit comments

Comments
 (0)