forked from Expensify/App
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSequentialQueueTest.ts
More file actions
279 lines (240 loc) · 11.6 KB
/
Copy pathSequentialQueueTest.ts
File metadata and controls
279 lines (240 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import Onyx from 'react-native-onyx';
import type {OnyxKey, OnyxUpdate} from 'react-native-onyx';
import {getAll, getLength, getOngoingRequest} from '@userActions/PersistedRequests';
import ONYXKEYS from '@src/ONYXKEYS';
import * as SequentialQueue from '../../src/libs/Network/SequentialQueue';
import type Request from '../../src/types/onyx/Request';
import type {AnyRequest, ConflictActionData} from '../../src/types/onyx/Request';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
const request: Request<'userMetadata'> = {
command: 'ReconnectApp',
successData: [{key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}],
failureData: [{key: 'userMetadata', onyxMethod: 'set', value: {}}],
};
beforeAll(() => {
Onyx.init({
keys: ONYXKEYS,
});
});
beforeEach(() => {
global.fetch = TestHelper.getGlobalFetchMock();
return Onyx.clear().then(waitForBatchedUpdates);
});
describe('SequentialQueue', () => {
it('should push one request and persist one', () => {
SequentialQueue.push(request);
expect(getLength()).toBe(1);
});
it('should push two requests and persist two', () => {
SequentialQueue.push(request);
SequentialQueue.push(request);
expect(getLength()).toBe(2);
});
it('should push two requests with conflict resolution and replace', () => {
SequentialQueue.push(request);
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: (persistedRequests) => {
// should be one instance of ReconnectApp, get the index to replace it later
const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp');
if (index === -1) {
return {conflictAction: {type: 'push'}};
}
return {
conflictAction: {type: 'replace', index},
};
},
};
SequentialQueue.push(requestWithConflictResolution);
expect(getLength()).toBe(1);
// We know there is only one request and it's ongoing.
// We can get it and verify that the ongoing request is the second one.
const ongoingRequest = getOngoingRequest();
expect(ongoingRequest?.data?.accountID).toBe(56789);
});
it('should push two requests with conflict resolution and push', () => {
SequentialQueue.push(request);
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: () => {
return {conflictAction: {type: 'push'}};
},
};
SequentialQueue.push(requestWithConflictResolution);
expect(getLength()).toBe(2);
});
it('should push two requests with conflict resolution and noAction', () => {
SequentialQueue.push(request);
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: () => {
return {conflictAction: {type: 'noAction'}};
},
};
SequentialQueue.push(requestWithConflictResolution);
expect(getLength()).toBe(1);
});
it('should add a new request even if a similar one is ongoing', async () => {
// .push at the end flush the queue
SequentialQueue.push(request);
// wait for Onyx.connect execute the callback and start processing the queue
await Promise.resolve();
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: (persistedRequests) => {
// should be one instance of ReconnectApp, get the index to replace it later
const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp');
if (index === -1) {
return {conflictAction: {type: 'push'}};
}
return {
conflictAction: {type: 'replace', index},
};
},
};
SequentialQueue.push(requestWithConflictResolution);
const ongoingRequest = getOngoingRequest();
expect(ongoingRequest?.data?.accountID).toBe(56789);
});
it('should replace request request in queue while a similar one is ongoing', async () => {
// .push at the end flush the queue
SequentialQueue.push(request);
// wait for Onyx.connect execute the callback and start processing the queue
await Promise.resolve();
const conflictResolver = <TKey extends OnyxKey>(persistedRequests: Array<Request<TKey>>): ConflictActionData => {
// should be one instance of ReconnectApp, get the index to replace it later
const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp');
if (index === -1) {
return {conflictAction: {type: 'push'}};
}
return {
conflictAction: {type: 'replace', index},
};
};
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: conflictResolver,
};
const requestWithConflictResolution2: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: conflictResolver,
};
SequentialQueue.push(requestWithConflictResolution);
SequentialQueue.push(requestWithConflictResolution2);
expect(getLength()).toBe(2);
});
it('should replace request request in queue while a similar one is ongoing and keep the same index', () => {
SequentialQueue.push({command: 'OpenReport'});
SequentialQueue.push(request);
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp',
data: {accountID: 56789},
checkAndFixConflictingRequest: (persistedRequests) => {
// should be one instance of ReconnectApp, get the index to replace it later
const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp');
if (index === -1) {
return {conflictAction: {type: 'push'}};
}
return {
conflictAction: {type: 'replace', index},
};
},
};
SequentialQueue.push(requestWithConflictResolution);
SequentialQueue.push({command: 'AddComment'});
SequentialQueue.push({command: 'OpenReport'});
expect(getLength()).toBe(4);
const persistedRequests = getAll();
const ongoingRequest = getOngoingRequest();
// The first OpenReport call is ongoing
expect(ongoingRequest?.command).toBe('OpenReport');
// We know ReconnectApp is at index 0 in the queue now, so we can get it to verify
// that was replaced by the new request.
expect(persistedRequests.at(0)?.data?.accountID).toBe(56789);
});
// need to test a rance condition between processing the next request and then pushing a new request with conflict resolver
it('should resolve the conflict and replace the correct request in the queue while a new request is picked up after unpausing', async () => {
SequentialQueue.pause();
for (let i = 0; i < 5; i++) {
SequentialQueue.push({command: `OpenReport${i}`});
SequentialQueue.push({command: `AddComment${i}`});
}
SequentialQueue.push(request);
SequentialQueue.push({command: 'AddComment6'});
SequentialQueue.push({command: 'OpenReport6'});
// wait for Onyx.connect execute the callback and start processing the queue
await Promise.resolve();
const requestWithConflictResolution: Request<never> = {
command: 'ReconnectApp-replaced',
data: {accountID: 56789},
checkAndFixConflictingRequest: (persistedRequests) => {
// should be one instance of ReconnectApp, get the index to replace it later
const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp');
if (index === -1) {
return {conflictAction: {type: 'push'}};
}
return {
conflictAction: {type: 'replace', index},
};
},
};
Promise.resolve().then(() => {
SequentialQueue.unpause();
});
Promise.resolve().then(() => {
SequentialQueue.push(requestWithConflictResolution);
});
await Promise.resolve();
await Promise.resolve();
const persistedRequests = getAll();
// We know ReconnectApp is at index 9 in the queue, so we can get it to verify
// that was replaced by the new request.
expect(persistedRequests.at(9)?.command).toBe('ReconnectApp-replaced');
expect(persistedRequests.at(9)?.data?.accountID).toBe(56789);
});
// I need to test now when moving the request from the queue to the ongoing request the PERSISTED_REQUESTS is decreased and PERSISTED_ONGOING_REQUESTS has the new request
it('should move the request from the queue to the ongoing request and save it into Onyx', () => {
const persistedRequest = {...request, persistWhenOngoing: true, initiatedOffline: false};
SequentialQueue.push(persistedRequest);
const connectionId = Onyx.connect<typeof ONYXKEYS.PERSISTED_ONGOING_REQUESTS>({
key: ONYXKEYS.PERSISTED_ONGOING_REQUESTS,
callback: (ongoingRequest) => {
if (!ongoingRequest) {
return;
}
Onyx.disconnect(connectionId);
expect(ongoingRequest).toEqual(persistedRequest);
expect(ongoingRequest).toEqual(getOngoingRequest());
expect(getAll().length).toBe(0);
},
});
});
it('should get the ongoing request from onyx and start processing it', async () => {
const persistedRequest = {...request, persistWhenOngoing: true, initiatedOffline: false};
Onyx.set<typeof ONYXKEYS.PERSISTED_ONGOING_REQUESTS>(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, persistedRequest as AnyRequest);
SequentialQueue.push({command: 'OpenReport'});
await Promise.resolve();
expect(persistedRequest).toEqual(getOngoingRequest());
expect(getAll().length).toBe(1);
});
});
describe('SequentialQueue - QueueFlushedData', () => {
it('should add to queueFlushedData', async () => {
const updates: Array<OnyxUpdate<typeof ONYXKEYS.USER_METADATA>> = [{key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}];
await SequentialQueue.saveQueueFlushedData(...updates);
expect(SequentialQueue.getQueueFlushedData()).toEqual([{key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}]);
});
it('should clear queueFlushedData', async () => {
const updates: Array<OnyxUpdate<typeof ONYXKEYS.USER_METADATA>> = [{key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}];
await SequentialQueue.saveQueueFlushedData(...updates);
await SequentialQueue.clearQueueFlushedData();
expect(SequentialQueue.getQueueFlushedData()).toEqual([]);
});
});