diff --git a/lib/adapters/test-result/testplane.ts b/lib/adapters/test-result/testplane.ts index 457703bec..108a42873 100644 --- a/lib/adapters/test-result/testplane.ts +++ b/lib/adapters/test-result/testplane.ts @@ -10,8 +10,7 @@ import { hasUnrelatedToScreenshotsErrors, isImageDiffError, isInvalidRefImageError, - isNoRefImageError, - wrapLinkByTag + isNoRefImageError } from '../../common-utils'; import { ErrorDetails, @@ -50,7 +49,7 @@ const getSkipComment = (suite: TestplaneTestResult | TestplaneSuite): string | n }; const wrapSkipComment = (skipComment: string | null | undefined): string => { - return skipComment ? wrapLinkByTag(skipComment) : 'Unknown reason'; + return skipComment ?? 'Unknown reason'; }; const getHistory = (history?: TestplaneTestResult['history']): TestStepCompressed[] => { diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 6b96ac1f8..9da559bd3 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -107,12 +107,6 @@ export const getRelativeUrl = (absoluteUrl: string): string => { } }; -export const wrapLinkByTag = (text: string): string => { - return text.replace(/https?:\/\/[^\s]*/g, (url) => { - return `${url}`; - }); -}; - export const mkTestId = (fullTitle: string, browserId: string): string => { return fullTitle + '.' + browserId; }; diff --git a/lib/static/components/section/section-browser.jsx b/lib/static/components/section/section-browser.jsx index 146efb603..31abb96e7 100644 --- a/lib/static/components/section/section-browser.jsx +++ b/lib/static/components/section/section-browser.jsx @@ -2,7 +2,6 @@ import React, {Fragment, useContext, useLayoutEffect} from 'react'; import {last, isEmpty} from 'lodash'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import Parser from 'html-react-parser'; import PropTypes from 'prop-types'; import * as actions from '../../modules/actions'; import BrowserTitle from './title/browser'; @@ -11,6 +10,7 @@ import Body from './body'; import {isSkippedStatus} from '../../../common-utils'; import {sectionStatusResolver} from './utils'; import {MeasurementContext} from '../measurement-context'; +import {makeLinksClickable} from '@/static/new-ui/utils'; function SectionBrowser(props) { const onToggleSection = () => { @@ -24,7 +24,7 @@ function SectionBrowser(props) { [skipped] {props.browser.name} {skipReason && ', reason: '} - {skipReason && Parser(skipReason)} + {skipReason && makeLinksClickable(skipReason)} ); }; diff --git a/lib/static/new-ui/components/AttemptPickerItem/index.module.css b/lib/static/new-ui/components/AttemptPickerItem/index.module.css index df6eb526f..16c83af48 100644 --- a/lib/static/new-ui/components/AttemptPickerItem/index.module.css +++ b/lib/static/new-ui/components/AttemptPickerItem/index.module.css @@ -23,6 +23,13 @@ --g-button-text-color-hover: hsl(208 88% 48% / 1); } +.attempt-picker-item--skipped { + background-color: var(--g-color-private-black-50); + color: var(--g-color-private-black-600); + --box-shadow-color: var(--g-color-private-black-250); + --g-button-text-color-hover: var(--g-color-private-black-700); +} + .attempt-picker-item--success { --box-shadow-color: #21a95661; } diff --git a/lib/static/new-ui/components/MetaInfo/index.tsx b/lib/static/new-ui/components/MetaInfo/index.tsx index 21402b2ec..aa2de7ee7 100644 --- a/lib/static/new-ui/components/MetaInfo/index.tsx +++ b/lib/static/new-ui/components/MetaInfo/index.tsx @@ -1,15 +1,17 @@ import path from 'path'; import {DefinitionList} from '@gravity-ui/components'; -import {mapValues, isObject, omitBy, isEmpty} from 'lodash'; +import {isEmpty, isObject, mapValues, omitBy} from 'lodash'; import React, {ReactNode} from 'react'; import {connect} from 'react-redux'; -import {isUrl, getUrlWithBase, getRelativeUrl} from '@/common-utils'; +import {getRelativeUrl, getUrlWithBase, isUrl} from '@/common-utils'; import {ResultEntity, State} from '@/static/new-ui/types/store'; import {HtmlReporterValues} from '@/plugin-api'; import {ReporterConfig} from '@/types'; import styles from './index.module.css'; +import {makeLinksClickable} from '@/static/new-ui/utils'; +import {TestStatus} from '@/constants'; const serializeMetaValues = (metaInfo: Record): Record => mapValues(metaInfo, (v): string => { @@ -20,9 +22,9 @@ const serializeMetaValues = (metaInfo: Record): Record { label: string; - content: string; + content: T; url?: string; copyText?: string; } @@ -58,12 +60,12 @@ function MetaInfoInternal(props: MetaInfoInternalProps): ReactNode { ...resolveMetaInfoExtenders() }); - const metaInfoItems: MetaInfoItem[] = Object.entries(serializedMetaValues).map(([key, value]) => ({ + const metaInfoItems: MetaInfoItem[] = Object.entries(serializedMetaValues).map(([key, value]) => ({ label: key, content: value })); - const metaInfoItemsWithResolvedUrls = metaInfoItems.map((item) => { + const metaInfoItemsWithResolvedUrls: MetaInfoItem[] = metaInfoItems.map((item) => { if (item.label === 'url' || metaInfoBaseUrls[item.label] === 'auto') { const url = getUrlWithBase(item.content, baseHost); return { @@ -109,6 +111,14 @@ function MetaInfoInternal(props: MetaInfoInternalProps): ReactNode { }); } + if (result.status === TestStatus.SKIPPED && result.skipReason) { + metaInfoItemsWithResolvedUrls.push({ + label: 'skipReason', + content: makeLinksClickable(result.skipReason), + copyText: result.skipReason + }); + } + return { if (item.url) { @@ -117,14 +127,14 @@ function MetaInfoInternal(props: MetaInfoInternalProps): ReactNode { content: {item.content} , - copyText: item.copyText ?? item.content + copyText: item.copyText ?? item.content as string }; } return { name: item.label, content: {item.content}, - copyText: item.copyText ?? item.content + copyText: item.copyText ?? item.content as string }; }) }/>; diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/types.ts b/lib/static/new-ui/features/suites/components/SuitesPage/types.ts index 9b0c96e89..0b1041497 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/types.ts +++ b/lib/static/new-ui/features/suites/components/SuitesPage/types.ts @@ -19,6 +19,7 @@ export interface TreeViewItemData { errorStack?: string; images?: ImageEntity[]; parentData?: TreeViewItemData; + skipReason?: string; } export interface TreeRoot { diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css index f4f2c88dd..ff7c5736d 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css @@ -57,6 +57,8 @@ color: #fff !important; /* Sets spinner color */ --g-color-line-brand: #fff; + --color-link: #fff; + --color-link-hover: rgba(255, 255, 255, 0.6); } .tree-view__item__title--current { diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts index dc5579b1e..d39df0e1a 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts @@ -66,7 +66,8 @@ export const getTreeViewItems = createSelector( images: resultImages, errorTitle, errorStack, - parentData + parentData, + skipReason: lastResult.skipReason }; }; 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 ad9ef8b21..fe0e05ff4 100644 --- a/lib/static/new-ui/features/suites/components/TestSteps/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestSteps/index.tsx @@ -32,6 +32,10 @@ interface TestStepsProps { } function TestStepsInternal(props: TestStepsProps): ReactNode { + if (props.testSteps.length === 0) { + return null; + } + const items = useList({ items: props.testSteps, withExpandedState: true, diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css index 40e586c24..3295d40f4 100644 --- a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css @@ -20,3 +20,21 @@ font-size: 15px; word-break: break-word; } + +.skip-reason-container { + font-size: 15px; + overflow: hidden; +} + +.skip-reason { + overflow: hidden; + text-overflow: ellipsis; +} + +.skip-reason-container a:link, .skip-reason-container a:visited { + color: var(--color-link); +} + +.skip-reason-container a:hover { + color: var(--color-link-hover); +} diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx index ccdced86f..c43c115a2 100644 --- a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx @@ -7,6 +7,7 @@ import {ImageWithMagnifier} from '@/static/new-ui/components/ImageWithMagnifier' import {ImageEntityFail} from '@/static/new-ui/types/store'; import styles from './index.module.css'; import {getAssertViewStatusMessage} from '@/static/new-ui/utils/assert-view-status'; +import {makeLinksClickable} from '@/static/new-ui/utils'; interface TreeViewItemSubtitleProps { item: TreeViewItemData; @@ -16,7 +17,11 @@ interface TreeViewItemSubtitleProps { } export function TreeViewItemSubtitle(props: TreeViewItemSubtitleProps): ReactNode { - if (props.item.images?.length) { + if (props.item.skipReason) { + return
+
Skipped ⋅ {makeLinksClickable(props.item.skipReason)}
+
; + } else if (props.item.images?.length) { return
{props.item.images.map((imageEntity, index) => { const image = (imageEntity as ImageEntityFail).diffImg ?? (imageEntity as ImageEntityFail).actualImg; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 97d217df6..dad16c696 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -70,6 +70,7 @@ export interface ResultEntityCommon { suitePath: string[]; /** @note Browser Name/ID, e.g. `chrome-desktop` */ name: string; + skipReason?: string; } export interface ResultEntityError extends ResultEntityCommon { diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 254571cea..de40fc851 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -8,7 +8,7 @@ import { CloudCheck } from '@gravity-ui/icons'; import {Spin} from '@gravity-ui/uikit'; -import React from 'react'; +import React, {ReactNode} from 'react'; import {TestStatus} from '@/constants'; import {ImageFile} from '@/types'; @@ -34,12 +34,6 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; }; -export const getFullTitleByTitleParts = (titleParts: string[]): string => { - const DELIMITER = ' '; - - return titleParts.join(DELIMITER).trim(); -}; - export const getImageDisplayedSize = (image: ImageFile): string => `${image.size.width}×${image.size.height}`; export const stringify = (value: unknown): string => { @@ -61,3 +55,35 @@ export const stringify = (value: unknown): string => { return toString(value); }; + +export const makeLinksClickable = (text: string): React.JSX.Element => { + const urlRegex = /https?:\/\/[^\s]*/g; + + const parts = text.split(urlRegex); + const urls = text.match(urlRegex) || []; + + return <>{ + parts.reduce((elements, part, index) => { + elements.push(part); + + if (urls[index]) { + const href = urls[index].startsWith('www.') + ? `http://${urls[index]}` + : urls[index]; + + elements.push( + + {urls[index]} + + ); + } + + return elements; + }, [] as ReactNode[]) + }; +}; diff --git a/test/unit/lib/static/components/section/section-browser.jsx b/test/unit/lib/static/components/section/section-browser.jsx index 0d3a16959..3cb95f101 100644 --- a/test/unit/lib/static/components/section/section-browser.jsx +++ b/test/unit/lib/static/components/section/section-browser.jsx @@ -1,5 +1,6 @@ +import {expect} from 'chai'; import React from 'react'; -import {defaults, set} from 'lodash'; +import {defaults} from 'lodash'; import proxyquire from 'proxyquire'; import {SUCCESS, SKIPPED, ERROR} from 'lib/constants/test-statuses'; import {UNCHECKED} from 'lib/constants/checked-statuses'; @@ -30,7 +31,6 @@ describe('', () => { SectionBrowser = proxyquire('lib/static/components/section/section-browser', { '../../modules/actions': actionsStub, - './title/browser-skipped': {default: BrowserSkippedTitle}, './title/browser': {default: BrowserTitle}, './body': {default: Body} }).default; @@ -45,12 +45,9 @@ describe('', () => { const resultsById = mkResult({id: 'res', status: SKIPPED}); const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + const section = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.calledWithMatch( - BrowserSkippedTitle, - set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', undefined, undefined]) - ); + expect(section.getByText(/.*?\[skipped\].*?/)).to.exist; }); it('should pass skip reason', () => { @@ -59,12 +56,9 @@ describe('', () => { const resultsById = mkResult({id: 'res', status: SKIPPED, skipReason: 'some-reason'}); const tree = mkStateTree({browsersById, browsersStateById, resultsById}); - mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + const section = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.calledWithMatch( - BrowserSkippedTitle, - set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', ', reason: ', 'some-reason']) - ); + expect(section.getByText(/.*?reason:\s*some-reason.*?/)).to.exist; }); it('should not render body even if browser in opened state', () => { @@ -81,6 +75,13 @@ describe('', () => { describe('executed test with fails in retries and skip in result', () => { it('should render not skipped title', () => { + SectionBrowser = proxyquire('lib/static/components/section/section-browser', { + '../../modules/actions': actionsStub, + './title/browser-skipped': {default: BrowserSkippedTitle}, + './title/browser': {default: BrowserTitle}, + './body': {default: Body} + }).default; + const browsersById = mkBrowser( {id: 'yabro-1', name: 'yabro', resultIds: ['res-1', 'res-2'], parentId: 'test'} ); @@ -93,10 +94,28 @@ describe('', () => { mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); - assert.calledWithMatch( - BrowserTitle, - set({}, 'title.props.children', [`[${SKIPPED}] `, 'yabro', ', reason: ', 'some-reason']) + assert.notCalled(BrowserSkippedTitle); + }); + + it('should render title with skip reason', () => { + SectionBrowser = proxyquire('lib/static/components/section/section-browser', { + '../../modules/actions': actionsStub, + './body': {default: Body} + }).default; + + const browsersById = mkBrowser( + {id: 'yabro-1', name: 'yabro', resultIds: ['res-1', 'res-2'], parentId: 'test'} ); + const browsersStateById = {'yabro-1': {shouldBeShown: true, shouldBeOpened: true}}; + const resultsById = { + ...mkResult({id: 'res-1', status: ERROR, error: {}}), + ...mkResult({id: 'res-2', status: SKIPPED, skipReason: 'some-reason'}) + }; + const tree = mkStateTree({browsersById, browsersStateById, resultsById}); + + const section = mkSectionBrowserComponent({browserId: 'yabro-1'}, {tree}); + + expect(section.getByText(/.*?reason:\s*some-reason.*?/)).to.exist; }); it('should render body if browser in opened state', () => {