diff --git a/lib/constants/diff-modes.ts b/lib/constants/diff-modes.ts index af9176002..771c88309 100644 --- a/lib/constants/diff-modes.ts +++ b/lib/constants/diff-modes.ts @@ -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; diff --git a/lib/static/components/state/index.jsx b/lib/static/components/state/index.jsx index b14ded48a..379f0519b 100644 --- a/lib/static/components/state/index.jsx +++ b/lib/static/components/state/index.jsx @@ -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 = { @@ -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; @@ -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}%)`; } diff --git a/lib/static/new-ui/components/AssertViewResult/index.module.css b/lib/static/new-ui/components/AssertViewResult/index.module.css new file mode 100644 index 000000000..1fd3b7988 --- /dev/null +++ b/lib/static/new-ui/components/AssertViewResult/index.module.css @@ -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; +} diff --git a/lib/static/new-ui/components/AssertViewResult/index.tsx b/lib/static/new-ui/components/AssertViewResult/index.tsx new file mode 100644 index 000000000..050c81e45 --- /dev/null +++ b/lib/static/new-ui/components/AssertViewResult/index.tsx @@ -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
+ + {Object.values(DiffModes).map(diffMode => + + )} + + +
; + } else if (result.status === TestStatus.ERROR) { + return ; + } else if (result.status === TestStatus.SUCCESS || result.status === TestStatus.UPDATED) { + return ; + } + + return null; +} + +export const AssertViewResult = connect((state: State) => ({ + diffMode: state.view.diffMode +}), (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(AssertViewResultInternal); diff --git a/lib/static/new-ui/components/AttemptPicker/index.tsx b/lib/static/new-ui/components/AttemptPicker/index.tsx index 2225140de..e21d60667 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.tsx +++ b/lib/static/new-ui/components/AttemptPicker/index.tsx @@ -28,7 +28,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { return

Attempts

-
+ {resultIds.map((resultId, index) => { const isActive = resultId === currentResultId; @@ -39,7 +39,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { onClick={(): unknown => onClickHandler(resultId, index)} />; })} -
+
; } diff --git a/lib/static/new-ui/components/AttemptPickerItem/index.module.css b/lib/static/new-ui/components/AttemptPickerItem/index.module.css index 356efdd92..a3ccbe947 100644 --- a/lib/static/new-ui/components/AttemptPickerItem/index.module.css +++ b/lib/static/new-ui/components/AttemptPickerItem/index.module.css @@ -1,6 +1,5 @@ .attempt-picker-item { --g-button-padding: 8px; - margin-right: 2px; } .attempt-picker-item--active { diff --git a/lib/static/new-ui/components/DiffViewer/index.module.css b/lib/static/new-ui/components/DiffViewer/index.module.css new file mode 100644 index 000000000..39472559e --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/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/DiffViewer/index.tsx b/lib/static/new-ui/components/DiffViewer/index.tsx new file mode 100644 index 000000000..ac39f643d --- /dev/null +++ b/lib/static/new-ui/components/DiffViewer/index.tsx @@ -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
+ {title} + {subtitle && {subtitle}} +
; + }; + + 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 ; + + case DiffModes.SWITCH.id: + return ; + + case DiffModes.SWIPE.id: + return ; + + case DiffModes.ONION_SKIN.id: + return ; + + case DiffModes.THREE_UP_SCALED.id: + return ; + + case DiffModes.THREE_UP_SCALED_TO_FIT.id: { + const desiredHeight = props.desiredHeight ?? 'calc(100vh - 180px)'; + + return ; + } + case DiffModes.THREE_UP.id: + default: + return ; + } +} diff --git a/lib/static/new-ui/components/DiffViewer/utils.ts b/lib/static/new-ui/components/DiffViewer/utils.ts index 9c8af1375..744eaaa80 100644 --- a/lib/static/new-ui/components/DiffViewer/utils.ts +++ b/lib/static/new-ui/components/DiffViewer/utils.ts @@ -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); +}; diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css b/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css index 97bbc36bd..ff7fd0109 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css @@ -46,6 +46,6 @@ background-color: white; position: sticky; top: 0; - z-index: 1; + z-index: 10; padding-bottom: 4px; } diff --git a/lib/static/new-ui/features/suites/components/TestSteps/index.module.css b/lib/static/new-ui/features/suites/components/TestSteps/index.module.css index 7ac22d4ef..a30d373a1 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/index.module.css +++ b/lib/static/new-ui/features/suites/components/TestSteps/index.module.css @@ -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; 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 8215794fe..1f4ddb2fb 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestSteps/index.tsx @@ -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'; @@ -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; @@ -55,7 +58,7 @@ 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 { @@ -63,7 +66,7 @@ function TestStepsInternal(props: TestStepsProps): ReactNode { title:
{item.title} - {item.duration} ms + {item.duration !== undefined && {item.duration} ms}
, startSlot: {getIconByStatus(item.status)} }; @@ -79,6 +82,10 @@ function TestStepsInternal(props: TestStepsProps): ReactNode { } else if (item.type === StepType.ErrorInfo) { const indent = items.structure.itemsState[itemId].indentation; return ; + } else if (item.type === StepType.SingleImage) { + return ; + } else if (item.type === StepType.AssertViewResult) { + return ; } return null; diff --git a/lib/static/new-ui/features/suites/components/TestSteps/selectors.ts b/lib/static/new-ui/features/suites/components/TestSteps/selectors.ts index 08e8b4cb3..06c00db7e 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/selectors.ts +++ b/lib/static/new-ui/features/suites/components/TestSteps/selectors.ts @@ -1,15 +1,16 @@ import {createSelector} from 'reselect'; -import {getCurrentResult, getExpandedStepsById} from '@/static/new-ui/store/selectors'; -import {Attachment, ErrorInfo, Step, StepType} from './types'; +import {getCurrentResult, getCurrentResultImages, getExpandedStepsById} from '@/static/new-ui/store/selectors'; +import {AssertViewResult, Attachment, ErrorInfo, Step, StepType} from './types'; import {TestStepCompressed, TestStepKey} from '@/types'; import {unstable_ListTreeItemType as ListTreeItemType} from '@gravity-ui/uikit/unstable'; import {TestStatus} from '@/constants'; import {isAssertViewError, isImageDiffError, mergeSnippetIntoErrorStack} from '@/common-utils'; import {traverseTree} from '@/static/new-ui/features/suites/components/TestSteps/utils'; +import {ImageEntityError} from '@/static/new-ui/types/store'; export const getTestSteps = createSelector( - [getCurrentResult], - (result): ListTreeItemType[] => { + [getCurrentResult, getCurrentResultImages], + (result, images): ListTreeItemType[] => { if (!result || !result.history) { return []; } @@ -50,6 +51,7 @@ export const getTestSteps = createSelector( const steps = formatTestSteps(result.history, result.id); + // Adding error stack if (result.error && !isImageDiffError(result.error) && !isAssertViewError(result.error)) { const lastErroredStep = getLastErroredStep(steps); const error = mergeSnippetIntoErrorStack(result.error); @@ -76,6 +78,104 @@ export const getTestSteps = createSelector( } } + // Integrating images into test steps + for (const image of images) { + if (!image.stateName) { + continue; + } + + let matchedStep: ListTreeItemType | undefined; + + traverseTree(steps, step => { + if (step.data.type === StepType.Action && step.data.title === 'assertView' && step.data.args[0] === image.stateName) { + matchedStep = step; + } + }); + + if (!matchedStep) { + matchedStep = { + id: `${image.id} assertView`, + data: { + type: StepType.Action, + title: 'assertView', + status: image.status, + args: [image.stateName], + isGroup: false + } + }; + steps.push(matchedStep); + } + + if (image.status === TestStatus.ERROR && image.error) { + const errorInfo: ListTreeItemType = { + id: `${image.id} assertView error-info`, + data: { + type: StepType.ErrorInfo, + name: image.error.name, + stack: image.error.stack + } + }; + const errorAttachment = { + id: `${image.id} assertView error`, + data: { + type: StepType.Attachment, + title: 'Error' + } satisfies Attachment, + children: [errorInfo] + }; + + if (!matchedStep.children) { + matchedStep.children = []; + } + + matchedStep.children.push(errorAttachment); + } + + const imageStep: ListTreeItemType = { + id: `${image.id} image`, + data: { + type: StepType.AssertViewResult, + result: image + } + }; + + if (matchedStep.data.type === StepType.Action) { + matchedStep.data.status = image.status; + } + + if (matchedStep.children && matchedStep.children.length > 0) { + matchedStep.children.push({ + id: `${matchedStep.id} attachment`, + data: { + type: StepType.Attachment, + title: 'Screenshots' + }, + children: [imageStep] + }); + } else { + matchedStep.children = [imageStep]; + } + } + + // Adding page screenshot + const pageScreenshot = images.find(image => image.status === TestStatus.ERROR && !image.stateName); + if (pageScreenshot) { + steps.push({ + id: 'page-screenshot', + data: { + type: StepType.Attachment, + title: 'Page Screenshot' + }, + children: [{ + id: 'page-screenshot-image', + data: { + type: StepType.SingleImage, + image: (pageScreenshot as ImageEntityError).actualImg + } + }] + }); + } + // TODO: add type-level check that each step has explicit ID return steps; } diff --git a/lib/static/new-ui/features/suites/components/TestSteps/types.ts b/lib/static/new-ui/features/suites/components/TestSteps/types.ts index 2dc8646c4..3887420ab 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/types.ts +++ b/lib/static/new-ui/features/suites/components/TestSteps/types.ts @@ -1,11 +1,13 @@ import {TestStatus} from '@/constants'; import {ImageFile} from '@/types'; +import {ImageEntity} from '@/static/new-ui/types/store'; export enum StepType { Action, Attachment, ErrorInfo, - SingleImage + SingleImage, + AssertViewResult } export interface Action { @@ -13,7 +15,7 @@ export interface Action { status: TestStatus; title: string; args: string[]; - duration: number; + duration?: number; isGroup: boolean; } @@ -33,4 +35,9 @@ export interface SingleImage { image: ImageFile; } -export type Step = Action | Attachment | ErrorInfo | SingleImage; +export interface AssertViewResult { + type: StepType.AssertViewResult; + result: ImageEntity; +} + +export type Step = Action | Attachment | ErrorInfo | SingleImage | AssertViewResult; diff --git a/lib/static/new-ui/features/suites/components/TestSteps/utils.ts b/lib/static/new-ui/features/suites/components/TestSteps/utils.ts index 00708d802..816a5e1c5 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/utils.ts +++ b/lib/static/new-ui/features/suites/components/TestSteps/utils.ts @@ -1,4 +1,5 @@ -import {unstable_ListTreeItemType as ListTreeItemType} from '@gravity-ui/uikit/unstable'; +import {unstable_ListTreeItemType as ListTreeItemType, unstable_UseListResult as UseListResult} from '@gravity-ui/uikit/unstable'; +import React from 'react'; export const traverseTree = (treeItems: ListTreeItemType[], cb: (item: ListTreeItemType) => unknown): void => { function dfs(step: ListTreeItemType): void { @@ -15,3 +16,7 @@ export const traverseTree = (treeItems: ListTreeItemType[], cb: (item: Lis dfs(step); } }; + +export const getIndentStyle = (list: UseListResult, id: string): React.CSSProperties => { + return {'--indent': list.structure.itemsState[id].indentation} as React.CSSProperties; +}; diff --git a/lib/static/new-ui/store/selectors.ts b/lib/static/new-ui/store/selectors.ts index 093b6f783..74ebbff4c 100644 --- a/lib/static/new-ui/store/selectors.ts +++ b/lib/static/new-ui/store/selectors.ts @@ -42,3 +42,9 @@ export const getExpandedStepsById = (state: State): Record => { return resultId ? state.ui.suitesPage.expandedStepsByResultId[resultId] ?? {} : {}; }; + +export const getCurrentResultImages = (state: State): ImageEntity[] => { + const result = getCurrentResult(state); + + return result?.imageIds.map(imageId => state.tree.images.byId[imageId]) ?? []; +}; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 801d12fa5..57b75bcda 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -1,6 +1,7 @@ import {DiffModeId, TestStatus, ViewMode} from '@/constants'; import {BrowserItem, ImageFile, ReporterConfig, TestError, TestStepCompressed} from '@/types'; import {HtmlReporterValues} from '@/plugin-api'; +import {CoordBounds} from 'looks-same'; export interface SuiteEntityNode { id: string; @@ -56,22 +57,35 @@ interface ImageEntityCommon { parentId: string; } +export interface ImageEntitySuccess extends ImageEntityCommon { + status: TestStatus.SUCCESS; + stateName: string; + expectedImg: ImageFile; +} + +export interface ImageEntityUpdated extends ImageEntityCommon { + status: TestStatus.UPDATED; + stateName: string; + expectedImg: ImageFile; +} + export interface ImageEntityError extends ImageEntityCommon { status: TestStatus.ERROR; - // TODO: can a screenshot in error status even have stateName? stateName?: string; actualImg: ImageFile; + error?: TestError; } export interface ImageEntityFail extends ImageEntityCommon { status: TestStatus.FAIL; stateName: string; + diffClusters: CoordBounds[]; diffImg: ImageFile; actualImg: ImageFile; expectedImg: ImageFile; } -export type ImageEntity = ImageEntityError | ImageEntityFail; +export type ImageEntity = ImageEntityError | ImageEntityFail | ImageEntitySuccess | ImageEntityUpdated; export const isImageEntityFail = (image: ImageEntity): image is ImageEntityFail => Boolean((image as ImageEntityFail).stateName); diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 79871a0b1..96bd298fb 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -1,5 +1,5 @@ import {TestStatus} from '@/constants'; -import {ArrowRotateLeft, CircleCheck, CircleDashed, CircleMinus, CircleXmark} from '@gravity-ui/icons'; +import {ArrowRotateLeft, CircleCheck, CircleDashed, CircleMinus, CircleXmark, ArrowsRotateLeft} from '@gravity-ui/icons'; import React from 'react'; export const getIconByStatus = (status: TestStatus): React.JSX.Element => { @@ -11,6 +11,8 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; } else if (status === TestStatus.RETRY) { return ; + } else if (status === TestStatus.UPDATED) { + return ; } return ; diff --git a/lib/static/styles.css b/lib/static/styles.css index 20136ae36..75a6652c6 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -982,3 +982,7 @@ a:active { .icon-retry { color: var(--g-color-private-orange-600-solid); } + +.icon-updated { + color: var(--color-pink-600); +}