diff --git a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts index 7d1a4a0c..ddaef23b 100644 --- a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts +++ b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts @@ -6,6 +6,7 @@ import {setPageCookies, setPageRequestHeaders} from 'autotests/context'; import {E2edReportExample as E2edReportExampleRoute} from 'autotests/routes/pageRoutes'; import {locator} from 'autotests/selectors'; import {Page} from 'e2ed'; +import {click} from 'e2ed/actions'; import {setReadonlyProperty} from 'e2ed/utils'; import {TestRunButton} from './TestRunButton'; @@ -65,7 +66,7 @@ export class E2edReportExample extends Page { * List of test runs of retry. */ get testRunsList(): Selector { - return locator('column1'); + return locator('column-2'); } /** @@ -81,6 +82,10 @@ export class E2edReportExample extends Page { } } + async clickLogo(): Promise { + await click(this.header, {position: {x: 30, y: 30}}); + } + getRoute(): E2edReportExampleRoute { return new E2edReportExampleRoute(); } diff --git a/autotests/tests/e2edReportExample/browserData.ts b/autotests/tests/e2edReportExample/browserData.ts index 5fd1aaa9..e9dd0226 100644 --- a/autotests/tests/e2edReportExample/browserData.ts +++ b/autotests/tests/e2edReportExample/browserData.ts @@ -10,50 +10,56 @@ import { waitForInterfaceStabilization, } from 'e2ed/actions'; -test('correctly read data from browser', {meta: {testId: '14'}}, async () => { - await navigateToPage(E2edReportExample); - - await createClientFunction(() => { - console.error('error'); - console.info('info'); - console.log('log'); - console.warn('warning'); - - setTimeout(() => { - throw new Error('foo'); - }, 100); - })(); - - const consoleMessages = getBrowserConsoleMessages(); - const columnNumber = 12; - const url = ''; - - const consoleMessagesWithoutDate = consoleMessages.map( - ({dateTimeInIso: _, ...messageWithoutDate}) => messageWithoutDate, - ); - - await expect(consoleMessagesWithoutDate, 'getBrowserConsoleMessages read all of messages').eql([ - {args: ['error'], location: {columnNumber, lineNumber: 3, url}, text: 'error', type: 'error'}, - {args: ['info'], location: {columnNumber, lineNumber: 4, url}, text: 'info', type: 'info'}, - {args: ['log'], location: {columnNumber, lineNumber: 5, url}, text: 'log', type: 'log'}, - { - args: ['warning'], - location: {columnNumber, lineNumber: 6, url}, - text: 'warning', - type: 'warning', - }, - ]); - - const jsErrors = getBrowserJsErrors(); - - await expect( - jsErrors.length, - 'getBrowserJsErrors read zero JS errors when there are no errors', - ).eql(0); - - await waitForInterfaceStabilization(100); - - await expect(jsErrors.length, 'getBrowserJsErrors read all JS errors').eql(1); - - await expect(String(jsErrors[0]?.error), 'getBrowserJsErrors read all JS errors').contains('foo'); -}); +test( + 'correctly read data from browser', + {meta: {testId: '14'}, viewportHeight: 1200, viewportWidth: 1600}, + async () => { + await navigateToPage(E2edReportExample); + + await createClientFunction(() => { + console.error('error'); + console.info('info'); + console.log('log'); + console.warn('warning'); + + setTimeout(() => { + throw new Error('foo'); + }, 100); + })(); + + const consoleMessages = getBrowserConsoleMessages(); + const columnNumber = 12; + const url = ''; + + const consoleMessagesWithoutDate = consoleMessages.map( + ({dateTimeInIso: _, ...messageWithoutDate}) => messageWithoutDate, + ); + + await expect(consoleMessagesWithoutDate, 'getBrowserConsoleMessages read all of messages').eql([ + {args: ['error'], location: {columnNumber, lineNumber: 3, url}, text: 'error', type: 'error'}, + {args: ['info'], location: {columnNumber, lineNumber: 4, url}, text: 'info', type: 'info'}, + {args: ['log'], location: {columnNumber, lineNumber: 5, url}, text: 'log', type: 'log'}, + { + args: ['warning'], + location: {columnNumber, lineNumber: 6, url}, + text: 'warning', + type: 'warning', + }, + ]); + + const jsErrors = getBrowserJsErrors(); + + await expect( + jsErrors.length, + 'getBrowserJsErrors read zero JS errors when there are no errors', + ).eql(0); + + await waitForInterfaceStabilization(100); + + await expect(jsErrors.length, 'getBrowserJsErrors read all JS errors').eql(1); + + await expect(String(jsErrors[0]?.error), 'getBrowserJsErrors read all JS errors').contains( + 'foo', + ); + }, +); diff --git a/autotests/tests/internalTypeTests/selectors.skip.ts b/autotests/tests/internalTypeTests/selectors.skip.ts index e90bfc22..40c7a195 100644 --- a/autotests/tests/internalTypeTests/selectors.skip.ts +++ b/autotests/tests/internalTypeTests/selectors.skip.ts @@ -4,8 +4,18 @@ import type {Selector} from 'e2ed/types'; // @ts-expect-error: wrong number of arguments htmlElementSelector.findByTestId(); -// @ts-expect-error: wrong type of arguments -htmlElementSelector.findByTestId(0); +// ok +htmlElementSelector.filterByTestId(0); +// ok +htmlElementSelector.findByTestId(true); +// ok +htmlElementSelector.filterByTestId(undefined); +// ok +htmlElementSelector.filterByTestId(null); +// ok +htmlElementSelector.findByTestId(1, 2, 2); +// ok +htmlElementSelector.filterByTestId('foo', 'bar', 'baz'); // ok htmlElementSelector.findByTestId('id') satisfies Selector; // ok diff --git a/autotests/tests/switchingPagesForRequests.ts b/autotests/tests/switchingPagesForRequests.ts index 38aa80dd..19a15f4c 100644 --- a/autotests/tests/switchingPagesForRequests.ts +++ b/autotests/tests/switchingPagesForRequests.ts @@ -6,7 +6,6 @@ import {E2edReportExample} from 'autotests/pageObjects/pages'; import {GetUsers} from 'autotests/routes/apiRoutes'; import {expect} from 'e2ed'; import { - click, navigateToPage, switchToTab, waitForNewTab, @@ -54,7 +53,7 @@ test( await waitForTimeout(maxNumberOfRequests * 333); const npmPageTab = await waitForNewTab(async () => { - await click(reportPage.header); + await reportPage.clickLogo(); }); await switchToTab(npmPageTab); diff --git a/autotests/tests/switchingPagesForResponses.ts b/autotests/tests/switchingPagesForResponses.ts index 84fb232d..b39d9923 100644 --- a/autotests/tests/switchingPagesForResponses.ts +++ b/autotests/tests/switchingPagesForResponses.ts @@ -6,7 +6,6 @@ import {E2edReportExample} from 'autotests/pageObjects/pages'; import {GetUsers} from 'autotests/routes/apiRoutes'; import {expect} from 'e2ed'; import { - click, navigateToPage, switchToTab, waitForNewTab, @@ -54,7 +53,7 @@ test( await waitForTimeout(maxNumberOfRequests * 333); const npmPageTab = await waitForNewTab(async () => { - await click(reportPage.header); + await reportPage.clickLogo(); }); await switchToTab(npmPageTab); diff --git a/package-lock.json b/package-lock.json index 5db1762a..bb42d2e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.21.0", "license": "MIT", "dependencies": { - "@playwright/test": "1.56.0", + "@playwright/test": "1.57.0", "create-locator": "0.0.27", "get-modules-graph": "0.0.11", "sort-json-keys": "1.0.3" @@ -20,8 +20,8 @@ "e2ed-install-browsers": "bin/installBrowsers.js" }, "devDependencies": { - "@playwright/browser-chromium": "1.56.0", - "@types/node": "24.7.0", + "@playwright/browser-chromium": "1.57.0", + "@types/node": "24.10.1", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -33,7 +33,7 @@ "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-typescript-sort-keys": "3.3.0", "husky": "9.1.7", - "prettier": "3.6.2", + "prettier": "3.7.4", "typescript": "5.9.3" }, "engines": { @@ -183,26 +183,26 @@ } }, "node_modules/@playwright/browser-chromium": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.56.0.tgz", - "integrity": "sha512-+OABx0PwbzoWXO5qOmonvQlIZq0u89XpDkRYf+ZTOs+wsI3r/NV90rzGr8nsJZTj7o10tdPMmuGmZ3OKP9ag4Q==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.57.0.tgz", + "integrity": "sha512-pUg+2p6HwewLp8KCD9G6VYaS2iewdkNkyqMcSIxXBXOlp1ojTxLF6/bwyR4ixLMy6tyv75jhE8PzzMZiX5KzwQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0" + "playwright-core": "1.57.0" }, "engines": { "node": ">=18" } }, "node_modules/@playwright/test": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", - "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0" + "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -231,13 +231,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/semver": { @@ -2674,10 +2674,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3035,12 +3036,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", - "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -3053,9 +3054,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", - "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -3084,9 +3085,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "bin": { @@ -3795,9 +3796,9 @@ } }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 21bb9d05..b535d5ed 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,14 @@ "url": "git+https://github.com/joomcode/e2ed.git" }, "dependencies": { - "@playwright/test": "1.56.0", + "@playwright/test": "1.57.0", "create-locator": "0.0.27", "get-modules-graph": "0.0.11", "sort-json-keys": "1.0.3" }, "devDependencies": { - "@playwright/browser-chromium": "1.56.0", - "@types/node": "24.7.0", + "@playwright/browser-chromium": "1.57.0", + "@types/node": "24.10.1", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -44,7 +44,7 @@ "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-typescript-sort-keys": "3.3.0", "husky": "9.1.7", - "prettier": "3.6.2", + "prettier": "3.7.4", "typescript": "5.9.3" }, "peerDependencies": { diff --git a/src/actions/blur.ts b/src/actions/blur.ts new file mode 100644 index 00000000..6e2f1daa --- /dev/null +++ b/src/actions/blur.ts @@ -0,0 +1,17 @@ +import {LogEventType} from '../constants/internal'; +import {log} from '../utils/log'; + +import type {Locator} from '@playwright/test'; + +import type {Selector} from '../types/internal'; + +type Options = Parameters[0]; + +/** + * Blur an element. + */ +export const blur = async (selector: Selector, options: Options = {}): Promise => { + log('Blur an element', {...options, selector}, LogEventType.InternalAction); + + await selector.getPlaywrightLocator().blur(options); +}; diff --git a/src/actions/focus.ts b/src/actions/focus.ts new file mode 100644 index 00000000..a3fafa8e --- /dev/null +++ b/src/actions/focus.ts @@ -0,0 +1,17 @@ +import {LogEventType} from '../constants/internal'; +import {log} from '../utils/log'; + +import type {Locator} from '@playwright/test'; + +import type {Selector} from '../types/internal'; + +type Options = Parameters[0]; + +/** + * Focuses an element. + */ +export const focus = async (selector: Selector, options: Options = {}): Promise => { + log('Focus an element', {...options, selector}, LogEventType.InternalAction); + + await selector.getPlaywrightLocator().focus(options); +}; diff --git a/src/actions/getBrowserConsoleMessages.ts b/src/actions/getBrowserConsoleMessages.ts index d2dd3043..ecbaf26a 100644 --- a/src/actions/getBrowserConsoleMessages.ts +++ b/src/actions/getBrowserConsoleMessages.ts @@ -4,7 +4,7 @@ import {log} from '../utils/log'; import type {ConsoleMessage} from '../types/internal'; -type Options = Readonly<{showMessagesInLog?: boolean}>; +type Options = Readonly<{showMessagesInLog?: boolean; skipLogs?: boolean}>; const logMessage = 'Get browser console messages'; @@ -12,9 +12,13 @@ const logMessage = 'Get browser console messages'; * Returns an object that contains messages output to the browser console. */ export const getBrowserConsoleMessages = (options: Options = {}): readonly ConsoleMessage[] => { - const {showMessagesInLog = false} = options; + const {skipLogs = false, showMessagesInLog = false} = options; const consoleMessages = getConsoleMessagesFromContext(); + if (skipLogs) { + return consoleMessages; + } + if (showMessagesInLog === false) { log(logMessage, LogEventType.InternalAction); } else { diff --git a/src/actions/index.ts b/src/actions/index.ts index e7ca6783..00118799 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -6,6 +6,7 @@ export { assertSelectorInViewport, assertUrlMatchRoute, } from './asserts'; +export {blur} from './blur'; export {clearUpload} from './clearUpload'; export {click} from './click'; export {deleteCookies} from './deleteCookies'; @@ -13,6 +14,7 @@ export {dispatchEvent} from './dispatchEvent'; export {doubleClick} from './doubleClick'; export {drag} from './drag'; export {dragToElement} from './dragToElement'; +export {focus} from './focus'; export {getBrowserConsoleMessages} from './getBrowserConsoleMessages'; export {getBrowserJsErrors} from './getBrowserJsErrors'; export {getCookies} from './getCookies'; diff --git a/src/actions/resizeWindow.ts b/src/actions/resizeWindow.ts index f2184ebb..89563954 100644 --- a/src/actions/resizeWindow.ts +++ b/src/actions/resizeWindow.ts @@ -1,15 +1,17 @@ import {LogEventType} from '../constants/internal'; +import {getPlaywrightPage} from '../useContext'; import {log} from '../utils/log'; /** * Set the browser window size. */ -export const resizeWindow = (width: number, height: number): Promise => { +export const resizeWindow = async (width: number, height: number): Promise => { log( `Set the browser window size to width ${width} and height ${height}`, LogEventType.InternalAction, ); - // TODO - return Promise.resolve(); + const page = getPlaywrightPage(); + + await page.setViewportSize({height, width}); }; diff --git a/src/actions/scroll.ts b/src/actions/scroll.ts index 20157f3a..40c0efaa 100644 --- a/src/actions/scroll.ts +++ b/src/actions/scroll.ts @@ -37,6 +37,7 @@ export const scroll = ((...args) => { cssString: 'html', parameterAttributePrefix: 'data-test-', testIdAttribute: 'data-testid', + testIdSeparator: '-', }); log( diff --git a/src/constants/internal.ts b/src/constants/internal.ts index b83fc28b..57e115ff 100644 --- a/src/constants/internal.ts +++ b/src/constants/internal.ts @@ -40,6 +40,8 @@ export { export {LogEventStatus, LogEventType} from './log'; /** @internal */ export {MESSAGE_BACKGROUND_COLOR_BY_STATUS} from './log'; +/** @internal */ +export {SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE} from './matchScreenshot'; export {CREATE_PAGE_TOKEN} from './pages'; /** @internal */ export { diff --git a/src/constants/matchScreenshot.ts b/src/constants/matchScreenshot.ts new file mode 100644 index 00000000..2bb6f362 --- /dev/null +++ b/src/constants/matchScreenshot.ts @@ -0,0 +1,6 @@ +/** + * Error message for "Expected screenshot not specified" error from the `toMatchScreenshot` assert (in `expect`). + * @internal + */ +export const SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE = + 'Expected screenshot not specified. If the actual screenshot is correct, specify screenshot from actualScreenshotId field'; diff --git a/src/selectors/createSelectorFunction.ts b/src/selectors/createSelectorFunction.ts index 11041913..1f7aae03 100644 --- a/src/selectors/createSelectorFunction.ts +++ b/src/selectors/createSelectorFunction.ts @@ -11,7 +11,5 @@ export const createSelectorFunction = ( ): CreateSelector => { generalLog('Create selector function', {attributesOptions}); - const {parameterAttributePrefix, testIdAttribute} = attributesOptions; - - return (cssString) => Selector.create({cssString, parameterAttributePrefix, testIdAttribute}); + return (cssString) => Selector.create({cssString, ...attributesOptions}); }; diff --git a/src/test.ts b/src/test.ts index e7a34f60..e3e0776e 100644 --- a/src/test.ts +++ b/src/test.ts @@ -32,5 +32,11 @@ export const test: TestFunction = (name, options, testFn) => { playwrightTest.use({bypassCSP: !options.enableCsp}); } + if (options.viewportHeight !== undefined && options.viewportWidth !== undefined) { + playwrightTest.use({ + viewport: {height: options.viewportHeight, width: options.viewportWidth}, + }); + } + playwrightTest(playwrightTestName, runTest); }; diff --git a/src/types/checks.ts b/src/types/checks.ts index 6a86867b..d945a051 100644 --- a/src/types/checks.ts +++ b/src/types/checks.ts @@ -1,7 +1,7 @@ /** * This type checks that the type `true` is passed to it. */ -export type Expect = T; +export type Expect = Type; /** * Returns `true` if types are exactly equal and `false` otherwise. @@ -9,7 +9,7 @@ export type Expect = T; * IsEqual<{readonly foo: string}, {foo: string}> = false. */ export type IsEqual = - (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; + (() => Type extends X ? 1 : 2) extends () => Type extends Y ? 1 : 2 ? true : false; /** * Returns `true` if key is readonly in object and `false` otherwise. diff --git a/src/types/testRun.ts b/src/types/testRun.ts index e9a491d2..99c8dbae 100644 --- a/src/types/testRun.ts +++ b/src/types/testRun.ts @@ -69,14 +69,19 @@ export type TestFn = (testController: PlaywrightTestArgs) => Promise; /** * Test options with userland metadata. */ -export type TestOptions = DeepReadonly<{ - enableCsp?: boolean; - meta: TestMeta; - takeFullPageScreenshotOnError?: boolean; - takeViewportScreenshotOnError?: boolean; - testIdleTimeout?: number; - testTimeout?: number; -}>; +export type TestOptions = DeepReadonly< + { + enableCsp?: boolean; + meta: TestMeta; + takeFullPageScreenshotOnError?: boolean; + takeViewportScreenshotOnError?: boolean; + testIdleTimeout?: number; + testTimeout?: number; + } & ( + | {viewportHeight: number; viewportWidth: number} + | {viewportHeight?: undefined; viewportWidth?: undefined} + ) +>; /** * The complete static test options, that is, the options diff --git a/src/utils/expect/additionalMatchers.ts b/src/utils/expect/additionalMatchers.ts index 30bd319e..a9c5b1d8 100644 --- a/src/utils/expect/additionalMatchers.ts +++ b/src/utils/expect/additionalMatchers.ts @@ -5,81 +5,79 @@ import {toMatchScreenshot} from './toMatchScreenshot'; import type {Expect} from './Expect'; import type {NonSelectorAdditionalMatchers, SelectorMatchers} from './types'; -import {expect} from '@playwright/test'; +import {expect as playwrightExpect} from '@playwright/test'; /** * Addition matchers. * @internal */ export const additionalMatchers: NonSelectorAdditionalMatchers & SelectorMatchers = { - contains(this: Expect, expected) { + async contains(this: Expect, expected) { const {actualValue, description} = this; if (typeof actualValue === 'string' || Array.isArray(actualValue)) { - return Promise.resolve(expect(actualValue, description).toContain(expected)); + return playwrightExpect(actualValue, description).toContain(expected); } - return Promise.resolve( - expect(actualValue, description).toEqual( - expect.objectContaining(expected as Record), - ), + return playwrightExpect(actualValue, description).toEqual( + playwrightExpect.objectContaining(expected as Record), ); }, async eql(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toEqual(expected); + return playwrightExpect(actualValue, description).toEqual(expected); }, async gt(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toBeGreaterThan(expected); + return playwrightExpect(actualValue, description).toBeGreaterThan(expected); }, async gte(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toBeGreaterThanOrEqual(expected); + return playwrightExpect(actualValue, description).toBeGreaterThanOrEqual(expected); }, async lt(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toBeLessThan(expected); + return playwrightExpect(actualValue, description).toBeLessThan(expected); }, async lte(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toBeLessThanOrEqual(expected); + return playwrightExpect(actualValue, description).toBeLessThanOrEqual(expected); }, async match(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).toMatch(expected); + return playwrightExpect(actualValue, description).toMatch(expected); }, async notContains(this: Expect, expected) { const {actualValue, description} = this; if (typeof actualValue === 'string' || Array.isArray(actualValue)) { - return expect(actualValue, description).not.toContain(expected); + return playwrightExpect(actualValue, description).not.toContain(expected); } - return expect(actualValue, description).not.toEqual( - expect.objectContaining(expected as Record), + return playwrightExpect(actualValue, description).not.toEqual( + playwrightExpect.objectContaining(expected as Record), ); }, async notEql(this: Expect, expected) { const {actualValue, description} = this; - return expect(actualValue, description).not.toEqual(expected); + return playwrightExpect(actualValue, description).not.toEqual(expected); }, async notOk(this: Expect) { const {actualValue, description} = this; - return expect(actualValue, description).not.toBeTruthy(); + return playwrightExpect(actualValue, description).not.toBeTruthy(); }, async ok(this: Expect) { const {actualValue, description} = this; - return expect(actualValue, description).toBeTruthy(); + return playwrightExpect(actualValue, description).toBeTruthy(); }, toMatchScreenshot(this: Expect, expectedScreenshotId, options = {}) { diff --git a/src/utils/expect/applyAdditionalMatcher.ts b/src/utils/expect/applyAdditionalMatcher.ts index 69ba2e3a..e678394d 100644 --- a/src/utils/expect/applyAdditionalMatcher.ts +++ b/src/utils/expect/applyAdditionalMatcher.ts @@ -4,7 +4,7 @@ import type {Fn, Selector, SelectorPropertyRetryData} from '../../types/internal import type {Expect} from './Expect'; -import {expect} from '@playwright/test'; +import {expect as playwrightExpect} from '@playwright/test'; /** * Apply additional matcher (with retrying, if needed). @@ -25,7 +25,7 @@ export const applyAdditionalMatcher = ( let context: Expect; - return expect(() => { + return playwrightExpect(() => { const {args: selectorArgs, property, selector} = selectorPropertyRetryData; const actualValue = diff --git a/src/utils/expect/createExpectMethod.ts b/src/utils/expect/createExpectMethod.ts index 668a10e3..cd446a13 100644 --- a/src/utils/expect/createExpectMethod.ts +++ b/src/utils/expect/createExpectMethod.ts @@ -18,7 +18,7 @@ import type {Fn, SelectorPropertyRetryData} from '../../types/internal'; import type {Expect} from './Expect'; import type {AssertionFunction, ExpectMethod} from './types'; -import {expect} from '@playwright/test'; +import {expect as playwrightExpect} from '@playwright/test'; const additionalAssertionTimeoutInMs = 1_000; @@ -66,7 +66,7 @@ export const createExpectMethod = ( }); } - const assertion = expect(value, ctx.description) as unknown as Record< + const assertion = playwrightExpect(value, ctx.description) as unknown as Record< string, Fn> >; diff --git a/src/utils/expect/toMatchScreenshot.ts b/src/utils/expect/toMatchScreenshot.ts index 17e48a2a..106e828a 100644 --- a/src/utils/expect/toMatchScreenshot.ts +++ b/src/utils/expect/toMatchScreenshot.ts @@ -6,6 +6,7 @@ import {isLocalRun} from '../../configurator'; import { EXPECTED_SCREENSHOTS_DIRECTORY_PATH, INTERNAL_REPORTS_DIRECTORY_PATH, + SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE, } from '../../constants/internal'; import {getOutputDirectoryName} from '../../context/outputDirectoryName'; @@ -20,7 +21,7 @@ import type {FilePathFromRoot, Selector, ToMatchScreenshotOptions, Url} from '.. import type {Expect} from './Expect'; -import {expect} from '@playwright/test'; +import {expect as playwrightExpect} from '@playwright/test'; type AdditionalLogFields = { actualScreenshotId: string | undefined; @@ -84,7 +85,7 @@ export const toMatchScreenshot = async ( const message = expectedScreenshotId ? `Cannot read expected screenshot ${expectedScreenshotId}` - : 'Expected screenshot not specified'; + : SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE; if (isLocalRun) { if (expectedScreenshotFound) { @@ -99,7 +100,7 @@ export const toMatchScreenshot = async ( try { const playwrightLocator = actualValue.getPlaywrightLocator(); - await expect(playwrightLocator, description).toHaveScreenshot(screenshotFileName, { + await playwrightExpect(playwrightLocator, description).toHaveScreenshot(screenshotFileName, { mask: mask.map((selector) => selector.getPlaywrightLocator()), ...restOptions, }); diff --git a/src/utils/report/client/addDomContentLoadedHandler.ts b/src/utils/report/client/addDomContentLoadedHandler.ts index d04bd1c6..17f032ba 100644 --- a/src/utils/report/client/addDomContentLoadedHandler.ts +++ b/src/utils/report/client/addDomContentLoadedHandler.ts @@ -4,7 +4,7 @@ * This client function should not use scope variables (except global functions). * @internal */ -export function addDomContentLoadedHandler(handler: () => void): void { +export const addDomContentLoadedHandler = (handler: () => void): void => { if (document.readyState !== 'loading') { handler(); @@ -12,4 +12,4 @@ export function addDomContentLoadedHandler(handler: () => void): void { } document.addEventListener('DOMContentLoaded', handler); -} +}; diff --git a/src/utils/report/client/chooseTestRun.tsx b/src/utils/report/client/chooseTestRun.tsx index 09b20317..e62b0b4b 100644 --- a/src/utils/report/client/chooseTestRun.tsx +++ b/src/utils/report/client/chooseTestRun.tsx @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import {assertValueIsDefined as clientAssertValueIsDefined} from './assertValueIsDefined'; import { MaybeApiStatistics as clientMaybeApiStatistics, @@ -19,11 +21,10 @@ declare const reportClientState: ReportClientState; * @internal */ // eslint-disable-next-line max-statements -export function chooseTestRun(runHash: RunHash): void { +export const chooseTestRun = (runHash: RunHash): void => { const {e2edRightColumnContainer} = reportClientState; - if (e2edRightColumnContainer === undefined) { - // eslint-disable-next-line no-console + if (!e2edRightColumnContainer) { console.error( 'Cannot find right column container (id="e2edRightColumnContainer"). Probably page not yet completely loaded. Please try click again later', ); @@ -45,6 +46,10 @@ export function chooseTestRun(runHash: RunHash): void { e2edRightColumnContainer.firstElementChild as HTMLElement | null; if (!previousTestRunDetailsElement) { + console.error( + 'Cannot find first child element in right column container (id="e2edRightColumnContainer"). Probably page not yet completely loaded. Please try click again later', + ); + return; } @@ -72,7 +77,6 @@ export function chooseTestRun(runHash: RunHash): void { const fullTestRun = fullTestRuns.find((testRun) => testRun.runHash === runHash); if (fullTestRun === undefined) { - // eslint-disable-next-line no-console console.error( `Cannot find test run with hash ${runHash} in JSON report data. Probably JSON report data for this test run not yet loaded. Please try click again later`, ); @@ -88,4 +92,4 @@ export function chooseTestRun(runHash: RunHash): void { const nextTestRunDetailsElement = e2edRightColumnContainer.firstElementChild as HTMLElement; testRunDetailsElementsByHash[runHash] = nextTestRunDetailsElement; -} +}; diff --git a/src/utils/report/client/createJsxRuntime.ts b/src/utils/report/client/createJsxRuntime.ts index 1fa0e241..6d02fd0e 100644 --- a/src/utils/report/client/createJsxRuntime.ts +++ b/src/utils/report/client/createJsxRuntime.ts @@ -15,7 +15,7 @@ const sanitizeHtml = clientSanitizeHtml; * This client function should not use scope variables (except global functions). * @internal */ -export function createJsxRuntime(): JSX.Runtime { +export const createJsxRuntime = (): JSX.Runtime => { const maxDepth = 8; const createElement: JSX.CreateElement = (type, properties, ...children) => { @@ -104,4 +104,4 @@ export function createJsxRuntime(): JSX.Runtime { }; return {Fragment, createElement}; -} +}; diff --git a/src/utils/report/client/groupLogEvents.ts b/src/utils/report/client/groupLogEvents.ts index c4e3b760..86454dc3 100644 --- a/src/utils/report/client/groupLogEvents.ts +++ b/src/utils/report/client/groupLogEvents.ts @@ -1,4 +1,4 @@ -import {LogEventType} from '../../../constants/internal'; +import {LogEventStatus, LogEventType} from '../../../constants/internal'; import type {LogEvent, LogEventWithChildren} from '../../../types/internal'; @@ -16,14 +16,18 @@ export const groupLogEvents = (logEvents: readonly LogEvent[]): readonly LogEven LogEventType.InternalAssert, ]; + const isTopLevelEvent = (logEvent: LogEvent): boolean => + topLevelTypes.includes(logEvent.type) || + logEvent.payload?.logEventStatus === LogEventStatus.Failed; + const result: LogEventWithChildren[] = []; for (const logEvent of logEvents) { const last = result.at(-1); const newEvent: LogEventWithChildren = {children: [], ...logEvent}; - if (topLevelTypes.includes(logEvent.type)) { - if (last && !topLevelTypes.includes(last.type)) { + if (isTopLevelEvent(logEvent)) { + if (last && !isTopLevelEvent(last)) { const firstTopLevel: LogEventWithChildren = { children: [...result], message: 'Initialization', @@ -38,7 +42,7 @@ export const groupLogEvents = (logEvents: readonly LogEvent[]): readonly LogEven } result.push(newEvent); - } else if (last && topLevelTypes.includes(last.type)) { + } else if (last && isTopLevelEvent(last)) { (last.children as LogEventWithChildren[]).push(newEvent); } else { result.push(newEvent); diff --git a/src/utils/report/client/onDomContentLoad.ts b/src/utils/report/client/onDomContentLoad.ts index ae1521fa..04790b9d 100644 --- a/src/utils/report/client/onDomContentLoad.ts +++ b/src/utils/report/client/onDomContentLoad.ts @@ -11,10 +11,10 @@ const readJsonReportData = clientReadJsonReportData; * This client function should not use scope variables (except global functions). * @internal */ -export function onDomContentLoad(): void { +export const onDomContentLoad = (): void => { const e2edRightColumnContainer = document.getElementById('e2edRightColumnContainer') ?? undefined; - if (e2edRightColumnContainer === undefined) { + if (!e2edRightColumnContainer) { // eslint-disable-next-line no-console console.error( 'Cannot find right column container (id="e2edRightColumnContainer") after DOMContentLoaded.', @@ -32,4 +32,4 @@ export function onDomContentLoad(): void { } reportClientState.readJsonReportDataObservers.length = 0; -} +}; diff --git a/src/utils/report/client/parseMarkdownLinks.ts b/src/utils/report/client/parseMarkdownLinks.ts index 83632e45..39c59e3d 100644 --- a/src/utils/report/client/parseMarkdownLinks.ts +++ b/src/utils/report/client/parseMarkdownLinks.ts @@ -13,10 +13,10 @@ const sanitizeHtml = clientSanitizeHtml; * This client function should not use scope variables (except global functions). * @internal */ -export function parseMarkdownLinks( +export const parseMarkdownLinks = ( stringParts: readonly string[], ...values: readonly unknown[] -): SafeHtml { +): SafeHtml => { const sanitizedHtml = sanitizeHtml(stringParts, ...values); const htmlWithLinks = sanitizedHtml.replace( @@ -25,4 +25,4 @@ export function parseMarkdownLinks( ); return createSafeHtmlWithoutSanitize`${htmlWithLinks}`; -} +}; diff --git a/src/utils/report/client/readJsonReportData.ts b/src/utils/report/client/readJsonReportData.ts index bcdb3a62..e22dbb1c 100644 --- a/src/utils/report/client/readJsonReportData.ts +++ b/src/utils/report/client/readJsonReportData.ts @@ -15,7 +15,7 @@ declare const reportClientState: ReportClientState; * This client function should not use scope variables (except global functions). * @internal */ -export function readJsonReportData(areAllScriptsLoaded = false): void { +export const readJsonReportData = (areAllScriptsLoaded = false): void => { const {lengthOfReadedJsonReportDataParts} = reportClientState; const scripts = document.querySelectorAll('body > script.e2edJsonReportData'); const {length} = scripts; @@ -40,4 +40,4 @@ export function readJsonReportData(areAllScriptsLoaded = false): void { } reportClientState.lengthOfReadedJsonReportDataParts = newLength; -} +}; diff --git a/src/utils/report/client/readPartOfJsonReportData.ts b/src/utils/report/client/readPartOfJsonReportData.ts index 23c27ffe..6d7b9383 100644 --- a/src/utils/report/client/readPartOfJsonReportData.ts +++ b/src/utils/report/client/readPartOfJsonReportData.ts @@ -13,7 +13,7 @@ type Options = Readonly<{ * This client function should not use scope variables (except global functions). * @internal */ -export function readPartOfJsonReportData({scriptToRead, shouldLogError}: Options): boolean { +export const readPartOfJsonReportData = ({scriptToRead, shouldLogError}: Options): boolean => { try { const data = JSON.parse(scriptToRead?.textContent ?? '') as ScriptJsonData; @@ -32,4 +32,4 @@ export function readPartOfJsonReportData({scriptToRead, shouldLogError}: Options } return true; -} +}; diff --git a/src/utils/report/client/render/Step.tsx b/src/utils/report/client/render/Step.tsx index 2e5ee7f6..3fc2a39c 100644 --- a/src/utils/report/client/render/Step.tsx +++ b/src/utils/report/client/render/Step.tsx @@ -21,6 +21,7 @@ type Props = Readonly<{ isEnd?: boolean; logEvent: LogEventWithChildren; nextLogEventTime: UtcTimeInMs; + open?: boolean; }>; /** @@ -28,7 +29,7 @@ type Props = Readonly<{ * This base client function should not use scope variables (except other base functions). * @internal */ -export const Step: JSX.Component = ({isEnd = false, logEvent, nextLogEventTime}) => { +export const Step: JSX.Component = ({isEnd = false, logEvent, nextLogEventTime, open}) => { const {children, message, payload, time, type} = logEvent; const date = new Date(time).toISOString(); const isPayloadEmpty = !payload || Object.keys(payload).length === 0; @@ -49,6 +50,7 @@ export const Step: JSX.Component = ({isEnd = false, logEvent, nextLogEven } let content = <>; + const isErrorScreenshot = pathToScreenshotOfPage !== undefined; if (!isEnd) { content = @@ -60,7 +62,7 @@ export const Step: JSX.Component = ({isEnd = false, logEvent, nextLogEven ) : ( -
+
{message} diff --git a/src/utils/report/client/render/TestRunDescription.tsx b/src/utils/report/client/render/TestRunDescription.tsx index 3fc09f3c..4cb652f0 100644 --- a/src/utils/report/client/render/TestRunDescription.tsx +++ b/src/utils/report/client/render/TestRunDescription.tsx @@ -31,7 +31,7 @@ export const TestRunDescription: JSX.Component = ({fullTestRun}) => { const valueWithLinks = parseMarkdownLinks`${value}`; const metaHtml = ( <> -
{key}
+
{key}
{' '}
{valueWithLinks}
); @@ -49,7 +49,7 @@ export const TestRunDescription: JSX.Component = ({fullTestRun}) => { traceHtml = ( <> -
{traceLabel}
+
{traceLabel}
{' '}
{traceName} @@ -63,11 +63,11 @@ export const TestRunDescription: JSX.Component = ({fullTestRun}) => {
{traceHtml} -
Date
+
Date
{' '}
-
Duration
+
Duration
{' '}
diff --git a/src/utils/report/client/sanitizeHtml.ts b/src/utils/report/client/sanitizeHtml.ts index 349aefa0..ec8869e7 100644 --- a/src/utils/report/client/sanitizeHtml.ts +++ b/src/utils/report/client/sanitizeHtml.ts @@ -70,10 +70,10 @@ export function sanitizeValue(value: unknown): string { * This base client function should not use scope variables (except other base functions). * @internal */ -export function sanitizeHtml( +export const sanitizeHtml = ( stringParts: readonly string[], ...values: readonly unknown[] -): SafeHtml { +): SafeHtml => { const parts: string[] = []; for (let index = 0; index < values.length; index += 1) { @@ -96,13 +96,11 @@ export function sanitizeHtml( const html = parts.join(''); return createSafeHtmlWithoutSanitize`${html}`; -} +}; /** * Sanitizes JSON string (simple protection against XSS attacks). * This base client function should not use scope variables (except other base functions). * @internal */ -export function sanitizeJson(json: string): string { - return json.replace(/ json.replace(/ { const observeChildList = {childList: true}; const {readJsonReportDataObservers} = reportClientState; const scriptsObserver = new MutationObserver(() => readJsonReportData()); @@ -35,4 +35,4 @@ export function setReadJsonReportDataObservers(): void { htmlObserver.observe(document.documentElement, observeChildList); } -} +}; diff --git a/src/utils/report/collectReportData.ts b/src/utils/report/collectReportData.ts index ddb00a8a..cf3e6de8 100644 --- a/src/utils/report/collectReportData.ts +++ b/src/utils/report/collectReportData.ts @@ -38,8 +38,8 @@ export const collectReportData = async ({ const retries = getRetries(fullTestRuns); const exitCode = getExitCode(errors.length > 0, retries); - const failedTestsMainParams = getFailedTestsMainParams(retries.at(-1)); - const summaryPackResults = getSummaryPackResults(fullTestRuns, retries.at(-1)); + const failedTestsMainParams = getFailedTestsMainParams(retries); + const summaryPackResults = getSummaryPackResults(fullTestRuns, retries); return { apiStatistics, diff --git a/src/utils/report/getFailedTestsMainParams.ts b/src/utils/report/getFailedTestsMainParams.ts index bb637d71..cd46eceb 100644 --- a/src/utils/report/getFailedTestsMainParams.ts +++ b/src/utils/report/getFailedTestsMainParams.ts @@ -1,4 +1,6 @@ -import {TestRunStatus} from '../../constants/internal'; +import {SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE, TestRunStatus} from '../../constants/internal'; + +import {assertValueIsFalse} from '../asserts'; import type {Retry} from '../../types/internal'; @@ -6,11 +8,35 @@ import type {Retry} from '../../types/internal'; * Get array of main parameters of pack's failed tests. * @internal */ -export const getFailedTestsMainParams = (lastRetry: Retry | undefined): readonly string[] => { +export const getFailedTestsMainParams = (retries: readonly Retry[]): readonly string[] => { + const firstRetry = retries[0]; + const lastRetry = retries.at(-1); + const failedTests = lastRetry?.fullTestRuns.filter((fullTestRun) => fullTestRun.status === TestRunStatus.Failed) ?? []; const failedTestsMainParams = failedTests.map(({mainParams}) => mainParams); + if (retries.length <= 1) { + return failedTestsMainParams; + } + + const failedScreenshotTests = + firstRetry?.fullTestRuns.filter( + (fullTestRun) => + fullTestRun.status === TestRunStatus.Failed && + String(fullTestRun.runError).includes(SCREENSHOT_NOT_SPECIFIED_ERROR_MESSAGE), + ) ?? []; + + for (const failedScreenshotTest of failedScreenshotTests) { + assertValueIsFalse( + failedTestsMainParams.includes(failedScreenshotTest.mainParams), + 'mainParams of failed screenshot test is unique', + {duplicatedTest: failedScreenshotTest, failedScreenshotTests}, + ); + + failedTestsMainParams.push(failedScreenshotTest.mainParams); + } + return failedTestsMainParams; }; diff --git a/src/utils/report/getSummaryPackResults.ts b/src/utils/report/getSummaryPackResults.ts index 9f6253b2..ec706e43 100644 --- a/src/utils/report/getSummaryPackResults.ts +++ b/src/utils/report/getSummaryPackResults.ts @@ -16,9 +16,11 @@ const MAX_FAILED_TESTS_COUNT = 8; */ export const getSummaryPackResults = ( fullTestRuns: readonly FullTestRun[], - lastRetry: Retry | undefined, + retries: readonly Retry[], ): string => { - const allFailedTestsMainParams = getFailedTestsMainParams(lastRetry); + const lastRetry = retries.at(-1); + + const allFailedTestsMainParams = getFailedTestsMainParams(retries); const failedTestsMainParams = allFailedTestsMainParams.slice(0, MAX_FAILED_TESTS_COUNT); if (allFailedTestsMainParams.length > MAX_FAILED_TESTS_COUNT) { @@ -34,7 +36,10 @@ export const getSummaryPackResults = ( ? fullTestRuns : lastRetry?.fullTestRuns; - const count = source?.filter((fullTestRun) => fullTestRun.status === status)?.length ?? 0; + const count = + status === TestRunStatus.Failed + ? allFailedTestsMainParams.length + : (source?.filter((fullTestRun) => fullTestRun.status === status)?.length ?? 0); if (count > 0) { countsOfStatuses.push(`${count} ${status}`); diff --git a/src/utils/report/render/Logo.tsx b/src/utils/report/render/Logo.tsx index ab1ba3b8..833907f5 100644 --- a/src/utils/report/render/Logo.tsx +++ b/src/utils/report/render/Logo.tsx @@ -5,6 +5,8 @@ import {INSTALLED_E2ED_DIRECTORY_PATH, READ_FILE_OPTIONS} from '../../../constan import {SafeHtml} from '../client'; +import {locator} from './locator'; + declare const jsx: JSX.Runtime; /** @@ -24,6 +26,7 @@ export const Logo: JSX.Component = () => { target="_blank" title="e2ed package" aria-label="e2ed package" + {...locator('logo')} >
diff --git a/src/utils/report/render/Script.tsx b/src/utils/report/render/Script.tsx index 6021ce0d..69d37cf4 100644 --- a/src/utils/report/render/Script.tsx +++ b/src/utils/report/render/Script.tsx @@ -13,6 +13,6 @@ export const Script: JSX.Component = () => ( ); diff --git a/src/utils/report/render/ScriptGlobals.tsx b/src/utils/report/render/ScriptGlobals.tsx index 81b13f52..214e6607 100644 --- a/src/utils/report/render/ScriptGlobals.tsx +++ b/src/utils/report/render/ScriptGlobals.tsx @@ -15,13 +15,12 @@ declare const jsx: JSX.Runtime; * @internal */ export const ScriptGlobals: JSX.Component = () => { - const e2edRightColumnContainer = {} as unknown as HTMLElement; const locator = {} as unknown as ReportClientState['locator']; const {pathToScreenshotsDirectoryForReport} = getFullPackConfig(); const reportClientState: ReportClientState = { createLocatorOptions, - e2edRightColumnContainer, + e2edRightColumnContainer: undefined, fullTestRuns: [], internalDirectoryName: INTERNAL_DIRECTORY_NAME, lengthOfReadedJsonReportDataParts: 0, @@ -30,7 +29,9 @@ export const ScriptGlobals: JSX.Component = () => { readJsonReportDataObservers: [], }; - const code = `var jsx; const reportClientState = ${JSON.stringify(reportClientState)};`; + const code = `var jsx; +const reportClientState = ${JSON.stringify(reportClientState)}; +`; return ; }; diff --git a/src/utils/selectors/Selector.ts b/src/utils/selectors/Selector.ts index 368f7e01..b85be74d 100644 --- a/src/utils/selectors/Selector.ts +++ b/src/utils/selectors/Selector.ts @@ -4,13 +4,18 @@ import {inspect} from 'node:util'; import {RETRY_KEY} from '../../constants/internal'; import {getFrameContext} from '../../context/frameContext'; +import { + type AttributesOptions, + createTestLocator, + type LocatorFunction, + type Stringifiable, +} from '../../createLocator'; import {getPlaywrightPage} from '../../useContext'; import {getAttributeCssSelector} from './getAttributeCssSelector'; import type {Locator as PlaywrightLocator} from '@playwright/test'; -import type {AttributesOptions} from '../../createLocator'; import type {SelectorPropertyRetryData} from '../../types/internal'; const setRetryData = ( @@ -35,7 +40,7 @@ type Options = Readonly<{ kind?: Kind; parentSelector?: Selector; }> & - Omit; + AttributesOptions; /** * Selector. @@ -49,12 +54,16 @@ class Selector { private readonly kind: 'css' | 'filter' | 'find' | 'nth' | 'parent' | 'withText'; + private readonly locator: LocatorFunction; + private readonly parameterAttributePrefix: string; private readonly parentSelector: Selector | undefined; private readonly testIdAttribute: string; + private readonly testIdSeparator: string; + protected constructor({ args, cssString, @@ -62,15 +71,22 @@ class Selector { parameterAttributePrefix, parentSelector, testIdAttribute, + testIdSeparator, }: Options) { this.args = args; this.cssString = cssString; this.description = kind === 'css' ? cssString : `${parentSelector!.description}.${kind}(${args?.join(', ')})`; this.kind = kind; + this.locator = createTestLocator({ + attributesOptions: {parameterAttributePrefix, testIdAttribute, testIdSeparator}, + createLocatorByCssSelector: (selector) => selector, + supportWildcardsInCssSelectors: true, + }).locator; this.parameterAttributePrefix = parameterAttributePrefix; this.parentSelector = parentSelector; this.testIdAttribute = testIdAttribute; + this.testIdSeparator = testIdSeparator; } get boundingClientRect(): Promise { @@ -90,10 +106,10 @@ class Selector { return result; } - get checked(): Promise { + get checked(): Promise { const result = this.getPlaywrightLocator() .isChecked() - .catch(() => undefined); + .catch(() => null); setRetryData(result, {property: 'checked', selector: this}); @@ -160,10 +176,10 @@ class Selector { return result; } - get value(): Promise { + get value(): Promise { const result = this.getPlaywrightLocator() .inputValue() - .catch(() => undefined); + .catch(() => null); setRetryData(result, {property: 'value', selector: this}); @@ -182,14 +198,15 @@ class Selector { cssString, parameterAttributePrefix, testIdAttribute, - }: Pick): Selector { - return new Selector({cssString, parameterAttributePrefix, testIdAttribute}); + testIdSeparator, + }: Pick): Selector { + return new Selector({cssString, parameterAttributePrefix, testIdAttribute, testIdSeparator}); } createSelector(cssString: string): Selector { - const {parameterAttributePrefix, testIdAttribute} = this; + const {parameterAttributePrefix, testIdAttribute, testIdSeparator} = this; - return new Selector({cssString, parameterAttributePrefix, testIdAttribute}); + return new Selector({cssString, parameterAttributePrefix, testIdAttribute, testIdSeparator}); } filter(cssSelectorString: string): Selector { @@ -200,8 +217,8 @@ class Selector { return this.filter(getAttributeCssSelector(this.getParameterAttribute(parameter), value)); } - filterByTestId(testId: string): Selector { - return this.filter(getAttributeCssSelector(this.testIdAttribute, testId)); + filterByTestId(...args: readonly [Stringifiable, ...Stringifiable[]]): Selector { + return this.filter(this.locator(...(args as [Stringifiable]))); } find(cssSelectorString: string): Selector { @@ -212,8 +229,8 @@ class Selector { return this.find(getAttributeCssSelector(this.getParameterAttribute(parameter), value)); } - findByTestId(testId: string): Selector { - return this.find(getAttributeCssSelector(this.testIdAttribute, testId)); + findByTestId(...args: readonly [Stringifiable, ...Stringifiable[]]): Selector { + return this.find(this.locator(...(args as [Stringifiable]))); } getAttribute(attributeName: string): Promise { @@ -316,7 +333,7 @@ class Selector { } private createChildSelector(kind: Kind, args?: Args): Selector { - const {cssString, parameterAttributePrefix, testIdAttribute} = this; + const {cssString, parameterAttributePrefix, testIdAttribute, testIdSeparator} = this; return new Selector({ args, @@ -325,6 +342,7 @@ class Selector { parameterAttributePrefix, parentSelector: this, testIdAttribute, + testIdSeparator, }); } diff --git a/src/utils/test/beforeTest.ts b/src/utils/test/beforeTest.ts index 3d94f87b..36a2ddd7 100644 --- a/src/utils/test/beforeTest.ts +++ b/src/utils/test/beforeTest.ts @@ -21,7 +21,7 @@ import {getTestFnAndReject} from './getTestFnAndReject'; import type {TestRunEvent, TestUnit, UtcTimeInMs} from '../../types/internal'; -import {test} from '@playwright/test'; +import {test as playwrightTest} from '@playwright/test'; const additionToPlaywrightTestTimeout = 500; @@ -59,7 +59,9 @@ export const beforeTest = ({ const testTimeout = IS_DEBUG || isUiMode ? MAX_TIMEOUT_IN_MS : (options.testTimeout ?? testTimeoutFromConfig); - test.setTimeout(testTimeout + additionToPlaywrightTestTimeout + (Date.now() - startTimeInMs)); + playwrightTest.setTimeout( + testTimeout + additionToPlaywrightTestTimeout + (Date.now() - startTimeInMs), + ); setTestIdleTimeout(testIdleTimeout); setTestTimeout(testTimeout); @@ -75,6 +77,7 @@ export const beforeTest = ({ const {onlog, reject, testFnWithReject} = getTestFnAndReject({ isSkipped, runId, + skipReason, testFn, testIdleTimeout, testTimeout, diff --git a/src/utils/test/getTestFnAndReject.ts b/src/utils/test/getTestFnAndReject.ts index 9676b390..ccea1ea8 100644 --- a/src/utils/test/getTestFnAndReject.ts +++ b/src/utils/test/getTestFnAndReject.ts @@ -8,9 +8,12 @@ import {getPromiseWithResolveAndReject} from '../promise'; import type {Onlog, RejectTestRun, RunId, TestFn, Void} from '../../types/internal'; +import {test as playwrightTest} from '@playwright/test'; + type Options = Readonly<{ isSkipped: boolean; runId: RunId; + skipReason: string | undefined; testFn: TestFn; testIdleTimeout: number; testTimeout: number; @@ -18,26 +21,31 @@ type Options = Readonly<{ type Return = Readonly<{onlog: Onlog; reject: RejectTestRun; testFnWithReject: TestFn}>; -const skippedTestFnAndReject: Return = { - onlog: () => undefined, - reject: () => undefined, - testFnWithReject: () => RESOLVED_PROMISE, -}; - /** * Get test function with execution timeout, idle timeout, reject and onlog functions, - * by isSkipped flag, test function, runId, test execution timeout and test idle timeouts. + * by `isSkipped` flag, test function, runId, test execution timeout and test idle timeouts. * @internal */ export const getTestFnAndReject = ({ isSkipped, runId, + skipReason, testFn, testIdleTimeout, testTimeout, }: Options): Return => { if (isSkipped) { - return skippedTestFnAndReject; + return { + onlog: () => undefined, + reject: () => undefined, + testFnWithReject: () => { + try { + playwrightTest.skip(true, skipReason); + } catch {} + + return RESOLVED_PROMISE; + }, + }; } const { diff --git a/src/utils/test/waitBeforeRetry.ts b/src/utils/test/waitBeforeRetry.ts index d5d797c5..248ddf52 100644 --- a/src/utils/test/waitBeforeRetry.ts +++ b/src/utils/test/waitBeforeRetry.ts @@ -7,7 +7,7 @@ import {getPreviousRunId} from './getPreviousRunId'; import type {FullTestRun, RunId, TestStaticOptions} from '../../types/internal'; -import {test} from '@playwright/test'; +import {test as playwrightTest} from '@playwright/test'; const additionToTimeout = 10_000; @@ -57,7 +57,7 @@ export const waitBeforeRetry = async ( return; } - test.setTimeout(timeoutInMs + additionToTimeout); + playwrightTest.setTimeout(timeoutInMs + additionToTimeout); const timeoutObject = setInterval(() => { void writeLogEventTime(); diff --git a/styles/report.css b/styles/report.css index b6f2d870..bc66611a 100644 --- a/styles/report.css +++ b/styles/report.css @@ -82,7 +82,6 @@ body { margin: 0; padding: 0; width: 100%; - height: auto; min-height: 100vh; font-family: system-ui; font-size: var(--font-size); @@ -121,6 +120,7 @@ a:visited { .errors, .warnings { padding: 2px var(--retry-padding); + overflow-wrap: break-word; } .errors__error { margin: 4px 0; @@ -146,7 +146,6 @@ a:visited { .column-1 { grid-area: column-1; width: 100vw; - overflow: hidden; } .header, .column-1 { @@ -176,6 +175,9 @@ a:visited { background: var(--item-bg-color); } .main { + overflow: hidden; + contain: layout; + container-type: inline-size; grid-area: main; min-width: 0; display: flex; @@ -553,7 +555,7 @@ a:visited { color: var(--subtitle-color); } .test-description__term::after { - content: ':\00a0'; + content: ':'; } .test-description__definition { margin: 0; @@ -997,7 +999,7 @@ a:visited { position: relative; overflow: hidden; - contain: layout paint; + container-type: inline-size; display: flex; flex-direction: column; margin-left: calc(var(--horizontal-line-to-icon-width) + var(--test-link-half-icon-width)); @@ -1064,7 +1066,6 @@ a:visited { text-decoration: none; } .test-link { - container-type: inline-size; margin-bottom: 1px; text-align: left; } @@ -1073,10 +1074,14 @@ a:visited { } .test-link[aria-current='true'] { --box-shadow-width: 1px; + --box-shadow-color: var(--passed-color); - box-shadow: 0 0 0 var(--box-shadow-width) var(--drag-container-bg-color) inset; + box-shadow: 0 0 0 var(--box-shadow-width) var(--box-shadow-color) inset; cursor: default; } +.test-link[aria-current='true'][data-status='failed'] { + --box-shadow-color: var(--failed-color); +} .test-link[data-status='failed'], .test-link[data-status='unknown'], .step[data-status='failed'] > .step__head, @@ -1207,7 +1212,7 @@ a:visited { overflow: hidden; border-radius: var(--main-radius); background: var(--secondary-bg-color); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 10px rgb(0, 0, 0, 0.3); } .screenshot-dialog[open] { display: flex; @@ -1227,7 +1232,7 @@ a:visited { text-overflow: ellipsis; } .screenshot-dialog::backdrop { - background: rgba(0, 0, 0, 0.5); + background: rgb(0, 0, 0, 0.5); } .screenshot-dialog__main { padding: var(--padding); @@ -1349,10 +1354,6 @@ button:focus-visible, outline-color: var(--accent-color); outline-offset: -2px; } -.main { - contain: layout; - container-type: inline-size; -} @container (min-width: 350px) { .test-link__name, .step__name { @@ -1362,17 +1363,20 @@ button:focus-visible, } @media (min-width: 390px) { + body { + height: 100vh; + } .main { flex-direction: row; } .column-2 { + overflow-y: auto; width: calc(40% - var(--drag-container-width)); } .column-3 { - max-height: 100vh; + overflow-y: auto; will-change: transform; flex: 1 0 180px; - overflow-y: scroll; } .drag-container { position: relative;