Skip to content

Commit

Permalink
feat(new-ui): add ability to accept and undo screenshots (#605)
Browse files Browse the repository at this point in the history
* feat(new-ui): add ability to accept images in suites mode

* feat(new-ui): add ability to accept and undo screenshots

* feat(new-ui): enhance image labels

* feat(new-ui): draw diff mode picker in the center
  • Loading branch information
shadowusr authored Oct 8, 2024
1 parent eb131fd commit 503a9a5
Show file tree
Hide file tree
Showing 22 changed files with 420 additions and 96 deletions.
4 changes: 4 additions & 0 deletions lib/constants/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 2 additions & 2 deletions lib/static/modules/reducers/gui.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
6 changes: 6 additions & 0 deletions lib/static/modules/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/static/new-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
19 changes: 5 additions & 14 deletions lib/static/new-ui/components/AssertViewResult/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 17 additions & 24 deletions lib/static/new-ui/components/AssertViewResult/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,38 @@
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 <div style={style} className={styles.diffViewerContainer}>
<RadioButton onUpdate={onChangeHandler} value={diffMode} className={styles.diffModeSwitcher}>
{Object.values(DiffModes).map(diffMode =>
<RadioButton.Option value={diffMode.id} content={diffMode.title} title={diffMode.description} key={diffMode.id}/>
)}
</RadioButton>
<DiffViewer diffMode={diffMode} {...result} />
</div>;
return <DiffViewer diffMode={diffMode} {...result} />;
} else if (result.status === TestStatus.ERROR) {
return <Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />;
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Actual'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
} else if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) {
return <Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />;
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Expected'} subtitle={getImageDisplayedSize(result.expectedImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />
</div>;
}

return null;
}

export const AssertViewResult = connect((state: State) => ({
diffMode: state.view.diffMode
}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(AssertViewResultInternal);
}))(AssertViewResultInternal);
Original file line number Diff line number Diff line change
@@ -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;
}
29 changes: 29 additions & 0 deletions lib/static/new-ui/components/AssertViewStatus/index.tsx
Original file line number Diff line number Diff line change
@@ -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 = <><Icon data={SquareXmark} width={16}/><span>Failed to compare</span></>;

if (image === null) {
status = <><Icon data={SquareExclamation} width={16}/><span>Image is absent</span></>;
} else if (image.status === TestStatus.SUCCESS) {
status = <><Icon data={CircleCheck} width={16}/><span>Images match</span></>;
} else if (isNoRefImageError((image as ImageEntityError).error)) {
status = <><Icon data={FileLetterX} width={16}/><span>Reference not found</span></>;
} else if (image.status === TestStatus.FAIL) {
status = <><Icon data={ArrowRightArrowLeft} width={16}/><span>Difference detected</span></>;
} else if (image.status === TestStatus.UPDATED) {
status = <><Icon data={FileCheck} width={16}/><span>Reference updated</span></>;
}

return <div className={styles.container}>{status}</div>;
}
2 changes: 0 additions & 2 deletions lib/static/new-ui/components/AttemptPicker/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
21 changes: 21 additions & 0 deletions lib/static/new-ui/components/CompactAttemptPicker/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
70 changes: 70 additions & 0 deletions lib/static/new-ui/components/CompactAttemptPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={styles.container}>
<Button view={'outlined'} onClick={onPreviousClick} disabled={currentAttemptIndex === 0}><Icon data={ChevronLeft}/></Button>
<Select renderControl={({onClick, onKeyDown, ref}): React.JSX.Element => {
return <Button className={styles.attemptSelect} onClick={onClick} extraProps={{onKeyDown}} ref={ref} view={'flat'}>
Attempt <span className={styles.attemptNumber}>
{currentAttemptIndex !== null ? currentAttemptIndex + 1 : '–'}
</span> of <span className={styles.attemptNumber}>{totalAttemptsCount ?? '–'}</span>
</Button>;
}}
renderOption={(option): React.JSX.Element => {
const attemptStatus = resultsById[option.data.resultId].status;
return <div className={styles.attemptOption}>
{getIconByStatus(attemptStatus)}
<span>{option.content}</span>
</div>;
}} popupClassName={styles.attemptSelectPopup}
onUpdate={onUpdate}
>
{currentBrowser.resultIds.map((resultId, index) => {
return <Select.Option key={index} value={index.toString()} content={`Attempt #${index + 1}`} data={{resultId}}></Select.Option>;
})}
</Select>
<Button view={'outlined'} onClick={onNextClick} disabled={totalAttemptsCount === null || currentAttemptIndex === totalAttemptsCount - 1}>
<Icon data={ChevronRight}/>
</Button>
</div>;
}
8 changes: 0 additions & 8 deletions lib/static/new-ui/components/DiffViewer/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 5 additions & 12 deletions lib/static/new-ui/components/DiffViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <div className={styles.imageLabel}>
<span>{title}</span>
{subtitle && <span className={styles.imageLabelSubtitle}>{subtitle}</span>}
</div>;
};

const expectedImg = Object.assign({}, props.expectedImg, {
label: getImageLabel('Expected', getImageDisplayedSize(props.expectedImg))
label: <ImageLabel title={'Expected'} subtitle={getImageDisplayedSize(props.expectedImg)} />
});
const actualImg = Object.assign({}, props.actualImg, {
label: getImageLabel('Actual', getImageDisplayedSize(props.actualImg))
label: <ImageLabel title={'Actual'} subtitle={getImageDisplayedSize(props.actualImg)} />
});
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: <ImageLabel title={'Diff'} subtitle={diffSubtitle} />,
diffClusters: props.diffClusters
});

Expand Down
8 changes: 8 additions & 0 deletions lib/static/new-ui/components/ImageLabel/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions lib/static/new-ui/components/ImageLabel/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={styles.imageLabel}>
<span>{title}</span>
{subtitle && <span className={styles.imageLabelSubtitle}>{subtitle}</span>}
</div>;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 503a9a5

Please sign in to comment.