diff --git a/README.md b/README.md index 1f2e2de6..0534ac19 100644 --- a/README.md +++ b/README.md @@ -341,20 +341,20 @@ This parameter can be overridden in the test-specific options. If the test run takes longer than this timeout, the test fails and rerun on the next retry. This parameter can be overridden in the test-specific options. -`waitForAllRequestsComplete.firstRequestTimeout: number`: default timeout (in milliseconds) for -first request for waitForAllRequestsComplete function. -If before the expiration of this timeout there is no request that satisfies the predicate, -then the promise returned by the waitForAllRequestsComplete function will be successfully resolved. +`waitForAllRequestsComplete.maxIntervalBetweenRequestsInMs: number`: default maximum interval +(in milliseconds) between requests for `waitForAllRequestsComplete` function. +If there are no new requests for more than this interval, then the promise +returned by the `waitForAllRequestsComplete` function will be successfully resolved. -`waitForAllRequestsComplete.timeout: number`: default timeout (in milliseconds) for waitForAllRequestsComplete function. +`waitForAllRequestsComplete.timeout: number`: default timeout (in milliseconds) for `waitForAllRequestsComplete` function. If the wait is longer than this timeout, then the promise -returned by the waitForAllRequestsComplete function will be rejected. +returned by the `waitForAllRequestsComplete` function will be rejected. -`waitForRequestTimeout: number`: default timeout (in milliseconds) for waitForRequest function. -If the wait is longer than this timeout, then the promise returned by the waitForRequest function will be rejected. +`waitForRequestTimeout: number`: default timeout (in milliseconds) for `waitForRequest` function. +If the wait is longer than this timeout, then the promise returned by the `waitForRequest` function will be rejected. -`waitForResponseTimeout: number`: default timeout (in milliseconds) for waitForResponse function. -If the wait is longer than this timeout, then the promise returned by the waitForResponse function will be rejected. +`waitForResponseTimeout: number`: default timeout (in milliseconds) for `waitForResponse` function. +If the wait is longer than this timeout, then the promise returned by the `waitForResponse` function will be rejected. ### Environment variables diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index c48069d3..2cc4a927 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -57,7 +57,7 @@ export const pack: Pack = { testIdleTimeout: 20_000, testTimeout: 60_000, waitForAllRequestsComplete: { - firstRequestTimeout: 500, + maxIntervalBetweenRequestsInMs: 500, timeout: 30_000, }, waitForRequestTimeout: 30_000, diff --git a/autotests/tests/main/exists.ts b/autotests/tests/main/exists.ts index 5e1305e0..18b4f728 100644 --- a/autotests/tests/main/exists.ts +++ b/autotests/tests/main/exists.ts @@ -11,7 +11,7 @@ import { waitForRequest, waitForResponse, } from 'e2ed/actions'; -import {getDocumentUrl} from 'e2ed/utils'; +import {E2edError, getDocumentUrl} from 'e2ed/utils'; const language = 'en'; const searchQuery = 'foo'; @@ -23,7 +23,7 @@ it('exists', {meta: {testId: '1'}, testIdleTimeout: 35_000, testTimeout: 90_000} .eql(2) .then( () => { - throw new Error('the "expect" function did not throw an error'); + throw new E2edError('the "expect" function did not throw an error'); }, () => undefined, ); diff --git a/autotests/tests/notInAllTestsPack.ts b/autotests/tests/notInAllTestsPack.ts index 4f5579ae..d3506d27 100644 --- a/autotests/tests/notInAllTestsPack.ts +++ b/autotests/tests/notInAllTestsPack.ts @@ -3,7 +3,8 @@ */ import {it} from 'autotests'; +import {E2edError} from 'e2ed/utils'; it('not in allTests pack', {meta: {testId: '13'}}, () => { - throw new Error('Test filtered from the pack "allTests" was running'); + throw new E2edError('Test filtered from the pack "allTests" was running'); }); diff --git a/autotests/tests/request.ts b/autotests/tests/request.ts index f7372374..26429906 100644 --- a/autotests/tests/request.ts +++ b/autotests/tests/request.ts @@ -1,7 +1,7 @@ import {it} from 'autotests'; import {GetUsers} from 'autotests/routes/apiRoutes'; import {expect} from 'e2ed'; -import {request} from 'e2ed/utils'; +import {E2edError, request} from 'e2ed/utils'; it( 'send correct requests and rejects on timeout', @@ -15,7 +15,7 @@ it( await request(GetUsers, {maxRetriesCount: 1, timeout: 2_000}).then( () => { - throw new Error('the "request" function did not throw timeout error'); + throw new E2edError('the "request" function did not throw timeout error'); }, () => undefined, ); diff --git a/autotests/tests/waitForAllRequestsComplete.ts b/autotests/tests/waitForAllRequestsComplete.ts index b9476f00..f005555e 100644 --- a/autotests/tests/waitForAllRequestsComplete.ts +++ b/autotests/tests/waitForAllRequestsComplete.ts @@ -1,25 +1,29 @@ import {it} from 'autotests'; import {createClientFunction} from 'e2ed'; -import {waitForAllRequestsComplete} from 'e2ed/actions'; -import {log} from 'e2ed/utils'; +import {waitForAllRequestsComplete, waitForTimeout} from 'e2ed/actions'; +import {E2edError, log} from 'e2ed/utils'; it( 'waitForAllRequestsComplete works correct with timeout and predicate in base cases', - {meta: {testId: '9'}, testIdleTimeout: 3_000}, + {meta: {testId: '9'}, testIdleTimeout: 6_000}, + // eslint-disable-next-line complexity, max-statements async () => { let startRequestInMs = Date.now(); - await waitForAllRequestsComplete(() => true, {firstRequestTimeout: 300}); + await waitForAllRequestsComplete(() => true, {maxIntervalBetweenRequestsInMs: 300}); let waitedInMs = Date.now() - startRequestInMs; if (waitedInMs < 300 || waitedInMs > 400) { - throw new Error('waitForAllRequestsComplete did not wait for firstRequestTimeout'); + throw new E2edError( + 'waitForAllRequestsComplete did not wait for maxIntervalBetweenRequestsInMs in the beginning', + {waitedInMs}, + ); } await waitForAllRequestsComplete(() => true, {timeout: 100}).then( () => { - throw new Error('waitForAllRequestsComplete did not throw an error after timeout'); + throw new E2edError('waitForAllRequestsComplete did not throw an error after timeout'); }, (error: unknown) => { log('Catch error from waitForAllRequestsComplete for {timeout: 100}', {error}); @@ -30,18 +34,23 @@ it( startRequestInMs = Date.now(); - const promise = waitForAllRequestsComplete(() => true, {timeout: 1000}); + let promise = waitForAllRequestsComplete(() => true, {timeout: 1000}); - const getUsers = createClientFunction( - () => fetch('https://reqres.in/api/users?delay=2', {method: 'GET'}), - {name: 'getUsers', timeout: 3_000}, + const clientGetUsers = createClientFunction( + (delay: number) => fetch(`https://reqres.in/api/users?delay=${delay}`, {method: 'GET'}), + {name: 'getUsers', timeout: 6_000}, ); + const getUsers = (delay: number): Promise => { + log(`Send API request with delay = ${delay}000ms`); + + return clientGetUsers(delay); + }; - void getUsers(); + void getUsers(2); await promise.then( () => { - throw new Error( + throw new E2edError( 'waitForAllRequestsComplete did not throw an error after timeout with real request', ); }, @@ -53,23 +62,60 @@ it( waitedInMs = Date.now() - startRequestInMs; if (waitedInMs < 1000 || waitedInMs > 1100) { - throw new Error('waitForAllRequestsComplete did not wait for timeout'); + throw new E2edError('waitForAllRequestsComplete did not wait for timeout', {waitedInMs}); } - void getUsers(); + void getUsers(2); startRequestInMs = Date.now(); await waitForAllRequestsComplete( - ({url}) => !url.includes('https://reqres.in/api/users?delay=2'), + ({url}) => !url.includes('https://reqres.in/api/users?delay='), {timeout: 1000}, ); waitedInMs = Date.now() - startRequestInMs; if (waitedInMs < 500 || waitedInMs > 600) { - throw new Error( - 'waitForAllRequestsComplete did not wait for firstRequestTimeout with filtered request', + throw new E2edError( + 'waitForAllRequestsComplete did not wait for maxIntervalBetweenRequestsInMs with filtered request', + {waitedInMs}, + ); + } + + promise = waitForAllRequestsComplete(() => true); + + await getUsers(1); + await waitForTimeout(400); + await getUsers(1); + + startRequestInMs = Date.now(); + + await promise; + + waitedInMs = Date.now() - startRequestInMs; + + if (waitedInMs < 400 || waitedInMs > 600) { + throw new E2edError( + 'waitForAllRequestsComplete did not wait for maxIntervalBetweenRequestsInMs between requests', + {waitedInMs}, + ); + } + + promise = waitForAllRequestsComplete(() => true, {maxIntervalBetweenRequestsInMs: 300}); + + await getUsers(1); + + startRequestInMs = Date.now(); + + await promise; + + waitedInMs = Date.now() - startRequestInMs; + + if (waitedInMs < 200 || waitedInMs > 400) { + throw new E2edError( + 'waitForAllRequestsComplete did not wait for maxIntervalBetweenRequestsInMs in the end', + {waitedInMs}, ); } }, diff --git a/autotests/tests/waitForRequest.ts b/autotests/tests/waitForRequest.ts index a2f3fdba..e329155d 100644 --- a/autotests/tests/waitForRequest.ts +++ b/autotests/tests/waitForRequest.ts @@ -1,6 +1,7 @@ import {it} from 'autotests'; import {createClientFunction, expect} from 'e2ed'; import {waitForRequest} from 'e2ed/actions'; +import {E2edError} from 'e2ed/utils'; import type {Request} from 'e2ed/types'; @@ -33,7 +34,7 @@ it( await waitForRequest(() => false, {timeout: 100}).then( () => { - throw new Error('waitForRequest did not throw an error after timeout'); + throw new E2edError('waitForRequest did not throw an error after timeout'); }, () => undefined, ); diff --git a/autotests/tests/waitForResponse.ts b/autotests/tests/waitForResponse.ts index 8b03b3c7..57fe1b03 100644 --- a/autotests/tests/waitForResponse.ts +++ b/autotests/tests/waitForResponse.ts @@ -1,6 +1,7 @@ import {it} from 'autotests'; import {createClientFunction, expect} from 'e2ed'; import {waitForResponse} from 'e2ed/actions'; +import {E2edError} from 'e2ed/utils'; import type {Response} from 'e2ed/types'; @@ -33,7 +34,7 @@ it( await waitForResponse(() => false, {timeout: 100}).then( () => { - throw new Error('waitForResponse did not throw an error after timeout'); + throw new E2edError('waitForResponse did not throw an error after timeout'); }, () => undefined, ); diff --git a/src/actions/waitFor/waitForAllRequestsComplete.ts b/src/actions/waitFor/waitForAllRequestsComplete.ts index 3e81e335..01b21a77 100644 --- a/src/actions/waitFor/waitForAllRequestsComplete.ts +++ b/src/actions/waitFor/waitForAllRequestsComplete.ts @@ -20,12 +20,17 @@ import type { Void, } from '../../types/internal'; +type Options = Readonly<{maxIntervalBetweenRequestsInMs?: number; timeout?: number}>; + /** * Wait for the complete of all requests that satisfy the request predicate. + * If the function runs longer than the specified timeout, it is rejected. + * If there are no new requests for more than `maxIntervalBetweenRequestsInMs`ms, + * the function resolves successfully. */ export const waitForAllRequestsComplete = async ( predicate: RequestPredicate, - {firstRequestTimeout, timeout}: {firstRequestTimeout?: number; timeout?: number} = {}, + {maxIntervalBetweenRequestsInMs, timeout}: Options = {}, ): Promise => { const startTimeInMs = Date.now() as UtcTimeInMs; @@ -34,25 +39,31 @@ export const waitForAllRequestsComplete = async ( const {allRequestsCompletePredicates, hashOfNotCompleteRequests} = getWaitForEventsState( RequestHookToWaitForEvents, ); + const {waitForAllRequestsComplete: defaultTimeouts} = getFullPackConfig(); + const resolveTimeout = + maxIntervalBetweenRequestsInMs ?? defaultTimeouts.maxIntervalBetweenRequestsInMs; + const rejectTimeout = timeout ?? defaultTimeouts.timeout; + + log( + `Set wait for all requests complete with timeout ${rejectTimeout}ms`, + {maxIntervalBetweenRequestsInMs: resolveTimeout, predicate}, + LogEventType.InternalCore, + ); + const requestHookContextIds = await getInitialIdsForAllRequestsCompletePredicate( hashOfNotCompleteRequests, predicate, ); - const {waitForAllRequestsComplete: defaultTimeouts} = getFullPackConfig(); - const firstRequestResolveTimeout = firstRequestTimeout ?? defaultTimeouts.firstRequestTimeout; - const rejectTimeout = timeout ?? defaultTimeouts.timeout; - const {clearRejectTimeout, promise, reject, resolve, setRejectTimeoutFunction} = getPromiseWithResolveAndReject(rejectTimeout); const allRequestsCompletePredicateWithPromise: AllRequestsCompletePredicateWithPromise = { - clearResolveTimeout: undefined, + clearResolveTimeout: () => {}, predicate, reject, requestHookContextIds, - resolve, - startTimeInMs, + setResolveTimeout: () => {}, }; const testRunPromise = getTestRunPromise(); @@ -70,22 +81,16 @@ export const waitForAllRequestsComplete = async ( allRequestsCompletePredicates.delete(allRequestsCompletePredicateWithPromise); - allRequestsCompletePredicateWithPromise.clearResolveTimeout?.(); + allRequestsCompletePredicateWithPromise.clearResolveTimeout(); reject(error); }); - allRequestsCompletePredicates.add(allRequestsCompletePredicateWithPromise); - - log( - `Set wait for all requests complete with timeout ${rejectTimeout}ms and first request timeout ${firstRequestResolveTimeout}ms`, - {predicate}, - LogEventType.InternalCore, - ); + const setResolveTimeout = (): void => { + allRequestsCompletePredicateWithPromise.clearResolveTimeout(); - if (requestHookContextIds.size === 0) { - const {clearRejectTimeout: clearResolveTimeout, promise: firstRequestResolvePromise} = - getPromiseWithResolveAndReject(firstRequestResolveTimeout); + const {clearRejectTimeout: clearResolveTimeout, promise: resolvePromise} = + getPromiseWithResolveAndReject(resolveTimeout); setReadonlyProperty( allRequestsCompletePredicateWithPromise, @@ -95,19 +100,31 @@ export const waitForAllRequestsComplete = async ( void testRunPromise.then(clearResolveTimeout); - firstRequestResolvePromise.catch(() => { + resolvePromise.catch(() => { allRequestsCompletePredicates.delete(allRequestsCompletePredicateWithPromise); const waitInMs = Date.now() - startTimeInMs; log( - `Have waited for all requests complete by first request timeout for ${waitInMs}ms`, - {firstRequestResolveTimeout, predicate, rejectTimeout}, + `Have waited for all requests complete for ${waitInMs}ms`, + {maxIntervalBetweenRequestsInMs: resolveTimeout, predicate, timeout: rejectTimeout}, LogEventType.InternalUtil, ); resolve(); }); + }; + + setReadonlyProperty( + allRequestsCompletePredicateWithPromise, + 'setResolveTimeout', + setResolveTimeout, + ); + + allRequestsCompletePredicates.add(allRequestsCompletePredicateWithPromise); + + if (requestHookContextIds.size === 0) { + allRequestsCompletePredicateWithPromise.setResolveTimeout(); } return promise; diff --git a/src/actions/waitFor/waitForInterfaceStabilization.ts b/src/actions/waitFor/waitForInterfaceStabilization.ts index d3a55fd2..c2b9827d 100644 --- a/src/actions/waitFor/waitForInterfaceStabilization.ts +++ b/src/actions/waitFor/waitForInterfaceStabilization.ts @@ -132,7 +132,7 @@ export const waitForInterfaceStabilization = async (stabilizationInterval = 500) const startDateTimeInIso = new Date(startTimeInMs).toISOString(); log( - `Waited for interface stabilization for ${waitInMs}ms with stabilization interval ${stabilizationInterval}ms`, + `Have waited for interface stabilization for ${waitInMs}ms with stabilization interval ${stabilizationInterval}ms`, {error: maybeErrorReason, startDateTimeInIso}, LogEventType.InternalAction, ); diff --git a/src/actions/waitFor/waitForRequest.ts b/src/actions/waitFor/waitForRequest.ts index 5f24718b..13f4ae16 100644 --- a/src/actions/waitFor/waitForRequest.ts +++ b/src/actions/waitFor/waitForRequest.ts @@ -17,6 +17,7 @@ import type { /** * Wait for some request (from browser) by the request predicate. + * If the function runs longer than the specified timeout, it is rejected. */ export const waitForRequest = ( predicate: RequestPredicate, diff --git a/src/actions/waitFor/waitForResponse.ts b/src/actions/waitFor/waitForResponse.ts index 8af9112e..a7ed6af4 100644 --- a/src/actions/waitFor/waitForResponse.ts +++ b/src/actions/waitFor/waitForResponse.ts @@ -17,6 +17,7 @@ import type { /** * Wait for some response (from browser) by the response predicate. + * If the function runs longer than the specified timeout, it is rejected. */ export const waitForResponse = ( predicate: ResponsePredicate, diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index eff8056f..1849992f 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -164,35 +164,35 @@ export type OwnE2edConfig< testTimeout: number; /** - * Group of settings for the waitForAllRequestsComplete function. + * Group of settings for the `waitForAllRequestsComplete` function. */ waitForAllRequestsComplete: Readonly<{ /** - * Default timeout (in milliseconds) for first request for waitForAllRequestsComplete function. - * If before the expiration of this timeout there is no request that satisfies the predicate, - * then the promise returned by the waitForAllRequestsComplete function will be successfully resolved. + * Default maximum interval (in milliseconds) between requests. + * If there are no new requests for more than this interval, then the promise + * returned by the `waitForAllRequestsComplete` function will be successfully resolved. */ - firstRequestTimeout: number; + maxIntervalBetweenRequestsInMs: number; /** * Default timeout (in milliseconds) for waitForAllRequestsComplete function. * If the wait is longer than this timeout, then the promise - * returned by the waitForAllRequestsComplete function will be rejected. + * returned by the `waitForAllRequestsComplete` function will be rejected. */ timeout: number; }>; /** - * Default timeout (in milliseconds) for waitForRequest function. + * Default timeout (in milliseconds) for `waitForRequest` function. * If the wait is longer than this timeout, then the promise - * returned by the waitForRequest function will be rejected. + * returned by the `waitForRequest` function will be rejected. */ waitForRequestTimeout: number; /** - * Default timeout (in milliseconds) for waitForResponse function. + * Default timeout (in milliseconds) for `waitForResponse` function. * If the wait is longer than this timeout, then the promise - * returned by the waitForResponse function will be rejected. + * returned by the `waitForResponse` function will be rejected. */ waitForResponseTimeout: number; }>; diff --git a/src/types/waitForEvents.ts b/src/types/waitForEvents.ts index 828cf911..67f9a1a6 100644 --- a/src/types/waitForEvents.ts +++ b/src/types/waitForEvents.ts @@ -24,12 +24,11 @@ type RequestOrResponseResolve = MergeFunctions< * @internal */ export type AllRequestsCompletePredicateWithPromise = Readonly<{ - clearResolveTimeout: (() => void) | undefined; + clearResolveTimeout: () => void; predicate: RequestPredicate; reject: (error: unknown) => void; requestHookContextIds: Set; - resolve: () => void; - startTimeInMs: UtcTimeInMs; + setResolveTimeout: () => void; }>; /** diff --git a/src/utils/waitForEvents/completeRequest.ts b/src/utils/waitForEvents/completeRequest.ts index cb9e85b2..3bf449bc 100644 --- a/src/utils/waitForEvents/completeRequest.ts +++ b/src/utils/waitForEvents/completeRequest.ts @@ -1,7 +1,4 @@ -import {LogEventType} from '../../constants/internal'; - import {assertValueIsDefined} from '../asserts'; -import {log} from '../log'; import type {RequestHookContextId, WaitForEventsState} from '../../types/internal'; @@ -21,27 +18,13 @@ export const completeRequest = ( delete hashOfNotCompleteRequests[requestHookContextId]; for (const allRequestsCompletePredicateWithPromise of allRequestsCompletePredicates) { - const {requestHookContextIds, resolve, startTimeInMs} = allRequestsCompletePredicateWithPromise; + const {requestHookContextIds, setResolveTimeout} = allRequestsCompletePredicateWithPromise; const requestWasWaited = requestHookContextIds.has(requestHookContextId); requestHookContextIds.delete(requestHookContextId); - if (requestHookContextIds.size > 0 || !requestWasWaited) { - continue; + if (requestHookContextIds.size === 0 && requestWasWaited) { + setResolveTimeout(); } - - allRequestsCompletePredicates.delete(allRequestsCompletePredicateWithPromise); - - const {predicate} = allRequestsCompletePredicateWithPromise; - - const waitInMs = Date.now() - startTimeInMs; - - log( - `Have waited for all requests complete for ${waitInMs}ms`, - {predicate}, - LogEventType.InternalUtil, - ); - - resolve(); } }; diff --git a/src/utils/waitForEvents/processAllRequestsCompletePredicate.ts b/src/utils/waitForEvents/processAllRequestsCompletePredicate.ts index 9ac4d083..aded82f9 100644 --- a/src/utils/waitForEvents/processAllRequestsCompletePredicate.ts +++ b/src/utils/waitForEvents/processAllRequestsCompletePredicate.ts @@ -26,11 +26,11 @@ export const processAllRequestsCompletePredicate = async ( return false; } - clearResolveTimeout?.(); + clearResolveTimeout(); requestHookContextIds.add(requestHookContextId); } catch (cause) { - clearResolveTimeout?.(); + clearResolveTimeout(); const error = new E2edError( 'waitForAllRequestsComplete promise rejected due to error in predicate function',