diff --git a/.eslintignore b/.eslintignore index 2341bc551..0d98e309d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,3 +15,4 @@ hot /testplane-report tmp **/playwright-report +**/html-report/* diff --git a/.eslintrc.js b/.eslintrc.js index aad139d8b..21d545924 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,7 +30,8 @@ module.exports = { files: ['test/**'], rules: { // For convenient casting of test objects - '@typescript-eslint/no-explicit-any': 'off' + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-restricted-imports': 'off' } } ] diff --git a/.github/workflows/component-tests.yml b/.github/workflows/component-tests.yml new file mode 100644 index 000000000..aba5508b9 --- /dev/null +++ b/.github/workflows/component-tests.yml @@ -0,0 +1,98 @@ +name: Testplane Component Tests + +on: + pull_request: + branches: [ master ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + testplane-component: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + env: + DOCKER_IMAGE_NAME: html-reporter-browsers + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build the package + run: npm run build + + - name: 'Prepare component tests: Cache browser docker image' + uses: actions/cache@v3 + with: + path: ~/.docker/cache + key: docker-browser-image-${{ hashFiles('test/func/docker/Dockerfile') }} + + - name: 'Prepare component tests: Pull browser docker image' + run: | + mkdir -p ~/.docker/cache + if [ -f ~/.docker/cache/image.tar ]; then + docker load -i ~/.docker/cache/image.tar + else + docker pull yinfra/html-reporter-browsers + docker save yinfra/html-reporter-browsers -o ~/.docker/cache/image.tar + fi + + - name: 'Prepare component tests: Run browser docker image' + run: docker run -d --name ${{ env.DOCKER_IMAGE_NAME }} -it --rm --network=host $(which colima >/dev/null || echo --add-host=host.docker.internal:0.0.0.0) yinfra/html-reporter-browsers + + - name: 'component-tests: Run Testplane' + id: 'testplane' + continue-on-error: true + run: npm run component-tests + + - name: 'component-tests: Stop browser docker image' + run: | + docker kill ${{ env.DOCKER_IMAGE_NAME }} || true + docker rm ${{ env.DOCKER_IMAGE_NAME }} || true + + - name: Upload Testplane html-reporter reports to S3 + uses: jakejarvis/s3-sync-action@v0.5.1 + with: + args: --acl public-read --follow-symlinks + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_S3_ENDPOINT: https://s3.yandexcloud.net/ + SOURCE_DIR: "test/component/html-report" + DEST_DIR: "testplane-ci/component-tests-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/" + + - name: Construct PR comment + run: | + link="https://${{ secrets.AWS_S3_BUCKET }}.s3.yandexcloud.net/testplane-ci/component-tests-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/index.html" + if [ "${{ steps.testplane.outcome }}" != "success" ]; then + comment="### ❌ Component tests failed

[Report](${link})" + echo "PR_COMMENT=${comment}" >> $GITHUB_ENV + else + comment="### ✅ Component tests succeed

[Report](${link})" + echo "PR_COMMENT=${comment}" >> $GITHUB_ENV + fi + + - name: Leave comment to PR with link to Testplane HTML reports + if: github.event.pull_request + uses: thollander/actions-comment-pull-request@v3 + with: + message: ${{ env.PR_COMMENT }} + comment-tag: testplane_component_results + + - name: Fail the job if any Testplane job is failed + if: ${{ steps.testplane.outcome != 'success' }} + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b29cf0932..ebb3bf0bd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -64,12 +64,6 @@ jobs: - name: 'Prepare e2e tests: Generate fixtures' run: npm run e2e:generate-fixtures || true - - name: 'Prepare e2e tests: Setup env' - run: | - REPORT_PREFIX=testplane-reports - REPORT_DATE=$(date '+%Y-%m-%d') - echo "DEST_REPORTS_DIR=$REPORT_PREFIX/$REPORT_DATE/${{ github.run_id }}/${{ github.run_attempt }}" >> $GITHUB_ENV - - name: 'e2e: Run Testplane' id: 'testplane' continue-on-error: true @@ -84,25 +78,29 @@ jobs: - name: Merge Testplane html-reporter reports working-directory: 'test/func/tests' run: | - mkdir -p ../../../${{ env.DEST_REPORTS_DIR }} - npx testplane merge-reports reports/*/sqlite.db -d ../../../${{ env.DEST_REPORTS_DIR }} + mkdir -p ../../../e2e-report + npx testplane merge-reports reports/*/sqlite.db -d ../../../e2e-report - - name: Deploy Testplane html-reporter reports - uses: peaceiris/actions-gh-pages@v4 + - name: Upload Testplane html-reporter reports to S3 + uses: jakejarvis/s3-sync-action@v0.5.1 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ${{ env.DEST_REPORTS_DIR }} - destination_dir: ${{ env.DEST_REPORTS_DIR }} - keep_files: true + args: --acl public-read --follow-symlinks + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_S3_ENDPOINT: https://s3.yandexcloud.net/ + SOURCE_DIR: "e2e-report" + DEST_DIR: "testplane-ci/e2e-tests-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/" - name: Construct PR comment run: | - link="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ env.DEST_REPORTS_DIR }}" + link="https://${{ secrets.AWS_S3_BUCKET }}.s3.yandexcloud.net/testplane-ci/e2e-tests-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/index.html" if [ "${{ steps.testplane.outcome }}" != "success" ]; then - comment="### ❌ Testplane run failed

[Report](${link})" + comment="### ❌ E2E tests failed

[Report](${link})" echo "PR_COMMENT=${comment}" >> $GITHUB_ENV else - comment="### ✅ Testplane run succeed

[Report](${link})" + comment="### ✅ E2E tests succeed

[Report](${link})" echo "PR_COMMENT=${comment}" >> $GITHUB_ENV fi @@ -111,7 +109,7 @@ jobs: uses: thollander/actions-comment-pull-request@v3 with: message: ${{ env.PR_COMMENT }} - comment-tag: testplane_results + comment-tag: testplane_e2e_results - name: Fail the job if any Testplane job is failed if: ${{ steps.testplane.outcome != 'success' }} diff --git a/.gitignore b/.gitignore index ef3b67f6a..30d96d2c1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ html-reporter-*.tgz **/playwright-report hermione-report +html-report testplane-report test/func/**/report test/func/**/report-backup diff --git a/lib/constants/diff-modes.ts b/lib/constants/diff-modes.ts index cccab011e..595847a8d 100644 --- a/lib/constants/diff-modes.ts +++ b/lib/constants/diff-modes.ts @@ -1,3 +1,4 @@ +import {pick} from 'lodash'; import type {ValueOf} from 'type-fest'; export const DiffModes = { @@ -35,9 +36,16 @@ export const DiffModes = { id: 'onion-skin', title: 'Onion skin', description: 'Onion Skin. Change the image opacity to compare expected and actual images.' + }, + TWO_UP_INTERACTIVE: { + id: '2-up-interactive', + title: '2-up Interactive', + description: '2-up Interactive. Compare expected and actual images side by side with synchronized pan and zoom.' } } as const; +export const ClassicDiffModes = pick(DiffModes, ['THREE_UP', 'THREE_UP_SCALED', 'THREE_UP_SCALED_TO_FIT', 'ONLY_DIFF', 'SWITCH', 'SWIPE', 'ONION_SKIN']); + export type DiffModes = typeof DiffModes; export type DiffMode = ValueOf; diff --git a/lib/constants/index.ts b/lib/constants/index.ts index 68992a17e..9223c00ef 100644 --- a/lib/constants/index.ts +++ b/lib/constants/index.ts @@ -11,4 +11,5 @@ export * from './plugin-events'; export * from './save-formats'; export * from './test-statuses'; export * from './tool-names'; +export * from './two-up-modes'; export * from './view-modes'; diff --git a/lib/constants/local-storage.ts b/lib/constants/local-storage.ts index 41dd0913a..0027b435e 100644 --- a/lib/constants/local-storage.ts +++ b/lib/constants/local-storage.ts @@ -4,6 +4,8 @@ export enum LocalStorageKey { } export const TIME_TRAVEL_PLAYER_VISIBILITY_KEY = 'time-travel-player-visibility'; +export const TWO_UP_DIFF_VISIBILITY_KEY = '2up-diff-visibility'; +export const TWO_UP_FIT_MODE_KEY = '2up-fit-mode'; export enum UiMode { Old = 'old', diff --git a/lib/constants/two-up-modes.ts b/lib/constants/two-up-modes.ts new file mode 100644 index 000000000..a9315b938 --- /dev/null +++ b/lib/constants/two-up-modes.ts @@ -0,0 +1,4 @@ +export enum TwoUpFitMode { + FitToView = 'fit-to-view', + FitToWidth = 'fit-to-width' +} diff --git a/lib/static/components/controls/common-controls.jsx b/lib/static/components/controls/common-controls.jsx index da4903618..a3773ab1c 100644 --- a/lib/static/components/controls/common-controls.jsx +++ b/lib/static/components/controls/common-controls.jsx @@ -10,7 +10,7 @@ import BaseHostInput from './base-host-input'; import MenuBar from './menu-bar'; import ReportInfo from './report-info'; import {ViewMode} from '../../../constants/view-modes'; -import {DiffModes} from '../../../constants/diff-modes'; +import {ClassicDiffModes} from '../../../constants/diff-modes'; import {EXPAND_ALL, COLLAPSE_ALL, EXPAND_ERRORS, EXPAND_RETRIES} from '../../../constants/expand-modes'; class ControlButtons extends Component { @@ -77,7 +77,7 @@ class ControlButtons extends Component { label="Diff mode" value={view.diffMode} handler={diffModeId => actions.setDiffMode({diffModeId})} - options = {Object.values(DiffModes).map((dm) => { + options = {Object.values(ClassicDiffModes).map((dm) => { return {value: dm.id, content: dm.title}; })} extendClassNames="diff-mode" diff --git a/lib/static/components/modals/screenshot-accepter/header.jsx b/lib/static/components/modals/screenshot-accepter/header.jsx index 68873a2d9..7b1d36961 100644 --- a/lib/static/components/modals/screenshot-accepter/header.jsx +++ b/lib/static/components/modals/screenshot-accepter/header.jsx @@ -1,13 +1,13 @@ import React, {Component, Fragment} from 'react'; import {GlobalHotKeys} from 'react-hotkeys'; import PropTypes from 'prop-types'; -import {uniqBy} from 'lodash'; +import {uniqBy, pick} from 'lodash'; import ProgressBar from '../../progress-bar'; import ControlButton from '../../controls/control-button'; import ControlSelect from '../../controls/selects/control'; import RetrySwitcher from '../../retry-switcher'; -import {DiffModes} from '../../../../constants/diff-modes'; +import {ClassicDiffModes} from '../../../../constants/diff-modes'; import {ChevronsExpandUpRight, ArrowUturnCcwDown, ArrowUp, ArrowDown, Check} from '@gravity-ui/icons'; import {staticImageAccepterPropType} from '../../../modules/static-image-accepter'; @@ -194,7 +194,7 @@ export default class ScreenshotAccepterHeader extends Component { label="Diff mode" value={view.diffMode} handler={diffModeId => actions.setDiffMode({diffModeId})} - options = {Object.values(DiffModes).map((dm) => { + options = {Object.values(ClassicDiffModes).map((dm) => { return {value: dm.id, content: dm.title}; })} extendClassNames="screenshot-accepter__diff-mode-select" diff --git a/lib/static/components/state/state-fail/index.jsx b/lib/static/components/state/state-fail/index.jsx index d9abd9438..9b42685d1 100644 --- a/lib/static/components/state/state-fail/index.jsx +++ b/lib/static/components/state/state-fail/index.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import React, {Fragment, useEffect, useState} from 'react'; import {connect} from 'react-redux'; -import {DiffModes} from '@/constants'; +import {ClassicDiffModes as DiffModes} from '@/constants'; import {SwitchMode} from '@/static/new-ui/components/DiffViewer/SwitchMode'; import {SwipeMode} from '@/static/new-ui/components/DiffViewer/SwipeMode'; import {OnionSkinMode} from '@/static/new-ui/components/DiffViewer/OnionSkinMode'; diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index fd488510c..12cad9a26 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -63,6 +63,9 @@ export default { PAGE_SET_SECTION_SIZES: 'PAGE_SET_SECTION_SIZES', PAGE_SET_BACKUP_SECTION_SIZES: 'PAGE_SET_BACKUP_SECTION_SIZES', VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 'VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE', + VISUAL_CHECKS_TOGGLE_2UP_DIFF_VISIBILITY: 'VISUAL_CHECKS_TOGGLE_2UP_DIFF_VISIBILITY', + VISUAL_CHECKS_SET_2UP_FIT_MODE: 'VISUAL_CHECKS_SET_2UP_FIT_MODE', + VISUAL_CHECKS_SET_DIFF_MODE: 'VISUAL_CHECKS_SET_DIFF_MODE', UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS', UPDATE_LOADING_VISIBILITY: 'UPDATE_LOADING_VISIBILITY', UPDATE_LOADING_TITLE: 'UPDATE_LOADING_TITLE', diff --git a/lib/static/modules/actions/visual-checks-page.ts b/lib/static/modules/actions/visual-checks-page.ts index 553335a1a..fdf77d6ba 100644 --- a/lib/static/modules/actions/visual-checks-page.ts +++ b/lib/static/modules/actions/visual-checks-page.ts @@ -1,5 +1,6 @@ import actionNames from '@/static/modules/action-names'; import {Action} from '@/static/modules/actions/types'; +import {DiffModeId, TwoUpFitMode} from '@/constants'; export type VisualChecksPageSetCurrentNamedImageAction = Action; + +export const toggle2UpDiffVisibility = (isVisible: boolean): Toggle2UpDiffVisibilityAction => { + return {type: actionNames.VISUAL_CHECKS_TOGGLE_2UP_DIFF_VISIBILITY, payload: {isVisible}}; +}; + +export type Set2UpFitModeAction = Action; + +export const set2UpFitMode = (fitMode: TwoUpFitMode): Set2UpFitModeAction => { + return {type: actionNames.VISUAL_CHECKS_SET_2UP_FIT_MODE, payload: {fitMode}}; +}; + +export type SetVisualChecksDiffModeAction = Action; + +export const setVisualChecksDiffMode = (diffModeId: DiffModeId): SetVisualChecksDiffModeAction => { + return {type: actionNames.VISUAL_CHECKS_SET_DIFF_MODE, payload: {diffModeId}}; +}; + export type VisualChecksPageAction = - | VisualChecksPageSetCurrentNamedImageAction; + | VisualChecksPageSetCurrentNamedImageAction + | Toggle2UpDiffVisibilityAction + | Set2UpFitModeAction + | SetVisualChecksDiffModeAction; diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index 5b8c76a45..1a75bf2fb 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -3,7 +3,7 @@ import {ViewMode} from '../../constants/view-modes'; import {DiffModes} from '../../constants/diff-modes'; import {EXPAND_ERRORS} from '../../constants/expand-modes'; import {RESULT_KEYS} from '../../constants/group-tests'; -import {ToolName} from '../../constants'; +import {ToolName, TwoUpFitMode} from '../../constants'; import {Page, SortDirection, State, TreeViewMode} from '@/static/new-ui/types/store'; import {MIN_SECTION_SIZE_PERCENT} from '../new-ui/features/suites/constants'; @@ -119,7 +119,8 @@ export default Object.assign({config: configDefaults}, { nameFilter: '', useRegexFilter: false, useMatchCaseFilter: false, - filteredBrowsers: [] + filteredBrowsers: [], + diffMode: DiffModes.TWO_UP_INTERACTIVE.id }, loading: { taskTitle: 'Loading Testplane UI', @@ -165,7 +166,9 @@ export default Object.assign({config: configDefaults}, { }, [Page.visualChecksPage]: { sectionSizes: [MIN_SECTION_SIZE_PERCENT, 100 - MIN_SECTION_SIZE_PERCENT], - backupSectionSizes: [MIN_SECTION_SIZE_PERCENT, 100 - MIN_SECTION_SIZE_PERCENT] + backupSectionSizes: [MIN_SECTION_SIZE_PERCENT, 100 - MIN_SECTION_SIZE_PERCENT], + is2UpDiffVisible: true, + twoUpFitMode: TwoUpFitMode.FitToWidth }, staticImageAccepterToolbar: { offset: {x: 0, y: 0} diff --git a/lib/static/modules/middlewares/local-storage.js b/lib/static/modules/middlewares/local-storage.js index 61d426b27..947b77c89 100644 --- a/lib/static/modules/middlewares/local-storage.js +++ b/lib/static/modules/middlewares/local-storage.js @@ -17,6 +17,7 @@ export default store => next => action => { localStorageWrapper.setItem('app.suitesPage.viewMode', app.suitesPage.viewMode); localStorageWrapper.setItem('app.visualChecksPage.viewMode', app.visualChecksPage.viewMode); + localStorageWrapper.setItem('app.visualChecksPage.diffMode', app.visualChecksPage.diffMode); } return result; @@ -28,6 +29,7 @@ function shouldUpdateLocalStorage(actionType) { actionNames.INIT_GUI_REPORT, actionNames.INIT_STATIC_REPORT, actionNames.CHANGE_VIEW_MODE, - actionNames.SET_DIFF_MODE + actionNames.SET_DIFF_MODE, + actionNames.VISUAL_CHECKS_SET_DIFF_MODE ].includes(actionType); } diff --git a/lib/static/modules/reducers/filters.ts b/lib/static/modules/reducers/filters.ts index d3bd25bf2..e90246fdc 100644 --- a/lib/static/modules/reducers/filters.ts +++ b/lib/static/modules/reducers/filters.ts @@ -1,7 +1,7 @@ import {Page, State} from '@/static/new-ui/types/store'; import actionNames from '@/static/modules/action-names'; import {FiltersAction, InitGuiReportAction, InitStaticReportAction} from '@/static/modules/actions'; -import {ViewMode} from '@/constants'; +import {DiffModeId, DiffModes, ViewMode} from '@/constants'; import {BrowserItem} from '@/types'; import * as localStorageWrapper from '@/static/modules/local-storage-wrapper'; import {getViewQuery} from '@/static/modules/custom-queries'; @@ -26,6 +26,7 @@ export default (state: State, action: FiltersAction | InitGuiReportAction | Init case actionNames.INIT_STATIC_REPORT: { const suitesPageViewMode = localStorageWrapper.getItem('app.suitesPage.viewMode', ViewMode.ALL) as ViewMode; const visualChecksPageViewMode = localStorageWrapper.getItem('app.visualChecksPage.viewMode', ViewMode.ALL) as ViewMode; + const visualChecksPageDiffMode = localStorageWrapper.getItem('app.visualChecksPage.diffMode', DiffModes.TWO_UP_INTERACTIVE.id) as DiffModeId; const viewQuery = getViewQuery(window.location.search); @@ -41,7 +42,8 @@ export default (state: State, action: FiltersAction | InitGuiReportAction | Init viewMode: suitesPageViewMode }, [Page.visualChecksPage]: { - viewMode: visualChecksPageViewMode + viewMode: visualChecksPageViewMode, + diffMode: visualChecksPageDiffMode } } } diff --git a/lib/static/modules/reducers/suites-page.ts b/lib/static/modules/reducers/suites-page.ts index aa4c9eee6..c820323ec 100644 --- a/lib/static/modules/reducers/suites-page.ts +++ b/lib/static/modules/reducers/suites-page.ts @@ -6,7 +6,8 @@ import {getSuitesTreeViewData} from '@/static/new-ui/features/suites/components/ import {findTreeNodeByBrowserId, findTreeNodeById, getGroupId} from '@/static/new-ui/features/suites/utils'; import * as localStorageWrapper from '../local-storage-wrapper'; import {MIN_SECTION_SIZE_PERCENT} from '@/static/new-ui/features/suites/constants'; -import {TIME_TRAVEL_PLAYER_VISIBILITY_KEY} from '@/constants/local-storage'; +import {TIME_TRAVEL_PLAYER_VISIBILITY_KEY, TWO_UP_DIFF_VISIBILITY_KEY, TWO_UP_FIT_MODE_KEY} from '@/constants/local-storage'; +import {TwoUpFitMode} from '@/constants'; const SECTION_SIZES_LOCAL_STORAGE_KEY = 'suites-page-section-sizes'; @@ -59,6 +60,8 @@ export default (state: State, action: SomeAction): State => { ) as number[]; const isSnapshotsPlayerVisible = Boolean(localStorageWrapper.getItem(TIME_TRAVEL_PLAYER_VISIBILITY_KEY, true)); + const is2UpDiffVisible = Boolean(localStorageWrapper.getItem(TWO_UP_DIFF_VISIBILITY_KEY, true)); + const twoUpFitMode = localStorageWrapper.getItem(TWO_UP_FIT_MODE_KEY, TwoUpFitMode.FitToView) as TwoUpFitMode; return applyStateUpdate(state, { app: { @@ -75,7 +78,9 @@ export default (state: State, action: SomeAction): State => { isSnapshotsPlayerVisible }, visualChecksPage: { - sectionSizes: visualChecksSectionSizes + sectionSizes: visualChecksSectionSizes, + is2UpDiffVisible, + twoUpFitMode } } }); diff --git a/lib/static/modules/reducers/visual-checks-page.ts b/lib/static/modules/reducers/visual-checks-page.ts index 777d34772..867f20e59 100644 --- a/lib/static/modules/reducers/visual-checks-page.ts +++ b/lib/static/modules/reducers/visual-checks-page.ts @@ -2,11 +2,39 @@ import {State} from '@/static/new-ui/types/store'; import actionNames from '@/static/modules/action-names'; import {applyStateUpdate} from '@/static/modules/utils/state'; import {VisualChecksPageAction} from '@/static/modules/actions'; +import * as localStorageWrapper from '../local-storage-wrapper'; +import {TWO_UP_DIFF_VISIBILITY_KEY, TWO_UP_FIT_MODE_KEY} from '@/constants/local-storage'; export default (state: State, action: VisualChecksPageAction): State => { switch (action.type) { case actionNames.VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: return applyStateUpdate(state, {app: {visualChecksPage: {currentNamedImageId: action.payload.namedImageId}}}) as State; + case actionNames.VISUAL_CHECKS_TOGGLE_2UP_DIFF_VISIBILITY: + localStorageWrapper.setItem(TWO_UP_DIFF_VISIBILITY_KEY, action.payload.isVisible); + return applyStateUpdate(state, { + ui: { + visualChecksPage: { + is2UpDiffVisible: action.payload.isVisible + } + } + }) as State; + case actionNames.VISUAL_CHECKS_SET_2UP_FIT_MODE: + localStorageWrapper.setItem(TWO_UP_FIT_MODE_KEY, action.payload.fitMode); + return applyStateUpdate(state, { + ui: { + visualChecksPage: { + twoUpFitMode: action.payload.fitMode + } + } + }) as State; + case actionNames.VISUAL_CHECKS_SET_DIFF_MODE: + return applyStateUpdate(state, { + app: { + visualChecksPage: { + diffMode: action.payload.diffModeId + } + } + }) as State; default: return state; } diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 93a554d53..116728fab 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -29,6 +29,7 @@ --g-color-base-background: #eee; --g-color-text-danger-heavy: #e9043a; --g-color-text-brand-heavy: var(--g-color-private-color-550-solid); + font-family: var(--g-font-family-sans), sans-serif !important; } body { @@ -38,7 +39,6 @@ body { .report { min-width: 100%; min-height: 100%; - font-family: var(--g-font-family-sans), sans-serif !important; margin-bottom: 0 !important;; } diff --git a/lib/static/new-ui/components/AssertViewResult/index.tsx b/lib/static/new-ui/components/AssertViewResult/index.tsx index 369319ceb..2a7560e97 100644 --- a/lib/static/new-ui/components/AssertViewResult/index.tsx +++ b/lib/static/new-ui/components/AssertViewResult/index.tsx @@ -1,5 +1,4 @@ import React, {ReactNode} from 'react'; -import {connect} from 'react-redux'; import {ImageEntity} from '@/static/new-ui/types/store'; import {DiffModeId, TestStatus} from '@/constants'; @@ -15,7 +14,7 @@ interface AssertViewResultProps { diffMode: DiffModeId; } -function AssertViewResultInternal({result, diffMode, style}: AssertViewResultProps): ReactNode { +export function AssertViewResult({result, diffMode, style}: AssertViewResultProps): ReactNode { if (result.status === TestStatus.FAIL) { return ; } @@ -50,7 +49,3 @@ function AssertViewResultInternal({result, diffMode, style}: AssertViewResultPro return null; } - -export const AssertViewResult = connect(state => ({ - diffMode: state.view.diffMode -}))(AssertViewResultInternal); diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.module.css b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.module.css new file mode 100644 index 000000000..1d74889fe --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.module.css @@ -0,0 +1,23 @@ +.toolbar { + position: absolute; + bottom: 16px; + right: 16px; + display: flex; + flex-direction: column; + gap: 4px; + z-index: 100; + pointer-events: auto; + --g-button-background-color: rgba(255, 255, 255, 0.8); + --g-button-background-color-hover: var(--color-neutral-100); + --g-button-border-color: rgba(210, 210, 210, 0.4); + --g-button-border-width: 1px; + --g-button-text-color: var(--color-neutral-500); +} + +.toolbar-button { + opacity: 0.6; +} + +.toolbar-button:hover { + opacity: 1; +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.tsx b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.tsx new file mode 100644 index 000000000..ac0c6e400 --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveActionsToolbar.tsx @@ -0,0 +1,152 @@ +import React, {ReactNode, useCallback} from 'react'; +import {Button, Icon} from '@gravity-ui/uikit'; +import {ArrowDownToSquare, SquareDashed, Plus, Minus, ChevronsExpandToLines} from '@gravity-ui/icons'; +import {useViewportContext} from '../hooks/useSyncedViewport'; +import {ScreenshotDisplayData} from '../types'; +import styles from './InteractiveActionsToolbar.module.css'; +import classNames from 'classnames'; +import {calculateZoomAtPoint} from './utils'; +import {MIN_SCALE, MAX_SCALE, ZOOM_STEP, InteractiveFitMode} from './constants'; + +interface InteractiveActionsToolbarProps { + image: ScreenshotDisplayData; + className?: string; + containerRef: React.RefObject; + imageWrapperRef: React.RefObject; +} + +export function InteractiveActionsToolbar(props: InteractiveActionsToolbarProps): ReactNode { + const {updateViewport, setFitMode} = useViewportContext(); + + const handleZoomIn = useCallback(() => { + const container = props.containerRef.current; + if (!container) { + return; + } + + const rect = container.getBoundingClientRect(); + const centerX = rect.width / 2 - (props.imageWrapperRef.current?.offsetLeft ?? 0); + const centerY = rect.height / 2 - (props.imageWrapperRef.current?.offsetTop ?? 0); + + updateViewport((current) => { + const newScale = Math.min(MAX_SCALE, current.scale * (1 + ZOOM_STEP)); + if (newScale !== current.scale) { + return calculateZoomAtPoint( + current.scale, + current.translateX, + current.translateY, + newScale, + centerX, + centerY + ); + } + return {}; + }); + }, [updateViewport, props.containerRef]); + + const handleZoomOut = useCallback(() => { + const container = props.containerRef.current; + if (!container) { + return; + } + + const rect = container.getBoundingClientRect(); + const centerX = rect.width / 2 - (props.imageWrapperRef.current?.offsetLeft ?? 0); + const centerY = rect.height / 2 - (props.imageWrapperRef.current?.offsetTop ?? 0); + + updateViewport((current) => { + const newScale = Math.max(MIN_SCALE, current.scale / (1 + ZOOM_STEP)); + if (newScale !== current.scale) { + return calculateZoomAtPoint( + current.scale, + current.translateX, + current.translateY, + newScale, + centerX, + centerY + ); + } + return {}; + }); + }, [updateViewport, props.containerRef]); + + const handleFitToView = useCallback(() => { + updateViewport({ + scale: 1, + translateX: 0, + translateY: 0 + }); + setFitMode(InteractiveFitMode.FitView); + }, [updateViewport, setFitMode]); + + const handleFitToWidth = useCallback(() => { + updateViewport({ + scale: 1, + translateX: 0, + translateY: 0 + }); + setFitMode(InteractiveFitMode.FitWidth); + }, [updateViewport, setFitMode]); + + const handleDownload = useCallback(() => { + const link = document.createElement('a'); + link.target = '_blank'; + link.href = props.image.path; + const filename = props.image.path.split('/').pop() || 'screenshot.png'; + link.download = filename; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, [props.image.path]); + + return ( +
+ + + + + +
+ ); +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.module.css b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.module.css new file mode 100644 index 000000000..74df2b6a7 --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.module.css @@ -0,0 +1,40 @@ +.interactive-container { + overflow: hidden; + user-select: none; + touch-action: none; + display: flex; + justify-content: center; + align-items: start; +} + +.unified-container { + transform-origin: top left; + position: relative; +} + +.screenshot { + pointer-events: none; + max-height: 100%; + max-width: 100%; + outline: var(--color-neutral-300) solid 1px; +} + +.screenshot-overlay { + position: absolute; + top: 0; + left: 0; +} + +html, body { + /* This is needed to prevent back/forward swipes in Chrome and other browsers */ + overscroll-behavior-x: none; +} + +.toolbar { + opacity: 0; + transition: opacity 0.2s ease; +} + +.interactive-container:hover .toolbar { + opacity: 1; +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.tsx b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.tsx new file mode 100644 index 000000000..bc7bf03bd --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/InteractiveScreenshot.tsx @@ -0,0 +1,239 @@ +import classNames from 'classnames'; +import React, {ReactNode, useCallback, useRef, useState, useEffect} from 'react'; +import {useViewportContext} from '../hooks/useSyncedViewport'; +import {ScreenshotDisplayData} from '../types'; +import {InteractiveActionsToolbar} from './InteractiveActionsToolbar'; +import {calculateZoomAtPoint} from './utils'; +import styles from './InteractiveScreenshot.module.css'; +import {MIN_SCALE, MAX_SCALE, EXPONENTIAL_ZOOM_FACTOR, PAN_SENSITIVITY, InteractiveFitMode} from './constants'; + +interface InteractiveScreenshotProps { + image: ScreenshotDisplayData; + unifiedDimensions: {width: number; height: number}; + containerClassName?: string; + defaultFitMode?: InteractiveFitMode; + overlayImage?: ScreenshotDisplayData; + showOverlay?: boolean; +} + +export function InteractiveScreenshot(props: InteractiveScreenshotProps): ReactNode { + const {viewport, updateViewport, fitMode, setFitMode} = useViewportContext(); + const containerRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const lastPointerPositionRef = useRef({x: 0, y: 0}); + const previousImagePathRef = useRef(props.image.path); + const velocityRef = useRef({x: 0, y: 0}); + const animationRef = useRef(null); + const imageWrapperRef = useRef(null); + + useEffect(() => { + if (previousImagePathRef.current !== props.image.path) { + updateViewport({ + scale: 1, + translateX: 0, + translateY: 0 + }); + setFitMode(props.defaultFitMode || InteractiveFitMode.FitWidth); + previousImagePathRef.current = props.image.path; + } + }, [props.image.path, props.defaultFitMode, updateViewport, setFitMode]); + + const animateMomentum = useCallback(() => { + const vx = velocityRef.current.x; + const vy = velocityRef.current.y; + + if (Math.abs(vx) > 0.5 || Math.abs(vy) > 0.5) { + updateViewport((current) => ({ + translateX: current.translateX + vx, + translateY: current.translateY + vy + })); + + velocityRef.current.x *= 0.3; + velocityRef.current.y *= 0.3; + + animationRef.current = requestAnimationFrame(animateMomentum); + } else { + velocityRef.current = {x: 0, y: 0}; + animationRef.current = null; + } + }, [updateViewport]); + + const handleNativeWheel = useCallback((e: WheelEvent): void => { + e.preventDefault(); + e.stopPropagation(); + + const container = containerRef.current; + if (!container) { + return; + } + + const rect = container.getBoundingClientRect(); + const pointerX = e.clientX - rect.left - (imageWrapperRef.current?.offsetLeft ?? 0); + const pointerY = e.clientY - rect.top - (imageWrapperRef.current?.offsetTop ?? 0); + + let zoomMultiplier = 1; + let deltaX = 0; + let deltaY = 0; + + if (e.ctrlKey || e.metaKey) { + zoomMultiplier = Math.exp(-e.deltaY * EXPONENTIAL_ZOOM_FACTOR); + } else { + deltaX = -e.deltaX * PAN_SENSITIVITY; + deltaY = -e.deltaY * PAN_SENSITIVITY; + + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + + velocityRef.current.x = deltaX * 0.3; + velocityRef.current.y = deltaY * 0.3; + } + + if (zoomMultiplier !== 1) { + updateViewport((current) => { + const currentScale = current.scale; + const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, currentScale * zoomMultiplier)); + + if (newScale !== currentScale) { + return calculateZoomAtPoint( + currentScale, + current.translateX, + current.translateY, + newScale, + pointerX, + pointerY + ); + } + return {}; + }); + } else if (deltaX !== 0 || deltaY !== 0) { + updateViewport((current) => { + return { + translateX: current.translateX + deltaX, + translateY: current.translateY + deltaY + }; + }); + + if (!animationRef.current) { + animationRef.current = requestAnimationFrame(animateMomentum); + } + } + }, [animateMomentum, updateViewport]); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + container.addEventListener('wheel', handleNativeWheel, {passive: false}); + + return () => { + container.removeEventListener('wheel', handleNativeWheel); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, []); + + const handlePointerDown = useCallback((e: React.PointerEvent) => { + if (e.button !== 0) { + return; + } + + const target = e.target as HTMLElement; + if (target.closest('[data-toolbar]')) { + return; + } + + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + velocityRef.current = {x: 0, y: 0}; + + setIsDragging(true); + lastPointerPositionRef.current = {x: e.clientX, y: e.clientY}; + e.currentTarget.setPointerCapture(e.pointerId); + }, []); + + const handlePointerMove = useCallback((e: React.PointerEvent) => { + if (!isDragging) { + return; + } + + const deltaX = e.clientX - lastPointerPositionRef.current.x; + const deltaY = e.clientY - lastPointerPositionRef.current.y; + + updateViewport((current) => { + return { + translateX: current.translateX + deltaX, + translateY: current.translateY + deltaY + }; + }); + + lastPointerPositionRef.current = {x: e.clientX, y: e.clientY}; + }, [isDragging, updateViewport]); + + const handlePointerUp = useCallback((e: React.PointerEvent) => { + if (isDragging) { + setIsDragging(false); + e.currentTarget.releasePointerCapture(e.pointerId); + } + }, [isDragging]); + + const transform = `translate(${viewport.translateX}px, ${viewport.translateY}px) scale(${viewport.scale})`; + + return ( +
+
+ Screenshot 1 ? 'pixelated' : 'auto' + }} + /> + {props.overlayImage && props.showOverlay && ( + Diff overlay 1 ? 'pixelated' : 'auto' + }} + /> + )} +
+ +
+ ); +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/constants.ts b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/constants.ts new file mode 100644 index 000000000..f250f4fad --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/constants.ts @@ -0,0 +1,11 @@ +export const MIN_SCALE = 0.1; +export const MAX_SCALE = 50; +export const ZOOM_STEP = 0.2; + +export const EXPONENTIAL_ZOOM_FACTOR = 0.015; +export const PAN_SENSITIVITY = 1; + +export enum InteractiveFitMode { + FitView = 'fit-view', + FitWidth = 'fit-width' +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.module.css b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.module.css new file mode 100644 index 000000000..4a0098f2a --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.module.css @@ -0,0 +1,31 @@ +.two-up-interactive-mode { + display: flex; + height: 100%; + width: 100%; + min-height: 400px; +} + +.image-panel { + flex: 1; + position: relative; + overflow: hidden; + background: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + border-radius: 5px; +} + +.divider { + width: 10px; +} + +.side-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.image-container { + width: 100%; + height: 100%; +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.tsx b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.tsx new file mode 100644 index 000000000..d7c1b97a6 --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/index.tsx @@ -0,0 +1,102 @@ +import React, {ReactNode, useMemo, useEffect} from 'react'; +import {useSelector} from 'react-redux'; +import {ViewportContext, useSyncedViewport} from '../hooks/useSyncedViewport'; +import {InteractiveScreenshot} from './InteractiveScreenshot'; +import {InteractiveFitMode} from './constants'; +import styles from './index.module.css'; +import {ImageLabel} from '../../ImageLabel'; +import {getImageDisplayedSize} from '../../../utils'; +import {ImageFile} from '@/types'; +import {getDisplayedDiffPercentValue, getDisplayedDiffPixelsCountValue} from '../utils'; +import {TwoUpFitMode} from '@/constants'; + +interface TwoUpInteractiveModeProps { + expected: ImageFile; + actual: ImageFile; + diff?: ImageFile; + differentPixels?: number; + diffRatio?: number; +} + +interface TwoUpInteractiveModePureProps extends TwoUpInteractiveModeProps { + is2UpDiffVisible: boolean; + globalTwoUpFitMode: TwoUpFitMode; +} + +export function TwoUpInteractiveModePure(props: TwoUpInteractiveModePureProps): ReactNode { + const viewportContextValue = useSyncedViewport(); + + const defaultFitMode = props.globalTwoUpFitMode === TwoUpFitMode.FitToView + ? InteractiveFitMode.FitView + : InteractiveFitMode.FitWidth; + + const diffInfo = props.differentPixels && props.diffRatio + ? ` ⋅ ${getDisplayedDiffPixelsCountValue(props.differentPixels)} ${props.differentPixels > 1 ? 'are' : 'is'} different (${getDisplayedDiffPercentValue(props.diffRatio)}%)` + : ''; + const actualImageSubtitle = getImageDisplayedSize(props.actual) + diffInfo; + + const unifiedDimensions = useMemo(() => { + return { + width: Math.max(props.expected.size.width, props.actual.size.width), + height: Math.max(props.expected.size.height, props.actual.size.height) + }; + }, [props.expected.size, props.actual.size]); + + useEffect(() => { + if (props.globalTwoUpFitMode === TwoUpFitMode.FitToView) { + viewportContextValue.setFitMode(InteractiveFitMode.FitView); + } else if (props.globalTwoUpFitMode === TwoUpFitMode.FitToWidth) { + viewportContextValue.setFitMode(InteractiveFitMode.FitWidth); + } + viewportContextValue.updateViewport({ + scale: 1, + translateX: 0, + translateY: 0 + }); + }, [props.globalTwoUpFitMode]); + + return ( + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ + ); +} + +export function TwoUpInteractiveMode(props: TwoUpInteractiveModeProps): ReactNode { + const is2UpDiffVisible = useSelector(state => state.ui.visualChecksPage.is2UpDiffVisible); + const globalTwoUpFitMode = useSelector(state => state.ui.visualChecksPage.twoUpFitMode); + + return ( + + ); +} diff --git a/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/utils.ts b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/utils.ts new file mode 100644 index 000000000..3babdd009 --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode/utils.ts @@ -0,0 +1,22 @@ +export function calculateZoomAtPoint( + currentScale: number, + currentTranslateX: number, + currentTranslateY: number, + newScale: number, + zoomPointX: number, + zoomPointY: number +): {scale: number; translateX: number; translateY: number} { + const scaleRatio = newScale / currentScale; + + const pointViewportX = zoomPointX - currentTranslateX; + const pointViewportY = zoomPointY - currentTranslateY; + + const newTranslateX = zoomPointX - pointViewportX * scaleRatio; + const newTranslateY = zoomPointY - pointViewportY * scaleRatio; + + return { + scale: newScale, + translateX: newTranslateX, + translateY: newTranslateY + }; +} diff --git a/lib/static/new-ui/components/DiffViewer/hooks/useSyncedViewport.ts b/lib/static/new-ui/components/DiffViewer/hooks/useSyncedViewport.ts new file mode 100644 index 000000000..1eeed2ef3 --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/hooks/useSyncedViewport.ts @@ -0,0 +1,53 @@ +import {createContext, useContext, useCallback, useState} from 'react'; +import {InteractiveFitMode} from '../TwoUpInteractiveMode/constants'; + +export interface ViewportState { + scale: number; + translateX: number; + translateY: number; +} + +export interface ViewportContextValue { + viewport: ViewportState; + updateViewport: (newViewport: Partial | ((current: ViewportState) => Partial)) => void; + fitMode: InteractiveFitMode; + setFitMode: (mode: InteractiveFitMode) => void; +} + +const initialViewport: ViewportState = { + scale: 1, + translateX: 0, + translateY: 0 +}; + +export const ViewportContext = createContext(null); + +export function useSyncedViewport(customInitialViewport?: ViewportState): ViewportContextValue { + const [viewport, setViewport] = useState(customInitialViewport || initialViewport); + const [fitMode, setFitMode] = useState(InteractiveFitMode.FitWidth); + + const updateViewport = useCallback((newViewport: Partial | ((current: ViewportState) => Partial)) => { + setViewport(current => { + const updates = typeof newViewport === 'function' ? newViewport(current) : newViewport; + return { + ...current, + ...updates + }; + }); + }, []); + + return { + viewport, + updateViewport, + fitMode, + setFitMode + }; +} + +export function useViewportContext(): ViewportContextValue { + const context = useContext(ViewportContext); + if (!context) { + throw new Error('useViewportContext must be used within ViewportContext.Provider'); + } + return context; +} diff --git a/lib/static/new-ui/components/DiffViewer/index.tsx b/lib/static/new-ui/components/DiffViewer/index.tsx index fab6659df..67d39ba5f 100644 --- a/lib/static/new-ui/components/DiffViewer/index.tsx +++ b/lib/static/new-ui/components/DiffViewer/index.tsx @@ -9,6 +9,7 @@ import {OnionSkinMode} from '@/static/new-ui/components/DiffViewer/OnionSkinMode import {SideBySideMode} from '@/static/new-ui/components/DiffViewer/SideBySideMode'; import {SideBySideToFitMode} from '@/static/new-ui/components/DiffViewer/SideBySideToFitMode'; import {ListMode} from '@/static/new-ui/components/DiffViewer/ListMode'; +import {TwoUpInteractiveMode} from './TwoUpInteractiveMode'; import {getDisplayedDiffPercentValue} from '@/static/new-ui/components/DiffViewer/utils'; import {ImageLabel} from '@/static/new-ui/components/ImageLabel'; @@ -68,6 +69,9 @@ export function DiffViewer(props: DiffViewerProps): ReactNode { return ; } + case DiffModes.TWO_UP_INTERACTIVE.id: + return ; + case DiffModes.THREE_UP.id: default: return ; diff --git a/lib/static/new-ui/components/DiffViewer/utils.ts b/lib/static/new-ui/components/DiffViewer/utils.ts index f94b1fee3..705659882 100644 --- a/lib/static/new-ui/components/DiffViewer/utils.ts +++ b/lib/static/new-ui/components/DiffViewer/utils.ts @@ -19,3 +19,15 @@ export const getDisplayedDiffPercentValue = (diffRatio: number): string => { return String(percentRounded); }; + +export const getDisplayedDiffPixelsCountValue = (diffPixels: number): string => { + if (diffPixels < 1000) { + return `${diffPixels} px`; + } + + if (diffPixels < 1_000_000) { + return `~${(diffPixels / 1_000).toFixed(0)}k px`; + } + + return `~${(diffPixels / 1_000_000).toFixed(0)}M px`; +}; diff --git a/lib/static/new-ui/components/ImageLabel/index.module.css b/lib/static/new-ui/components/ImageLabel/index.module.css index 39472559e..7494df44f 100644 --- a/lib/static/new-ui/components/ImageLabel/index.module.css +++ b/lib/static/new-ui/components/ImageLabel/index.module.css @@ -1,8 +1,13 @@ -.image-label, .image-label + div { +.image-label { margin-bottom: 8px; + display: flex; } .image-label-subtitle { color: var(--g-color-private-black-400); margin-left: 4px; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/lib/static/new-ui/components/ImageLabel/index.tsx b/lib/static/new-ui/components/ImageLabel/index.tsx index 3b4a26fda..1bc8f0c2b 100644 --- a/lib/static/new-ui/components/ImageLabel/index.tsx +++ b/lib/static/new-ui/components/ImageLabel/index.tsx @@ -7,7 +7,7 @@ interface ImageLabelProps { } export function ImageLabel({title, subtitle}: ImageLabelProps): ReactNode { - return
+ return
{title} {subtitle && {subtitle}}
; diff --git a/lib/static/new-ui/components/RunTest/index.module.css b/lib/static/new-ui/components/RunTest/index.module.css index 533071a9f..d26aa4c62 100644 --- a/lib/static/new-ui/components/RunTest/index.module.css +++ b/lib/static/new-ui/components/RunTest/index.module.css @@ -1,14 +1,3 @@ -.buttons-container { - display: flex; - align-items: center; - gap: 8px; - margin-left: auto; -} - -.divider { - height: 24px; -} - .retry-button { composes: regular-button from global, action-button from global; } diff --git a/lib/static/new-ui/components/RunTest/index.tsx b/lib/static/new-ui/components/RunTest/index.tsx index 648192a97..7f53624c7 100644 --- a/lib/static/new-ui/components/RunTest/index.tsx +++ b/lib/static/new-ui/components/RunTest/index.tsx @@ -1,66 +1,45 @@ -import React, {ReactNode} from 'react'; +import React, {forwardRef} from 'react'; import styles from './index.module.css'; -import {IconButton} from '@/static/new-ui/components/IconButton'; -import {Button, Divider, Icon, Spin} from '@gravity-ui/uikit'; -import {ArrowRotateRight, CirclePlay} from '@gravity-ui/icons'; +import {Button, ButtonProps, Icon, Spin} from '@gravity-ui/uikit'; +import {ArrowRotateRight} from '@gravity-ui/icons'; import {thunkRunTest} from '@/static/modules/actions'; -import {toggleTimeTravelPlayerVisibility} from '@/static/modules/actions/snapshots'; import {useDispatch, useSelector} from 'react-redux'; -import {isTimeTravelPlayerAvailable} from '../../features/suites/selectors'; import {RunTestsFeature} from '@/constants'; import {useAnalytics} from '../../hooks/useAnalytics'; import type {BrowserEntity} from '@/static/new-ui/types/store'; +import {isFeatureAvailable} from '../../utils/features'; interface RunTestProps { - showPlayer?: boolean; browser: BrowserEntity | null; + buttonText?: string | null; + buttonProps?: ButtonProps; } -export const RunTest = ({showPlayer = true, browser}: RunTestProps): ReactNode => { - const isPlayerVisible = useSelector(state => state.ui.suitesPage.isSnapshotsPlayerVisible); - const isRunning = useSelector(state => state.running); +export const RunTestButton = forwardRef( + ({browser, buttonProps, buttonText}, ref) => { + const isRunning = useSelector(state => state.running); - const analytics = useAnalytics(); - const dispatch = useDispatch(); - const isRunTestsAvailable = useSelector(state => state.app.availableFeatures) - .find(feature => feature.name === RunTestsFeature.name); + const analytics = useAnalytics(); + const dispatch = useDispatch(); + const isRunTestsAvailable = isFeatureAvailable(RunTestsFeature); - const isPlayerAvailable = useSelector(isTimeTravelPlayerAvailable); + const onRetryTestHandler = (): void => { + if (browser) { + analytics?.trackFeatureUsage({featureName: 'Retry test button click in test control panel'}); + dispatch(thunkRunTest({test: {testName: browser.parentId, browserName: browser.name}})); + } + }; - const onRetryTestHandler = (): void => { - if (browser) { - analytics?.trackFeatureUsage({featureName: 'Retry test button click in test control panel'}); - dispatch(thunkRunTest({test: {testName: browser.parentId, browserName: browser.name}})); + if (!isRunTestsAvailable) { + return null; } - }; - const onTogglePlayerVisibility = (): void => { - analytics?.trackFeatureUsage({featureName: 'Toggle time travel player visibility'}); - dispatch(toggleTimeTravelPlayerVisibility(!isPlayerVisible)); - }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ; + } +); - const showRetryButton = Boolean(isRunTestsAvailable); - const showPlayerButton = isPlayerAvailable && showPlayer; - const showDivider = showRetryButton && showPlayerButton; - - return ( -
- {showPlayerButton && ( - } - onClick={onTogglePlayerVisibility} - view='outlined' - selected={isPlayerVisible} - /> - )} - {showDivider && } - {showRetryButton && ( - - )} -
- ); -}; +RunTestButton.displayName = 'RunTestButton'; diff --git a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx index 7fefe09e0..498869877 100644 --- a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx +++ b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx @@ -1,11 +1,12 @@ -import {ArrowUturnCcwLeft, Check, Eye} from '@gravity-ui/icons'; +import {ArrowRightArrowLeft, ArrowUturnCcwLeft, Check, Eye} from '@gravity-ui/icons'; import {Button, Icon, SegmentedRadioGroup as RadioButton, Select, Flex} from '@gravity-ui/uikit'; import React, {ReactNode, createRef, useEffect, useRef} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; import {ImageEntity} from '@/static/new-ui/types/store'; -import {DiffModeId, DiffModes, EditScreensFeature, TestStatus} from '@/constants'; +import {DiffModeId, EditScreensFeature, TestStatus} from '@/constants'; +import {getAvailableDiffModes} from '@/static/new-ui/utils/diffModes'; import { setDiffMode, staticAccepterStageScreenshot, @@ -109,17 +110,17 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re {isDiffModeSwitcherVisible && (
- {Object.values(DiffModes).map(diffMode => + {getAvailableDiffModes('suites').map(diffMode => )} @@ -164,7 +165,7 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re )} }> - +
); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx index 4b984dceb..8e1487cbf 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -74,9 +74,9 @@ export function SuitesPage(): ReactNode { return; } - dispatch(setStrictMatchFilter(false)); - if (isInitialized && params.suiteId) { + dispatch(setStrictMatchFilter(false)); + const treeNode = findTreeNodeByBrowserId(treeData.tree, params.suiteId); if (!treeNode) { diff --git a/lib/static/new-ui/features/suites/components/TestControlPanel/index.module.css b/lib/static/new-ui/features/suites/components/TestControlPanel/index.module.css index 836cf7f1c..720493579 100644 --- a/lib/static/new-ui/features/suites/components/TestControlPanel/index.module.css +++ b/lib/static/new-ui/features/suites/components/TestControlPanel/index.module.css @@ -12,3 +12,14 @@ flex-wrap: wrap; gap: 4px; } + +.buttons-container { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.divider { + height: 24px; +} diff --git a/lib/static/new-ui/features/suites/components/TestControlPanel/index.tsx b/lib/static/new-ui/features/suites/components/TestControlPanel/index.tsx index 62a055854..d24f588cc 100644 --- a/lib/static/new-ui/features/suites/components/TestControlPanel/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestControlPanel/index.tsx @@ -1,11 +1,18 @@ +import {Divider, Icon} from '@gravity-ui/uikit'; +import {CirclePlay} from '@gravity-ui/icons'; import React, {ReactNode} from 'react'; -import {useSelector} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import {AttemptPickerItem} from '@/static/new-ui/components/AttemptPickerItem'; import styles from './index.module.css'; import classNames from 'classnames'; -import {getCurrentBrowser, getCurrentResultId} from '@/static/new-ui/features/suites/selectors'; -import {RunTest} from '@/static/new-ui/components/RunTest'; +import {getCurrentBrowser, getCurrentResultId, isTimeTravelPlayerAvailable} from '@/static/new-ui/features/suites/selectors'; +import {RunTestButton} from '@/static/new-ui/components/RunTest'; +import {useAnalytics} from '../../../../hooks/useAnalytics'; +import {IconButton} from '../../../../components/IconButton'; +import {isFeatureAvailable} from '../../../../utils/features'; +import {RunTestsFeature} from '@/constants'; +import {toggleTimeTravelPlayerVisibility} from '@/static/modules/actions/snapshots'; interface TestControlPanelProps { onAttemptChange?: (browserId: string, resultId: string, attemptIndex: number) => unknown; @@ -14,6 +21,9 @@ interface TestControlPanelProps { export function TestControlPanel(props: TestControlPanelProps): ReactNode { const {onAttemptChange} = props; + const dispatch = useDispatch(); + const analytics = useAnalytics(); + const browserId = useSelector(state => state.app.suitesPage.currentBrowserId); const resultIds = useSelector(state => { if (browserId && state.tree.browsers.byId[browserId]) { @@ -32,6 +42,17 @@ export function TestControlPanel(props: TestControlPanelProps): ReactNode { onAttemptChange?.(browserId, resultId, attemptIndex); }; + const isRunTestsAvailable = isFeatureAvailable(RunTestsFeature); + const isPlayerAvailable = useSelector(isTimeTravelPlayerAvailable); + const isPlayerVisible = useSelector(state => state.ui.suitesPage.isSnapshotsPlayerVisible); + + const showDivider = isRunTestsAvailable && isPlayerAvailable; + + const onTogglePlayerVisibility = (): void => { + analytics?.trackFeatureUsage({featureName: 'Toggle time travel player visibility'}); + dispatch(toggleTimeTravelPlayerVisibility(!isPlayerVisible)); + }; + return (

Attempts

@@ -45,7 +66,19 @@ export function TestControlPanel(props: TestControlPanelProps): ReactNode { /> ))}
- +
+ {isPlayerAvailable && ( + } + onClick={onTogglePlayerVisibility} + view='outlined' + selected={isPlayerVisible} + /> + )} + {showDivider && } + +
); } diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx index fc24c0a92..2810f617b 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx @@ -1,5 +1,5 @@ -import {ArrowUturnCcwLeft, Check, ListCheck} from '@gravity-ui/icons'; -import {Button, Divider, Icon, Select, Flex} from '@gravity-ui/uikit'; +import {ArrowRightArrowLeft, ArrowUturnCcwLeft, Check, LayersVertical, ListCheck, SquareDashed, ChevronsExpandToLines} from '@gravity-ui/icons'; +import {Button, Divider, Icon, Select, Flex, Tooltip} from '@gravity-ui/uikit'; import React, {ReactNode, useEffect, useRef} from 'react'; import {useDispatch, useSelector} from 'react-redux'; @@ -13,12 +13,15 @@ import { import {SuiteTitle} from '@/static/new-ui/components/SuiteTitle'; import styles from './index.module.css'; import {CompactAttemptPicker} from '@/static/new-ui/components/CompactAttemptPicker'; -import {DiffModeId, DiffModes, EditScreensFeature} from '@/constants'; +import {DiffModeId, EditScreensFeature, RunTestsFeature, TwoUpFitMode} from '@/constants'; +import {getAvailableDiffModes} from '@/static/new-ui/utils/diffModes'; import { - setDiffMode, staticAccepterStageScreenshot, - staticAccepterUnstageScreenshot + staticAccepterUnstageScreenshot, + toggle2UpDiffVisibility, + set2UpFitMode } from '@/static/modules/actions'; +import {setVisualChecksDiffMode} from '@/static/modules/actions/visual-checks-page'; import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; import {thunkAcceptImages, thunkRevertImages} from '@/static/modules/actions/screenshots'; @@ -26,7 +29,8 @@ import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics'; import {preloadImageEntity} from '../../../../../modules/utils/imageEntity'; import {useNavigate} from 'react-router-dom'; -import {RunTest} from '../../../../components/RunTest'; +import {RunTestButton} from '../../../../components/RunTest'; +import {IconButton} from '../../../../components/IconButton'; interface VisualChecksStickyHeaderProps { currentNamedImage: NamedImageEntity | null; @@ -72,9 +76,20 @@ export function VisualChecksStickyHeader({currentNamedImage, visibleNamedImageId usePreloadImages(currentNamedImageIndex, visibleNamedImageIds); - const diffMode = useSelector(state => state.view.diffMode); + const diffMode = useSelector(state => state.app.visualChecksPage.diffMode); + const is2UpDiffVisible = useSelector(state => state.ui.visualChecksPage.is2UpDiffVisible); + const twoUpFitMode = useSelector(state => state.ui.visualChecksPage.twoUpFitMode); const onChangeHandler = (diffModeId: DiffModeId): void => { - dispatch(setDiffMode({diffModeId})); + dispatch(setVisualChecksDiffMode(diffModeId)); + }; + const onToggle2UpDiffVisibility = (): void => { + analytics?.trackFeatureUsage({featureName: 'Toggle 2-up diff visibility'}); + dispatch(toggle2UpDiffVisibility(!is2UpDiffVisible)); + }; + const onToggle2UpFitMode = (): void => { + const newFitMode = twoUpFitMode === TwoUpFitMode.FitToView ? TwoUpFitMode.FitToWidth : TwoUpFitMode.FitToView; + analytics?.trackFeatureUsage({featureName: 'Toggle 2-up fit mode'}); + dispatch(set2UpFitMode(newFitMode)); }; const isStaticImageAccepterEnabled = useSelector(state => state.staticImageAccepter.enabled); @@ -114,6 +129,9 @@ export function VisualChecksStickyHeader({currentNamedImage, visibleNamedImageId const isLastResult = Boolean(currentResultId && currentBrowser && currentResultId === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]); const isUndoAvailable = isScreenRevertable({gui: isGui, image: currentImage ?? {}, isLastResult, isStaticImageAccepterEnabled}); + const isRunTestsAvailable = Boolean(useSelector(state => state.app.availableFeatures) + .find(feature => feature.name === RunTestsFeature.name)); + const onSuites = (): void => { if (currentNamedImage) { navigate('/' + [ @@ -145,22 +163,44 @@ export function VisualChecksStickyHeader({currentNamedImage, visibleNamedImageId - as unknown as string} value={[diffMode]} onUpdate={([diffMode]): void => onChangeHandler(diffMode as DiffModeId)} multiple={false}> + {getAvailableDiffModes('visual-checks').map(diffMode => + + )} + + {diffMode === '2-up-interactive' && ( + <> + } + view="outlined" + onClick={onToggle2UpDiffVisibility} + tooltip={is2UpDiffVisible ? 'Diff is visible. Click to hide' : 'Diff is hidden. Click to show'} + selected={is2UpDiffVisible} + /> + } + view="outlined" + onClick={onToggle2UpFitMode} + tooltip={twoUpFitMode === TwoUpFitMode.FitToView ? 'Fit to view by default. Click to switch' : 'Fit to width by default. Click to switch'} + /> + )} - + - + tooltip="Go to test" + /> + {isRunTestsAvailable && + + } {isEditScreensAvailable && ( <> {isUndoAvailable && ( @@ -174,18 +214,17 @@ export function VisualChecksStickyHeader({currentNamedImage, visibleNamedImageId Undo )} - {currentImage && isAcceptable(currentImage) && ( + {!isUndoAvailable && ( )} - )} diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css index 65fb3cca4..6d6354f16 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css @@ -40,6 +40,7 @@ .current-image { overflow: auto; + height: 100%; } .test-view-card { diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx index c2634e302..6fd5f278f 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx @@ -41,6 +41,7 @@ export function VisualChecksPage(): ReactNode { const lastAttempt = useSelector(getLastAttempt); const currentImage = useSelector(getCurrentImage); const currentBrowser = useSelector(getCurrentBrowser); + const diffMode = useSelector(state => state.app.visualChecksPage.diffMode); const [imageChanged, setImageChanged] = useState(false); const navigate = useNavigate(); @@ -172,7 +173,7 @@ export function VisualChecksPage(): ReactNode { {currentImage && !isRunning && ( }>
- +
)} diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 88553ca9a..798abc450 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -1,5 +1,5 @@ import {CoordBounds} from 'looks-same'; -import {BrowserFeature, DiffModeId, Feature, TestStatus, ViewMode} from '@/constants'; +import {BrowserFeature, DiffModeId, Feature, TestStatus, ViewMode, TwoUpFitMode} from '@/constants'; import { Attachment, BrowserItem, @@ -285,6 +285,7 @@ export interface State { useMatchCaseFilter: boolean; viewMode: ViewMode; filteredBrowsers: BrowserItem[]; + diffMode: DiffModeId; }; loading: { /** @note Determines whether the loading bar is visible */ @@ -332,6 +333,8 @@ export interface State { sectionSizes: number[]; // Used to restore the previous sections sizes after collapsing the tree with a button backupSectionSizes: number[]; + is2UpDiffVisible: boolean; + twoUpFitMode: TwoUpFitMode; }; staticImageAccepterToolbar: { offset: Point; diff --git a/lib/static/new-ui/utils/assert-view-status.tsx b/lib/static/new-ui/utils/assert-view-status.tsx index 95fdf94d6..679cd13fe 100644 --- a/lib/static/new-ui/utils/assert-view-status.tsx +++ b/lib/static/new-ui/utils/assert-view-status.tsx @@ -1,12 +1,11 @@ import {ImageEntity, ImageEntityError} from '@/static/new-ui/types/store'; import {Icon} from '@gravity-ui/uikit'; import { - ArrowRightArrowLeft, - CircleCheck, FileArrowUp, FileCheck, FileExclamation, FileLetterX, FilePlus, + FileXmark, SquareExclamation, SquareXmark } from '@gravity-ui/icons'; @@ -29,15 +28,15 @@ export const getAssertViewStatusIcon = (image: ImageEntity | null, color = false switch (image.status) { case TestStatus.SUCCESS: - return ; + return ; case TestStatus.STAGED: return ; case TestStatus.COMMITED: return ; case TestStatus.FAIL: - return ; + return ; case TestStatus.UPDATED: - return ; + return ; } return ; diff --git a/lib/static/new-ui/utils/diffModes.ts b/lib/static/new-ui/utils/diffModes.ts new file mode 100644 index 000000000..2d4c49834 --- /dev/null +++ b/lib/static/new-ui/utils/diffModes.ts @@ -0,0 +1,11 @@ +import {DiffMode, DiffModes} from '@/constants'; + +export function getAvailableDiffModes(context: 'visual-checks' | 'suites'): DiffMode[] { + const allModes = Object.values(DiffModes); + + if (context === 'visual-checks') { + return allModes; + } + + return allModes.filter(mode => mode.id !== DiffModes.TWO_UP_INTERACTIVE.id); +} diff --git a/lib/static/new-ui/utils/features.ts b/lib/static/new-ui/utils/features.ts new file mode 100644 index 000000000..199c6ede1 --- /dev/null +++ b/lib/static/new-ui/utils/features.ts @@ -0,0 +1,7 @@ +import {Feature} from '@/constants'; +import {useSelector} from 'react-redux'; + +export const isFeatureAvailable = (feature: Feature): boolean => { + return Boolean(useSelector(state => state.app.availableFeatures) + .find(f => f.name === feature.name)); +}; diff --git a/package-lock.json b/package-lock.json index d0ea63c7c..9de8bde30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,13 @@ "packages": { "": { "name": "html-reporter", - "version": "11.0.3", + "version": "11.2.1", "license": "MIT", "workspaces": [ "test/func/fixtures/*", "test/func/packages/*", - "test/func/tests" + "test/func/tests", + "test/component" ], "dependencies": { "@gemini-testing/commander": "^2.15.3", @@ -70,6 +71,7 @@ "@tanstack/react-virtual": "^3.8.3", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@testplane/testing-library": "^1.0.2", "@types/babel__core": "^7.20.5", "@types/better-sqlite3": "^7.6.4", "@types/bluebird": "^3.5.3", @@ -183,10 +185,11 @@ "stylus": "^0.57.0", "stylus-loader": "^7.1.3", "terser-webpack-plugin": "^5.3.9", - "testplane": "^8.30.3", + "testplane": "^8.31.7", "tree-kill": "^1.2.2", "ts-node": "^10.9.1", "ts-patch": "^3.3.0", + "tsconfig-paths": "^4.2.0", "type-fest": "^3.13.1", "typescript": "^5.0.4", "typescript-transform-paths": "^3.5.5", @@ -229,57 +232,45 @@ "node": ">=0.10.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -310,15 +301,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -349,14 +341,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -492,6 +484,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-hoist-variables": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", @@ -517,29 +518,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -561,9 +560,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -642,27 +641,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -684,13 +683,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -765,10 +764,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, + "dependencies": { + "@babel/types": "^7.28.4" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1764,6 +1766,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", @@ -2172,49 +2204,45 @@ "dev": true }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4663,17 +4691,23 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -4685,15 +4719,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", @@ -4711,9 +4736,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -5079,6 +5104,12 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", + "dev": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", @@ -5287,16 +5318,6 @@ "win32" ] }, - "node_modules/@rrweb/record": { - "version": "2.0.0-alpha.18", - "resolved": "https://registry.npmjs.org/@rrweb/record/-/record-2.0.0-alpha.18.tgz", - "integrity": "sha512-WbzcybTEqT+cKkOnzYiyaAYvNzAIxTK9f8qNLNOG9lOqWsmi+qu/W7CEdxHmfjlfgXGw/f7bxGZggAWVaizKqg==", - "dev": true, - "dependencies": { - "@rrweb/types": "^2.0.0-alpha.18", - "rrweb": "^2.0.0-alpha.18" - } - }, "node_modules/@rrweb/replay": { "version": "2.0.0-alpha.18", "resolved": "https://registry.npmjs.org/@rrweb/replay/-/replay-2.0.0-alpha.18.tgz", @@ -5671,7 +5692,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5691,7 +5711,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -5701,7 +5720,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -5714,7 +5732,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5728,8 +5745,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@testing-library/react": { "version": "16.0.0", @@ -5937,6 +5953,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@testplane/testing-library": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@testplane/testing-library/-/testing-library-1.0.2.tgz", + "integrity": "sha512-FNnmsZ6aW8VRhXpAZDeTAlpvi1e3AUTi65GtP82t9PJtM8GdIuPVx+gmcgedtAdRXbhZtBk0NN9v2xrO3Uk4eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.4.0" + }, + "peerDependencies": { + "testplane": "^8.10.0" + } + }, "node_modules/@testplane/wdio-config": { "version": "9.5.3", "resolved": "https://registry.npmjs.org/@testplane/wdio-config/-/wdio-config-9.5.3.tgz", @@ -6373,8 +6402,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7526,6 +7554,26 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@vitest/spy": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", @@ -9131,6 +9179,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -9463,9 +9520,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -9482,10 +9539,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -9741,9 +9799,9 @@ "dev": true }, "node_modules/caniuse-lite": { - "version": "1.0.30001640", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", - "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true, "funding": [ { @@ -13367,8 +13425,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -13671,9 +13728,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.4.818", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", - "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", "dev": true }, "node_modules/elliptic": { @@ -14084,9 +14141,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -17202,6 +17259,10 @@ "resolved": "test/func/packages/basic", "link": true }, + "node_modules/html-reporter-component-tests": { + "resolved": "test/component", + "link": true + }, "node_modules/html-reporter-e2e-testplane-tests": { "resolved": "test/func/tests", "link": true @@ -21199,15 +21260,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -21892,7 +21953,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -23307,9 +23367,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "dev": true }, "node_modules/noms": { @@ -24473,9 +24533,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -26189,6 +26249,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "6.25.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", @@ -29617,9 +29686,9 @@ } }, "node_modules/testplane": { - "version": "8.30.3", - "resolved": "https://registry.npmjs.org/testplane/-/testplane-8.30.3.tgz", - "integrity": "sha512-xswEyCAlgzwktk9IK3h7W7NLrDGT9NmNHRclufGRWqUss0Yfuprj2gP3H034JwG4y5F518WEky7rqS6kJ+JwoA==", + "version": "8.31.7", + "resolved": "https://registry.npmjs.org/testplane/-/testplane-8.31.7.tgz", + "integrity": "sha512-huQnWwaIQQIYwvOA6AiFYj+IoW0idGo3+0E/MI2UqbrQz1x4F1McRTWBLKbrICbUBFa3p5QomtHndeo0meE0mw==", "dev": true, "license": "MIT", "dependencies": { @@ -29628,7 +29697,6 @@ "@jspm/core": "2.0.1", "@jsquash/png": "3.1.1", "@puppeteer/browsers": "2.7.1", - "@rrweb/record": "2.0.0-alpha.18", "@testplane/devtools": "8.32.3", "@testplane/wdio-protocols": "9.4.6", "@testplane/wdio-utils": "9.5.3", @@ -29645,9 +29713,9 @@ "expect-webdriverio": "3.6.0", "extract-zip": "2.0.1", "fastq": "1.13.0", - "fs-extra": "5.0.0", + "fs-extra": "7.0.1", "geckodriver": "4.5.0", - "gemini-configparser": "1.4.1", + "gemini-configparser": "1.4.2", "get-port": "5.1.1", "import-meta-resolve": "4.0.0", "load-esm": "1.0.2", @@ -30302,26 +30370,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/testplane/node_modules/fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/testplane/node_modules/gemini-configparser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/gemini-configparser/-/gemini-configparser-1.4.1.tgz", - "integrity": "sha512-vfaj/nMgyHrcruEyk9BApLLqWusuQbdH0+awSTCrpTMam1XoM1NDYJEz/iEW7NZjUEl+Bh/tYM9lLPlvwCi1kA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.4" - } - }, "node_modules/testplane/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -30820,15 +30868,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -31098,6 +31137,20 @@ "node": ">=10" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -31675,9 +31728,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -31694,8 +31747,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -33866,6 +33919,13 @@ "safe-buffer": "~5.2.0" } }, + "test/component": { + "name": "html-reporter-component-tests", + "version": "0.0.0", + "devDependencies": { + "@vitejs/plugin-react": "^5.0.2" + } + }, "test/func/fixtures/analytics": { "version": "0.0.0" }, @@ -33978,48 +34038,39 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, "@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true }, "@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -34042,15 +34093,16 @@ } }, "@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "requires": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { @@ -34072,14 +34124,14 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -34183,6 +34235,12 @@ "@babel/types": "^7.24.7" } }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, "@babel/helper-hoist-variables": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", @@ -34202,26 +34260,24 @@ } }, "@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" } }, "@babel/helper-optimise-call-expression": { @@ -34234,9 +34290,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true }, "@babel/helper-remap-async-to-generator": { @@ -34294,21 +34350,21 @@ } }, "@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true }, "@babel/helper-wrap-function": { @@ -34324,13 +34380,13 @@ } }, "@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" } }, "@babel/highlight": { @@ -34392,10 +34448,13 @@ } }, "@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "requires": { + "@babel/types": "^7.28.4" + } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.22.5", @@ -35035,6 +35094,24 @@ "@babel/plugin-transform-react-jsx": "^7.22.5" } }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, "@babel/plugin-transform-react-pure-annotations": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", @@ -35338,43 +35415,39 @@ } }, "@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@bcoe/v8-coverage": { @@ -37090,13 +37163,22 @@ } }, "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, @@ -37106,12 +37188,6 @@ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, - "@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true - }, "@jridgewell/source-map": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", @@ -37129,9 +37205,9 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", @@ -37392,6 +37468,12 @@ "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", "dev": true }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", + "dev": true + }, "@rollup/rollup-android-arm-eabi": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", @@ -37504,16 +37586,6 @@ "dev": true, "optional": true }, - "@rrweb/record": { - "version": "2.0.0-alpha.18", - "resolved": "https://registry.npmjs.org/@rrweb/record/-/record-2.0.0-alpha.18.tgz", - "integrity": "sha512-WbzcybTEqT+cKkOnzYiyaAYvNzAIxTK9f8qNLNOG9lOqWsmi+qu/W7CEdxHmfjlfgXGw/f7bxGZggAWVaizKqg==", - "dev": true, - "requires": { - "@rrweb/types": "^2.0.0-alpha.18", - "rrweb": "^2.0.0-alpha.18" - } - }, "@rrweb/replay": { "version": "2.0.0-alpha.18", "resolved": "https://registry.npmjs.org/@rrweb/replay/-/replay-2.0.0-alpha.18.tgz", @@ -37753,7 +37825,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -37769,22 +37840,19 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "peer": true + "dev": true }, "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true + "dev": true }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, "requires": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -37795,8 +37863,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true + "dev": true } } }, @@ -37924,6 +37991,15 @@ } } }, + "@testplane/testing-library": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@testplane/testing-library/-/testing-library-1.0.2.tgz", + "integrity": "sha512-FNnmsZ6aW8VRhXpAZDeTAlpvi1e3AUTi65GtP82t9PJtM8GdIuPVx+gmcgedtAdRXbhZtBk0NN9v2xrO3Uk4eg==", + "dev": true, + "requires": { + "@testing-library/dom": "^10.4.0" + } + }, "@testplane/wdio-config": { "version": "9.5.3", "resolved": "https://registry.npmjs.org/@testplane/wdio-config/-/wdio-config-9.5.3.tgz", @@ -38241,8 +38317,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "@types/babel__core": { "version": "7.20.5", @@ -39215,6 +39290,20 @@ } } }, + "@vitejs/plugin-react": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "dev": true, + "requires": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + } + }, "@vitest/spy": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", @@ -40390,6 +40479,12 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, + "baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true + }, "basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -40666,15 +40761,16 @@ } }, "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" } }, "bser": { @@ -40868,9 +40964,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001640", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", - "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true }, "chai": { @@ -43654,8 +43750,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true + "dev": true }, "dom-converter": { "version": "0.2.0", @@ -43873,9 +43968,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.4.818", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", - "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", "dev": true }, "elliptic": { @@ -44200,9 +44295,9 @@ } }, "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-html": { @@ -46538,6 +46633,12 @@ "html-reporter-basic-plugin": { "version": "file:test/func/packages/basic" }, + "html-reporter-component-tests": { + "version": "file:test/component", + "requires": { + "@vitejs/plugin-react": "^5.0.2" + } + }, "html-reporter-e2e-testplane-tests": { "version": "file:test/func/tests", "requires": { @@ -49497,9 +49598,9 @@ "requires": {} }, "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true }, "json-buffer": { @@ -50018,8 +50119,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true + "dev": true }, "make-dir": { "version": "4.0.0", @@ -51072,9 +51172,9 @@ "dev": true }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "dev": true }, "noms": { @@ -51979,9 +52079,9 @@ "dev": true }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { @@ -53255,6 +53355,12 @@ } } }, + "react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true + }, "react-router": { "version": "6.25.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", @@ -55941,9 +56047,9 @@ } }, "testplane": { - "version": "8.30.3", - "resolved": "https://registry.npmjs.org/testplane/-/testplane-8.30.3.tgz", - "integrity": "sha512-xswEyCAlgzwktk9IK3h7W7NLrDGT9NmNHRclufGRWqUss0Yfuprj2gP3H034JwG4y5F518WEky7rqS6kJ+JwoA==", + "version": "8.31.7", + "resolved": "https://registry.npmjs.org/testplane/-/testplane-8.31.7.tgz", + "integrity": "sha512-huQnWwaIQQIYwvOA6AiFYj+IoW0idGo3+0E/MI2UqbrQz1x4F1McRTWBLKbrICbUBFa3p5QomtHndeo0meE0mw==", "dev": true, "requires": { "@babel/code-frame": "7.24.2", @@ -55951,7 +56057,6 @@ "@jspm/core": "2.0.1", "@jsquash/png": "3.1.1", "@puppeteer/browsers": "2.7.1", - "@rrweb/record": "2.0.0-alpha.18", "@testplane/devtools": "8.32.3", "@testplane/wdio-protocols": "9.4.6", "@testplane/wdio-utils": "9.5.3", @@ -55968,9 +56073,9 @@ "expect-webdriverio": "3.6.0", "extract-zip": "2.0.1", "fastq": "1.13.0", - "fs-extra": "5.0.0", + "fs-extra": "7.0.1", "geckodriver": "4.5.0", - "gemini-configparser": "1.4.1", + "gemini-configparser": "1.4.2", "get-port": "5.1.1", "import-meta-resolve": "4.0.0", "load-esm": "1.0.2", @@ -56310,26 +56415,6 @@ "path-exists": "^4.0.0" } }, - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "gemini-configparser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/gemini-configparser/-/gemini-configparser-1.4.1.tgz", - "integrity": "sha512-vfaj/nMgyHrcruEyk9BApLLqWusuQbdH0+awSTCrpTMam1XoM1NDYJEz/iEW7NZjUEl+Bh/tYM9lLPlvwCi1kA==", - "dev": true, - "requires": { - "lodash": "^4.17.4" - } - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -56698,12 +56783,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -56894,6 +56973,17 @@ } } }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -57295,13 +57385,13 @@ "dev": true }, "update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "uri-js": { diff --git a/package.json b/package.json index 7ace5fcc1..15ae85304 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "e2e:generate-fixtures": "npm run --workspace=test/func/fixtures generate", "e2e:test": "npm run --workspace=test/func/tests test", "e2e": "npm run e2e:build-packages && npm run e2e:generate-fixtures ; npm run e2e:test", + "component-tests": "npm run --workspace=test/component test", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "test": "npm run lint && npm run test-unit && npm run test-check-types", "test-check-types": "tsc --project test/unit/lib/static/tsconfig.json && tsc --project test/tsconfig.json", @@ -61,7 +62,8 @@ "workspaces": [ "test/func/fixtures/*", "test/func/packages/*", - "test/func/tests" + "test/func/tests", + "test/component" ], "repository": { "type": "git", @@ -160,6 +162,7 @@ "@tanstack/react-virtual": "^3.8.3", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@testplane/testing-library": "^1.0.2", "@types/babel__core": "^7.20.5", "@types/better-sqlite3": "^7.6.4", "@types/bluebird": "^3.5.3", @@ -273,10 +276,11 @@ "stylus": "^0.57.0", "stylus-loader": "^7.1.3", "terser-webpack-plugin": "^5.3.9", - "testplane": "^8.30.3", + "testplane": "^8.31.7", "tree-kill": "^1.2.2", "ts-node": "^10.9.1", "ts-patch": "^3.3.0", + "tsconfig-paths": "^4.2.0", "type-fest": "^3.13.1", "typescript": "^5.0.4", "typescript-transform-paths": "^3.5.5", diff --git a/test/component/package.json b/test/component/package.json new file mode 100644 index 000000000..46e4d4a0d --- /dev/null +++ b/test/component/package.json @@ -0,0 +1,16 @@ +{ + "name": "html-reporter-component-tests", + "version": "0.0.0", + "private": true, + "scripts": { + "test": "npm run test:docker", + "gui": "npm run gui:docker", + "test:docker": "npx testplane", + "gui:docker": "npx testplane gui", + "test:local": "BROWSER_ENV=local npx testplane", + "gui:local": "BROWSER_ENV=local npx testplane gui" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.2" + } +} diff --git a/test/component/styles.css b/test/component/styles.css new file mode 100644 index 000000000..9eae6c851 --- /dev/null +++ b/test/component/styles.css @@ -0,0 +1,20 @@ +/* Import external dependencies first */ +@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'); +@import url('https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap'); +@import '@gravity-ui/uikit/styles/fonts.css'; +@import '@gravity-ui/uikit/styles/styles.css'; +@import 'react-checkbox-tree/lib/react-checkbox-tree.css'; +@import 'rc-slider/assets/index.css'; + +/* Import main project styles for component testing */ +@import '../../lib/static/variables.css'; +@import '../../lib/static/styles.css'; +@import '../../lib/static/new-ui.css'; + +/* Import CSS module styles for DiffViewer */ +@import '../../lib/static/new-ui/components/DiffViewer/index.module.css'; +@import '../../lib/static/new-ui/components/DiffViewer/common.module.css'; +@import '../../lib/static/new-ui/components/DiffViewer/SideBySideMode.module.css'; +@import '../../lib/static/new-ui/components/DiffViewer/OnionSkinMode.module.css'; +@import '../../lib/static/new-ui/components/DiffViewer/SwipeMode.module.css'; +@import '../../lib/static/new-ui/components/DiffViewer/SwitchMode.module.css'; diff --git a/test/component/testplane.config.ts b/test/component/testplane.config.ts new file mode 100644 index 000000000..d5f5f9bed --- /dev/null +++ b/test/component/testplane.config.ts @@ -0,0 +1,50 @@ +import 'tsconfig-paths/register'; +import {setupBrowser} from '@testplane/testing-library'; +import type {ConfigInput} from 'testplane'; + +export default { + timeTravel: 'off', + baseUrl: process.env['BROWSER_ENV'] === 'local' ? 'http://localhost:5173' : 'http://host.docker.internal:5173', + gridUrl: process.env['BROWSER_ENV'] === 'local' ? 'local' : 'http://127.0.0.1:4444/', + sessionsPerBrowser: 1, + testsPerSession: 10, + system: { + workers: 1, + testRunEnv: ['browser', {viteConfig: './vite.config.ts'}], + mochaOpts: {timeout: 3600000} + }, + sets: { + component: { + files: [ + 'tests/**/*.testplane.tsx' + ], + browsers: [ + 'chrome' + ] + } + }, + browsers: { + chrome: { + windowSize: {width: 1650, height: 1000}, + headless: process.env['BROWSER_ENV'] !== 'local', + desiredCapabilities: { + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--no-sandbox', '--disable-dev-shm-usage'], + binary: process.env['BROWSER_ENV'] === 'local' ? undefined : '/usr/bin/chromium' + } + }, + waitTimeout: 3000 + } + }, + plugins: { + 'html-reporter-tester': { + enabled: true, + path: 'html-report', + defaultView: 'all' + } + }, + prepareBrowser(browser): void { + setupBrowser(browser); + } +} satisfies ConfigInput; diff --git a/test/component/testplane/screens/0015119/chrome/landscape-fit-to-width-default.png b/test/component/testplane/screens/0015119/chrome/landscape-fit-to-width-default.png new file mode 100644 index 000000000..d0aee2466 Binary files /dev/null and b/test/component/testplane/screens/0015119/chrome/landscape-fit-to-width-default.png differ diff --git a/test/component/testplane/screens/0b236d3/chrome/small-fit-to-width-default.png b/test/component/testplane/screens/0b236d3/chrome/small-fit-to-width-default.png new file mode 100644 index 000000000..85a32f210 Binary files /dev/null and b/test/component/testplane/screens/0b236d3/chrome/small-fit-to-width-default.png differ diff --git a/test/component/testplane/screens/17feb9f/chrome/different-dimensions.png b/test/component/testplane/screens/17feb9f/chrome/different-dimensions.png new file mode 100644 index 000000000..d9befe9a6 Binary files /dev/null and b/test/component/testplane/screens/17feb9f/chrome/different-dimensions.png differ diff --git a/test/component/testplane/screens/2fe43d4/chrome/mismatched-fit-to-width-reduced.png b/test/component/testplane/screens/2fe43d4/chrome/mismatched-fit-to-width-reduced.png new file mode 100644 index 000000000..366834cb0 Binary files /dev/null and b/test/component/testplane/screens/2fe43d4/chrome/mismatched-fit-to-width-reduced.png differ diff --git a/test/component/testplane/screens/36ca11e/chrome/diff-overlay-hidden.png b/test/component/testplane/screens/36ca11e/chrome/diff-overlay-hidden.png new file mode 100644 index 000000000..f321fe40f Binary files /dev/null and b/test/component/testplane/screens/36ca11e/chrome/diff-overlay-hidden.png differ diff --git a/test/component/testplane/screens/454f025/chrome/pan-drag.png b/test/component/testplane/screens/454f025/chrome/pan-drag.png new file mode 100644 index 000000000..ecd925cc2 Binary files /dev/null and b/test/component/testplane/screens/454f025/chrome/pan-drag.png differ diff --git a/test/component/testplane/screens/4c8525a/chrome/same-dimensions.png b/test/component/testplane/screens/4c8525a/chrome/same-dimensions.png new file mode 100644 index 000000000..f321fe40f Binary files /dev/null and b/test/component/testplane/screens/4c8525a/chrome/same-dimensions.png differ diff --git a/test/component/testplane/screens/4cde262/chrome/small-fit-to-width-reduced.png b/test/component/testplane/screens/4cde262/chrome/small-fit-to-width-reduced.png new file mode 100644 index 000000000..cfd559247 Binary files /dev/null and b/test/component/testplane/screens/4cde262/chrome/small-fit-to-width-reduced.png differ diff --git a/test/component/testplane/screens/4eacfbc/chrome/zoom-min-limit.png b/test/component/testplane/screens/4eacfbc/chrome/zoom-min-limit.png new file mode 100644 index 000000000..0f056ac8c Binary files /dev/null and b/test/component/testplane/screens/4eacfbc/chrome/zoom-min-limit.png differ diff --git a/test/component/testplane/screens/69273ed/chrome/side-by-side-display.png b/test/component/testplane/screens/69273ed/chrome/side-by-side-display.png new file mode 100644 index 000000000..f321fe40f Binary files /dev/null and b/test/component/testplane/screens/69273ed/chrome/side-by-side-display.png differ diff --git a/test/component/testplane/screens/6d1b4c6/chrome/mismatched-fit-to-width-default.png b/test/component/testplane/screens/6d1b4c6/chrome/mismatched-fit-to-width-default.png new file mode 100644 index 000000000..97668b8b4 Binary files /dev/null and b/test/component/testplane/screens/6d1b4c6/chrome/mismatched-fit-to-width-default.png differ diff --git a/test/component/testplane/screens/6ee539e/chrome/diff-overlay-visible.png b/test/component/testplane/screens/6ee539e/chrome/diff-overlay-visible.png new file mode 100644 index 000000000..105d0a5b1 Binary files /dev/null and b/test/component/testplane/screens/6ee539e/chrome/diff-overlay-visible.png differ diff --git a/test/component/testplane/screens/70f6a43/chrome/portrait-fit-to-view-reduced.png b/test/component/testplane/screens/70f6a43/chrome/portrait-fit-to-view-reduced.png new file mode 100644 index 000000000..9e780cde5 Binary files /dev/null and b/test/component/testplane/screens/70f6a43/chrome/portrait-fit-to-view-reduced.png differ diff --git a/test/component/testplane/screens/73ac975/chrome/zoom-max-limit.png b/test/component/testplane/screens/73ac975/chrome/zoom-max-limit.png new file mode 100644 index 000000000..3cd0aad61 Binary files /dev/null and b/test/component/testplane/screens/73ac975/chrome/zoom-max-limit.png differ diff --git a/test/component/testplane/screens/750664a/chrome/zoom-centered.png b/test/component/testplane/screens/750664a/chrome/zoom-centered.png new file mode 100644 index 000000000..9dc9c7d52 Binary files /dev/null and b/test/component/testplane/screens/750664a/chrome/zoom-centered.png differ diff --git a/test/component/testplane/screens/86cb42a/chrome/portrait-fit-to-view-default.png b/test/component/testplane/screens/86cb42a/chrome/portrait-fit-to-view-default.png new file mode 100644 index 000000000..ad0caf370 Binary files /dev/null and b/test/component/testplane/screens/86cb42a/chrome/portrait-fit-to-view-default.png differ diff --git a/test/component/testplane/screens/8a013d9/chrome/zoom-out.png b/test/component/testplane/screens/8a013d9/chrome/zoom-out.png new file mode 100644 index 000000000..40a920b9a Binary files /dev/null and b/test/component/testplane/screens/8a013d9/chrome/zoom-out.png differ diff --git a/test/component/testplane/screens/b7ac6bc/chrome/small-fit-to-view-reduced.png b/test/component/testplane/screens/b7ac6bc/chrome/small-fit-to-view-reduced.png new file mode 100644 index 000000000..cfd559247 Binary files /dev/null and b/test/component/testplane/screens/b7ac6bc/chrome/small-fit-to-view-reduced.png differ diff --git a/test/component/testplane/screens/bc748b5/chrome/mismatched-fit-to-view-default.png b/test/component/testplane/screens/bc748b5/chrome/mismatched-fit-to-view-default.png new file mode 100644 index 000000000..d9befe9a6 Binary files /dev/null and b/test/component/testplane/screens/bc748b5/chrome/mismatched-fit-to-view-default.png differ diff --git a/test/component/testplane/screens/d15d9c7/chrome/landscape-fit-to-width-reduced.png b/test/component/testplane/screens/d15d9c7/chrome/landscape-fit-to-width-reduced.png new file mode 100644 index 000000000..b6ad2db24 Binary files /dev/null and b/test/component/testplane/screens/d15d9c7/chrome/landscape-fit-to-width-reduced.png differ diff --git a/test/component/testplane/screens/d1d2044/chrome/small-fit-to-view-default.png b/test/component/testplane/screens/d1d2044/chrome/small-fit-to-view-default.png new file mode 100644 index 000000000..85a32f210 Binary files /dev/null and b/test/component/testplane/screens/d1d2044/chrome/small-fit-to-view-default.png differ diff --git a/test/component/testplane/screens/d5b78c1/chrome/landscape-fit-to-view-default.png b/test/component/testplane/screens/d5b78c1/chrome/landscape-fit-to-view-default.png new file mode 100644 index 000000000..d0aee2466 Binary files /dev/null and b/test/component/testplane/screens/d5b78c1/chrome/landscape-fit-to-view-default.png differ diff --git a/test/component/testplane/screens/d7ea72b/chrome/landscape-fit-to-view-reduced.png b/test/component/testplane/screens/d7ea72b/chrome/landscape-fit-to-view-reduced.png new file mode 100644 index 000000000..b6ad2db24 Binary files /dev/null and b/test/component/testplane/screens/d7ea72b/chrome/landscape-fit-to-view-reduced.png differ diff --git a/test/component/testplane/screens/d850f26/chrome/fit-mode-reset.png b/test/component/testplane/screens/d850f26/chrome/fit-mode-reset.png new file mode 100644 index 000000000..34df38bc1 Binary files /dev/null and b/test/component/testplane/screens/d850f26/chrome/fit-mode-reset.png differ diff --git a/test/component/testplane/screens/e720214/chrome/portrait-fit-to-width-reduced.png b/test/component/testplane/screens/e720214/chrome/portrait-fit-to-width-reduced.png new file mode 100644 index 000000000..9e780cde5 Binary files /dev/null and b/test/component/testplane/screens/e720214/chrome/portrait-fit-to-width-reduced.png differ diff --git a/test/component/testplane/screens/ebeca08/chrome/portrait-fit-to-width-default.png b/test/component/testplane/screens/ebeca08/chrome/portrait-fit-to-width-default.png new file mode 100644 index 000000000..428ab1eee Binary files /dev/null and b/test/component/testplane/screens/ebeca08/chrome/portrait-fit-to-width-default.png differ diff --git a/test/component/testplane/screens/efdd329/chrome/mismatched-fit-to-view-reduced.png b/test/component/testplane/screens/efdd329/chrome/mismatched-fit-to-view-reduced.png new file mode 100644 index 000000000..e5ff1140b Binary files /dev/null and b/test/component/testplane/screens/efdd329/chrome/mismatched-fit-to-view-reduced.png differ diff --git a/test/component/testplane/screens/f08da40/chrome/pan-wheel.png b/test/component/testplane/screens/f08da40/chrome/pan-wheel.png new file mode 100644 index 000000000..23d52364b Binary files /dev/null and b/test/component/testplane/screens/f08da40/chrome/pan-wheel.png differ diff --git a/test/component/testplane/screens/fe500d2/chrome/zoom-in.png b/test/component/testplane/screens/fe500d2/chrome/zoom-in.png new file mode 100644 index 000000000..07e91b06f Binary files /dev/null and b/test/component/testplane/screens/fe500d2/chrome/zoom-in.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/actual.png b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/actual.png new file mode 100644 index 000000000..d1d320143 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/actual.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/diff.png b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/diff.png new file mode 100644 index 000000000..316016341 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/expected.png b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/expected.png new file mode 100644 index 000000000..980ebf0b1 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/black-square-on-green/expected.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/button/actual-button.png b/test/component/tests/TwoUpInteractiveMode/images/button/actual-button.png new file mode 100644 index 000000000..5326a3e44 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/button/actual-button.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/button/diff-button.png b/test/component/tests/TwoUpInteractiveMode/images/button/diff-button.png new file mode 100644 index 000000000..cc49c66d9 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/button/diff-button.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/button/expected-button.png b/test/component/tests/TwoUpInteractiveMode/images/button/expected-button.png new file mode 100644 index 000000000..2e5c09d7a Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/button/expected-button.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/detailed/actual-detailed.png b/test/component/tests/TwoUpInteractiveMode/images/detailed/actual-detailed.png new file mode 100644 index 000000000..49b627e04 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/detailed/actual-detailed.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/detailed/diff-detailed.png b/test/component/tests/TwoUpInteractiveMode/images/detailed/diff-detailed.png new file mode 100644 index 000000000..7ad58d80f Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/detailed/diff-detailed.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/detailed/expected-detailed.png b/test/component/tests/TwoUpInteractiveMode/images/detailed/expected-detailed.png new file mode 100644 index 000000000..17bb58b58 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/detailed/expected-detailed.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/landscape/actual-landscape.png b/test/component/tests/TwoUpInteractiveMode/images/landscape/actual-landscape.png new file mode 100644 index 000000000..7af6fa675 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/landscape/actual-landscape.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/landscape/diff-landscape.png b/test/component/tests/TwoUpInteractiveMode/images/landscape/diff-landscape.png new file mode 100644 index 000000000..858720f3b Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/landscape/diff-landscape.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/landscape/expected-landscape.png b/test/component/tests/TwoUpInteractiveMode/images/landscape/expected-landscape.png new file mode 100644 index 000000000..be2eaaf60 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/landscape/expected-landscape.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/large-diff/actual-large-diff.png b/test/component/tests/TwoUpInteractiveMode/images/large-diff/actual-large-diff.png new file mode 100644 index 000000000..93c1a6596 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/large-diff/actual-large-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/large-diff/diff-large-diff.png b/test/component/tests/TwoUpInteractiveMode/images/large-diff/diff-large-diff.png new file mode 100644 index 000000000..d827cac93 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/large-diff/diff-large-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/large-diff/expected-large-diff.png b/test/component/tests/TwoUpInteractiveMode/images/large-diff/expected-large-diff.png new file mode 100644 index 000000000..a5e6278e9 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/large-diff/expected-large-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/mismatched/actual-tall.png b/test/component/tests/TwoUpInteractiveMode/images/mismatched/actual-tall.png new file mode 100644 index 000000000..59a8e64d6 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/mismatched/actual-tall.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/mismatched/diff-mismatched.png b/test/component/tests/TwoUpInteractiveMode/images/mismatched/diff-mismatched.png new file mode 100644 index 000000000..ce552f2e9 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/mismatched/diff-mismatched.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/mismatched/expected-wide.png b/test/component/tests/TwoUpInteractiveMode/images/mismatched/expected-wide.png new file mode 100644 index 000000000..e4f9604f7 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/mismatched/expected-wide.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/portrait/actual-portrait.png b/test/component/tests/TwoUpInteractiveMode/images/portrait/actual-portrait.png new file mode 100644 index 000000000..9258502ab Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/portrait/actual-portrait.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/portrait/diff-portrait.png b/test/component/tests/TwoUpInteractiveMode/images/portrait/diff-portrait.png new file mode 100644 index 000000000..d601d81c2 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/portrait/diff-portrait.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/portrait/expected-portrait.png b/test/component/tests/TwoUpInteractiveMode/images/portrait/expected-portrait.png new file mode 100644 index 000000000..e7181e010 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/portrait/expected-portrait.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/small-diff/actual-small-diff.png b/test/component/tests/TwoUpInteractiveMode/images/small-diff/actual-small-diff.png new file mode 100644 index 000000000..92c57cc4f Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/small-diff/actual-small-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/small-diff/diff-small-diff.png b/test/component/tests/TwoUpInteractiveMode/images/small-diff/diff-small-diff.png new file mode 100644 index 000000000..fda9a3017 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/small-diff/diff-small-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/small-diff/expected-small-diff.png b/test/component/tests/TwoUpInteractiveMode/images/small-diff/expected-small-diff.png new file mode 100644 index 000000000..111b2d369 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/small-diff/expected-small-diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/standard/actual.png b/test/component/tests/TwoUpInteractiveMode/images/standard/actual.png new file mode 100644 index 000000000..aa945c327 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/standard/actual.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/standard/diff.png b/test/component/tests/TwoUpInteractiveMode/images/standard/diff.png new file mode 100644 index 000000000..c96b88753 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/standard/diff.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/images/standard/expected.png b/test/component/tests/TwoUpInteractiveMode/images/standard/expected.png new file mode 100644 index 000000000..2367e4cb7 Binary files /dev/null and b/test/component/tests/TwoUpInteractiveMode/images/standard/expected.png differ diff --git a/test/component/tests/TwoUpInteractiveMode/index.testplane.tsx b/test/component/tests/TwoUpInteractiveMode/index.testplane.tsx new file mode 100644 index 000000000..afd760a13 --- /dev/null +++ b/test/component/tests/TwoUpInteractiveMode/index.testplane.tsx @@ -0,0 +1,818 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import {TwoUpInteractiveModePure} from '../../../../lib/static/new-ui/components/DiffViewer/TwoUpInteractiveMode'; +import {TwoUpFitMode} from '../../../../lib/constants'; +import type {ImageFile} from '../../../../lib/types'; +import {ThemeProvider} from '@gravity-ui/uikit'; +import {Key} from 'testplane'; + +import '../../styles.css'; + +async function waitForFonts(browser: WebdriverIO.Browser): Promise { + await browser.waitUntil( + async () => { + return await browser.execute(() => { + return document.fonts.ready.then(() => true); + }); + }, + { + timeout: 5000, + timeoutMsg: 'Fonts did not load within 5 seconds' + } + ); +} + +import expectedStandard from './images/standard/expected.png'; +import diffStandard from './images/standard/diff.png'; +import actualStandard from './images/standard/actual.png'; + +import expectedWide from './images/mismatched/expected-wide.png'; +import expectedDetailed from './images/detailed/expected-detailed.png'; +import actualDetailed from './images/detailed/actual-detailed.png'; +import expectedPortrait from './images/portrait/expected-portrait.png'; +import actualPortrait from './images/portrait/actual-portrait.png'; + +import expectedLandscape from './images/landscape/expected-landscape.png'; +import actualLandscape from './images/landscape/actual-landscape.png'; +import actualTall from './images/mismatched/actual-tall.png'; + +import expectedButton from './images/button/expected-button.png'; +import actualButton from './images/button/actual-button.png'; + +describe('TwoUpInteractiveMode', () => { + describe('Side-by-Side Display', () => { + it('displays both expected and actual images side-by-side', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('side-by-side-display'); + }); + + it('shows image dimensions in labels', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const expectedLabel = await browser.findByTestId('image-label-expected'); + const actualLabel = await browser.findByTestId('image-label-actual'); + + const expectedText = await expectedLabel.getText(); + const actualText = await actualLabel.getText(); + + expect(expectedText).toContain('1920'); + expect(expectedText).toContain('1080'); + expect(actualText).toContain('1920'); + expect(actualText).toContain('1080'); + }); + + it('shows diff statistics when available', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const actualLabel = await browser.findByTestId('image-label-actual'); + const labelText = await actualLabel.getText(); + + // 2500 pixels is formatted as "~3k px" + expect(labelText).toContain('~3k px'); + expect(labelText).toContain('different'); + expect(labelText).toContain('12'); + }); + + it('handles images with different dimensions', async ({browser}) => { + const expected: ImageFile = {path: expectedWide, size: {width: 1000, height: 200}}; + const actual: ImageFile = {path: actualTall, size: {width: 200, height: 900}}; + + render( + + + + ); + + const images = await browser.$$('img'); + await expect(images).toHaveLength(2); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('different-dimensions'); + }); + + it('handles images with same dimensions', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const images = await browser.$$('img'); + await expect(images).toHaveLength(2); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('same-dimensions'); + }); + + describe('Diff Overlay Visualization', () => { + it('shows diff overlay when enabled', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + const diff: ImageFile = {path: diffStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const images = await browser.$$('img'); + await expect(images).toHaveLength(3); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('diff-overlay-visible'); + }); + + it('hides diff overlay when disabled', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + const diff: ImageFile = {path: diffStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const images = await browser.$$('img'); + await expect(images).toHaveLength(2); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('diff-overlay-hidden'); + }); + + it('diff overlay only appears on actual side', async ({browser}) => { + const expected: ImageFile = {path: expectedStandard, size: {width: 1920, height: 1080}}; + const actual: ImageFile = {path: actualStandard, size: {width: 1920, height: 1080}}; + const diff: ImageFile = {path: diffStandard, size: {width: 1920, height: 1080}}; + + render( + + + + ); + + const expectedPanel = await browser.$('[data-testid="image-panel-expected"]'); + const actualPanel = await browser.$('[data-testid="image-panel-actual"]'); + + const expectedImages = await expectedPanel.$$('img'); + const actualImages = await actualPanel.$$('img'); + + await expect(expectedImages).toHaveLength(1); + await expect(actualImages).toHaveLength(2); + }); + }); + }); +}); + +describe('Zoom Controls', () => { + it('zoom in button increases image size', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomInButton = await browser.$('[title="Zoom in"]'); + await zoomInButton.click(); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('zoom-in'); + }); + + it('zoom out button decreases image size', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomOutButton = await browser.$('[title="Zoom out"]'); + await zoomOutButton.click(); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('zoom-out'); + }); + + it('zoom centers on mouse cursor position', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const imageContainer = await browser.$('[data-testid="image-panel-expected"]'); + const size = await imageContainer.getSize(); + const offsetX = Math.round(-size.width / 2 + 2); + const offsetY = Math.round(-size.height / 2 + 10); + + await browser.actions([ + browser.action('key').down(Key.Ctrl), + browser.action('wheel').scroll({x: offsetX, y: offsetY, deltaX: 0, deltaY: -140, origin: imageContainer}), + browser.action('key').up(Key.Ctrl) + ]); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('zoom-centered'); + }); + + it('zoom has minimum limit', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomOutButton = await browser.$('[title="Zoom out"]'); + + for (let i = 0; i < 20; i++) { + await zoomOutButton.click(); + } + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('zoom-min-limit'); + }); + + it('zoom has maximum limit', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomInButton = await browser.$('[title="Zoom in"]'); + + for (let i = 0; i < 50; i++) { + await zoomInButton.click(); + } + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('zoom-max-limit'); + }); +}); + +describe('Pan Navigation', () => { + it('mouse drag pans the view', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomInButton = await browser.$('[title="Zoom in"]'); + await zoomInButton.click(); + + const imageContainer = await browser.$('[data-testid="image-panel-expected"]'); + + await browser.actions([ + browser.action('pointer') + .move({origin: imageContainer}) + .down() + .move({x: 200, y: 100, origin: 'pointer'}) + .up() + ]); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + // Wait for momentum to finish + await browser.pause(200); + await container.assertView('pan-drag'); + }); + + it('mouse wheel without Ctrl pans the view', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomInButton = await browser.$('[title="Zoom in"]'); + await zoomInButton.click(); + + const imageContainer = await browser.$('[data-testid="image-panel-expected"]'); + + await browser + .action('wheel') + .scroll({deltaX: -200, deltaY: -100, x: 0, y: 0, origin: imageContainer}) + .perform(); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + // Wait for momentum to finish + await browser.pause(200); + await container.assertView('pan-wheel'); + }); +}); + +describe('Fit Mode Controls', () => { + describe('Landscape Images', () => { + it('fit to width at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedLandscape, size: {width: 1600, height: 400}}; + const actual: ImageFile = {path: actualLandscape, size: {width: 1600, height: 400}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('landscape-fit-to-width-default'); + }); + + it('fit to width at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedLandscape, size: {width: 1600, height: 400}}; + const actual: ImageFile = {path: actualLandscape, size: {width: 1600, height: 400}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('landscape-fit-to-width-reduced'); + }); + + it('fit to view at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedLandscape, size: {width: 1600, height: 400}}; + const actual: ImageFile = {path: actualLandscape, size: {width: 1600, height: 400}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('landscape-fit-to-view-default'); + }); + + it('fit to view at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedLandscape, size: {width: 1600, height: 400}}; + const actual: ImageFile = {path: actualLandscape, size: {width: 1600, height: 400}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('landscape-fit-to-view-reduced'); + }); + }); + + describe('Portrait Images', () => { + it('fit to width at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedPortrait, size: {width: 600, height: 1200}}; + const actual: ImageFile = {path: actualPortrait, size: {width: 600, height: 1200}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('portrait-fit-to-width-default'); + }); + + it('fit to width at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedPortrait, size: {width: 600, height: 1200}}; + const actual: ImageFile = {path: actualPortrait, size: {width: 600, height: 1200}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('portrait-fit-to-width-reduced'); + }); + + it('fit to view at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedPortrait, size: {width: 600, height: 1200}}; + const actual: ImageFile = {path: actualPortrait, size: {width: 600, height: 1200}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('portrait-fit-to-view-default'); + }); + + it('fit to view at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedPortrait, size: {width: 600, height: 1200}}; + const actual: ImageFile = {path: actualPortrait, size: {width: 600, height: 1200}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('portrait-fit-to-view-reduced'); + }); + }); + + describe('Small Images', () => { + it('fit to width at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedButton, size: {width: 120, height: 40}}; + const actual: ImageFile = {path: actualButton, size: {width: 120, height: 40}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('small-fit-to-width-default'); + }); + + it('fit to width at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedButton, size: {width: 120, height: 40}}; + const actual: ImageFile = {path: actualButton, size: {width: 120, height: 40}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('small-fit-to-width-reduced'); + }); + + it('fit to view at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedButton, size: {width: 120, height: 40}}; + const actual: ImageFile = {path: actualButton, size: {width: 120, height: 40}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('small-fit-to-view-default'); + }); + + it('fit to view at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedButton, size: {width: 120, height: 40}}; + const actual: ImageFile = {path: actualButton, size: {width: 120, height: 40}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('small-fit-to-view-reduced'); + }); + }); + + describe('Mismatched Dimensions', () => { + it('fit to width with portrait and landscape images at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedWide, size: {width: 1000, height: 200}}; + const actual: ImageFile = {path: actualTall, size: {width: 200, height: 900}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('mismatched-fit-to-width-default'); + }); + + it('fit to width with portrait and landscape images at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedWide, size: {width: 1000, height: 200}}; + const actual: ImageFile = {path: actualTall, size: {width: 200, height: 900}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('mismatched-fit-to-width-reduced'); + }); + + it('fit to view with portrait and landscape images at default window size', async ({browser}) => { + const expected: ImageFile = {path: expectedWide, size: {width: 1000, height: 200}}; + const actual: ImageFile = {path: actualTall, size: {width: 200, height: 900}}; + + render( + + + + ); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('mismatched-fit-to-view-default'); + }); + + it('fit to view with portrait and landscape images at reduced window size', async ({browser}) => { + const expected: ImageFile = {path: expectedWide, size: {width: 1000, height: 200}}; + const actual: ImageFile = {path: actualTall, size: {width: 200, height: 900}}; + + render( + + + + ); + + await browser.setWindowSize(800, 600); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('mismatched-fit-to-view-reduced'); + }); + }); + + it('switching fit modes resets zoom and pan', async ({browser}) => { + const expected: ImageFile = {path: expectedDetailed, size: {width: 800, height: 600}}; + const actual: ImageFile = {path: actualDetailed, size: {width: 800, height: 600}}; + + render( + + + + ); + + const zoomInButton = await browser.$('[title="Zoom in"]'); + await zoomInButton.click(); + await zoomInButton.click(); + + const imageContainer = await browser.$('[data-testid="image-panel-expected"]'); + await imageContainer.dragAndDrop({x: -100, y: -50}); + + const fitToWidthButton = await browser.$('[title="Fit to width"]'); + await fitToWidthButton.click(); + + const container = await browser.$('[data-testid="two-up-interactive-mode"]'); + await waitForFonts(browser); + await container.assertView('fit-mode-reset'); + }); +}); diff --git a/test/component/tsconfig.json b/test/component/tsconfig.json new file mode 100644 index 000000000..7131f573f --- /dev/null +++ b/test/component/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@/*": ["../../lib/*"], + "lib/*": ["../../lib/*"] + }, + "types": ["testplane", "@testplane/testing-library"] + }, + "include": ["tests/**/*", "../../lib/**/*", "typings.d.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/test/component/typings.d.ts b/test/component/typings.d.ts new file mode 100644 index 000000000..36ea77aff --- /dev/null +++ b/test/component/typings.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const value: string; + export default value; +} diff --git a/test/component/vite.config.ts b/test/component/vite.config.ts new file mode 100644 index 000000000..b3f43bb75 --- /dev/null +++ b/test/component/vite.config.ts @@ -0,0 +1,44 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true + }, + define: { + // Provide CommonJS globals for browser + 'global': 'globalThis', + 'exports': '{}', + 'module': '{ exports: {} }' + }, + optimizeDeps: { + include: [ + 'lib/**/*.js', + 'expect', + 'aria-query', + 'css-shorthand-properties', + 'css-value', + 'grapheme-splitter', + 'lodash.clonedeep', + 'lodash.zip', + 'minimatch', + 'rgb2hex', + 'ws' + ] + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '../../lib'), + 'lib': path.resolve(__dirname, '../../lib') + } + }, + css: { + modules: { + localsConvention: 'camelCase' + } + } +});