diff --git a/lib/constants/features.ts b/lib/constants/features.ts index 0fa9ddc4f..e1c915a11 100644 --- a/lib/constants/features.ts +++ b/lib/constants/features.ts @@ -5,3 +5,7 @@ export interface Feature { export const RunTestsFeature = { name: 'run-tests' } as const satisfies Feature; + +export const EditScreensFeature = { + name: 'edit-screens' +} as const satisfies Feature; diff --git a/lib/static/modules/reducers/gui.js b/lib/static/modules/reducers/gui.js index 28ddca750..229e9d5e9 100644 --- a/lib/static/modules/reducers/gui.js +++ b/lib/static/modules/reducers/gui.js @@ -1,11 +1,11 @@ import actionNames from '../action-names'; import {applyStateUpdate} from '@/static/modules/utils/state'; -import {RunTestsFeature} from '@/constants'; +import {EditScreensFeature, RunTestsFeature} from '@/constants'; export default (state, action) => { switch (action.type) { case actionNames.INIT_GUI_REPORT: { - return applyStateUpdate(state, {gui: true, app: {availableFeatures: [RunTestsFeature]}}); + return applyStateUpdate(state, {gui: true, app: {availableFeatures: [RunTestsFeature, EditScreensFeature]}}); } case actionNames.INIT_STATIC_REPORT: { diff --git a/lib/static/modules/utils/index.js b/lib/static/modules/utils/index.js index 522772424..602318579 100644 --- a/lib/static/modules/utils/index.js +++ b/lib/static/modules/utils/index.js @@ -30,6 +30,12 @@ export function isNodeSuccessful(node) { return isSuccessStatus(node.status) || isUpdatedStatus(node.status) || isStagedStatus(node.status) || isCommitedStatus(node.status); } +/** + * @param {Object} params + * @param {string} params.status + * @param {Object} [params.error] + * @returns {boolean} + */ export function isAcceptable({status, error}) { return isErrorStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status); } diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index d10c3af8f..343e314b7 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -42,4 +42,6 @@ body { .action-button { font-size: 15px; font-weight: 450; + /* Sets spinner color */ + --g-color-line-brand: var(--g-color-text-hint); } diff --git a/lib/static/new-ui/components/AssertViewResult/index.module.css b/lib/static/new-ui/components/AssertViewResult/index.module.css index 1fd3b7988..40a43d65c 100644 --- a/lib/static/new-ui/components/AssertViewResult/index.module.css +++ b/lib/static/new-ui/components/AssertViewResult/index.module.css @@ -1,17 +1,8 @@ -.diff-viewer-container { - display: flex; - flex-direction: column; - padding-left: calc(var(--indent) * 24px); - padding-right: 1px -} - -.diff-mode-switcher { - --g-color-base-background: #fff; - margin: 12px auto; -} - .screenshot { - margin: 8px 0; - padding-left: calc(var(--indent) * 24px); padding-right: 1px; } + +.screenshot-container { + display: flex; + flex-direction: column; +} diff --git a/lib/static/new-ui/components/AssertViewResult/index.tsx b/lib/static/new-ui/components/AssertViewResult/index.tsx index 050c81e45..cb0679e46 100644 --- a/lib/static/new-ui/components/AssertViewResult/index.tsx +++ b/lib/static/new-ui/components/AssertViewResult/index.tsx @@ -1,39 +1,33 @@ import React, {ReactNode} from 'react'; +import {connect} from 'react-redux'; + import {ImageEntity, State} from '@/static/new-ui/types/store'; -import {DiffModeId, DiffModes, TestStatus} from '@/constants'; +import {DiffModeId, TestStatus} from '@/constants'; import {DiffViewer} from '../DiffViewer'; -import {RadioButton} from '@gravity-ui/uikit'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import * as actions from '@/static/modules/actions'; -import styles from './index.module.css'; import {Screenshot} from '@/static/new-ui/components/Screenshot'; +import {ImageLabel} from '@/static/new-ui/components/ImageLabel'; +import {getImageDisplayedSize} from '@/static/new-ui/utils'; +import styles from './index.module.css'; interface AssertViewResultProps { result: ImageEntity; style?: React.CSSProperties; - actions: typeof actions; diffMode: DiffModeId; } -function AssertViewResultInternal({result, actions, diffMode, style}: AssertViewResultProps): ReactNode { +function AssertViewResultInternal({result, diffMode, style}: AssertViewResultProps): ReactNode { if (result.status === TestStatus.FAIL) { - const onChangeHandler = (diffMode: DiffModeId): void => { - actions.changeDiffMode(diffMode); - }; - - return
- - {Object.values(DiffModes).map(diffMode => - - )} - - -
; + return ; } else if (result.status === TestStatus.ERROR) { - return ; + return
+ + +
; } else if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) { - return ; + return
+ + +
; } return null; @@ -41,5 +35,4 @@ function AssertViewResultInternal({result, actions, diffMode, style}: AssertView export const AssertViewResult = connect((state: State) => ({ diffMode: state.view.diffMode -}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(AssertViewResultInternal); +}))(AssertViewResultInternal); diff --git a/lib/static/new-ui/components/AssertViewStatus/index.module.css b/lib/static/new-ui/components/AssertViewStatus/index.module.css new file mode 100644 index 000000000..a18cc8567 --- /dev/null +++ b/lib/static/new-ui/components/AssertViewStatus/index.module.css @@ -0,0 +1,9 @@ +.container { + display: flex; + gap: 4px; + align-items: center; + + color: var(--g-color-private-cool-grey-700-solid); + font-size: 15px; + font-weight: 450; +} diff --git a/lib/static/new-ui/components/AssertViewStatus/index.tsx b/lib/static/new-ui/components/AssertViewStatus/index.tsx new file mode 100644 index 000000000..4fa582abb --- /dev/null +++ b/lib/static/new-ui/components/AssertViewStatus/index.tsx @@ -0,0 +1,29 @@ +import React, {ReactNode} from 'react'; +import {ImageEntity, ImageEntityError} from '@/static/new-ui/types/store'; +import {TestStatus} from '@/constants'; +import {Icon} from '@gravity-ui/uikit'; +import {FileCheck, CircleCheck, SquareExclamation, SquareXmark, FileLetterX, ArrowRightArrowLeft} from '@gravity-ui/icons'; +import {isNoRefImageError} from '@/common-utils'; +import styles from './index.module.css'; + +interface AssertViewStatusProps { + image: ImageEntity | null; +} + +export function AssertViewStatus({image}: AssertViewStatusProps): ReactNode { + let status = <>Failed to compare; + + if (image === null) { + status = <>Image is absent; + } else if (image.status === TestStatus.SUCCESS) { + status = <>Images match; + } else if (isNoRefImageError((image as ImageEntityError).error)) { + status = <>Reference not found; + } else if (image.status === TestStatus.FAIL) { + status = <>Difference detected; + } else if (image.status === TestStatus.UPDATED) { + status = <>Reference updated; + } + + return
{status}
; +} diff --git a/lib/static/new-ui/components/AttemptPicker/index.module.css b/lib/static/new-ui/components/AttemptPicker/index.module.css index 0dbcea499..a2c76f5f0 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.module.css +++ b/lib/static/new-ui/components/AttemptPicker/index.module.css @@ -16,6 +16,4 @@ .retry-button { composes: action-button from global; margin-left: auto; - /* Sets spinner color */ - --g-color-line-brand: var(--g-color-text-hint); } diff --git a/lib/static/new-ui/components/CompactAttemptPicker/index.module.css b/lib/static/new-ui/components/CompactAttemptPicker/index.module.css new file mode 100644 index 000000000..1b03e5f99 --- /dev/null +++ b/lib/static/new-ui/components/CompactAttemptPicker/index.module.css @@ -0,0 +1,21 @@ +.container { + display: flex; + gap: 4px; +} + +.attempt-select { + font-size: 15px; +} + +.attempt-number { + font-weight: 450; +} + +.attempt-option { + display: flex; + gap: 8px; +} + +.attempt-select-popup { + max-height: 40vh; +} diff --git a/lib/static/new-ui/components/CompactAttemptPicker/index.tsx b/lib/static/new-ui/components/CompactAttemptPicker/index.tsx new file mode 100644 index 000000000..c1c49cf31 --- /dev/null +++ b/lib/static/new-ui/components/CompactAttemptPicker/index.tsx @@ -0,0 +1,70 @@ +import {ChevronLeft, ChevronRight} from '@gravity-ui/icons'; +import {Button, Icon, Select} from '@gravity-ui/uikit'; +import React, {ReactNode} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; + +import styles from './index.module.css'; +import {State} from '@/static/new-ui/types/store'; +import {getCurrentNamedImage} from '@/static/new-ui/features/visual-checks/selectors'; +import {getIconByStatus} from '@/static/new-ui/utils'; +import {changeTestRetry} from '@/static/modules/actions'; + +export function CompactAttemptPicker(): ReactNode { + const dispatch = useDispatch(); + const currentImage = useSelector(getCurrentNamedImage); + const currentBrowserId = currentImage?.browserId; + const currentBrowser = useSelector((state: State) => currentBrowserId && state.tree.browsers.byId[currentBrowserId]); + const resultsById = useSelector((state: State) => state.tree.results.byId); + + const totalAttemptsCount = currentBrowser ? currentBrowser.resultIds.length : null; + const currentAttemptIndex = useSelector((state: State) => currentBrowser ? state.tree.browsers.stateById[currentBrowser.id].retryIndex : null); + + const onUpdate = ([value]: string[]): void => { + if (currentBrowserId) { + dispatch(changeTestRetry({browserId: currentBrowserId, retryIndex: Number(value)})); + } + }; + + const onPreviousClick = (): void => { + if (currentBrowserId && currentAttemptIndex !== null && currentAttemptIndex > 0) { + dispatch(changeTestRetry({browserId: currentBrowserId, retryIndex: currentAttemptIndex - 1})); + } + }; + + const onNextClick = (): void => { + if (currentBrowserId && currentAttemptIndex !== null && totalAttemptsCount !== null && currentAttemptIndex < totalAttemptsCount - 1) { + dispatch(changeTestRetry({browserId: currentBrowserId, retryIndex: currentAttemptIndex + 1})); + } + }; + + if (!currentBrowser) { + return null; + } + + return
+ + + +
; +} diff --git a/lib/static/new-ui/components/DiffViewer/index.module.css b/lib/static/new-ui/components/DiffViewer/index.module.css index 39472559e..e69de29bb 100644 --- a/lib/static/new-ui/components/DiffViewer/index.module.css +++ b/lib/static/new-ui/components/DiffViewer/index.module.css @@ -1,8 +0,0 @@ -.image-label, .image-label + div { - margin-bottom: 8px; -} - -.image-label-subtitle { - color: var(--g-color-private-black-400); - margin-left: 4px; -} diff --git a/lib/static/new-ui/components/DiffViewer/index.tsx b/lib/static/new-ui/components/DiffViewer/index.tsx index ac39f643d..fab6659df 100644 --- a/lib/static/new-ui/components/DiffViewer/index.tsx +++ b/lib/static/new-ui/components/DiffViewer/index.tsx @@ -11,7 +11,8 @@ import {SideBySideToFitMode} from '@/static/new-ui/components/DiffViewer/SideByS import {ListMode} from '@/static/new-ui/components/DiffViewer/ListMode'; import {getDisplayedDiffPercentValue} from '@/static/new-ui/components/DiffViewer/utils'; -import styles from './index.module.css'; +import {ImageLabel} from '@/static/new-ui/components/ImageLabel'; +import {getImageDisplayedSize} from '@/static/new-ui/utils'; interface DiffViewerProps { actualImg: ImageFile; @@ -31,26 +32,18 @@ interface DiffViewerProps { } export function DiffViewer(props: DiffViewerProps): ReactNode { - const getImageDisplayedSize = (image: ImageFile): string => `${image.size.width}×${image.size.height}`; - const getImageLabel = (title: string, subtitle?: string): ReactNode => { - return
- {title} - {subtitle && {subtitle}} -
; - }; - const expectedImg = Object.assign({}, props.expectedImg, { - label: getImageLabel('Expected', getImageDisplayedSize(props.expectedImg)) + label: }); const actualImg = Object.assign({}, props.actualImg, { - label: getImageLabel('Actual', getImageDisplayedSize(props.actualImg)) + label: }); let diffSubtitle: string | undefined; if (props.differentPixels !== undefined && props.diffRatio !== undefined) { diffSubtitle = `${props.differentPixels}px ⋅ ${getDisplayedDiffPercentValue(props.diffRatio)}%`; } const diffImg = Object.assign({}, props.diffImg, { - label: getImageLabel('Diff', diffSubtitle), + label: , diffClusters: props.diffClusters }); diff --git a/lib/static/new-ui/components/ImageLabel/index.module.css b/lib/static/new-ui/components/ImageLabel/index.module.css new file mode 100644 index 000000000..39472559e --- /dev/null +++ b/lib/static/new-ui/components/ImageLabel/index.module.css @@ -0,0 +1,8 @@ +.image-label, .image-label + div { + margin-bottom: 8px; +} + +.image-label-subtitle { + color: var(--g-color-private-black-400); + margin-left: 4px; +} diff --git a/lib/static/new-ui/components/ImageLabel/index.tsx b/lib/static/new-ui/components/ImageLabel/index.tsx new file mode 100644 index 000000000..3b4a26fda --- /dev/null +++ b/lib/static/new-ui/components/ImageLabel/index.tsx @@ -0,0 +1,14 @@ +import React, {ReactNode} from 'react'; +import styles from './index.module.css'; + +interface ImageLabelProps { + title: string; + subtitle?: string; +} + +export function ImageLabel({title, subtitle}: ImageLabelProps): ReactNode { + return
+ {title} + {subtitle && {subtitle}} +
; +} diff --git a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.module.css b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.module.css new file mode 100644 index 000000000..c04265b2d --- /dev/null +++ b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.module.css @@ -0,0 +1,50 @@ +.container { + display: flex; + flex-direction: column; + padding: 8px 1px 4px calc(var(--indent) * 24px); + row-gap: 8px; +} + +.toolbar-container { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.toolbar-container > div:only-child { + justify-content: center; +} + +.accept-button { + composes: action-button from global; +} + +.buttons-container { + display: flex; + margin-left: auto; +} + +.diff-mode-container { + container-type: inline-size; + flex-grow: 1; + display: flex; +} + +.diff-mode-switcher { + --g-color-base-background: #fff; +} + +.diff-mode-select { + display: none; +} + +@container (max-width: 500px) { + .diff-mode-switcher { + display: none !important; + } + + .diff-mode-select { + display: block; + } +} diff --git a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx new file mode 100644 index 000000000..b5c763044 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx @@ -0,0 +1,78 @@ +import {ArrowUturnCcwLeft, Check} from '@gravity-ui/icons'; +import {Button, Icon, RadioButton, Select} from '@gravity-ui/uikit'; +import React, {ReactNode} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; + +import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; +import {ImageEntity, ImageEntityFail, State} from '@/static/new-ui/types/store'; +import {DiffModeId, DiffModes, EditScreensFeature, TestStatus} from '@/constants'; +import {acceptTest, changeDiffMode, undoAcceptImage} from '@/static/modules/actions'; +import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; +import {getCurrentBrowser, getCurrentResult} from '@/static/new-ui/features/suites/selectors'; +import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; +import styles from './index.module.css'; + +interface ScreenshotsTreeViewItemProps { + image: ImageEntity; + style?: React.CSSProperties; +} + +export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): ReactNode { + const dispatch = useDispatch(); + const diffMode = useSelector((state: State) => state.view.diffMode); + const isEditScreensAvailable = useSelector((state: State) => state.app.availableFeatures) + .find(feature => feature.name === EditScreensFeature.name); + const isRunning = useSelector((state: State) => state.running); + const isGui = useSelector((state: State) => state.gui); + + const currentBrowser = useSelector(getCurrentBrowser); + const currentResult = useSelector(getCurrentResult); + const isLastResult = currentResult && currentBrowser && currentResult.id === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]; + const isUndoAvailable = isScreenRevertable({gui: isGui, image: props.image, isLastResult, isStaticImageAccepterEnabled: false}); + + const onDiffModeChangeHandler = (diffMode: DiffModeId): void => { + dispatch(changeDiffMode(diffMode)); + }; + + const onScreenshotAccept = (): void => { + dispatch(acceptTest(props.image.id)); + }; + const onScreenshotUndo = (): void => { + dispatch(undoAcceptImage(props.image.id)); + }; + + return
+ {props.image.status !== TestStatus.SUCCESS &&
+ {!(props.image as ImageEntityFail).diffImg && + } + {(props.image as ImageEntityFail).diffImg && +
+ + {Object.values(DiffModes).map(diffMode => + + )} + + +
+ } + {isEditScreensAvailable &&
+ {isUndoAvailable && } + {isAcceptable(props.image) && } +
} +
} + +
; +} diff --git a/lib/static/new-ui/features/suites/components/TestSteps/index.tsx b/lib/static/new-ui/features/suites/components/TestSteps/index.tsx index ccd3623a4..2cb033de2 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestSteps/index.tsx @@ -21,9 +21,9 @@ import {Step, StepType} from './types'; import {ListItemViewContentType, TreeViewItem} from '../../../../components/TreeViewItem'; import styles from './index.module.css'; import {Screenshot} from '@/static/new-ui/components/Screenshot'; -import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; import {getIndentStyle} from '@/static/new-ui/features/suites/components/TestSteps/utils'; import {isErrorStatus, isFailStatus} from '@/common-utils'; +import {ScreenshotsTreeViewItem} from '@/static/new-ui/features/suites/components/ScreenshotsTreeViewItem'; interface TestStepsProps { resultId: string; @@ -85,7 +85,7 @@ function TestStepsInternal(props: TestStepsProps): ReactNode { } else if (item.type === StepType.SingleImage) { return ; } else if (item.type === StepType.AssertViewResult) { - return ; + return ; } return null; 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 5f48448b9..7ab927ee5 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 @@ -6,3 +6,28 @@ margin-bottom: 8px; padding-top: 20px; } + +.toolbar-container { + --g-color-text-primary: var(--g-color-private-cool-grey-700-solid); + color: var(--g-color-private-cool-grey-700-solid); + display: flex; + gap: 16px; + margin-bottom: 8px; +} + +.accept-button { + composes: action-button from global; +} + +.buttons-container { + margin-left: auto; +} + +.hint { + align-items: center; + color: var(--g-color-private-black-400); + display: flex; + flex-grow: 1; + font-weight: 500; + justify-content: center; +} 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 ef9c49ed4..d0cbd89ba 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 @@ -1,38 +1,71 @@ +import {ArrowUturnCcwLeft, Check} from '@gravity-ui/icons'; +import {Button, Divider, Icon, Select} from '@gravity-ui/uikit'; import classNames from 'classnames'; import React, {ReactNode} from 'react'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; +import {useDispatch, useSelector} from 'react-redux'; import {SplitViewLayout} from '@/static/new-ui/components/SplitViewLayout'; -import {ImageEntity, State} from '@/static/new-ui/types/store'; -import * as actions from '@/static/modules/actions'; +import {State} from '@/static/new-ui/types/store'; import {UiCard} from '@/static/new-ui/components/Card/UiCard'; import { getCurrentImage, getCurrentNamedImage, - getVisibleNamedImageIds, - NamedImageEntity + getVisibleNamedImageIds } from '@/static/new-ui/features/visual-checks/selectors'; import {SuiteTitle} from '@/static/new-ui/components/SuiteTitle'; import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; import styles from './index.module.css'; +import {CompactAttemptPicker} from '@/static/new-ui/components/CompactAttemptPicker'; +import {DiffModeId, DiffModes, EditScreensFeature} from '@/constants'; +import { + acceptTest, + changeDiffMode, + undoAcceptImage, + visualChecksPageSetCurrentNamedImage +} from '@/static/modules/actions'; +import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; +import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; -interface VisualChecksPageProps { - currentNamedImage: NamedImageEntity | null; - currentImage: ImageEntity | null; - visibleNamedImageIds: string[]; - actions: typeof actions; -} +export function VisualChecksPage(): ReactNode { + const dispatch = useDispatch(); + + const currentNamedImage = useSelector(getCurrentNamedImage); + const currentImage = useSelector(getCurrentImage); + const visibleNamedImageIds = useSelector(getVisibleNamedImageIds); -export function VisualChecksPageInternal({currentNamedImage, currentImage, visibleNamedImageIds, actions}: VisualChecksPageProps): ReactNode { const currentNamedImageIndex = visibleNamedImageIds.indexOf(currentNamedImage?.id as string); - const onPreviousImageHandler = (): void => void actions.visualChecksPageSetCurrentNamedImage(visibleNamedImageIds[currentNamedImageIndex - 1]); - const onNextImageHandler = (): void => void actions.visualChecksPageSetCurrentNamedImage(visibleNamedImageIds[currentNamedImageIndex + 1]); + const onPreviousImageHandler = (): void => void dispatch(visualChecksPageSetCurrentNamedImage(visibleNamedImageIds[currentNamedImageIndex - 1])); + const onNextImageHandler = (): void => void dispatch(visualChecksPageSetCurrentNamedImage(visibleNamedImageIds[currentNamedImageIndex + 1])); + + const diffMode = useSelector((state: State) => state.view.diffMode); + const onChangeHandler = (diffMode: DiffModeId): void => { + dispatch(changeDiffMode(diffMode)); + }; + + const isEditScreensAvailable = useSelector((state: State) => state.app.availableFeatures) + .find(feature => feature.name === EditScreensFeature.name); + const isRunning = useSelector((state: State) => state.running); + const isGui = useSelector((state: State) => state.gui); + + const onScreenshotAccept = (): void => { + if (currentImage) { + dispatch(acceptTest(currentImage.id)); + } + }; + const onScreenshotUndo = (): void => { + if (currentImage) { + dispatch(undoAcceptImage(currentImage.id)); + } + }; + + const currentBrowserId = useSelector((state: State) => state.tree.results.byId[currentImage?.parentId ?? '']?.parentId); + const currentBrowser = useSelector((state: State) => currentBrowserId && state.tree.browsers.byId[currentBrowserId]); + + const currentResultId = currentImage?.parentId; + const isLastResult = Boolean(currentResultId && currentBrowser && currentResultId === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]); + const isUndoAvailable = isScreenRevertable({gui: isGui, image: currentImage ?? {}, isLastResult, isStaticImageAccepterEnabled: false}); return -

Visual Checks

- ,
{currentNamedImage && } +
+ + + + + + {isEditScreensAvailable &&
+ {isUndoAvailable && } + {currentImage && isAcceptable(currentImage) && } +
} +
{currentImage && } + {!currentImage &&
This run doesn't have an image with name "{currentNamedImage?.stateName}"
}
]}/>; } - -export const VisualChecksPage = connect( - (state: State) => { - const currentNamedImage = getCurrentNamedImage(state); - const currentImage = getCurrentImage(state); - - return { - currentNamedImage, - currentImage, - visibleNamedImageIds: getVisibleNamedImageIds(state) - }; - }, - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(VisualChecksPageInternal); diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 42d5c81bb..07ae0bc7e 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -157,6 +157,7 @@ export interface State { baseHost: string; }; running: boolean; + gui: boolean; apiValues: HtmlReporterValues; config: ReporterConfig; } diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 39d6c08d6..0415d12a2 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -3,6 +3,7 @@ import {Spin} from '@gravity-ui/uikit'; import React from 'react'; import {TestStatus} from '@/constants'; +import {ImageFile} from '@/types'; export const getIconByStatus = (status: TestStatus): React.JSX.Element => { if (status === TestStatus.FAIL || status === TestStatus.ERROR) { @@ -27,3 +28,5 @@ export const getFullTitleByTitleParts = (titleParts: string[]): string => { return titleParts.join(DELIMITER).trim(); }; + +export const getImageDisplayedSize = (image: ImageFile): string => `${image.size.width}×${image.size.height}`;