Skip to content

Commit

Permalink
e2e: terminate app only when all network requests were finished
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillzyusko committed Jun 4, 2024
1 parent 1d5f83d commit 25cecda
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/libs/E2E/tests/appStartTimeTest.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {PerformanceEntry} from 'react-native-performance';
import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor';
import Performance from '@libs/Performance';

const test = () => {
Expand Down Expand Up @@ -30,6 +31,7 @@ const test = () => {
}),
),
)
.then(waitForActiveRequestsToBeEmpty)
.then(() => {
console.debug('[E2E] Done, exiting…');
E2EClient.submitTestDone();
Expand Down
11 changes: 7 additions & 4 deletions src/libs/E2E/tests/chatOpeningTest.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve';
import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor';
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import CONST from '@src/CONST';
Expand All @@ -29,11 +30,13 @@ const test = (config: NativeConfig) => {
const [renderChatPromise, renderChatResolve] = getPromiseWithResolve();
const [chatTTIPromise, chatTTIResolve] = getPromiseWithResolve();

Promise.all([renderChatPromise, chatTTIPromise]).then(() => {
console.debug(`[E2E] Submitting!`);
Promise.all([renderChatPromise, chatTTIPromise])
.then(waitForActiveRequestsToBeEmpty)
.then(() => {
console.debug(`[E2E] Submitting!`);

E2EClient.submitTestDone();
});
E2EClient.submitTestDone();
});

Performance.subscribeToMeasurements((entry) => {
if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
Expand Down
2 changes: 2 additions & 0 deletions src/libs/E2E/tests/linkingTest.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor';
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -51,6 +52,7 @@ const test = (config: NativeConfig) => {
name: 'Comment linking',
duration: entry.duration,
})
.then(waitForActiveRequestsToBeEmpty)
.then(() => {
console.debug('[E2E] Test completed successfully, exiting…');
E2EClient.submitTestDone();
Expand Down
11 changes: 7 additions & 4 deletions src/libs/E2E/tests/openChatFinderPageTest.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve';
import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor';
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import CONST from '@src/CONST';
Expand All @@ -25,11 +26,13 @@ const test = () => {
const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve();
const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve();

Promise.all([openSearchPagePromise, loadSearchOptionsPromise]).then(() => {
console.debug(`[E2E] Submitting!`);
Promise.all([openSearchPagePromise, loadSearchOptionsPromise])
.then(waitForActiveRequestsToBeEmpty)
.then(() => {
console.debug(`[E2E] Submitting!`);

E2EClient.submitTestDone();
});
E2EClient.submitTestDone();
});

Performance.subscribeToMeasurements((entry) => {
if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
Expand Down
5 changes: 4 additions & 1 deletion src/libs/E2E/tests/reportTypingTest.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard';
import E2EClient from '@libs/E2E/client';
import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor';
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import {getRerenderCount, resetRerenderCount} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e';
Expand Down Expand Up @@ -54,7 +55,9 @@ const test = (config: NativeConfig) => {
branch: Config.E2E_BRANCH,
name: 'Composer typing rerender count',
renderCount: rerenderCount,
}).then(E2EClient.submitTestDone);
})
.then(waitForActiveRequestsToBeEmpty)
.then(E2EClient.submitTestDone);
}, 3000);
})
.catch((error) => {
Expand Down
40 changes: 40 additions & 0 deletions src/libs/E2E/utils/NetworkInterceptor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @lwc/lwc/no-async-await */
import {DeviceEventEmitter} from 'react-native';
import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types';

const LOG_TAG = `[E2E][NetworkInterceptor]`;
Expand Down Expand Up @@ -93,6 +94,33 @@ function hashFetchArgs(args: Parameters<typeof fetch>) {
return `${url}${JSON.stringify(headers)}`;
}

let activeRequestsCount = 0;

const ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT = 'activeRequestsQueueIsEmpty';

/**
* Assures that ongoing network requests are empty. **Highly desirable** to call this function before closing the app.
* Otherwise if some requests are persisted - they will be executed on the next app start. And it can lead to a situation
* where we can have `N * M` requests (where `N` is the number of app run per test and `M` is the number of test suites)
* and such big amount of requests can lead to a situation, where first app run (in test suite to cache network requests)
* may be blocked by spinners and lead to unbelievable big time execution, which eventually will be bigger than timeout and
* will lead to a test failure.
*/
function waitForActiveRequestsToBeEmpty(): Promise<void> {
console.debug('Waiting for requests queue to be empty...', activeRequestsCount);

if (activeRequestsCount === 0) {
return Promise.resolve();
}

return new Promise((resolve) => {
const subscription = DeviceEventEmitter.addListener(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT, () => {
subscription.remove();
resolve();
});
});
}

/**
* Install a network interceptor by overwriting the global fetch function:
* - Overwrites fetch globally with a custom implementation
Expand Down Expand Up @@ -145,6 +173,8 @@ export default function installNetworkInterceptor(
console.debug('!!! Missed cache hit for url:', url);
}

activeRequestsCount++;

return originalFetch(...args)
.then(async (res) => {
if (networkCache != null) {
Expand All @@ -166,6 +196,16 @@ export default function installNetworkInterceptor(
.then((res) => {
console.debug(LOG_TAG, 'Network cache updated!');
return res;
})
.finally(() => {
console.debug('Active requests count:', activeRequestsCount);

activeRequestsCount--;

if (activeRequestsCount === 0) {
DeviceEventEmitter.emit(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT);
}
});
};
}
export {waitForActiveRequestsToBeEmpty};
1 change: 0 additions & 1 deletion tests/e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export default {
TESTS_CONFIG: {
[TEST_NAMES.AppStartTime]: {
name: TEST_NAMES.AppStartTime,
warmupRuns: 1,
// ... any additional config you might need
},
[TEST_NAMES.OpenChatFinderPage]: {
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/testRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const runTests = async (): Promise<void> => {
// by default we do 2 warmups:
// - first warmup to pass a login flow
// - second warmup to pass an actual flow and cache network requests
const iterations = test.warmupRuns ?? 2;
const iterations = 2;
for (let i = 0; i < iterations; i++) {
// Warmup the main app:
await runTestIteration(config.MAIN_APP_PACKAGE, `[MAIN] ${warmupText}. Iteration ${i + 1}/${iterations}`);
Expand Down

0 comments on commit 25cecda

Please sign in to comment.