@luvio/adapter-test-library 0.138.0 → 0.138.2
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/dist/es/es2018/test-library.js +552 -552
- package/dist/es/es2018/{MockDurableStore.d.ts → types/MockDurableStore.d.ts} +16 -16
- package/dist/{umd/es5 → es/es2018/types}/adapterRoundtrip.d.ts +35 -35
- package/dist/{umd/es2018 → es/es2018/types}/customMatchers.d.ts +10 -10
- package/dist/{umd/es5 → es/es2018/types}/durableStorePersistence.d.ts +13 -13
- package/dist/es/es2018/{jest.setup.d.ts → types/jest.setup.d.ts} +1 -1
- package/dist/es/es2018/{main.d.ts → types/main.d.ts} +9 -9
- package/dist/es/es2018/{mocks.d.ts → types/mocks.d.ts} +17 -17
- package/dist/es/es2018/{network.d.ts → types/network.d.ts} +38 -38
- package/dist/{umd/es5 → es/es2018/types}/utils.d.ts +37 -37
- package/dist/es/es2018/{verification.d.ts → types/verification.d.ts} +2 -2
- package/dist/umd/es2018/test-library.js +552 -552
- package/dist/umd/es2018/{MockDurableStore.d.ts → types/MockDurableStore.d.ts} +16 -16
- package/dist/{es/es2018 → umd/es2018/types}/adapterRoundtrip.d.ts +35 -35
- package/dist/umd/{es5 → es2018/types}/customMatchers.d.ts +10 -10
- package/dist/umd/es2018/{durableStorePersistence.d.ts → types/durableStorePersistence.d.ts} +13 -13
- package/dist/umd/es2018/{jest.setup.d.ts → types/jest.setup.d.ts} +1 -1
- package/dist/umd/es2018/{main.d.ts → types/main.d.ts} +9 -9
- package/dist/umd/es2018/{mocks.d.ts → types/mocks.d.ts} +17 -17
- package/dist/umd/{es5 → es2018/types}/network.d.ts +38 -38
- package/dist/umd/es2018/{utils.d.ts → types/utils.d.ts} +37 -37
- package/dist/umd/es2018/{verification.d.ts → types/verification.d.ts} +2 -2
- package/dist/umd/es5/test-library.js +707 -707
- package/dist/umd/es5/{MockDurableStore.d.ts → types/MockDurableStore.d.ts} +16 -16
- package/dist/umd/{es2018 → es5/types}/adapterRoundtrip.d.ts +35 -35
- package/dist/{es/es2018 → umd/es5/types}/customMatchers.d.ts +10 -10
- package/dist/{es/es2018 → umd/es5/types}/durableStorePersistence.d.ts +13 -13
- package/dist/umd/es5/{jest.setup.d.ts → types/jest.setup.d.ts} +1 -1
- package/dist/umd/es5/{main.d.ts → types/main.d.ts} +9 -9
- package/dist/umd/es5/{mocks.d.ts → types/mocks.d.ts} +17 -17
- package/dist/umd/{es2018 → es5/types}/network.d.ts +38 -38
- package/dist/{es/es2018 → umd/es5/types}/utils.d.ts +37 -37
- package/dist/umd/es5/{verification.d.ts → types/verification.d.ts} +2 -2
- package/package.json +4 -4
- package/rollup.config.js +3 -7
|
@@ -1,572 +1,572 @@
|
|
|
1
1
|
import sinon from 'sinon';
|
|
2
2
|
import { InMemoryStore, Environment, Luvio } from '@luvio/engine';
|
|
3
3
|
|
|
4
|
-
function isImmutable(value) {
|
|
5
|
-
return !doesThrow(() => {
|
|
6
|
-
// if verifyImmutable does not throw then the object is immutable
|
|
7
|
-
verifyImmutable(value, '$');
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
function verifyImmutable(value, path) {
|
|
11
|
-
if (typeof value !== 'object' || value === null) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
if (Array.isArray(value)) {
|
|
15
|
-
if (doesThrow(() => {
|
|
16
|
-
const len = value.length;
|
|
17
|
-
value.push('__test');
|
|
18
|
-
if (len === value.length) {
|
|
19
|
-
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
20
|
-
}
|
|
21
|
-
}) === false) {
|
|
22
|
-
throw new Error(`Unexpected mutable property found at ${path}: Array is extensible!`);
|
|
23
|
-
}
|
|
24
|
-
value.forEach((item, index) => {
|
|
25
|
-
verifyImmutable(item, `${path}.${index}`);
|
|
26
|
-
});
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (doesThrow(() => {
|
|
30
|
-
value['__test_____'] = true;
|
|
31
|
-
if (value['__test_____'] !== true) {
|
|
32
|
-
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
33
|
-
}
|
|
34
|
-
}) === false) {
|
|
35
|
-
throw new Error(`Unexpected mutable property found at ${path}: Object is extensible!`);
|
|
36
|
-
}
|
|
37
|
-
Object.keys(value).forEach((key) => {
|
|
38
|
-
if (doesThrow(() => {
|
|
39
|
-
const old = value[key];
|
|
40
|
-
value[key] = '_______foo';
|
|
41
|
-
if (value[key] === old) {
|
|
42
|
-
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
43
|
-
}
|
|
44
|
-
}) === false) {
|
|
45
|
-
throw new Error(`Unexpected mutable property found at ${path}: "${path}.${key}" is mutable!`);
|
|
46
|
-
}
|
|
47
|
-
verifyImmutable(value[key], `${path}.${key}`);
|
|
48
|
-
});
|
|
4
|
+
function isImmutable(value) {
|
|
5
|
+
return !doesThrow(() => {
|
|
6
|
+
// if verifyImmutable does not throw then the object is immutable
|
|
7
|
+
verifyImmutable(value, '$');
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function verifyImmutable(value, path) {
|
|
11
|
+
if (typeof value !== 'object' || value === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
if (doesThrow(() => {
|
|
16
|
+
const len = value.length;
|
|
17
|
+
value.push('__test');
|
|
18
|
+
if (len === value.length) {
|
|
19
|
+
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
20
|
+
}
|
|
21
|
+
}) === false) {
|
|
22
|
+
throw new Error(`Unexpected mutable property found at ${path}: Array is extensible!`);
|
|
23
|
+
}
|
|
24
|
+
value.forEach((item, index) => {
|
|
25
|
+
verifyImmutable(item, `${path}.${index}`);
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (doesThrow(() => {
|
|
30
|
+
value['__test_____'] = true;
|
|
31
|
+
if (value['__test_____'] !== true) {
|
|
32
|
+
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
33
|
+
}
|
|
34
|
+
}) === false) {
|
|
35
|
+
throw new Error(`Unexpected mutable property found at ${path}: Object is extensible!`);
|
|
36
|
+
}
|
|
37
|
+
Object.keys(value).forEach((key) => {
|
|
38
|
+
if (doesThrow(() => {
|
|
39
|
+
const old = value[key];
|
|
40
|
+
value[key] = '_______foo';
|
|
41
|
+
if (value[key] === old) {
|
|
42
|
+
throw new Error('IE11 does not throw when mutating a frozen object');
|
|
43
|
+
}
|
|
44
|
+
}) === false) {
|
|
45
|
+
throw new Error(`Unexpected mutable property found at ${path}: "${path}.${key}" is mutable!`);
|
|
46
|
+
}
|
|
47
|
+
verifyImmutable(value[key], `${path}.${key}`);
|
|
48
|
+
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Clone an object
|
|
53
|
-
*
|
|
54
|
-
* @param {object} data The object to clone
|
|
55
|
-
* @returns {object} The cloned object
|
|
56
|
-
*/
|
|
57
|
-
function clone(data) {
|
|
58
|
-
if (data === undefined) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return JSON.parse(JSON.stringify(data));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Strips the given properties from an object. Useful for comparing data payloads
|
|
65
|
-
* where the given props may not be present in the data returned from an adapter.
|
|
66
|
-
*
|
|
67
|
-
* Does not mutate the passed in object.
|
|
68
|
-
*
|
|
69
|
-
* @param {object} obj The object from which to strip properties
|
|
70
|
-
* @param {string[]} props The properties to be removed
|
|
71
|
-
* @returns {object} The updated object with the given props stripped out
|
|
72
|
-
*/
|
|
73
|
-
function stripProperties(obj, props) {
|
|
74
|
-
// don't mutate the original object
|
|
75
|
-
const cloned = clone(obj);
|
|
76
|
-
stripPropertiesMutating(cloned, props);
|
|
77
|
-
return cloned;
|
|
78
|
-
}
|
|
79
|
-
// private version of strip properties that mutates
|
|
80
|
-
function stripPropertiesMutating(obj, props) {
|
|
81
|
-
props.forEach((prop) => {
|
|
82
|
-
delete obj[prop];
|
|
83
|
-
});
|
|
84
|
-
Object.keys(obj).forEach((key) => {
|
|
85
|
-
const value = obj[key];
|
|
86
|
-
if (typeof value === 'object' && value !== null) {
|
|
87
|
-
stripPropertiesMutating(value, props);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
function doesThrow(predicate) {
|
|
92
|
-
try {
|
|
93
|
-
predicate();
|
|
94
|
-
}
|
|
95
|
-
catch (e) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
function flushPromises() {
|
|
101
|
-
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
102
|
-
}
|
|
103
|
-
// Copied from engine to avoid build time dependency
|
|
104
|
-
// BEGIN OF COPY BLOCK
|
|
105
|
-
var SnapshotState;
|
|
106
|
-
(function (SnapshotState) {
|
|
107
|
-
SnapshotState["Fulfilled"] = "Fulfilled";
|
|
108
|
-
SnapshotState["Unfulfilled"] = "Unfulfilled";
|
|
109
|
-
SnapshotState["Error"] = "Error";
|
|
110
|
-
SnapshotState["Pending"] = "Pending";
|
|
111
|
-
SnapshotState["Stale"] = "Stale";
|
|
112
|
-
})(SnapshotState || (SnapshotState = {}));
|
|
113
|
-
function isErrorSnapshot(snapshot) {
|
|
114
|
-
return snapshot.state === SnapshotState.Error;
|
|
115
|
-
}
|
|
116
|
-
function isFulfilledSnapshot(snapshot) {
|
|
117
|
-
return snapshot.state === SnapshotState.Fulfilled;
|
|
118
|
-
}
|
|
119
|
-
function isStaleSnapshot(snapshot) {
|
|
120
|
-
return snapshot.state === SnapshotState.Stale;
|
|
121
|
-
}
|
|
122
|
-
// END OF COPY BLOCK
|
|
123
|
-
const customMatchers = {
|
|
124
|
-
toEqualFulfilledSnapshotWithData: (snapshot, expected, privateProperties) => {
|
|
125
|
-
if (isFulfilledSnapshot(snapshot)) {
|
|
126
|
-
const expectedWithoutPrivateProperties = stripProperties(expected, privateProperties || []);
|
|
127
|
-
expect(snapshot.data).toEqual(expectedWithoutPrivateProperties);
|
|
128
|
-
expect(isImmutable(snapshot.data)).toBe(true);
|
|
129
|
-
return { pass: true, message: () => 'Snapshot is a FulfilledSnapshot' };
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
pass: false,
|
|
133
|
-
message: () => 'Actual Snapshot is not a FulfilledSnapshot.',
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
toEqualStaleSnapshotWithData: (snapshot, expected, privateProperties) => {
|
|
137
|
-
if (isStaleSnapshot(snapshot)) {
|
|
138
|
-
const expectedWithoutPrivateProperties = stripProperties(expected, privateProperties || []);
|
|
139
|
-
expect(snapshot.data).toEqual(expectedWithoutPrivateProperties);
|
|
140
|
-
expect(isImmutable(snapshot.data)).toBe(true);
|
|
141
|
-
return { pass: true, message: () => 'Snapshot is a StaleSnapshot' };
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
pass: false,
|
|
145
|
-
message: () => 'Actual Snapshot is not a StaleSnapshot.',
|
|
146
|
-
};
|
|
147
|
-
},
|
|
148
|
-
toEqualErrorSnapshot: (actual, expectedStatus) => {
|
|
149
|
-
if (isErrorSnapshot(actual)) {
|
|
150
|
-
expect(isImmutable(actual.error)).toBe(true);
|
|
151
|
-
expect(actual.data).toBeUndefined();
|
|
152
|
-
if (expectedStatus !== undefined) {
|
|
153
|
-
expect(actual.error.status).toBe(expectedStatus);
|
|
154
|
-
}
|
|
155
|
-
return { pass: true, message: () => 'Snapshot is an ErrorSnapshot' };
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
pass: false,
|
|
159
|
-
message: () => 'Actual Snapshot is not a ErrorSnapshot.',
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
toEqualOfflineErrorSnapshot: (actual) => {
|
|
163
|
-
if (isErrorSnapshot(actual)) {
|
|
164
|
-
expect(isImmutable(actual.error)).toBe(true);
|
|
165
|
-
expect(actual.data).toBeUndefined();
|
|
166
|
-
expect(actual.error).toBeInstanceOf(Error);
|
|
167
|
-
expect(actual.error.errorType).toBe('networkAdapterError');
|
|
168
|
-
return {
|
|
169
|
-
pass: true,
|
|
170
|
-
message: () => 'Snapshot is an ErrorSnapshot',
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
pass: false,
|
|
175
|
-
message: () => 'Actual Snapshot is not a ErrorSnapshot.',
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
toHaveBeenHitTimes: (mockNetworkAdapter, expected) => {
|
|
179
|
-
const callCount = getMockNetworkAdapterCallCount(mockNetworkAdapter);
|
|
180
|
-
if (callCount !== expected) {
|
|
181
|
-
return {
|
|
182
|
-
pass: false,
|
|
183
|
-
message: () => `Number of network calls made are different. Actual ${callCount}, Expected ${expected}.`,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return { pass: true, message: () => `Network calls were made ${expected} times.` };
|
|
187
|
-
},
|
|
188
|
-
toHaveBeenHitOnce: (mockNetworkAdapter) => {
|
|
189
|
-
const callCount = getMockNetworkAdapterCallCount(mockNetworkAdapter);
|
|
190
|
-
if (callCount !== 1) {
|
|
191
|
-
return {
|
|
192
|
-
pass: false,
|
|
193
|
-
message: () => `Number of network calls was not 1. Actual ${callCount}.`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
return { pass: true, message: () => 'Number of network calls was 1' };
|
|
197
|
-
},
|
|
198
|
-
toEqualFetchResponse: (actual, expected) => {
|
|
199
|
-
expect(actual).toEqual({ ...expected, errorType: 'fetchResponse' });
|
|
200
|
-
expect(isImmutable(actual)).toBe(true);
|
|
201
|
-
return { pass: true, message: () => 'Actual response equals expected response' };
|
|
202
|
-
},
|
|
51
|
+
/**
|
|
52
|
+
* Clone an object
|
|
53
|
+
*
|
|
54
|
+
* @param {object} data The object to clone
|
|
55
|
+
* @returns {object} The cloned object
|
|
56
|
+
*/
|
|
57
|
+
function clone(data) {
|
|
58
|
+
if (data === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return JSON.parse(JSON.stringify(data));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Strips the given properties from an object. Useful for comparing data payloads
|
|
65
|
+
* where the given props may not be present in the data returned from an adapter.
|
|
66
|
+
*
|
|
67
|
+
* Does not mutate the passed in object.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} obj The object from which to strip properties
|
|
70
|
+
* @param {string[]} props The properties to be removed
|
|
71
|
+
* @returns {object} The updated object with the given props stripped out
|
|
72
|
+
*/
|
|
73
|
+
function stripProperties(obj, props) {
|
|
74
|
+
// don't mutate the original object
|
|
75
|
+
const cloned = clone(obj);
|
|
76
|
+
stripPropertiesMutating(cloned, props);
|
|
77
|
+
return cloned;
|
|
78
|
+
}
|
|
79
|
+
// private version of strip properties that mutates
|
|
80
|
+
function stripPropertiesMutating(obj, props) {
|
|
81
|
+
props.forEach((prop) => {
|
|
82
|
+
delete obj[prop];
|
|
83
|
+
});
|
|
84
|
+
Object.keys(obj).forEach((key) => {
|
|
85
|
+
const value = obj[key];
|
|
86
|
+
if (typeof value === 'object' && value !== null) {
|
|
87
|
+
stripPropertiesMutating(value, props);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function doesThrow(predicate) {
|
|
92
|
+
try {
|
|
93
|
+
predicate();
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function flushPromises() {
|
|
101
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
102
|
+
}
|
|
103
|
+
// Copied from engine to avoid build time dependency
|
|
104
|
+
// BEGIN OF COPY BLOCK
|
|
105
|
+
var SnapshotState;
|
|
106
|
+
(function (SnapshotState) {
|
|
107
|
+
SnapshotState["Fulfilled"] = "Fulfilled";
|
|
108
|
+
SnapshotState["Unfulfilled"] = "Unfulfilled";
|
|
109
|
+
SnapshotState["Error"] = "Error";
|
|
110
|
+
SnapshotState["Pending"] = "Pending";
|
|
111
|
+
SnapshotState["Stale"] = "Stale";
|
|
112
|
+
})(SnapshotState || (SnapshotState = {}));
|
|
113
|
+
function isErrorSnapshot(snapshot) {
|
|
114
|
+
return snapshot.state === SnapshotState.Error;
|
|
115
|
+
}
|
|
116
|
+
function isFulfilledSnapshot(snapshot) {
|
|
117
|
+
return snapshot.state === SnapshotState.Fulfilled;
|
|
118
|
+
}
|
|
119
|
+
function isStaleSnapshot(snapshot) {
|
|
120
|
+
return snapshot.state === SnapshotState.Stale;
|
|
121
|
+
}
|
|
122
|
+
// END OF COPY BLOCK
|
|
123
|
+
const customMatchers = {
|
|
124
|
+
toEqualFulfilledSnapshotWithData: (snapshot, expected, privateProperties) => {
|
|
125
|
+
if (isFulfilledSnapshot(snapshot)) {
|
|
126
|
+
const expectedWithoutPrivateProperties = stripProperties(expected, privateProperties || []);
|
|
127
|
+
expect(snapshot.data).toEqual(expectedWithoutPrivateProperties);
|
|
128
|
+
expect(isImmutable(snapshot.data)).toBe(true);
|
|
129
|
+
return { pass: true, message: () => 'Snapshot is a FulfilledSnapshot' };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
pass: false,
|
|
133
|
+
message: () => 'Actual Snapshot is not a FulfilledSnapshot.',
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
toEqualStaleSnapshotWithData: (snapshot, expected, privateProperties) => {
|
|
137
|
+
if (isStaleSnapshot(snapshot)) {
|
|
138
|
+
const expectedWithoutPrivateProperties = stripProperties(expected, privateProperties || []);
|
|
139
|
+
expect(snapshot.data).toEqual(expectedWithoutPrivateProperties);
|
|
140
|
+
expect(isImmutable(snapshot.data)).toBe(true);
|
|
141
|
+
return { pass: true, message: () => 'Snapshot is a StaleSnapshot' };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
pass: false,
|
|
145
|
+
message: () => 'Actual Snapshot is not a StaleSnapshot.',
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
toEqualErrorSnapshot: (actual, expectedStatus) => {
|
|
149
|
+
if (isErrorSnapshot(actual)) {
|
|
150
|
+
expect(isImmutable(actual.error)).toBe(true);
|
|
151
|
+
expect(actual.data).toBeUndefined();
|
|
152
|
+
if (expectedStatus !== undefined) {
|
|
153
|
+
expect(actual.error.status).toBe(expectedStatus);
|
|
154
|
+
}
|
|
155
|
+
return { pass: true, message: () => 'Snapshot is an ErrorSnapshot' };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
pass: false,
|
|
159
|
+
message: () => 'Actual Snapshot is not a ErrorSnapshot.',
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
toEqualOfflineErrorSnapshot: (actual) => {
|
|
163
|
+
if (isErrorSnapshot(actual)) {
|
|
164
|
+
expect(isImmutable(actual.error)).toBe(true);
|
|
165
|
+
expect(actual.data).toBeUndefined();
|
|
166
|
+
expect(actual.error).toBeInstanceOf(Error);
|
|
167
|
+
expect(actual.error.errorType).toBe('networkAdapterError');
|
|
168
|
+
return {
|
|
169
|
+
pass: true,
|
|
170
|
+
message: () => 'Snapshot is an ErrorSnapshot',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
pass: false,
|
|
175
|
+
message: () => 'Actual Snapshot is not a ErrorSnapshot.',
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
toHaveBeenHitTimes: (mockNetworkAdapter, expected) => {
|
|
179
|
+
const callCount = getMockNetworkAdapterCallCount(mockNetworkAdapter);
|
|
180
|
+
if (callCount !== expected) {
|
|
181
|
+
return {
|
|
182
|
+
pass: false,
|
|
183
|
+
message: () => `Number of network calls made are different. Actual ${callCount}, Expected ${expected}.`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { pass: true, message: () => `Network calls were made ${expected} times.` };
|
|
187
|
+
},
|
|
188
|
+
toHaveBeenHitOnce: (mockNetworkAdapter) => {
|
|
189
|
+
const callCount = getMockNetworkAdapterCallCount(mockNetworkAdapter);
|
|
190
|
+
if (callCount !== 1) {
|
|
191
|
+
return {
|
|
192
|
+
pass: false,
|
|
193
|
+
message: () => `Number of network calls was not 1. Actual ${callCount}.`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { pass: true, message: () => 'Number of network calls was 1' };
|
|
197
|
+
},
|
|
198
|
+
toEqualFetchResponse: (actual, expected) => {
|
|
199
|
+
expect(actual).toEqual({ ...expected, errorType: 'fetchResponse' });
|
|
200
|
+
expect(isImmutable(actual)).toBe(true);
|
|
201
|
+
return { pass: true, message: () => 'Actual response equals expected response' };
|
|
202
|
+
},
|
|
203
203
|
};
|
|
204
204
|
|
|
205
|
-
const networkConnectivityStateMap = new WeakMap();
|
|
206
|
-
var ConnectivityState;
|
|
207
|
-
(function (ConnectivityState) {
|
|
208
|
-
ConnectivityState[ConnectivityState["Online"] = 0] = "Online";
|
|
209
|
-
ConnectivityState[ConnectivityState["Offline"] = 1] = "Offline";
|
|
210
|
-
})(ConnectivityState || (ConnectivityState = {}));
|
|
211
|
-
const callCountMap = new WeakMap();
|
|
212
|
-
const mockPayloadsMap = new WeakMap();
|
|
213
|
-
function buildOfflineError() {
|
|
214
|
-
return new Error('Failed to fetch, request timeout');
|
|
215
|
-
}
|
|
216
|
-
function buildMockSetupError() {
|
|
217
|
-
return new Error('A mock network response was not setup for this request');
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* This function sorts two payloads based on number of parameters. It sorts in
|
|
221
|
-
* ascending order, meaning a MockPayload with fewer parameters in the networkArgs
|
|
222
|
-
* will come before a MockPayload with more parameters in the networkArgs.
|
|
223
|
-
*
|
|
224
|
-
* Sorting really only matters when a two networkArg objects have all the same
|
|
225
|
-
* fields except for uri/query/headers, so this sorting algorithm is simplistic.
|
|
226
|
-
* If two networkArg objects have different baseUri or basePath properties, for
|
|
227
|
-
* example, then the sinon matcher won't have a collision so sorting doesn't
|
|
228
|
-
* matter in that situation.
|
|
229
|
-
*
|
|
230
|
-
* @param {MockPayload} a
|
|
231
|
-
* @param {MockPayload} b
|
|
232
|
-
* @returns {number}
|
|
233
|
-
*/
|
|
234
|
-
function sortPayloads(a, b) {
|
|
235
|
-
const { networkArgs: { urlParams: bUrlParams, queryParams: bQueryParams, headers: bHeaders }, } = b;
|
|
236
|
-
const { networkArgs: { urlParams: aUrlParams, queryParams: aQueryParams, headers: aHeaders }, } = a;
|
|
237
|
-
const aParams = Object.keys(aUrlParams !== null && aUrlParams !== void 0 ? aUrlParams : {}).length +
|
|
238
|
-
Object.keys(aQueryParams !== null && aQueryParams !== void 0 ? aQueryParams : {}).length +
|
|
239
|
-
Object.keys(aHeaders !== null && aHeaders !== void 0 ? aHeaders : {}).length;
|
|
240
|
-
const bParams = Object.keys(bUrlParams !== null && bUrlParams !== void 0 ? bUrlParams : {}).length +
|
|
241
|
-
Object.keys(bQueryParams !== null && bQueryParams !== void 0 ? bQueryParams : {}).length +
|
|
242
|
-
Object.keys(bHeaders !== null && bHeaders !== void 0 ? bHeaders : {}).length;
|
|
243
|
-
return aParams - bParams;
|
|
244
|
-
}
|
|
245
|
-
function isOkResponse(status) {
|
|
246
|
-
return status >= 200 && status <= 299;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Flushes any pending network requests. Useful for tests that need to ensure all
|
|
250
|
-
* un-awaited background refreshes are complete
|
|
251
|
-
*
|
|
252
|
-
* @param _mockNetworkAdapter {NetworkAdapter} The network adapter instance to flush
|
|
253
|
-
*/
|
|
254
|
-
async function flushPendingNetworkRequests(_mockNetworkAdapter) {
|
|
255
|
-
// since the network mock is actually synchronous (just returns things wrapped
|
|
256
|
-
// in Promise.resolve/reject) the only thing necessary to flush any pending
|
|
257
|
-
// network activity is to flush the pending microtask queue
|
|
258
|
-
await flushPromises();
|
|
259
|
-
}
|
|
260
|
-
function buildMockNetworkAdapter(mockPayloads) {
|
|
261
|
-
// any endpoints not setup with a fake will return a rejected promise
|
|
262
|
-
const networkAdapter = sinon.stub().rejects(buildMockSetupError());
|
|
263
|
-
callCountMap.set(networkAdapter, 0);
|
|
264
|
-
mockPayloadsMap.set(networkAdapter, mockPayloads);
|
|
265
|
-
setMockNetworkPayloads(networkAdapter, mockPayloads);
|
|
266
|
-
return networkAdapter;
|
|
267
|
-
}
|
|
268
|
-
function setNetworkConnectivity(mockNetworkAdapter, connectivityState) {
|
|
269
|
-
const stub = mockNetworkAdapter;
|
|
270
|
-
networkConnectivityStateMap.set(stub, connectivityState);
|
|
271
|
-
}
|
|
272
|
-
function onNetworkStubCalled(stub, response) {
|
|
273
|
-
var _a;
|
|
274
|
-
const { body, status, statusText, ok, headers } = response;
|
|
275
|
-
const currentCallCount = callCountMap.get(stub);
|
|
276
|
-
callCountMap.set(stub, currentCallCount ? currentCallCount + 1 : 1);
|
|
277
|
-
const connectivityState = (_a = networkConnectivityStateMap.get(stub)) !== null && _a !== void 0 ? _a : ConnectivityState.Online;
|
|
278
|
-
if (connectivityState === ConnectivityState.Offline) {
|
|
279
|
-
return Promise.reject(buildOfflineError());
|
|
280
|
-
}
|
|
281
|
-
return Promise.resolve({
|
|
282
|
-
status,
|
|
283
|
-
statusText,
|
|
284
|
-
ok,
|
|
285
|
-
body: clone(body),
|
|
286
|
-
headers: clone(headers),
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function setMockNetworkPayloads(mockNetworkAdapter, mockPayloads) {
|
|
290
|
-
const stub = mockNetworkAdapter;
|
|
291
|
-
// sort mock payloads so least-specific network args are registered first
|
|
292
|
-
mockPayloads.sort(sortPayloads).forEach((mockPayload) => {
|
|
293
|
-
const { networkArgs, response } = mockPayload;
|
|
294
|
-
const args = sinon.match(networkArgs);
|
|
295
|
-
stub.withArgs(args).callsFake(() => {
|
|
296
|
-
return onNetworkStubCalled(stub, response);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Overrides the mock network adapter responses with the given "responses". Each request to
|
|
302
|
-
* the network adapter will "pop" a response from the response array. If more requests
|
|
303
|
-
* to the network adapter are made than responses in the given array then an error
|
|
304
|
-
* will be returned.
|
|
305
|
-
*
|
|
306
|
-
* This differs from setMockNetworkPayloads because it does not look at the incoming
|
|
307
|
-
* network request args to determine what the response should be, it just returns
|
|
308
|
-
* the response based on the request order.
|
|
309
|
-
*
|
|
310
|
-
* NOTE: this overrides any previously setup MockPayloads for the given network adapter
|
|
311
|
-
* until resetMockNetworkAdapter is called.
|
|
312
|
-
*/
|
|
313
|
-
function overrideMockNetworkResponses(mockNetworkAdapter, responses) {
|
|
314
|
-
const stub = mockNetworkAdapter;
|
|
315
|
-
let index = 0;
|
|
316
|
-
stub.withArgs(sinon.match.any).callsFake(() => {
|
|
317
|
-
const response = responses[index++];
|
|
318
|
-
if (response === undefined) {
|
|
319
|
-
// if they have more requests than expected
|
|
320
|
-
return Promise.reject(buildMockSetupError());
|
|
321
|
-
}
|
|
322
|
-
return onNetworkStubCalled(stub, response);
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
function resetMockNetworkAdapter(mockNetworkAdapter) {
|
|
326
|
-
const stub = mockNetworkAdapter;
|
|
327
|
-
// reset stub in case a test modified payload responses
|
|
328
|
-
stub.reset();
|
|
329
|
-
// reset call count
|
|
330
|
-
callCountMap.set(stub, 0);
|
|
331
|
-
// set original mock payloads up
|
|
332
|
-
const payloads = mockPayloadsMap.get(stub);
|
|
333
|
-
if (payloads) {
|
|
334
|
-
setMockNetworkPayloads(stub, payloads);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function getMockNetworkAdapterCallCount(mockNetworkAdapter) {
|
|
338
|
-
const stub = mockNetworkAdapter;
|
|
339
|
-
return callCountMap.get(stub);
|
|
340
|
-
}
|
|
341
|
-
function buildFetchResponse(body, status = 200, statusText = 'Ok', headers = {}) {
|
|
342
|
-
return {
|
|
343
|
-
headers,
|
|
344
|
-
body,
|
|
345
|
-
status,
|
|
346
|
-
statusText,
|
|
347
|
-
ok: isOkResponse(status),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
function buildSuccessMockPayload(networkArgs, body, headers = {}) {
|
|
351
|
-
const mockPayload = {
|
|
352
|
-
networkArgs,
|
|
353
|
-
response: buildFetchResponse(body, 200, 'Ok', headers),
|
|
354
|
-
};
|
|
355
|
-
return mockPayload;
|
|
356
|
-
}
|
|
357
|
-
function buildErrorMockPayload(networkArgs, body, errorStatusCode, errorStatusText, headers = {}) {
|
|
358
|
-
const mockPayload = {
|
|
359
|
-
networkArgs,
|
|
360
|
-
response: buildFetchResponse(body, errorStatusCode, errorStatusText, headers),
|
|
361
|
-
};
|
|
362
|
-
return mockPayload;
|
|
205
|
+
const networkConnectivityStateMap = new WeakMap();
|
|
206
|
+
var ConnectivityState;
|
|
207
|
+
(function (ConnectivityState) {
|
|
208
|
+
ConnectivityState[ConnectivityState["Online"] = 0] = "Online";
|
|
209
|
+
ConnectivityState[ConnectivityState["Offline"] = 1] = "Offline";
|
|
210
|
+
})(ConnectivityState || (ConnectivityState = {}));
|
|
211
|
+
const callCountMap = new WeakMap();
|
|
212
|
+
const mockPayloadsMap = new WeakMap();
|
|
213
|
+
function buildOfflineError() {
|
|
214
|
+
return new Error('Failed to fetch, request timeout');
|
|
215
|
+
}
|
|
216
|
+
function buildMockSetupError() {
|
|
217
|
+
return new Error('A mock network response was not setup for this request');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* This function sorts two payloads based on number of parameters. It sorts in
|
|
221
|
+
* ascending order, meaning a MockPayload with fewer parameters in the networkArgs
|
|
222
|
+
* will come before a MockPayload with more parameters in the networkArgs.
|
|
223
|
+
*
|
|
224
|
+
* Sorting really only matters when a two networkArg objects have all the same
|
|
225
|
+
* fields except for uri/query/headers, so this sorting algorithm is simplistic.
|
|
226
|
+
* If two networkArg objects have different baseUri or basePath properties, for
|
|
227
|
+
* example, then the sinon matcher won't have a collision so sorting doesn't
|
|
228
|
+
* matter in that situation.
|
|
229
|
+
*
|
|
230
|
+
* @param {MockPayload} a
|
|
231
|
+
* @param {MockPayload} b
|
|
232
|
+
* @returns {number}
|
|
233
|
+
*/
|
|
234
|
+
function sortPayloads(a, b) {
|
|
235
|
+
const { networkArgs: { urlParams: bUrlParams, queryParams: bQueryParams, headers: bHeaders }, } = b;
|
|
236
|
+
const { networkArgs: { urlParams: aUrlParams, queryParams: aQueryParams, headers: aHeaders }, } = a;
|
|
237
|
+
const aParams = Object.keys(aUrlParams !== null && aUrlParams !== void 0 ? aUrlParams : {}).length +
|
|
238
|
+
Object.keys(aQueryParams !== null && aQueryParams !== void 0 ? aQueryParams : {}).length +
|
|
239
|
+
Object.keys(aHeaders !== null && aHeaders !== void 0 ? aHeaders : {}).length;
|
|
240
|
+
const bParams = Object.keys(bUrlParams !== null && bUrlParams !== void 0 ? bUrlParams : {}).length +
|
|
241
|
+
Object.keys(bQueryParams !== null && bQueryParams !== void 0 ? bQueryParams : {}).length +
|
|
242
|
+
Object.keys(bHeaders !== null && bHeaders !== void 0 ? bHeaders : {}).length;
|
|
243
|
+
return aParams - bParams;
|
|
244
|
+
}
|
|
245
|
+
function isOkResponse(status) {
|
|
246
|
+
return status >= 200 && status <= 299;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Flushes any pending network requests. Useful for tests that need to ensure all
|
|
250
|
+
* un-awaited background refreshes are complete
|
|
251
|
+
*
|
|
252
|
+
* @param _mockNetworkAdapter {NetworkAdapter} The network adapter instance to flush
|
|
253
|
+
*/
|
|
254
|
+
async function flushPendingNetworkRequests(_mockNetworkAdapter) {
|
|
255
|
+
// since the network mock is actually synchronous (just returns things wrapped
|
|
256
|
+
// in Promise.resolve/reject) the only thing necessary to flush any pending
|
|
257
|
+
// network activity is to flush the pending microtask queue
|
|
258
|
+
await flushPromises();
|
|
259
|
+
}
|
|
260
|
+
function buildMockNetworkAdapter(mockPayloads) {
|
|
261
|
+
// any endpoints not setup with a fake will return a rejected promise
|
|
262
|
+
const networkAdapter = sinon.stub().rejects(buildMockSetupError());
|
|
263
|
+
callCountMap.set(networkAdapter, 0);
|
|
264
|
+
mockPayloadsMap.set(networkAdapter, mockPayloads);
|
|
265
|
+
setMockNetworkPayloads(networkAdapter, mockPayloads);
|
|
266
|
+
return networkAdapter;
|
|
267
|
+
}
|
|
268
|
+
function setNetworkConnectivity(mockNetworkAdapter, connectivityState) {
|
|
269
|
+
const stub = mockNetworkAdapter;
|
|
270
|
+
networkConnectivityStateMap.set(stub, connectivityState);
|
|
271
|
+
}
|
|
272
|
+
function onNetworkStubCalled(stub, response) {
|
|
273
|
+
var _a;
|
|
274
|
+
const { body, status, statusText, ok, headers } = response;
|
|
275
|
+
const currentCallCount = callCountMap.get(stub);
|
|
276
|
+
callCountMap.set(stub, currentCallCount ? currentCallCount + 1 : 1);
|
|
277
|
+
const connectivityState = (_a = networkConnectivityStateMap.get(stub)) !== null && _a !== void 0 ? _a : ConnectivityState.Online;
|
|
278
|
+
if (connectivityState === ConnectivityState.Offline) {
|
|
279
|
+
return Promise.reject(buildOfflineError());
|
|
280
|
+
}
|
|
281
|
+
return Promise.resolve({
|
|
282
|
+
status,
|
|
283
|
+
statusText,
|
|
284
|
+
ok,
|
|
285
|
+
body: clone(body),
|
|
286
|
+
headers: clone(headers),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function setMockNetworkPayloads(mockNetworkAdapter, mockPayloads) {
|
|
290
|
+
const stub = mockNetworkAdapter;
|
|
291
|
+
// sort mock payloads so least-specific network args are registered first
|
|
292
|
+
mockPayloads.sort(sortPayloads).forEach((mockPayload) => {
|
|
293
|
+
const { networkArgs, response } = mockPayload;
|
|
294
|
+
const args = sinon.match(networkArgs);
|
|
295
|
+
stub.withArgs(args).callsFake(() => {
|
|
296
|
+
return onNetworkStubCalled(stub, response);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Overrides the mock network adapter responses with the given "responses". Each request to
|
|
302
|
+
* the network adapter will "pop" a response from the response array. If more requests
|
|
303
|
+
* to the network adapter are made than responses in the given array then an error
|
|
304
|
+
* will be returned.
|
|
305
|
+
*
|
|
306
|
+
* This differs from setMockNetworkPayloads because it does not look at the incoming
|
|
307
|
+
* network request args to determine what the response should be, it just returns
|
|
308
|
+
* the response based on the request order.
|
|
309
|
+
*
|
|
310
|
+
* NOTE: this overrides any previously setup MockPayloads for the given network adapter
|
|
311
|
+
* until resetMockNetworkAdapter is called.
|
|
312
|
+
*/
|
|
313
|
+
function overrideMockNetworkResponses(mockNetworkAdapter, responses) {
|
|
314
|
+
const stub = mockNetworkAdapter;
|
|
315
|
+
let index = 0;
|
|
316
|
+
stub.withArgs(sinon.match.any).callsFake(() => {
|
|
317
|
+
const response = responses[index++];
|
|
318
|
+
if (response === undefined) {
|
|
319
|
+
// if they have more requests than expected
|
|
320
|
+
return Promise.reject(buildMockSetupError());
|
|
321
|
+
}
|
|
322
|
+
return onNetworkStubCalled(stub, response);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function resetMockNetworkAdapter(mockNetworkAdapter) {
|
|
326
|
+
const stub = mockNetworkAdapter;
|
|
327
|
+
// reset stub in case a test modified payload responses
|
|
328
|
+
stub.reset();
|
|
329
|
+
// reset call count
|
|
330
|
+
callCountMap.set(stub, 0);
|
|
331
|
+
// set original mock payloads up
|
|
332
|
+
const payloads = mockPayloadsMap.get(stub);
|
|
333
|
+
if (payloads) {
|
|
334
|
+
setMockNetworkPayloads(stub, payloads);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function getMockNetworkAdapterCallCount(mockNetworkAdapter) {
|
|
338
|
+
const stub = mockNetworkAdapter;
|
|
339
|
+
return callCountMap.get(stub);
|
|
340
|
+
}
|
|
341
|
+
function buildFetchResponse(body, status = 200, statusText = 'Ok', headers = {}) {
|
|
342
|
+
return {
|
|
343
|
+
headers,
|
|
344
|
+
body,
|
|
345
|
+
status,
|
|
346
|
+
statusText,
|
|
347
|
+
ok: isOkResponse(status),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function buildSuccessMockPayload(networkArgs, body, headers = {}) {
|
|
351
|
+
const mockPayload = {
|
|
352
|
+
networkArgs,
|
|
353
|
+
response: buildFetchResponse(body, 200, 'Ok', headers),
|
|
354
|
+
};
|
|
355
|
+
return mockPayload;
|
|
356
|
+
}
|
|
357
|
+
function buildErrorMockPayload(networkArgs, body, errorStatusCode, errorStatusText, headers = {}) {
|
|
358
|
+
const mockPayload = {
|
|
359
|
+
networkArgs,
|
|
360
|
+
response: buildFetchResponse(body, errorStatusCode, errorStatusText, headers),
|
|
361
|
+
};
|
|
362
|
+
return mockPayload;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
const mockFulfilledSnapshot = { state: 'Fulfilled' };
|
|
366
|
-
const mockFulfilledSnapshotPromise = {
|
|
367
|
-
then() {
|
|
368
|
-
return mockFulfilledSnapshot;
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
const mockLuvio = {
|
|
372
|
-
storeLookup() {
|
|
373
|
-
return mockFulfilledSnapshot;
|
|
374
|
-
},
|
|
375
|
-
snapshotAvailable() {
|
|
376
|
-
return true;
|
|
377
|
-
},
|
|
378
|
-
dispatchResourceRequest() {
|
|
379
|
-
return mockFulfilledSnapshotPromise;
|
|
380
|
-
},
|
|
381
|
-
applyCachePolicy() {
|
|
382
|
-
return mockFulfilledSnapshot;
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
function getMockLuvioWithFulfilledSnapshot() {
|
|
386
|
-
return mockLuvio;
|
|
387
|
-
}
|
|
388
|
-
function getMockFulfilledSnapshot() {
|
|
389
|
-
return mockFulfilledSnapshot;
|
|
365
|
+
const mockFulfilledSnapshot = { state: 'Fulfilled' };
|
|
366
|
+
const mockFulfilledSnapshotPromise = {
|
|
367
|
+
then() {
|
|
368
|
+
return mockFulfilledSnapshot;
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
const mockLuvio = {
|
|
372
|
+
storeLookup() {
|
|
373
|
+
return mockFulfilledSnapshot;
|
|
374
|
+
},
|
|
375
|
+
snapshotAvailable() {
|
|
376
|
+
return true;
|
|
377
|
+
},
|
|
378
|
+
dispatchResourceRequest() {
|
|
379
|
+
return mockFulfilledSnapshotPromise;
|
|
380
|
+
},
|
|
381
|
+
applyCachePolicy() {
|
|
382
|
+
return mockFulfilledSnapshot;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
function getMockLuvioWithFulfilledSnapshot() {
|
|
386
|
+
return mockLuvio;
|
|
387
|
+
}
|
|
388
|
+
function getMockFulfilledSnapshot() {
|
|
389
|
+
return mockFulfilledSnapshot;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
function setupCustomLuvioMatchers() {
|
|
393
|
-
expect.extend(customMatchers);
|
|
392
|
+
function setupCustomLuvioMatchers() {
|
|
393
|
+
expect.extend(customMatchers);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
class MemoryDurableStorePersistence {
|
|
397
|
-
constructor() {
|
|
398
|
-
this.store = {};
|
|
399
|
-
}
|
|
400
|
-
async get(key) {
|
|
401
|
-
return this.store[key];
|
|
402
|
-
}
|
|
403
|
-
async set(key, value) {
|
|
404
|
-
// simulate a more realistic durable store by making the write wait a
|
|
405
|
-
// tick before actually setting the value
|
|
406
|
-
await flushPromises();
|
|
407
|
-
this.store[key] = value;
|
|
408
|
-
}
|
|
409
|
-
async delete(key) {
|
|
410
|
-
delete this.store[key];
|
|
411
|
-
}
|
|
412
|
-
async flushPendingWork() {
|
|
413
|
-
// since this implementation does actual "IO" synchronously the only
|
|
414
|
-
// thing necessary to await any pending IO is to flush the current
|
|
415
|
-
// microtask queue
|
|
416
|
-
await flushPromises();
|
|
417
|
-
}
|
|
396
|
+
class MemoryDurableStorePersistence {
|
|
397
|
+
constructor() {
|
|
398
|
+
this.store = {};
|
|
399
|
+
}
|
|
400
|
+
async get(key) {
|
|
401
|
+
return this.store[key];
|
|
402
|
+
}
|
|
403
|
+
async set(key, value) {
|
|
404
|
+
// simulate a more realistic durable store by making the write wait a
|
|
405
|
+
// tick before actually setting the value
|
|
406
|
+
await flushPromises();
|
|
407
|
+
this.store[key] = value;
|
|
408
|
+
}
|
|
409
|
+
async delete(key) {
|
|
410
|
+
delete this.store[key];
|
|
411
|
+
}
|
|
412
|
+
async flushPendingWork() {
|
|
413
|
+
// since this implementation does actual "IO" synchronously the only
|
|
414
|
+
// thing necessary to await any pending IO is to flush the current
|
|
415
|
+
// microtask queue
|
|
416
|
+
await flushPromises();
|
|
417
|
+
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
function waitForPromiseSet(set) {
|
|
421
|
-
// NOTE: we are building an array from the Set at this point in time. If
|
|
422
|
-
// more Promises are added to the Set while this is awaiting it won't
|
|
423
|
-
// await the newly-added Promise. That's what we want.
|
|
424
|
-
return Promise.all(Array.from(set)).then();
|
|
425
|
-
}
|
|
426
|
-
class MockDurableStore {
|
|
427
|
-
constructor(persistence) {
|
|
428
|
-
// for read/write synchronization
|
|
429
|
-
this.writePromises = new Set();
|
|
430
|
-
this.listeners = new Set();
|
|
431
|
-
this.persistence = persistence || new MemoryDurableStorePersistence();
|
|
432
|
-
}
|
|
433
|
-
async getEntries(entryIds, segment) {
|
|
434
|
-
// await any current write operations
|
|
435
|
-
if (this.writePromises.size > 0) {
|
|
436
|
-
await waitForPromiseSet(this.writePromises);
|
|
437
|
-
}
|
|
438
|
-
const entries = await this.persistence.get(segment);
|
|
439
|
-
if (entries === undefined) {
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
const returnSource = Object.create(null);
|
|
443
|
-
for (const entryId of entryIds) {
|
|
444
|
-
const entry = entries[entryId];
|
|
445
|
-
if (entry !== undefined) {
|
|
446
|
-
returnSource[entryId] = clone(entry);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return returnSource;
|
|
450
|
-
}
|
|
451
|
-
async getAllEntries(segment) {
|
|
452
|
-
// await any current write operations
|
|
453
|
-
if (this.writePromises.size > 0) {
|
|
454
|
-
await waitForPromiseSet(this.writePromises);
|
|
455
|
-
}
|
|
456
|
-
const entries = await this.persistence.get(segment);
|
|
457
|
-
return entries;
|
|
458
|
-
}
|
|
459
|
-
setEntries(entries, segment) {
|
|
460
|
-
return this.batchOperations([{ entries, segment, type: 'setEntries' }]);
|
|
461
|
-
}
|
|
462
|
-
evictEntries(ids, segment) {
|
|
463
|
-
return this.batchOperations([{ ids, segment, type: 'evictEntries' }]);
|
|
464
|
-
}
|
|
465
|
-
registerOnChangedListener(listener) {
|
|
466
|
-
this.listeners.add(listener);
|
|
467
|
-
return () => {
|
|
468
|
-
this.listeners.delete(listener);
|
|
469
|
-
return Promise.resolve();
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
async batchOperations(operations) {
|
|
473
|
-
// await any current write operations
|
|
474
|
-
if (this.writePromises.size > 0) {
|
|
475
|
-
await waitForPromiseSet(this.writePromises);
|
|
476
|
-
}
|
|
477
|
-
const changes = [];
|
|
478
|
-
const writePromise = (async () => {
|
|
479
|
-
for (const operation of operations) {
|
|
480
|
-
changes.push(await this.performOperation(operation));
|
|
481
|
-
}
|
|
482
|
-
})();
|
|
483
|
-
this.writePromises.add(writePromise);
|
|
484
|
-
try {
|
|
485
|
-
await writePromise;
|
|
486
|
-
}
|
|
487
|
-
finally {
|
|
488
|
-
this.writePromises.delete(writePromise);
|
|
489
|
-
}
|
|
490
|
-
const promises = [];
|
|
491
|
-
this.listeners.forEach((listener) => {
|
|
492
|
-
promises.push(listener(changes));
|
|
493
|
-
});
|
|
494
|
-
return Promise.all(promises).then(() => undefined);
|
|
495
|
-
}
|
|
496
|
-
async performOperation(operation) {
|
|
497
|
-
const segment = operation.segment;
|
|
498
|
-
const rawEntries = await this.persistence.get(segment);
|
|
499
|
-
const entries = rawEntries === undefined ? {} : rawEntries;
|
|
500
|
-
let ids = [];
|
|
501
|
-
switch (operation.type) {
|
|
502
|
-
case 'setEntries':
|
|
503
|
-
ids = Object.keys(operation.entries);
|
|
504
|
-
ids.forEach((id) => {
|
|
505
|
-
entries[id] = clone(operation.entries[id]);
|
|
506
|
-
});
|
|
507
|
-
break;
|
|
508
|
-
case 'evictEntries':
|
|
509
|
-
ids = operation.ids;
|
|
510
|
-
ids.forEach((id) => {
|
|
511
|
-
delete entries[id];
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
await this.persistence.set(operation.segment, entries);
|
|
515
|
-
return { ids, segment, type: operation.type };
|
|
516
|
-
}
|
|
517
|
-
async flushPendingOperations() {
|
|
518
|
-
// flush any pending read operations
|
|
519
|
-
await this.persistence.flushPendingWork();
|
|
520
|
-
// wait for any pending writes to finish
|
|
521
|
-
while (this.writePromises.size > 0) {
|
|
522
|
-
await waitForPromiseSet(this.writePromises);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
420
|
+
function waitForPromiseSet(set) {
|
|
421
|
+
// NOTE: we are building an array from the Set at this point in time. If
|
|
422
|
+
// more Promises are added to the Set while this is awaiting it won't
|
|
423
|
+
// await the newly-added Promise. That's what we want.
|
|
424
|
+
return Promise.all(Array.from(set)).then();
|
|
425
|
+
}
|
|
426
|
+
class MockDurableStore {
|
|
427
|
+
constructor(persistence) {
|
|
428
|
+
// for read/write synchronization
|
|
429
|
+
this.writePromises = new Set();
|
|
430
|
+
this.listeners = new Set();
|
|
431
|
+
this.persistence = persistence || new MemoryDurableStorePersistence();
|
|
432
|
+
}
|
|
433
|
+
async getEntries(entryIds, segment) {
|
|
434
|
+
// await any current write operations
|
|
435
|
+
if (this.writePromises.size > 0) {
|
|
436
|
+
await waitForPromiseSet(this.writePromises);
|
|
437
|
+
}
|
|
438
|
+
const entries = await this.persistence.get(segment);
|
|
439
|
+
if (entries === undefined) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const returnSource = Object.create(null);
|
|
443
|
+
for (const entryId of entryIds) {
|
|
444
|
+
const entry = entries[entryId];
|
|
445
|
+
if (entry !== undefined) {
|
|
446
|
+
returnSource[entryId] = clone(entry);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return returnSource;
|
|
450
|
+
}
|
|
451
|
+
async getAllEntries(segment) {
|
|
452
|
+
// await any current write operations
|
|
453
|
+
if (this.writePromises.size > 0) {
|
|
454
|
+
await waitForPromiseSet(this.writePromises);
|
|
455
|
+
}
|
|
456
|
+
const entries = await this.persistence.get(segment);
|
|
457
|
+
return entries;
|
|
458
|
+
}
|
|
459
|
+
setEntries(entries, segment) {
|
|
460
|
+
return this.batchOperations([{ entries, segment, type: 'setEntries' }]);
|
|
461
|
+
}
|
|
462
|
+
evictEntries(ids, segment) {
|
|
463
|
+
return this.batchOperations([{ ids, segment, type: 'evictEntries' }]);
|
|
464
|
+
}
|
|
465
|
+
registerOnChangedListener(listener) {
|
|
466
|
+
this.listeners.add(listener);
|
|
467
|
+
return () => {
|
|
468
|
+
this.listeners.delete(listener);
|
|
469
|
+
return Promise.resolve();
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async batchOperations(operations) {
|
|
473
|
+
// await any current write operations
|
|
474
|
+
if (this.writePromises.size > 0) {
|
|
475
|
+
await waitForPromiseSet(this.writePromises);
|
|
476
|
+
}
|
|
477
|
+
const changes = [];
|
|
478
|
+
const writePromise = (async () => {
|
|
479
|
+
for (const operation of operations) {
|
|
480
|
+
changes.push(await this.performOperation(operation));
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
483
|
+
this.writePromises.add(writePromise);
|
|
484
|
+
try {
|
|
485
|
+
await writePromise;
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
this.writePromises.delete(writePromise);
|
|
489
|
+
}
|
|
490
|
+
const promises = [];
|
|
491
|
+
this.listeners.forEach((listener) => {
|
|
492
|
+
promises.push(listener(changes));
|
|
493
|
+
});
|
|
494
|
+
return Promise.all(promises).then(() => undefined);
|
|
495
|
+
}
|
|
496
|
+
async performOperation(operation) {
|
|
497
|
+
const segment = operation.segment;
|
|
498
|
+
const rawEntries = await this.persistence.get(segment);
|
|
499
|
+
const entries = rawEntries === undefined ? {} : rawEntries;
|
|
500
|
+
let ids = [];
|
|
501
|
+
switch (operation.type) {
|
|
502
|
+
case 'setEntries':
|
|
503
|
+
ids = Object.keys(operation.entries);
|
|
504
|
+
ids.forEach((id) => {
|
|
505
|
+
entries[id] = clone(operation.entries[id]);
|
|
506
|
+
});
|
|
507
|
+
break;
|
|
508
|
+
case 'evictEntries':
|
|
509
|
+
ids = operation.ids;
|
|
510
|
+
ids.forEach((id) => {
|
|
511
|
+
delete entries[id];
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
await this.persistence.set(operation.segment, entries);
|
|
515
|
+
return { ids, segment, type: operation.type };
|
|
516
|
+
}
|
|
517
|
+
async flushPendingOperations() {
|
|
518
|
+
// flush any pending read operations
|
|
519
|
+
await this.persistence.flushPendingWork();
|
|
520
|
+
// wait for any pending writes to finish
|
|
521
|
+
while (this.writePromises.size > 0) {
|
|
522
|
+
await waitForPromiseSet(this.writePromises);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
// Use this util if you want to configure multiple network mocks in your test code.
|
|
528
|
-
// If you're only mocking one, we suggest you use buildLuvioWithNetwork instead.
|
|
529
|
-
function buildLuvioInstance(mockNetworkAdapter) {
|
|
530
|
-
const store = new InMemoryStore();
|
|
531
|
-
const env = new Environment(store, mockNetworkAdapter);
|
|
532
|
-
return new Luvio(env);
|
|
533
|
-
}
|
|
534
|
-
async function testAdapterCompletesRoundtrip(params) {
|
|
535
|
-
const adapterSetup = setupAdapter(params);
|
|
536
|
-
const result = await callAdapter(adapterSetup, params);
|
|
537
|
-
expect(result.data).toBeDefined();
|
|
538
|
-
expect(result).toEqualFulfilledSnapshotWithData(params.type === 'network'
|
|
539
|
-
? params.expectedData || params.network.response
|
|
540
|
-
: params.expectedData, params.privateProperties);
|
|
541
|
-
}
|
|
542
|
-
function setupAdapter(params) {
|
|
543
|
-
const luvio = params.type === 'network' ? buildLuvioWithNetwork(params.network).luvio : params.luvio;
|
|
544
|
-
return params.adapterFactory(luvio);
|
|
545
|
-
}
|
|
546
|
-
// Returns a tuple of the Luvio instance and network adapter so the test author may expect a number expected calls to the network.
|
|
547
|
-
function buildLuvioWithNetwork(networkConfig) {
|
|
548
|
-
// TODO: handle alternative expected network calls, errors and etc
|
|
549
|
-
const expectedNetwork = [
|
|
550
|
-
buildSuccessMockPayload(networkConfig.request, networkConfig.response),
|
|
551
|
-
];
|
|
552
|
-
const mockNetworkAdapter = buildMockNetworkAdapter(expectedNetwork);
|
|
553
|
-
return { luvio: buildLuvioInstance(mockNetworkAdapter), networkAdapter: mockNetworkAdapter };
|
|
554
|
-
}
|
|
555
|
-
async function callAdapter(adapter, params) {
|
|
556
|
-
const snapshotOrPromise = adapter(params.adapterConfig);
|
|
557
|
-
if (snapshotOrPromise === null) {
|
|
558
|
-
throw new Error('Result of calling the adapter was null. Is the adapter config valid?');
|
|
559
|
-
}
|
|
560
|
-
// Cache Miss - Check that the network call was right.
|
|
561
|
-
if (params.type === 'injectedLuvioNetwork' || params.type === 'network') {
|
|
562
|
-
expect(snapshotOrPromise).toBeInstanceOf(Promise);
|
|
563
|
-
return snapshotOrPromise;
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
// Cache hit
|
|
567
|
-
expect(snapshotOrPromise).not.toBeInstanceOf(Promise); // Expect cache hit to be synchronous
|
|
568
|
-
return Promise.resolve(snapshotOrPromise);
|
|
569
|
-
}
|
|
527
|
+
// Use this util if you want to configure multiple network mocks in your test code.
|
|
528
|
+
// If you're only mocking one, we suggest you use buildLuvioWithNetwork instead.
|
|
529
|
+
function buildLuvioInstance(mockNetworkAdapter) {
|
|
530
|
+
const store = new InMemoryStore();
|
|
531
|
+
const env = new Environment(store, mockNetworkAdapter);
|
|
532
|
+
return new Luvio(env);
|
|
533
|
+
}
|
|
534
|
+
async function testAdapterCompletesRoundtrip(params) {
|
|
535
|
+
const adapterSetup = setupAdapter(params);
|
|
536
|
+
const result = await callAdapter(adapterSetup, params);
|
|
537
|
+
expect(result.data).toBeDefined();
|
|
538
|
+
expect(result).toEqualFulfilledSnapshotWithData(params.type === 'network'
|
|
539
|
+
? params.expectedData || params.network.response
|
|
540
|
+
: params.expectedData, params.privateProperties);
|
|
541
|
+
}
|
|
542
|
+
function setupAdapter(params) {
|
|
543
|
+
const luvio = params.type === 'network' ? buildLuvioWithNetwork(params.network).luvio : params.luvio;
|
|
544
|
+
return params.adapterFactory(luvio);
|
|
545
|
+
}
|
|
546
|
+
// Returns a tuple of the Luvio instance and network adapter so the test author may expect a number expected calls to the network.
|
|
547
|
+
function buildLuvioWithNetwork(networkConfig) {
|
|
548
|
+
// TODO: handle alternative expected network calls, errors and etc
|
|
549
|
+
const expectedNetwork = [
|
|
550
|
+
buildSuccessMockPayload(networkConfig.request, networkConfig.response),
|
|
551
|
+
];
|
|
552
|
+
const mockNetworkAdapter = buildMockNetworkAdapter(expectedNetwork);
|
|
553
|
+
return { luvio: buildLuvioInstance(mockNetworkAdapter), networkAdapter: mockNetworkAdapter };
|
|
554
|
+
}
|
|
555
|
+
async function callAdapter(adapter, params) {
|
|
556
|
+
const snapshotOrPromise = adapter(params.adapterConfig);
|
|
557
|
+
if (snapshotOrPromise === null) {
|
|
558
|
+
throw new Error('Result of calling the adapter was null. Is the adapter config valid?');
|
|
559
|
+
}
|
|
560
|
+
// Cache Miss - Check that the network call was right.
|
|
561
|
+
if (params.type === 'injectedLuvioNetwork' || params.type === 'network') {
|
|
562
|
+
expect(snapshotOrPromise).toBeInstanceOf(Promise);
|
|
563
|
+
return snapshotOrPromise;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
// Cache hit
|
|
567
|
+
expect(snapshotOrPromise).not.toBeInstanceOf(Promise); // Expect cache hit to be synchronous
|
|
568
|
+
return Promise.resolve(snapshotOrPromise);
|
|
569
|
+
}
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
export { ConnectivityState, MemoryDurableStorePersistence, MockDurableStore, buildErrorMockPayload, buildFetchResponse, buildLuvioInstance, buildLuvioWithNetwork, buildMockNetworkAdapter, buildSuccessMockPayload, customMatchers, flushPendingNetworkRequests, getMockFulfilledSnapshot, getMockLuvioWithFulfilledSnapshot, getMockNetworkAdapterCallCount, isImmutable, overrideMockNetworkResponses, resetMockNetworkAdapter, setMockNetworkPayloads, setNetworkConnectivity, setupCustomLuvioMatchers, stripProperties, testAdapterCompletesRoundtrip, verifyImmutable };
|