diff --git a/lib/constants/features.ts b/lib/constants/features.ts new file mode 100644 index 000000000..0fa9ddc4f --- /dev/null +++ b/lib/constants/features.ts @@ -0,0 +1,7 @@ +export interface Feature { + name: string; +} + +export const RunTestsFeature = { + name: 'run-tests' +} as const satisfies Feature; diff --git a/lib/constants/index.ts b/lib/constants/index.ts index da621fc00..68992a17e 100644 --- a/lib/constants/index.ts +++ b/lib/constants/index.ts @@ -3,6 +3,7 @@ export * from './database'; export * from './defaults'; export * from './diff-modes'; export * from './errors'; +export * from './features'; export * from './group-tests'; export * from './paths'; export * from './tests'; diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index 9ca2b983e..861b8b847 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -94,6 +94,7 @@ export default Object.assign({config: configDefaults}, { }, app: { isInitialized: false, + availableFeatures: [], suitesPage: { currentBrowserId: null }, diff --git a/lib/static/modules/reducers/gui.js b/lib/static/modules/reducers/gui.js index e3fd36747..28ddca750 100644 --- a/lib/static/modules/reducers/gui.js +++ b/lib/static/modules/reducers/gui.js @@ -1,9 +1,11 @@ import actionNames from '../action-names'; +import {applyStateUpdate} from '@/static/modules/utils/state'; +import {RunTestsFeature} from '@/constants'; export default (state, action) => { switch (action.type) { case actionNames.INIT_GUI_REPORT: { - return {...state, gui: true}; + return applyStateUpdate(state, {gui: true, app: {availableFeatures: [RunTestsFeature]}}); } case actionNames.INIT_STATIC_REPORT: { diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 6ed97b89b..d10c3af8f 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -15,6 +15,7 @@ .g-root { --g-font-family-sans: 'Jost', sans-serif; --g-color-base-background: #eee; + --g-color-text-danger-heavy: #e9043a; } body { @@ -37,3 +38,8 @@ body { .gn-aside-header__header:after { display: none; } + +.action-button { + font-size: 15px; + font-weight: 450; +} diff --git a/lib/static/new-ui/app/gui.tsx b/lib/static/new-ui/app/gui.tsx index 5c9f3682f..7bf236f85 100644 --- a/lib/static/new-ui/app/gui.tsx +++ b/lib/static/new-ui/app/gui.tsx @@ -1,15 +1,43 @@ import React, {ReactNode, useEffect} from 'react'; import {createRoot} from 'react-dom/client'; + +import {ClientEvents} from '@/gui/constants'; import {App} from './App'; import store from '../../modules/store'; -import {finGuiReport, initGuiReport} from '../../modules/actions'; +import {finGuiReport, initGuiReport, suiteBegin, testBegin, testResult, testsEnd} from '../../modules/actions'; const rootEl = document.getElementById('app') as HTMLDivElement; const root = createRoot(rootEl); function Gui(): ReactNode { + const subscribeToEvents = (): void => { + const eventSource = new EventSource('/events'); + + eventSource.addEventListener(ClientEvents.BEGIN_SUITE, (e) => { + const data = JSON.parse(e.data); + store.dispatch(suiteBegin(data)); + }); + + eventSource.addEventListener(ClientEvents.BEGIN_STATE, (e) => { + const data = JSON.parse(e.data); + store.dispatch(testBegin(data)); + }); + + [ClientEvents.TEST_RESULT, ClientEvents.ERROR].forEach((eventName) => { + eventSource.addEventListener(eventName, (e) => { + const data = JSON.parse(e.data); + store.dispatch(testResult(data)); + }); + }); + + eventSource.addEventListener(ClientEvents.END, () => { + store.dispatch(testsEnd()); + }); + }; + useEffect(() => { store.dispatch(initGuiReport()); + subscribeToEvents(); return () => { store.dispatch(finGuiReport()); diff --git a/lib/static/new-ui/components/AttemptPicker/index.module.css b/lib/static/new-ui/components/AttemptPicker/index.module.css new file mode 100644 index 000000000..0dbcea499 --- /dev/null +++ b/lib/static/new-ui/components/AttemptPicker/index.module.css @@ -0,0 +1,21 @@ +.container { + display: flex; + gap: 16px; +} + +.heading { + line-height: 28px; +} + +.attempts-container { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.retry-button { + composes: action-button from global; + margin-left: auto; + /* Sets spinner color */ + --g-color-line-brand: var(--g-color-text-hint); +} diff --git a/lib/static/new-ui/components/AttemptPicker/index.tsx b/lib/static/new-ui/components/AttemptPicker/index.tsx index 51d314459..9cbcdd155 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.tsx +++ b/lib/static/new-ui/components/AttemptPicker/index.tsx @@ -1,9 +1,15 @@ -import {Flex} from '@gravity-ui/uikit'; import React, {ReactNode} from 'react'; -import {connect} from 'react-redux'; +import {connect, useDispatch, useSelector} from 'react-redux'; +import {ArrowRotateRight} from '@gravity-ui/icons'; import {State} from '@/static/new-ui/types/store'; import {AttemptPickerItem} from '@/static/new-ui/components/AttemptPickerItem'; +import styles from './index.module.css'; +import classNames from 'classnames'; +import {Button, Icon, Spin} from '@gravity-ui/uikit'; +import {RunTestsFeature} from '@/constants'; +import {retryTest} from '@/static/modules/actions'; +import {getCurrentBrowser} from '@/static/new-ui/features/suites/selectors'; interface AttemptPickerProps { onChange?: (browserId: string, resultId: string, attemptIndex: number) => unknown; @@ -18,7 +24,13 @@ interface AttemptPickerInternalProps extends AttemptPickerProps { function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { const {resultIds, currentResultId} = props; - const onClickHandler = (resultId: string, attemptIndex: number): void => { + const dispatch = useDispatch(); + const currentBrowser = useSelector(getCurrentBrowser); + const isRunTestsAvailable = useSelector((state: State) => state.app.availableFeatures) + .find(feature => feature.name === RunTestsFeature.name); + const isRunning = useSelector((state: State) => state.running); + + const onAttemptPickHandler = (resultId: string, attemptIndex: number): void => { if (!props.browserId || currentResultId === resultId) { return; } @@ -26,9 +38,15 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { props.onChange?.(props.browserId, resultId, attemptIndex); }; - return -

Attempts

- + const onRetryTestHandler = (): void => { + if (currentBrowser) { + dispatch(retryTest({testName: currentBrowser.parentId, browserName: currentBrowser.name})); + } + }; + + return
+

Attempts

+
{resultIds.map((resultId, index) => { const isActive = resultId === currentResultId; @@ -36,11 +54,14 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { key={resultId} resultId={resultId} isActive={isActive} - onClick={(): unknown => onClickHandler(resultId, index)} + onClick={(): unknown => onAttemptPickHandler(resultId, index)} />; })} - - ; +
+ {isRunTestsAvailable && } +
; } export const AttemptPicker = connect((state: State) => { diff --git a/lib/static/new-ui/components/AttemptPickerItem/index.module.css b/lib/static/new-ui/components/AttemptPickerItem/index.module.css index a3ccbe947..915cd7906 100644 --- a/lib/static/new-ui/components/AttemptPickerItem/index.module.css +++ b/lib/static/new-ui/components/AttemptPickerItem/index.module.css @@ -1,9 +1,10 @@ .attempt-picker-item { - --g-button-padding: 8px; + width: 28px; } .attempt-picker-item--active { --g-button-border-width: 1px; + box-shadow: 0px 1px 3px 0px var(--box-shadow-color); } .attempt-picker-item--staged { @@ -16,10 +17,21 @@ border-color: rgba(207, 231, 252, 1); } +.attempt-picker-item--success { + --box-shadow-color: #21a95661; +} + +.attempt-picker-item--error { + --box-shadow-color: #ff004b61; +} + .attempt-picker-item--fail { + --g-button-text-color-hover: #c522ff; + --g-button-background-color-hover: #cf49ff4f; --g-button-background-color: var(--color-pink-100); --g-button-text-color: var(--color-pink-600); --g-button-border-color: var(--color-pink-600); + --box-shadow-color: #cf49ff61; } .attempt-picker-item--fail_error { 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 aed678de5..9772c4203 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 @@ -39,5 +39,6 @@ position: sticky; top: 0; z-index: 10; - padding-bottom: 4px; + padding-bottom: 8px; + border-bottom: 1px solid #eee; } 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 396a611c8..ca9f84b1a 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 @@ -55,6 +55,8 @@ .tree-view__item--current { background: #a28aff !important; color: #fff !important; + /* Sets spinner color */ + --g-color-line-brand: #fff; } .tree-view__item__title--current { diff --git a/lib/static/new-ui/features/suites/selectors.ts b/lib/static/new-ui/features/suites/selectors.ts index c662f47a0..04ab133b8 100644 --- a/lib/static/new-ui/features/suites/selectors.ts +++ b/lib/static/new-ui/features/suites/selectors.ts @@ -1,4 +1,13 @@ -import {ImageEntity, ResultEntity, State} from '@/static/new-ui/types/store'; +import {BrowserEntity, ImageEntity, ResultEntity, State} from '@/static/new-ui/types/store'; + +export const getCurrentBrowser = (state: State): BrowserEntity | null => { + const browserId = state.app.suitesPage.currentBrowserId; + if (!browserId) { + return null; + } + + return state.tree.browsers.byId[browserId]; +}; export const getCurrentResultId = (state: State): string | null => { const browserId = state.app.suitesPage.currentBrowserId; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index f374849a2..42d5c81bb 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -1,4 +1,4 @@ -import {DiffModeId, TestStatus, ViewMode} from '@/constants'; +import {DiffModeId, Feature, TestStatus, ViewMode} from '@/constants'; import {BrowserItem, ImageFile, ReporterConfig, TestError, TestStepCompressed} from '@/types'; import {HtmlReporterValues} from '@/plugin-api'; import {CoordBounds} from 'looks-same'; @@ -132,6 +132,7 @@ export interface TreeEntity { export interface State { app: { isInitialized: boolean; + availableFeatures: Feature[], suitesPage: { currentBrowserId: string | null; }; @@ -155,6 +156,7 @@ export interface State { keyToGroupTestsBy: string; baseHost: string; }; + running: boolean; apiValues: HtmlReporterValues; config: ReporterConfig; } diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 96bd298fb..39d6c08d6 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -1,7 +1,9 @@ -import {TestStatus} from '@/constants'; import {ArrowRotateLeft, CircleCheck, CircleDashed, CircleMinus, CircleXmark, ArrowsRotateLeft} from '@gravity-ui/icons'; +import {Spin} from '@gravity-ui/uikit'; import React from 'react'; +import {TestStatus} from '@/constants'; + export const getIconByStatus = (status: TestStatus): React.JSX.Element => { if (status === TestStatus.FAIL || status === TestStatus.ERROR) { return ; @@ -13,6 +15,8 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; } else if (status === TestStatus.UPDATED) { return ; + } else if (status === TestStatus.RUNNING) { + return ; } return ; diff --git a/lib/static/styles.css b/lib/static/styles.css index 92bc3a233..5fa41a936 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -858,15 +858,16 @@ a:active { } .g-root_theme_light { - --g-color-base-brand: var(--g-color-private-color-400); - --g-color-base-brand-hover: var(--g-color-private-color-150); + --g-color-base-brand: var(--g-color-private-color-550-solid); + --g-color-base-brand-hover: var(--g-color-private-color-450-solid); --g-color-base-selection: var(--g-color-private-color-100); --g-color-base-selection-hover: var(--g-color-private-color-150); - --g-color-line-brand: var(--g-color-private-color-200); + --g-color-line-brand: var(--g-color-private-color-500); --g-color-text-brand: var(--g-color-private-color-300); --g-color-text-link: var(--g-color-private-color-300); --g-color-text-link-hover: var(--g-color-private-color-500); --gn-aside-header-decoration-collapsed-background-color: var(--g-color-private-color-150); + --g-color-text-brand-contrast: var(--g-color-private-color-50-solid); --g-color-private-color-50: rgba(108,71,255,0.1); --g-color-private-color-100: rgba(108,71,255,0.15); @@ -950,8 +951,8 @@ h1, h2, h3, h4, h5, h6 { .text-header-1 { font-family: Jost, sans-serif; - font-weight: 500; - font-size: var(--g-text-header-1-font-size); + font-weight: 450; + font-size: 18px; line-height: var(--g-text-header-1-line-height); } diff --git a/test/func/tests/screens/0049570/chrome/retry-switcher.png b/test/func/tests/screens/0049570/chrome/retry-switcher.png index 22ebec71d..59e28a8ef 100644 Binary files a/test/func/tests/screens/0049570/chrome/retry-switcher.png and b/test/func/tests/screens/0049570/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/07c99c0/chrome/retry-switcher.png b/test/func/tests/screens/07c99c0/chrome/retry-switcher.png index 22ebec71d..d645a380e 100644 Binary files a/test/func/tests/screens/07c99c0/chrome/retry-switcher.png and b/test/func/tests/screens/07c99c0/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/1361a92/chrome/retry-selector.png b/test/func/tests/screens/1361a92/chrome/retry-selector.png index f446cdd2f..2e291679c 100644 Binary files a/test/func/tests/screens/1361a92/chrome/retry-selector.png and b/test/func/tests/screens/1361a92/chrome/retry-selector.png differ diff --git a/test/func/tests/screens/1bb949f/chrome/retry-switcher.png b/test/func/tests/screens/1bb949f/chrome/retry-switcher.png index 22ebec71d..20d6c2e63 100644 Binary files a/test/func/tests/screens/1bb949f/chrome/retry-switcher.png and b/test/func/tests/screens/1bb949f/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/42ea26d/chrome/retry-selector.png b/test/func/tests/screens/42ea26d/chrome/retry-selector.png index c6d69f136..5759bd8a9 100644 Binary files a/test/func/tests/screens/42ea26d/chrome/retry-selector.png and b/test/func/tests/screens/42ea26d/chrome/retry-selector.png differ diff --git a/test/func/tests/screens/45b9477/chrome/retry-switcher.png b/test/func/tests/screens/45b9477/chrome/retry-switcher.png index 8bef028a3..c3911945b 100644 Binary files a/test/func/tests/screens/45b9477/chrome/retry-switcher.png and b/test/func/tests/screens/45b9477/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/67cd8d8/chrome/retry-switcher.png b/test/func/tests/screens/67cd8d8/chrome/retry-switcher.png index 8bef028a3..7c3afc6ce 100644 Binary files a/test/func/tests/screens/67cd8d8/chrome/retry-switcher.png and b/test/func/tests/screens/67cd8d8/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/bdf4a21/chrome/retry-switcher.png b/test/func/tests/screens/bdf4a21/chrome/retry-switcher.png index 8bef028a3..d3788e671 100644 Binary files a/test/func/tests/screens/bdf4a21/chrome/retry-switcher.png and b/test/func/tests/screens/bdf4a21/chrome/retry-switcher.png differ diff --git a/test/func/tests/screens/d90f7de/chrome/retry-selector.png b/test/func/tests/screens/d90f7de/chrome/retry-selector.png index e9cbb1c30..6274f4e57 100644 Binary files a/test/func/tests/screens/d90f7de/chrome/retry-selector.png and b/test/func/tests/screens/d90f7de/chrome/retry-selector.png differ diff --git a/test/func/tests/screens/ff4deba/chrome/retry-selector.png b/test/func/tests/screens/ff4deba/chrome/retry-selector.png index c6d69f136..5759bd8a9 100644 Binary files a/test/func/tests/screens/ff4deba/chrome/retry-selector.png and b/test/func/tests/screens/ff4deba/chrome/retry-selector.png differ