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}`;