From 9fd63e3933773f82999d2730fb4126eabe2e9002 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:52:12 -0700 Subject: [PATCH] test(mobile): pin both stale-failure guards and the in-flight retry precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found two coverage gaps, each proven by mutation: - The `requestIsCurrent()` guard on the not-ok/`{error}` page path could be deleted with all 913 tests still green — only the rejection twin was pinned. The swapped-out-from-under test now runs over both failure shapes. - `not.toContain('Tap to retry')` was asserted with no `loadEarlierError` set, so it could not fail. It now renders both props, pinning that an in-flight retry outranks the failure it is retrying. --- .../src/session/MobileNativeChatView.test.ts | 12 +++++++++-- .../use-mobile-native-chat-session.test.ts | 20 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/mobile/src/session/MobileNativeChatView.test.ts b/mobile/src/session/MobileNativeChatView.test.ts index e8325dbb125..4ea87922a9a 100644 --- a/mobile/src/session/MobileNativeChatView.test.ts +++ b/mobile/src/session/MobileNativeChatView.test.ts @@ -269,11 +269,19 @@ describe('MobileNativeChatView', () => { expect(header.props.accessibilityLabel).toBe('Couldn’t load earlier messages. Tap to retry') }) - it('keeps the header tappable-but-idle while a page is in flight', async () => { - await render({ hasMore: true, loadingEarlier: true, onLoadEarlier: vi.fn() }) + // An in-flight retry outranks the failure it is retrying, so the header shows + // the spinner rather than a stale error the user has already acted on. + it('disables the header and hides the failure while a retry is in flight', async () => { + await render({ + hasMore: true, + loadingEarlier: true, + loadEarlierError: 'Couldn’t load earlier messages', + onLoadEarlier: vi.fn() + }) const header = loadEarlierHeader() expect(header.props.disabled).toBe(true) + expect(headerText(header)).not.toContain('Couldn’t load earlier messages') expect(headerText(header)).not.toContain('Tap to retry') }) }) diff --git a/mobile/src/session/use-mobile-native-chat-session.test.ts b/mobile/src/session/use-mobile-native-chat-session.test.ts index 0461b074164..f0a7b0b5d96 100644 --- a/mobile/src/session/use-mobile-native-chat-session.test.ts +++ b/mobile/src/session/use-mobile-native-chat-session.test.ts @@ -8,6 +8,9 @@ import { type MobileNativeChatSession } from './use-mobile-native-chat-session' +/** Hands a pending `sendRequest` promise's settlers to the test body. */ +type PageSettle = { resolve: (response: unknown) => void; reject: (reason: unknown) => void } + function message(id: string): NativeChatMessage { return { id, @@ -255,10 +258,19 @@ describe('useMobileNativeChatSession', () => { expect(state?.loadingEarlier).toBe(false) }) - it('drops a failure from a page the session swapped out from under', async () => { - let rejectEarlier: (reason: unknown) => void = () => {} + // Both stale-failure guards have to hold: a swapped-out page can fail by + // rejecting or by resolving not-ok, and each has its own `requestIsCurrent()`. + it.each([ + ['a rejected request', (settle: PageSettle) => settle.reject(new Error('disconnected'))], + ['a not-ok response', (settle: PageSettle) => settle.resolve({ ok: false, error: 'offline' })] + ])('drops %s from a page the session swapped out from under', async (_label, settleEarlier) => { + const settle: PageSettle = { resolve: () => {}, reject: () => {} } const sendRequest = vi.fn( - () => new Promise((_resolve, reject) => (rejectEarlier = reject)) + () => + new Promise((resolve, reject) => { + settle.resolve = resolve + settle.reject = reject + }) ) as unknown as RpcClient['sendRequest'] const subscribe: RpcClient['subscribe'] = vi.fn((_method, _params, onData) => { emit = onData @@ -277,7 +289,7 @@ describe('useMobileNativeChatSession', () => { emit({ type: 'replacement', messages: [message('fresh')], hasMore: true, beforeOffset: 0 }) ) await act(async () => { - rejectEarlier(new Error('disconnected')) + settleEarlier(settle) await Promise.resolve() })