Skip to content

Commit 0debff1

Browse files
committed
Error report dialog: clearer dev-mode UX
- "Save bundle to disk (debug)" now toasts a persistent success with the saved path and a "Reveal in Finder" button. Previously the action quietly toasted info-level text behind the dialog and looked like nothing happened. New `BundleSavedToastContent.svelte` follows the same module-state bridging pattern as `ErrorReportToastContent.svelte`. - "Send report" is now disabled in dev builds with a tooltip explaining why (`upload()` short-circuits to skip the network in `cfg!(debug_assertions)` so dev runs don't pollute prod data). Pre-fix the click pretended to succeed and silently no-op'd.
1 parent 01bc0da commit 0debff1

4 files changed

Lines changed: 196 additions & 10 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Tier 3 a11y tests for `BundleSavedToastContent.svelte`.
3+
*
4+
* Toast body shown after a successful "Save bundle to disk (debug)" action.
5+
* Reads the saved-bundle path from a module-level `$state` set via
6+
* `setLastSavedBundlePath(path)`.
7+
*/
8+
9+
import { describe, it, vi, expect } from 'vitest'
10+
import { mount, tick } from 'svelte'
11+
import BundleSavedToastContent, { setLastSavedBundlePath } from './BundleSavedToastContent.svelte'
12+
import { expectNoA11yViolations } from '$lib/test-a11y'
13+
import { dismissToast } from '$lib/ui/toast'
14+
import { showInFinder } from '$lib/tauri-commands'
15+
16+
vi.mock('$lib/ui/toast', () => ({
17+
dismissToast: vi.fn(),
18+
}))
19+
20+
vi.mock('$lib/tauri-commands', () => ({
21+
showInFinder: vi.fn(() => Promise.resolve()),
22+
}))
23+
24+
describe('BundleSavedToastContent', () => {
25+
it('default render has no a11y violations', async () => {
26+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte module export type not resolved
27+
setLastSavedBundlePath('/Users/test/Application Support/com.veszelovszki.cmdr-dev/error-report-debug.zip')
28+
const target = document.createElement('div')
29+
document.body.appendChild(target)
30+
mount(BundleSavedToastContent, { target, props: {} })
31+
await tick()
32+
await expectNoA11yViolations(target)
33+
})
34+
35+
it('renders the most recently saved path', () => {
36+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte module export type not resolved
37+
setLastSavedBundlePath('/tmp/bundle-XYZ.zip')
38+
const target = document.createElement('div')
39+
document.body.appendChild(target)
40+
mount(BundleSavedToastContent, { target, props: {} })
41+
expect(target.textContent).toContain('/tmp/bundle-XYZ.zip')
42+
})
43+
44+
it('Reveal in Finder button calls showInFinder with the saved path', async () => {
45+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte module export type not resolved
46+
setLastSavedBundlePath('/tmp/bundle-REV.zip')
47+
const target = document.createElement('div')
48+
document.body.appendChild(target)
49+
mount(BundleSavedToastContent, { target, props: {} })
50+
await tick()
51+
const revealButton = Array.from(target.querySelectorAll('button')).find(
52+
(b) => b.textContent.trim() === 'Reveal in Finder',
53+
)
54+
if (!revealButton) throw new Error('Reveal in Finder button missing')
55+
revealButton.click()
56+
expect(showInFinder).toHaveBeenCalledWith('/tmp/bundle-REV.zip')
57+
})
58+
59+
it('Dismiss button calls dismissToast with the toast ID', async () => {
60+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte module export type not resolved
61+
setLastSavedBundlePath('/tmp/bundle-DIS.zip')
62+
const target = document.createElement('div')
63+
document.body.appendChild(target)
64+
mount(BundleSavedToastContent, { target, props: {} })
65+
await tick()
66+
const dismissButton = Array.from(target.querySelectorAll('button')).find((b) => b.textContent.trim() === 'Dismiss')
67+
if (!dismissButton) throw new Error('Dismiss button missing')
68+
dismissButton.click()
69+
expect(dismissToast).toHaveBeenCalledWith('error-report-bundle-saved')
70+
})
71+
})
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<script module lang="ts">
2+
/**
3+
* Module-level slot for the path of the most recently saved debug bundle.
4+
* `ErrorReportDialog` sets this right before `addToast(BundleSavedToastContent, ...)`.
5+
* Same prop-bridging pattern as `ErrorReportToastContent`.
6+
*/
7+
let lastSavedBundlePath = $state('')
8+
9+
export function setLastSavedBundlePath(path: string): void {
10+
lastSavedBundlePath = path
11+
}
12+
</script>
13+
14+
<script lang="ts">
15+
import { dismissToast } from '$lib/ui/toast'
16+
import { showInFinder } from '$lib/tauri-commands'
17+
18+
const toastId = 'error-report-bundle-saved'
19+
20+
function handleReveal() {
21+
if (lastSavedBundlePath) {
22+
void showInFinder(lastSavedBundlePath)
23+
}
24+
}
25+
26+
function handleDismiss() {
27+
dismissToast(toastId)
28+
}
29+
</script>
30+
31+
<div class="content">
32+
<span class="message">Saved bundle to disk</span>
33+
<span class="path" title={lastSavedBundlePath}>{lastSavedBundlePath}</span>
34+
<div class="actions">
35+
<button class="link-button" onclick={handleReveal}>Reveal in Finder</button>
36+
<button class="link-button" onclick={handleDismiss}>Dismiss</button>
37+
</div>
38+
</div>
39+
40+
<style>
41+
.content {
42+
display: flex;
43+
flex-direction: column;
44+
gap: var(--spacing-xs);
45+
font-size: var(--font-size-sm);
46+
}
47+
48+
.message {
49+
color: var(--color-text-primary);
50+
line-height: 1.4;
51+
}
52+
53+
.path {
54+
font-family: var(--font-mono);
55+
font-size: var(--font-size-xs);
56+
color: var(--color-text-tertiary);
57+
white-space: nowrap;
58+
overflow: hidden;
59+
text-overflow: ellipsis;
60+
max-width: 320px;
61+
}
62+
63+
.actions {
64+
display: flex;
65+
gap: var(--spacing-md);
66+
}
67+
68+
.link-button {
69+
background: none;
70+
border: none;
71+
padding: 0;
72+
font-size: var(--font-size-xs);
73+
color: var(--color-text-tertiary);
74+
cursor: default;
75+
}
76+
77+
.link-button:hover {
78+
color: var(--color-text-secondary);
79+
}
80+
</style>

apps/desktop/src/lib/error-reporter/ErrorReportDialog.a11y.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* The `prepareErrorReportPreview` IPC is mocked so the test runs deterministically.
66
*/
77

8-
import { describe, it, vi, expect, beforeEach } from 'vitest'
8+
import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'
99
import { mount, tick } from 'svelte'
1010
import ErrorReportDialog from './ErrorReportDialog.svelte'
1111
import { expectNoA11yViolations } from '$lib/test-a11y'
@@ -57,6 +57,15 @@ Object.defineProperty(navigator, 'clipboard', {
5757
describe('ErrorReportDialog', () => {
5858
beforeEach(() => {
5959
closeErrorReportDialog()
60+
// Tests run in Vitest's `test` mode where `import.meta.env.DEV` is true
61+
// by default. Pretend we're a release build so the dialog's dev-mode
62+
// Send-disable doesn't shadow the assertions below — there's a separate
63+
// test ("disables Send in dev builds") for the dev-disable behavior.
64+
vi.stubEnv('DEV', false)
65+
})
66+
67+
afterEach(() => {
68+
vi.unstubAllEnvs()
6069
})
6170

6271
it('default render has no a11y violations', async () => {

apps/desktop/src/lib/error-reporter/ErrorReportDialog.svelte

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
} from '$lib/tauri-commands/error-reporter'
1919
2020
import ErrorReportToastContent, { setLastSentReportId } from './ErrorReportToastContent.svelte'
21+
import BundleSavedToastContent, { setLastSavedBundlePath } from './BundleSavedToastContent.svelte'
2122
import { closeErrorReportDialog, errorReportFlow } from './error-report-flow.svelte'
2223
import { getAppLogger } from '$lib/logging/logger'
24+
import { tooltip } from '$lib/tooltip/tooltip'
2325
2426
const log = getAppLogger('errorReportDialog')
2527
@@ -109,12 +111,28 @@
109111
async function handleSaveToDisk() {
110112
try {
111113
const path = await saveErrorReportToDisk(userNote || undefined)
112-
addToast(`Saved bundle to ${path}`, { level: 'info', timeoutMs: 8000 })
114+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Svelte module export type not resolved
115+
setLastSavedBundlePath(path)
116+
addToast(BundleSavedToastContent, {
117+
id: 'error-report-bundle-saved',
118+
level: 'success',
119+
dismissal: 'persistent',
120+
})
113121
} catch (e) {
114122
addToast(`Couldn't save bundle: ${String(e)}`, { level: 'error' })
115123
}
116124
}
117125
126+
// In dev / debug builds the backend's `upload()` function intentionally
127+
// short-circuits before hitting the network (see `error_reporter::upload`),
128+
// so clicking Send used to look like it succeeded but did nothing. Disable
129+
// the button instead and explain via a tooltip — the Save-to-disk button
130+
// is right next to it for inspecting the bundle locally.
131+
const sendDisabledInDev = isDev
132+
const sendTooltip = sendDisabledInDev
133+
? "Disabled in dev builds — uploads are skipped on purpose so dev runs don't pollute prod. Use 'Save bundle to disk' to inspect the report locally."
134+
: undefined
135+
118136
async function handleCopyId() {
119137
if (!preview) return
120138
await navigator.clipboard.writeText(preview.id)
@@ -128,7 +146,13 @@
128146
129147
function handleKeydown(event: KeyboardEvent) {
130148
// Cmd/Ctrl+Enter sends. Plain Enter is consumed by the textarea.
131-
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && !sending && !noteOverLimit) {
149+
if (
150+
(event.metaKey || event.ctrlKey) &&
151+
event.key === 'Enter' &&
152+
!sending &&
153+
!noteOverLimit &&
154+
!sendDisabledInDev
155+
) {
132156
event.preventDefault()
133157
void handleSend()
134158
}
@@ -239,13 +263,15 @@
239263
{/if}
240264
<span class="spacer"></span>
241265
<Button variant="secondary" onclick={handleClose} disabled={sending}>Cancel</Button>
242-
<Button
243-
variant="primary"
244-
onclick={() => void handleSend()}
245-
disabled={sending || noteOverLimit || preparing}
246-
>
247-
{sending ? 'Sending…' : 'Send report'}
248-
</Button>
266+
<span use:tooltip={sendTooltip}>
267+
<Button
268+
variant="primary"
269+
onclick={() => void handleSend()}
270+
disabled={sending || noteOverLimit || preparing || sendDisabledInDev}
271+
>
272+
{sending ? 'Sending…' : 'Send report'}
273+
</Button>
274+
</span>
249275
</div>
250276
</div>
251277
</ModalDialog>

0 commit comments

Comments
 (0)