mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(mobile): let an unmounted quick-command row emit nothing on a refusal
The haptic I added ran before the mounted guard, so a copy pressed on a row that then scrolled out of the list, or a sheet closed over it, still buzzed when the rejection arrived. A buzz with no row to explain it is feedback for nothing, and the guard was already there for the feedback state one line below. Red-first: press, unmount, then reject. The success path already guarded first. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -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<void> {
|
||||
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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user