Skip to content

Commit ff9b4a9

Browse files
committed
feat: initial corpus — React monorepo with 4 cross-package bugs
Tier 3 e2e corpus for QuantCode autonomous agent testing. Bugs: - packages/utils/src/format/date.ts: formatDate produces US-style MM/DD not AU DD/MM - packages/ui/src/components/Button/Button.tsx: icon-only button missing aria-label (WCAG 4.1.2) - packages/ui/src/components/DataTable/DataTable.tsx: stale closure in sort handler - apps/web/src/lib/api.ts: imports useThrottle (renamed to useDebounce)
0 parents  commit ff9b4a9

20 files changed

Lines changed: 464 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# quantcode-e2e-medium
2+
3+
**Tier 3 corpus** for the QuantCode autonomous agent e2e harness.
4+
5+
A medium-sized TypeScript monorepo with a React component library, shared utilities, and a Next.js-style web app. Contains intentional bugs spanning multiple packages.
6+
7+
## Structure
8+
9+
```
10+
packages/
11+
ui/ React component library (Button, DataTable)
12+
utils/ Shared hooks and formatting utilities
13+
apps/
14+
web/ Web application consuming both packages
15+
```
16+
17+
## Running tests
18+
19+
```bash
20+
bun test --recursive
21+
```
22+
23+
## Known issues (intentional — for agent testing)
24+
25+
1. **`packages/utils/src/format/date.ts`** — `formatDate` produces US-style dates instead of AU-style
26+
2. **`packages/ui/src/components/Button/Button.tsx`** — icon-only button missing `aria-label` attribute (WCAG 4.1.2)
27+
3. **`packages/ui/src/components/DataTable/DataTable.tsx`** — stale closure in sort handler
28+
4. **`apps/web/src/lib/api.ts`** — imports `useThrottle` which was renamed to `useDebounce`

‎apps/web/package.json‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@e2e/web",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"scripts": {
6+
"test": "bun test"
7+
},
8+
"dependencies": {
9+
"@e2e/ui": "workspace:*",
10+
"@e2e/utils": "workspace:*",
11+
"react": "^18.3.0"
12+
}
13+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* 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`.
8+
*/
9+
10+
// BUG: useThrottle no longer exists — was renamed to useDebounce
11+
import { useThrottle } from "@e2e/utils"
12+
import { formatDate, formatAUD } from "@e2e/utils"
13+
14+
export const BASE_URL = process.env.API_URL ?? "http://localhost:3000"
15+
16+
export async function fetchUsers() {
17+
const res = await fetch(`${BASE_URL}/users`)
18+
if (!res.ok) throw new Error(`Failed to fetch users: ${res.status}`)
19+
return res.json()
20+
}
21+
22+
export async function fetchPosts() {
23+
const res = await fetch(`${BASE_URL}/posts`)
24+
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
25+
return res.json()
26+
}
27+
28+
// Re-export formatting utilities used throughout the app
29+
export { formatDate, formatAUD }
30+
31+
// Re-export the debounce hook (currently broken import)
32+
export { useThrottle as useSearchDebounce }

‎apps/web/test/api.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, test, expect } from "bun:test"
2+
3+
describe("api module", () => {
4+
test("imports without error", async () => {
5+
// This will fail if the import in api.ts is broken (useThrottle doesn't exist)
6+
const mod = await import("../src/lib/api")
7+
expect(mod.BASE_URL).toBeDefined()
8+
expect(typeof mod.fetchUsers).toBe("function")
9+
})
10+
11+
test("useSearchDebounce is exported", async () => {
12+
const mod = await import("../src/lib/api")
13+
expect(typeof mod.useSearchDebounce).toBe("function")
14+
})
15+
})

‎package.json‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "quantcode-e2e-medium",
3+
"version": "1.0.0",
4+
"description": "E2E test corpus (Tier 3) — medium React monorepo for QuantCode autonomous agent testing",
5+
"private": true,
6+
"workspaces": [
7+
"packages/*",
8+
"apps/*"
9+
],
10+
"scripts": {
11+
"test": "bun test --recursive",
12+
"typecheck": "tsc --noEmit"
13+
},
14+
"devDependencies": {
15+
"typescript": "^5.4.0",
16+
"bun-types": "latest",
17+
"@types/react": "^18.3.0",
18+
"react": "^18.3.0",
19+
"react-dom": "^18.3.0"
20+
}
21+
}

‎packages/ui/package.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@e2e/ui",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"main": "./src/index.ts",
6+
"scripts": {
7+
"test": "bun test"
8+
},
9+
"dependencies": {
10+
"@e2e/utils": "workspace:*"
11+
},
12+
"devDependencies": {
13+
"react": "^18.3.0",
14+
"react-dom": "^18.3.0",
15+
"@testing-library/react": "^16.0.0",
16+
"@testing-library/jest-dom": "^6.4.0",
17+
"happy-dom": "^14.0.0"
18+
}
19+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react"
2+
3+
type Variant = "primary" | "secondary" | "danger"
4+
5+
type Props = {
6+
children?: React.ReactNode
7+
/** Icon-only button — renders without visible text. Requires aria-label for accessibility. */
8+
icon?: React.ReactNode
9+
iconOnly?: boolean
10+
variant?: Variant
11+
disabled?: boolean
12+
onClick?: () => void
13+
/** Accessible label — REQUIRED when iconOnly is true */
14+
"aria-label"?: string
15+
}
16+
17+
/**
18+
* Button component.
19+
*
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.
27+
*/
28+
export function Button({
29+
children,
30+
icon,
31+
iconOnly = false,
32+
variant = "primary",
33+
disabled = false,
34+
onClick,
35+
"aria-label": ariaLabel,
36+
}: Props) {
37+
return (
38+
<button
39+
className={`btn btn-${variant}`}
40+
disabled={disabled}
41+
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
44+
>
45+
{icon && <span className="btn-icon">{icon}</span>}
46+
{!iconOnly && children}
47+
</button>
48+
)
49+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { useState } from "react"
2+
3+
type SortDir = "asc" | "desc"
4+
5+
type Column<T> = {
6+
key: keyof T
7+
label: string
8+
sortable?: boolean
9+
}
10+
11+
type Props<T extends Record<string, unknown>> = {
12+
data: T[]
13+
columns: Column<T>[]
14+
}
15+
16+
/**
17+
* DataTable with client-side sorting.
18+
*
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.
26+
*/
27+
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")
30+
31+
// BUG: stale closure — sortDir is captured at handler creation time
32+
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+
}
39+
}
40+
41+
const sorted = sortKey
42+
? [...data].sort((a, b) => {
43+
const av = a[sortKey]
44+
const bv = b[sortKey]
45+
const cmp = av < bv ? -1 : av > bv ? 1 : 0
46+
return sortDir === "asc" ? cmp : -cmp
47+
})
48+
: data
49+
50+
return (
51+
<table>
52+
<thead>
53+
<tr>
54+
{columns.map((col) => (
55+
<th
56+
key={String(col.key)}
57+
onClick={col.sortable ? () => handleSort(col.key) : undefined}
58+
aria-sort={sortKey === col.key ? (sortDir === "asc" ? "ascending" : "descending") : undefined}
59+
>
60+
{col.label}
61+
</th>
62+
))}
63+
</tr>
64+
</thead>
65+
<tbody>
66+
{sorted.map((row, i) => (
67+
<tr key={i}>
68+
{columns.map((col) => (
69+
<td key={String(col.key)}>{String(row[col.key])}</td>
70+
))}
71+
</tr>
72+
))}
73+
</tbody>
74+
</table>
75+
)
76+
}

‎packages/ui/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Button } from "./components/Button/Button"
2+
export { DataTable } from "./components/DataTable/DataTable"

‎packages/ui/test/Button.test.tsx‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { render } from "@testing-library/react"
3+
import { Button } from "../src/components/Button/Button"
4+
import React from "react"
5+
6+
describe("Button", () => {
7+
test("renders with text", () => {
8+
const { getByText } = render(<Button>Click me</Button>)
9+
expect(getByText("Click me")).toBeDefined()
10+
})
11+
12+
test("icon-only button has an accessible name via aria-label", () => {
13+
const { getByRole } = render(<Button iconOnly icon={<span>★</span>} aria-label="Add to favourites" />)
14+
const btn = getByRole("button")
15+
// The button must have an accessible name — aria-label must be applied to the element
16+
expect(btn.getAttribute("aria-label")).toBe("Add to favourites")
17+
})
18+
19+
test("icon-only button without aria-label still renders (but should warn)", () => {
20+
// This test documents the current broken behaviour — the button renders
21+
// but has no accessible name, which fails WCAG 4.1.2
22+
const { getByRole } = render(<Button iconOnly icon={<span>★</span>} />)
23+
const btn = getByRole("button")
24+
// After the fix, this should pass — the aria-label should be present
25+
// For now this test is asserting the DESIRED state (will fail until fixed)
26+
expect(btn.getAttribute("aria-label")).not.toBeNull()
27+
})
28+
})

0 commit comments

Comments
 (0)