@applicaster/zapp-react-native-utils 16.0.0-alpha.3112220329 → 16.0.0-alpha.3202041045
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/actionUtils/index.ts +3 -1
- package/actionsExecutor/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +3 -0
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
- package/actionsExecutor/actions/index.ts +2 -0
- package/actionsExecutor/actions/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +20 -6
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +1 -0
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
- package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
- package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
- package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/index.ts +7 -0
- package/appUtils/playerManager/player.ts +25 -0
- package/appUtils/playerManager/playerNative.ts +22 -0
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +196 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
- package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
- package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
- package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
- package/cellUtils/index.ts +40 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +178 -46
- package/configurationUtils/modalBlocksParser.ts +57 -101
- package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
- package/entryActions/aliasPresentation.ts +26 -42
- package/entryActions/buildEntryActions.ts +1 -0
- package/entryActions/index.ts +0 -2
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
- package/manifestUtils/keys.js +7 -2
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +2 -1
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/RemoteEditableCollection.ts +9 -1
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +3 -0
- package/modalState/useBottomSheetContent.ts +17 -12
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +3 -2
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/quickBrickAssets/__tests__/index.test.ts +18 -0
- package/quickBrickAssets/index.ts +35 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +29 -23
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
- package/reactHooks/navigation/useRoute.ts +8 -4
- package/reactHooks/usePluginConfiguration.ts +84 -46
- package/riverComponetsMeasurementProvider/index.tsx +3 -1
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
- package/uiActionsRegistrator/registry.ts +16 -0
|
@@ -83,6 +83,12 @@ export const useCellClick = ({
|
|
|
83
83
|
|
|
84
84
|
logOnPress(selectedItem, pathname, component, onCellTap);
|
|
85
85
|
|
|
86
|
+
const tapActions = selectedItem?.extensions?.tap_actions?.actions;
|
|
87
|
+
|
|
88
|
+
const finishesHook =
|
|
89
|
+
Array.isArray(tapActions) &&
|
|
90
|
+
tapActions.some((action) => action?.type === "finishHook");
|
|
91
|
+
|
|
86
92
|
if (selectedItem) {
|
|
87
93
|
await actionExecutor?.handleEntryActions(selectedItem, {
|
|
88
94
|
component,
|
|
@@ -101,20 +107,21 @@ export const useCellClick = ({
|
|
|
101
107
|
url: selectedItem.link.href,
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
|
-
} else {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
} else if (isFunction(onCellTap)) {
|
|
111
|
+
onCellTap?.(selectedItem, _index);
|
|
112
|
+
} else if (!finishesHook) {
|
|
113
|
+
// finishHook already completes the hook and navigates to the original
|
|
114
|
+
// target. Pushing the cell as well stacks a second copy of that screen
|
|
115
|
+
// (e.g. home/home after a profile selector hook).
|
|
116
|
+
if (currentRoute === pathname || pathname.includes("video-modal")) {
|
|
117
|
+
const targetScreen = component?.data?.target;
|
|
118
|
+
|
|
119
|
+
push(
|
|
120
|
+
R.when(
|
|
121
|
+
() => targetScreen,
|
|
122
|
+
R.mergeLeft({ screen_type: targetScreen })
|
|
123
|
+
)(selectedItem)
|
|
124
|
+
);
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
},
|
|
@@ -4,6 +4,11 @@ import { NetInfoState } from "@react-native-community/netinfo";
|
|
|
4
4
|
import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
|
|
5
5
|
import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
|
|
6
6
|
|
|
7
|
+
type ConnectionInfo = NetInfoState & {
|
|
8
|
+
deviceStatus: NetStatus;
|
|
9
|
+
stillOfflineAfterResumeCount?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* This hook returns connection type from RN NetInfo component or boolean for iConnected if justOnlineStatus flag is set
|
|
9
14
|
* @param {boolean} justOnlineStatus - flag if to return boolean for isConnected
|
|
@@ -12,16 +17,14 @@ import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvide
|
|
|
12
17
|
|
|
13
18
|
export function useConnectionInfo(justOnlineStatus: true): boolean;
|
|
14
19
|
|
|
15
|
-
export function useConnectionInfo(
|
|
16
|
-
justOnlineStatus: false
|
|
17
|
-
): NetInfoState & { deviceStatus: NetStatus };
|
|
20
|
+
export function useConnectionInfo(justOnlineStatus: false): ConnectionInfo;
|
|
18
21
|
|
|
19
22
|
export function useConnectionInfo(
|
|
20
23
|
justOnlineStatus: boolean
|
|
21
|
-
): boolean |
|
|
24
|
+
): boolean | ConnectionInfo {
|
|
22
25
|
const connectionInfo = React.useContext(
|
|
23
26
|
NetworkStatusContext
|
|
24
|
-
) as
|
|
27
|
+
) as ConnectionInfo;
|
|
25
28
|
|
|
26
29
|
return justOnlineStatus ? isOnline(connectionInfo) === true : connectionInfo;
|
|
27
30
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import * as zappPipesModule from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
3
|
+
import * as useRouteHook from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
|
|
4
|
+
import * as useNavigationHooks from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
|
|
5
|
+
import * as useIsScreenActiveHook from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";
|
|
6
|
+
import { refreshCoordinator } from "../../../refreshUtils/RefreshCoordinator";
|
|
7
|
+
import { useMarkPipesDataStale } from "../useMarkPipesDataStale";
|
|
8
|
+
|
|
9
|
+
const mockDispatch = jest.fn();
|
|
10
|
+
|
|
11
|
+
jest.mock("@applicaster/zapp-react-native-redux", () => ({
|
|
12
|
+
...jest.requireActual("@applicaster/zapp-react-native-redux"),
|
|
13
|
+
useAppDispatch: () => mockDispatch,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock(
|
|
17
|
+
"@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl",
|
|
18
|
+
() => ({
|
|
19
|
+
getDatasourceUrl: jest.fn(
|
|
20
|
+
(_ctx, component) => component?.data?.source || null
|
|
21
|
+
),
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
jest.mock(
|
|
26
|
+
"@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts",
|
|
27
|
+
() => ({
|
|
28
|
+
usePipesContexts: jest.fn(() => ({})),
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
describe("useMarkPipesDataStale", () => {
|
|
33
|
+
let mockNavigator: any;
|
|
34
|
+
let mockRoute: any;
|
|
35
|
+
let mockIsActive: boolean;
|
|
36
|
+
|
|
37
|
+
const riverId = "river-123";
|
|
38
|
+
|
|
39
|
+
const refreshableComponent: any = {
|
|
40
|
+
id: "comp-1",
|
|
41
|
+
rules: { clear_cache_on_reload: true },
|
|
42
|
+
data: { source: "https://feed.example.com/data1" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const stringBooleanComponent: any = {
|
|
46
|
+
id: "comp-2",
|
|
47
|
+
rules: { clear_cache_on_reload: "true" },
|
|
48
|
+
data: { source: "https://feed.example.com/data2" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const nonRefreshableComponent: any = {
|
|
52
|
+
id: "comp-3",
|
|
53
|
+
rules: { clear_cache_on_reload: false },
|
|
54
|
+
data: { source: "https://feed.example.com/data3" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const noSourceComponent: any = {
|
|
58
|
+
id: "comp-4",
|
|
59
|
+
rules: { clear_cache_on_reload: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const allComponents = [
|
|
63
|
+
refreshableComponent,
|
|
64
|
+
stringBooleanComponent,
|
|
65
|
+
nonRefreshableComponent,
|
|
66
|
+
noSourceComponent,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
|
|
72
|
+
mockIsActive = true;
|
|
73
|
+
|
|
74
|
+
mockNavigator = {
|
|
75
|
+
currentRoute: "river/river-123",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
mockRoute = {
|
|
79
|
+
pathname: "river/river-123",
|
|
80
|
+
screenData: { id: riverId },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
jest
|
|
84
|
+
.spyOn(useNavigationHooks, "useNavigation")
|
|
85
|
+
.mockImplementation(() => mockNavigator);
|
|
86
|
+
|
|
87
|
+
jest.spyOn(useRouteHook, "useRoute").mockImplementation(() => mockRoute);
|
|
88
|
+
|
|
89
|
+
jest
|
|
90
|
+
.spyOn(useIsScreenActiveHook, "useIsScreenActive")
|
|
91
|
+
.mockImplementation(() => mockIsActive);
|
|
92
|
+
|
|
93
|
+
jest
|
|
94
|
+
.spyOn(refreshCoordinator, "triggerRefresh")
|
|
95
|
+
.mockImplementation(jest.fn());
|
|
96
|
+
|
|
97
|
+
jest
|
|
98
|
+
.spyOn(zappPipesModule, "markPipesDataStale")
|
|
99
|
+
.mockImplementation(
|
|
100
|
+
(url, opts) => ({ type: "MARK_STALE", url, opts }) as any
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("on unmount", () => {
|
|
105
|
+
it("marks clear_cache_on_reload feeds stale on unmount", () => {
|
|
106
|
+
const { unmount } = renderHook(() =>
|
|
107
|
+
useMarkPipesDataStale(riverId, allComponents)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
111
|
+
|
|
112
|
+
unmount();
|
|
113
|
+
|
|
114
|
+
expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
|
|
115
|
+
"https://feed.example.com/data1",
|
|
116
|
+
{ riverId }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
|
|
120
|
+
"https://feed.example.com/data2",
|
|
121
|
+
{ riverId }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(zappPipesModule.markPipesDataStale).not.toHaveBeenCalledWith(
|
|
125
|
+
"https://feed.example.com/data3",
|
|
126
|
+
expect.anything()
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does not mark stale on unmount if pathname includes videoModal", () => {
|
|
133
|
+
mockRoute.pathname = "videoModal/item-123";
|
|
134
|
+
|
|
135
|
+
const { unmount } = renderHook(() =>
|
|
136
|
+
useMarkPipesDataStale(riverId, allComponents)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
unmount();
|
|
140
|
+
|
|
141
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("on screen return (screen not unmounted in Transitioner)", () => {
|
|
146
|
+
it("does not trigger refresh on initial mount", () => {
|
|
147
|
+
renderHook(() => useMarkPipesDataStale(riverId, allComponents));
|
|
148
|
+
|
|
149
|
+
expect(refreshCoordinator.triggerRefresh).not.toHaveBeenCalled();
|
|
150
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("triggers refresh and marks stale when returning from a deeper screen", () => {
|
|
154
|
+
const { rerender } = renderHook(() =>
|
|
155
|
+
useMarkPipesDataStale(riverId, allComponents)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// 1. User navigates away to a deeper screen
|
|
159
|
+
mockNavigator.currentRoute = "river/deeper-screen";
|
|
160
|
+
mockIsActive = false;
|
|
161
|
+
rerender({});
|
|
162
|
+
|
|
163
|
+
expect(refreshCoordinator.triggerRefresh).not.toHaveBeenCalled();
|
|
164
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
165
|
+
|
|
166
|
+
// 2. User presses back and returns to original screen
|
|
167
|
+
mockNavigator.currentRoute = "river/river-123";
|
|
168
|
+
mockIsActive = true;
|
|
169
|
+
rerender({});
|
|
170
|
+
|
|
171
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
|
|
172
|
+
|
|
173
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
|
|
174
|
+
[refreshableComponent, stringBooleanComponent],
|
|
175
|
+
riverId
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
|
|
179
|
+
"https://feed.example.com/data1",
|
|
180
|
+
{ riverId }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(zappPipesModule.markPipesDataStale).toHaveBeenCalledWith(
|
|
184
|
+
"https://feed.example.com/data2",
|
|
185
|
+
{ riverId }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("does not trigger duplicate refreshes on subsequent re-renders on the same screen", () => {
|
|
192
|
+
const { rerender } = renderHook(() =>
|
|
193
|
+
useMarkPipesDataStale(riverId, allComponents)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Navigate away
|
|
197
|
+
mockNavigator.currentRoute = "river/deeper-screen";
|
|
198
|
+
mockIsActive = false;
|
|
199
|
+
rerender({});
|
|
200
|
+
|
|
201
|
+
// Return
|
|
202
|
+
mockNavigator.currentRoute = "river/river-123";
|
|
203
|
+
mockIsActive = true;
|
|
204
|
+
rerender({});
|
|
205
|
+
|
|
206
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
|
|
207
|
+
|
|
208
|
+
// Subsequent re-render without navigation
|
|
209
|
+
rerender({});
|
|
210
|
+
rerender({});
|
|
211
|
+
|
|
212
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("triggers refresh again if user navigates away a second time and returns", () => {
|
|
216
|
+
const { rerender } = renderHook(() =>
|
|
217
|
+
useMarkPipesDataStale(riverId, allComponents)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// First trip: navigate away and return
|
|
221
|
+
mockNavigator.currentRoute = "river/screen-2";
|
|
222
|
+
mockIsActive = false;
|
|
223
|
+
rerender({});
|
|
224
|
+
|
|
225
|
+
mockNavigator.currentRoute = "river/river-123";
|
|
226
|
+
mockIsActive = true;
|
|
227
|
+
rerender({});
|
|
228
|
+
|
|
229
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(1);
|
|
230
|
+
|
|
231
|
+
// Second trip: navigate away to another screen and return
|
|
232
|
+
mockNavigator.currentRoute = "river/screen-3";
|
|
233
|
+
mockIsActive = false;
|
|
234
|
+
rerender({});
|
|
235
|
+
|
|
236
|
+
mockNavigator.currentRoute = "river/river-123";
|
|
237
|
+
mockIsActive = true;
|
|
238
|
+
rerender({});
|
|
239
|
+
|
|
240
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(2);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -3,46 +3,91 @@ import React from "react";
|
|
|
3
3
|
import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
|
|
4
4
|
import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
|
|
5
5
|
import { markPipesDataStale } from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
6
|
-
|
|
7
|
-
import { useRoute } from "../navigation";
|
|
8
6
|
import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
|
|
7
|
+
import { isTrue } from "../../booleanUtils";
|
|
8
|
+
import { refreshCoordinator } from "../../refreshUtils/RefreshCoordinator";
|
|
9
|
+
import { useNavigation, useRoute } from "../navigation";
|
|
10
|
+
import { usePrevious } from "../utils";
|
|
11
|
+
|
|
12
|
+
const getRefreshableComponents = (components: ZappUIComponent[]) =>
|
|
13
|
+
components?.filter(
|
|
14
|
+
(c) => isTrue(c?.rules?.clear_cache_on_reload) && c?.data?.source
|
|
15
|
+
) || [];
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Mark a river's `clear_cache_on_reload` feeds as stale when the screen is
|
|
12
19
|
* unmounted. The cached data is kept (so a screen sharing the same feed does
|
|
13
20
|
* not lose it mid-mount) and revalidated on next access.
|
|
21
|
+
*
|
|
22
|
+
* Also triggers refresh for mounted components and marks feeds stale when
|
|
23
|
+
* returning back to this screen from a deeper route in the Transitioner
|
|
24
|
+
* (where the screen was kept mounted in the background).
|
|
25
|
+
*
|
|
14
26
|
* @param {string} riverId screen id
|
|
15
27
|
* @param {Array} riverComponents list of UI components
|
|
16
28
|
*/
|
|
17
|
-
export const useMarkPipesDataStale = (
|
|
29
|
+
export const useMarkPipesDataStale = (
|
|
30
|
+
riverId: string,
|
|
31
|
+
riverComponents: ZappUIComponent[]
|
|
32
|
+
) => {
|
|
18
33
|
const dispatch = useAppDispatch();
|
|
19
34
|
const { screenData, pathname } = useRoute();
|
|
20
35
|
const pipesContexts = usePipesContexts(riverId, pathname);
|
|
36
|
+
const { currentRoute } = useNavigation();
|
|
37
|
+
const prevRoute = usePrevious(currentRoute);
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
component
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (url) {
|
|
40
|
-
dispatch(markPipesDataStale(url, { riverId }));
|
|
41
|
-
}
|
|
39
|
+
const isBackToScreen = Boolean(
|
|
40
|
+
prevRoute && prevRoute !== currentRoute && currentRoute === pathname
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const invalidateComponents = React.useCallback(
|
|
44
|
+
(components: ZappUIComponent[]) => {
|
|
45
|
+
components.forEach((component) => {
|
|
46
|
+
const url = getDatasourceUrl(
|
|
47
|
+
{ screenData, ...pipesContexts },
|
|
48
|
+
component
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (url) {
|
|
52
|
+
dispatch(markPipesDataStale(url, { riverId }));
|
|
42
53
|
}
|
|
43
54
|
});
|
|
55
|
+
},
|
|
56
|
+
[dispatch, screenData, pipesContexts, riverId]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Stale invalidation on unmount (existing behavior)
|
|
60
|
+
/* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
return () => {
|
|
63
|
+
if (pathname?.includes("videoModal")) return;
|
|
64
|
+
|
|
65
|
+
invalidateComponents(getRefreshableComponents(riverComponents));
|
|
44
66
|
};
|
|
45
67
|
}, []);
|
|
68
|
+
/* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
69
|
+
|
|
70
|
+
// Reload when returning back to this screen from another screen
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (!isBackToScreen || pathname?.includes("videoModal")) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const refreshable = getRefreshableComponents(riverComponents);
|
|
77
|
+
|
|
78
|
+
if (refreshable.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
invalidateComponents(refreshable);
|
|
83
|
+
refreshCoordinator.triggerRefresh(refreshable, riverId);
|
|
84
|
+
}, [
|
|
85
|
+
isBackToScreen,
|
|
86
|
+
pathname,
|
|
87
|
+
riverComponents,
|
|
88
|
+
riverId,
|
|
89
|
+
invalidateComponents,
|
|
90
|
+
]);
|
|
46
91
|
};
|
|
47
92
|
|
|
48
93
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
|
|
3
|
-
import { renderHook } from "@testing-library/react-native";
|
|
3
|
+
import { act, render, renderHook } from "@testing-library/react-native";
|
|
4
4
|
import { isNavBarVisible, useRoute } from "../";
|
|
5
5
|
import { Provider } from "react-redux";
|
|
6
6
|
import configureMockStore from "redux-mock-store";
|
|
@@ -181,6 +181,45 @@ const hookModalWrapper = ({ children }) => (
|
|
|
181
181
|
</Provider>
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
+
// Reports every render of useRoute so the tests below can assert on route
|
|
185
|
+
// transitions and on subscription behaviour, not just on a one-shot result.
|
|
186
|
+
function RouteProbe({ onRender }: { onRender: (route: any) => void }) {
|
|
187
|
+
onRender(useRoute());
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const openRiverModal = () =>
|
|
193
|
+
act(() => {
|
|
194
|
+
modalStore.getState().openModal({
|
|
195
|
+
item: rivers["river-general"] as any,
|
|
196
|
+
props: { entry: modalEntry },
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const renderRouteProbe = (pathname: string, onRender: (route: any) => void) => {
|
|
201
|
+
const tree = (currentPathname: string) => (
|
|
202
|
+
<Provider store={store}>
|
|
203
|
+
<ScreenDataContext.Provider value={homeStack.state}>
|
|
204
|
+
<NavigationContext.Provider value={videoModalNavigator}>
|
|
205
|
+
<PathnameContext.Provider value={currentPathname}>
|
|
206
|
+
<RouteProbe onRender={onRender} />
|
|
207
|
+
</PathnameContext.Provider>
|
|
208
|
+
</NavigationContext.Provider>
|
|
209
|
+
</ScreenDataContext.Provider>
|
|
210
|
+
</Provider>
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const { rerender } = render(tree(pathname));
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
navigateTo: (nextPathname: string) =>
|
|
217
|
+
act(() => {
|
|
218
|
+
rerender(tree(nextPathname));
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
184
223
|
describe("navigation", () => {
|
|
185
224
|
describe("useRoute", () => {
|
|
186
225
|
describe("Main stack components", () => {
|
|
@@ -279,6 +318,94 @@ describe("navigation", () => {
|
|
|
279
318
|
);
|
|
280
319
|
});
|
|
281
320
|
});
|
|
321
|
+
|
|
322
|
+
describe("store subscriptions", () => {
|
|
323
|
+
afterEach(() => {
|
|
324
|
+
act(() => {
|
|
325
|
+
modalStore.getState().dismissModal();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Every hook in useRoute must run on every render. When the modal and
|
|
330
|
+
// hook-modal subscriptions lived inside the route branches, walking a
|
|
331
|
+
// single mounted component through the route types changed the hook
|
|
332
|
+
// count and React threw "Rendered more hooks than during the previous
|
|
333
|
+
// render".
|
|
334
|
+
it("keeps a stable hook order when the route type changes", () => {
|
|
335
|
+
let route: any;
|
|
336
|
+
|
|
337
|
+
const { navigateTo } = renderRouteProbe(homeStack.route, (current) => {
|
|
338
|
+
route = current;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(route.screenData.id).toEqual(entry.id);
|
|
342
|
+
expect(route.screenData.targetScreen.id).toEqual(screen.id);
|
|
343
|
+
|
|
344
|
+
openRiverModal();
|
|
345
|
+
navigateTo(modalPathname);
|
|
346
|
+
|
|
347
|
+
expect(route.pathname).toEqual(modalPathname);
|
|
348
|
+
expect(route.screenData.id).toEqual(modalEntry.id);
|
|
349
|
+
|
|
350
|
+
expect(route.screenData.targetScreen.id).toEqual(
|
|
351
|
+
rivers["river-general"].id
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
navigateTo(hooksModalPathname);
|
|
355
|
+
|
|
356
|
+
expect(route.pathname).toEqual(hooksModalPathname);
|
|
357
|
+
|
|
358
|
+
expect(route.screenData.id).toEqual(
|
|
359
|
+
hookModalContextState.state.screenData.payload.id
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
navigateTo(videoModalPathname);
|
|
363
|
+
|
|
364
|
+
expect(route.pathname).toEqual(videoModalPathname);
|
|
365
|
+
expect(route.screenData.id).toEqual(videoItem.id);
|
|
366
|
+
|
|
367
|
+
expect(route.screenData.targetScreen.id).toEqual(
|
|
368
|
+
rivers["river-player"].id
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
navigateTo(homeStack.route);
|
|
372
|
+
|
|
373
|
+
expect(route.pathname).toEqual(homeStack.route);
|
|
374
|
+
expect(route.screenData.id).toEqual(entry.id);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("does not re-render a non-modal route when modal state changes", () => {
|
|
378
|
+
let renders = 0;
|
|
379
|
+
|
|
380
|
+
renderRouteProbe(homeStack.route, () => {
|
|
381
|
+
renders += 1;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const rendersBeforeOpen = renders;
|
|
385
|
+
|
|
386
|
+
openRiverModal();
|
|
387
|
+
|
|
388
|
+
expect(renders).toBe(rendersBeforeOpen);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("re-renders a modal route when modal state changes", () => {
|
|
392
|
+
let route: any;
|
|
393
|
+
|
|
394
|
+
renderRouteProbe(modalPathname, (current) => {
|
|
395
|
+
route = current;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(route.screenData).toEqual({});
|
|
399
|
+
|
|
400
|
+
openRiverModal();
|
|
401
|
+
|
|
402
|
+
expect(route.screenData.id).toEqual(modalEntry.id);
|
|
403
|
+
|
|
404
|
+
expect(route.screenData.targetScreen.id).toEqual(
|
|
405
|
+
rivers["river-general"].id
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
282
409
|
});
|
|
283
410
|
|
|
284
411
|
describe("isNavBarVisible", () => {
|
|
@@ -50,14 +50,18 @@ export const useRoute = (
|
|
|
50
50
|
const rivers = useRivers();
|
|
51
51
|
const contentTypes = useContentTypes();
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// useRoute runs in every cell, so both modal stores are subscribed to only on
|
|
54
|
+
// the routes that actually read them. Off those routes the selectors return a
|
|
55
|
+
// stable reference, which keeps an unrelated modal opening or closing from
|
|
56
|
+
// re-rendering the whole tree.
|
|
57
57
|
const hookModalScreenData = useHookModalScreenData(
|
|
58
58
|
!isHookModalPathname(pathname)
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
+
const modalState = useModalStoreState(!isModalPathname(pathname));
|
|
62
|
+
|
|
63
|
+
const modalScreenData = modalState.screen;
|
|
64
|
+
|
|
61
65
|
const videoModalScreenData =
|
|
62
66
|
navigator?.videoModalState?.item &&
|
|
63
67
|
legacyScreenData(
|