bench

tests · Frontend

Issue board filters

Can it extend an existing app in its own conventions?

Post on X
v1authored Sep 6, 202670 turns · $12 budget · 35m timebox
gateshidden testsrubric

Prompt

# Add filters to the issue board

You are working inside an existing Next.js app that renders a list of issues on the home
page (`/`), reading from static JSON data. There is already one working filter, for issue
status. Read `README.md` in this fixture before making changes; it documents the existing
`components/` and `lib/` structure and the `data-testid` convention used by this app's
tests.

## Requirements

Add the following to the existing issue board, matching the way the status filter is
already built (same folder, same state and query-param approach, same component style):

- A label filter that lets the user select multiple labels at once. An issue matches when it
  has at least one of the selected labels.
- An assignee filter that lets the user pick a single assignee (or "unassigned").
- A free-text search box that filters issues by matching the search text against the issue
  title, case-insensitively.
- All filter state (status, labels, assignees, search text) must be reflected in the URL
  query string, so that reloading the page or sharing the URL preserves the current filters.
- When the combination of active filters matches zero issues, show a clear empty state
  instead of an empty list.
- Every new interactive control must be usable with the keyboard alone (tab to reach it,
  and the expected key operates it), and must have an accessible name.
- Use the existing `data-testid` convention documented in the README for every new
  interactive element, so they are consistent with the existing status filter's test ids.

## Constraints

- Follow the existing patterns in `components/` and `lib/` rather than introducing a new
  state-management approach, a new folder layout, or a new UI library.
- Do not change the on-disk shape of the issue data or remove the existing status filter.
- Keep `bun run build`, `bun run start`, and `bun run typecheck` working exactly as
  documented in the fixture's README.

Work only within this fixture directory.

Fixture

17 paths under fixture/, copied into a fresh run dir for every attempt.

.gitignore
app/
app/globals.css
app/layout.tsx
app/page.tsx
components/
components/IssueList.tsx
components/StatusFilter.tsx
data/
data/issues.json
lib/
lib/issues.ts
next.config.ts
package.json
postcss.config.mjs
README.md
tsconfig.json

README.md

# Issue board

A Next.js App Router app that lists software issues from static JSON data, with filters
synced to the URL query string. One filter (status) is already implemented; this README
documents its pattern precisely enough to extend it with more filters.

## Layout

- `data/issues.json` — the on-disk issue data. Do not change its shape.
- `lib/issues.ts` — the `Issue` / `IssueStatus` types, `getIssues()`, small derived-data
  helpers (`getAllLabels()`, `getAllAssignees()`), and pure filter functions
  (`filterByStatus`, and comments describing the filter functions still to add). A filter
  function always has the shape `(issues: Issue[], criterion) => Issue[]` and is pure: no
  React, no URL access, no side effects.
- `components/` — one client component per filter, plus `IssueList`. Each filter component
  is `'use client'`, reads its own value from `useSearchParams()`, and writes it back with
  `useRouter().replace(...)` built from `usePathname()` and a copy of the current
  `URLSearchParams`. A filter component never filters the issue list itself; it only reads
  and writes its slice of the URL.
- `app/page.tsx` — a server component. It awaits the `searchParams` promise, parses each
  known query key into a typed value, runs the issue list through every filter function
  from `lib/issues.ts` in sequence, and renders the filter components plus `IssueList` with
  the final filtered array.

Adding a new filter means: add its query-param parsing to `app/page.tsx`, add its pure
filter function to `lib/issues.ts`, add its filter component to `components/`, and render
that component in `app/page.tsx`. Nothing else changes.

## URL query-param convention

Every filter's state lives in the URL so a reload or a shared link preserves it.

| Filter | Query key | Value format | Status |
|---|---|---|---|
| Status | `status` | one of `open`, `in-progress`, `done`; key absent = all statuses | implemented |
| Labels | `labels` | comma-separated label names, e.g. `labels=bug,docs`; key absent = no label filter | not yet implemented |
| Assignee | `assignee` | an exact assignee name, or the literal string `unassigned`; key absent = no assignee filter | not yet implemented |
| Search | `q` | free text, matched case-insensitively as a substring of the issue title; key absent or empty = no search filter | not yet implemented |

The existing `StatusFilter` component (`components/StatusFilter.tsx`) shows the pattern:
read the current value with `searchParams.get("status")`, and on change, clone the current
params with `new URLSearchParams(searchParams.toString())`, `set()` or `delete()` only its
own key, then `router.replace(`${pathname}?${params.toString()}`, { scroll: false })`. A new
filter component follows the same shape for its own key, leaving every other key untouched.

## `data-testid` convention

| Element | `data-testid` | Notes | Status |
|---|---|---|---|
| Issue list container | `issue-list` | A `<ul>` (or similar) wrapping all visible issue rows. Rendered only when at least one issue matches; absent when the result is empty. | implemented |
| Each issue row | `` issue-item-<id> `` | `<id>` is the issue's `id` field verbatim, e.g. `issue-item-ISSUE-101`. | implemented |
| Status filter control | `filter-status` | The `<select>` for status. | implemented |
| Labels filter container | `filter-labels` | The element wrapping all label checkboxes (e.g. a `<fieldset>`). | not yet implemented |
| Each label checkbox | `` filter-label-option-<label> `` | `<label>` is the label string verbatim, e.g. `filter-label-option-bug`. One `<input type="checkbox">` per label returned by `getAllLabels()`, each with an accessible name (e.g. wrapped in a `<label>` element). | not yet implemented |
| Assignee filter control | `filter-assignee` | A `<select>` with an `unassigned` option plus one option per name from `getAllAssignees()`. | not yet implemented |
| Search filter control | `filter-search` | A text `<input>` with an accessible name (e.g. `aria-label`), filtering by title substring. | not yet implemented |
| Empty state | `empty-state` | Rendered instead of `issue-list` when the active filters match zero issues. | not yet implemented (the code path already exists in `IssueList`; wiring more filters into `app/page.tsx` is what triggers it) |

`IssueList` (`components/IssueList.tsx`) already renders `empty-state` in place of
`issue-list` whenever it receives zero issues. Composing the new filters in `app/page.tsx`
is enough to make that path reachable; no change to `IssueList` is required.

## Accessibility

Every filter control must be reachable by `Tab` alone and operable with its native key (a
`<select>` with arrow keys, a checkbox with `Space`, a text input by typing), and must have
an accessible name — either a `<label htmlFor>` pairing or an `aria-label`. The existing
`StatusFilter` demonstrates both.

## Scripts

- `bun run dev` — start the dev server.
- `bun run build` — production build.
- `bun run start` — serve the production build on `${PORT:-4174}`.
- `bun run typecheck` — `tsc --noEmit`.

How it is scored

Gates, run in order

  1. installRuns `bun install` and must exit 0.
  2. buildRuns `bun run build` and must exit 0.
  3. typecheckRuns `bun run typecheck` and must exit 0.
  4. axe-homeServes the app and scans / for accessibility violations, failing on serious impact or worse.

Objective layer

9 hidden test cases the agent never saw, run against its own code.

Subjective layer · weights

  • Code quality and pattern conformance (50)
  • UX polish (50)

Rubric: Issue board filters

Score each dimension 0 to 4, based on the diff and the screenshots.

Code quality and pattern conformance (weight 50)

  • 0: New code ignores the existing components/ and lib/ structure, introduces a different state-management approach than the existing status filter, or duplicates logic the fixture already provides.
  • 1: Code runs but the new filters are implemented as one large, unstructured addition rather than following the existing per-filter component shape.
  • 2: New filters mirror the existing status filter's structure and naming, but with noticeable inconsistency (different prop shapes, mixed conventions, or logic that should live in lib/ left inline in a component).
  • 3: New filters closely match the existing status filter's component boundaries, state handling, and URL-sync approach, with clear, single-purpose functions in lib/.
  • 4: Indistinguishable in style and structure from code the fixture's original author would have written: consistent naming, no duplication between the label, assignee, and search filters, and a lib/ layer that composes cleanly with the existing status filter.

UX polish (weight 50)

  • 0: One or more required filters (label, assignee, search) does not work, or filter state is lost on reload despite being expected in the URL.
  • 1: Filters work but combining more than one produces wrong results, or the empty state is missing or confusing.
  • 2: All filters work individually and combine correctly, with a plain empty state and basic keyboard access, but interactions feel rough (no clear indication of active filters, awkward multi-select for labels).
  • 3: Filters combine correctly, the empty state is clear and helpful, active filters are visibly indicated, and every control is comfortably keyboard-operable.
  • 4: The filtering experience feels considered end to end: clearing filters is easy and discoverable, the label multi-select and assignee filter are pleasant to use with a keyboard or a mouse, and the empty state suggests a next action.

Results across releases

v2026.09-smoke · Sep 6, 2026

AgentObjectiveSubjectiveCombinedRun
grok · grok-4.6#1
claude · haiku · low
55.6 (55.655.6, n=1)
62.5 (62.562.5, n=1)
58.3 (58.358.3, n=1)
#1
codex · gpt-5.6-sol · low
55.6 (55.655.6, n=1)
87.5 (87.587.5, n=1)
68.3 (68.368.3, n=1)
#1