Skip to content

Commit

Permalink
feat(new-ui): add ability to accept and undo screenshots
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowusr committed Oct 2, 2024
1 parent af724f9 commit 2478972
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
.screenshot {
margin: 8px 0;
padding-left: calc(var(--indent) * 24px);
padding-right: 1px;
}
3 changes: 2 additions & 1 deletion lib/static/new-ui/components/AssertViewResult/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, {ReactNode} from 'react';
import {connect} from 'react-redux';

import {ImageEntity, State} from '@/static/new-ui/types/store';
import {DiffModeId, TestStatus} from '@/constants';
import {DiffViewer} from '../DiffViewer';
import {connect} from 'react-redux';
import styles from './index.module.css';
import {Screenshot} from '@/static/new-ui/components/Screenshot';

Expand Down
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 {ArrowsRotateLeft, CircleCheck, CircleDashed, CircleExclamation} 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={CircleDashed} width={16}/><span>Failed to compare</span></>;

if (image === null) {
status = <><Icon data={CircleDashed} 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={CircleExclamation} width={16}/><span>Reference not found</span></>;
} else if (image.status === TestStatus.FAIL) {
status = <><Icon data={CircleExclamation} width={16}/><span>Difference detected</span></>;
} else if (image.status === TestStatus.UPDATED) {
status = <><Icon data={ArrowsRotateLeft} width={16}/><span>Reference updated</span></>;
}

return <div className={styles.container}>{status}</div>;
}
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;
}
68 changes: 68 additions & 0 deletions lib/static/new-ui/components/CompactAttemptPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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}><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}><Icon data={ChevronRight}/></Button>
</div>;
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
.container {
display: flex;
flex-direction: column;
padding-left: calc(var(--indent) * 24px);
padding-right: 1px
padding: 8px 1px 4px calc(var(--indent) * 24px);
row-gap: 8px;
}

.toolbar-container {
display: flex;
justify-content: space-between;
padding: 12px 0;
align-items: center;
gap: 8px;
}

.toolbar-container:has(> 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: size;
container-type: inline-size;
flex-grow: 1;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {Check, ArrowUturnCcwLeft} from '@gravity-ui/icons';
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, State} from '@/static/new-ui/types/store';
import {DiffModeId, DiffModes, EditScreensFeature} from '@/constants';
import {ImageEntity, ImageEntityError, State} from '@/static/new-ui/types/store';
import {DiffModeId, DiffModes, EditScreensFeature, TestStatus} from '@/constants';
import {acceptTest, changeDiffMode, undoAcceptImage} from '@/static/modules/actions';
import styles from './index.module.css';
import {isAcceptable, isScreenRevertable} from '@/static/modules/utils';
import {getCurrentBrowser, getCurrentResult} from '@/static/new-ui/features/suites/selectors';
import {isNoRefImageError} from '@/common-utils';
import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus';
import styles from './index.module.css';

interface ScreenshotsTreeViewItemProps {
result: ImageEntity;
image: ImageEntity;
style?: React.CSSProperties;
}

Expand All @@ -27,40 +29,49 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re
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.result, isLastResult, isStaticImageAccepterEnabled: false});
const isUndoAvailable = isScreenRevertable({gui: isGui, image: props.image, isLastResult, isStaticImageAccepterEnabled: false});

const onChangeHandler = (diffMode: DiffModeId): void => {
const onDiffModeChangeHandler = (diffMode: DiffModeId): void => {
dispatch(changeDiffMode(diffMode));
};

const onScreenshotAccept = (): void => {
dispatch(acceptTest(props.result.id));
dispatch(acceptTest(props.image.id));
};
const onScreenshotUndo = (): void => {
dispatch(undoAcceptImage(props.result.id));
dispatch(undoAcceptImage(props.image.id));
};

return <div style={props.style} className={styles.container}>
<div className={styles.toolbarContainer}>
<div className={styles.diffModeContainer}>
<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>
<Select className={styles.diffModeSelect} label='Diff Mode' value={[diffMode]} onUpdate={([diffMode]): void => onChangeHandler(diffMode as DiffModeId)} multiple={false}>
{Object.values(DiffModes).map(diffMode =>
<Select.Option value={diffMode.id} content={diffMode.title} title={diffMode.description} key={diffMode.id}/>
)}
</Select>
</div>
{isEditScreensAvailable && <>
{isUndoAvailable && <Button view={'action'} className={styles.acceptButton} disabled={isRunning} onClick={onScreenshotUndo}><Icon
data={ArrowUturnCcwLeft}/>Undo</Button>}
{isAcceptable(props.result) && <Button view={'action'} className={styles.acceptButton} disabled={isRunning} onClick={onScreenshotAccept}><Icon
data={Check}/>Accept</Button>}
</>}
</div>
<AssertViewResult result={props.result} />
{props.image.status !== TestStatus.UPDATED && props.image.status !== TestStatus.SUCCESS && <div className={styles.toolbarContainer}>
{isNoRefImageError((props.image as ImageEntityError).error) ?
<AssertViewStatus image={props.image}/> :
<div className={styles.diffModeContainer}>
<RadioButton onUpdate={onDiffModeChangeHandler} 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>
<Select
className={styles.diffModeSelect}
label="Diff Mode" value={[diffMode]}
onUpdate={([diffMode]): void => onDiffModeChangeHandler(diffMode as DiffModeId)}
multiple={false}
>
{Object.values(DiffModes).map(diffMode =>
<Select.Option value={diffMode.id} content={diffMode.title} title={diffMode.description} key={diffMode.id}/>
)}
</Select>
</div>}
{isEditScreensAvailable && <div className={styles.buttonsContainer}>
{isUndoAvailable && <Button view={'action'} className={styles.acceptButton} disabled={isRunning} onClick={onScreenshotUndo}>
<Icon data={ArrowUturnCcwLeft}/>Undo
</Button>}
{isAcceptable(props.image) && <Button view={'action'} className={styles.acceptButton} disabled={isRunning} onClick={onScreenshotAccept}>
<Icon data={Check}/>Accept
</Button>}
</div>}
</div>}
<AssertViewResult result={props.image} />
</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function TestStepsInternal(props: TestStepsProps): ReactNode {
} else if (item.type === StepType.SingleImage) {
return <Screenshot containerClassName={styles.pageScreenshot} image={item.image} key={itemId} />;
} else if (item.type === StepType.AssertViewResult) {
return <ScreenshotsTreeViewItem key={itemId} result={item.result} style={getIndentStyle(items, itemId)} />;
return <ScreenshotsTreeViewItem key={itemId} image={item.result} style={getIndentStyle(items, itemId)} />;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,28 @@
margin-bottom: 8px !important;
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;
}
Loading

0 comments on commit 2478972

Please sign in to comment.