Skip to content

Commit 3a84264

Browse files
authored
feat(new-ui): add test badges
1 parent 8401fe8 commit 3a84264

File tree

13 files changed

+231
-11
lines changed

13 files changed

+231
-11
lines changed

lib/config/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import chalk from 'chalk';
55
import {logger} from '../common-utils';
66
import {configDefaults, DiffModeId, DiffModes, SaveFormat, ViewMode} from '../constants';
77
import {assertCustomGui} from './custom-gui-asserts';
8-
import {ErrorPattern, PluginDescription, ReporterConfig, ReporterOptions} from '../types';
8+
import {GenerateBadges, ErrorPattern, PluginDescription, ReporterConfig, ReporterOptions} from '../types';
99
import {UiMode} from '../constants/local-storage';
1010

1111
const ENV_PREFIX = 'html_reporter_';
@@ -28,6 +28,7 @@ const assertType = <T>(name: string, validationFn: (value: unknown) => value is
2828
};
2929
};
3030
const assertString = (name: string): AssertionFn<string> => assertType(name, _.isString, 'string');
31+
const assertFunction = (name: string): AssertionFn<string> => assertType(name, _.isFunction, 'function');
3132
const assertBoolean = (name: string): AssertionFn<boolean> => assertType(name, _.isBoolean, 'boolean');
3233
export const assertNumber = (name: string): AssertionFn<number> => assertType(name, _.isNumber, 'number');
3334
const assertPlainObject = (name: string): AssertionFn<Record<string, unknown>> => assertType(name, isPlainObject, 'plain object');
@@ -287,6 +288,10 @@ const getParser = (): ReturnType<typeof root<ReporterConfig>> => {
287288
parseCli: JSON.parse,
288289
validate: assertPlainObject('staticImageAccepter.axiosRequestOptions')
289290
})
291+
}),
292+
generateBadges: option<GenerateBadges | null>({
293+
defaultValue: configDefaults.generateBadges,
294+
validate: (value) => _.isNull(value) || assertFunction('generateBadges')
290295
})
291296
}), {envPrefix: ENV_PREFIX, cliPrefix: CLI_PREFIX});
292297
};

lib/constants/defaults.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ export const configDefaults: StoreReporterConfig = {
3232
serviceUrl: '',
3333
meta: {},
3434
axiosRequestOptions: {}
35-
}
35+
},
36+
generateBadges: null
3637
};

lib/report-builder/static.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import type {DbTestResult, SqliteClient} from '../sqlite-client';
1414
import {ReporterTestResult} from '../adapters/test-result';
1515
import {saveErrorDetails, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils';
16-
import {ReporterConfig} from '../types';
16+
import {AttachmentType, Badge, ReporterConfig} from '../types';
1717
import {HtmlReporter} from '../plugin-api';
1818
import {getTestFromDb} from '../db-utils/server';
1919
import {TestAttemptManager} from '../test-attempt-manager';
@@ -129,6 +129,13 @@ export class StaticReportBuilder {
129129
const isPreviouslySkippedTest = isImgSkipped && getTestFromDb<DbTestResult>(this._dbClient, formattedResult);
130130

131131
if (!ignoredStatuses.includes(testResultWithImagePaths.status) && !isPreviouslySkippedTest) {
132+
if (typeof this._reporterConfig.generateBadges === 'function' && testResultWithImagePaths.attachments) {
133+
testResultWithImagePaths.attachments.push({
134+
type: AttachmentType.Badges,
135+
list: this._reporterConfig.generateBadges(testResultWithImagePaths)?.filter((badge) => badge && badge.title) as Badge[]
136+
});
137+
}
138+
132139
this._dbClient.write(testResultWithImagePaths);
133140
}
134141

lib/static/components/modals/screenshot-accepter/header.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {Component, Fragment} from 'react';
22
import {GlobalHotKeys} from 'react-hotkeys';
33
import PropTypes from 'prop-types';
4-
import {uniqBy, pick} from 'lodash';
4+
import {uniqBy} from 'lodash';
55

66
import ProgressBar from '../../progress-bar';
77
import ControlButton from '../../controls/control-button';

lib/static/new-ui/features/suites/components/SnapshotsPlayer/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {ReactNode, useCallback, useEffect, useRef, useState} from 'react'
77
import {useDispatch, useSelector} from 'react-redux';
88

99
import {TestStatus} from '@/constants';
10-
import {AttachmentType, ImageSize} from '@/types';
10+
import {AttachmentType, ImageSize, SnapshotAttachment} from '@/types';
1111
import {getCurrentResult} from '@/static/new-ui/features/suites/selectors';
1212
import {Timeline} from './Timeline';
1313
import {NumberedSnapshot} from './types';
@@ -327,7 +327,8 @@ export function SnapshotsPlayer(): ReactNode {
327327

328328
playerRef.current.on('custom-event', handleCustomEvent);
329329
} else {
330-
const snapshot = currentResult?.attachments?.find(attachment => attachment.type === AttachmentType.Snapshot);
330+
const snapshot = currentResult?.attachments?.find(attachment => attachment.type === AttachmentType.Snapshot) as SnapshotAttachment;
331+
331332
if (!snapshot) {
332333
return;
333334
}

lib/static/new-ui/features/suites/components/SuitesPage/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {getIconByStatus} from '@/static/new-ui/utils';
3131
import {Page} from '@/static/new-ui/types/store';
3232
import {usePage} from '@/static/new-ui/hooks/usePage';
3333
import {changeTestRetry, setCurrentTreeNode, setStrictMatchFilter} from '@/static/modules/actions';
34+
import {TestStatusBar} from '@/static/new-ui/features/suites/components/TestStatusBar';
3435

3536
export function SuitesPage(): ReactNode {
3637
const page = usePage();
@@ -224,10 +225,11 @@ export function SuitesPage(): ReactNode {
224225
index={currentIndex}
225226
totalItems={visibleTreeNodeIds.length}
226227
onNext={(): void => onPrevNextSuiteHandler(1)}
227-
onPrevious={(): void => onPrevNextSuiteHandler(-1)}/>
228+
onPrevious={(): void => onPrevNextSuiteHandler(-1)}
229+
/>
228230
<TestControlPanel onAttemptChange={onAttemptChangeHandler}/>
229231
</div>
230-
232+
<TestStatusBar />
231233
<TestInfo/>
232234
</>}
233235
{!params.suiteId && !currentResult && <div className={styles.hintContainer}><span className={styles.hint}>Select a test to see details</span></div>}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.test-status-bar {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
justify-content: start;
6+
gap: 20px;
7+
margin-top: 6px;
8+
}
9+
10+
.test-status-bar__main {
11+
display: flex;
12+
flex-direction: row;
13+
align-items: center;
14+
justify-content: start;
15+
gap: 5px;
16+
margin: 2px 0;
17+
}
18+
19+
.test-status-bar__status {
20+
font-size: 15px;
21+
}
22+
23+
.test-status-bar__duration {
24+
font-size: 15px;
25+
color: #7F7F7F;
26+
}
27+
28+
.test-status-bar__badges {
29+
display: flex;
30+
gap: 10px;
31+
32+
button {
33+
--g-button-border-radius: 8px;
34+
color: var(--color-neutral-700);
35+
padding: 0 10px;
36+
gap: 8px;
37+
}
38+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, {ReactNode} from 'react';
2+
import _ from 'lodash';
3+
import * as icons from '@gravity-ui/icons';
4+
import {Button, Icon} from '@gravity-ui/uikit';
5+
6+
import {useSelector} from 'react-redux';
7+
import {ResultEntityCommon} from '@/static/new-ui/types/store';
8+
import {getCurrentResult} from '@/static/new-ui/features/suites/selectors';
9+
import {getIconByStatus} from '@/static/new-ui/utils';
10+
import {Badge, AttachmentType, BadgesAttachment} from '@/types';
11+
12+
import styles from './index.module.css';
13+
import {IconData} from '@gravity-ui/uikit';
14+
15+
const allIcons = icons as unknown as Record<string, IconData>;
16+
17+
const getSuiteDuration = (suite: ResultEntityCommon): string | undefined => {
18+
if (suite.duration !== undefined) {
19+
return `in ${(suite.duration / 1000).toFixed(1)}s`;
20+
}
21+
22+
return;
23+
};
24+
25+
export const TestStatusBar = (): ReactNode => {
26+
const suite = useSelector(getCurrentResult);
27+
28+
if (!suite) {
29+
return null;
30+
}
31+
32+
const badges = suite.attachments?.find(({type}) => type === AttachmentType.Badges) as BadgesAttachment;
33+
34+
return (
35+
<div className={styles['test-status-bar']}>
36+
<div className={styles['test-status-bar__main']}>
37+
{getIconByStatus(suite.status)}
38+
39+
<div className={styles['test-status-bar__status']}>
40+
{_.startCase(suite.status)}
41+
</div>
42+
<div className={styles['test-status-bar__duration']}>
43+
{getSuiteDuration(suite)}
44+
</div>
45+
</div>
46+
{(badges && badges.list.length > 0) && (
47+
<div className={styles['test-status-bar__badges']} data-qa="suite-badges">
48+
{badges.list.map((badge: Badge) => (
49+
<a
50+
key={badge.title}
51+
href={badge.url}
52+
target="_blank"
53+
rel="noreferrer"
54+
>
55+
<Button
56+
size="xs"
57+
>
58+
{badge.icon && allIcons[badge.icon] && <Icon data={allIcons[badge.icon]} size={14} />}
59+
{badge.title}
60+
</Button>
61+
</a>
62+
))}
63+
</div>
64+
)}
65+
</div>
66+
);
67+
};

lib/types.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {BrowserFeature, DiffModeId, SaveFormat, SUITES_TABLE_COLUMNS, TestStatus
55
import type {HtmlReporter} from './plugin-api';
66
import {ImageDiffError, NoRefImageError} from './errors';
77
import {UiMode} from './constants/local-storage';
8+
import {ReporterTestResult} from './adapters/test-result';
89

910
declare module 'tmp' {
1011
export const tmpdir: string;
@@ -233,6 +234,14 @@ export interface StaticImageAccepterRequest extends Pick<StaticImageAccepterConf
233234
}>;
234235
}
235236

237+
export interface Badge {
238+
title?: string;
239+
url?: string;
240+
icon?: string;
241+
}
242+
243+
export type GenerateBadges = (suite: ReporterTestResult) => Array<Badge | null>;
244+
236245
export interface ReporterConfig {
237246
baseHost: string;
238247
commandsWithShortHistory: string[];
@@ -252,6 +261,7 @@ export interface ReporterConfig {
252261
yandexMetrika: { enabled?: boolean; counterNumber: null | number };
253262
staticImageAccepter: StaticImageAccepterConfig;
254263
uiMode: UiMode | null;
264+
generateBadges: GenerateBadges | null;
255265
}
256266

257267
export type ReporterOptions = Omit<ReporterConfig, 'errorPatterns'> & {errorPatterns: (string | ErrorPattern)[]};
@@ -314,7 +324,8 @@ export interface TestStepCompressed {
314324
}
315325

316326
export enum AttachmentType {
317-
Snapshot
327+
Snapshot = 0,
328+
Badges = 1
318329
}
319330

320331
export interface SnapshotAttachment {
@@ -324,7 +335,12 @@ export interface SnapshotAttachment {
324335
maxHeight: number;
325336
}
326337

327-
export type Attachment = SnapshotAttachment;
338+
export interface BadgesAttachment {
339+
type: AttachmentType.Badges;
340+
list: Badge[];
341+
}
342+
343+
export type Attachment = SnapshotAttachment | BadgesAttachment;
328344

329345
export interface ApiErrorResponse {
330346
error: {

test/func/fixtures/fixtures.testplane.conf.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,21 @@ module.exports.getFixturesConfig = (projectDir, projectName) => {
2929
},
3030
'html-reporter-tester': {
3131
enabled: true,
32-
path: fixturesPath
32+
path: fixturesPath,
33+
generateBadges: () => [
34+
{
35+
title: 'TASK-128',
36+
icon: 'LogoYandexTracker'
37+
},
38+
{
39+
title: 'master',
40+
icon: 'BranchesRight'
41+
},
42+
null,
43+
{
44+
icon: 'BranchesRight'
45+
}
46+
]
3347
}
3448
}
3549
});

0 commit comments

Comments
 (0)