661a4e7572
A new in-app toast surface is the batch's spine, making the silent-failure and
global-status gaps fixable:
- Toast infra: store/toasts.ts (useToasts + toast(); auto-dismiss) + ui/Toaster.tsx
(non-portal bottom-center stack, avoids the Fluent-portal GPU flicker). Unit-
tested (test/toasts.test.ts). Mounted at the app root beside LiveRegion.
- UX6: shared renderer reveal.ts revealFile('open'|'folder') awaits the IPC result
and toasts the main-process reason instead of silently no-oping on a moved/typed-
out file. safeShowInFolder upgraded to return an error string like safeOpenPath
(preload/mock/Api types updated).
- UX9 / UI25: a Fluent CounterBadge on the sidebar Downloads nav item shows the
active (downloading + queued) count from any tab, via a count-only App selector
that doesn't re-render on progress ticks.
- UX15: cancelAll store action + a "Cancel all (N)" header button.
- UX24: the disabled "Check watched" button uses disabledFocusable + a Hint that
explains it needs a watched source.
- UX12: the Terminal gate gains an inline "Enable custom commands" button, so it's
no longer a dead-end pointing at another screen.
- L144: the DownloadBar duplicate guard now also checks history — a URL downloaded
earlier warns "Already downloaded: …" (dupInHistory) vs "Already in your queue: …".
Deferred with rationale (CODE-AUDIT.md): UX7/UX8 (Settings IA redesign — visual),
UX18/UX26 (subjective/visual polish), L131 (arguably by-design, needs live taskbar),
L142 (history/templates/sources IPC-return reconciliation — a focused own PR).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
41 lines
1.2 KiB
TypeScript
41 lines
1.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { useToasts, toast } from '../src/renderer/src/store/toasts'
|
|
|
|
describe('toasts store', () => {
|
|
beforeEach(() => {
|
|
useToasts.setState({ toasts: [] })
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('show() adds a toast with the given message and tone', () => {
|
|
toast('could not open file', 'error')
|
|
const ts = useToasts.getState().toasts
|
|
expect(ts).toHaveLength(1)
|
|
expect(ts[0]?.message).toBe('could not open file')
|
|
expect(ts[0]?.tone).toBe('error')
|
|
})
|
|
|
|
it('defaults the tone to info', () => {
|
|
toast('heads up')
|
|
expect(useToasts.getState().toasts[0]?.tone).toBe('info')
|
|
})
|
|
|
|
it('dismiss() removes only the matching toast', () => {
|
|
toast('a')
|
|
toast('b')
|
|
const firstId = useToasts.getState().toasts[0]?.id ?? ''
|
|
useToasts.getState().dismiss(firstId)
|
|
const ts = useToasts.getState().toasts
|
|
expect(ts).toHaveLength(1)
|
|
expect(ts[0]?.message).toBe('b')
|
|
})
|
|
|
|
it('auto-dismisses after the timeout elapses', () => {
|
|
vi.useFakeTimers()
|
|
toast('temporary')
|
|
expect(useToasts.getState().toasts).toHaveLength(1)
|
|
vi.advanceTimersByTime(5000)
|
|
expect(useToasts.getState().toasts).toHaveLength(0)
|
|
})
|
|
})
|