-
- Included tags:
- handleAddTag({ tag, to: 'include' })}
- handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'include' })}
- tagsHintList={tagsHintList}
- handleValidation={handleValidation}
- />
-
-
- Excluded tags:
- handleAddTag({ tag, to: 'exclude' })}
- handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'exclude' })}
- tagsHintList={tagsHintList}
- handleValidation={handleValidation}
- />
-
+
+
+
+ Include tags
+ handleAddTag({ tag, to: 'include' })}
+ handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'include' })}
+ tagsHintList={tagsHintList}
+ handleValidation={handleValidation}
+ />
+
+
+ Exclude tags
+ handleAddTag({ tag, to: 'exclude' })}
+ handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'exclude' })}
+ tagsHintList={tagsHintList}
+ handleValidation={handleValidation}
+ />
- )}
+
);
};
diff --git a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
index b9750b23bcd..18fb3ecf09f 100644
--- a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
@@ -3,18 +3,48 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.textbox {
padding: 0.2rem 0.5rem;
- box-shadow: none;
- border-radius: 0px;
outline: none;
- box-shadow: none;
- transition: border-color ease-in-out 0.1s;
- border-radius: 3px;
+ font-size: ${(props) => props.theme.font.size.sm};
+ border-radius: ${(props) => props.theme.border.radius.sm};
background-color: ${(props) => props.theme.input.bg};
border: 1px solid ${(props) => props.theme.input.border};
+ height: 1.875rem;
&:focus {
- border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
- outline: none !important;
+ outline: none;
+ border-color: ${(props) => props.theme.input.focusBorder};
+ }
+
+ &[type='number'] {
+ -moz-appearance: textfield;
+ appearance: textfield;
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+ }
+
+ /* Radio button styles */
+ input[type='radio'] {
+ cursor: pointer;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 1px solid ${(props) => props.theme.input.border};
+ background-color: ${(props) => props.theme.bg};
+ flex-shrink: 0;
+
+ &:focus-visible {
+ outline: 2px solid ${(props) => props.theme.input.focusBorder};
+ outline-offset: 2px;
+ }
+
+ &:checked {
+ border: 1px solid ${(props) => props.theme.primary.solid};
+ background-image: radial-gradient(circle, ${(props) => props.theme.primary.solid} 40%, ${(props) => props.theme.bg} 42%);
}
}
@@ -78,6 +108,43 @@ const Wrapper = styled.div`
border-color: ${(props) => props.theme.background.surface1};
}
+ .runner-section-title {
+ font-size: ${(props) => props.theme.font.size.sm};
+ font-weight: 600;
+ }
+
+ .runner-section {
+ font-size: ${(props) => props.theme.font.size.sm};
+
+ div:has(> .single-line-editor) {
+ height: 1.875rem;
+ border: 1px solid ${(props) => props.theme.input.border};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ background-color: ${(props) => props.theme.input.bg};
+ padding: 0.2rem 0.5rem;
+ }
+
+ div:has(> .single-line-editor):focus-within {
+ border-color: ${(props) => props.theme.input.focusBorder};
+ }
+
+ .single-line-editor {
+ height: 1.475rem;
+ font-size: ${(props) => props.theme.font.size.sm};
+
+ .CodeMirror {
+ height: 1.475rem;
+ line-height: 1.475rem;
+ }
+
+ .CodeMirror-cursor {
+ height: 0.875rem !important;
+ margin-top: 0.3rem !important;
+ }
+ }
+ }
+
+
.filter-bar {
display: flex;
align-items: stretch;
diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx
index 62255514560..f915ed1cf54 100644
--- a/packages/bruno-app/src/components/RunnerResults/index.jsx
+++ b/packages/bruno-app/src/components/RunnerResults/index.jsx
@@ -3,8 +3,8 @@ import path from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { get, cloneDeep } from 'lodash';
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
-import { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';
-import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections';
+import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
+import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
@@ -81,7 +81,6 @@ export default function RunnerResults({ collection }) {
const [delay, setDelay] = useState(null);
const [activeFilter, setActiveFilter] = useState('all');
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
- const [configureMode, setConfigureMode] = useState(false);
// ref for the runner output body
const runnerBodyRef = useRef();
@@ -91,16 +90,9 @@ export default function RunnerResults({ collection }) {
// tags for the collection run
const tags = get(collection, 'runnerTags', { include: [], exclude: [] });
- // have tags been enabled for the collection run
- const tagsEnabled = get(collection, 'runnerTagsEnabled', false);
-
// have tags been added for the collection run
const areTagsAdded = tags.include.length > 0 || tags.exclude.length > 0;
- const requestItemsForCollectionRun = getRequestItemsForCollectionRun({ recursive: true, tags, items: collection.items });
- const totalRequestItemsCountForCollectionRun = requestItemsForCollectionRun.length;
- const shouldDisableCollectionRun = totalRequestItemsCountForCollectionRun <= 0;
-
const items = cloneDeep(get(collection, 'runnerResult.items', []))
.map((item) => {
const info = findItemInCollection(collectionCopy, item.uid);
@@ -164,24 +156,14 @@ export default function RunnerResults({ collection }) {
}
}, [filteredItems]);
- useEffect(() => {
- const runnerInfo = get(collection, 'runnerResult.info', {});
- if (runnerInfo.status === 'running') {
- setConfigureMode(false);
- }
- }, [collection.runnerResult]);
-
useEffect(() => {
const savedConfiguration = get(collection, 'runnerConfiguration', null);
if (savedConfiguration) {
- if (savedConfiguration.selectedRequestItems && configureMode) {
- setSelectedRequestItems(savedConfiguration.selectedRequestItems);
- }
if (savedConfiguration.delay !== undefined && delay === null) {
setDelay(savedConfiguration.delay);
}
}
- }, [collection.runnerConfiguration, configureMode, delay]);
+ }, [collection.runnerConfiguration, delay]);
const ensureCollectionIsMounted = () => {
if (collection.mountStatus === 'mounted') {
@@ -195,13 +177,9 @@ export default function RunnerResults({ collection }) {
};
const runCollection = () => {
- if (configureMode && selectedRequestItems.length > 0) {
- dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems, delay));
- dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));
- } else {
- dispatch(updateRunnerConfiguration(collection.uid, [], [], delay));
- dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
- }
+ const savedOrder = get(collection, 'runnerConfiguration.requestItemsOrder', selectedRequestItems);
+ dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, savedOrder, delay));
+ dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tags, selectedRequestItems));
};
const runAgain = () => {
@@ -216,7 +194,7 @@ export default function RunnerResults({ collection }) {
runnerInfo.folderUid,
true,
Number(savedDelay),
- tagsEnabled && tags,
+ tags,
savedSelectedItems
)
);
@@ -228,8 +206,6 @@ export default function RunnerResults({ collection }) {
collectionUid: collection.uid
})
);
- setSelectedRequestItems([]);
- setConfigureMode(false);
setDelay(null);
};
@@ -237,17 +213,6 @@ export default function RunnerResults({ collection }) {
dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));
};
- const toggleConfigureMode = () => {
- dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false }));
- setConfigureMode(!configureMode);
- };
-
- useEffect(() => {
- if (tagsEnabled) {
- setConfigureMode(false);
- }
- }, [tagsEnabled]);
-
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
const filterCounts = {
all: items.length,
@@ -261,13 +226,13 @@ export default function RunnerResults({ collection }) {
return (
-
+
+
Runner
-
-
- You have
{totalRequestsInCollection} requests in this collection.
+
+ You have {totalRequestsInCollection} {totalRequestsInCollection === 1 ? 'request' : 'requests'} in this collection.
{isCollectionLoading && (
(Loading...)
@@ -275,47 +240,40 @@ export default function RunnerResults({ collection }) {
)}
{isCollectionLoading ?
Requests in this collection are still loading.
: null}
-
-
+
+ {/* Timings */}
+
Timings
+
+
setDelay(e.target.value)}
/>
- {/* Tags for the collection run */}
-
-
- {/* Configure requests option */}
-
-
-
-
-
+ {/* Filters */}
+
Filters
+
+ {/* Tags for the collection run */}
+
- {configureMode && (
-
-
-
- )}
+
+
+
);
@@ -399,7 +356,7 @@ export default function RunnerResults({ collection }) {
- {tagsEnabled && areTagsAdded && (
+ {areTagsAdded && (
Tags:
@@ -457,7 +414,7 @@ export default function RunnerResults({ collection }) {
)}
- {tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
+ {areTagsAdded && item?.tags?.length > 0 && (
Tags: {item.tags.filter((t) => tags.include.includes(t)).join(', ')}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
index e7dd94d2fc2..0f308b0baed 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
@@ -4,9 +4,93 @@ const Wrapper = styled.div`
.bruno-modal-content {
padding-bottom: 1rem;
}
+
+ .description {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+
+ .divider {
+ border: none;
+ border-top: 1px solid ${(props) => props.theme.input.border};
+ margin: 1rem 0rem;
+ }
+
.warning {
color: ${(props) => props.theme.colors.text.danger};
}
+
+ .textbox {
+ padding: 0.2rem 0.5rem;
+ outline: none;
+ font-size: ${(props) => props.theme.font.size.sm};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ background-color: ${(props) => props.theme.input.bg};
+ border: 1px solid ${(props) => props.theme.input.border};
+ height: 1.875rem;
+
+ &:focus {
+ outline: none;
+ border-color: ${(props) => props.theme.input.focusBorder};
+ }
+
+ &[type='number'] {
+ -moz-appearance: textfield;
+ appearance: textfield;
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+ }
+
+ div:has(> .single-line-editor) {
+ height: 1.875rem;
+ border: 1px solid ${(props) => props.theme.input.border};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ background-color: ${(props) => props.theme.input.bg};
+ padding: 0.2rem 0.5rem;
+ }
+
+ div:has(> .single-line-editor):focus-within {
+ border-color: ${(props) => props.theme.input.focusBorder};
+ }
+
+ .single-line-editor {
+ height: 1.475rem;
+ font-size: ${(props) => props.theme.font.size.sm};
+
+ .CodeMirror {
+ height: 1.475rem;
+ line-height: 1.475rem;
+ }
+
+ .CodeMirror-cursor {
+ height: 0.875rem !important;
+ margin-top: 0.3rem !important;
+ }
+ }
+
+ input[type='radio'] {
+ cursor: pointer;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 1px solid ${(props) => props.theme.input.border};
+ background-color: ${(props) => props.theme.bg};
+ flex-shrink: 0;
+
+ &:focus-visible {
+ outline: 2px solid ${(props) => props.theme.input.focusBorder};
+ outline-offset: 2px;
+ }
+
+ &:checked {
+ border: 1px solid ${(props) => props.theme.primary.solid};
+ background-image: radial-gradient(circle, ${(props) => props.theme.primary.solid} 40%, ${(props) => props.theme.bg} 42%);
+ }
+ }
`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
index af0e0eea836..a96afcaae91 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import get from 'lodash/get';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
@@ -14,6 +14,7 @@ import Button from 'ui/Button';
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
+ const [delay, setDelay] = useState('');
const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
@@ -21,9 +22,6 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
// tags for the collection run
const tags = get(collection, 'runnerTags', { include: [], exclude: [] });
- // have tags been enabled for the collection run
- const tagsEnabled = get(collection, 'runnerTagsEnabled', false);
-
const onSubmit = (recursive) => {
dispatch(
addTab({
@@ -33,7 +31,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
})
);
if (!isCollectionRunInProgress) {
- dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, 0, tagsEnabled && tags));
+ dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, delay ? Number(delay) : null, tags));
}
onClose();
};
@@ -68,15 +66,34 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
Run
({totalRequestItemsCountForFolderRun} requests)
-
This will only run the requests in this folder.
+
This will only run the requests in this folder.
Recursive Run
({totalRequestItemsCountForRecursiveFolderRun} requests)
-
This will run all the requests in this folder and all its subfolders.
+
This will run all the requests in this folder and all its subfolders.
{isFolderLoading ?
Requests in this folder are still loading.
: null}
{isCollectionRunInProgress ?
A Collection Run is already in progress.
: null}
+
+
+ {/* Timings */}
+
+
+ setDelay(e.target.value)}
+ />
+
+
{/* Tags for the collection run */}
diff --git a/packages/bruno-app/src/components/TagList/StyledWrapper.js b/packages/bruno-app/src/components/TagList/StyledWrapper.js
index efb5daacd06..603a716c6ab 100644
--- a/packages/bruno-app/src/components/TagList/StyledWrapper.js
+++ b/packages/bruno-app/src/components/TagList/StyledWrapper.js
@@ -66,14 +66,12 @@ const StyledWrapper = styled.div`
opacity: 0.7;
&:hover {
- background-color: ${(props) => props.theme.danger};
- color: white;
+ color: ${(props) => props.theme.text};
opacity: 1;
- transform: scale(1.1);
}
&:focus-visible {
- outline: 2px solid ${(props) => props.theme.danger};
+ outline: 2px solid ${(props) => props.theme.text};
outline-offset: 1px;
}
}
diff --git a/packages/bruno-app/src/components/TagList/index.js b/packages/bruno-app/src/components/TagList/index.js
index b35af1743a5..4317116dd17 100644
--- a/packages/bruno-app/src/components/TagList/index.js
+++ b/packages/bruno-app/src/components/TagList/index.js
@@ -47,7 +47,7 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav
{
// Wait for the runner tab to open
// If there are existing results, reset first, otherwise wait for Run Collection button
const resetButton = page.getByRole('button', { name: 'Reset' });
- const runCollectionButton = page.getByRole('button', { name: 'Run Collection' });
+ const runCollectionButton = page.getByTestId('runner-run-button');
// Check if Reset button is visible (means there are existing results)
const resetVisible = await resetButton.isVisible().catch(() => false);
diff --git a/tests/runner/runner-configuration.spec.ts b/tests/runner/runner-configuration.spec.ts
new file mode 100644
index 00000000000..b80612c2e3c
--- /dev/null
+++ b/tests/runner/runner-configuration.spec.ts
@@ -0,0 +1,156 @@
+import { test, expect } from '../../playwright';
+import { openRunnerTab, buildRunnerLocators } from '../utils/page/index';
+
+const COLLECTION_NAME = 'bruno-testbench';
+
+/**
+ * Waits for the config panel to finish loading and initializing requests.
+ * On first load, all enabled requests are auto-selected, so we wait until selected === total.
+ */
+const waitForRequestsInitialized = async (locators) => {
+ await expect(async () => {
+ const text = await locators.configCounter().innerText();
+ const match = text.match(/(\d+) of (\d+) selected/);
+ expect(match).toBeTruthy();
+ const selected = parseInt(match![1]);
+ const total = parseInt(match![2]);
+ expect(total).toBeGreaterThan(0);
+ expect(selected).toBe(total);
+ }).toPass({ timeout: 30000 });
+};
+
+test.describe('Runner Configuration Panel', () => {
+ test('should display config panel with all requests selected by default', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+ await waitForRequestsInitialized(locators);
+
+ await test.step('Config panel is visible with request items', async () => {
+ await expect(locators.configPanel()).toBeVisible();
+ const itemCount = await locators.requestItems().count();
+ expect(itemCount).toBeGreaterThan(0);
+ });
+
+ await test.step('Counter shows all enabled requests selected', async () => {
+ const counterText = await locators.configCounter().innerText();
+ const match = counterText.match(/(\d+) of (\d+) selected/);
+ expect(match).toBeTruthy();
+ expect(match![1]).toBe(match![2]);
+ });
+
+ await test.step('Select All button shows "Deselect All" when all selected', async () => {
+ await expect(locators.selectAllButton()).toContainText('Deselect All');
+ });
+ });
+
+ test('should toggle select all / deselect all', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+ await waitForRequestsInitialized(locators);
+
+ await test.step('Click Deselect All', async () => {
+ await locators.selectAllButton().click();
+ await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 });
+ await expect(async () => {
+ const counterText = await locators.configCounter().innerText();
+ expect(counterText).toMatch(/^0 of \d+ selected$/);
+ }).toPass({ timeout: 5000 });
+ });
+
+ await test.step('Click Select All', async () => {
+ await locators.selectAllButton().click();
+ await expect(locators.selectAllButton()).toContainText('Deselect All', { timeout: 10000 });
+ });
+ });
+
+ test('should deselect individual request items', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+ await waitForRequestsInitialized(locators);
+
+ await test.step('Deselect first item and verify count decreases', async () => {
+ const counterText = await locators.configCounter().innerText();
+ const match = counterText.match(/(\d+) of (\d+) selected/);
+ expect(match).toBeTruthy();
+ const initialSelected = parseInt(match![1]);
+ expect(initialSelected).toBeGreaterThan(1);
+
+ // Click the checkbox area of the first request item to deselect it
+ const firstItem = locators.requestItems().first();
+ await firstItem.locator('.checkbox-container').click();
+
+ // Verify count decreased by 1
+ await expect(async () => {
+ const newCounterText = await locators.configCounter().innerText();
+ const newMatch = newCounterText.match(/(\d+) of (\d+) selected/);
+ expect(parseInt(newMatch![1])).toBe(initialSelected - 1);
+ }).toPass({ timeout: 5000 });
+ });
+
+ await test.step('Re-select item to restore state', async () => {
+ const firstItem = locators.requestItems().first();
+ await firstItem.locator('.checkbox-container').click();
+ await waitForRequestsInitialized(locators);
+ });
+ });
+
+ test('should set delay value', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+
+ await test.step('Enter delay value', async () => {
+ const delayInput = locators.delayInput();
+ await expect(delayInput).toBeVisible();
+ await delayInput.fill('500');
+ await expect(delayInput).toHaveValue('500');
+ });
+ });
+
+ test('should reset config panel to defaults', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+ await waitForRequestsInitialized(locators);
+
+ const counterText = await locators.configCounter().innerText();
+ const initialMatch = counterText.match(/(\d+) of (\d+) selected/);
+ const totalEnabled = parseInt(initialMatch![2]);
+
+ await test.step('Deselect all items first', async () => {
+ await locators.selectAllButton().click();
+ await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 });
+ });
+
+ await test.step('Click config reset to restore defaults', async () => {
+ await locators.configResetButton().click();
+
+ // After reset, all enabled items should be re-selected
+ await expect(async () => {
+ const text = await locators.configCounter().innerText();
+ const match = text.match(/(\d+) of (\d+) selected/);
+ expect(match).toBeTruthy();
+ expect(parseInt(match![1])).toBe(totalEnabled);
+ }).toPass({ timeout: 5000 });
+ await expect(locators.selectAllButton()).toContainText('Deselect All');
+ });
+ });
+
+ test('should disable run button when no requests selected', async ({ pageWithUserData: page }) => {
+ const locators = buildRunnerLocators(page);
+ await openRunnerTab(page, COLLECTION_NAME);
+ await waitForRequestsInitialized(locators);
+
+ await test.step('Deselect all and check run button is disabled', async () => {
+ await locators.selectAllButton().click();
+ await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 });
+ const runButton = page.locator('button[type="submit"]');
+ await expect(runButton).toBeDisabled({ timeout: 10000 });
+ });
+
+ await test.step('Select all and check run button is enabled', async () => {
+ await locators.selectAllButton().click();
+ await expect(locators.selectAllButton()).toContainText('Deselect All', { timeout: 10000 });
+ const runButton = page.locator('button[type="submit"]');
+ await expect(runButton).toBeEnabled({ timeout: 10000 });
+ });
+ });
+});
diff --git a/tests/utils/page/runner.ts b/tests/utils/page/runner.ts
index 3131d70b995..edeb762102f 100644
--- a/tests/utils/page/runner.ts
+++ b/tests/utils/page/runner.ts
@@ -12,8 +12,14 @@ export const buildRunnerLocators = (page: Page) => ({
failedButton: () => page.locator('button').filter({ hasText: /^Failed/ }),
skippedButton: () => page.locator('button').filter({ hasText: /^Skipped/ }),
resetButton: () => page.getByRole('button', { name: 'Reset' }),
- runCollectionButton: () => page.getByRole('button', { name: 'Run Collection' }),
- runAgainButton: () => page.getByRole('button', { name: 'Run Again' })
+ runCollectionButton: () => page.getByTestId('runner-run-button'),
+ runAgainButton: () => page.getByRole('button', { name: 'Run Again' }),
+ configPanel: () => page.getByTestId('runner-config-panel'),
+ configCounter: () => page.getByTestId('runner-config-counter'),
+ selectAllButton: () => page.getByTestId('runner-select-all'),
+ configResetButton: () => page.getByTestId('runner-config-reset'),
+ requestItems: () => page.getByTestId('runner-request-item'),
+ delayInput: () => page.getByTestId('runner-delay-input')
});
/**
@@ -32,6 +38,35 @@ export const getRunnerResultCounts = async (page: Page) => {
return { totalRequests, passed, failed, skipped };
};
+/**
+ * Opens the runner tab for a collection without starting a run
+ * @param page - The Playwright page object
+ * @param collectionName - The name of the collection to open the runner for
+ * @returns void
+ */
+export const openRunnerTab = async (page: Page, collectionName: string) => {
+ await test.step(`Open runner tab for "${collectionName}"`, async () => {
+ const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName });
+ await collectionContainer.waitFor({ state: 'visible' });
+
+ const actionsContainer = collectionContainer.locator('.collection-actions');
+ await collectionContainer.hover();
+ await actionsContainer.waitFor({ state: 'visible' });
+
+ const icon = actionsContainer.locator('.icon');
+ await icon.waitFor({ state: 'visible', timeout: 5000 });
+ await icon.click();
+
+ const runMenuItem = page.getByText('Run', { exact: true });
+ await runMenuItem.waitFor({ state: 'visible' });
+ await runMenuItem.click();
+
+ // Wait for the config panel to load
+ const locators = buildRunnerLocators(page);
+ await locators.configPanel().waitFor({ state: 'visible', timeout: 10000 });
+ });
+};
+
/**
* Runs a collection by clicking the Run menu item and handling the runner tab
* Includes logic to reset existing results if present