From 3ad54fc2a3b3ddc6036371131a210ad20cf1edd4 Mon Sep 17 00:00:00 2001 From: rocketraccoon Date: Thu, 2 Jul 2026 03:21:41 +0700 Subject: [PATCH] feat: add refresh tests --- lib/gui/app.ts | 5 +++++ lib/gui/server.ts | 9 ++++++++ lib/gui/tool-runner/index.ts | 5 +++++ lib/report-builder/gui.ts | 7 ++++++ lib/report-builder/static.ts | 4 ++++ lib/sqlite-client.ts | 4 ++++ lib/static/modules/action-names.ts | 1 + lib/static/modules/actions/filters.ts | 6 +++++ lib/static/modules/actions/lifecycle.ts | 20 +++++++++++++++++ lib/static/modules/reducers/filters.ts | 4 ++++ .../components/AttemptPickerItem/index.tsx | 8 +++---- .../TreeActionsToolbar/index.module.css | 9 ++++++++ .../components/TreeActionsToolbar/index.tsx | 22 +++++++++++++++++-- .../suites/components/SuitesPage/index.tsx | 6 +++++ .../new-ui/features/suites/selectors.ts | 8 +++++-- .../features/visual-checks/selectors.ts | 2 +- lib/static/new-ui/types/store.ts | 1 + 17 files changed, 112 insertions(+), 9 deletions(-) diff --git a/lib/gui/app.ts b/lib/gui/app.ts index 4bac8d6ea..2c7ad5c8c 100644 --- a/lib/gui/app.ts +++ b/lib/gui/app.ts @@ -53,6 +53,11 @@ export class App { return this._toolRunner.findEqualDiffs(data); } + async refreshTests(): Promise { + await this._toolRunner.refreshTests(); + return this._toolRunner.tree; + } + addClient(connection: Response): void { this._toolRunner.addClient(connection); } diff --git a/lib/gui/server.ts b/lib/gui/server.ts index 33838dbf5..d9f393b2c 100644 --- a/lib/gui/server.ts +++ b/lib/gui/server.ts @@ -274,6 +274,15 @@ export const start = async (args: ServerArgs): Promise => { logger.log('server shutting down'); }); + server.get('/refresh-tests', async (_req, res) => { + try { + const tree = await app.refreshTests(); + res.json(tree satisfies GetInitResponse); + } catch (e: unknown) { + res.status(INTERNAL_SERVER_ERROR).send(`Error while refreshing tests: ${(e as Error).message}`); + } + }); + server.post('/stop', (_req, res) => { try { // pass 0 to prevent terminating testplane process diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index 0120154ed..24f6c678f 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -154,6 +154,11 @@ export class ToolRunner { return this._toolAdapter.readTests(this._testFiles, this._globalOpts); } + async refreshTests(): Promise { + this._ensureReportBuilder().saveDb(); + await this.initialize(); + } + protected _ensureReportBuilder(): GuiReportBuilder { if (!this._reportBuilder) { throw new Error('ToolRunner has to be initialized before usage'); diff --git a/lib/report-builder/gui.ts b/lib/report-builder/gui.ts index 58c0feced..7a7d24267 100644 --- a/lib/report-builder/gui.ts +++ b/lib/report-builder/gui.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import {StaticReportBuilder, StaticReportBuilderOptions} from './static'; +import {TestAttemptManager} from '../test-attempt-manager'; import {GuiTestsTreeBuilder, TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../tests-tree-builder/gui'; import {UPDATED, DB_COLUMNS, TestStatus, DEFAULT_TITLE_DELIMITER, SKIPPED, SUCCESS} from '../constants'; import {ConfigForStaticFile, getConfigForStaticFile} from '../server-utils'; @@ -76,6 +77,12 @@ export class GuiReportBuilder extends StaticReportBuilder { }; } + resetTree(): void { + this._testsTree = GuiTestsTreeBuilder.create({baseHost: this._reporterConfig.baseHost}); + this._testAttemptManager = new TestAttemptManager(); + this._skips = []; + } + getTestBranch(id: string): TestBranch { return this._testsTree.getTestBranch(id); } diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts index fccf2160f..b09a3d461 100644 --- a/lib/report-builder/static.ts +++ b/lib/report-builder/static.ts @@ -76,6 +76,10 @@ export class StaticReportBuilder { this._workers = workers; } + saveDb(): void { + this._dbClient.save(); + } + registerAttempt(testInfo: {fullName: string, browserId: string}, status: TestStatus): number { return this._testAttemptManager.registerAttempt(testInfo, status); } diff --git a/lib/sqlite-client.ts b/lib/sqlite-client.ts index 333706366..34783da0f 100644 --- a/lib/sqlite-client.ts +++ b/lib/sqlite-client.ts @@ -120,6 +120,10 @@ export class SqliteClient { return this._db; } + save(): void { + fs.writeFileSync(this._dbPath, this._db.export()); + } + close(): void { fs.writeFileSync(this._dbPath, this._db.run('VACUUM').export()); diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 4af3d8e91..ad48d2cd0 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -82,5 +82,6 @@ export default { SNAPSHOTS_PLAYER_GO_TO_TIME: 'SNAPSHOTS_PLAYER_GO_TO_TIME', SNAPSHOTS_PLAYER_TOGGLE_VISIBILITY: 'SNAPSHOTS_PLAYER_TOGGLE_VISIBILITY', SET_BROWSER_FEATURES: 'SET_BROWSER_FEATURES', + SET_REFRESH_LOADING: 'SET_REFRESH_LOADING', SET_SEARCH_LOADING: 'SET_SEARCH_LOADING' } as const; diff --git a/lib/static/modules/actions/filters.ts b/lib/static/modules/actions/filters.ts index 3f140a82e..e8ca94b10 100644 --- a/lib/static/modules/actions/filters.ts +++ b/lib/static/modules/actions/filters.ts @@ -38,10 +38,16 @@ export const setSearchLoading = (payload: boolean): SetSearchLoading => { export type ChangeViewModeAction = Action>; export const changeViewMode = (payload: ChangeViewModeAction['payload']): ChangeViewModeAction => ({type: actionNames.CHANGE_VIEW_MODE, payload}); +export type SetRefreshLoading = Action; +export const setRefreshLoading = (payload: boolean): SetRefreshLoading => { + return {type: actionNames.SET_REFRESH_LOADING, payload}; +}; + export type FiltersAction = | UpdateNameFilterAction | SetMatchCaseFilterAction | SetUseRegexFilterAction | SelectBrowsersAction | ChangeViewModeAction + | SetRefreshLoading | SetSearchLoading; diff --git a/lib/static/modules/actions/lifecycle.ts b/lib/static/modules/actions/lifecycle.ts index 4d8a463e9..c33c54bad 100644 --- a/lib/static/modules/actions/lifecycle.ts +++ b/lib/static/modules/actions/lifecycle.ts @@ -18,6 +18,7 @@ import {GetInitResponse} from '@/gui/server'; import {Tree} from '@/tests-tree-builder/base'; import {BrowserItem} from '@/types'; import {createNotificationError} from '@/static/modules/actions/notifications'; +import {setRefreshLoading} from '@/static/modules/actions/filters'; import {LocalStorageKey} from '@/constants/local-storage'; import * as localStorageWrapper from '@/static/modules/local-storage-wrapper'; import {updateTimeTravelSettings} from '../../new-ui/utils/api'; @@ -139,6 +140,25 @@ export const thunkInitStaticReport = ({isNewUi}: InitStaticReportData = {}): App }; }; +export const thunkRefreshGuiReport = (): AppThunk => { + return async (dispatch, getState) => { + dispatch(setRefreshLoading(true)); + try { + const {db} = getState(); + const response = await axios.get('/refresh-tests'); + + if (!response.data || !db) { + throw new Error('Could not refresh tests data.'); + } + + dispatch(initGuiReport({...response.data, db})); + } catch (e: unknown) { + dispatch(createNotificationError('thunkRefreshGuiReport', e as Error)); + dispatch(setRefreshLoading(false)); + } + }; +}; + export type FinGuiReportAction = Action; export const finGuiReport = (): FinGuiReportAction => ({type: actionNames.FIN_GUI_REPORT}); diff --git a/lib/static/modules/reducers/filters.ts b/lib/static/modules/reducers/filters.ts index 7b5541ae3..ad649977c 100644 --- a/lib/static/modules/reducers/filters.ts +++ b/lib/static/modules/reducers/filters.ts @@ -41,6 +41,7 @@ export default (state: State, action: FiltersAction | InitGuiReportAction | Init state, { app: { + isRefreshLoading: false, viewMode, [Page.visualChecksPage]: { diffMode: visualChecksPageDiffMode @@ -99,6 +100,9 @@ export default (state: State, action: FiltersAction | InitGuiReportAction | Init case actionNames.SET_SEARCH_LOADING: return applyStateUpdate(state, {app: {isSearchLoading: action.payload}}); + case actionNames.SET_REFRESH_LOADING: + return applyStateUpdate(state, {app: {isRefreshLoading: action.payload}}); + default: return state; } diff --git a/lib/static/new-ui/components/AttemptPickerItem/index.tsx b/lib/static/new-ui/components/AttemptPickerItem/index.tsx index 64294b3e8..af280c0ed 100644 --- a/lib/static/new-ui/components/AttemptPickerItem/index.tsx +++ b/lib/static/new-ui/components/AttemptPickerItem/index.tsx @@ -75,14 +75,14 @@ export const AttemptPickerItem = (props: AttemptPickerItemProps): ReactNode => { const isGroupingEnabled = useSelector(({view: {keyToGroupTestsBy}, app: {isNewUi, suitesPage: {currentGroupId}}}: State) => isNewUi ? Boolean(currentGroupId) : Boolean(keyToGroupTestsBy)); - const status = isFailStatus(result.status) && hasUnrelatedToScreenshotsErrors((result as ResultEntityError).error) ? TestStatus.FAIL_ERROR : result.status; - - const buttonStyle = getButtonStyleByStatus(status); - if (!result) { return null; } + const status = isFailStatus(result.status) && hasUnrelatedToScreenshotsErrors((result as ResultEntityError).error) ? TestStatus.FAIL_ERROR : result.status; + + const buttonStyle = getButtonStyleByStatus(status); + const className = classNames( styles.attemptPickerItem, {[styles['attempt-picker-item--active']]: isActive}, diff --git a/lib/static/new-ui/components/TreeActionsToolbar/index.module.css b/lib/static/new-ui/components/TreeActionsToolbar/index.module.css index 39de01932..898d0e2fa 100644 --- a/lib/static/new-ui/components/TreeActionsToolbar/index.module.css +++ b/lib/static/new-ui/components/TreeActionsToolbar/index.module.css @@ -13,6 +13,15 @@ z-index: 1; } +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.is-refresh-loading { + animation: spin 0.8s linear infinite; +} + .buttons-container { margin-left: auto; display: flex; diff --git a/lib/static/new-ui/components/TreeActionsToolbar/index.tsx b/lib/static/new-ui/components/TreeActionsToolbar/index.tsx index 801a2f3c2..8d12e02e1 100644 --- a/lib/static/new-ui/components/TreeActionsToolbar/index.tsx +++ b/lib/static/new-ui/components/TreeActionsToolbar/index.tsx @@ -13,7 +13,8 @@ import { SquareMinus, ListUl, Hierarchy, - GearPlay + GearPlay, + ArrowsRotateRight } from '@gravity-ui/icons'; import React, {ReactNode, useCallback, useMemo} from 'react'; import {useDispatch, useSelector} from 'react-redux'; @@ -24,7 +25,9 @@ import { selectAll, setAllTreeNodesState, setTreeViewMode, staticAccepterStageScreenshot, - staticAccepterUnstageScreenshot, thunkRunTests + staticAccepterUnstageScreenshot, + thunkRunTests, + thunkRefreshGuiReport } from '@/static/modules/actions'; import {ImageEntity, TreeViewMode} from '@/static/new-ui/types/store'; import {CHECKED, INDETERMINATE} from '@/constants/checked-statuses'; @@ -74,6 +77,7 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi const selectedTests = useSelector(getCheckedTests); const visibleBrowserIds: string[] = useSelector(getVisibleBrowserIds); const isInitialized = useSelector(getIsInitialized); + const isRefreshLoading = useSelector((state) => state.app.isRefreshLoading); const isRunTestsAvailable = useSelector(state => state.app.availableFeatures) .find(feature => feature.name === RunTestsFeature.name); @@ -192,8 +196,22 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, ExtensionPointName.RunTestOptions); const hasRunTestOptions = pluginComponents.length > 0; + const handleRefresh = useCallback(() => { + dispatch(thunkRefreshGuiReport()); + }, [dispatch]); + const getViewButtons = (): ReactNode => ( <> + {isRunTestsAvailable && ( + } + tooltip="Refresh tests tree" + view="flat" + onClick={handleRefresh} + disabled={isRunning || !isInitialized || isRefreshLoading} + /> + )} {isRunTestsAvailable && ( isRunning ? ( diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx index 92ec31c44..8fc12cfea 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -218,6 +218,12 @@ export function SuitesPage(): ReactNode { } }, [visibleTreeNodeIds, tree, dispatch]); + useEffect(() => { + if (currentBrowser && !currentBrowserEntity && isInitialized && visibleTreeNodeIds.length > 0) { + onSelectFirstResult(); + } + }, [currentBrowser, currentBrowserEntity, isInitialized, visibleTreeNodeIds.length]); + useHotkey('ArrowDown', goToNextSuite); useHotkey('ArrowUp', goToPrevSuite); useHotkey('ArrowRight', goToNextAttempt); diff --git a/lib/static/new-ui/features/suites/selectors.ts b/lib/static/new-ui/features/suites/selectors.ts index d80507dd3..bcde81e21 100644 --- a/lib/static/new-ui/features/suites/selectors.ts +++ b/lib/static/new-ui/features/suites/selectors.ts @@ -18,7 +18,11 @@ export const getCurrentResultId = (state: State): string | null => { return null; } - const resultIds = state.tree.browsers.byId[browserId].resultIds; + const browser = state.tree.browsers.byId[browserId]; + if (!browser) { + return null; + } + const resultIds = browser.resultIds; const groupId = state.app.suitesPage.currentGroupId; @@ -90,7 +94,7 @@ export const getAttempt = (state: State): number | null => { const browserId = state.app.suitesPage.currentBrowserId; if (browserId) { - return state.tree.browsers.stateById[browserId].retryIndex; + return state.tree.browsers.stateById[browserId]?.retryIndex; } return null; diff --git a/lib/static/new-ui/features/visual-checks/selectors.ts b/lib/static/new-ui/features/visual-checks/selectors.ts index d7777efe9..5746b567c 100644 --- a/lib/static/new-ui/features/visual-checks/selectors.ts +++ b/lib/static/new-ui/features/visual-checks/selectors.ts @@ -172,7 +172,7 @@ export const getAttempt = (state: State): number | null => { const namedImage = getCurrentNamedImage(state); if (namedImage) { - return state.tree.browsers.stateById[namedImage?.browserId].retryIndex; + return state.tree.browsers.stateById[namedImage?.browserId]?.retryIndex; } return null; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 9a3871de2..58d3cb0e4 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -260,6 +260,7 @@ export interface State { isInitialized: boolean; availableFeatures: Feature[], isSearchLoading?: boolean; + isRefreshLoading?: boolean; // Filters in top of sidebar nameFilter: string;