Skip to content
Open
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
6 changes: 3 additions & 3 deletions lib/gui/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {Response} from 'express';

import {ToolRunner, ToolRunnerTree, UndoAcceptImagesResult} from './tool-runner';
import {RunParams, ToolRunner, ToolRunnerTree, UndoAcceptImagesResult} from './tool-runner';
import {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../tests-tree-builder/gui';

import type {ServerArgs} from './index';
Expand Down Expand Up @@ -29,8 +29,8 @@ export class App {
return this._toolRunner.finalize();
}

async run(tests: TestSpec[]): Promise<boolean> {
return this._toolRunner.run(tests);
async run(tests: TestSpec[], params: RunParams): Promise<boolean> {
return this._toolRunner.run(tests, params);
}

getTestsDataToUpdateRefs(imageIds: string[] = []): TestRefUpdateData[] {
Expand Down
8 changes: 6 additions & 2 deletions lib/gui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,14 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {
}
});

server.post('/run', (req, res) => {
server.post('/run', async (req, res) => {
try {
const {tests, repeatCount} = req.body;
// do not wait for completion so that response does not hang and browser does not restart it by timeout
app.run(req.body);
for (let i = 0; i < repeatCount; i++) {
await app.run(tests, {retry: repeatCount === 1});
Comment on lines +167 to +168
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore non-blocking response for /run

The /run handler now awaits every app.run call before returning 200, which turns this endpoint into a long-lived request for the full test duration (and for all repeats). In environments with request timeouts (browser/network/proxy), this can cause the request to be aborted or retried while tests are still executing, leading to duplicate runs or flaky UI behavior; the previous contract in this handler was explicitly fire-and-forget.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does passing retry change?

Also, the comments in code above and codex's are valid IMO. I've tried running slower tests on a large project and the request takes minutes to complete. I'm afraid it will timeout if I tried to run more tests.

⚠️ Another important issue I've noticed is that the retry button becomes active again after one retry is finished and we are waiting for a session, which is quite misleading, especially when getting a new session takes a while.

}

res.sendStatus(OK);
} catch (e) {
res.status(INTERNAL_SERVER_ERROR).send(`Error while trying to run tests: ${(e as Error).message}`);
Expand Down
8 changes: 6 additions & 2 deletions lib/gui/tool-runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface UndoAcceptImagesResult {
removedResults: string[];
}

export interface RunParams {
retry?: boolean;
}

export class ToolRunner {
private _testFiles: string[];
private _toolAdapter: ToolAdapter;
Expand Down Expand Up @@ -337,13 +341,13 @@ export class ToolRunner {
return comparisons.filter(Boolean).map(image => (image as TestEqualDiffsData).id);
}

async run(tests: TestSpec[] = []): Promise<boolean> {
async run(tests: TestSpec[] = [], runParams: RunParams = {retry: true}): Promise<boolean> {
const testCollection = this._ensureTestCollection();
const shouldRunAllTests = _.isEmpty(tests);

// if tests are not passed, then run all tests with all available retries
// if tests are specified, then retry only passed tests without retries
return shouldRunAllTests
return (shouldRunAllTests && runParams.retry)
? this._toolAdapter.run(testCollection, tests, this._globalOpts)
: this._toolAdapter.runWithoutRetries(testCollection, tests, this._globalOpts);
}
Expand Down
62 changes: 39 additions & 23 deletions lib/static/components/extension-point.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
import ErrorBoundary from './error-boundary';
import * as plugins from '../modules/plugins';

import {TestRepeaterComponent} from './test-repeater';
import {ExtensionPointName} from '@/static/new-ui/constants/plugins';

export default class ExtensionPoint extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
Expand All @@ -12,10 +15,19 @@ export default class ExtensionPoint extends Component {
render() {
const loadedPluginConfigs = plugins.getLoadedConfigs();

if (loadedPluginConfigs.length) {
const {name: pointName, children: reportComponent, ...componentProps} = this.props;
const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, pointName);
return getComponentsComposition(pluginComponents, reportComponent, componentProps);
const {name: pointName, children: reportComponent, ...componentProps} = this.props;
const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, pointName);

const style = pointName === ExtensionPointName.RunTestOptions ?
{display: 'flex', gap: '12px', flexDirection: 'column'} : {}
;

if (pluginComponents.length) {
return (
<div style={style}>
{getComponentsComposition(pluginComponents, reportComponent, componentProps)}
</div>
);
}

return this.props.children;
Expand Down Expand Up @@ -60,26 +72,30 @@ function composeComponents(PluginComponent, pluginProps, currentComponent, posit
}
}

const defaultComponents = [
TestRepeaterComponent
];

export function getExtensionPointComponents(loadedPluginConfigs, pointName) {
return loadedPluginConfigs
.map(config => {
try {
const PluginComponent = plugins.get(config.name, config.component);
return {
PluginComponent,
name,
point: getComponentPoint(PluginComponent, config),
position: getComponentPosition(PluginComponent, config),
config
};
} catch (err) {
console.error(err);
return {};
}
})
.filter(({point, position}) => {
return point && position && point === pointName;
});
return [
...defaultComponents,
...loadedPluginConfigs
.map(config => {
try {
const PluginComponent = plugins.get(config.name, config.component);
return {
PluginComponent,
name,
point: getComponentPoint(PluginComponent, config),
position: getComponentPosition(PluginComponent, config),
config
};
} catch (err) {
console.error(err);
return {};
}
})
].filter(({point, position}) => (point && position && point === pointName));
}

function getComponentPoint(component, config) {
Expand Down
11 changes: 11 additions & 0 deletions lib/static/components/test-repeater/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.test-repeater-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}

.test-repeater-input {
display: flex;
flex-direction: row;
}
53 changes: 53 additions & 0 deletions lib/static/components/test-repeater/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {NumberInput, Button, Icon} from '@gravity-ui/uikit';
import {Plus, Minus} from '@gravity-ui/icons';
import styles from './index.module.css';
import {setRepeatCount} from '@/static/modules/actions';
import {ExtensionPointName} from '@/static/new-ui/constants/plugins';

const MAX_REPEATER_COUNT = 99;
const MIN_REPEATER_COUNT = 1;

const PluginComponent = (): React.ReactNode => {
const dispatch = useDispatch();
const repeatCount = useSelector((state) => state.repeatCount);

const changeRepeatCount = (newValue: number): void => {
if (newValue >= MIN_REPEATER_COUNT && newValue < MAX_REPEATER_COUNT) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Permit the declared maximum repeat count

The guard uses newValue < MAX_REPEATER_COUNT, so MAX_REPEATER_COUNT itself (99) is unreachable from both typing and the increment button. This is an off-by-one bug: users can only set up to 98 even though the constant declares 99 as the limit.

Useful? React with 👍 / 👎.

dispatch(setRepeatCount(newValue));
}
};

return (
<div className={styles.testRepeaterContainer}>
<span className="text-header-1">Number of repeats</span>
<NumberInput
size='m'
value={repeatCount}
style={{maxWidth: '78px'}}
onChange={(e): void => changeRepeatCount(parseInt(e.target.value, 10))}
qa='repeat-count'
hiddenControls
endContent={(
<div className={styles.testRepeaterInput}>
<Button view="flat" size="s" onClick={(): void => changeRepeatCount(repeatCount - 1)}>
<Icon data={Minus} size={14}/>
</Button>
<Button view="flat" size="s" onClick={(): void => changeRepeatCount(repeatCount + 1)}>
<Icon data={Plus} size={14}/>
</Button>
</div>
)}
/>
</div>
);
};

export const TestRepeaterComponent = {
PluginComponent,
name: 'Test Repeater',
position: 'after',
point: ExtensionPointName.RunTestOptions,
config: {}
};
1 change: 1 addition & 0 deletions lib/static/modules/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
FIN_STATIC_REPORT: 'FIN_STATIC_REPORT',
RUN_ALL_TESTS: 'RUN_ALL_TESTS',
RUN_FAILED_TESTS: 'RUN_FAILED_TESTS',
SET_REPEAT_COUNT: 'SET_REPEAT_COUNT',
STOP_TESTS: 'STOP_TESTS',
RETRY_SUITE: 'RETRY_SUITE',
RETRY_TEST: 'RETRY_TEST',
Expand Down
7 changes: 5 additions & 2 deletions lib/static/modules/actions/run-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import {TestStatus} from '@/constants';

export type RunTestAction = Action<typeof actionNames.RETRY_TEST>;
export const runTest = (): RunTestAction => ({type: actionNames.RETRY_TEST});
export const setRepeatCount = (repeatCount: number): Action<typeof actionNames.SET_REPEAT_COUNT, {repeatCount: number}> => ({type: actionNames.SET_REPEAT_COUNT, payload: {repeatCount}});

export const thunkRunTests = ({tests = []}: {tests?: TestSpec[]} = {}): AppThunk => {
return async (dispatch) => {
return async (dispatch, getState) => {
const {repeatCount} = getState();

dispatch(runTest());
try {
await axios.post('/run', tests);
await axios.post('/run', {tests, repeatCount});
} catch (e) {
// TODO: report error via notifications
console.error('Error while running tests:', e);
Expand Down
1 change: 1 addition & 0 deletions lib/static/modules/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {MIN_SECTION_SIZE_PERCENT} from '../new-ui/features/suites/constants';
export default Object.assign({config: configDefaults}, {
gui: true,
running: false,
repeatCount: 1,
processing: false,
stopping: false,
autoRun: false,
Expand Down
4 changes: 4 additions & 0 deletions lib/static/modules/reducers/running.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {initSearch} from '@/static/modules/search';

export default (state, action) => {
switch (action.type) {
case actionNames.SET_REPEAT_COUNT: {
return {...state, repeatCount: action.payload.repeatCount};
}
case actionNames.TEST_BEGIN:
case actionNames.RUN_ALL_TESTS:
case actionNames.RUN_FAILED_TESTS:
case actionNames.RETRY_SUITE:
Expand Down
6 changes: 6 additions & 0 deletions lib/static/new-ui/components/RunTest/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
padding-right: 4px;
}

.repeat-count {
display: flex;
align-items: center;
opacity: 0.5;
}

.run-options-button::before {
border-left: none;
}
Expand Down
9 changes: 7 additions & 2 deletions lib/static/new-ui/components/RunTest/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {forwardRef, ReactNode, useCallback, useState} from 'react';

import styles from './index.module.css';
import {Button, ButtonProps, Icon, Popover, Spin} from '@gravity-ui/uikit';
import {ArrowRotateRight, ChevronDown} from '@gravity-ui/icons';
import {ArrowRotateRight, ChevronDown, Xmark} from '@gravity-ui/icons';
import {thunkRunTest} from '@/static/modules/actions';
import {useDispatch, useSelector} from 'react-redux';
import {RunTestsFeature} from '@/constants';
Expand All @@ -24,6 +24,7 @@ interface RunTestProps {
export const RunTestButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, RunTestProps>(
({browser, buttonProps, buttonText, hotkey}, ref) => {
const isRunning = useSelector(state => state.running);
const repeatCount = useSelector(state => state.repeatCount);

const analytics = useAnalytics();
const dispatch = useDispatch();
Expand Down Expand Up @@ -57,9 +58,12 @@ export const RunTestButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, R
disabled={isRunning}
style={{width: buttonText === null ? '28px' : undefined}}
pin={hasRunTestOptions ? 'round-brick' : undefined}
qa='run-test'
{...buttonProps}
>
{isRunning ? <Spin size={'xs'} /> : <Icon data={ArrowRotateRight}/>}{buttonText === undefined ? 'Retry' : buttonText}{hotkey}
{isRunning ? <Spin size={'xs'} /> : <Icon data={ArrowRotateRight}/>}{buttonText === undefined ? 'Retry' : buttonText}
{(!isRunning && repeatCount > 1) && <span className={styles.repeatCount}><Icon data={Xmark} size={12}/>{repeatCount}</span>}
{hotkey}
</Button>
{hasRunTestOptions && <Popover
onOpenChange={onRunOptionsOpenChange}
Expand All @@ -73,6 +77,7 @@ export const RunTestButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, R
className={classNames(styles.retryButton, styles.runOptionsButton)}
style={{width: buttonText === null ? '28px' : undefined}}
pin='brick-round'
qa='run-test-options'
{...buttonProps}
>
<Icon data={ChevronDown} className={classNames(styles.runOptionsButtonIcon, {[styles.runOptionsButtonIconRotated]: isRunOptionsOpen})}/>
Expand Down
3 changes: 3 additions & 0 deletions lib/static/new-ui/components/TreeActionsToolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi
const dispatch = useDispatch();
const analytics = useAnalytics();

const repeatCount = useSelector(state => state.repeatCount);
const rootSuiteIds = useSelector(state => state.tree.suites.allRootIds);
const suitesStateById = useSelector(state => state.tree.suites.stateById);
const browsersStateById = useSelector(state => state.tree.browsers.stateById);
Expand Down Expand Up @@ -212,11 +213,13 @@ export function TreeActionsToolbar({onHighlightCurrentTest, className}: TreeActi
trigger='click'
>
<IconButton
selected={repeatCount > 1}
view='flat'
disabled={isRunning || !isInitialized}
className={classNames(styles.iconButton)}
icon={<Icon data={GearPlay} height={14}/>}
tooltip='View run options'
qa="tree-run-test-options"
/>
</Popover>}
{isEditScreensAvailable && (
Expand Down
1 change: 1 addition & 0 deletions lib/static/new-ui/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export interface State {
baseHost: string;
};
running: boolean;
repeatCount: number;
processing: boolean;
gui: boolean;
apiValues: HtmlReporterValues;
Expand Down
Loading
Loading