Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/accessibility/bf-036-bf-038-keyboard-visual-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Scope: representative navigation, discovery, tool action, upload, and output sur
## Automated Coverage

- `tests/guards/focus-visible-guard.test.ts` prevents removing visible focus rings from app/component surfaces.
- `tests/guards/shared-a11y-surfaces.test.ts` guards All Tools dialog focus trap/restore behavior, shared action toolbar names, and Pipeline Builder run status.
- `tests/guards/shared-a11y-surfaces.test.ts` guards All Tools dialog focus trap/restore behavior, shared action-group names, and Pipeline Builder run status.
- `tests/guards/a11y-mobile-baseline.test.ts` guards skip-link behavior, live toast feedback, mobile touch target baseline, and core mobile routes.
- `tests/guards/all-tools-performance-a11y-guard.test.ts` guards disabled explanations plus long-text/mobile containment on All Tools.
- `tests/guards/theme-contrast-token-guard.test.ts` checks light/dark foreground and muted text contrast tokens against WCAG AA.
Expand Down
2 changes: 1 addition & 1 deletion docs/accessibility/bf-037-screen-reader-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Scope: representative dynamic tool interfaces for BF-037.
- All Tools: search exposes the result-count status as its accessible description; filtered result counts announce through a polite live region; the mobile filter drawer has a dialog name, description, trapped focus, Escape close, and focus restoration.
- Pipeline Builder: final output is label-associated; run-log status announces through a polite live region; the run-log table has an accessible name; run failures expose an alert.
- External-request media flow: confirmation panel identifies the destination/purpose/data sent; preview/download actions remain disabled with accessible descriptions until explicit confirmation.
- Shared tool actions: action groups expose a toolbar label; disabled actions use visible button names plus programmatic descriptions, without exposing tool payloads.
- Shared tool actions: action groups expose a named group container; disabled actions use visible button names plus programmatic descriptions, without exposing tool payloads.
- Copy, download, export, save, and share feedback: toast title/description text is mirrored into the shared polite status region so success and failure states are announced consistently.

## Automated Coverage
Expand Down
1 change: 1 addition & 0 deletions src/app/[lang]/all-tools/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang:
popularTags: requireTranslationValue(t.common.popular_tags, "common.popular_tags"),
removeFilter: requireTranslationValue(t.common.remove_filter, "common.remove_filter"),
showFilters: requireTranslationValue(t.common.show_filters, "common.show_filters"),
showFiltersWithCount: requireTranslationValue(t.common.show_filters_with_count, "common.show_filters_with_count"),
showFewerTools: requireTranslationValue(t.common.show_fewer_tools, "common.show_fewer_tools"),
showMoreTools: requireTranslationValue(t.common.show_more_tools, "common.show_more_tools"),
closeFilters: requireTranslationValue(t.common.close_filters, "common.close_filters"),
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/Domain",
"remove_filter": "Filter entfernen",
"show_filters": "Filter anzeigen",
"show_filters_with_count": "Filter anzeigen. Aktive Filter: {count}",
"show_fewer_tools": "Weniger anzeigen",
"show_more_tools": "Mehr anzeigen",
"use_case_encode": "Kodieren/dekodieren",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/domain",
"remove_filter": "Remove filter",
"show_filters": "Show filters",
"show_filters_with_count": "Show filters. Active filters: {count}",
"show_fewer_tools": "Show fewer",
"show_more_tools": "Show more",
"use_case_encode": "Encode/decode",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/domaine",
"remove_filter": "Retirer le filtre",
"show_filters": "Afficher les filtres",
"show_filters_with_count": "Afficher les filtres. Filtres actifs : {count}",
"show_fewer_tools": "Afficher moins",
"show_more_tools": "Afficher plus",
"use_case_encode": "Encoder/décoder",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/ドメイン",
"remove_filter": "フィルターを削除",
"show_filters": "フィルターを表示",
"show_filters_with_count": "フィルターを表示(適用中: {count} 件)",
"show_fewer_tools": "少なく表示",
"show_more_tools": "もっと表示",
"use_case_encode": "エンコード/デコード",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/도메인",
"remove_filter": "필터 제거",
"show_filters": "필터 표시",
"show_filters_with_count": "필터 표시, 적용됨 {count}개",
"show_fewer_tools": "줄여 보기",
"show_more_tools": "더 보기",
"use_case_encode": "인코드/디코드",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/域名",
"remove_filter": "移除筛选",
"show_filters": "显示筛选",
"show_filters_with_count": "显示筛选,已选 {count} 项",
"show_fewer_tools": "收起",
"show_more_tools": "显示更多",
"use_case_encode": "编码/解码",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
"input_url_domain": "URL/網域",
"remove_filter": "移除篩選",
"show_filters": "顯示篩選",
"show_filters_with_count": "顯示篩選,已選 {count} 項",
"show_fewer_tools": "收合",
"show_more_tools": "顯示更多",
"use_case_encode": "編碼/解碼",
Expand Down
2 changes: 2 additions & 0 deletions src/features/tool-discovery/all-tools-discovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type AllToolsDiscoveryLabels = {
removeFilter: string
closeFilters: string
showFilters: string
showFiltersWithCount: string
showFewerTools: string
showMoreTools: string
clearRecentTools: string
Expand Down Expand Up @@ -925,6 +926,7 @@ export function AllToolsDiscovery({
type="button"
className="mt-4 flex min-h-11 w-full items-center justify-between gap-3 rounded-lg border border-border/70 bg-background/45 px-3 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 lg:hidden"
onClick={() => setShowMobileFilters(true)}
aria-label={labels.showFiltersWithCount.replace("{count}", String(activeFilters.length))}
aria-expanded={showMobileFilters}
aria-controls="all-tools-filter-drawer"
aria-describedby="all-tools-result-status"
Expand Down
14 changes: 10 additions & 4 deletions src/features/tool-shell/external-request-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,17 @@ export function ExternalRequestStatus({
</div>
<span
className="sr-only"
role={role}
aria-live={role === "alert" ? "assertive" : "polite"}
role="status"
aria-live="polite"
aria-atomic="true"
data-external-request-announcement
>{announcement}</span>
data-external-request-announcement={role === "status" ? true : undefined}
>{role === "status" ? announcement : ""}</span>
<span
className="sr-only"
role="alert"
aria-atomic="true"
data-external-request-announcement={role === "alert" ? true : undefined}
>{role === "alert" ? announcement : ""}</span>
</section>
)
}
2 changes: 1 addition & 1 deletion src/features/tool-shell/tool-action-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export function ToolActionBar({
return (
<div
className={joinClasses("flex w-full flex-wrap items-center gap-2 sm:w-auto", className)}
role="toolbar"
role="group"
aria-label={t.common.tool_actions}
>
{primaryActions.map((action) => {
Expand Down
14 changes: 7 additions & 7 deletions src/lib/sitemap-lastmod.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,13 @@
},
"static": {
"all-tools": {
"en": "2026-07-16T00:00:00.000Z",
"zh-CN": "2026-07-16T00:00:00.000Z",
"zh-TW": "2026-07-16T00:00:00.000Z",
"ja": "2026-07-16T00:00:00.000Z",
"ko": "2026-07-16T00:00:00.000Z",
"de": "2026-07-16T00:00:00.000Z",
"fr": "2026-07-16T00:00:00.000Z"
"en": "2026-07-17T00:00:00.000Z",
"zh-CN": "2026-07-17T00:00:00.000Z",
"zh-TW": "2026-07-17T00:00:00.000Z",
"ja": "2026-07-17T00:00:00.000Z",
"ko": "2026-07-17T00:00:00.000Z",
"de": "2026-07-17T00:00:00.000Z",
"fr": "2026-07-17T00:00:00.000Z"
},
"about": {
"en": "2026-07-16T00:00:00.000Z",
Expand Down
4 changes: 4 additions & 0 deletions tests/component/all-tools-discovery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const labels = {
popularTags: "Popular tags",
removeFilter: "Remove filter",
showFilters: "Show filters",
showFiltersWithCount: "Show filters. Active filters: {count}",
showFewerTools: "Show fewer",
showMoreTools: "Show more",
closeFilters: "Done",
Expand Down Expand Up @@ -387,6 +388,7 @@ describe("AllToolsDiscovery", () => {
renderDiscovery()

const trigger = screen.getByRole("button", { name: /Show filters/ })
expect(trigger).toHaveAccessibleName("Show filters. Active filters: 0")
trigger.focus()
fireEvent.click(trigger)

Expand All @@ -396,9 +398,11 @@ describe("AllToolsDiscovery", () => {
expect(within(drawer).getByText(/10 tools/)).toBeInTheDocument()
fireEvent.click(within(drawer).getAllByRole("button", { name: "File" })[0])
expect(within(drawer).getByText(/1 tools/)).toBeInTheDocument()
expect(trigger).toHaveAccessibleName("Show filters. Active filters: 1")

fireEvent.click(within(drawer).getByRole("button", { name: "Clear filters" }))
expect(within(drawer).getByText(/10 tools/)).toBeInTheDocument()
expect(trigger).toHaveAccessibleName("Show filters. Active filters: 0")

const doneButtons = within(drawer).getAllByRole("button", { name: "Done" })
fireEvent.click(doneButtons[doneButtons.length - 1])
Expand Down
2 changes: 1 addition & 1 deletion tests/component/http-request-builder-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe("HttpRequestBuilderPage", () => {

expect(screen.getByRole("group", { name: "Add to favorites / Remove from favorites" })).toHaveAttribute("data-tool-global-actions")
expect(screen.getByRole("button", { name: "Add to favorites" })).toBeInTheDocument()
expect(screen.getByRole("toolbar", { name: "Tool actions" })).toBeInTheDocument()
expect(screen.getByRole("group", { name: "Tool actions" })).toBeInTheDocument()

fireEvent.click(screen.getByRole("button", { name: "Add Header" }))
fireEvent.change(screen.getAllByRole("textbox", { name: "Header name" })[1], {
Expand Down
18 changes: 9 additions & 9 deletions tests/component/phase3-pipeline-builder-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("phase 3 pipeline builder page", () => {

expect(screen.getByRole("heading", { name: "Pipeline Builder" })).toBeInTheDocument()
expect(screen.getByRole("heading", { name: "Build a recipe in three moves" })).toBeInTheDocument()
expect(screen.getByRole("toolbar", { name: "Tool actions" })).toBeInTheDocument()
expect(screen.getByRole("group", { name: "Tool actions" })).toBeInTheDocument()
expect(screen.getByLabelText("Initial input")).toBeInTheDocument()
expect(screen.getByLabelText("Final output")).toBeInTheDocument()
expect(screen.getAllByText("Steps").length).toBeGreaterThan(0)
Expand Down Expand Up @@ -310,13 +310,13 @@ describe("phase 3 pipeline builder page", () => {
expect(screen.getByRole("button", { name: "Export structure only" })).toBeInTheDocument()
}, 10_000)

it("restores privacy dialog focus for toolbar and inspector actions", async () => {
it("restores privacy dialog focus for action-bar and inspector actions", async () => {
renderWithEnglish(<PipelineBuilderPage />)

const toolbar = screen.getByRole("toolbar", { name: "Tool actions" })
const toolbarExport = within(toolbar).getByRole("button", { name: "Export JSON" })
toolbarExport.focus()
fireEvent.click(toolbarExport)
const actionGroup = screen.getByRole("group", { name: "Tool actions" })
const actionGroupExport = within(actionGroup).getByRole("button", { name: "Export JSON" })
actionGroupExport.focus()
fireEvent.click(actionGroupExport)

const firstDialog = await screen.findByRole("dialog", { name: "Privacy preview" })
expect(firstDialog).toHaveAccessibleDescription("Export a structure-only JSON recipe file.")
Expand All @@ -339,14 +339,14 @@ describe("phase 3 pipeline builder page", () => {
fireEvent.keyDown(document, { key: "Escape" })

await waitFor(() => expect(screen.queryByRole("dialog", { name: "Privacy preview" })).not.toBeInTheDocument())
await waitFor(() => expect(toolbarExport).toHaveFocus())
await waitFor(() => expect(actionGroupExport).toHaveFocus())

fireEvent.click(toolbarExport)
fireEvent.click(actionGroupExport)
const cancelDialog = await screen.findByRole("dialog", { name: "Privacy preview" })
fireEvent.click(within(cancelDialog).getByRole("button", { name: "Cancel" }))

await waitFor(() => expect(screen.queryByRole("dialog", { name: "Privacy preview" })).not.toBeInTheDocument())
await waitFor(() => expect(toolbarExport).toHaveFocus())
await waitFor(() => expect(actionGroupExport).toHaveFocus())

const inspector = screen.getByRole("complementary", { name: "Recipe inspector" })
const inspectorExport = within(inspector).getByRole("button", { name: "Export JSON" })
Expand Down
1 change: 1 addition & 0 deletions tests/component/representative-a11y-axe.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const discoveryLabels = {
popularTags: "Popular tags",
removeFilter: "Remove filter",
showFilters: "Show filters",
showFiltersWithCount: "Show filters. Active filters: {count}",
showFewerTools: "Show fewer",
showMoreTools: "Show more",
closeFilters: "Done",
Expand Down
7 changes: 5 additions & 2 deletions tests/component/tool-action-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ describe("ToolActionBar", () => {

render(<ToolActionBar actions={actions} />)

const toolbar = screen.getByRole("toolbar", { name: "Tool actions" })
const labels = within(toolbar).getAllByRole("button").map((button) => button.textContent)
const actionGroup = screen.getByRole("group", { name: "Tool actions" })
const actionButtons = within(actionGroup).getAllByRole("button")
const labels = actionButtons.map((button) => button.textContent)

expect(screen.queryByRole("toolbar", { name: "Tool actions" })).not.toBeInTheDocument()
expect(actionButtons.filter((button) => !button.hasAttribute("disabled")).every((button) => button.tabIndex === 0)).toBe(true)
expect(labels).toEqual(["Sample", "Clear", "Format", "Copy", "Download"])
expect(screen.getByRole("button", { name: "Download", description: "Nothing to download." })).toHaveAttribute("title", "Download: Nothing to download.")
})
Expand Down
7 changes: 6 additions & 1 deletion tests/component/tool-shell-status-output.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@ describe("shared tool shell status and output components", () => {
)

const permissionStatus = screen.getByRole("status")
const failureAlert = screen.getByRole("alert")
expect(permissionStatus.closest('[data-external-request-status="permission"]')).not.toBeNull()
expect(permissionStatus).toHaveAttribute("aria-live", "polite")
expect(permissionStatus.childNodes).toHaveLength(1)
expect(permissionStatus.firstChild?.nodeType).toBe(Node.TEXT_NODE)
expect(failureAlert).toBeEmptyDOMElement()

rerender(
<LangProvider lang="en" translations={getTranslation("en")}>
Expand All @@ -184,7 +186,9 @@ describe("shared tool shell status and output components", () => {
)

expect(screen.getByRole("alert").closest('[data-external-request-status="offline"]')).not.toBeNull()
expect(screen.getByRole("alert")).toHaveAttribute("aria-live", "assertive")
expect(screen.getByRole("alert")).not.toHaveAttribute("aria-live")
expect(screen.getByRole("alert")).toHaveAttribute("data-external-request-announcement")
expect(permissionStatus).toBeEmptyDOMElement()
})

it("exposes external request status, next step, hosts, and alert semantics for failures", () => {
Expand All @@ -201,5 +205,6 @@ describe("shared tool shell status and output components", () => {
expect(screen.getByRole("alert")).toHaveTextContent("What to do next: Reconnect and retry.")
expect(screen.getByRole("alert")).toHaveTextContent("i.ytimg.com")
expect(screen.getByRole("alert")).toHaveTextContent("Network access starts only after you choose the external-request action.")
expect(screen.getByRole("status")).toBeEmptyDOMElement()
})
})
9 changes: 6 additions & 3 deletions tests/guards/shared-a11y-surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ describe("shared accessibility surfaces", () => {
it("keeps the shared tool action bar named and described", () => {
const source = readSource("src/features/tool-shell/tool-action-bar.tsx")

expect(source).toContain("role=\"toolbar\"")
expect(source).toContain("role=\"group\"")
expect(source).not.toContain("role=\"toolbar\"")
expect(source).toContain("aria-label={t.common.tool_actions}")
expect(source).toContain("aria-describedby={disabledDescriptionId}")
expect(source).toContain("aria-describedby={handoffDisabledDescriptionId}")
Expand Down Expand Up @@ -66,8 +67,10 @@ describe("shared accessibility surfaces", () => {
for (const source of [youtube, vimeo, instagram]) {
expect(source).toContain("ExternalRequestStatus")
}
expect(externalRequestStatus).toContain('role={role}')
expect(externalRequestStatus).toContain('aria-live={role === "alert" ? "assertive" : "polite"}')
expect(externalRequestStatus).toContain('role="status"')
expect(externalRequestStatus).toContain('aria-live="polite"')
expect(externalRequestStatus).toContain('role="alert"')
expect(externalRequestStatus).toContain('{role === "alert" ? announcement : ""}')
expect(externalRequestStatus).toContain("data-external-request-status")
})
})