diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts index 65e7539340d876b753e154774dba5fd030f717c2..975bf7be13d1fd2a198ca75a640c72e20b184908 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts @@ -58,10 +58,23 @@ describe("resolveNativeReviewDiffView", () => { it("returns null when the view manager cannot be required", async () => { setExpoViewConfigAvailable(); + const cause = new Error("boom"); expoMocks.requireNativeView.mockImplementation(() => { - throw new Error("boom"); + throw cause; }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface"); + expect(resolveNativeReviewDiffView()).toBeNull(); + expect(resolveNativeReviewDiffView()).toBeNull(); + expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1); + expect(consoleError).toHaveBeenCalledWith( + expect.objectContaining({ + _tag: "NativeViewResolutionError", + nativeModuleName: "T3ReviewDiffSurface", + cause, + }), + ); + expect(consoleError).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 4a68166347137efebd3ab62e8f7f9eebb6f8b4ea..7660a047752bbd849b6961cc87bf15c03665a42e 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -2,6 +2,8 @@ import type { ComponentType } from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { requireNativeView } from "expo"; +import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; + const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface"; interface ExpoGlobalWithViewConfig { @@ -128,6 +130,7 @@ export interface NativeReviewDiffViewProps extends ViewProps { } let cachedNativeReviewDiffView: ComponentType | undefined; +let nativeReviewDiffViewResolutionFailed = false; function getExpoViewConfig(moduleName: string) { return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( @@ -140,6 +143,10 @@ export function resolveNativeReviewDiffView(): ComponentType( NATIVE_REVIEW_DIFF_MODULE_NAME, ); - } catch { + } catch (cause) { + nativeReviewDiffViewResolutionFailed = true; + console.error( + new NativeViewResolutionError({ + nativeModuleName: NATIVE_REVIEW_DIFF_MODULE_NAME, + cause, + }), + ); return null; } diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts index c7418a5253398cda6c132accc003270fe1c0e311..5cb37cbb0a9dcfdc3709fc10f100037ad1e301d7 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts @@ -43,10 +43,23 @@ describe("resolveNativeTerminalSurfaceView", () => { it("returns null when the view manager cannot be required", async () => { setExpoViewConfigAvailable(); + const cause = new Error("boom"); expoMocks.requireNativeView.mockImplementation(() => { - throw new Error("boom"); + throw cause; }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); + expect(resolveNativeTerminalSurfaceView()).toBeNull(); + expect(resolveNativeTerminalSurfaceView()).toBeNull(); + expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1); + expect(consoleError).toHaveBeenCalledWith( + expect.objectContaining({ + _tag: "NativeViewResolutionError", + nativeModuleName: "T3TerminalSurface", + cause, + }), + ); + expect(consoleError).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.ts index c4686a38b4e8bc255612b4b19556a9d0b0fce33b..e5b1f6300734baa5fdf22f10dc9b69901921bb55 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.ts @@ -2,6 +2,8 @@ import type { ComponentType } from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { requireNativeView } from "expo"; +import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; + const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface"; interface ExpoGlobalWithViewConfig { @@ -33,6 +35,7 @@ export interface NativeTerminalSurfaceProps extends ViewProps { } let cachedNativeTerminalSurfaceView: ComponentType | undefined; +let nativeTerminalSurfaceViewResolutionFailed = false; function getExpoViewConfig(moduleName: string) { return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( @@ -45,6 +48,10 @@ export function resolveNativeTerminalSurfaceView(): ComponentType( NATIVE_TERMINAL_MODULE_NAME, ); - } catch { + } catch (cause) { + nativeTerminalSurfaceViewResolutionFailed = true; + console.error( + new NativeViewResolutionError({ + nativeModuleName: NATIVE_TERMINAL_MODULE_NAME, + cause, + }), + ); return null; } diff --git a/apps/mobile/src/native/nativeViewResolutionError.ts b/apps/mobile/src/native/nativeViewResolutionError.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfcf8351a66c7e874cd45b374774df8a7ee3ed9c --- /dev/null +++ b/apps/mobile/src/native/nativeViewResolutionError.ts @@ -0,0 +1,13 @@ +import * as Schema from "effect/Schema"; + +export class NativeViewResolutionError extends Schema.TaggedErrorClass()( + "NativeViewResolutionError", + { + nativeModuleName: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to resolve native view ${this.nativeModuleName}.`; + } +}