@terreno/rtk 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.ai/guidelines/core.md +2 -0
  2. package/dist/authSlice.d.ts.map +1 -1
  3. package/dist/authSlice.js +3 -1
  4. package/dist/authSlice.js.map +1 -1
  5. package/dist/authSlice.test.js +15 -8
  6. package/dist/authSlice.test.js.map +1 -1
  7. package/dist/authSliceNative.test.js +3 -16
  8. package/dist/authSliceNative.test.js.map +1 -1
  9. package/dist/betterAuthClient.d.ts.map +1 -1
  10. package/dist/betterAuthClient.js +0 -1
  11. package/dist/betterAuthClient.js.map +1 -1
  12. package/dist/betterAuthSlice.test.js +1 -0
  13. package/dist/betterAuthSlice.test.js.map +1 -1
  14. package/dist/constants.d.ts +7 -1
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js.map +1 -1
  17. package/dist/emptyApi.d.ts.map +1 -1
  18. package/dist/emptyApi.js +0 -2
  19. package/dist/emptyApi.js.map +1 -1
  20. package/dist/isolated/constants.isolated.d.ts +2 -0
  21. package/dist/isolated/constants.isolated.d.ts.map +1 -0
  22. package/dist/isolated/constants.isolated.js +81 -0
  23. package/dist/isolated/constants.isolated.js.map +1 -0
  24. package/dist/isolated/constants.test.js +53 -75
  25. package/dist/isolated/constants.test.js.map +1 -1
  26. package/dist/isolated/offlineMiddleware.isolated.js +0 -1
  27. package/dist/isolated/offlineMiddleware.isolated.js.map +1 -1
  28. package/dist/isolated/offlineMiddleware.native.isolated.js +0 -1
  29. package/dist/isolated/offlineMiddleware.native.isolated.js.map +1 -1
  30. package/dist/isolated/useUpgradeCheck.mobile.isolated.js +47 -0
  31. package/dist/isolated/useUpgradeCheck.mobile.isolated.js.map +1 -1
  32. package/dist/mongooseSlice.test.js +0 -1
  33. package/dist/mongooseSlice.test.js.map +1 -1
  34. package/dist/offlineGate.d.ts +1 -1
  35. package/dist/offlineGate.d.ts.map +1 -1
  36. package/dist/offlineGate.js +1 -3
  37. package/dist/offlineGate.js.map +1 -1
  38. package/dist/offlineMiddleware.d.ts.map +1 -1
  39. package/dist/offlineMiddleware.js +26 -0
  40. package/dist/offlineMiddleware.js.map +1 -1
  41. package/dist/offlineMiddleware.test.js +5 -1
  42. package/dist/offlineMiddleware.test.js.map +1 -1
  43. package/dist/realtime.test.js +1 -0
  44. package/dist/realtime.test.js.map +1 -1
  45. package/dist/sessionApi.test.d.ts +2 -0
  46. package/dist/sessionApi.test.d.ts.map +1 -0
  47. package/dist/sessionApi.test.js +84 -0
  48. package/dist/sessionApi.test.js.map +1 -0
  49. package/dist/sync.d.ts.map +1 -1
  50. package/dist/sync.js.map +1 -1
  51. package/dist/sync.test.d.ts +2 -0
  52. package/dist/sync.test.d.ts.map +1 -0
  53. package/dist/sync.test.js +296 -0
  54. package/dist/sync.test.js.map +1 -0
  55. package/dist/testPreload.test.js +6 -0
  56. package/dist/testPreload.test.js.map +1 -1
  57. package/dist/useFeatureFlags.d.ts.map +1 -1
  58. package/dist/useFeatureFlags.js.map +1 -1
  59. package/dist/useTerrenoFeatureFlags.d.ts.map +1 -1
  60. package/dist/useTerrenoFeatureFlags.js.map +1 -1
  61. package/dist/useTerrenoFeatureFlags.test.js +10 -0
  62. package/dist/useTerrenoFeatureFlags.test.js.map +1 -1
  63. package/dist/useUpgradeCheckNative.test.js +21 -0
  64. package/dist/useUpgradeCheckNative.test.js.map +1 -1
  65. package/package.json +2 -2
  66. package/src/authSlice.test.ts +25 -13
  67. package/src/authSlice.ts +3 -1
  68. package/src/authSliceNative.test.ts +4 -18
  69. package/src/betterAuthClient.ts +1 -2
  70. package/src/betterAuthSlice.test.ts +1 -0
  71. package/src/constants.ts +7 -2
  72. package/src/emptyApi.ts +5 -4
  73. package/src/isolated/constants.isolated.ts +118 -0
  74. package/src/isolated/constants.test.ts +81 -78
  75. package/src/isolated/offlineMiddleware.isolated.ts +1 -2
  76. package/src/isolated/offlineMiddleware.native.isolated.ts +1 -2
  77. package/src/isolated/useUpgradeCheck.mobile.isolated.ts +60 -0
  78. package/src/mongooseSlice.test.ts +1 -2
  79. package/src/offlineGate.ts +1 -2
  80. package/src/offlineMiddleware.test.ts +6 -1
  81. package/src/offlineMiddleware.ts +27 -0
  82. package/src/realtime.test.ts +1 -0
  83. package/src/sessionApi.test.ts +115 -0
  84. package/src/sync.test.ts +390 -0
  85. package/src/sync.ts +2 -1
  86. package/src/testPreload.test.ts +7 -0
  87. package/src/useFeatureFlags.ts +1 -0
  88. package/src/useTerrenoFeatureFlags.test.ts +16 -0
  89. package/src/useTerrenoFeatureFlags.ts +2 -0
  90. package/src/useUpgradeCheckNative.test.ts +26 -0
@@ -1,9 +1,11 @@
1
+ // noExplicitAny: test mock typing
1
2
  // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
- import {beforeEach, describe, expect, it} from "bun:test";
3
+ import {beforeEach, describe, expect, it, mock} from "bun:test";
3
4
  import AsyncStorage from "@react-native-async-storage/async-storage";
4
5
  import {configureStore} from "@reduxjs/toolkit";
5
6
  import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
6
7
  import {renderHook} from "@testing-library/react-native";
8
+ import {assert} from "chai";
7
9
  import React from "react";
8
10
  import {Provider} from "react-redux";
9
11
 
@@ -18,6 +20,10 @@ import {
18
20
  useSelectCurrentUserId,
19
21
  useSelectIsAuthenticating,
20
22
  } from "./authSlice";
23
+ import type {RootState} from "./constants";
24
+
25
+ // Force IsWeb=true regardless of load order with the native test files.
26
+ mock.module("./platform", () => ({IsWeb: true}));
21
27
 
22
28
  // Create a real RTK Query API with the endpoints that generateAuthSlice expects
23
29
  const api = createApi({
@@ -41,7 +47,9 @@ const api = createApi({
41
47
  });
42
48
 
43
49
  const createTestStore = () => {
44
- const {authReducer, middleware, ...rest} = generateAuthSlice(api as any);
50
+ const {authReducer, middleware, ...rest} = generateAuthSlice(
51
+ api as unknown as Parameters<typeof generateAuthSlice>[0]
52
+ );
45
53
 
46
54
  return {
47
55
  ...rest,
@@ -281,26 +289,30 @@ describe("generateAuthSlice", () => {
281
289
 
282
290
  describe("selectors", () => {
283
291
  it("selectCurrentUserId returns userId", () => {
284
- const state = {auth: {userId: "user-123"}} as any;
285
- expect(selectCurrentUserId(state)).toBe("user-123");
292
+ const state: RootState = {auth: {lastTokenRefreshTimestamp: null, userId: "user-123"}};
293
+ assert.equal(selectCurrentUserId(state), "user-123");
286
294
  });
287
295
 
288
296
  it("selectCurrentUserId returns undefined when no auth state", () => {
289
- expect(selectCurrentUserId({} as any)).toBeUndefined();
297
+ assert.isUndefined(selectCurrentUserId({}));
290
298
  });
291
299
 
292
300
  it("selectIsAuthenticating returns isAuthenticating", () => {
293
- const state = {auth: {isAuthenticating: true}} as any;
294
- expect(selectIsAuthenticating(state)).toBe(true);
301
+ const state: RootState = {auth: {isAuthenticating: true, lastTokenRefreshTimestamp: null}};
302
+ assert.isTrue(selectIsAuthenticating(state));
295
303
  });
296
304
 
297
305
  it("selectIsAuthenticating defaults to false", () => {
298
- expect(selectIsAuthenticating({} as any)).toBe(false);
306
+ assert.isFalse(selectIsAuthenticating({}));
299
307
  });
300
308
 
301
309
  it("selectLastTokenRefreshTimestamp returns timestamp", () => {
302
- const state = {auth: {lastTokenRefreshTimestamp: 12345}} as any;
303
- expect(selectLastTokenRefreshTimestamp(state)).toBe(12345);
310
+ const state: RootState = {auth: {lastTokenRefreshTimestamp: 12345}};
311
+ assert.equal(selectLastTokenRefreshTimestamp(state), 12345);
312
+ });
313
+
314
+ it("selectLastTokenRefreshTimestamp defaults to null", () => {
315
+ assert.isNull(selectLastTokenRefreshTimestamp({}));
304
316
  });
305
317
  });
306
318
 
@@ -321,9 +333,9 @@ describe("EmailLoginRequest type", () => {
321
333
  describe("generateProfileEndpoints", () => {
322
334
  it("builds endpoint query payloads", () => {
323
335
  const builder = {
324
- mutation: (config: any) => config,
325
- };
326
- const endpoints = generateProfileEndpoints(builder as any, "todos");
336
+ mutation: (config: unknown) => config,
337
+ } as unknown as Parameters<typeof generateProfileEndpoints>[0];
338
+ const endpoints = generateProfileEndpoints(builder, "todos");
327
339
  const createEmailUserQuery = endpoints.createEmailUser.query;
328
340
  const emailLoginQuery = endpoints.emailLogin.query;
329
341
  const emailSignUpQuery = endpoints.emailSignUp.query;
package/src/authSlice.ts CHANGED
@@ -66,6 +66,7 @@ export interface GoogleLoginRequest {
66
66
  }
67
67
 
68
68
  export const generateProfileEndpoints = (
69
+ // noExplicitAny: Generic
69
70
  // biome-ignore lint/suspicious/noExplicitAny: Generic
70
71
  builder: EndpointBuilder<BaseQueryFn<unknown, unknown, unknown>, any, string>,
71
72
  path: string
@@ -119,6 +120,7 @@ export const generateProfileEndpoints = (
119
120
  };
120
121
  };
121
122
 
123
+ // noExplicitAny: Generic
122
124
  // biome-ignore lint/suspicious/noExplicitAny: Generic
123
125
  export const generateAuthSlice = (api: Api<any, any, any, any, any>) => {
124
126
  const authSlice = createSlice({
@@ -298,7 +300,7 @@ export const generateAuthSlice = (api: Api<any, any, any, any, any>) => {
298
300
 
299
301
  export const selectCurrentUserId = (state: RootState): string | undefined => state.auth?.userId;
300
302
  export const selectLastTokenRefreshTimestamp = (state: RootState): number | null =>
301
- state.auth?.lastTokenRefreshTimestamp;
303
+ state.auth?.lastTokenRefreshTimestamp ?? null;
302
304
  export const selectIsAuthenticating = (state: RootState): boolean =>
303
305
  state.auth?.isAuthenticating ?? false;
304
306
 
@@ -1,5 +1,4 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
- import {afterAll, afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
1
+ import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
3
2
 
4
3
  // Keep this mock a superset of the preload's react-native mock so that other
5
4
  // test files (e.g. useUpgradeCheck.test.ts) can still find AppState and Linking
@@ -17,7 +16,8 @@ mock.module("react-native", () => ({
17
16
 
18
17
  // Force IsWeb=false regardless of whether ./platform was already imported
19
18
  // elsewhere in the test run. `mock.module` is hoisted in bun, so this takes
20
- // effect before the dynamic imports below.
19
+ // effect before the dynamic imports below. Web-path test files must set their
20
+ // own top-of-file `IsWeb: true` guard; do not rely on afterAll restore here.
21
21
  mock.module("./platform", () => ({IsWeb: false}));
22
22
 
23
23
  const secureCalls = {
@@ -69,7 +69,7 @@ const api = createApi({
69
69
  });
70
70
 
71
71
  const createTestStore = () => {
72
- const {authReducer, middleware, authSlice} = auth.generateAuthSlice(api as any);
72
+ const {authReducer, middleware, authSlice} = auth.generateAuthSlice(api);
73
73
  const store = configureStore({
74
74
  middleware: (getDefault) =>
75
75
  getDefault({serializableCheck: false}).concat(api.middleware, ...middleware),
@@ -111,20 +111,6 @@ describe("native listener middleware side effects", () => {
111
111
  console.error = originalError;
112
112
  });
113
113
 
114
- afterAll(() => {
115
- // Restore mocks to the values the rest of the suite expects.
116
- mock.module("react-native", () => ({
117
- Platform: {OS: "web"},
118
- StyleSheet: {create: (s: unknown) => s},
119
- }));
120
- mock.module("./platform", () => ({IsWeb: true}));
121
- mock.module("expo-secure-store", () => ({
122
- deleteItemAsync: async () => {},
123
- getItemAsync: async () => null,
124
- setItemAsync: async () => {},
125
- }));
126
- });
127
-
128
114
  it("stores tokens in SecureStore on native login", async () => {
129
115
  const {store} = createTestStore();
130
116
  store.dispatch({
@@ -96,8 +96,7 @@ export const createBetterAuthClient = (config: BetterAuthClientConfig) => {
96
96
  plugins: [
97
97
  expoClient({
98
98
  scheme: config.scheme,
99
- // biome-ignore lint/suspicious/noExplicitAny: Better Auth storage type is flexible
100
- storage: storage as any,
99
+ storage: storage as NonNullable<Parameters<typeof expoClient>[0]>["storage"],
101
100
  storagePrefix: config.storagePrefix ?? "terreno",
102
101
  }),
103
102
  ],
@@ -1,3 +1,4 @@
1
+ // noExplicitAny: test mock typing
1
2
  // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
3
  import {beforeEach, describe, expect, it, mock} from "bun:test";
3
4
  import {configureStore} from "@reduxjs/toolkit";
package/src/constants.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import Constants from "expo-constants";
2
2
 
3
- // biome-ignore lint/suspicious/noExplicitAny: RootState is hard to type without becoming circular.
4
- export type RootState = any;
3
+ export interface RootState {
4
+ auth?: {
5
+ isAuthenticating?: boolean;
6
+ lastTokenRefreshTimestamp: number | null;
7
+ userId?: string;
8
+ };
9
+ }
5
10
  export const LOGOUT_ACTION_TYPE = "auth/logout";
6
11
  export const TOKEN_REFRESHED_SUCCESS = "auth/tokenRefreshedSuccess";
7
12
 
package/src/emptyApi.ts CHANGED
@@ -207,8 +207,7 @@ export const getBaseQuery = (
207
207
  return result;
208
208
  }
209
209
  },
210
- // biome-ignore lint/suspicious/noExplicitAny: Weird typing from rtk query
211
- })(args, api, extraOptions as any);
210
+ })(args, api, extraOptions as Parameters<ReturnType<typeof fetchBaseQuery>>[2]);
212
211
  };
213
212
 
214
213
  export const staggeredBaseQuery = retry(
@@ -355,8 +354,10 @@ export const emptySplitApi = createApi({
355
354
  url: "/version-check",
356
355
  }),
357
356
  }),
358
- // biome-ignore lint/suspicious/noExplicitAny: Generic
359
- ...generateProfileEndpoints(builder as any, "users"), // using 'users' here since it is highly intertwined with Users
357
+ ...generateProfileEndpoints(
358
+ builder as unknown as Parameters<typeof generateProfileEndpoints>[0],
359
+ "users"
360
+ ), // using 'users' here since it is highly intertwined with Users
360
361
  }),
361
362
  reducerPath: "terreno-rtk",
362
363
  });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * In-process isolated coverage for constants.ts module-load debug paths.
3
+ *
4
+ * The base test suite loads constants.ts once with debug flags disabled, so the
5
+ * enable-time branches (AUTH_DEBUG / WEBSOCKETS_DEBUG log lines and the Expo
6
+ * tunnel warning) never execute under coverage. Here we re-mock expo-constants
7
+ * with every flag enabled and re-import the module through a cache-busting query
8
+ * so its top-level runs again while coverage instrumentation is active.
9
+ */
10
+ import {beforeEach, describe, it, mock} from "bun:test";
11
+ import {assert} from "chai";
12
+
13
+ interface ConstantsModule {
14
+ AUTH_DEBUG: boolean;
15
+ logAuth: (...args: string[]) => void;
16
+ logSocket: (
17
+ user?: {featureFlags?: {debugWebsockets?: {enabled?: boolean}}} | boolean,
18
+ ...args: string[]
19
+ ) => void;
20
+ isWebsocketsDebugEnabled: () => boolean;
21
+ }
22
+
23
+ const loadWithEnabledFlags = async (): Promise<{
24
+ module: ConstantsModule;
25
+ debugCalls: unknown[][];
26
+ errorCalls: unknown[][];
27
+ infoCalls: unknown[][];
28
+ }> => {
29
+ mock.module("expo-constants", () => ({
30
+ default: {
31
+ expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}},
32
+ expoGoConfig: {debuggerHost: "abc.exp.direct"},
33
+ },
34
+ }));
35
+
36
+ const debugCalls: unknown[][] = [];
37
+ const errorCalls: unknown[][] = [];
38
+ const infoCalls: unknown[][] = [];
39
+ const originalDebug = console.debug;
40
+ const originalError = console.error;
41
+ const originalInfo = console.info;
42
+ console.debug = (...args: unknown[]): void => {
43
+ debugCalls.push(args);
44
+ };
45
+ console.error = (...args: unknown[]): void => {
46
+ errorCalls.push(args);
47
+ };
48
+ console.info = (...args: unknown[]): void => {
49
+ infoCalls.push(args);
50
+ };
51
+
52
+ try {
53
+ const constantsUrl = new URL("../constants.ts", import.meta.url).href;
54
+ const module = (await import(`${constantsUrl}?enabledFlags`)) as ConstantsModule;
55
+ return {debugCalls, errorCalls, infoCalls, module};
56
+ } finally {
57
+ console.debug = originalDebug;
58
+ console.error = originalError;
59
+ console.info = originalInfo;
60
+ }
61
+ };
62
+
63
+ describe("constants module-load debug paths", () => {
64
+ beforeEach(() => {
65
+ mock.restore();
66
+ });
67
+
68
+ it("logs debug lines and the tunnel warning when every flag is enabled", async () => {
69
+ const {module, debugCalls, errorCalls} = await loadWithEnabledFlags();
70
+
71
+ assert.isTrue(module.AUTH_DEBUG);
72
+ assert.isTrue(module.isWebsocketsDebugEnabled());
73
+ assert.isTrue(
74
+ debugCalls.some((args) =>
75
+ args.some((value) => String(value).includes("AUTH_DEBUG is enabled"))
76
+ )
77
+ );
78
+ assert.isTrue(
79
+ debugCalls.some((args) =>
80
+ args.some((value) => String(value).includes("WEBSOCKETS_DEBUG is enabled"))
81
+ )
82
+ );
83
+ assert.isTrue(
84
+ errorCalls.some((args) =>
85
+ args.some((value) => String(value).includes("Expo Tunnel is not currently"))
86
+ )
87
+ );
88
+ });
89
+
90
+ it("routes logAuth and logSocket through console when flags are enabled", async () => {
91
+ const {module} = await loadWithEnabledFlags();
92
+
93
+ const debugCalls: unknown[][] = [];
94
+ const infoCalls: unknown[][] = [];
95
+ const originalDebug = console.debug;
96
+ const originalInfo = console.info;
97
+ console.debug = (...args: unknown[]): void => {
98
+ debugCalls.push(args);
99
+ };
100
+ console.info = (...args: unknown[]): void => {
101
+ infoCalls.push(args);
102
+ };
103
+ try {
104
+ module.logAuth("auth message");
105
+ module.logSocket(undefined, "socket message");
106
+ } finally {
107
+ console.debug = originalDebug;
108
+ console.info = originalInfo;
109
+ }
110
+
111
+ assert.isTrue(
112
+ debugCalls.some((args) => args.some((value) => String(value).includes("auth message")))
113
+ );
114
+ assert.isTrue(
115
+ infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "socket message")
116
+ );
117
+ });
118
+ });
@@ -1,92 +1,95 @@
1
1
  /**
2
2
  * Isolated tests for constants.ts paths that require mock.module.
3
3
  *
4
- * These live in isolated/ so that mock.module calls do not interfere
5
- * with coverage tracking in the main constants.test.ts file.
4
+ * The module is loaded in a subprocess so mock.module does not replace
5
+ * expo-constants for the rest of the package test process.
6
6
  */
7
- import {describe, expect, it, mock} from "bun:test";
7
+ import {describe, it} from "bun:test";
8
+ import {assert} from "chai";
9
+
10
+ interface ConstantsModuleResult {
11
+ authDebug: boolean;
12
+ debugCalls: unknown[][];
13
+ errorCalls: unknown[][];
14
+ infoCalls: unknown[][];
15
+ }
16
+
17
+ const runConstantsModule = async (
18
+ expoConstants: Record<string, unknown>
19
+ ): Promise<ConstantsModuleResult> => {
20
+ const constantsUrl = new URL("../constants.ts", import.meta.url).href;
21
+ const script = `
22
+ import {mock} from "bun:test";
23
+ const output = console.log.bind(console);
24
+ const debugCalls = [];
25
+ const errorCalls = [];
26
+ const infoCalls = [];
27
+ console.debug = (...args) => debugCalls.push(args);
28
+ console.error = (...args) => errorCalls.push(args);
29
+ console.info = (...args) => infoCalls.push(args);
30
+ mock.module("expo-constants", () => ({default: ${JSON.stringify(expoConstants)}}));
31
+ const loaded = await import(${JSON.stringify(constantsUrl)});
32
+ loaded.logAuth("hello");
33
+ loaded.logSocket(undefined, "ws on");
34
+ output(JSON.stringify({
35
+ authDebug: loaded.AUTH_DEBUG,
36
+ debugCalls,
37
+ errorCalls,
38
+ infoCalls,
39
+ }));
40
+ `;
41
+ const child = Bun.spawn([process.execPath, "-e", script], {
42
+ cwd: import.meta.dir,
43
+ stderr: "pipe",
44
+ stdout: "pipe",
45
+ });
46
+ const [exitCode, stdout, stderr] = await Promise.all([
47
+ child.exited,
48
+ new Response(child.stdout).text(),
49
+ new Response(child.stderr).text(),
50
+ ]);
51
+
52
+ assert.equal(exitCode, 0, stderr);
53
+ return JSON.parse(stdout.trim()) as ConstantsModuleResult;
54
+ };
8
55
 
9
56
  describe("expo tunnel warning", () => {
10
57
  it("warns when expoGoConfig.debuggerHost contains exp.direct", async () => {
11
- const errorCalls: unknown[][] = [];
12
- const originalError = console.error;
13
- console.error = (...args: unknown[]): void => {
14
- errorCalls.push(args);
15
- };
16
- mock.module("expo-constants", () => ({
17
- default: {
18
- expoConfig: {extra: {}},
19
- expoGoConfig: {debuggerHost: "abc.exp.direct"},
20
- },
21
- }));
22
- try {
23
- const testId = `${Date.now()}-${Math.random()}`;
24
- await import(`../constants?case=${testId}`);
25
- const warning = errorCalls.find((args) =>
26
- args.some((v) => typeof v === "string" && v.includes("Expo Tunnel is not currently"))
27
- );
28
- expect(warning).toBeDefined();
29
- } finally {
30
- console.error = originalError;
31
- mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
32
- }
58
+ const result = await runConstantsModule({
59
+ expoConfig: {extra: {}},
60
+ expoGoConfig: {debuggerHost: "abc.exp.direct"},
61
+ });
62
+
63
+ assert.isTrue(
64
+ result.errorCalls.some((args) =>
65
+ args.some((value) => String(value).includes("Expo Tunnel is not currently"))
66
+ )
67
+ );
33
68
  });
34
69
  });
35
70
 
36
71
  describe("AUTH_DEBUG enabled path", () => {
37
- it("logs debug messages from logAuth when AUTH_DEBUG is true on module load", async () => {
38
- const debugCalls: unknown[][] = [];
39
- const infoCalls: unknown[][] = [];
40
- const originalDebug = console.debug;
41
- const originalInfo = console.info;
42
- console.debug = (...args: unknown[]): void => {
43
- debugCalls.push(args);
44
- };
45
- console.info = (...args: unknown[]): void => {
46
- infoCalls.push(args);
47
- };
48
- mock.module("expo-constants", () => ({
49
- default: {expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}}},
50
- }));
51
- try {
52
- const testId = `${Date.now()}-${Math.random()}`;
53
- const loaded = (await import(`../constants?case=${testId}`)) as typeof import("../constants");
54
- expect(loaded.AUTH_DEBUG).toBe(true);
55
- const preLength = debugCalls.length;
56
- loaded.logAuth("hello");
57
- expect(debugCalls.length).toBe(preLength + 1);
58
- loaded.logSocket(undefined, "ws on");
59
- expect(infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "ws on")).toBe(true);
60
- } finally {
61
- console.debug = originalDebug;
62
- console.info = originalInfo;
63
- mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
64
- }
65
- });
72
+ it("logs auth and websocket debug messages when enabled", async () => {
73
+ const result = await runConstantsModule({
74
+ expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}},
75
+ });
66
76
 
67
- it("emits AUTH_DEBUG enabled and WEBSOCKETS_DEBUG enabled messages on module load", async () => {
68
- const debugCalls: unknown[][] = [];
69
- const originalDebug = console.debug;
70
- console.debug = (...args: unknown[]): void => {
71
- debugCalls.push(args);
72
- };
73
- mock.module("expo-constants", () => ({
74
- default: {expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}}},
75
- }));
76
- try {
77
- const testId = `${Date.now()}-${Math.random()}`;
78
- await import(`../constants?debug=${testId}`);
79
- const authMsg = debugCalls.find((args) =>
80
- args.some((v) => typeof v === "string" && v.includes("AUTH_DEBUG is enabled"))
81
- );
82
- const wsMsg = debugCalls.find((args) =>
83
- args.some((v) => typeof v === "string" && v.includes("WEBSOCKETS_DEBUG is enabled"))
84
- );
85
- expect(authMsg).toBeDefined();
86
- expect(wsMsg).toBeDefined();
87
- } finally {
88
- console.debug = originalDebug;
89
- mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
90
- }
77
+ assert.isTrue(result.authDebug);
78
+ assert.isTrue(
79
+ result.debugCalls.some((args) =>
80
+ args.some((value) => String(value).includes("AUTH_DEBUG is enabled"))
81
+ )
82
+ );
83
+ assert.isTrue(
84
+ result.debugCalls.some((args) =>
85
+ args.some((value) => String(value).includes("WEBSOCKETS_DEBUG is enabled"))
86
+ )
87
+ );
88
+ assert.isTrue(
89
+ result.debugCalls.some((args) => args.some((value) => String(value).includes("hello")))
90
+ );
91
+ assert.isTrue(
92
+ result.infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "ws on")
93
+ );
91
94
  });
92
95
  });
@@ -89,8 +89,7 @@ const api = createApi({
89
89
 
90
90
  const createTestStore = (endpoints = ["postTodos", "patchTodosById", "deleteTodosById"]) => {
91
91
  const offline = createOfflineMiddleware({
92
- // biome-ignore lint/suspicious/noExplicitAny: Generic API type is intentionally broad.
93
- api: api as any,
92
+ api: api as unknown as Parameters<typeof createOfflineMiddleware>[0]["api"],
94
93
  endpoints,
95
94
  });
96
95
 
@@ -45,8 +45,7 @@ const api = createApi({
45
45
 
46
46
  const createNativeStore = () => {
47
47
  const offline = createOfflineMiddleware({
48
- // biome-ignore lint/suspicious/noExplicitAny: Generic API type is intentionally broad.
49
- api: api as any,
48
+ api: api as unknown as Parameters<typeof createOfflineMiddleware>[0]["api"],
50
49
  endpoints: ["patchTodosById"],
51
50
  });
52
51
 
@@ -195,6 +195,66 @@ describe("useUpgradeCheck (mobile)", () => {
195
195
  });
196
196
  });
197
197
 
198
+ describe("recheckOnForeground", () => {
199
+ it("re-checks when the app returns to the foreground", async () => {
200
+ const {result} = renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
201
+
202
+ await act(async () => {
203
+ await flushPromises();
204
+ });
205
+
206
+ expect(result.current).toBeDefined();
207
+ expect(appStateListeners.length).toBeGreaterThan(0);
208
+ const initialCalls = mockTrigger.mock.calls.length;
209
+
210
+ await act(async () => {
211
+ // First go to the background, then return to the foreground: the
212
+ // background transition primes appState.current so the subsequent
213
+ // active transition satisfies the wasBackground && isNowActive guard.
214
+ for (const listener of appStateListeners) {
215
+ listener("background");
216
+ }
217
+ for (const listener of appStateListeners) {
218
+ listener("active");
219
+ }
220
+ await flushPromises();
221
+ });
222
+
223
+ expect(mockTrigger.mock.calls.length).toBeGreaterThan(initialCalls);
224
+ });
225
+
226
+ it("does not re-check when the app stays inactive", async () => {
227
+ renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
228
+
229
+ await act(async () => {
230
+ await flushPromises();
231
+ });
232
+
233
+ const initialCalls = mockTrigger.mock.calls.length;
234
+
235
+ await act(async () => {
236
+ for (const listener of appStateListeners) {
237
+ listener("inactive");
238
+ }
239
+ await flushPromises();
240
+ });
241
+
242
+ expect(mockTrigger.mock.calls.length).toBe(initialCalls);
243
+ });
244
+
245
+ it("removes the AppState subscription on unmount", async () => {
246
+ const {unmount} = renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
247
+
248
+ await act(async () => {
249
+ await flushPromises();
250
+ });
251
+
252
+ mockRemove.mockClear();
253
+ unmount();
254
+ expect(mockRemove).toHaveBeenCalled();
255
+ });
256
+ });
257
+
198
258
  describe("canUpdate", () => {
199
259
  it("is false on mobile when updateUrl is not set", () => {
200
260
  const {result} = renderHook(() => useUpgradeCheck());
@@ -1,4 +1,3 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
1
  import {describe, expect, it} from "bun:test";
3
2
 
4
3
  import {type ListResponse, populateId} from "./mongooseSlice";
@@ -39,7 +38,7 @@ describe("populateId", () => {
39
38
 
40
39
  it("handles sparse arrays without throwing", () => {
41
40
  const sparse: ListResponse<{_id: string}> = {
42
- data: [undefined as any, {_id: "found"}],
41
+ data: [undefined as unknown as {_id: string}, {_id: "found"}],
43
42
  };
44
43
  expect(populateId("found", sparse)).toEqual({_id: "found"});
45
44
  });
@@ -13,8 +13,7 @@ export const configureOfflineMutationEndpoints = (endpoints: string[]): void =>
13
13
  */
14
14
  export const shouldDeferOfflineMutation = (
15
15
  endpointName: string,
16
- // biome-ignore lint/suspicious/noExplicitAny: Redux getState is app-specific
17
- getState: () => any
16
+ getState: () => unknown
18
17
  ): boolean => {
19
18
  if (!offlineMutationEndpoints.has(endpointName)) {
20
19
  return false;
@@ -1,4 +1,4 @@
1
- import {describe, expect, it} from "bun:test";
1
+ import {describe, expect, it, mock} from "bun:test";
2
2
  import {configureStore, type UnknownAction} from "@reduxjs/toolkit";
3
3
  import type {Api} from "@reduxjs/toolkit/query/react";
4
4
 
@@ -10,6 +10,9 @@ import {
10
10
  } from "./offlineMiddleware";
11
11
  import {type OfflineState, type QueuedMutation, selectOfflineQueue} from "./offlineSlice";
12
12
 
13
+ // Force IsWeb=true regardless of load order with the native test files.
14
+ mock.module("./platform", () => ({IsWeb: true}));
15
+
13
16
  interface QueryEntry {
14
17
  data?: {data: Record<string, unknown>[]};
15
18
  originalArgs: unknown;
@@ -47,6 +50,7 @@ const initialTestApiState: TestApiState = {
47
50
 
48
51
  const testApiReducer = (state: TestApiState = initialTestApiState): TestApiState => state;
49
52
 
53
+ // noExplicitAny: Test double covers only fields used by offline middleware.
50
54
  // biome-ignore lint/suspicious/noExplicitAny: Test double covers only fields used by offline middleware.
51
55
  const createTestApi = (): Api<any, any, any, any> => {
52
56
  return {
@@ -66,6 +70,7 @@ const createTestApi = (): Api<any, any, any, any> => {
66
70
  invalidateTags: () => ({type: "testApi/invalidateTags"}),
67
71
  updateQueryData: () => ({type: "testApi/updateQueryData"}),
68
72
  },
73
+ // noExplicitAny: Test double covers only fields used by offline middleware.
69
74
  // biome-ignore lint/suspicious/noExplicitAny: Test double covers only fields used by offline middleware.
70
75
  } as unknown as Api<any, any, any, any>;
71
76
  };