diff --git a/Dockerfile b/Dockerfile index df91b81e..6d843c15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,15 @@ -FROM alpine:3.18.4 +FROM node:20.10.0-alpine AS node + +FROM alpine:3.18.5 + +COPY --from=node /usr/lib /usr/lib +COPY --from=node /usr/local/lib /usr/local/lib +COPY --from=node /usr/local/include /usr/local/include +COPY --from=node /usr/local/bin /usr/local/bin RUN apk --no-cache upgrade && \ apk --no-cache add \ - bash libevent nodejs npm chromium firefox xwininfo xvfb dbus eudev ttf-freefont fluxbox procps tzdata icu-data-full + bash libevent npm chromium firefox xwininfo xvfb dbus eudev ttf-freefont fluxbox procps tzdata icu-data-full COPY ./build/node_modules/e2ed /node_modules/e2ed diff --git a/autotests/configurator/index.ts b/autotests/configurator/index.ts index 4598239b..e7c33c3e 100644 --- a/autotests/configurator/index.ts +++ b/autotests/configurator/index.ts @@ -1,5 +1,7 @@ export {doAfterPack} from './doAfterPack'; export {doBeforePack} from './doBeforePack'; +export {mapBackendResponseErrorToLog} from './mapBackendResponseErrorToLog'; +export {mapBackendResponseToLog} from './mapBackendResponseToLog'; export {mapLogPayloadInConsole} from './mapLogPayloadInConsole'; export {mapLogPayloadInLogFile} from './mapLogPayloadInLogFile'; export {mapLogPayloadInReport} from './mapLogPayloadInReport'; diff --git a/autotests/configurator/mapBackendResponseErrorToLog.ts b/autotests/configurator/mapBackendResponseErrorToLog.ts new file mode 100644 index 00000000..1a62adf3 --- /dev/null +++ b/autotests/configurator/mapBackendResponseErrorToLog.ts @@ -0,0 +1,42 @@ +import {getDurationWithUnits} from 'e2ed/utils'; + +import type {MapBackendResponseErrorToLog} from 'autotests/types/packSpecific'; + +/** + * Maps responses with errors from the backend to "red" logs (as errors) during the test. + * It is assumed that the function will select responses with + * statuse codes of 400 and higher (client and server errors). + * Backend responses with errors are accumulated in separate "red" log step + * (with `logEventStatus: 'failed'`). + * Log the `responseBody` field carefully, as the body of backend response can be very large. + * If the function returns `undefined`, the response is not logged (skipped). + */ +export const mapBackendResponseErrorToLog: MapBackendResponseErrorToLog = ({ + completionTimeInMs, + request, + responseBody, + responseHeaders, + statusCode, +}) => { + if (statusCode < 400) { + return undefined; + } + + const {requestBody, utcTimeInMs, ...requestWithoutBody} = request ?? {}; + + const maybeWithDuration: {duration?: string} = {}; + + if (completionTimeInMs !== undefined && utcTimeInMs !== undefined) { + maybeWithDuration.duration = getDurationWithUnits(completionTimeInMs - utcTimeInMs); + } + + return { + ...maybeWithDuration, + request: { + requestBody: requestBody instanceof Buffer ? String(requestBody) : requestBody, + ...requestWithoutBody, + }, + responseBody: responseBody instanceof Buffer ? String(responseBody) : responseBody, + responseHeaders, + }; +}; diff --git a/autotests/configurator/mapBackendResponseToLog.ts b/autotests/configurator/mapBackendResponseToLog.ts new file mode 100644 index 00000000..50ae34c2 --- /dev/null +++ b/autotests/configurator/mapBackendResponseToLog.ts @@ -0,0 +1,38 @@ +import {getDurationWithUnits} from 'e2ed/utils'; + +import type {MapBackendResponseToLog} from 'autotests/types/packSpecific'; + +/** + * Maps responses from the backend to logs during the test. + * Backend responses received during a certain test step are accumulated + * in an array in the `backendResponses` field of the log of this step. + * Log the `responseBody` field carefully, as the body of backend response can be very large. + * If the function returns `undefined`, the response is not logged (skipped). + */ +export const mapBackendResponseToLog: MapBackendResponseToLog = ({ + completionTimeInMs, + request, + responseBody, + responseHeaders, + statusCode, +}) => { + if (statusCode >= 400) { + return undefined; + } + + if (request) { + const maybeWithDuration: {duration?: string} = {}; + + if (completionTimeInMs !== undefined && request.utcTimeInMs !== undefined) { + maybeWithDuration.duration = getDurationWithUnits(completionTimeInMs - request.utcTimeInMs); + } + + return {...maybeWithDuration, statusCode, url: request?.url}; + } + + return { + responseBody: responseBody instanceof Buffer ? String(responseBody) : responseBody, + responseHeaders, + statusCode, + }; +}; diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index cfc8b380..cb8ed5db 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -10,6 +10,8 @@ import {RunEnvironment, runEnvironment} from 'e2ed/configurator'; import { doAfterPack, doBeforePack, + mapBackendResponseErrorToLog, + mapBackendResponseToLog, mapLogPayloadInConsole, mapLogPayloadInLogFile, mapLogPayloadInReport, @@ -42,10 +44,8 @@ export const pack: Pack = { filterTestsIntoPack, liteReportFileName: 'lite-report.json', logFileName: 'pack-logs.log', - mapBackendResponseErrorToLog: ({request, responseHeaders, statusCode}) => - statusCode >= 400 ? {request, responseHeaders, statusCode} : undefined, - mapBackendResponseToLog: ({request, statusCode}) => - statusCode < 400 ? {statusCode, url: request?.url} : undefined, + mapBackendResponseErrorToLog, + mapBackendResponseToLog, mapLogPayloadInConsole, mapLogPayloadInLogFile, mapLogPayloadInReport, diff --git a/autotests/pageObjects/pages/Main.ts b/autotests/pageObjects/pages/Main.ts index 7ac83b85..fc6a4fa9 100644 --- a/autotests/pageObjects/pages/Main.ts +++ b/autotests/pageObjects/pages/Main.ts @@ -74,13 +74,19 @@ export class Main extends Page { } override async waitForPageLoaded(): Promise { - await waitForAllRequestsComplete(({url}) => { - if (url.startsWith('https://adservice.google.com/')) { - return false; - } - - return true; - }); + await waitForAllRequestsComplete( + ({url}) => { + if ( + url.startsWith('https://adservice.google.com/') || + url.startsWith('https://play.google.com/') + ) { + return false; + } + + return true; + }, + {maxIntervalBetweenRequestsInMs: this.maxIntervalBetweenRequestsInMs}, + ); await waitForInterfaceStabilization(this.pageStabilizationInterval); } diff --git a/autotests/pageObjects/pages/Search.ts b/autotests/pageObjects/pages/Search.ts index 5e1bec21..a51dcb65 100644 --- a/autotests/pageObjects/pages/Search.ts +++ b/autotests/pageObjects/pages/Search.ts @@ -34,18 +34,21 @@ export class Search extends MobilePage { } override async waitForPageLoaded(): Promise { - await waitForAllRequestsComplete(({url}) => { - if ( - url.startsWith('https://adservice.google.com/') || - url.startsWith('https://googleads.g.doubleclick.net/') || - url.startsWith('https://play.google.com/') || - url.startsWith('https://static.doubleclick.net/') - ) { - return false; - } - - return true; - }); + await waitForAllRequestsComplete( + ({url}) => { + if ( + url.startsWith('https://adservice.google.com/') || + url.startsWith('https://googleads.g.doubleclick.net/') || + url.startsWith('https://play.google.com/') || + url.startsWith('https://static.doubleclick.net/') + ) { + return false; + } + + return true; + }, + {maxIntervalBetweenRequestsInMs: this.maxIntervalBetweenRequestsInMs}, + ); await waitForInterfaceStabilization(this.pageStabilizationInterval); } diff --git a/autotests/tests/e2edReportExample/customSelectorMethods.ts b/autotests/tests/e2edReportExample/selectorCustomMethods.ts similarity index 88% rename from autotests/tests/e2edReportExample/customSelectorMethods.ts rename to autotests/tests/e2edReportExample/selectorCustomMethods.ts index 1ae5d3c5..54fec1f6 100644 --- a/autotests/tests/e2edReportExample/customSelectorMethods.ts +++ b/autotests/tests/e2edReportExample/selectorCustomMethods.ts @@ -2,10 +2,13 @@ import {it} from 'autotests'; import {E2edReportExample} from 'autotests/pageObjects/pages'; import {expect} from 'e2ed'; import {click, navigateToPage} from 'e2ed/actions'; +import {getTimeoutPromise} from 'e2ed/utils'; -it('custom selector methods', {meta: {testId: '15'}}, async () => { +it('selector custom methods', {meta: {testId: '15'}}, async () => { const reportPage = await navigateToPage(E2edReportExample); + await expect(getTimeoutPromise(2_0000), 'should failed by timeout').ok(); + await expect(reportPage.navigationRetries.exists, 'navigation retries exists').ok(); await expect(reportPage.navigationRetriesButton.exists, ' exists').ok(); diff --git a/autotests/tests/internalTypeTests/selectors.skip.ts b/autotests/tests/internalTypeTests/selectors.skip.ts index 439ffc75..367756a2 100644 --- a/autotests/tests/internalTypeTests/selectors.skip.ts +++ b/autotests/tests/internalTypeTests/selectors.skip.ts @@ -5,25 +5,30 @@ import { locatorIdSelector, } from 'autotests/selectors'; +import type {Selector} from 'e2ed/types'; + // @ts-expect-error: wrong number of arguments htmlElementSelector.findByLocatorId(); // @ts-expect-error: wrong type of arguments htmlElementSelector.findByLocatorId(0); // ok -htmlElementSelector.findByLocatorId('id'); +htmlElementSelector.findByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.findByLocatorId('id').findByLocatorId('id2'); +htmlElementSelector.findByLocatorId('id').findByLocatorId('id2') satisfies Selector; // ok -htmlElementSelector.findByLocatorId('id').find('.test-children'); +htmlElementSelector.findByLocatorId('id').find('.test-children') satisfies Selector; // ok -htmlElementSelector.find('body').findByLocatorId('id'); +htmlElementSelector.find('body').findByLocatorId('id') satisfies Selector; // ok -createSelector('id').findByLocatorId('id').find('body').findByLocatorId('id'); +createSelector('id').findByLocatorId('id').find('body').findByLocatorId('id') satisfies Selector; // ok -createSelectorByCss('id').findByLocatorId('id').find('body').findByLocatorId('id'); +createSelectorByCss('id') + .findByLocatorId('id') + .find('body') + .findByLocatorId('id') satisfies Selector; // ok -locatorIdSelector('id').findByLocatorId('id').find('body').findByLocatorId('id'); +locatorIdSelector('id').findByLocatorId('id').find('body').findByLocatorId('id') satisfies Selector; // @ts-expect-error: wrong number of arguments locatorIdSelector(); @@ -32,32 +37,32 @@ locatorIdSelector(); locatorIdSelector(3); // ok -htmlElementSelector.filterByLocatorId('id'); +htmlElementSelector.filterByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.parentByLocatorId('id'); +htmlElementSelector.parentByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.childByLocatorId('id'); +htmlElementSelector.childByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.siblingByLocatorId('id'); +htmlElementSelector.siblingByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.nextSiblingByLocatorId('id'); +htmlElementSelector.nextSiblingByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.prevSiblingByLocatorId('id'); +htmlElementSelector.prevSiblingByLocatorId('id') satisfies Selector; // ok -htmlElementSelector.filterByLocatorParameter('prop', 'value'); +htmlElementSelector.filterByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.findByLocatorParameter('prop', 'value'); +htmlElementSelector.findByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.parentByLocatorParameter('prop', 'value'); +htmlElementSelector.parentByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.childByLocatorParameter('prop', 'value'); +htmlElementSelector.childByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.siblingByLocatorParameter('prop', 'value'); +htmlElementSelector.siblingByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.nextSiblingByLocatorParameter('prop', 'value'); +htmlElementSelector.nextSiblingByLocatorParameter('prop', 'value') satisfies Selector; // ok -htmlElementSelector.prevSiblingByLocatorParameter('prop', 'value'); +htmlElementSelector.prevSiblingByLocatorParameter('prop', 'value') satisfies Selector; // ok void htmlElementSelector.getLocatorId(); @@ -65,8 +70,8 @@ void htmlElementSelector.getLocatorId(); // TODO: should be an error "wrong number of arguments" void htmlElementSelector.getLocatorId('foo'); -// ok -void htmlElementSelector.hasLocatorId(); +// @ts-expect-error: TODO: should be ok +void htmlElementSelector.hasLocatorId() satisfies Promise; // TODO: should be an error "wrong number of arguments" void htmlElementSelector.hasLocatorId('foo'); @@ -77,10 +82,10 @@ void htmlElementSelector.hasLocatorParameter(); // @ts-expect-error: wrong number of arguments void htmlElementSelector.getLocatorParameter(); -// ok -void htmlElementSelector.getLocatorParameter('prop'); -// ok -void htmlElementSelector.hasLocatorParameter('prop'); +// @ts-expect-error: TODO: should be ok +void htmlElementSelector.getLocatorParameter('prop') satisfies Promise; +// @ts-expect-error: TODO: should be ok +void htmlElementSelector.hasLocatorParameter('prop') satisfies Promise; // ok htmlElementSelector.getDescription() satisfies string | undefined; diff --git a/scripts/updateChangelog.ts b/scripts/updateChangelog.ts index fdc9cc9e..4ae55017 100644 --- a/scripts/updateChangelog.ts +++ b/scripts/updateChangelog.ts @@ -29,7 +29,7 @@ const SEPARATOR = '\n'.repeat(64); const gitOptions = [ 'log', `HEAD...v${previousVersion}`, - `--pretty=tformat:%H%aN %s%n%b${'%n'.repeat(SEPARATOR.length)}`, + `--pretty="tformat:%H%aN|%s%n%b${'%n'.repeat(SEPARATOR.length)}"`, ]; const commits = execFileSync('git', gitOptions, EXEC_FILE_OPTIONS) @@ -42,15 +42,19 @@ const markdownCommits = commits.map((commit) => { assertValueIsDefined(firstLine); - const firstSpaceIndex = firstLine.indexOf(' '); - const subject = firstLine.slice(firstSpaceIndex + 1); + const authorNameIndex = firstLine.indexOf('|'); + const subject = firstLine.slice(authorNameIndex + 1); if (/^\d+\.\d+\.\d+$/.test(subject)) { return ''; } const hash = firstLine.slice(0, 40); - const authorName = firstLine.slice(40, firstSpaceIndex); + let authorName = firstLine.slice(40, authorNameIndex); + + if (authorName.includes('Torchinskiy')) { + authorName = 'nnn3d'; + } const body = bodyLines.length === 0 ? '' : `\n\n ${bodyLines.join('\n\n ')}\n`; diff --git a/src/Page.ts b/src/Page.ts index 269e1b91..b4a5cd45 100644 --- a/src/Page.ts +++ b/src/Page.ts @@ -26,11 +26,25 @@ export abstract class Page { this.pageParams = pageParams as PageParams; - const {pageStabilizationInterval} = getFullPackConfig(); + const { + pageStabilizationInterval, + waitForAllRequestsComplete: {maxIntervalBetweenRequestsInMs}, + } = getFullPackConfig(); + this.maxIntervalBetweenRequestsInMs = maxIntervalBetweenRequestsInMs; this.pageStabilizationInterval = pageStabilizationInterval; } + /** + * Default maximum interval (in milliseconds) between requests. + * After navigating to the page, `e2ed` will wait until + * all requests will complete, and only after that it will consider the page loaded. + * If there are no new requests for more than this interval, + * then we will consider that all requests completes + * The default value is taken from the corresponding field of the pack config. + */ + readonly maxIntervalBetweenRequestsInMs: number; + /** * Immutable page parameters. */ @@ -40,6 +54,7 @@ export abstract class Page { * After navigating to the page, `e2ed` will wait until * the page is stable for the specified time in millisecond, * and only after that it will consider the page loaded. + * The default value is taken from the corresponding field of the pack config. */ readonly pageStabilizationInterval: number; @@ -80,7 +95,9 @@ export abstract class Page { abstract getRoute(): PageRoute; async waitForPageLoaded(): Promise { - await waitForAllRequestsComplete(() => true); + await waitForAllRequestsComplete(() => true, { + maxIntervalBetweenRequestsInMs: this.maxIntervalBetweenRequestsInMs, + }); await waitForInterfaceStabilization(this.pageStabilizationInterval); } diff --git a/src/README.md b/src/README.md index 43879e28..2d9748eb 100644 --- a/src/README.md +++ b/src/README.md @@ -4,34 +4,38 @@ This is a graph of the base modules of the project with dependencies between the Modules in the dependency graph should only import the modules above them: -0. `../scripts/*` -1. `types/*` -2. `configurator/*` -3. `constants/*` +0. `../scripts` +1. `types` +2. `configurator` +3. `constants` 4. `testcaferc` 5. `testcafe` -6. `esm/*` -7. `generators/*` +6. `esm` +7. `generators` 8. `utils/getDurationWithUnits` 9. `utils/setReadonlyProperty` -10. `utils/paths` -11. `utils/valueToString` -12. `utils/error` -13. `utils/asserts` -14. `utils/userlandHooks` -15. `utils/fn` -16. `utils/environment` -17. `utils/getFullPackConfig` -18. `utils/runLabel` -19. `utils/generalLog` -20. `utils/runArrayOfFunctionsSafely` -21. `utils/fs` -22. `Route` -23. `ApiRoute` -24. `PageRoute` -25. `testController` -26. `useContext` -27. `context/*` -28. `utils/log` -29. `utils/waitForEvents` -30. ... +10. `utils/selectors` +11. `utils/paths` +12. `utils/valueToString` +13. `utils/error` +14. `utils/asserts` +15. `utils/userlandHooks` +16. `utils/fn` +17. `utils/environment` +18. `utils/getFullPackConfig` +19. `utils/runLabel` +20. `utils/generalLog` +21. `utils/runArrayOfFunctionsSafely` +22. `utils/fs` +23. `selectors` +24. `Route` +25. `ApiRoute` +26. `PageRoute` +27. `testController` +28. `useContext` +29. `context` +30. `utils/log` +31. `utils/waitForEvents` +32. `utils/expect` +33. `expect` +34. ... diff --git a/src/actions/asserts/viewport/assertSelectorEntirelyInViewport.ts b/src/actions/asserts/viewport/assertSelectorEntirelyInViewport.ts index 6059f291..21bbbb51 100644 --- a/src/actions/asserts/viewport/assertSelectorEntirelyInViewport.ts +++ b/src/actions/asserts/viewport/assertSelectorEntirelyInViewport.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../../../constants/internal'; import {expect} from '../../../expect'; -import {getDescriptionFromSelector} from '../../../utils/locators'; import {log} from '../../../utils/log'; +import {getDescriptionFromSelector} from '../../../utils/selectors'; import {isSelectorEntirelyInViewport} from '../../../utils/viewport'; import type {Selector} from '../../../types/internal'; @@ -12,10 +12,10 @@ import type {Selector} from '../../../types/internal'; */ export const assertSelectorEntirelyInViewport = async (selector: Selector): Promise => { const isEntirelyInViewport = await isSelectorEntirelyInViewport(selector); - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); const message = 'selector is entirely in the viewport'; - log(`Asserts that ${message}`, {locator}, LogEventType.InternalAssert); + log(`Asserts that ${message}`, {description}, LogEventType.InternalAssert); // TODO: support Smart Assertions await expect(isEntirelyInViewport, message).ok(); diff --git a/src/actions/asserts/viewport/assertSelectorInViewport.ts b/src/actions/asserts/viewport/assertSelectorInViewport.ts index 15dc0979..d5eed5f7 100644 --- a/src/actions/asserts/viewport/assertSelectorInViewport.ts +++ b/src/actions/asserts/viewport/assertSelectorInViewport.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../../../constants/internal'; import {expect} from '../../../expect'; -import {getDescriptionFromSelector} from '../../../utils/locators'; import {log} from '../../../utils/log'; +import {getDescriptionFromSelector} from '../../../utils/selectors'; import {isSelectorInViewport} from '../../../utils/viewport'; import type {Selector} from '../../../types/internal'; @@ -12,10 +12,10 @@ import type {Selector} from '../../../types/internal'; */ export const assertSelectorInViewport = async (selector: Selector): Promise => { const isInViewport = await isSelectorInViewport(selector); - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); const message = 'selector is in the viewport'; - log(`Asserts that ${message}`, {locator}, LogEventType.InternalAssert); + log(`Asserts that ${message}`, {description}, LogEventType.InternalAssert); // TODO: support Smart Assertions await expect(isInViewport, message).ok(); diff --git a/src/actions/clearUpload.ts b/src/actions/clearUpload.ts index 2fc01223..de113dc2 100644 --- a/src/actions/clearUpload.ts +++ b/src/actions/clearUpload.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Selector, TestCafeSelector} from '../types/internal'; @@ -9,9 +9,9 @@ import type {Selector, TestCafeSelector} from '../types/internal'; * Removes all file paths from the specified file upload input. */ export const clearUpload = (selector: Selector): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); - log('Remove all file paths from file upload input', {locator}, LogEventType.InternalAction); + log('Remove all file paths from file upload input', {description}, LogEventType.InternalAction); return testController.clearUpload(selector as unknown as TestCafeSelector); }; diff --git a/src/actions/click.ts b/src/actions/click.ts index 8c0cfea6..5a4c5db8 100644 --- a/src/actions/click.ts +++ b/src/actions/click.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -16,11 +16,11 @@ export const click = async ( selector: Selector, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); - const withLocator = locator ? ` with locator ${locator}` : ''; + const description = getDescriptionFromSelector(selector); + const withDescription = description ? ` with description ${description}` : ''; log( - `Click an element${withLocator}`, + `Click an element${withDescription}`, {...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/dispatchEvent.ts b/src/actions/dispatchEvent.ts index d12adbdd..2a8d0fb9 100644 --- a/src/actions/dispatchEvent.ts +++ b/src/actions/dispatchEvent.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -17,11 +17,11 @@ export const dispatchEvent = async ( eventName: string, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( 'Dispatches an event over a specified element', - {locator, ...options, stabilizationInterval}, + {description, ...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/doubleClick.ts b/src/actions/doubleClick.ts index 609e391d..b88adcdf 100644 --- a/src/actions/doubleClick.ts +++ b/src/actions/doubleClick.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -16,11 +16,11 @@ export const doubleClick = async ( selector: Selector, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); - const withLocator = locator ? ` with locator ${locator}` : ''; + const description = getDescriptionFromSelector(selector); + const withDescription = description ? ` with description ${description}` : ''; log( - `Double-click an element${withLocator}`, + `Double-click an element${withDescription}`, {...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/drag.ts b/src/actions/drag.ts index ca47d282..0786b0a5 100644 --- a/src/actions/drag.ts +++ b/src/actions/drag.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -19,11 +19,11 @@ export const drag = async ( {stabilizationInterval, ...options}: Options = {}, // eslint-disable-next-line max-params ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( 'Drag an element by an offset', - {dragOffsetX, dragOffsetY, locator, ...options, stabilizationInterval}, + {description, dragOffsetX, dragOffsetY, ...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/dragToElement.ts b/src/actions/dragToElement.ts index 6cf4f58d..3bdab6c0 100644 --- a/src/actions/dragToElement.ts +++ b/src/actions/dragToElement.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -17,12 +17,12 @@ export const dragToElement = async ( destinationSelector: Selector, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); - const destinationLocator = getDescriptionFromSelector(destinationSelector); + const description = getDescriptionFromSelector(selector); + const destinationDescription = getDescriptionFromSelector(destinationSelector); log( 'Drag an element onto another one', - {destinationLocator, locator, ...options, stabilizationInterval}, + {description, destinationDescription, ...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/hover.ts b/src/actions/hover.ts index 67084e92..f8189c8f 100644 --- a/src/actions/hover.ts +++ b/src/actions/hover.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -16,11 +16,11 @@ export const hover = async ( selector: Selector, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( 'Hover the mouse pointer over an element', - {locator, ...options, stabilizationInterval}, + {description, ...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/rightClick.ts b/src/actions/rightClick.ts index b18c8d8e..8f96adb1 100644 --- a/src/actions/rightClick.ts +++ b/src/actions/rightClick.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -16,11 +16,11 @@ export const rightClick = async ( selector: Selector, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); - const withLocator = locator ? ` with locator ${locator}` : ''; + const description = getDescriptionFromSelector(selector); + const withDescription = description ? ` with description ${description}` : ''; log( - `Right-click an element${withLocator}`, + `Right-click an element${withDescription}`, {...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/scroll.ts b/src/actions/scroll.ts index 43a269e7..c2b756c4 100644 --- a/src/actions/scroll.ts +++ b/src/actions/scroll.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Inner} from 'testcafe-without-typecheck'; @@ -26,7 +26,7 @@ type Scroll = ((posX: number, posY: number) => Promise) & */ // @ts-expect-error: e2ed Selector type is incompatible with TS Selector export const scroll: Scroll = (...args) => { - const locator = getDescriptionFromSelector(args[0] as Selector); + const description = getDescriptionFromSelector(args[0] as Selector); const printedArgs = [...args]; if (typeof args[0] === 'object') { @@ -35,7 +35,7 @@ export const scroll: Scroll = (...args) => { log( 'Scroll the document (or element) to the specified position', - {args: printedArgs, locator}, + {args: printedArgs, description}, LogEventType.InternalAction, ); diff --git a/src/actions/scrollBy.ts b/src/actions/scrollBy.ts index 1344d7e1..93666105 100644 --- a/src/actions/scrollBy.ts +++ b/src/actions/scrollBy.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Inner} from 'testcafe-without-typecheck'; @@ -15,7 +15,7 @@ type ScrollBy = ((x: number, y: number) => Promise) & */ // @ts-expect-error: e2ed Selector type is incompatible with TS Selector export const scrollBy: ScrollBy = (...args) => { - const locator = getDescriptionFromSelector(args[0] as Selector); + const description = getDescriptionFromSelector(args[0] as Selector); const printedArgs = [...args]; if (typeof args[0] === 'object') { @@ -24,7 +24,7 @@ export const scrollBy: ScrollBy = (...args) => { log( 'Scroll the document (or element) by the given offset', - {args: printedArgs, locator}, + {args: printedArgs, description}, LogEventType.InternalAction, ); diff --git a/src/actions/scrollIntoView.ts b/src/actions/scrollIntoView.ts index f3cdf278..3f1e2a6c 100644 --- a/src/actions/scrollIntoView.ts +++ b/src/actions/scrollIntoView.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Selector, TestCafeSelector} from '../types/internal'; @@ -11,9 +11,13 @@ type Options = Parameters[1]; * Scrolls the specified element into view. */ export const scrollIntoView = (selector: Selector, options?: Options): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); - log('Scroll the specified element into view', {locator, options}, LogEventType.InternalAction); + log( + 'Scroll the specified element into view', + {description, options}, + LogEventType.InternalAction, + ); return testController.scrollIntoView(selector as unknown as TestCafeSelector, options); }; diff --git a/src/actions/selectText.ts b/src/actions/selectText.ts index 55029970..24a5ef0a 100644 --- a/src/actions/selectText.ts +++ b/src/actions/selectText.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Selector, TestCafeSelector} from '../types/internal'; @@ -17,13 +17,13 @@ export const selectText = ( options?: Options, // eslint-disable-next-line max-params ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( `Select text in input element, from ${startPos} to ${ endPos === undefined ? 'the end' : endPos }`, - {locator, options}, + {description, options}, LogEventType.InternalAction, ); diff --git a/src/actions/setFilesToUpload.ts b/src/actions/setFilesToUpload.ts index 8536c778..c76633e4 100644 --- a/src/actions/setFilesToUpload.ts +++ b/src/actions/setFilesToUpload.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Selector, TestCafeSelector} from '../types/internal'; @@ -13,11 +13,11 @@ export const setFilesToUpload = ( filePath: string | string[], ): Promise => { const hasManyFiles = Array.isArray(filePath) && filePath.length > 0; - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( `Populate file upload input with file${hasManyFiles ? 's' : ''} "${String(filePath)}"`, - {locator}, + {description}, LogEventType.InternalAction, ); diff --git a/src/actions/switchToIframe.ts b/src/actions/switchToIframe.ts index e534ea95..fae8a650 100644 --- a/src/actions/switchToIframe.ts +++ b/src/actions/switchToIframe.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -14,11 +14,11 @@ export const switchToIframe = async ( iframeSelector: Selector, {stabilizationInterval}: WithStabilizationInterval = {}, ): Promise => { - const locator = getDescriptionFromSelector(iframeSelector); + const description = getDescriptionFromSelector(iframeSelector); log( 'Switch browsing context to the specified iframe', - {locator, stabilizationInterval}, + {description, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/actions/takeElementScreenshot.ts b/src/actions/takeElementScreenshot.ts index 51492ca3..1721c5b1 100644 --- a/src/actions/takeElementScreenshot.ts +++ b/src/actions/takeElementScreenshot.ts @@ -2,9 +2,9 @@ import {DEFAULT_TAKE_SCREENSHOT_TIMEOUT_IN_MS, LogEventType} from '../constants/ import {testController} from '../testController'; import {E2edError} from '../utils/error'; import {getDurationWithUnits} from '../utils/getDurationWithUnits'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; import {getPromiseWithResolveAndReject} from '../utils/promise'; +import {getDescriptionFromSelector} from '../utils/selectors'; import type {Selector, TestCafeSelector} from '../types/internal'; @@ -19,13 +19,13 @@ export const takeElementScreenshot = ( pathToScreenshot?: string, {timeout = DEFAULT_TAKE_SCREENSHOT_TIMEOUT_IN_MS, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); const timeoutWithUnits = getDurationWithUnits(timeout); log( 'Take a screenshot of the element', - {locator, options, pathToScreenshot, timeoutWithUnits}, + {description, options, pathToScreenshot, timeoutWithUnits}, LogEventType.InternalAction, ); @@ -45,7 +45,7 @@ export const takeElementScreenshot = ( setRejectTimeoutFunction(() => { const error = new E2edError( `takeElementScreenshot promise rejected after ${timeoutWithUnits} timeout`, - {locator, options, pathToScreenshot}, + {description, options, pathToScreenshot}, ); reject(error); diff --git a/src/actions/typeText.ts b/src/actions/typeText.ts index f8ba515c..6e2e4d4f 100644 --- a/src/actions/typeText.ts +++ b/src/actions/typeText.ts @@ -1,7 +1,7 @@ import {LogEventType} from '../constants/internal'; import {testController} from '../testController'; -import {getDescriptionFromSelector} from '../utils/locators'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; import {waitForInterfaceStabilization} from './waitFor'; @@ -17,11 +17,11 @@ export const typeText = async ( text: string, {stabilizationInterval, ...options}: Options = {}, ): Promise => { - const locator = getDescriptionFromSelector(selector); + const description = getDescriptionFromSelector(selector); log( `Type "${text}" into an input element`, - {locator, ...options, stabilizationInterval}, + {description, ...options, stabilizationInterval}, LogEventType.InternalAction, ); diff --git a/src/expect.ts b/src/expect.ts index ac9f31a8..a6ac5672 100644 --- a/src/expect.ts +++ b/src/expect.ts @@ -3,7 +3,7 @@ import {Expect} from './utils/expect'; import type {Inner} from 'testcafe-without-typecheck'; /** - * Wraps a promised value to assertion for further checks. + * Wraps a value or promised value to assertion for further checks. */ export const expect = ( actual: Actual | Promise, diff --git a/src/selectors/createSelectorFunctions.ts b/src/selectors/createSelectorFunctions.ts index b779b086..ed5b73fc 100644 --- a/src/selectors/createSelectorFunctions.ts +++ b/src/selectors/createSelectorFunctions.ts @@ -1,24 +1,26 @@ -import {createCustomMethods} from './createCustomMethods'; -import {createSelectorByCssCreator} from './createSelectorByCssCreator'; -import {createSelectorCreator} from './createSelectorCreator'; -import {htmlElementSelectorCreator} from './htmlElementSelectorCreator'; -import {locatorIdSelectorCreator} from './locatorIdSelectorCreator'; +import {setCustomInspectOnFunction} from '../utils/fn'; +import {generalLog} from '../utils/generalLog'; +import { + createSelectorByCssCreator, + createSelectorCreator, + htmlElementSelectorCreator, + locatorIdSelectorCreator, +} from '../utils/selectors'; -import type { - CreateSelectorFunctionsOptions, - GetLocatorAttributeNameFn, - SelectorCustomMethods, - SelectorFunctions, -} from '../types/internal'; +import type {CreateSelectorFunctionsOptions, SelectorFunctions} from '../types/internal'; -const createSelectorFunctionsWithCustomMethods = ( - getLocatorAttributeName: GetLocatorAttributeNameFn, - // force `this` to be Selector - customMethods: SelectorCustomMethods, -): SelectorFunctions => { - const createSelector = createSelectorCreator(customMethods); +/** + * Creates main functions for creating selectors and working with selectors. + */ +export const createSelectorFunctions = ({ + getLocatorAttributeName, +}: CreateSelectorFunctionsOptions): SelectorFunctions => { + setCustomInspectOnFunction(getLocatorAttributeName); + generalLog('Create selector functions', {getLocatorAttributeName}); + + const createSelector = createSelectorCreator(getLocatorAttributeName); - const selectorsWithCustomMethods = { + return { /** * Creates selector by locator and optional parameters. */ @@ -36,17 +38,4 @@ const createSelectorFunctionsWithCustomMethods = ( */ locatorIdSelector: locatorIdSelectorCreator(createSelector, getLocatorAttributeName), }; - - return selectorsWithCustomMethods; }; - -/** - * Creates main functions for creating selectors and working with selectors. - */ -export const createSelectorFunctions = ({ - getLocatorAttributeName, -}: CreateSelectorFunctionsOptions): SelectorFunctions => - createSelectorFunctionsWithCustomMethods( - getLocatorAttributeName, - createCustomMethods(getLocatorAttributeName), - ); diff --git a/src/selectors/internal.ts b/src/selectors/internal.ts deleted file mode 100644 index 2d7564ab..00000000 --- a/src/selectors/internal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {createSelectorFunctions} from './createSelectorFunctions'; - -/** - * Internal implementation of `htmlElementSelector` selector. - * @internal - */ -export const {htmlElementSelector} = createSelectorFunctions({ - getLocatorAttributeName: (parameter) => - parameter === 'id' ? 'data-testid' : `data-test-${parameter}`, -}); diff --git a/src/testcaferc.ts b/src/testcaferc.ts index 0efa9691..0cb2fe15 100644 --- a/src/testcaferc.ts +++ b/src/testcaferc.ts @@ -1,6 +1,6 @@ /** * @file Full pack configuration (extended TestCafe configuration) for running tests. - * Don't import this module. Instead, use utils/getFullPackConfig. + * Don't import this module. Instead, use `utils/getFullPackConfig`. */ import {join} from 'node:path'; diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index f5face6b..7ba89bab 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -191,6 +191,7 @@ export type OwnE2edConfig< * 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. + * This parameter can be overridden on a specific page instance. */ maxIntervalBetweenRequestsInMs: number; diff --git a/src/types/http/cookie.ts b/src/types/http/cookie.ts index aa83963f..47e7b83b 100644 --- a/src/types/http/cookie.ts +++ b/src/types/http/cookie.ts @@ -20,14 +20,14 @@ export type Cookie = Readonly<{ export type SameSite = 'lax' | 'none' | 'strict'; /** - * Value of cookie (request) header for one or several cookies. + * Value of `cookie` (request) header for one or several cookies. * @example * maps_los=1; _ge=GA1.2.1967685687 */ export type CookieHeaderString = Brand; /** - * Value of set-cookie (response) header for single cookie. + * Value of `set-cookie` (response) header for single cookie. * @example * maps_los=1; expires=Tue, 07-Nov-2023 00:20:49 GMT; path=/; domain=.example.com; Secure; HttpOnly; SameSite=none */ diff --git a/src/types/http/http.ts b/src/types/http/http.ts index 2023ae14..22ceb0d8 100644 --- a/src/types/http/http.ts +++ b/src/types/http/http.ts @@ -1,6 +1,7 @@ import type {IncomingHttpHeaders} from 'node:http'; import type {Brand} from '../brand'; +import type {UtcTimeInMs} from '../date'; import type {CookieHeaderString, SetCookieHeaderString} from './cookie'; import type {StatusCode} from './statusCode'; @@ -76,10 +77,11 @@ export type Request< requestBody: RequestBody; requestHeaders: RequestHeaders; url: Url; + utcTimeInMs?: UtcTimeInMs; }>; /** - * HTTP response object with its corresponding request.. + * HTTP response object with its corresponding request. */ export type Response< ResponseBody = unknown, @@ -87,6 +89,7 @@ export type Response< SomeStatusCode extends StatusCode = StatusCode, SomeRequest extends Request = Request, > = Readonly<{ + completionTimeInMs?: UtcTimeInMs; request?: SomeRequest; responseBody: ResponseBody; responseHeaders: ResponseHeaders; diff --git a/src/types/index.ts b/src/types/index.ts index d3dfa887..2b38f31e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,7 +31,13 @@ export type { PageClassTypeArgs, } from './pages'; export type {FilePathFromRoot, TestFilePath} from './paths'; -export type {AsyncVoid, MaybePromise, UnwrapPromise} from './promise'; +export type { + AsyncVoid, + MaybePromise, + ReExecutablePromise, + Thenable, + UnwrapPromise, +} from './promise'; export type { AnyObject, FieldReplacer, diff --git a/src/types/internal.ts b/src/types/internal.ts index 6b35ead1..b85906b8 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -76,7 +76,13 @@ export type { } from './paths'; /** @internal */ export type {ImgData, PixelmatchOptions} from './pixelmatch'; -export type {AsyncVoid, MaybePromise, UnwrapPromise} from './promise'; +export type { + AsyncVoid, + MaybePromise, + ReExecutablePromise, + Thenable, + UnwrapPromise, +} from './promise'; export type { AnyObject, FieldReplacer, diff --git a/src/types/promise.ts b/src/types/promise.ts index 8621dc38..743bbf49 100644 --- a/src/types/promise.ts +++ b/src/types/promise.ts @@ -1,3 +1,5 @@ +import type {Brand} from './brand'; + /** * `void` or `Promise` as return value for maybe async functions. */ @@ -8,6 +10,18 @@ export type AsyncVoid = MaybePromise; */ export type MaybePromise = Type | Promise; +/** + * Reexecutable promise from TestCafe. + */ +export type ReExecutablePromise = Brand, 'ReExecutablePromise'>; + +/** + * Thenable object, that is, an object with a `then` method. + */ +export type Thenable = Readonly<{ + then: Promise['then']; +}>; + /** * If the type is a promise, unwraps it and returns the promise value type * (until a non-promise value is obtained). diff --git a/src/types/selectors.ts b/src/types/selectors.ts index c137dabc..2148cdd6 100644 --- a/src/types/selectors.ts +++ b/src/types/selectors.ts @@ -80,7 +80,7 @@ export type Selector = ReplaceObjectSelectors & Readonly<{[DESCRIPTION_KEY]?: string}>; /** - * Native `e2ed` methods of selector. + * Custom methods that `e2ed` adds to selector. */ export type SelectorCustomMethods = Readonly<{ /** Creates a selector that filters a matching set by locatorId. */ diff --git a/src/utils/expect/Expect.ts b/src/utils/expect/Expect.ts index 46cc53a1..181d77a3 100644 --- a/src/utils/expect/Expect.ts +++ b/src/utils/expect/Expect.ts @@ -1,21 +1,14 @@ -import {LogEventStatus, LogEventType} from '../../constants/internal'; -import {testController} from '../../testController'; - -import {getDescriptionFromSelector} from '../locators'; -import {log} from '../log'; -import {valueToString, wrapStringForLogs} from '../valueToString'; - import {assertionMessageGetters} from './assertionMessageGetters'; +import {createExpectMethod} from './createExpectMethod'; -import type {Selector} from '../../types/internal'; - -import type {AssertionFunctionKeys, AssertionFunctions} from './types'; +import type {AssertionFunctionKey} from './types'; /** - * testController.expect wrapper with logs. + * `testController.expect` wrapper with logs. * @internal */ -class Expect { +// eslint-disable-next-line import/exports-last +export class Expect { constructor( readonly actualValue: unknown, readonly description: string, // eslint-disable-next-line no-empty-function @@ -25,42 +18,5 @@ class Expect { } for (const [key, getAssertionMessage] of Object.entries(assertionMessageGetters)) { - // eslint-disable-next-line no-restricted-syntax - Expect.prototype[key] = function method(...args: unknown[]) { - const message = getAssertionMessage(...args); - - const assertPromise = new Promise((resolve) => { - const assert = testController.expect(this.actualValue) as AssertionFunctions>; - - assert[key as AssertionFunctionKeys](...args) - .then(() => resolve(undefined)) - .catch((error: Error) => resolve(error)); - }); - - return assertPromise.then((maybeError) => - Promise.resolve(this.actualValue) - .then((actualValue) => - log( - `Assert: ${this.description}`, - { - actualValue, - assertion: wrapStringForLogs(`value ${valueToString(actualValue)} ${message}`), - assertionArguments: args, - error: maybeError, - locator: getDescriptionFromSelector(this.actualValue as Selector), - logEventStatus: maybeError ? LogEventStatus.Failed : LogEventStatus.Passed, - }, - LogEventType.InternalAssert, - ), - ) - .then(() => { - if (maybeError) { - throw maybeError; - } - }), - ); - }; + Expect.prototype[key] = createExpectMethod(key as AssertionFunctionKey, getAssertionMessage); } - -/** @internal */ -export {Expect}; diff --git a/src/utils/expect/assertionMessageGetters.ts b/src/utils/expect/assertionMessageGetters.ts index 7c5acead..d138b6bc 100644 --- a/src/utils/expect/assertionMessageGetters.ts +++ b/src/utils/expect/assertionMessageGetters.ts @@ -1,12 +1,12 @@ import {valueToString} from '../valueToString'; -import type {AssertionFunctions} from './types'; +import type {AssertionFunctionsRecord} from './types'; /** * Assertion message getters. * @internal */ -export const assertionMessageGetters: AssertionFunctions = { +export const assertionMessageGetters: AssertionFunctionsRecord = { contains: (expected) => `contains ${valueToString(expected)}`, eql: (expected) => `is deeply equal to ${valueToString(expected)}`, gt: (expected) => `is strictly greater than ${valueToString(expected)}`, diff --git a/src/utils/expect/createExpectMethod.ts b/src/utils/expect/createExpectMethod.ts new file mode 100644 index 00000000..7b5520cc --- /dev/null +++ b/src/utils/expect/createExpectMethod.ts @@ -0,0 +1,96 @@ +import {LogEventStatus, LogEventType, RESOLVED_PROMISE} from '../../constants/internal'; +import {testController} from '../../testController'; + +import {E2edError} from '../error'; +import {getDurationWithUnits} from '../getDurationWithUnits'; +import {getFullPackConfig} from '../getFullPackConfig'; +import {log} from '../log'; +import {getPromiseWithResolveAndReject} from '../promise'; +import {getDescriptionFromSelector} from '../selectors'; +import {isReExecutablePromise, isThenable} from '../typeGuards'; +import {valueToString, wrapStringForLogs} from '../valueToString'; + +import type {Selector} from '../../types/internal'; + +import type { + AssertionFunction, + AssertionFunctionKey, + AssertionFunctionsRecord, + ExpectMethod, +} from './types'; + +let assertionTimeout: number | undefined; + +/** + * Creates method of `Expect` class. + * @internal + */ +export const createExpectMethod = ( + key: AssertionFunctionKey, + getAssertionMessage: AssertionFunction, +): ExpectMethod => + // eslint-disable-next-line no-restricted-syntax + function method(...args: Parameters) { + assertionTimeout ??= getFullPackConfig().assertionTimeout; + + const timeout = assertionTimeout ?? 0; + const message = getAssertionMessage(...args); + + const {clearRejectTimeout, promiseWithTimeout, reject, setRejectTimeoutFunction} = + getPromiseWithResolveAndReject(timeout); + + setRejectTimeoutFunction(() => { + const timeoutWithUnits = getDurationWithUnits(timeout); + const error = new E2edError( + `${key}-assertion promise rejected after ${timeoutWithUnits} timeout`, + ); + + reject(error); + }); + + const runAssertion = (value: unknown): Promise => { + const assertion = testController.expect(value) as AssertionFunctionsRecord>; + + return assertion[key](...args); + }; + + const assertionPromise = RESOLVED_PROMISE.then(() => { + if ( + isThenable(this.actualValue) && + !isReExecutablePromise(this.actualValue as Promise) + ) { + return this.actualValue.then(runAssertion); + } + + return runAssertion(this.actualValue); + }); + + const assertionPromiseWithTimeout = Promise.race([assertionPromise, promiseWithTimeout]).then( + () => undefined, + (error: Error) => error, + ); + + return assertionPromiseWithTimeout.then((maybeError) => + Promise.all([this.actualValue, promiseWithTimeout]) + .then(([actualValue]) => + log( + `Assert: ${this.description}`, + { + actualValue, + assertion: wrapStringForLogs(`value ${valueToString(actualValue)} ${message}`), + assertionArguments: args, + description: getDescriptionFromSelector(this.actualValue as Selector), + error: maybeError, + logEventStatus: maybeError ? LogEventStatus.Failed : LogEventStatus.Passed, + }, + LogEventType.InternalAssert, + ), + ) + .then(() => { + if (maybeError) { + throw maybeError; + } + }) + .finally(clearRejectTimeout), + ); + }; diff --git a/src/utils/expect/types.ts b/src/utils/expect/types.ts index 6abd0669..9789bf2e 100644 --- a/src/utils/expect/types.ts +++ b/src/utils/expect/types.ts @@ -1,15 +1,29 @@ import type {Inner} from 'testcafe-without-typecheck'; +import type {Expect} from './Expect'; + /** * All assertion functions keys (names of assertion functions, like eql, match, etc). * @internal */ -export type AssertionFunctionKeys = keyof Inner.Assertion; +export type AssertionFunctionKey = keyof Inner.Assertion; + +/** + * Assertion function built in `Expect` class. + * @internal + */ +export type AssertionFunction = (this: void, ...args: readonly unknown[]) => Type; /** * Object with all assertion functions. * @internal */ -export type AssertionFunctions = Readonly< - Record T> +export type AssertionFunctionsRecord = Readonly< + Record> >; + +/** + * Method of `Expect` class. + * @internal + */ +export type ExpectMethod = (this: Expect, ...args: readonly unknown[]) => Promise; diff --git a/src/utils/fn/setCustomInspectOnFunction.ts b/src/utils/fn/setCustomInspectOnFunction.ts index 659e394f..6c83fbdc 100644 --- a/src/utils/fn/setCustomInspectOnFunction.ts +++ b/src/utils/fn/setCustomInspectOnFunction.ts @@ -16,18 +16,18 @@ function getFunctionPresentationForThis(this: Fn): string | StringForLogs { * Set custom `node:inspect` and toJSON presentation (with function code) on function. */ export const setCustomInspectOnFunction = ( - func: Fn, + fn: Fn, ): void => { - assertValueHasProperty(func, inspect.custom, { + assertValueHasProperty(fn, inspect.custom, { check: '`func` has `inspect.custom` property', skipCheckInRuntime: true, }); - if (func[inspect.custom]) { + if (fn[inspect.custom]) { return; } - func[inspect.custom] = getFunctionPresentationForThis; + fn[inspect.custom] = getFunctionPresentationForThis; - (func as unknown as {toJSON(): string | StringForLogs}).toJSON = getFunctionPresentationForThis; + (fn as unknown as {toJSON(): string | StringForLogs}).toJSON = getFunctionPresentationForThis; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 09136245..5d9316d7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -32,17 +32,17 @@ export {removeStyleFromString} from './generalLog'; export {getDurationWithUnits} from './getDurationWithUnits'; export {getFullPackConfig as untypedGetFullPackConfig} from './getFullPackConfig'; export {getKeysCounter} from './getKeysCounter'; -export {getDescriptionFromSelector} from './locators'; export {log} from './log'; export {parseMaybeEmptyValueAsJson} from './parseMaybeEmptyValueAsJson'; export {getPromiseWithResolveAndReject, getTimeoutPromise, waitForAllProperties} from './promise'; export {getContentJsonHeaders, request} from './request'; export {SetHeadersRequestHook} from './requestHooks'; export {getRunLabelObject} from './runLabel'; +export {getDescriptionFromSelector} from './selectors'; export {setReadonlyProperty} from './setReadonlyProperty'; export {getPackageInfo} from './startInfo'; export {wrapInTestRunTracker} from './testRun'; -export {isArray} from './typeGuards'; +export {isArray, isReExecutablePromise, isThenable} from './typeGuards'; export { getShallowCopyOfObjectForLogs, getStringTrimmedToMaxLength, diff --git a/src/utils/locators/index.ts b/src/utils/locators/index.ts deleted file mode 100644 index 6929a5ff..00000000 --- a/src/utils/locators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {getDescriptionFromSelector} from './getDescriptionFromSelector'; diff --git a/src/utils/report/client/render/renderStep.ts b/src/utils/report/client/render/renderStep.ts index 2855f358..ece9ef37 100644 --- a/src/utils/report/client/render/renderStep.ts +++ b/src/utils/report/client/render/renderStep.ts @@ -26,6 +26,7 @@ type Options = Readonly<{ export function renderStep({logEvent, nextLogEventTime}: Options): SafeHtml { const {message, payload, time, type} = logEvent; const durationInMs = nextLogEventTime - time; + const isPayloadEmpty = !payload || Object.keys(payload).length === 0; const status = payload?.logEventStatus ?? LogEventStatus.Passed; let pathToScreenshotFromReportPage: string | undefined; @@ -42,11 +43,15 @@ export function renderStep({logEvent, nextLogEventTime}: Options): SafeHtml { } } - const content = renderStepContent({pathToScreenshotFromReportPage, payload}); + const content = renderStepContent({ + pathToScreenshotFromReportPage, + payload: isPayloadEmpty ? undefined : payload, + }); + const maybeEmptyClass = isPayloadEmpty ? 'step-expanded_is-empty' : ''; const isErrorScreenshot = pathToScreenshotFromReportPage !== undefined; return sanitizeHtml` - diff --git a/src/utils/report/client/render/renderStepContent.ts b/src/utils/report/client/render/renderStepContent.ts index e6545a53..b2b8a619 100644 --- a/src/utils/report/client/render/renderStepContent.ts +++ b/src/utils/report/client/render/renderStepContent.ts @@ -15,9 +15,7 @@ type Options = Readonly<{ * @internal */ export function renderStepContent({pathToScreenshotFromReportPage, payload}: Options): SafeHtml { - const isPayloadEmpty = !payload || Object.keys(payload).length === 0; - - if (isPayloadEmpty) { + if (payload === undefined) { return sanitizeHtml``; } diff --git a/src/utils/requestHooks/getRequestFromRequestOptions.ts b/src/utils/requestHooks/getRequestFromRequestOptions.ts index 899047dd..60f95e73 100644 --- a/src/utils/requestHooks/getRequestFromRequestOptions.ts +++ b/src/utils/requestHooks/getRequestFromRequestOptions.ts @@ -3,7 +3,7 @@ import {URL} from 'node:url'; import {parseMaybeEmptyValueAsJson} from '../parseMaybeEmptyValueAsJson'; -import type {Method, Request, RequestOptions, Url} from '../../types/internal'; +import type {Method, Request, RequestOptions, Url, UtcTimeInMs} from '../../types/internal'; /** * Get Request object from the original TestCafe request options object. @@ -17,6 +17,7 @@ export const getRequestFromRequestOptions = ( isRequestBodyInJsonFormat?: boolean, ): Request => { const url = String(requestOptions.url) as Url; + const utcTimeInMs = Date.now() as UtcTimeInMs; const {search} = new URL(url); const method = (requestOptions.method ?? 'GET').toUpperCase() as Method; @@ -39,5 +40,5 @@ export const getRequestFromRequestOptions = ( const requestHeaders = requestOptions.headers ?? {}; - return {method, query, requestBody, requestHeaders, url}; + return {method, query, requestBody, requestHeaders, url, utcTimeInMs}; }; diff --git a/src/utils/requestHooks/getResponseFromResponseEvent.ts b/src/utils/requestHooks/getResponseFromResponseEvent.ts index 92b867cd..f66c7adf 100644 --- a/src/utils/requestHooks/getResponseFromResponseEvent.ts +++ b/src/utils/requestHooks/getResponseFromResponseEvent.ts @@ -2,7 +2,12 @@ import {parseMaybeEmptyValueAsJson} from '../parseMaybeEmptyValueAsJson'; import {charsetPath, encodingPath} from './testCafeHammerheadUpPaths'; -import type {RequestHookEncoding, RequestHookResponseEvent, Response} from '../../types/internal'; +import type { + RequestHookEncoding, + RequestHookResponseEvent, + Response, + UtcTimeInMs, +} from '../../types/internal'; type CharsetType = typeof import('testcafe-hammerhead-up/lib/processing/encoding/charset').default; type EncodingType = typeof import('testcafe-hammerhead-up/lib/processing/encoding'); @@ -21,6 +26,7 @@ export const getResponseFromResponseEvent = async ({ headers = {}, statusCode = 200, }: RequestHookResponseEvent): Promise => { + const completionTimeInMs = Date.now() as UtcTimeInMs; const charset = new Charset(); const encoding = (headers['content-encoding'] ?? 'identity') as RequestHookEncoding; @@ -36,5 +42,5 @@ export const getResponseFromResponseEvent = async ({ } } - return {responseBody, responseHeaders: headers, statusCode}; + return {completionTimeInMs, responseBody, responseHeaders: headers, statusCode}; }; diff --git a/src/selectors/createCustomMethods.ts b/src/utils/selectors/createCustomMethods.ts similarity index 90% rename from src/selectors/createCustomMethods.ts rename to src/utils/selectors/createCustomMethods.ts index cfb12e20..491f8048 100644 --- a/src/selectors/createCustomMethods.ts +++ b/src/utils/selectors/createCustomMethods.ts @@ -1,9 +1,13 @@ -import {getDescriptionFromSelector} from '../utils/locators'; +import {getDescriptionFromSelector} from './getDescriptionFromSelector'; -import type {GetLocatorAttributeNameFn, Selector, SelectorCustomMethods} from '../types/internal'; +import type { + GetLocatorAttributeNameFn, + Selector, + SelectorCustomMethods, +} from '../../types/internal'; /** - * Creates native `e2ed` methods of selector. + * Creates custom `e2ed` methods of selector (additional to selector's own methods from TestCafe). * @internal */ export const createCustomMethods = ( diff --git a/src/selectors/createSelectorCreator.ts b/src/utils/selectors/createGetTrap.ts similarity index 57% rename from src/selectors/createSelectorCreator.ts rename to src/utils/selectors/createGetTrap.ts index e23ac460..01be0f43 100644 --- a/src/selectors/createSelectorCreator.ts +++ b/src/utils/selectors/createGetTrap.ts @@ -1,24 +1,21 @@ -import {Selector} from 'testcafe-without-typecheck'; - -import {DESCRIPTION_KEY} from '../constants/internal'; -import {setReadonlyProperty} from '../utils/setReadonlyProperty'; +import {DESCRIPTION_KEY} from '../../constants/internal'; import type { - CreateSelector, Fn, - Selector as SelectorType, + Selector, SelectorCustomMethods, TestCafeSelector, Values, -} from '../types/internal'; +} from '../../types/internal'; + +type Return = Required>['get']; /** - * Proxy handler for wrapping all selector properties. + * Creates "get" trap for proxy handler for wrapping all selector properties. + * @internal */ -const createGet = ( - customMethods: SelectorCustomMethods, -): Required>['get'] => { - const get: Required>['get'] = (target, property, receiver) => { +export const createGetTrap = (customMethods: SelectorCustomMethods): Return => { + const get: Return = (target, property, receiver) => { const customMethod = typeof property === 'string' ? customMethods[property as keyof SelectorCustomMethods] @@ -28,7 +25,7 @@ const createGet = ( customMethod ? customMethod.bind(target as unknown as TestCafeSelector) : Reflect.get(target, property, receiver) - ) as Values & {[DESCRIPTION_KEY]?: string}; + ) as Values & {[DESCRIPTION_KEY]?: string}; if (typeof property === 'symbol') { return result; @@ -42,7 +39,7 @@ const createGet = ( const originalFunction = result as Fn; result = // eslint-disable-next-line no-restricted-syntax - function selectorMethodWrapper(this: SelectorType, ...args: never[]) { + function selectorMethodWrapper(this: Selector, ...args: never[]) { const callResult = originalFunction.apply(this, args); if ( @@ -74,22 +71,3 @@ const createGet = ( return get; }; - -/** - * Creates `createSelector` function. - * @internal - */ -export const createSelectorCreator = (customMethods: SelectorCustomMethods): CreateSelector => { - const createSelector: CreateSelector = (...args) => { - const locator = args[0]; - const selector = Selector(...args) as unknown as SelectorType; - - if (typeof locator === 'string') { - setReadonlyProperty(selector, DESCRIPTION_KEY, locator); - } - - return new Proxy(selector, {get: createGet(customMethods)}); - }; - - return createSelector; -}; diff --git a/src/selectors/createSelectorByCssCreator.ts b/src/utils/selectors/createSelectorByCssCreator.ts similarity index 94% rename from src/selectors/createSelectorByCssCreator.ts rename to src/utils/selectors/createSelectorByCssCreator.ts index c653fdee..094c1b13 100644 --- a/src/selectors/createSelectorByCssCreator.ts +++ b/src/utils/selectors/createSelectorByCssCreator.ts @@ -1,4 +1,4 @@ -import type {CreateSelector, CreateSelectorByCss, Selector} from '../types/internal'; +import type {CreateSelector, CreateSelectorByCss, Selector} from '../../types/internal'; /** * Creates `createSelectorByCss` function. diff --git a/src/utils/selectors/createSelectorCreator.ts b/src/utils/selectors/createSelectorCreator.ts new file mode 100644 index 00000000..102ea3c7 --- /dev/null +++ b/src/utils/selectors/createSelectorCreator.ts @@ -0,0 +1,33 @@ +import {Selector as TestCafeSelector} from 'testcafe-without-typecheck'; + +import {DESCRIPTION_KEY} from '../../constants/internal'; + +import {setReadonlyProperty} from '../setReadonlyProperty'; + +import {createCustomMethods} from './createCustomMethods'; +import {createGetTrap} from './createGetTrap'; + +import type {CreateSelector, GetLocatorAttributeNameFn, Selector} from '../../types/internal'; + +/** + * Creates `createSelector` function. + * @internal + */ +export const createSelectorCreator = ( + getLocatorAttributeName: GetLocatorAttributeNameFn, +): CreateSelector => { + const customMethods = createCustomMethods(getLocatorAttributeName); + + const createSelector: CreateSelector = (...args) => { + const locator = args[0]; + const selector = TestCafeSelector(...args) as unknown as Selector; + + if (typeof locator === 'string') { + setReadonlyProperty(selector, DESCRIPTION_KEY, locator); + } + + return new Proxy(selector, {get: createGetTrap(customMethods)}); + }; + + return createSelector; +}; diff --git a/src/utils/locators/getDescriptionFromSelector.ts b/src/utils/selectors/getDescriptionFromSelector.ts similarity index 100% rename from src/utils/locators/getDescriptionFromSelector.ts rename to src/utils/selectors/getDescriptionFromSelector.ts diff --git a/src/selectors/htmlElementSelectorCreator.ts b/src/utils/selectors/htmlElementSelectorCreator.ts similarity index 74% rename from src/selectors/htmlElementSelectorCreator.ts rename to src/utils/selectors/htmlElementSelectorCreator.ts index e8736471..2961c772 100644 --- a/src/selectors/htmlElementSelectorCreator.ts +++ b/src/utils/selectors/htmlElementSelectorCreator.ts @@ -1,4 +1,4 @@ -import type {CreateSelector, Selector} from '../types/internal'; +import type {CreateSelector, Selector} from '../../types/internal'; /** * Creates selector of page HTML element ("documentElement"). diff --git a/src/utils/selectors/index.ts b/src/utils/selectors/index.ts new file mode 100644 index 00000000..6abd16cb --- /dev/null +++ b/src/utils/selectors/index.ts @@ -0,0 +1,9 @@ +/** @internal */ +export {createSelectorCreator} from './createSelectorCreator'; +/** @internal */ +export {createSelectorByCssCreator} from './createSelectorByCssCreator'; +export {getDescriptionFromSelector} from './getDescriptionFromSelector'; +/** @internal */ +export {htmlElementSelectorCreator} from './htmlElementSelectorCreator'; +/** @internal */ +export {locatorIdSelectorCreator} from './locatorIdSelectorCreator'; diff --git a/src/selectors/locatorIdSelectorCreator.ts b/src/utils/selectors/locatorIdSelectorCreator.ts similarity index 94% rename from src/selectors/locatorIdSelectorCreator.ts rename to src/utils/selectors/locatorIdSelectorCreator.ts index 3196f903..85cc614c 100644 --- a/src/selectors/locatorIdSelectorCreator.ts +++ b/src/utils/selectors/locatorIdSelectorCreator.ts @@ -3,7 +3,7 @@ import type { GetLocatorAttributeNameFn, LocatorIdSelector, Selector, -} from '../types/internal'; +} from '../../types/internal'; /** * Creates `locatorIdSelector` function. diff --git a/src/utils/typeGuards.ts b/src/utils/typeGuards.ts index 2d8e00db..d102dee7 100644 --- a/src/utils/typeGuards.ts +++ b/src/utils/typeGuards.ts @@ -1,6 +1,24 @@ +import type {ReExecutablePromise, Thenable} from '../types/internal'; + /** * Returns `true`, if value is array, and `false` otherwise. */ export function isArray(value: unknown): value is Type[] { return Array.isArray(value); } + +/** + * Returns `true`, if value is thenable (an object with a `then` method), and `false` otherwise. + */ +export function isThenable(value: unknown): value is Thenable { + return value instanceof Object && 'then' in value && typeof value.then === 'function'; +} + +/** + * Returns `true`, if value is reexecutable promise, and `false` otherwise. + */ +export function isReExecutablePromise( + promise: Promise, +): promise is ReExecutablePromise { + return isThenable(promise) && '_taskPromise' in promise; +} diff --git a/src/utils/viewport/isSelectorEntirelyInViewport.ts b/src/utils/viewport/isSelectorEntirelyInViewport.ts index 9089fc09..015afd4c 100644 --- a/src/utils/viewport/isSelectorEntirelyInViewport.ts +++ b/src/utils/viewport/isSelectorEntirelyInViewport.ts @@ -1,4 +1,4 @@ -import {htmlElementSelector} from '../../selectors/internal'; +import {Selector as TestCafeSelector} from 'testcafe-without-typecheck'; import type {Selector} from '../../types/internal'; @@ -7,6 +7,8 @@ import type {Selector} from '../../types/internal'; * (all selector points are in the viewport), and `false` otherwise. */ export const isSelectorEntirelyInViewport = async (selector: Selector): Promise => { + const htmlElementSelector = TestCafeSelector('html'); + const clientHeight = await htmlElementSelector.clientHeight; const clientWidth = await htmlElementSelector.clientWidth; diff --git a/src/utils/viewport/isSelectorInViewport.ts b/src/utils/viewport/isSelectorInViewport.ts index 4aad7e9f..230365bb 100644 --- a/src/utils/viewport/isSelectorInViewport.ts +++ b/src/utils/viewport/isSelectorInViewport.ts @@ -1,4 +1,4 @@ -import {htmlElementSelector} from '../../selectors/internal'; +import {Selector as TestCafeSelector} from 'testcafe-without-typecheck'; import type {Selector} from '../../types/internal'; @@ -7,6 +7,8 @@ import type {Selector} from '../../types/internal'; * (intersects with the viewport at least in one point), and `false` otherwise. */ export const isSelectorInViewport = async (selector: Selector): Promise => { + const htmlElementSelector = TestCafeSelector('html'); + const clientHeight = await htmlElementSelector.clientHeight; const clientWidth = await htmlElementSelector.clientWidth; diff --git a/styles/report.css b/styles/report.css index b272c1b8..77beccaf 100644 --- a/styles/report.css +++ b/styles/report.css @@ -583,6 +583,9 @@ a:visited { font-size: 14px; color: var(--font-color); } +.step-expanded.step-expanded_is-empty { + cursor: default; +} .step-expanded_type_inner { padding: 7px calc(var(--test-details-padding) * 2); transition: top 0.2s linear;