Skip to content

Commit 1108978

Browse files
chiciozoontek
authored andcommitted
loadBundleFromServer test: remove legacy Jest timers, increased coverage and improved assertions (facebook#55863)
Summary: This PR removes the dependency on Jest legacy fake timers from the `loadBundleFromServer` test suite. Together with facebook#55757, this should remove the last usages of `jest.useFakeTimers({legacyFakeTimers: true});` in the codebase. In this PR in particular, instead of migrating to modern timers, I refactored the `addListener` mock to use a Promise based approach. By leveraging microtask scheduling, the tests simulate asynchronous functions flushed after the execution of the `sendRequest` without relying on timers. In addition, I also improved the tests in other areas: * improved readability by extracting a constant for the request id, used in both `sendRequest` and `addListener` mocks * improved errors assertions to explicitly verify the errors are thrown (before the refactoring these tests could still be green if no error was thrown, because the expectations were only in the catch clause that could be skipped) * improved the tests that verify the successful bundle load: * spit long test in ad hoc isolated ones * added listeners registration and cleanup verification to improve the coverage * added more fine-grained check for the `sendRequest` parameters I tried to separate all the changes above in specific commit so that the review can check the incremental improvements I applied with the refactoring steps. ## Changelog: [GENERAL] [CHANGED] - loadBundleFromServer test: remove legacy Jest timers, increased coverage and improved assertions Pull Request resolved: facebook#55863 Test Plan: * Ran loadBundleFromServer-test and verified all tests cases passed * Ran the React Native test suite to ensure all tests pass * Verified test correctness by intentionally breaking production code after fixing the tests (eg. remove observer/change listener code) Reviewed By: cortinico Differential Revision: D95034650 Pulled By: vzaidman fbshipit-source-id: f6ae7cbc95ccfe606c1054c9044bf733e41bc2f9
1 parent f89b505 commit 1108978

File tree

1 file changed

+70
-49
lines changed

1 file changed

+70
-49
lines changed

packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010

1111
'use strict';
1212

13-
// TODO(legacy-fake-timers): Fix these tests to work with modern timers.
14-
jest.useFakeTimers({legacyFakeTimers: true});
13+
import {
14+
LoadBundleFromServerError,
15+
LoadBundleFromServerRequestError,
16+
} from '../loadBundleFromServer';
17+
import invariant from 'invariant';
1518

1619
jest.mock('../../../Utilities/HMRClient');
1720

@@ -36,6 +39,8 @@ jest.mock('../../../Utilities/DevLoadingView', () => ({
3639
default: loadingViewMock,
3740
}));
3841

42+
const REQUEST_ID = 1;
43+
3944
const sendRequest = jest.fn(
4045
(
4146
method,
@@ -49,32 +54,31 @@ const sendRequest = jest.fn(
4954
callback,
5055
withCredentials,
5156
) => {
52-
callback(1);
57+
callback(REQUEST_ID);
5358
},
5459
);
5560

61+
const removeListener = jest.fn();
62+
const addListener = jest.fn((name, fn) => {
63+
if (name === 'didReceiveNetworkData') {
64+
void Promise.resolve().then(() => fn([REQUEST_ID, mockDataResponse]));
65+
} else if (name === 'didCompleteNetworkResponse') {
66+
void Promise.resolve().then(() => fn([REQUEST_ID, mockRequestError]));
67+
} else if (name === 'didReceiveNetworkResponse') {
68+
void Promise.resolve().then(() => fn([REQUEST_ID, null, mockHeaders]));
69+
}
70+
return {remove: removeListener};
71+
});
72+
5673
jest.mock('../../../Network/RCTNetworking', () => ({
5774
__esModule: true,
5875
default: {
5976
sendRequest,
60-
addListener: jest.fn((name, fn) => {
61-
if (name === 'didReceiveNetworkData') {
62-
setImmediate(() => fn([1, mockDataResponse]));
63-
} else if (name === 'didCompleteNetworkResponse') {
64-
setImmediate(() => fn([1, mockRequestError]));
65-
} else if (name === 'didReceiveNetworkResponse') {
66-
setImmediate(() => fn([1, null, mockHeaders]));
67-
}
68-
return {remove: () => {}};
69-
}),
77+
addListener,
7078
},
7179
}));
7280

7381
let loadBundleFromServer: (bundlePathAndQuery: string) => Promise<void>;
74-
const {
75-
LoadBundleFromServerError,
76-
LoadBundleFromServerRequestError,
77-
} = require('../loadBundleFromServer');
7882

7983
let mockHeaders: {'Content-Type': string};
8084
let mockDataResponse;
@@ -91,30 +95,36 @@ test('loadBundleFromServer will throw for JSON responses', async () => {
9195
mockDataResponse = JSON.stringify({message: 'Error thrown from Metro'});
9296
mockRequestError = null;
9397

94-
try {
95-
await loadBundleFromServer('/Fail.bundle?platform=ios');
96-
} catch (e) {
97-
expect(e).toBeInstanceOf(LoadBundleFromServerError);
98-
expect(e.message).toBe('Could not load bundle');
99-
expect(e.cause).toBe('Error thrown from Metro');
100-
}
98+
const error: mixed = await loadBundleFromServer(
99+
'/Fail.bundle?platform=ios',
100+
).catch(e => e);
101+
102+
invariant(
103+
error instanceof LoadBundleFromServerError,
104+
'Expected a LoadBundleFromServerError',
105+
);
106+
expect(error.message).toBe('Could not load bundle');
107+
expect(error.cause).toBe('Error thrown from Metro');
101108
});
102109

103110
test('loadBundleFromServer will throw LoadBundleFromServerError for request errors', async () => {
104111
mockHeaders = {'Content-Type': 'application/json'};
105112
mockDataResponse = '';
106113
mockRequestError = 'Some error';
107114

108-
try {
109-
await loadBundleFromServer('/Fail.bundle?platform=ios');
110-
} catch (e) {
111-
expect(e).toBeInstanceOf(LoadBundleFromServerRequestError);
112-
expect(e.message).toBe('Could not load bundle');
113-
expect(e.cause).toBe('Some error');
114-
}
115+
const error: mixed = await loadBundleFromServer(
116+
'/Fail.bundle?platform=ios',
117+
).catch(e => e);
118+
119+
invariant(
120+
error instanceof LoadBundleFromServerRequestError,
121+
'Expected a LoadBundleFromServerRequestError',
122+
);
123+
expect(error.message).toBe('Could not load bundle');
124+
expect(error.cause).toBe('Some error');
115125
});
116126

117-
test('loadBundleFromServer will request a bundle if import bundles are available', async () => {
127+
test('loadBundleFromServer successfully requests a bundle (root bundle)', async () => {
118128
mockDataResponse = '"code";';
119129
mockHeaders = {'Content-Type': 'application/javascript'};
120130
mockRequestError = null;
@@ -126,37 +136,48 @@ test('loadBundleFromServer will request a bundle if import bundles are available
126136
expect(sendRequest.mock.calls).toEqual([
127137
[
128138
'GET',
129-
expect.anything(),
139+
'asyncRequest',
130140
'localhost:8042/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false',
131-
expect.anything(),
132-
expect.anything(),
133-
expect.anything(),
134-
expect.anything(),
135-
expect.anything(),
136-
expect.anything(),
137-
expect.anything(),
141+
{},
142+
'',
143+
'text',
144+
true,
145+
0,
146+
expect.any(Function),
147+
true,
138148
],
139149
]);
140150

141-
sendRequest.mockClear();
151+
expect(addListener).toHaveBeenCalledTimes(4);
152+
expect(removeListener).toHaveBeenCalledTimes(addListener.mock.calls.length);
153+
});
154+
155+
test('loadBundleFromServer successfully requests a bundle (subdir bundle)', async () => {
156+
mockDataResponse = '"code";';
157+
mockHeaders = {'Content-Type': 'application/javascript'};
158+
mockRequestError = null;
159+
142160
await loadBundleFromServer(
143161
'/Tiny/Apple.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false',
144162
);
145163

146164
expect(sendRequest.mock.calls).toEqual([
147165
[
148166
'GET',
149-
expect.anything(),
167+
'asyncRequest',
150168
'localhost:8042/Tiny/Apple.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false',
151-
expect.anything(),
152-
expect.anything(),
153-
expect.anything(),
154-
expect.anything(),
155-
expect.anything(),
156-
expect.anything(),
157-
expect.anything(),
169+
{},
170+
'',
171+
'text',
172+
true,
173+
0,
174+
expect.any(Function),
175+
true,
158176
],
159177
]);
178+
179+
expect(addListener).toHaveBeenCalledTimes(4);
180+
expect(removeListener).toHaveBeenCalledTimes(addListener.mock.calls.length);
160181
});
161182

162183
test('shows and hides the loading view around a request', async () => {

0 commit comments

Comments
 (0)