Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/adapters/test-result/transformers/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand All @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestError, 'name' | 'message' | 'stack' | 'stateName' | 'snippet'> => {
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 => {
Expand Down
9 changes: 9 additions & 0 deletions lib/gui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {

const app = App.create(args);
const server = express();
let stopAll = false;

server.use(bodyParser.json({limit: MAX_REQUEST_SIZE}));

Expand Down Expand Up @@ -162,12 +163,19 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {

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});
Expand Down Expand Up @@ -285,6 +293,7 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {

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);
Expand Down
1 change: 1 addition & 0 deletions lib/static/new-ui/components/MainLayout/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down
7 changes: 7 additions & 0 deletions lib/static/new-ui/components/RunTest/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
}

.retry-button {
padding-right: 4px;
}

.stop-button, .retry-button {
composes: regular-button from global;
}

.stop-button {
padding-right: 4px;
}

Expand Down
28 changes: 25 additions & 3 deletions lib/static/new-ui/components/RunTest/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -52,6 +52,28 @@ export const RunTestButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, R
setIsRunOptionsOpen(open);
}, []);

const onStopClick = useCallback((): void => {
dispatch(thunkStopTests());
}, [thunkStopTests, dispatch]);

if (isRunning) {
return (
<div className={styles.buttonsContainer}>
<Button
ref={ref as any} // eslint-disable-line @typescript-eslint/no-explicit-any
view={'action'}
className={classNames(styles.stopButton, className)}
onClick={onStopClick}
{...buttonProps}
>
<Icon data={Stop}/>
Stop all
<Hotkey value="shift+s" view={buttonProps?.view === 'outlined' ? 'light' : 'dark'} />
</Button>
</div>
);
}

return <div className={styles.buttonsContainer}>
<Button
ref={ref as any} // eslint-disable-line @typescript-eslint/no-explicit-any
Expand Down
39 changes: 29 additions & 10 deletions lib/static/new-ui/components/TreeActionsToolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Hierarchy,
ListUl,
Play,
Stop,
Square,
SquareCheck,
SquareDashed,
Expand All @@ -28,7 +29,8 @@ import {
staticAccepterStageScreenshot,
staticAccepterUnstageScreenshot,
thunkRefreshGuiReport,
thunkRunTests
thunkRunTests,
thunkStopTests
} from '@/static/modules/actions';
import {ImageEntity, TreeViewMode} from '@/static/new-ui/types/store';
import {CHECKED, INDETERMINATE} from '@/constants/checked-statuses';
Expand Down Expand Up @@ -163,6 +165,10 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi
}
}, [analytics, isSelectedAtLeastOne, selectedTests, visibleBrowserIds, browsersById, dispatch]);

const handleStop = useCallback((): void => {
dispatch(thunkStopTests());
}, [thunkStopTests, dispatch]);

const handleUndo = (): void => {
const acceptableImageIds = activeImages
.filter(image => isScreenRevertable({image, gui: isGuiMode, isLastResult: true, isStaticImageAccepterEnabled}))
Expand Down Expand Up @@ -197,6 +203,7 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi
};

useHotkey('shift+r', handleRun, {enabled: Boolean(isRunTestsAvailable) && !isRunning && isInitialized});
useHotkey('shift+s', handleStop, {enabled: Boolean(isRunTestsAvailable) && isRunning && isInitialized});
useHotkey('shift+a', handleAccept, {enabled: Boolean(isEditScreensAvailable) && !areActionsDisabled && isAtLeastOneAcceptable && !isUndoButtonVisible});

const loadedPluginConfigs = plugins.getLoadedConfigs();
Expand Down Expand Up @@ -225,15 +232,27 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi
/>
)}
{isRunTestsAvailable && (
<IconButton
className={styles.iconButton}
icon={<Icon data={Play} height={14}/>}
tooltip={<>Run {selectedOrVisible} ⋅ <Hotkey value="shift+r" view="light" /></>}
text="Run"
view={'flat'}
onClick={handleRun}
disabled={isRunning || !isInitialized}
/>
isRunning ? (
<IconButton
className={styles.iconButton}
icon={<Icon data={Stop} height={14}/>}
tooltip={<>Stop all ⋅ <Hotkey value="shift+s" view="light" /></>}
text="Stop"
view={'flat'}
onClick={handleStop}
disabled={!isInitialized}
/>
) : (
<IconButton
className={styles.iconButton}
icon={<Icon data={Play} height={14}/>}
tooltip={<>Run {selectedOrVisible} ⋅ <Hotkey value="shift+r" view="light" /></>}
text="Run"
view={'flat'}
onClick={handleRun}
disabled={!isInitialized}
/>
)
)}
{isRunTestsAvailable && hasRunTestOptions && <Popover
content={<div className={styles.runOptionsContainer}><ExtensionPoint name={ExtensionPointName.RunTestOptions}></ExtensionPoint></div>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function VisualChecksStickyHeader({currentNamedImage, treeData, onImageCh
>
<Button
view="outlined"
className={styles.actionButton}
className={styles.goToTest}
disabled={isRunning || isProcessing}
onClick={onSuites}
qa="go-suites-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
margin-left: 4px;
}

.action-button {
.go-to-test {
composes: regular-button from global;
padding-right: 4px;
}
64 changes: 64 additions & 0 deletions test/unit/lib/adapters/test-result/transformers/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {DbTestResultTransformer} from 'lib/adapters/test-result/transformers/db';
import type {ReporterTestResult} from 'lib/adapters/test-result';
import {ERROR} from 'lib/constants';

describe('DbTestResultTransformer', () => {
it('should preserve non-object image info', () => {
const testResult = {
testPath: ['suite'],
browserId: 'chrome',
meta: {},
file: 'test.ts',
sessionId: 'session-id',
history: [],
imagesInfo: ['some-images'],
multipleTabs: true,
status: ERROR,
timestamp: 1,
duration: 1,
attachments: []
} as unknown as ReporterTestResult;

const result = new DbTestResultTransformer({}).transform(testResult);

assert.deepEqual(result.imagesInfo, ['some-images']);
});

it('should format an error stack inside image info', () => {
const stack = [
'NoRefImageError: can not find reference image at /reference/image.png',
' at assertView (/path/to/assert-view.ts:1:1)'
].join('\n');
const testResult = {
testPath: ['suite'],
browserId: 'chrome',
meta: {},
file: 'test.ts',
sessionId: 'session-id',
history: [],
imagesInfo: [{
status: ERROR,
stateName: 'plain',
actualImg: {path: '/actual/image.png'},
error: {
name: 'NoRefImageError',
message: 'can not find reference image at /reference/image.png',
stack
}
}],
multipleTabs: true,
status: ERROR,
timestamp: 1,
duration: 1,
attachments: []
} as ReporterTestResult;

const result = new DbTestResultTransformer({}).transform(testResult);
const imageInfo = result.imagesInfo[0];

assert.equal(
'error' in imageInfo && imageInfo.error?.stack,
'NoRefImageError: can not find reference image at /reference/image.png'
);
});
});
25 changes: 25 additions & 0 deletions test/unit/lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,31 @@ describe('common-utils', () => {
stateName: 'some-test'
});
});

it('should keep only first stack line for stopped-by-user error', () => {
const error = {
name: 'Error',
message: 'Tests were stopped by the user',
stack: [
'Error: Tests were stopped by the user',
' at stopTests (/path/to/server.ts:1:1)',
' at processRequest (/path/to/router.ts:2:2)'
].join('\n')
};

const result = getError(error);

assert.equal(result?.stack, 'Error: Tests were stopped by the user');
assert.include(error.stack, 'stopTests');
});

it('should keep full stack for other errors', () => {
const stack = 'Error: some-message\n at some-function (/path/to/file.ts:1:1)';

const result = getError({name: 'Error', message: 'some-message', stack});

assert.equal(result?.stack, stack);
});
});

describe('hasDiff', () => {
Expand Down
Loading