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
52 changes: 51 additions & 1 deletion app/ui/lib/Combobox.browser.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Copyright Oxide Computer Company
*/
import { useState } from 'react'
import { expect, test } from 'vitest'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import { render } from 'vitest-browser-react'
import { commands, userEvent } from 'vitest/browser'

Expand All @@ -19,6 +19,22 @@ declare module 'vitest/browser' {
}
}

// Combobox's onClose branches on document.hasFocus(): a focused close clears
// the query and ends editing; an unfocused close (tab/window switch) preserves
// the query. Vitest renders tests in an iframe whose focus state is
// nondeterministic (Firefox CI sometimes reports unfocused mid-test), and
// Playwright has no supported way to background a page, so we mock hasFocus
// rather than depend on the runner's focus state. All tests assume a focused
// document except the one that overrides this to false.
// See https://playwright.dev/docs/pages#multiple-pages
beforeEach(() => {
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
})

afterEach(() => {
vi.restoreAllMocks()
})

const items = toComboboxItems([{ name: 'disk-3' }, { name: 'disk-4' }])

function ComboboxHarness({
Expand Down Expand Up @@ -76,6 +92,40 @@ test('preserves the committed selection while editing', async () => {
await expect.element(combobox).toHaveValue('disk-3')
})

test('clears the query when the user clicks outside', async () => {
const screen = await render(<ComboboxHarness />)
const combobox = screen.getByRole('combobox', { name: 'Disk name' })

await combobox.fill('disk')
await commands.pressComboboxKey('Disk name', 'ArrowDown')
await expect.element(combobox).toHaveValue('disk')
await expect.element(screen.getByRole('option', { name: 'disk-3' })).toBeVisible()

await screen.getByRole('button', { name: 'Outside' }).click()

await expect.element(screen.getByRole('option')).not.toBeInTheDocument()
await expect.element(combobox).toHaveValue('')
})

test('preserves the query when closing while the document is unfocused', async () => {
const screen = await render(<ComboboxHarness />)
const combobox = screen.getByRole('combobox', { name: 'Disk name' })

await combobox.fill('disk')
await commands.pressComboboxKey('Disk name', 'ArrowDown')
await expect.element(combobox).toHaveValue('disk')
await expect.element(screen.getByRole('option', { name: 'disk-3' })).toBeVisible()

// There's no supported way to actually background the page (see beforeEach
// comment), so mock hasFocus to false. The click and Headless UI close are
// still real.
vi.spyOn(document, 'hasFocus').mockReturnValue(false)
await screen.getByRole('button', { name: 'Outside' }).click()

await expect.element(screen.getByRole('option')).not.toBeInTheDocument()
await expect.element(combobox).toHaveValue('disk')
})

test('commits a different selected option after editing', async () => {
const screen = await render(<ComboboxHarness initialValue="disk-3" />)
const combobox = screen.getByRole('combobox', { name: 'Disk name' })
Expand Down
3 changes: 3 additions & 0 deletions app/ui/lib/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ export const Combobox = ({
}}
onClose={() => {
isOpenRef.current = false
// A window or tab switch also closes the combobox. Keep the active query
// in that case so it is still visible when the user returns.
if (!document.hasFocus()) return
setIsEditing(false)
if (!allowArbitraryValues) setQuery('')
}}
Expand Down
Loading