@terreno/rtk 0.9.3 → 0.11.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 (67) hide show
  1. package/dist/authSlice.test.js +257 -1
  2. package/dist/authSlice.test.js.map +1 -1
  3. package/dist/authSliceNative.test.d.ts +2 -0
  4. package/dist/authSliceNative.test.d.ts.map +1 -0
  5. package/dist/authSliceNative.test.js +167 -0
  6. package/dist/authSliceNative.test.js.map +1 -0
  7. package/dist/betterAuthClient.d.ts +16 -0
  8. package/dist/betterAuthClient.d.ts.map +1 -1
  9. package/dist/betterAuthClient.js +5 -2
  10. package/dist/betterAuthClient.js.map +1 -1
  11. package/dist/betterAuthClient.test.d.ts +2 -0
  12. package/dist/betterAuthClient.test.d.ts.map +1 -0
  13. package/dist/betterAuthClient.test.js +151 -0
  14. package/dist/betterAuthClient.test.js.map +1 -0
  15. package/dist/betterAuthSlice.test.js +54 -1
  16. package/dist/betterAuthSlice.test.js.map +1 -1
  17. package/dist/buildNumber.test.d.ts +2 -0
  18. package/dist/buildNumber.test.d.ts.map +1 -0
  19. package/dist/buildNumber.test.js +95 -0
  20. package/dist/buildNumber.test.js.map +1 -0
  21. package/dist/constants.d.ts +27 -3
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +45 -56
  24. package/dist/constants.js.map +1 -1
  25. package/dist/constants.test.d.ts +2 -0
  26. package/dist/constants.test.d.ts.map +1 -0
  27. package/dist/constants.test.js +195 -0
  28. package/dist/constants.test.js.map +1 -0
  29. package/dist/mongooseSlice.test.d.ts +2 -0
  30. package/dist/mongooseSlice.test.d.ts.map +1 -0
  31. package/dist/mongooseSlice.test.js +39 -0
  32. package/dist/mongooseSlice.test.js.map +1 -0
  33. package/dist/tagGenerator.test.d.ts +2 -0
  34. package/dist/tagGenerator.test.d.ts.map +1 -0
  35. package/dist/tagGenerator.test.js +96 -0
  36. package/dist/tagGenerator.test.js.map +1 -0
  37. package/dist/testPreload.test.d.ts +2 -0
  38. package/dist/testPreload.test.d.ts.map +1 -0
  39. package/dist/testPreload.test.js +27 -0
  40. package/dist/testPreload.test.js.map +1 -0
  41. package/dist/useFeatureFlags.d.ts +25 -1
  42. package/dist/useFeatureFlags.d.ts.map +1 -1
  43. package/dist/useFeatureFlags.js +18 -16
  44. package/dist/useFeatureFlags.js.map +1 -1
  45. package/dist/useFeatureFlags.test.d.ts +2 -0
  46. package/dist/useFeatureFlags.test.d.ts.map +1 -0
  47. package/dist/useFeatureFlags.test.js +162 -0
  48. package/dist/useFeatureFlags.test.js.map +1 -0
  49. package/package.json +6 -3
  50. package/src/authSlice.test.ts +298 -0
  51. package/src/authSliceNative.test.ts +187 -0
  52. package/src/betterAuthClient.test.ts +176 -0
  53. package/src/betterAuthClient.ts +6 -3
  54. package/src/betterAuthSlice.test.ts +67 -0
  55. package/src/buildNumber.test.ts +120 -0
  56. package/src/constants.test.ts +228 -0
  57. package/src/constants.ts +72 -70
  58. package/src/mongooseSlice.test.ts +46 -0
  59. package/src/tagGenerator.test.ts +109 -0
  60. package/src/testPreload.test.ts +30 -0
  61. package/src/useFeatureFlags.test.ts +209 -0
  62. package/src/useFeatureFlags.ts +44 -5
  63. package/dist/test-preload.d.ts +0 -2
  64. package/dist/test-preload.d.ts.map +0 -1
  65. package/dist/test-preload.js +0 -24
  66. package/dist/test-preload.js.map +0 -1
  67. package/src/test-preload.ts +0 -28
@@ -0,0 +1,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
+ import { renderHook } from "@testing-library/react-native";
3
+ import { resolveFeatureFlagsOptions, useFeatureFlags } from "./useFeatureFlags";
4
+ describe("resolveFeatureFlagsOptions", () => {
5
+ it("applies defaults when no argument is provided", () => {
6
+ expect(resolveFeatureFlagsOptions()).toEqual({
7
+ basePath: "/feature-flags",
8
+ skip: false,
9
+ });
10
+ });
11
+ it("applies defaults when an empty options object is provided", () => {
12
+ expect(resolveFeatureFlagsOptions({})).toEqual({
13
+ basePath: "/feature-flags",
14
+ skip: false,
15
+ });
16
+ });
17
+ it("treats a string argument as a legacy basePath with skip=false", () => {
18
+ expect(resolveFeatureFlagsOptions("/custom-path")).toEqual({
19
+ basePath: "/custom-path",
20
+ skip: false,
21
+ });
22
+ });
23
+ it("preserves an empty string basePath when passed as a legacy string argument", () => {
24
+ expect(resolveFeatureFlagsOptions("")).toEqual({
25
+ basePath: "",
26
+ skip: false,
27
+ });
28
+ });
29
+ it("uses options.basePath when provided", () => {
30
+ expect(resolveFeatureFlagsOptions({ basePath: "/flags" })).toEqual({
31
+ basePath: "/flags",
32
+ skip: false,
33
+ });
34
+ });
35
+ it("uses options.skip when provided", () => {
36
+ expect(resolveFeatureFlagsOptions({ skip: true })).toEqual({
37
+ basePath: "/feature-flags",
38
+ skip: true,
39
+ });
40
+ });
41
+ it("uses both options.basePath and options.skip when provided together", () => {
42
+ expect(resolveFeatureFlagsOptions({ basePath: "/flags", skip: true })).toEqual({
43
+ basePath: "/flags",
44
+ skip: true,
45
+ });
46
+ });
47
+ it("does not let a legacy string basePath override skip to true", () => {
48
+ expect(resolveFeatureFlagsOptions("/custom-path").skip).toBe(false);
49
+ });
50
+ });
51
+ const buildApi = (queryResult) => {
52
+ const refetch = mock(() => { });
53
+ const capturedQueryBuilder = [];
54
+ const useEvaluateFeatureFlagsQuery = mock(() => ({
55
+ data: queryResult.data,
56
+ error: queryResult.error,
57
+ isLoading: queryResult.isLoading ?? false,
58
+ refetch,
59
+ }));
60
+ const api = {
61
+ injectEndpoints: mock((opts) => {
62
+ const builder = {
63
+ query: (def) => {
64
+ capturedQueryBuilder.push(def.query());
65
+ return { useQuery: useEvaluateFeatureFlagsQuery };
66
+ },
67
+ };
68
+ opts.endpoints(builder);
69
+ return { useEvaluateFeatureFlagsQuery };
70
+ }),
71
+ };
72
+ return { api, capturedQueryBuilder, refetch, useEvaluateFeatureFlagsQuery };
73
+ };
74
+ describe("useFeatureFlags hook", () => {
75
+ const debugCalls = [];
76
+ const originalDebug = console.debug;
77
+ beforeEach(() => {
78
+ debugCalls.length = 0;
79
+ console.debug = (...args) => {
80
+ debugCalls.push(args);
81
+ };
82
+ });
83
+ afterEach(() => {
84
+ console.debug = originalDebug;
85
+ });
86
+ it("builds the evaluate endpoint with the default basePath", () => {
87
+ const { api, capturedQueryBuilder } = buildApi({ data: { foo: true } });
88
+ renderHook(() => useFeatureFlags(api));
89
+ expect(capturedQueryBuilder[0]).toEqual({
90
+ method: "GET",
91
+ url: "/feature-flags/evaluate",
92
+ });
93
+ });
94
+ it("builds the evaluate endpoint with a custom basePath from legacy string argument", () => {
95
+ const { api, capturedQueryBuilder } = buildApi({ data: { foo: true } });
96
+ renderHook(() => useFeatureFlags(api, "/flags"));
97
+ expect(capturedQueryBuilder[0]).toEqual({ method: "GET", url: "/flags/evaluate" });
98
+ });
99
+ it("returns flag accessors that read boolean and string values", () => {
100
+ const { api } = buildApi({
101
+ data: { booleanOff: false, booleanOn: true, variantFlag: "variant-a" },
102
+ });
103
+ const { result } = renderHook(() => useFeatureFlags(api));
104
+ expect(result.current.getFlag("booleanOn")).toBe(true);
105
+ expect(result.current.getFlag("booleanOff")).toBe(false);
106
+ expect(result.current.getFlag("variantFlag")).toBe(false);
107
+ expect(result.current.getVariant("variantFlag")).toBe("variant-a");
108
+ expect(result.current.getVariant("booleanOn")).toBeNull();
109
+ expect(result.current.getVariant("missing")).toBeNull();
110
+ });
111
+ it("returns an empty flags map when no data is available", () => {
112
+ const { api } = buildApi({ isLoading: true });
113
+ const { result } = renderHook(() => useFeatureFlags(api));
114
+ expect(result.current.flags).toEqual({});
115
+ expect(result.current.isLoading).toBe(true);
116
+ });
117
+ it("exposes refetch from the underlying query hook", () => {
118
+ const { api, refetch } = buildApi({ data: { foo: true } });
119
+ const { result } = renderHook(() => useFeatureFlags(api));
120
+ result.current.refetch();
121
+ expect(refetch).toHaveBeenCalled();
122
+ });
123
+ it("logs evaluate request started when loading begins without prior data", () => {
124
+ const { api } = buildApi({ isLoading: true });
125
+ renderHook(() => useFeatureFlags(api));
126
+ const started = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request started");
127
+ expect(started).toBeDefined();
128
+ });
129
+ it("logs evaluate request completed when data resolves after a loading phase", () => {
130
+ const api = buildApi({ isLoading: true });
131
+ const { rerender } = renderHook(({ queryResult }) => {
132
+ api.useEvaluateFeatureFlagsQuery.mockImplementationOnce(() => ({
133
+ data: queryResult.data,
134
+ error: queryResult.error,
135
+ isLoading: queryResult.isLoading ?? false,
136
+ refetch: api.refetch,
137
+ }));
138
+ return useFeatureFlags(api.api);
139
+ }, { initialProps: { queryResult: { isLoading: true } } });
140
+ rerender({ queryResult: { data: { alpha: true }, isLoading: false } });
141
+ const completed = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request completed");
142
+ const started = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request started");
143
+ expect(started).toBeDefined();
144
+ expect(completed).toBeDefined();
145
+ });
146
+ it("logs evaluate request failed when error is returned after loading", () => {
147
+ const api = buildApi({ isLoading: true });
148
+ const { rerender } = renderHook(({ queryResult }) => {
149
+ api.useEvaluateFeatureFlagsQuery.mockImplementationOnce(() => ({
150
+ data: queryResult.data,
151
+ error: queryResult.error,
152
+ isLoading: queryResult.isLoading ?? false,
153
+ refetch: api.refetch,
154
+ }));
155
+ return useFeatureFlags(api.api);
156
+ }, { initialProps: { queryResult: { isLoading: true } } });
157
+ rerender({ queryResult: { error: new Error("boom"), isLoading: false } });
158
+ const failed = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request failed");
159
+ expect(failed).toBeDefined();
160
+ });
161
+ });
162
+ //# sourceMappingURL=useFeatureFlags.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFeatureFlags.test.js","sourceRoot":"","sources":["../src/useFeatureFlags.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "@better-auth/expo": "^1.2.8",
4
4
  "@react-native-async-storage/async-storage": "2.2.0",
5
5
  "@reduxjs/toolkit": "^2.11.1",
6
- "@terreno/ui": "0.9.3",
6
+ "@terreno/ui": "0.11.0",
7
7
  "async-mutex": "^0.5.0",
8
8
  "axios": "^1.13.2",
9
9
  "axios-retry": "^4.5.0",
@@ -22,12 +22,14 @@
22
22
  "description": "Redux Toolkit Query utilities for @terreno/api backends",
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "^2.3.6",
25
+ "@testing-library/react-native": "^13.2.0",
25
26
  "@types/bun": "^1.2.4",
26
27
  "@types/luxon": "^3.7.1",
27
28
  "@types/node": "^25.0.3",
28
29
  "@types/qs": "^6.14.0",
29
30
  "@types/react": "~19.1.10",
30
31
  "react": "19.1.0",
32
+ "react-test-renderer": "19.1.0",
31
33
  "typescript": "~5.9.2"
32
34
  },
33
35
  "exports": {
@@ -61,8 +63,9 @@
61
63
  "lint:fix": "biome check --write .",
62
64
  "lint:unsafefix": "biome check --write --unsafe .",
63
65
  "test": "bun test",
64
- "test:ci": "bun test"
66
+ "test:ci": "bun test",
67
+ "test:coverage": "bun run ../scripts/check-coverage.ts"
65
68
  },
66
69
  "types": "dist/index.d.ts",
67
- "version": "0.9.3"
70
+ "version": "0.11.0"
68
71
  }
@@ -1,12 +1,18 @@
1
1
  import {beforeEach, describe, expect, it} from "bun:test";
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
2
3
  import {configureStore} from "@reduxjs/toolkit";
3
4
  import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
4
5
 
5
6
  import {
6
7
  type EmailLoginRequest,
7
8
  generateAuthSlice,
9
+ generateProfileEndpoints,
10
+ getAuthToken,
8
11
  selectCurrentUserId,
9
12
  selectIsAuthenticating,
13
+ selectLastTokenRefreshTimestamp,
14
+ useSelectCurrentUserId,
15
+ useSelectIsAuthenticating,
10
16
  } from "./authSlice";
11
17
 
12
18
  // Create a real RTK Query API with the endpoints that generateAuthSlice expects
@@ -49,6 +55,11 @@ const createTestStore = () => {
49
55
  };
50
56
  };
51
57
 
58
+ const flushAsyncListeners = async (): Promise<void> => {
59
+ await Promise.resolve();
60
+ await new Promise((resolve) => setTimeout(resolve, 0));
61
+ };
62
+
52
63
  describe("generateAuthSlice", () => {
53
64
  let store: ReturnType<typeof createTestStore>["store"];
54
65
  let authSlice: ReturnType<typeof createTestStore>["authSlice"];
@@ -289,6 +300,12 @@ describe("selectors", () => {
289
300
  // biome-ignore lint/suspicious/noExplicitAny: Test mock state
290
301
  expect(selectIsAuthenticating({} as any)).toBe(false);
291
302
  });
303
+
304
+ it("selectLastTokenRefreshTimestamp returns timestamp", () => {
305
+ // biome-ignore lint/suspicious/noExplicitAny: Test mock state
306
+ const state = {auth: {lastTokenRefreshTimestamp: 12345}} as any;
307
+ expect(selectLastTokenRefreshTimestamp(state)).toBe(12345);
308
+ });
292
309
  });
293
310
 
294
311
  describe("EmailLoginRequest type", () => {
@@ -304,3 +321,284 @@ describe("EmailLoginRequest type", () => {
304
321
  expect(request.password).toBe("pass");
305
322
  });
306
323
  });
324
+
325
+ describe("generateProfileEndpoints", () => {
326
+ it("builds endpoint query payloads", () => {
327
+ const builder = {
328
+ // biome-ignore lint/suspicious/noExplicitAny: Testing generated endpoint configs
329
+ mutation: (config: any) => config,
330
+ };
331
+ // biome-ignore lint/suspicious/noExplicitAny: Lightweight fake builder for unit test
332
+ const endpoints = generateProfileEndpoints(builder as any, "todos");
333
+ const createEmailUserQuery = endpoints.createEmailUser.query;
334
+ const emailLoginQuery = endpoints.emailLogin.query;
335
+ const emailSignUpQuery = endpoints.emailSignUp.query;
336
+ const googleLoginQuery = endpoints.googleLogin.query;
337
+ const resetPasswordQuery = endpoints.resetPassword.query;
338
+
339
+ expect(createEmailUserQuery).toBeDefined();
340
+ expect(emailLoginQuery).toBeDefined();
341
+ expect(emailSignUpQuery).toBeDefined();
342
+ expect(googleLoginQuery).toBeDefined();
343
+ expect(resetPasswordQuery).toBeDefined();
344
+
345
+ if (
346
+ !createEmailUserQuery ||
347
+ !emailLoginQuery ||
348
+ !emailSignUpQuery ||
349
+ !googleLoginQuery ||
350
+ !resetPasswordQuery
351
+ ) {
352
+ throw new Error("Expected all generated profile endpoint queries to be defined");
353
+ }
354
+
355
+ expect(endpoints.createEmailUser.invalidatesTags).toEqual(["todos", "conversations"]);
356
+ expect(
357
+ createEmailUserQuery({
358
+ email: "new@example.com",
359
+ password: "secret",
360
+ role: "admin",
361
+ })
362
+ ).toEqual({
363
+ body: {email: "new@example.com", password: "secret", role: "admin"},
364
+ method: "POST",
365
+ url: "auth/signup",
366
+ });
367
+
368
+ expect(endpoints.emailLogin.extraOptions).toEqual({maxRetries: 0});
369
+ expect(emailLoginQuery({email: "a@example.com", password: "pw"})).toEqual({
370
+ body: {email: "a@example.com", password: "pw", username: undefined},
371
+ method: "POST",
372
+ url: "auth/login",
373
+ });
374
+
375
+ expect(
376
+ emailSignUpQuery({
377
+ email: "signup@example.com",
378
+ name: "New User",
379
+ password: "pw",
380
+ })
381
+ ).toEqual({
382
+ body: {email: "signup@example.com", name: "New User", password: "pw"},
383
+ method: "POST",
384
+ url: "auth/signup",
385
+ });
386
+
387
+ expect(googleLoginQuery({idToken: "id-token"})).toEqual({
388
+ body: {idToken: "id-token"},
389
+ method: "POST",
390
+ url: "/auth/google",
391
+ });
392
+
393
+ expect(
394
+ resetPasswordQuery({
395
+ _id: "u-1",
396
+ email: "user@example.com",
397
+ newPassword: "new-secret",
398
+ oldPassword: "old-secret",
399
+ password: "current-secret",
400
+ })
401
+ ).toEqual({
402
+ body: {
403
+ _id: "u-1",
404
+ email: "user@example.com",
405
+ newPassword: "new-secret",
406
+ oldPassword: "old-secret",
407
+ password: "current-secret",
408
+ },
409
+ method: "POST",
410
+ url: "/resetPassword",
411
+ });
412
+ });
413
+ });
414
+
415
+ describe("listener middleware side effects", () => {
416
+ it("stores tokens in AsyncStorage on web login when window exists", async () => {
417
+ const {store} = createTestStore();
418
+ const setItemCalls: Array<[string, string]> = [];
419
+ const originalSetItem = AsyncStorage.setItem;
420
+ const globalWithWindow = globalThis as {window?: unknown};
421
+ const originalWindow = globalWithWindow.window;
422
+
423
+ AsyncStorage.setItem = async (key: string, value: string): Promise<void> => {
424
+ setItemCalls.push([key, value]);
425
+ };
426
+ globalWithWindow.window = {};
427
+
428
+ try {
429
+ store.dispatch({
430
+ meta: {arg: {endpointName: "emailLogin", type: "mutation"}, requestId: "listener-login-1"},
431
+ payload: {refreshToken: "refresh-token", token: "auth-token", userId: "user-123"},
432
+ type: "terreno-rtk/executeMutation/fulfilled",
433
+ });
434
+
435
+ await flushAsyncListeners();
436
+
437
+ expect(setItemCalls).toEqual([
438
+ ["AUTH_TOKEN", "auth-token"],
439
+ ["REFRESH_TOKEN", "refresh-token"],
440
+ ]);
441
+ expect(store.getState().auth.userId).toBe("user-123");
442
+ } finally {
443
+ AsyncStorage.setItem = originalSetItem;
444
+ if (typeof originalWindow === "undefined") {
445
+ delete globalWithWindow.window;
446
+ } else {
447
+ globalWithWindow.window = originalWindow;
448
+ }
449
+ }
450
+ });
451
+
452
+ it("re-throws and logs when AsyncStorage.setItem fails on web login", async () => {
453
+ const {store} = createTestStore();
454
+ const originalSetItem = AsyncStorage.setItem;
455
+ const originalConsoleError = console.error;
456
+ const globalWithWindow = globalThis as {window?: unknown};
457
+ const originalWindow = globalWithWindow.window;
458
+ const errorCalls: unknown[][] = [];
459
+
460
+ console.error = (...args: unknown[]): void => {
461
+ errorCalls.push(args);
462
+ };
463
+ AsyncStorage.setItem = async (): Promise<void> => {
464
+ throw new Error("storage quota exceeded");
465
+ };
466
+ globalWithWindow.window = {};
467
+
468
+ try {
469
+ store.dispatch({
470
+ meta: {
471
+ arg: {endpointName: "emailLogin", type: "mutation"},
472
+ requestId: "listener-login-error",
473
+ },
474
+ payload: {refreshToken: "refresh-token", token: "auth-token", userId: "user-err"},
475
+ type: "terreno-rtk/executeMutation/fulfilled",
476
+ });
477
+
478
+ await flushAsyncListeners();
479
+
480
+ const loggedErrorMessage = errorCalls.find((args) =>
481
+ args.some(
482
+ (value) => typeof value === "string" && value.includes("Error setting auth token")
483
+ )
484
+ );
485
+ expect(loggedErrorMessage).toBeDefined();
486
+ } finally {
487
+ AsyncStorage.setItem = originalSetItem;
488
+ console.error = originalConsoleError;
489
+ if (typeof originalWindow === "undefined") {
490
+ delete globalWithWindow.window;
491
+ } else {
492
+ globalWithWindow.window = originalWindow;
493
+ }
494
+ }
495
+ });
496
+
497
+ it("skips storing auth tokens when window is undefined (SSR context)", async () => {
498
+ const {store} = createTestStore();
499
+ const setItemCalls: Array<[string, string]> = [];
500
+ const originalSetItem = AsyncStorage.setItem;
501
+ const globalWithWindow = globalThis as {window?: unknown};
502
+ const originalWindow = globalWithWindow.window;
503
+
504
+ AsyncStorage.setItem = async (key: string, value: string): Promise<void> => {
505
+ setItemCalls.push([key, value]);
506
+ };
507
+ delete globalWithWindow.window;
508
+
509
+ try {
510
+ store.dispatch({
511
+ meta: {
512
+ arg: {endpointName: "emailSignUp", type: "mutation"},
513
+ requestId: "listener-ssr-1",
514
+ },
515
+ payload: {refreshToken: "r", token: "t", userId: "user-ssr"},
516
+ type: "terreno-rtk/executeMutation/fulfilled",
517
+ });
518
+
519
+ await flushAsyncListeners();
520
+
521
+ expect(setItemCalls).toEqual([]);
522
+ expect(store.getState().auth.userId).toBe("user-ssr");
523
+ } finally {
524
+ AsyncStorage.setItem = originalSetItem;
525
+ if (typeof originalWindow !== "undefined") {
526
+ globalWithWindow.window = originalWindow;
527
+ }
528
+ }
529
+ });
530
+
531
+ it("removes tokens from AsyncStorage on web logout when window exists", async () => {
532
+ const {store, authSlice} = createTestStore();
533
+ const removeItemCalls: string[] = [];
534
+ const originalRemoveItem = AsyncStorage.removeItem;
535
+ const globalWithWindow = globalThis as {window?: unknown};
536
+ const originalWindow = globalWithWindow.window;
537
+
538
+ AsyncStorage.removeItem = async (key: string): Promise<void> => {
539
+ removeItemCalls.push(key);
540
+ };
541
+ globalWithWindow.window = {};
542
+
543
+ try {
544
+ store.dispatch(authSlice.actions.logout());
545
+ await flushAsyncListeners();
546
+
547
+ expect(removeItemCalls).toEqual(["AUTH_TOKEN", "REFRESH_TOKEN"]);
548
+ } finally {
549
+ AsyncStorage.removeItem = originalRemoveItem;
550
+ if (typeof originalWindow === "undefined") {
551
+ delete globalWithWindow.window;
552
+ } else {
553
+ globalWithWindow.window = originalWindow;
554
+ }
555
+ }
556
+ });
557
+ });
558
+
559
+ describe("hook wrappers", () => {
560
+ it("throws when hook selectors are called outside React render", () => {
561
+ expect(() => useSelectCurrentUserId()).toThrow();
562
+ expect(() => useSelectIsAuthenticating()).toThrow();
563
+ });
564
+ });
565
+
566
+ describe("getAuthToken", () => {
567
+ it("reads AUTH_TOKEN from AsyncStorage when window exists", async () => {
568
+ const originalGetItem = AsyncStorage.getItem;
569
+ const globalWithWindow = globalThis as {window?: unknown};
570
+ const originalWindow = globalWithWindow.window;
571
+
572
+ AsyncStorage.getItem = async (key: string): Promise<string | null> => {
573
+ return key === "AUTH_TOKEN" ? "stored-auth-token" : null;
574
+ };
575
+ globalWithWindow.window = {};
576
+
577
+ try {
578
+ const token = await getAuthToken();
579
+ expect(token).toBe("stored-auth-token");
580
+ } finally {
581
+ AsyncStorage.getItem = originalGetItem;
582
+ if (typeof originalWindow === "undefined") {
583
+ delete globalWithWindow.window;
584
+ } else {
585
+ globalWithWindow.window = originalWindow;
586
+ }
587
+ }
588
+ });
589
+
590
+ it("returns null when window is unavailable in SSR context", async () => {
591
+ const globalWithWindow = globalThis as {window?: unknown};
592
+ const originalWindow = globalWithWindow.window;
593
+
594
+ delete globalWithWindow.window;
595
+ try {
596
+ const token = await getAuthToken();
597
+ expect(token).toBeNull();
598
+ } finally {
599
+ if (typeof originalWindow !== "undefined") {
600
+ globalWithWindow.window = originalWindow;
601
+ }
602
+ }
603
+ });
604
+ });