Skip to content

Commit

Permalink
chore(new-ui): implement screenshots display (#601)
Browse files Browse the repository at this point in the history
* chore(new-ui): implement screenshots display

* chore(new-ui): fix review issues
  • Loading branch information
shadowusr authored Sep 17, 2024
1 parent daf13ed commit 2a58893
Show file tree
Hide file tree
Showing 19 changed files with 345 additions and 42 deletions.
22 changes: 11 additions & 11 deletions lib/constants/diff-modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,38 @@ import {ValueOf} from 'type-fest';
export const DiffModes = {
THREE_UP: {
id: '3-up',
title: '3-up',
description: 'images in column'
title: 'List',
description: 'List. Show images one after another in vertical layout.'
},
THREE_UP_SCALED: {
id: '3-up-scaled',
title: '3-up scaled',
description: 'scaled images in row'
title: 'SbS',
description: 'Side by Side. Show images in one row.'
},
THREE_UP_SCALED_TO_FIT: {
id: '3-up-scaled-to-fit',
title: '3-up scaled to fit',
description: 'scaled to browser height images in row'
title: 'SbS (fit screen)',
description: 'Side by Side. Show images in one row and scale them down if needed to fit the screen.'
},
ONLY_DIFF: {
id: 'only-diff',
title: 'Only diff',
description: 'click on image to see area with diff'
title: 'Only Diff',
description: 'Only Diff. Show only diff image, click to highlight diff areas.'
},
SWITCH: {
id: 'switch',
title: 'Switch',
description: 'click on image to switch'
description: 'Switch. Click to switch between expected and actual images.'
},
SWIPE: {
id: 'swipe',
title: 'Swipe',
description: 'move divider'
description: 'Swipe. Move the divider to compare expected and actual images.'
},
ONION_SKIN: {
id: 'onion-skin',
title: 'Onion skin',
description: 'move slider'
description: 'Onion Skin. Change the image opacity to compare expected and actual images.'
}
} as const;

Expand Down
15 changes: 2 additions & 13 deletions lib/static/components/state/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {isAcceptable, isNodeStaged, isNodeSuccessful, isScreenRevertable} from '
import {isSuccessStatus, isFailStatus, isErrorStatus, isUpdatedStatus, isIdleStatus, isStagedStatus, isCommitedStatus} from '../../../common-utils';
import {Disclosure} from '@gravity-ui/uikit';
import {ChevronsExpandUpRight, Check, ArrowUturnCcwDown} from '@gravity-ui/icons';
import {getDisplayedDiffPercentValue} from '@/static/new-ui/components/DiffViewer/utils';

class State extends Component {
static propTypes = {
Expand Down Expand Up @@ -155,18 +156,6 @@ class State extends Component {
);
}

_getDisplayedDiffPercentValue() {
const percent = this.props.image.diffRatio * 100;
const percentRounded = Math.ceil(percent * 100) / 100;
const percentThreshold = 0.01;

if (percent < percentThreshold) {
return `< ${percentThreshold}`;
}

return String(percentRounded);
}

_getStateTitleWithDiffCount() {
const {image} = this.props;

Expand All @@ -182,7 +171,7 @@ class State extends Component {
let displayedText = image.stateName;

if (image.differentPixels && image.diffRatio) {
const displayedDiffPercent = this._getDisplayedDiffPercentValue();
const displayedDiffPercent = getDisplayedDiffPercentValue(image.diffRatio);

displayedText += ` (diff: ${image.differentPixels}px, ${displayedDiffPercent}%)`;
}
Expand Down
17 changes: 17 additions & 0 deletions lib/static/new-ui/components/AssertViewResult/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.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;
}
45 changes: 45 additions & 0 deletions lib/static/new-ui/components/AssertViewResult/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, {ReactNode} from 'react';
import {ImageEntity, State} from '@/static/new-ui/types/store';
import {DiffModeId, DiffModes, 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';

interface AssertViewResultProps {
result: ImageEntity;
style?: React.CSSProperties;
actions: typeof actions;
diffMode: DiffModeId;
}

function AssertViewResultInternal({result, actions, 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>;
} else if (result.status === TestStatus.ERROR) {
return <Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />;
} else if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) {
return <Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />;
}

return null;
}

export const AssertViewResult = connect((state: State) => ({
diffMode: state.view.diffMode
}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(AssertViewResultInternal);
4 changes: 2 additions & 2 deletions lib/static/new-ui/components/AttemptPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode {

return <Flex alignItems={'center'} gap={5}>
<h3 className='text-header-1'>Attempts</h3>
<div>
<Flex gap={0.5} wrap={'wrap'}>
{resultIds.map((resultId, index) => {
const isActive = resultId === currentResultId;

Expand All @@ -39,7 +39,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode {
onClick={(): unknown => onClickHandler(resultId, index)}
/>;
})}
</div>
</Flex>
</Flex>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.attempt-picker-item {
--g-button-padding: 8px;
margin-right: 2px;
}

.attempt-picker-item--active {
Expand Down
8 changes: 8 additions & 0 deletions lib/static/new-ui/components/DiffViewer/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;
}
82 changes: 82 additions & 0 deletions lib/static/new-ui/components/DiffViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {ImageFile} from '@/types';
import {CoordBounds} from 'looks-same';
import {DiffModeId, DiffModes} from '@/constants';
import React, {ReactNode} from 'react';
import {OnlyDiffMode} from '@/static/new-ui/components/DiffViewer/OnlyDiffMode';
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';
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 {getDisplayedDiffPercentValue} from '@/static/new-ui/components/DiffViewer/utils';

import styles from './index.module.css';

interface DiffViewerProps {
actualImg: ImageFile;
expectedImg: ImageFile;
diffImg: ImageFile;
diffClusters: CoordBounds[];
diffMode: DiffModeId;
/** For cosmetics, will be displayed in diff label. */
differentPixels?: number;
/** For cosmetics, will be displayed in diff label. */
diffRatio?: number;
/**
* A valid CSS value assignable to height, e.g. `10px` or `calc(100vh - 50px)`.
* Images will try to fit the `desiredHeight`, but will only shrink no more than 2 times.
* */
desiredHeight?: string;
}

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))
});
const actualImg = Object.assign({}, props.actualImg, {
label: getImageLabel('Actual', 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),
diffClusters: props.diffClusters
});

switch (props.diffMode) {
case DiffModes.ONLY_DIFF.id:
return <OnlyDiffMode diff={diffImg} />;

case DiffModes.SWITCH.id:
return <SwitchMode expected={expectedImg} actual={actualImg} />;

case DiffModes.SWIPE.id:
return <SwipeMode expected={expectedImg} actual={actualImg} />;

case DiffModes.ONION_SKIN.id:
return <OnionSkinMode expected={expectedImg} actual={actualImg} />;

case DiffModes.THREE_UP_SCALED.id:
return <SideBySideMode expected={expectedImg} actual={actualImg} diff={diffImg} />;

case DiffModes.THREE_UP_SCALED_TO_FIT.id: {
const desiredHeight = props.desiredHeight ?? 'calc(100vh - 180px)';

return <SideBySideToFitMode desiredHeight={desiredHeight} expected={expectedImg} actual={actualImg} diff={diffImg} />;
}
case DiffModes.THREE_UP.id:
default:
return <ListMode expected={expectedImg} actual={actualImg} diff={diffImg} />;
}
}
12 changes: 12 additions & 0 deletions lib/static/new-ui/components/DiffViewer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ export const getImageSizeCssVars = (size: ImageSize): React.CSSProperties => ({
'--natural-width': size.width,
'--natural-height': size.height
} as React.CSSProperties);

export const getDisplayedDiffPercentValue = (diffRatio: number): string => {
const percent = diffRatio * 100;
const percentRounded = Math.ceil(percent * 100) / 100;
const percentThreshold = 0.01;

if (percent < percentThreshold) {
return `< ${percentThreshold}`;
}

return String(percentRounded);
};
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@
background-color: white;
position: sticky;
top: 0;
z-index: 1;
z-index: 10;
padding-bottom: 4px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
margin: 8px 0;
}

.page-screenshot {
margin: 8px 0;
padding-left: 24px;
padding-right: 1px;
}

.step-duration {
margin-left: auto;
padding: 0 8px;
Expand Down
13 changes: 10 additions & 3 deletions lib/static/new-ui/features/suites/components/TestSteps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {bindActionCreators} from 'redux';
import {CollapsibleSection} from '@/static/new-ui/features/suites/components/CollapsibleSection';
import {State} from '@/static/new-ui/types/store';
import {TreeViewItemIcon} from '@/static/new-ui/components/TreeViewItemIcon';
import {TestStatus} from '@/constants';
import {TestStepArgs} from '@/static/new-ui/features/suites/components/TestStepArgs';
import {getIconByStatus} from '@/static/new-ui/utils';
import {ErrorInfo} from '@/static/new-ui/components/ErrorInfo';
Expand All @@ -21,6 +20,10 @@ import {getStepsExpandedById, getTestSteps} from './selectors';
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';

interface TestStepsProps {
resultId: string;
Expand Down Expand Up @@ -55,15 +58,15 @@ function TestStepsInternal(props: TestStepsProps): ReactNode {
const item = items.structure.itemsById[itemId];

if (item.type === StepType.Action) {
const shouldHighlightFail = item.status === TestStatus.ERROR && !item.isGroup;
const shouldHighlightFail = (isErrorStatus(item.status) || isFailStatus(item.status)) && !item.isGroup;

return <TreeViewItem id={itemId} key={itemId} list={items} isFailed={shouldHighlightFail} onItemClick={onItemClick}
mapItemDataToContentProps={(): ListItemViewContentType => {
return {
title: <div className={styles.stepContent}>
<span className={styles.stepTitle}>{item.title}</span>
<TestStepArgs args={item.args} isFailed={shouldHighlightFail}/>
<span className={styles.stepDuration}>{item.duration} ms</span>
{item.duration !== undefined && <span className={styles.stepDuration}>{item.duration} ms</span>}
</div>,
startSlot: <TreeViewItemIcon>{getIconByStatus(item.status)}</TreeViewItemIcon>
};
Expand All @@ -79,6 +82,10 @@ function TestStepsInternal(props: TestStepsProps): ReactNode {
} else if (item.type === StepType.ErrorInfo) {
const indent = items.structure.itemsState[itemId].indentation;
return <ErrorInfo className={styles.errorInfo} key={itemId} {...item} style={{marginLeft: `${indent * 24}px`}}/>;
} else if (item.type === StepType.SingleImage) {
return <Screenshot containerClassName={styles.pageScreenshot} image={item.image} key={itemId} />;
} else if (item.type === StepType.AssertViewResult) {
return <AssertViewResult result={item.result} key={itemId} style={getIndentStyle(items, itemId)} />;
}

return null;
Expand Down
Loading

0 comments on commit 2a58893

Please sign in to comment.