@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.
- package/.ai/guidelines/core.md +2 -0
- package/dist/authSlice.d.ts.map +1 -1
- package/dist/authSlice.js +3 -1
- package/dist/authSlice.js.map +1 -1
- package/dist/authSlice.test.js +15 -8
- package/dist/authSlice.test.js.map +1 -1
- package/dist/authSliceNative.test.js +3 -16
- package/dist/authSliceNative.test.js.map +1 -1
- package/dist/betterAuthClient.d.ts.map +1 -1
- package/dist/betterAuthClient.js +0 -1
- package/dist/betterAuthClient.js.map +1 -1
- package/dist/betterAuthSlice.test.js +1 -0
- package/dist/betterAuthSlice.test.js.map +1 -1
- package/dist/constants.d.ts +7 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/emptyApi.d.ts.map +1 -1
- package/dist/emptyApi.js +0 -2
- package/dist/emptyApi.js.map +1 -1
- package/dist/isolated/constants.isolated.d.ts +2 -0
- package/dist/isolated/constants.isolated.d.ts.map +1 -0
- package/dist/isolated/constants.isolated.js +81 -0
- package/dist/isolated/constants.isolated.js.map +1 -0
- package/dist/isolated/constants.test.js +53 -75
- package/dist/isolated/constants.test.js.map +1 -1
- package/dist/isolated/offlineMiddleware.isolated.js +0 -1
- package/dist/isolated/offlineMiddleware.isolated.js.map +1 -1
- package/dist/isolated/offlineMiddleware.native.isolated.js +0 -1
- package/dist/isolated/offlineMiddleware.native.isolated.js.map +1 -1
- package/dist/isolated/useUpgradeCheck.mobile.isolated.js +47 -0
- package/dist/isolated/useUpgradeCheck.mobile.isolated.js.map +1 -1
- package/dist/mongooseSlice.test.js +0 -1
- package/dist/mongooseSlice.test.js.map +1 -1
- package/dist/offlineGate.d.ts +1 -1
- package/dist/offlineGate.d.ts.map +1 -1
- package/dist/offlineGate.js +1 -3
- package/dist/offlineGate.js.map +1 -1
- package/dist/offlineMiddleware.d.ts.map +1 -1
- package/dist/offlineMiddleware.js +26 -0
- package/dist/offlineMiddleware.js.map +1 -1
- package/dist/offlineMiddleware.test.js +5 -1
- package/dist/offlineMiddleware.test.js.map +1 -1
- package/dist/realtime.test.js +1 -0
- package/dist/realtime.test.js.map +1 -1
- package/dist/sessionApi.test.d.ts +2 -0
- package/dist/sessionApi.test.d.ts.map +1 -0
- package/dist/sessionApi.test.js +84 -0
- package/dist/sessionApi.test.js.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js.map +1 -1
- package/dist/sync.test.d.ts +2 -0
- package/dist/sync.test.d.ts.map +1 -0
- package/dist/sync.test.js +296 -0
- package/dist/sync.test.js.map +1 -0
- package/dist/testPreload.test.js +6 -0
- package/dist/testPreload.test.js.map +1 -1
- package/dist/useFeatureFlags.d.ts.map +1 -1
- package/dist/useFeatureFlags.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.d.ts.map +1 -1
- package/dist/useTerrenoFeatureFlags.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.test.js +10 -0
- package/dist/useTerrenoFeatureFlags.test.js.map +1 -1
- package/dist/useUpgradeCheckNative.test.js +21 -0
- package/dist/useUpgradeCheckNative.test.js.map +1 -1
- package/package.json +2 -2
- package/src/authSlice.test.ts +25 -13
- package/src/authSlice.ts +3 -1
- package/src/authSliceNative.test.ts +4 -18
- package/src/betterAuthClient.ts +1 -2
- package/src/betterAuthSlice.test.ts +1 -0
- package/src/constants.ts +7 -2
- package/src/emptyApi.ts +5 -4
- package/src/isolated/constants.isolated.ts +118 -0
- package/src/isolated/constants.test.ts +81 -78
- package/src/isolated/offlineMiddleware.isolated.ts +1 -2
- package/src/isolated/offlineMiddleware.native.isolated.ts +1 -2
- package/src/isolated/useUpgradeCheck.mobile.isolated.ts +60 -0
- package/src/mongooseSlice.test.ts +1 -2
- package/src/offlineGate.ts +1 -2
- package/src/offlineMiddleware.test.ts +6 -1
- package/src/offlineMiddleware.ts +27 -0
- package/src/realtime.test.ts +1 -0
- package/src/sessionApi.test.ts +115 -0
- package/src/sync.test.ts +390 -0
- package/src/sync.ts +2 -1
- package/src/testPreload.test.ts +7 -0
- package/src/useFeatureFlags.ts +1 -0
- package/src/useTerrenoFeatureFlags.test.ts +16 -0
- package/src/useTerrenoFeatureFlags.ts +2 -0
- package/src/useUpgradeCheckNative.test.ts +26 -0
package/src/authSlice.test.ts
CHANGED
|
@@ -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(
|
|
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"}}
|
|
285
|
-
|
|
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
|
-
|
|
297
|
+
assert.isUndefined(selectCurrentUserId({}));
|
|
290
298
|
});
|
|
291
299
|
|
|
292
300
|
it("selectIsAuthenticating returns isAuthenticating", () => {
|
|
293
|
-
const state = {auth: {isAuthenticating: true}}
|
|
294
|
-
|
|
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
|
-
|
|
306
|
+
assert.isFalse(selectIsAuthenticating({}));
|
|
299
307
|
});
|
|
300
308
|
|
|
301
309
|
it("selectLastTokenRefreshTimestamp returns timestamp", () => {
|
|
302
|
-
const state = {auth: {lastTokenRefreshTimestamp: 12345}}
|
|
303
|
-
|
|
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:
|
|
325
|
-
};
|
|
326
|
-
const endpoints = generateProfileEndpoints(builder
|
|
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
|
-
|
|
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
|
|
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({
|
package/src/betterAuthClient.ts
CHANGED
|
@@ -96,8 +96,7 @@ export const createBetterAuthClient = (config: BetterAuthClientConfig) => {
|
|
|
96
96
|
plugins: [
|
|
97
97
|
expoClient({
|
|
98
98
|
scheme: config.scheme,
|
|
99
|
-
|
|
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
|
],
|
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import Constants from "expo-constants";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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,
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
41
|
+
data: [undefined as unknown as {_id: string}, {_id: "found"}],
|
|
43
42
|
};
|
|
44
43
|
expect(populateId("found", sparse)).toEqual({_id: "found"});
|
|
45
44
|
});
|
package/src/offlineGate.ts
CHANGED
|
@@ -13,8 +13,7 @@ export const configureOfflineMutationEndpoints = (endpoints: string[]): void =>
|
|
|
13
13
|
*/
|
|
14
14
|
export const shouldDeferOfflineMutation = (
|
|
15
15
|
endpointName: string,
|
|
16
|
-
|
|
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
|
};
|