Skip to content

Commit dada092

Browse files
grimenclaude
andcommitted
fix(test): eliminate all console noise from jest CI logs at the source
Follow-up to #3816. Jest output is now completely free of console log/debug/warn/error blocks; every source was root-fixed rather than suppressed (full suite: 401 suites / 4526 tests passing). Shared Apollo mocks (app/graphql/mocks.ts): - All mocks are reusable via maxUsageCount: Infinity (replaces the [...mocks x6] spread hack): components fire the same queries on every mount, re-render and refetch. - Add missing mocks: scanningQRCodeScreen, contacts, supportedCountries, SettingsScreen, walletOverviewScreen, Bulletins, language, notificationSettings, sendBitcoinWithdrawalLimits, realtimePriceUnauthed. - Backfill fields the queries select but the aging mock data lacked (me.email, pendingIncomingTransactions, preImage, paymentRequest), fixing Apollo "Missing field" invariant errors on cache writes. - Extract mockCurrencyList as a shared export so specs and mocks write a consistent Query.currencyList (avoids cache-replacement warnings). "No more mocked responses" (260+ -> 0, no suppression): - home.spec / transaction-history-screen.spec: the @app/graphql/mocks module replacement now backfills with the actual shared mocks (local mocks first and infinite-use, so they keep precedence). - send-bitcoin-completed-screen.stories: pass shared mocks to the stories' MockedProviders instead of []. - use-show-warning-secure-account.spec: reusable local mocks + missing displayCurrency mock. "Maximum update depth exceeded" (4 -> 0): - use-payment-request.spec: mutation hook mocks returned a fresh jest.fn() per render, invalidating the mutations memo and re-running useInvoiceLifecycle's layout effect every render. Stable fns now. - use-invoice-lifecycle.spec: inline prcd object literals changed identity every render, ping-ponging the layout effect against the hash-paid effect. Hoisted to a stable const. - The unmasked async generateRequest updates are settled with flushEffects() (pattern from #3820). Expected error/log output is now captured and asserted instead of printed (a logged-but-expected error is indistinguishable from a real failure in CI logs): - use-kyc-flow.spec, use-lnurl-withdraw.spec, network-error-component.spec: console.error/debug spies with assertions on the expected message. - receive.spec: decodeInvoiceString mocked to return no expiry instead of throwing on the intentionally fake invoice fixtures (27 bolt11 "Invalid checksum" errors); nfc-manager mock extended with requestTechnology/getTag/cancelTechnologyRequest since ModalNfc's init path now runs. - lazy-locale-loader.test, card-status-screen.spec, card-subscription-screen.spec, receive.spec: console.log spies for app placeholder/diagnostic logs. jest.setup.js CI suppression gains only the SafeAreaView deprecation warning (react-native-country-picker-modal), alongside the existing InteractionManager one. Also fixes latent spec bugs surfaced by the now-resolving queries: missing feeReimbursementMemo in home.spec's remote-config mock and missing homeAuthed user fields (language/username/phone/email). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aefd33b commit dada092

15 files changed

Lines changed: 686 additions & 232 deletions

__tests__/graphql/network-error-component.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ describe("NetworkErrorComponent", () => {
233233
})
234234

235235
it("ignores InvalidAuthentication when networkErrorToken differs from current token", async () => {
236+
// The component logs the ignored stale-token 401 via console.debug;
237+
// capture it so the expected log doesn't pollute CI logs (and assert it
238+
// actually happened).
239+
const consoleDebugSpy = jest.spyOn(console, "debug").mockImplementation(() => {})
236240
const { rerender } = render(<NetworkErrorComponent />)
237241

238242
;(useNetworkError as jest.Mock).mockReturnValue({
@@ -250,9 +254,18 @@ describe("NetworkErrorComponent", () => {
250254
expect(mockLogout).not.toHaveBeenCalled()
251255
expect(mockClearNetworkError).toHaveBeenCalled()
252256
})
257+
expect(consoleDebugSpy).toHaveBeenCalledWith(
258+
"Ignoring 401 for non-active token",
259+
expect.objectContaining({ networkErrorToken: "stale-token" }),
260+
)
261+
consoleDebugSpy.mockRestore()
253262
})
254263

255264
it("falls back to logout on error during token expiry handling", async () => {
265+
// The component logs the simulated storage failure via console.error;
266+
// capture it so the expected error doesn't pollute CI logs (and assert it
267+
// actually happened).
268+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
256269
;(KeyStoreWrapper.getSessionProfiles as jest.Mock).mockRejectedValue(
257270
new Error("Storage error"),
258271
)
@@ -275,5 +288,10 @@ describe("NetworkErrorComponent", () => {
275288
})
276289
expect(mockClearNetworkError).toHaveBeenCalled()
277290
})
291+
expect(consoleErrorSpy).toHaveBeenCalledWith(
292+
"Error handling token expiry:",
293+
expect.objectContaining({ message: "Storage error" }),
294+
)
295+
consoleErrorSpy.mockRestore()
278296
})
279297
})

__tests__/hooks/use-kyc-flow.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ describe("useKycFlow", () => {
187187
})
188188

189189
it("calls goBack on canceled error", async () => {
190+
// The hook logs the simulated failure via console.error; capture it so the
191+
// expected error doesn't pollute CI logs (and assert it actually happened).
192+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
190193
mockKycFlowStart.mockRejectedValue(new Error("Request canceled by user"))
191194

192195
const { result } = renderHook(() => useKycFlow())
@@ -197,9 +200,17 @@ describe("useKycFlow", () => {
197200

198201
expect(mockGoBack).toHaveBeenCalledTimes(1)
199202
expect(Alert.alert).not.toHaveBeenCalled()
203+
expect(consoleErrorSpy).toHaveBeenCalledWith(
204+
"error:",
205+
expect.objectContaining({ message: "Request canceled by user" }),
206+
)
207+
consoleErrorSpy.mockRestore()
200208
})
201209

202210
it("shows Alert on other errors", async () => {
211+
// The hook logs the simulated failure via console.error; capture it so the
212+
// expected error doesn't pollute CI logs (and assert it actually happened).
213+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
203214
mockKycFlowStart.mockRejectedValue(new Error("Network failure"))
204215

205216
const { result } = renderHook(() => useKycFlow())
@@ -213,6 +224,11 @@ describe("useKycFlow", () => {
213224
expect.stringContaining("Network failure"),
214225
expect.arrayContaining([expect.objectContaining({ text: "OK" })]),
215226
)
227+
expect(consoleErrorSpy).toHaveBeenCalledWith(
228+
"error:",
229+
expect.objectContaining({ message: "Network failure" }),
230+
)
231+
consoleErrorSpy.mockRestore()
216232
})
217233

218234
it("sets loading true during startKyc, false after", async () => {

__tests__/hooks/use-show-warning-secure-account.spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { act } from "react-test-renderer"
55
import { MockedProvider } from "@apollo/client/testing"
66
import {
77
CurrencyListDocument,
8+
DisplayCurrencyDocument,
89
RealtimePriceDocument,
910
WarningSecureAccountDocument,
1011
} from "@app/graphql/generated"
@@ -42,6 +43,8 @@ const mocksPrice = [
4243
request: {
4344
query: RealtimePriceDocument,
4445
},
46+
// The hook re-renders and re-requests; reusable so MockLink never warns
47+
maxUsageCount: Number.POSITIVE_INFINITY,
4548
result: {
4649
data: {
4750
me: {
@@ -77,6 +80,7 @@ const mocksPrice = [
7780
request: {
7881
query: CurrencyListDocument,
7982
},
83+
maxUsageCount: Number.POSITIVE_INFINITY,
8084
result: {
8185
data: {
8286
currencyList: [
@@ -92,6 +96,25 @@ const mocksPrice = [
9296
},
9397
},
9498
},
99+
{
100+
request: {
101+
query: DisplayCurrencyDocument,
102+
},
103+
maxUsageCount: Number.POSITIVE_INFINITY,
104+
result: {
105+
data: {
106+
me: {
107+
id: "70df9822-efe0-419c-b864-c9efa99872ea",
108+
defaultAccount: {
109+
id: "84b26b88-89b0-5c6f-9d3d-fbead08f79d8",
110+
displayCurrency: "USD",
111+
__typename: "ConsumerAccount",
112+
},
113+
__typename: "User",
114+
},
115+
},
116+
},
117+
},
95118
]
96119

97120
const mockLevelZeroLowBalance = [

__tests__/i18n/lazy-locale-loader.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,23 @@ const mockedLoadLocaleAsync = loadLocaleAsync as jest.MockedFunction<
2525
>
2626

2727
describe("lazy-locale-loader", () => {
28+
// ensureLocaleLoaded logs "Loading locale on demand: <locale>" in __DEV__
29+
// (which jest runs as); capture it so the expected log doesn't pollute CI logs.
30+
let consoleLogSpy: jest.SpyInstance
31+
2832
beforeEach(() => {
2933
jest.clearAllMocks()
34+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {})
3035
// Reset loadedLocales
3136
for (const key of Object.keys(loadedLocales)) {
3237
delete (loadedLocales as Record<string, unknown>)[key]
3338
}
3439
})
3540

41+
afterEach(() => {
42+
consoleLogSpy.mockRestore()
43+
})
44+
3645
describe("isLocaleLoaded", () => {
3746
it("returns false for an unloaded locale", () => {
3847
expect(isLocaleLoaded("fr" as Locales)).toBe(false)
@@ -48,6 +57,7 @@ describe("lazy-locale-loader", () => {
4857
it("calls loadLocaleAsync for an unloaded locale", async () => {
4958
await ensureLocaleLoaded("de" as Locales)
5059
expect(mockedLoadLocaleAsync).toHaveBeenCalledWith("de")
60+
expect(consoleLogSpy).toHaveBeenCalledWith("Loading locale on demand: de")
5161
})
5262

5363
it("skips loadLocaleAsync if locale is already loaded", async () => {

__tests__/receive-bitcoin/hooks/use-invoice-lifecycle.spec.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ jest.mock("@app/graphql/generated", () => ({
3131
WalletCurrency: { Btc: "BTC", Usd: "USD" },
3232
}))
3333

34+
// prcd must keep a stable identity across renders: useInvoiceLifecycle's
35+
// layout effect recreates the PR whenever prcd changes, so an inline object
36+
// literal per render would loop with the state-updating effects until React
37+
// aborts with "Maximum update depth exceeded".
38+
const stablePrcd = { id: "test" } as never
39+
3440
const mockMutations = {
3541
lnNoAmountInvoiceCreate: jest.fn(),
3642
lnInvoiceCreate: jest.fn(),
@@ -116,7 +122,7 @@ describe("useInvoiceLifecycle", () => {
116122
)
117123

118124
const { result } = renderHook(() =>
119-
useInvoiceLifecycle({ id: "test" } as never, mockMutations as never),
125+
useInvoiceLifecycle(stablePrcd, mockMutations as never),
120126
)
121127

122128
expect(result.current.regenerateInvoice).toBeDefined()
@@ -136,7 +142,7 @@ describe("useInvoiceLifecycle", () => {
136142
mockCreatePaymentRequest.mockReturnValue(mockPR)
137143
mockUseLnUpdateHashPaid.mockReturnValue("abc123")
138144

139-
renderHook(() => useInvoiceLifecycle({ id: "test" } as never, mockMutations as never))
145+
renderHook(() => useInvoiceLifecycle(stablePrcd, mockMutations as never))
140146

141147
expect(mockHaptic).toHaveBeenCalledWith("notificationSuccess", {
142148
ignoreAndroidSystemSettings: true,
@@ -156,7 +162,7 @@ describe("useInvoiceLifecycle", () => {
156162
mockCreatePaymentRequest.mockReturnValue(mockPR)
157163
mockUseLnUpdateHashPaid.mockReturnValue("different-hash")
158164

159-
renderHook(() => useInvoiceLifecycle({ id: "test" } as never, mockMutations as never))
165+
renderHook(() => useInvoiceLifecycle(stablePrcd, mockMutations as never))
160166

161167
expect(mockHaptic).not.toHaveBeenCalled()
162168
})
@@ -168,7 +174,7 @@ describe("useInvoiceLifecycle", () => {
168174
mockUseCountdown.mockReturnValue({ remainingSeconds: 300, isExpired: false })
169175

170176
const { result } = renderHook(() =>
171-
useInvoiceLifecycle({ id: "test" } as never, mockMutations as never),
177+
useInvoiceLifecycle(stablePrcd, mockMutations as never),
172178
)
173179

174180
expect(result.current.expiresInSeconds).toBe(300)

__tests__/receive-bitcoin/hooks/use-lnurl-withdraw.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ describe("useLnurlWithdraw", () => {
139139
})
140140

141141
it("shows submission error on non-ok response", async () => {
142+
// The hook logs the simulated failure via console.error; capture it so the
143+
// expected error doesn't pollute CI logs (and assert it actually happened).
144+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
142145
const alertSpy = jest.spyOn(Alert, "alert")
143146
mockFetch.mockResolvedValue({
144147
ok: false,
@@ -156,9 +159,17 @@ describe("useLnurlWithdraw", () => {
156159
})
157160

158161
expect(alertSpy).toHaveBeenCalledWith("Submission error")
162+
expect(consoleErrorSpy).toHaveBeenCalledWith(
163+
expect.anything(),
164+
"error with submitting withdrawalRequest",
165+
)
166+
consoleErrorSpy.mockRestore()
159167
})
160168

161169
it("shows redeeming error when response status is not ok", async () => {
170+
// The hook logs the simulated failure via console.error; capture it so the
171+
// expected error doesn't pollute CI logs (and assert it actually happened).
172+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
162173
const alertSpy = jest.spyOn(Alert, "alert")
163174
mockFetch.mockResolvedValue({
164175
ok: true,
@@ -176,6 +187,11 @@ describe("useLnurlWithdraw", () => {
176187
})
177188

178189
expect(alertSpy).toHaveBeenCalledWith("Redeeming error", "Invalid k1")
190+
expect(consoleErrorSpy).toHaveBeenCalledWith(
191+
expect.objectContaining({ status: "ERROR", reason: "Invalid k1" }),
192+
"error with redeeming",
193+
)
194+
consoleErrorSpy.mockRestore()
179195
})
180196

181197
it("does not alert on successful withdraw", async () => {

__tests__/receive-bitcoin/hooks/use-payment-request.spec.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { renderHook } from "@testing-library/react-hooks"
22

3+
import { flushEffects } from "../../helpers/flush-effects"
4+
35
import { usePaymentRequest } from "@app/screens/receive-bitcoin-screen/hooks/use-payment-request"
46
import { WalletCurrency } from "@app/graphql/generated"
57
import {
@@ -28,12 +30,20 @@ jest.mock("@app/screens/receive-bitcoin-screen/hooks/use-wallet-resolution", ()
2830
useWalletResolution: () => mockUseWalletResolution(),
2931
}))
3032

33+
// The mutation functions must keep a stable identity across renders: they
34+
// feed the `mutations` useMemo in usePaymentRequest, and a fresh jest.fn()
35+
// per render would re-trigger useInvoiceLifecycle's layout effect on every
36+
// render, looping until React aborts with "Maximum update depth exceeded".
37+
const mockLnInvoiceCreate = jest.fn()
38+
const mockLnNoAmountInvoiceCreate = jest.fn()
39+
const mockLnUsdInvoiceCreate = jest.fn()
40+
const mockOnChainAddressCurrent = jest.fn()
3141
jest.mock("@app/graphql/generated", () => ({
3242
WalletCurrency: { Btc: "BTC", Usd: "USD" },
33-
useLnInvoiceCreateMutation: () => [jest.fn()],
34-
useLnNoAmountInvoiceCreateMutation: () => [jest.fn()],
35-
useLnUsdInvoiceCreateMutation: () => [jest.fn()],
36-
useOnChainAddressCurrentMutation: () => [jest.fn()],
43+
useLnInvoiceCreateMutation: () => [mockLnInvoiceCreate],
44+
useLnNoAmountInvoiceCreateMutation: () => [mockLnNoAmountInvoiceCreate],
45+
useLnUsdInvoiceCreateMutation: () => [mockLnUsdInvoiceCreate],
46+
useOnChainAddressCurrentMutation: () => [mockOnChainAddressCurrent],
3747
}))
3848

3949
const mockUseLnUpdateHashPaid = jest.fn()
@@ -153,17 +163,20 @@ describe("usePaymentRequest", () => {
153163
expect(result.current).toBeNull()
154164
})
155165

156-
it("creates PRCD with PayCode for BTC default wallet with username", () => {
166+
it("creates PRCD with PayCode for BTC default wallet with username", async () => {
157167
setupMocksWithPR()
158168

159169
renderHook(() => usePaymentRequest())
160170

171+
// Settle generateRequest's async setPR inside act()
172+
await flushEffects()
173+
161174
expect(mockCreatePaymentRequestCreationData).toHaveBeenCalledWith(
162175
expect.objectContaining({ type: Invoice.PayCode }),
163176
)
164177
})
165178

166-
it("creates PRCD with Lightning for wallet without username", () => {
179+
it("creates PRCD with Lightning for wallet without username", async () => {
167180
const walletsNoUsername = { ...mockWallets, username: null }
168181
const mockPRCD = {
169182
...createFullMockPRCD(),
@@ -189,12 +202,15 @@ describe("usePaymentRequest", () => {
189202

190203
renderHook(() => usePaymentRequest())
191204

205+
// Settle generateRequest's async setPR inside act()
206+
await flushEffects()
207+
192208
expect(mockCreatePaymentRequestCreationData).toHaveBeenCalledWith(
193209
expect.objectContaining({ type: Invoice.Lightning }),
194210
)
195211
})
196212

197-
it("creates PRCD with Lightning for USD default wallet even with username", () => {
213+
it("creates PRCD with Lightning for USD default wallet even with username", async () => {
198214
const walletsUsdDefault = {
199215
...mockWallets,
200216
defaultWallet: { id: "usd-id", balance: 100, walletCurrency: WalletCurrency.Usd },
@@ -219,16 +235,22 @@ describe("usePaymentRequest", () => {
219235

220236
renderHook(() => usePaymentRequest())
221237

238+
// Settle generateRequest's async setPR inside act()
239+
await flushEffects()
240+
222241
expect(mockCreatePaymentRequestCreationData).toHaveBeenCalledWith(
223242
expect.objectContaining({ type: Invoice.Lightning }),
224243
)
225244
})
226245

227-
it("uses default expiration time for BTC wallet", () => {
246+
it("uses default expiration time for BTC wallet", async () => {
228247
setupMocksWithPR()
229248

230249
renderHook(() => usePaymentRequest())
231250

251+
// Settle generateRequest's async setPR inside act()
252+
await flushEffects()
253+
232254
expect(mockCreatePaymentRequestCreationData).toHaveBeenCalledWith(
233255
expect.objectContaining({ expirationTime: 1440 }),
234256
)

__tests__/screens/card-screen/card-status-screen.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ const cardApprovedWithCustomColorParams = {
6060
iconColor: "#FF5500",
6161
}
6262

63+
// Some app handlers log placeholder/diagnostic messages via console.log when
64+
// pressed (e.g. "Add to wallet pressed" in card-status-layout); capture them so expected logs don't pollute CI logs.
65+
let consoleLogSpy: jest.SpyInstance
66+
67+
beforeEach(() => {
68+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {})
69+
})
70+
71+
afterEach(() => {
72+
consoleLogSpy.mockRestore()
73+
})
74+
6375
describe("CardStatusScreen - Card Approved variant", () => {
6476
beforeEach(() => {
6577
loadLocale("en")

__tests__/screens/card-screen/onboarding/card-flow/card-subscription-screen.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ jest.mock("@app/hooks/use-display-currency", () => ({
8484
}),
8585
}))
8686

87+
// Some app handlers log placeholder/diagnostic messages via console.log when
88+
// pressed (e.g. "TODO: payment flow" in card-subscription-screen); capture them so expected logs don't pollute CI logs.
89+
let consoleLogSpy: jest.SpyInstance
90+
91+
beforeEach(() => {
92+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {})
93+
})
94+
95+
afterEach(() => {
96+
consoleLogSpy.mockRestore()
97+
})
98+
8799
describe("CardSubscriptionScreen - subscribe variant", () => {
88100
beforeEach(() => {
89101
loadLocale("en")

0 commit comments

Comments
 (0)