Skip to content
Open
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
61 changes: 61 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,67 @@ describe("App", () => {
await screen.findByText("Now")
})

it("switches sidebar tabs with Cmd+Up and Cmd+Down immediately after focus", async () => {
state.loadPluginSettingsMock.mockResolvedValueOnce({ order: ["a", "b"], disabled: [] })
state.invokeMock.mockImplementation(async (cmd: string) => {
if (cmd === "list_plugins") {
return [
{ id: "a", name: "Alpha", iconUrl: "icon-a", primaryProgressLabel: null, lines: [{ type: "text", label: "Alpha line", scope: "overview" }] },
{ id: "b", name: "Beta", iconUrl: "icon-b", primaryProgressLabel: null, lines: [{ type: "text", label: "Beta line", scope: "overview" }] },
]
}
return null
})

render(<App />)
await waitFor(() => expect(state.startBatchMock).toHaveBeenCalled())

state.probeHandlers?.onResult({
providerId: "a",
displayName: "Alpha",
iconUrl: "icon-a",
lines: [{ type: "text", label: "Alpha line", value: "A" }],
})
state.probeHandlers?.onResult({
providerId: "b",
displayName: "Beta",
iconUrl: "icon-b",
lines: [{ type: "text", label: "Beta line", value: "B" }],
})

await screen.findByText("Alpha line")
await screen.findByText("Beta line")

window.dispatchEvent(new Event("focus"))
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true }))

await waitFor(() => {
expect(screen.getByText("Alpha line")).toBeInTheDocument()
expect(screen.queryByText("Beta line")).not.toBeInTheDocument()
})

window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true }))

await waitFor(() => {
expect(screen.getByText("Beta line")).toBeInTheDocument()
expect(screen.queryByText("Alpha line")).not.toBeInTheDocument()
})

window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", metaKey: true }))

await waitFor(() => {
expect(screen.getByText("Alpha line")).toBeInTheDocument()
expect(screen.queryByText("Beta line")).not.toBeInTheDocument()
})

window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", metaKey: true }))

await waitFor(() => {
expect(screen.getByText("Alpha line")).toBeInTheDocument()
expect(screen.getByText("Beta line")).toBeInTheDocument()
})
})

it("coalesces pending tray icon timers on multiple settings changes", async () => {
state.loadPluginSettingsMock.mockResolvedValueOnce({ order: ["a", "b"], disabled: [] })
render(<App />)
Expand Down
6 changes: 5 additions & 1 deletion src/components/app/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export function AppShell({
const { updateStatus, triggerInstall, checkForUpdates } = useAppUpdate()

return (
<div ref={containerRef} className="flex flex-col items-center p-6 pt-1.5 bg-transparent">
<div
ref={containerRef}
tabIndex={-1}
className="flex flex-col items-center p-6 pt-1.5 bg-transparent outline-none"
>
<div className="tray-arrow" />
<div
className="relative bg-card rounded-xl overflow-hidden select-none w-full border shadow-lg flex flex-col"
Expand Down
191 changes: 191 additions & 0 deletions src/hooks/app/use-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe("usePanel", () => {

isTauriMock.mockReturnValue(true)
invokeMock.mockResolvedValue(undefined)
listenMock.mockResolvedValue(vi.fn())
currentMonitorMock.mockResolvedValue(null)
getCurrentWindowMock.mockReturnValue({ setSize: vi.fn().mockResolvedValue(undefined) })
})
Expand Down Expand Up @@ -150,4 +151,194 @@ describe("usePanel", () => {
expect(unlistenShowAbout).toHaveBeenCalledTimes(1)
})
})

it("switches views with Cmd+Arrow navigation", () => {
const setActiveView = vi.fn()

const firstHook = renderHook(() =>
usePanel({
activeView: "home",
setActiveView,
showAbout: false,
setShowAbout: vi.fn(),
displayPlugins: [
{
meta: { id: "a" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
{
meta: { id: "b" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
],
})
)

act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true }))
})

expect(setActiveView).toHaveBeenCalledWith("a")

firstHook.unmount()
setActiveView.mockClear()

const secondHook = renderHook(() =>
usePanel({
activeView: "b",
setActiveView,
showAbout: false,
setShowAbout: vi.fn(),
displayPlugins: [
{
meta: { id: "a" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
{
meta: { id: "b" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
],
})
)

act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true }))
})

expect(setActiveView).toHaveBeenCalledWith("home")
secondHook.unmount()
})

it("ignores Cmd+Arrow navigation from editable targets", () => {
const setActiveView = vi.fn()
const { result } = renderHook(() =>
usePanel({
activeView: "a",
setActiveView,
showAbout: false,
setShowAbout: vi.fn(),
displayPlugins: [
{
meta: { id: "a" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
],
})
)

const textbox = document.createElement("div")
textbox.setAttribute("role", "textbox")
document.body.appendChild(textbox)

act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true, bubbles: true }))
})

expect(setActiveView).toHaveBeenCalledWith("home")

setActiveView.mockClear()

act(() => {
textbox.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true, bubbles: true }))
})

expect(setActiveView).not.toHaveBeenCalled()
document.body.removeChild(textbox)
expect(result.current.containerRef.current).toBeNull()
})

it("skips settings when navigating with Cmd+Arrow", () => {
const setActiveView = vi.fn()

renderHook(() =>
usePanel({
activeView: "settings",
setActiveView,
showAbout: false,
setShowAbout: vi.fn(),
displayPlugins: [
{
meta: { id: "a" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
{
meta: { id: "b" },
data: null,
loading: false,
error: null,
lastManualRefreshAt: null,
} as any,
],
})
)

act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", metaKey: true }))
})

expect(setActiveView).toHaveBeenCalledWith("home")

setActiveView.mockClear()

act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", metaKey: true }))
})

expect(setActiveView).toHaveBeenCalledWith("b")
})

it("focuses the panel container when the window regains focus", () => {
const requestAnimationFrameSpy = vi
.spyOn(window, "requestAnimationFrame")
.mockImplementation((callback: FrameRequestCallback) => {
callback(0)
return 0
})

const { result } = renderHook(() =>
usePanel({
activeView: "home",
setActiveView: vi.fn(),
showAbout: false,
setShowAbout: vi.fn(),
displayPlugins: [],
})
)

const container = document.createElement("div")
container.tabIndex = -1
document.body.appendChild(container)

act(() => {
result.current.containerRef.current = container
})

act(() => {
window.dispatchEvent(new Event("focus"))
})

expect(container).toHaveFocus()

document.body.removeChild(container)
requestAnimationFrameSpy.mockRestore()
})
})
Loading
Loading