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
2 changes: 2 additions & 0 deletions packages/bruno-common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ export {
BRUNO_VARIABLE_DATATYPES,
isBrunoVariableDataType
} from './datatype';

export const TIMEOUT_INHERIT = 'inherit' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { fromOpenCollectionVariables, toOpenCollectionVariables } from './variab
export { fromOpenCollectionActions, toOpenCollectionActions } from './actions';
export { fromOpenCollectionScripts, toOpenCollectionScripts } from './scripts';
export { fromOpenCollectionAssertions, toOpenCollectionAssertions } from './assertions';
export { TIMEOUT_INHERIT } from '@usebruno/common/utils';
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
fromOpenCollectionActions,
toOpenCollectionActions,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
toOpenCollectionAssertions,
TIMEOUT_INHERIT
} from '../common';
import type {
GraphQLRequest,
Expand Down Expand Up @@ -179,7 +180,7 @@ export const toOpenCollectionGraphqlItem = (item: BrunoItem): GraphQLRequest =>

const settings: GraphQLRequestSettings = {
encodeUrl: typeof brunoSettings.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings.timeout === 'number' ? brunoSettings.timeout : 0,
timeout: typeof brunoSettings.timeout === 'number' || brunoSettings.timeout === TIMEOUT_INHERIT ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
};
Expand Down
7 changes: 4 additions & 3 deletions packages/bruno-converters/src/opencollection/items/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
fromOpenCollectionActions,
toOpenCollectionActions,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
toOpenCollectionAssertions,
TIMEOUT_INHERIT
} from '../common';
import type {
HttpRequest,
Expand Down Expand Up @@ -105,7 +106,7 @@ export const fromOpenCollectionHttpItem = (ocRequest: HttpRequest): BrunoItem =>
if (ocRequest.settings) {
const settings: BrunoHttpItemSettings = {
encodeUrl: typeof ocRequest.settings.encodeUrl === 'boolean' ? ocRequest.settings.encodeUrl : true,
timeout: typeof ocRequest.settings.timeout === 'number' ? ocRequest.settings.timeout : 0,
timeout: typeof ocRequest.settings.timeout === 'number' || ocRequest.settings.timeout === TIMEOUT_INHERIT ? ocRequest.settings.timeout : 0,
followRedirects: typeof ocRequest.settings.followRedirects === 'boolean' ? ocRequest.settings.followRedirects : true,
maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5
};
Expand Down Expand Up @@ -221,7 +222,7 @@ export const toOpenCollectionHttpItem = (item: BrunoItem): HttpRequest => {

const settings: HttpRequestSettings = {
encodeUrl: typeof brunoSettings?.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings?.timeout === 'number' ? brunoSettings.timeout : 0,
timeout: typeof brunoSettings?.timeout === 'number' || brunoSettings?.timeout === TIMEOUT_INHERIT ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings?.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { toOpenCollectionVariables } from '../common/variables';
import { toOpenCollectionActions } from '../common/actions';
import { toOpenCollectionScripts } from '../common/scripts';
import { toOpenCollectionAssertions } from '../common/assertions';
import { TIMEOUT_INHERIT } from '@usebruno/common/utils';

const stringifyGraphQLRequest = (item: BrunoItem): string => {
try {
Expand Down Expand Up @@ -130,7 +131,8 @@ const stringifyGraphQLRequest = (item: BrunoItem): string => {
}

const timeout = httpSettings?.timeout;
if (isNumber(timeout)) {

if (isNumber(timeout) || timeout === TIMEOUT_INHERIT) {
settings.timeout = timeout;
} else {
settings.timeout = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { toOpenCollectionActions } from '../common/actions';
import { toOpenCollectionScripts } from '../common/scripts';
import { toOpenCollectionAssertions } from '../common/assertions';
import { isNumber, isNonEmptyString } from '../../../utils';
import { TIMEOUT_INHERIT } from '@usebruno/common/utils';

const stringifyHttpRequest = (item: BrunoItem): string => {
try {
Expand Down Expand Up @@ -118,7 +119,7 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
}

const timeout = httpSettings?.timeout;
if (isNumber(timeout)) {
if (isNumber(timeout) || timeout === TIMEOUT_INHERIT) {
settings.timeout = timeout;
} else {
settings.timeout = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
opencollection: "1.0.0"

info:
name: settings-yaml

bundled: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
info:
name: timeout-test-yaml
type: http
seq: 1

http:
method: GET
url: https://testbench-sanity.usebruno.com/redirect-to-ping
auth: inherit

settings:
followRedirects: false
maxRedirects: 0
timeout: 5
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/request/settings/collection",
"path": "{{projectRoot}}/tests/request/settings/collection/requests-settings-bru",
"securityConfig": {
"jsSandboxMode": "safe"
}
},
{
"path": "{{projectRoot}}/tests/request/settings/collection/requests-settings-yml",
"securityConfig": {
"jsSandboxMode": "safe"
}
Expand Down
3 changes: 2 additions & 1 deletion tests/request/settings/init-user-data/preferences.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/request/settings/collection"
"{{projectRoot}}/tests/request/settings/collection/requests-settings-bru",
"{{projectRoot}}/tests/request/settings/collection/requests-settings-yml"
],
"preferences": {
"onboarding": {
Expand Down
206 changes: 168 additions & 38 deletions tests/request/settings/timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,186 @@
import { test, expect } from '../../../playwright';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { expect, Page, test } from '../../../playwright';
import { closeAllCollections, selectRequestPaneTab } from '../../utils/page';

const bruRequestPath = path.join(__dirname, 'collection', 'requests-settings-bru', 'timeout.bru');
const yamlRequestPath = path.join(__dirname, 'collection', 'requests-settings-yml', 'timeout.yml');

const setGlobalRequestTimeout = async (page: Page, value: string) => {
// Open preferences tab

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

redundant comment. please remove this

await page.locator('.status-bar button[data-trigger="preferences"]').click();

// Navigate to General tab (default, but ensure it)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as above

const generalTab = page.getByRole('tab', { name: 'General' });
await expect(generalTab).toBeVisible({ timeout: 10000 });
await generalTab.click();

// Update the Request Timeout (in ms) preference
const timeoutPreference = page.locator('input[name="timeout"]');
await expect(timeoutPreference).toBeVisible({ timeout: 10000 });
await timeoutPreference.fill(value);
await expect(timeoutPreference).toHaveValue(value, { timeout: 5000 });

const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });
await preferencesTab.hover();
await preferencesTab.locator('.close-icon').click({ force: true });
await expect(preferencesTab).not.toBeVisible({ timeout: 10000 });
};

test.describe('Timeout Settings Tests', () => {
test('should configure and test timeout settings', async ({
pageWithUserData: page
}) => {
// Navigate to the test collection and request
await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();
test.afterEach(async ({ pageWithUserData: page }) => {
await closeAllCollections(page);
});

test.afterAll(() => {
execSync(`git checkout -- "${bruRequestPath}" "${yamlRequestPath}"`);
});

test.describe('bru request timeout settings', () => {
test('should configure and test timeout settings', async ({
pageWithUserData: page
}) => {
// Navigate to the test collection and request
await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();

await page.locator('#sidebar-collection-name').getByText('settings-test').click();
// Navigate to thetimeout request
await page.getByRole('complementary').getByText('timeout-test').click();

// Go to Settings tab
await selectRequestPaneTab(page, 'Settings');

// Test Timeout Settings with custom value
const timeoutInput = page.locator('input[id="timeout"]');
await expect(timeoutInput).toBeVisible();

await page.locator('#sidebar-collection-name').getByText('settings-test').click();
// Navigate to thetimeout request
await page.getByRole('complementary').getByText('timeout-test').click();
// Verify default value from .bru file (5)
await expect(timeoutInput).toHaveValue('5');

// Go to Settings tab
await selectRequestPaneTab(page, 'Settings');
await page.getByTestId('send-arrow-icon').click();

// Test Timeout Settings with custom value
const timeoutInput = page.locator('input[id="timeout"]');
await expect(timeoutInput).toBeVisible();
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('timeout of 5ms exceeded');

// Verify default value from .bru file (5)
await expect(timeoutInput).toHaveValue('5');
// Change the global request timeout preference that "inherit" should fall back to
await setGlobalRequestTimeout(page, '10');

await page.getByTestId('send-arrow-icon').click();
// Return to the request and Settings tab
await page.getByRole('complementary').getByText('timeout-test').click();
await selectRequestPaneTab(page, 'Settings');

const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('timeout of 5ms exceeded');
// Now test inherit functionality
// Click the X button to reset to inherit
const resetButton = page.locator('button[title="Reset to inherit"]');
await expect(resetButton).toBeVisible();
await resetButton.click();

// Now test inherit functionality
// Click the X button to reset to inherit
const resetButton = page.locator('button[title="Reset to inherit"]');
await expect(resetButton).toBeVisible();
await resetButton.click();
// After reset, should see "Inherit" button instead of input
const inheritButton = page.locator('button:has-text("Inherit")');
await expect(inheritButton).toBeVisible();
await expect(timeoutInput).not.toBeVisible();

// After reset, should see "Inherit" button instead of input
const inheritButton = page.locator('button:has-text("Inherit")');
await expect(inheritButton).toBeVisible();
await expect(timeoutInput).not.toBeVisible();
// Save the request so the inherit setting is serialized to the .bru file
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible();

// Run the request with inherit timeout
await page.getByTestId('send-arrow-icon').click();
// Verify persistence: the serialized file must keep timeout: inherit (not reset to a custom value)
const savedContent = fs.readFileSync(bruRequestPath, 'utf-8');
expect(savedContent).toMatch(/timeout:\s*['"]?inherit['"]?/);

// Verify the request runs successfully with inherited timeout (should not timeout)
await expect(responsePane).toContainText('302');
// Reopen the request to confirm the inherit state persists in the Settings UI
const requestTab = page.locator('.request-tab').filter({ hasText: 'timeout-test' });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });

// Close without saving to avoid modifying the .bru file
await page.locator('.close-icon-container').click({ force: true });
await page.locator('button:has-text("Don\'t Save")').first().click();
await page.getByRole('complementary').getByText('timeout-test').click();
await selectRequestPaneTab(page, 'Settings');

// Settings UI should still show Inherit (not a custom value) after reopening
await expect(inheritButton).toBeVisible();
await expect(timeoutInput).not.toBeVisible();

// Run the request with the inherited timeout
await page.getByTestId('send-arrow-icon').click();

// Verify the inherited timeout resolves to the global preference (10ms), not the file value (5ms)
await expect(responsePane).toContainText('timeout of 10ms exceeded', { timeout: 15000 });
});
});

test.afterEach(async ({ pageWithUserData: page }) => {
// cleanup: close all collections
await closeAllCollections(page);
test.describe('yaml request timeout settings', () => {
test('should configure and test timeout settings for yaml request', async ({
pageWithUserData: page
}) => {
// Navigate to the yaml test collection and request
await expect(page.locator('#sidebar-collection-name').getByText('settings-yaml')).toBeVisible();

await page.locator('#sidebar-collection-name').getByText('settings-yaml').click();
// Navigate to the timeout request
await page.getByRole('complementary').getByText('timeout-test-yaml').click();

// Go to Settings tab
await selectRequestPaneTab(page, 'Settings');

// Test Timeout Settings with custom value
const timeoutInput = page.locator('input[id="timeout"]');

@sachin-thakur-bruno sachin-thakur-bruno Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: we can use page.locator('#timeout'); directly

await expect(timeoutInput).toBeVisible();

// Verify default value from .yml file (5)
await expect(timeoutInput).toHaveValue('5');

await page.getByTestId('send-arrow-icon').click();

// Verify the custom timeout (5ms) is applied
const responsePane = page.locator('.response-pane');
await expect(responsePane).toContainText('timeout of 5ms exceeded');

// Change the global request timeout preference that "inherit" should fall back to
await setGlobalRequestTimeout(page, '10');

// Return to the request and Settings tab
await page.getByRole('complementary').getByText('timeout-test-yaml').click();
await selectRequestPaneTab(page, 'Settings');

// Now test inherit functionality
// Click the X button to reset to inherit
const resetButton = page.locator('button[title="Reset to inherit"]');
await expect(resetButton).toBeVisible();
await resetButton.click();

// After reset, should see "Inherit" button instead of input
const inheritButton = page.locator('button:has-text("Inherit")');
await expect(inheritButton).toBeVisible();
await expect(timeoutInput).not.toBeVisible();

// Save the request so the inherit setting is serialized to the .yml file
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible();

// Verify YAML persistence: the serialized file must keep timeout: inherit (not reset to 0)
const savedContent = fs.readFileSync(yamlRequestPath, 'utf-8');
expect(savedContent).toMatch(/timeout:\s*['"]?inherit['"]?/);

// Reopen the request to confirm the inherit state persists in the Settings UI
const requestTab = page.locator('.request-tab').filter({ hasText: 'timeout-test-yaml' });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });

await page.getByRole('complementary').getByText('timeout-test-yaml').click();
await selectRequestPaneTab(page, 'Settings');

// Settings UI should still show Inherit (not a custom value) after reopening
await expect(inheritButton).toBeVisible();
await expect(timeoutInput).not.toBeVisible();

// Run the request with the inherited timeout
await page.getByTestId('send-arrow-icon').click();

// Verify the inherited timeout resolves to the global preference (10ms), not the file value (5ms)
await expect(responsePane).toContainText('timeout of 10ms exceeded', { timeout: 15000 });
});
});
});
Loading