diff --git a/lib/adapters/test-result/transformers/db.ts b/lib/adapters/test-result/transformers/db.ts index 3043766e8..1daff2ab1 100644 --- a/lib/adapters/test-result/transformers/db.ts +++ b/lib/adapters/test-result/transformers/db.ts @@ -16,6 +16,16 @@ export class DbTestResultTransformer { transform(testResult: ReporterTestResult): DbTestResult { const suiteUrl = getUrlWithBase(testResult.url, this._options.baseHost); + const imagesInfo = (testResult.imagesInfo ?? []).map(imageInfo => { + if (!_.isObject(imageInfo) || !('error' in imageInfo) || !imageInfo.error) { + return imageInfo; + } + + return { + ...imageInfo, + error: getError(imageInfo.error) + }; + }); const metaInfoFull = _.merge(_.cloneDeep(testResult.meta), { url: testResult.meta?.url ?? suiteUrl ?? '', @@ -34,7 +44,7 @@ export class DbTestResultTransformer { description: testResult.description, error: getError(testResult.error), skipReason: testResult.skipReason, - imagesInfo: testResult.imagesInfo ?? [], + imagesInfo, screenshot: Boolean(testResult.screenshot), multipleTabs: testResult.multipleTabs, status: testResult.status, diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 7e0f09053..f4d689f50 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -148,12 +148,29 @@ export const hasUnrelatedToScreenshotsErrors = (error: TestError): boolean => { !isAssertViewError(error); }; +const SHORT_ERROR_STACK_MARKERS = [ + 'Tests were stopped by the user', + 'NoRefImageError', + 'Too many requests for session creation' +]; + +const formatErrorStack = (stack?: string): string | undefined => { + if (!stack || !SHORT_ERROR_STACK_MARKERS.some(marker => stack.includes(marker))) { + return stack; + } + + return stack.split('\n')[0]; +}; + export const getError = (error?: TestError): undefined | Pick => { if (!error) { return undefined; } - return pick(error, ['name', 'message', 'stack', 'stateName', 'snippet']); + return { + ...pick(error, ['name', 'message', 'stack', 'stateName', 'snippet']), + stack: formatErrorStack(error.stack) + }; }; export const hasDiff = (assertViewResults: {name?: string}[]): boolean => { diff --git a/lib/gui/server.ts b/lib/gui/server.ts index d9f393b2c..45643eab7 100644 --- a/lib/gui/server.ts +++ b/lib/gui/server.ts @@ -44,6 +44,7 @@ export const start = async (args: ServerArgs): Promise => { const app = App.create(args); const server = express(); + let stopAll = false; server.use(bodyParser.json({limit: MAX_REQUEST_SIZE})); @@ -162,12 +163,19 @@ export const start = async (args: ServerArgs): Promise => { server.post('/run', async (req, res) => { try { + stopAll = false; // do not wait for completion so that response does not hang and browser does not restart it by timeout // eslint-disable-next-line @typescript-eslint/explicit-function-return-type (async () => { const {tests, repeatCount} = req.body; for (let i = 0; i < repeatCount; i++) { + if (stopAll) { + stopAll = false; + app.sendClientEvent(ClientEvents.REPEAT_LEFT, {repeatLeft: 0}); + break; + } + await app.run(tests, {retry: repeatCount === 1}); app.sendClientEvent(ClientEvents.REPEAT_LEFT, {repeatLeft: repeatCount - i - 1}); @@ -285,6 +293,7 @@ export const start = async (args: ServerArgs): Promise => { server.post('/stop', (_req, res) => { try { + stopAll = true; // pass 0 to prevent terminating testplane process toolAdapter.halt(new Error('Tests were stopped by the user'), 0); res.sendStatus(OK); diff --git a/lib/static/new-ui/components/MainLayout/hotkeys.ts b/lib/static/new-ui/components/MainLayout/hotkeys.ts index 1136f5a8c..96b2a48e8 100644 --- a/lib/static/new-ui/components/MainLayout/hotkeys.ts +++ b/lib/static/new-ui/components/MainLayout/hotkeys.ts @@ -28,6 +28,7 @@ export const HOTKEYS_GROUPS: HotkeysGroup[] = [ {title: 'Next attempt', value: '→'}, {title: 'Run current test', value: 'r'}, {title: 'Run all/selected tests', value: 'shift+r'}, + {title: 'Stop all tests', value: 'shift+s'}, {title: 'Accept screenshot', value: 'a'}, {title: 'Undo accept', value: 'u'}, {title: 'Accept all/selected', value: 'shift+a'}, diff --git a/lib/static/new-ui/components/RunTest/index.module.css b/lib/static/new-ui/components/RunTest/index.module.css index 7f7d66787..de04ef0e5 100644 --- a/lib/static/new-ui/components/RunTest/index.module.css +++ b/lib/static/new-ui/components/RunTest/index.module.css @@ -5,7 +5,14 @@ } .retry-button { + padding-right: 4px; +} + +.stop-button, .retry-button { composes: regular-button from global; +} + +.stop-button { padding-right: 4px; } diff --git a/lib/static/new-ui/components/RunTest/index.tsx b/lib/static/new-ui/components/RunTest/index.tsx index d0e139664..acac3aebb 100644 --- a/lib/static/new-ui/components/RunTest/index.tsx +++ b/lib/static/new-ui/components/RunTest/index.tsx @@ -1,9 +1,9 @@ import React, {forwardRef, ReactNode, useCallback, useState} from 'react'; import styles from './index.module.css'; -import {Button, ButtonProps, Icon, Popover} from '@gravity-ui/uikit'; -import {ArrowRotateRight, ChevronDown} from '@gravity-ui/icons'; -import {thunkRunTest} from '@/static/modules/actions'; +import {Button, ButtonProps, Icon, Popover, Hotkey} from '@gravity-ui/uikit'; +import {ArrowRotateRight, ChevronDown, Stop} from '@gravity-ui/icons'; +import {thunkRunTest, thunkStopTests} from '@/static/modules/actions'; import {useDispatch} from 'react-redux'; import {RunTestsFeature} from '@/constants'; import {useAnalytics} from '../../hooks/useAnalytics'; @@ -52,6 +52,28 @@ export const RunTestButton = forwardRef { + dispatch(thunkStopTests()); + }, [thunkStopTests, dispatch]); + + if (isRunning) { + return ( +
+ +
+ ); + } + return
} diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx index 8dc0beefc..59d7308dd 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/VisualChecksStickyHeader.tsx @@ -216,7 +216,7 @@ export function VisualChecksStickyHeader({currentNamedImage, treeData, onImageCh >