diff --git a/mobile/src/session/QuickCommandRow.test.ts b/mobile/src/session/QuickCommandRow.test.ts index c7f7fa4f31c..b47e2160bf3 100644 --- a/mobile/src/session/QuickCommandRow.test.ts +++ b/mobile/src/session/QuickCommandRow.test.ts @@ -87,18 +87,20 @@ describe('the quick-command row when the pasteboard refuses the text', () => { return button } + function rowProps() { + return { + command: COMMAND, + first: true, + onLaunch: vi.fn(), + onEdit: vi.fn(), + onDelete: vi.fn(), + disabled: false + } + } + async function mountAndCopy(): Promise { await act(async () => { - renderer = create( - createElement(QuickCommandRow, { - command: COMMAND, - first: true, - onLaunch: vi.fn(), - onEdit: vi.fn(), - onDelete: vi.fn(), - disabled: false - }) - ) + renderer = create(createElement(QuickCommandRow, rowProps())) }) await act(async () => { copyButton().props.onPress() @@ -112,6 +114,30 @@ describe('the quick-command row when the pasteboard refuses the text', () => { expect(haptics.notificationAsync).toHaveBeenCalledWith('error') }) + it('emits nothing at all when the row is gone before the refusal arrives', async () => { + // A copy pressed on a row that then scrolls out of the list, or a sheet closed over it. The + // rejection still arrives, and a buzz with no row to explain it is feedback for nothing. + let refuse: ((error: Error) => void) | undefined + clipboard.setStringAsync.mockImplementation( + () => + new Promise((_resolve, reject) => { + refuse = reject + }) + ) + await act(async () => { + renderer = create(createElement(QuickCommandRow, rowProps())) + }) + await act(async () => { + copyButton().props.onPress() + }) + act(() => renderer?.unmount()) + renderer = null + await act(async () => { + refuse?.(new Error('pasteboard refused')) + }) + expect(haptics.notificationAsync).not.toHaveBeenCalled() + }) + it('shows the copied label and no error buzz when the write lands', async () => { // The control: a failure assertion is only evidence if the success path reads differently. clipboard.setStringAsync.mockResolvedValue(true) diff --git a/mobile/src/session/QuickCommandRow.tsx b/mobile/src/session/QuickCommandRow.tsx index 4bb9b8aec8f..abf2df50d58 100644 --- a/mobile/src/session/QuickCommandRow.tsx +++ b/mobile/src/session/QuickCommandRow.tsx @@ -75,12 +75,14 @@ export function QuickCommandRow({ } setFeedback({ body, status: 'copied' }) } catch { - // The row says so on its own control rather than in a toast; the buzz is the part a thumb - // resting on the button it just pressed can notice without looking. - triggerError() + // The guard first: a row unmounted before the refusal arrives has nothing to explain a buzz + // with, and the feedback it would set is read by a component that is gone. if (!mountedRef.current) { return } + // The row says so on its own control rather than in a toast; the buzz is the part a thumb + // resting on the button it just pressed can notice without looking. + triggerError() setFeedback({ body, status: 'failed' }) } if (copyResetTimerRef.current) {