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
5 changes: 5 additions & 0 deletions .changeset/fix-model-picker-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Improve screen reader navigation and provider group controls in the model picker.
102 changes: 93 additions & 9 deletions packages/kilo-vscode/tests/model-selector-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ test("model selector exposes combobox relationships and active option movement",

await page.getByRole("button", { name: "Review model: Alpha" }).click()
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
const listbox = page.getByRole("listbox", { name: "Review model" })
const alpha = page.getByRole("option", { name: "Alpha" })
const bravo = page.getByRole("option", { name: "Bravo" })
const tree = page.getByRole("tree", { name: "Review model" })
const alpha = page.getByRole("treeitem", { name: "Alpha" })
const bravo = page.getByRole("treeitem", { name: "Bravo" })

await expect(combobox).toBeFocused()
await expect(combobox).toHaveAttribute("aria-expanded", "true")
await expect(combobox).toHaveAttribute("aria-controls", await listbox.getAttribute("id"))
await expect(combobox).toHaveAttribute("aria-haspopup", "tree")
await expect(combobox).toHaveAttribute("aria-controls", await tree.getAttribute("id"))
await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id"))
await expect(combobox).toHaveAccessibleDescription("Choose the model used for code review tasks.")
await expect(alpha.locator("button")).toHaveCount(0)
Expand All @@ -46,12 +47,81 @@ test("model selector exposes combobox relationships and active option movement",
await expect(preview.getByRole("button", { name: "Add to favorites" })).toBeVisible()
})

test("typing a provider initial moves the active descendant to matching results", async ({ page }) => {
await load(page, "shared--model-selector-accessible")

await page.getByRole("button", { name: "Review model: Alpha" }).click()
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
await combobox.fill("N")

const nova = page.getByRole("treeitem", { name: "Nova" })
await expect(nova).toBeVisible()
await expect(combobox).toHaveAttribute("aria-activedescendant", await nova.getAttribute("id"))
await expect(page.getByRole("treeitem", { name: "NVIDIA" })).toHaveAttribute("aria-expanded", "true")
})

test("provider groups collapse, expand, and skip their model rows", async ({ page }) => {
await load(page, "shared--model-selector-accessible")

await page.getByRole("button", { name: "Review model: Alpha" }).click()
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
const kilo = page.getByRole("treeitem", { name: "Kilo" })
const nvidia = page.getByRole("treeitem", { name: "NVIDIA" })

await combobox.press("ArrowDown")
await combobox.press("ArrowLeft")
await expect(combobox).toHaveAttribute("aria-activedescendant", await kilo.getAttribute("id"))
await combobox.press("ArrowLeft")
await expect(kilo).toHaveAttribute("aria-expanded", "false")
await expect(page.getByRole("treeitem", { name: "Bravo" })).toBeHidden()

await combobox.press("ArrowDown")
await expect(combobox).toHaveAttribute("aria-activedescendant", await nvidia.getAttribute("id"))
await combobox.press("ArrowLeft")
await expect(nvidia).toHaveAttribute("aria-expanded", "false")
await combobox.press("ArrowRight")
await expect(nvidia).toHaveAttribute("aria-expanded", "true")
await combobox.press("ArrowRight")
await expect(combobox).toHaveAttribute(
"aria-activedescendant",
await page.getByRole("treeitem", { name: "Nemotron" }).getAttribute("id"),
)
})

test("active descendant always identifies a visible tree item", async ({ page }) => {
await load(page, "shared--model-selector-accessible")

await page.getByRole("button", { name: "Review model: Alpha" }).click()
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
const active = async () => {
await expect.poll(() => combobox.getAttribute("aria-activedescendant")).toBeTruthy()
const id = await combobox.getAttribute("aria-activedescendant")
await expect(page.locator(`[id="${id}"]`)).toBeVisible()
}

await active()
await combobox.press("ArrowDown")
await active()
await combobox.press("ArrowLeft")
await active()
await combobox.press("ArrowRight")
await active()
await combobox.fill("N")
await active()
await combobox.press("ArrowLeft")
await combobox.press("ArrowDown")
await combobox.press("ArrowLeft")
await active()
await combobox.fill("no matching model")
await active()
})

test("expanded preview waits for explicit pointer selection", async ({ page }) => {
await load(page, "shared--model-selector-accessible")

await page.getByRole("button", { name: "Review model: Alpha" }).click()
await page.getByRole("button", { name: "Expand" }).click()
await page.getByRole("option", { name: "Bravo" }).click()
await page.getByRole("treeitem", { name: "Bravo" }).click()

await expect(page.getByTestId("model-selector-value")).toHaveText("alpha")
await expect(page.getByRole("combobox", { name: "Review model: Alpha. Search models" })).toBeVisible()
Expand All @@ -66,15 +136,15 @@ test("selected favorite remains selected when its duplicate group is collapsed",

await page.getByRole("button", { name: "Review model: Alpha" }).click()
const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" })
const alpha = page.getByRole("option", { name: "Alpha" })
const favorites = page.getByRole("button", { name: "Collapse Favorites" })
const alpha = page.getByRole("treeitem", { name: "Alpha" })
const favorites = page.getByRole("treeitem", { name: "Favorites" })
await expect(alpha.first()).toHaveAttribute("aria-selected", "true")
await expect.poll(() => favorites.evaluate((el) => getComputedStyle(el).borderTopStyle)).toBe("solid")

await favorites.click()
await expect(alpha).toHaveCount(1)
await expect(alpha).toHaveAttribute("aria-selected", "true")
await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id"))
await expect(combobox).toHaveAttribute("aria-activedescendant", await favorites.getAttribute("id"))
})

test("Enter selects the active option and Escape restores selector focus", async ({ page }) => {
Expand Down Expand Up @@ -106,7 +176,7 @@ test("no-match search announces the empty result and can choose the default opti
await combobox.fill("no matching model")

await expect(page.locator(".model-selector-empty")).toHaveText("No model results")
const clear = page.getByRole("option", { name: "Use default model" })
const clear = page.getByRole("treeitem", { name: "Use default model" })
await expect(combobox).toHaveAttribute("aria-activedescendant", await clear.getAttribute("id"))
await combobox.press("Enter")

Expand Down Expand Up @@ -156,3 +226,17 @@ test("chat picker Escape returns focus to the prompt", async ({ page }) => {

await expect(page.locator("textarea.prompt-input")).toBeFocused()
})

test("slash model picker Escape returns focus to the prompt", async ({ page }) => {
await load(page, "prompt-input--default-420")

const prompt = page.locator("textarea.prompt-input")
await prompt.evaluate((el) => el.setAttribute("aria-disabled", "false"))
await prompt.fill("/model")
await prompt.press("Enter")
const combobox = page.getByRole("combobox", { name: /^Select model:.*Search models$/ })
await expect(combobox).toBeFocused()
await combobox.press("Escape")

await expect(prompt).toBeFocused()
})
Loading
Loading