Files
bruno/tests/utils/page/locators.ts
sanish chirayath 646c90819d feat: enhance ScriptError with source context and remove auto-commenting of untranslated pm commands (#7449)
* feat: enhance ScriptError with source context, code snippets, and navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: remove auto-commenting of untranslated pm commands during import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update CodeSnippet styles to use theme colors for error and warning highlights

* fix: remove unused SCRIPT_TYPES import from network IPC module

* refactor: remove unused functions and clean up source-context utility

- Removed `getUnifiedScriptContext`, `getWarningSourceGroups`, and related helper functions from `source-context.js` to streamline the utility.
- Updated tests in `source-context.spec.js` to reflect the removal of unused functions, ensuring only relevant tests for `findLineInSource` and `getScriptContext` remain.

* refactor: simplify ScriptError component and update styles

* refactor: streamline tab management in Script components

* refactor: enhance tab management in ScriptError and add testsMetadata handling in prepare-request

* refactor: improve error source identification in ScriptError component

- Enhanced the logic for determining error source types by introducing separate checks for folder and collection files.
- Updated the handling of folder file names to ensure accurate UID retrieval and labeling.
- Streamlined the overall structure of the getErrorSourceInfo function for better readability and maintainability.

* refactor: improve ScriptError component and enhance styling

- Simplified the conditions for displaying the ScriptError component in ResponsePane.
- Updated navigation logic in ScriptErrorCard to ensure proper handling of source information.
- Adjusted styles in StyledWrapper to allow for visible overflow.
- Enhanced ScriptErrorIcon to accept additional class names for better styling flexibility.
- Minor layout adjustments in RunnerResults ResponsePane for improved UI consistency.

* refactor: simplify test description for untranslated pm commands and consolidate error formatter imports

* fixes

* refactor: update focusedTab logic in Script components to use collection and folder UIDthe respective collection and folder UID instead of the activeTabUid.

* feat: add buildErrorContext utility for enhanced error handling in network IPC

- Introduced a new utility function `buildErrorContext` to construct detailed error context from script errors.
- This function parses error locations, adjusts line numbers, and retrieves relevant source context, improving error reporting.
- Removed the previous inline error handling logic from the network IPC module to streamline the codebase.

* feat: added playwright test cases to test scriptError behavior

* refactor: enhance ScriptError component and improve error handling

- Updated labels for error source types in ScriptError to be more concise (e.g., "Request Script" to "Request").
- Improved navigation logic in ScriptErrorCard to ensure proper handling of navigable file paths.
- Enhanced styling in StyledWrapper for better visual consistency and user experience.
- Added tests to verify fallback behavior for missing error types and non-navigable file paths.
- Refined utility functions for better context extraction from script errors.

* refactor: normalize file paths for cross-platform compatibility in ScriptError component

* refactor: improve file path handling in ScriptError component

* refactor: enhance buildErrorContext for improved error handling

* docs: add detailed comments to build-error-context for better understanding of error handling

* refactor: enhance error block line detection in error formatter

- Updated `findScriptBlockEndLine` and `findYmlScriptBlockEndLine` functions to return null for empty or missing blocks, improving accuracy in line detection.
- Added comprehensive tests for both functions to ensure correct behavior across various scenarios, including handling of non-.bru and non-.yml files.

* refactor: improve error handling and testing in ScriptError and buildErrorContext

- Updated ScriptError component to streamline tab management by replacing focusTab with addTab for better request handling.
- Enhanced buildErrorContext to return null for empty or missing script blocks in .bru and .yml files, ensuring accurate error reporting.
- Added tests to validate behavior for empty script blocks and improved error context extraction in various scenarios.

* test: add new tests for script error navigation and handling

- Implemented tests for post-response file-path navigation to the Script tab and verification of active sub-tabs.
- Added keyboard navigation tests to trigger file-path navigation using the Enter key.
- Included a test for multiple error cards to ensure closing one does not affect others.
- Enhanced runner tests to verify navigation to the Tests tab from script error results.

* refactor: enhance CodeSnippet line rendering and remove unused source-context utilities

* refactor: update locators in script-errors tests for improved readability and maintainability

* test: enhance script-errors tests to verify error line content

* review fixes

* refactor: update RunnerResults component and enhance locators for improved testability

* refactor: remove buildErrorContext and replace with formatErrorWithContextV2

- Deleted the buildErrorContext function and its associated tests.
- Updated network IPC to utilize formatErrorWithContextV2 for improved error context handling.
- Enhanced error reporting by ensuring structured error context is returned for desktop UI.

* refactor: enhance tab components with data-testid attributes for improved testability

- Added data-testid attributes to tab elements in CollectionSettings and FolderSettings components for better integration with testing frameworks.
- Updated Tabs and ResponsiveTabs components to include data-testid attributes for tab triggers, enhancing the ability to select and verify tabs in tests.
- Modified script-errors tests to utilize new locators for improved readability and maintainability.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:36:02 +05:30

264 lines
13 KiB
TypeScript

import { Page, Locator } from '../../../playwright';
export const buildCommonLocators = (page: Page) => ({
runner: () => page.getByTestId('run-button'),
saveButton: () => page
.locator('.infotip')
.filter({ hasText: /^Save/ }),
sidebar: {
collectionsContainer: () => page.getByTestId('collections'),
collection: (name: string) => page.locator('#sidebar-collection-name').filter({ hasText: name }),
folder: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),
request: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),
folderRequest: (folderName: string, requestName: string) => {
// Find the folder's collection-item-name, then navigate to its parent wrapper container (StyledWrapper),
// and search for the request within that container's descendants.
// Using .locator('..') gets the parent element of the folder's collection-item-name div.
const folderWrapper = page.locator('.collection-item-name').filter({ hasText: folderName }).locator('..');
return folderWrapper.locator('.collection-item-name').filter({ hasText: requestName });
},
closeAllCollectionsButton: () => page.getByTestId('collections-header-actions-menu-close-all'),
collectionRow: (name: string) => page.getByTestId('sidebar-collection-row').filter({ hasText: name })
},
actions: {
collectionActions: (collectionName: string) =>
page.getByTestId('collections').locator('.collection-name')
.filter({ hasText: collectionName })
.locator('.collection-actions .icon'),
collectionItemActions: (itemName: string) =>
page.locator('.collection-item-name')
.filter({ hasText: itemName })
.locator('.menu-icon')
},
dropdown: {
item: (text: string) => page.locator('.dropdown-item').filter({ hasText: text }),
tippyItem: (text: string) => page.locator('.tippy-box .dropdown-item').filter({ hasText: text })
},
tabs: {
requestTab: (requestName: string) => page.locator('.request-tab .tab-label').filter({ hasText: requestName }),
activeRequestTab: () => page.locator('.request-tab.active'),
closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).getByTestId('request-tab-close-icon')
},
paneTabs: {
responsiveTab: (key: string) => page.getByTestId(`responsive-tab-${key}`),
collectionSettingsTab: (key: string) => page.getByTestId(`collection-settings-tab-${key}`),
folderSettingsTab: (key: string) => page.getByTestId(`folder-settings-tab-${key}`),
tabTrigger: (key: string) => page.getByTestId(`tab-trigger-${key}`)
},
folder: {
chevron: (folderName: string) => page.locator('.collection-item-name').filter({ hasText: folderName }).getByTestId('folder-chevron')
},
modal: {
title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }),
byTitle: (title: string) => page.locator('.bruno-modal').filter({ has: page.locator('.bruno-modal-header-title').filter({ hasText: title }) }),
button: (name: string) => page.locator('.bruno-modal').getByRole('button', { name: name, exact: true }),
closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button'),
card: () => page.locator('.bruno-modal-card'),
footer: () => page.locator('.bruno-modal-footer'),
submitButton: () => page.locator('.bruno-modal-footer .submit'),
newRequestMethodOption: (id: string) => page.getByTestId(`method-selector-${id.toLowerCase()}`)
},
environment: {
selector: () => page.getByTestId('environment-selector-trigger'),
collectionTab: () => page.getByTestId('env-tab-collection'),
globalTab: () => page.getByTestId('env-tab-global'),
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }),
currentEnvironment: () => page.locator('.current-environment'),
addVariableButton: () => page.locator('button[data-testid="add-variable"]'),
variableNameInput: (index: number) => page.locator(`input[name="${index}.name"]`),
variableSecretCheckbox: (index: number) => page.locator(`input[name="${index}.secret"]`),
variableRow: (index: number) => page.locator('tr').filter({ has: page.locator(`input[name="${index}.name"]`) }),
createEnvButton: () => page.locator('button[id="create-env"]'),
envNameInput: () => page.locator('input[name="name"]')
},
request: {
urlInput: () => page.locator('#request-url .CodeMirror'),
urlLine: () => page.locator('#request-url .CodeMirror-line'),
sendButton: () => page.getByTestId('send-arrow-icon'),
methodDropdown: () => page.getByTestId('request-method-selector'),
newRequestUrl: () => page.locator('#new-request-url .CodeMirror'),
requestNameInput: () => page.getByPlaceholder('Request Name'),
requestTestId: () => page.getByTestId('request-name'),
generateCodeButton: () => page.locator('#send-request .infotip').first(),
bodyModeSelector: () => page.getByTestId('request-body-mode-selector'),
bodyEditor: () => page.getByTestId('request-body-editor')
},
tags: {
input: () => page.getByTestId('tag-input').getByRole('textbox'),
item: (tagName: string) => page.locator('.tag-item', { hasText: tagName })
},
runnerResults: {
itemPath: (name: string) => page.getByTestId('runner-result-item').filter({ hasText: name })
},
response: {
statusCode: () => page.getByTestId('response-status-code'),
pane: () => page.locator('.response-pane'),
copyButton: () => page.locator('button[title="Copy response to clipboard"]'),
body: () => page.locator('.response-pane'),
editorContainer: () => page.locator('.response-pane .editor-container'),
formatTab: () => page.getByTestId('format-response-tab'),
formatTabDropdown: () => page.getByTestId('format-response-tab-dropdown'),
previewContainer: () => page.getByTestId('response-preview-container'),
previewContainerCodeMirror: () => page.getByTestId('response-preview-container').locator('.CodeMirror').first(),
codeLine: () => page.locator('.response-pane .editor-container .CodeMirror-line'),
jsonTreeLine: () => page.locator('.response-pane .object-content')
},
plusMenu: {
button: () => page.getByTestId('collections-header-add-menu'),
createCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }),
importCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' })
},
import: {
modal: () => page.locator('[data-testid="import-collection-modal"]'),
locationModal: () => page.locator('[data-testid="import-collection-location-modal"]'),
locationInput: () => page.locator('#collection-location'),
fileInput: () => page.locator('input[type="file"]'),
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true })
},
/**
* Build generic table locators for any table with a testId
* @param testId - The testId of the table
* @returns Table locators object
*/
table: (testId: string) => {
const container = () => page.getByTestId(testId);
const getBodyRow = (index?: number) => {
const locator = container().locator('tbody tr');
return index !== undefined ? locator.nth(index) : locator;
};
return {
container,
row: (index?: number) => getBodyRow(index),
rowCell: (columnKey: string, rowIndex?: number) => {
const row = getBodyRow(rowIndex);
return row.getByTestId(`column-${columnKey}`);
},
rowCheckbox: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-checkbox'),
rowDeleteButton: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-delete'),
allRows: () => container().locator('tbody tr')
};
},
/**
* Assertions table locators (extends generic table with assertion-specific helpers)
* @returns Assertions table locators object
*/
assertionsTable: () => {
const baseTable = buildCommonLocators(page).table('assertions-table');
return {
...baseTable,
// Assertion-specific helpers
rowExprInput: (rowIndex: number) => {
const cell = baseTable.rowCell('name', rowIndex);
// Wait for the cell to be visible, then find the textbox
return cell.getByRole('textbox').or(cell.locator('input[type="text"]'));
},
rowOperatorSelect: (rowIndex: number) => {
const cell = baseTable.rowCell('operator', rowIndex);
return cell.getByTestId('assertion-operator-select').or(cell.locator('select'));
},
rowValueInput: (rowIndex: number) => baseTable.rowCell('value', rowIndex)
};
}
});
export const buildWebsocketCommonLocators = (page: Page) => ({
...buildCommonLocators(page),
connectionControls: {
connect: () =>
page
.locator('div.connection-controls')
.locator('.infotip')
.filter({ hasText: /^Connect$/ }),
disconnect: () =>
page
.locator('div.connection-controls')
.locator('.infotip')
.filter({ hasText: /^Close Connection$/ })
},
messages: () => page.locator('.ws-message'),
toolbar: {
latestFirst: () => page.getByRole('button', { name: 'Latest First' }),
latestLast: () => page.getByRole('button', { name: 'Latest Last' }),
clearResponse: () => page.getByTestId('response-clear-btn')
}
});
export const getTableCell = (row, index) => row.locator('td').nth(index + 1);
export const buildGrpcCommonLocators = (page: Page) => ({
...buildCommonLocators(page),
method: {
dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),
indicator: () => page.getByTestId('grpc-method-indicator')
},
request: {
queryUrlContainer: () => page.getByTestId('grpc-query-url-container'),
sendButton: () => page.getByTestId('grpc-send-request-button'),
messagesContainer: () => page.getByTestId('grpc-messages-container'),
addMessageButton: () => page.getByTestId('grpc-add-message-button'),
sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),
endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),
cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')
},
response: {
statusCode: () => page.getByTestId('grpc-response-status-code'),
statusText: () => page.getByTestId('grpc-response-status-text'),
content: () => page.getByTestId('grpc-response-content'),
container: () => page.getByTestId('grpc-responses-container'),
singleResponse: () => page.getByTestId('grpc-single-response'),
list: () => page.getByTestId('grpc-responses-list'),
responseItem: (index: number) => page.getByTestId(`grpc-response-item-${index}`),
responseItems: () => page.locator('[data-testid^="grpc-response-item-"]'),
tabCount: () => page.getByRole('tab', { name: 'Response' }).getByTestId('grpc-tab-response-count')
}
});
/**
* Builds locators for script error display elements
* @param page - The Playwright page object
* @returns Object with locators for script error elements
*/
export const buildScriptErrorLocators = (page: Page) => ({
/** All error cards on the page */
cards: () => page.getByTestId('script-error-card'),
/** Nth error card (0-indexed) */
card: (index?: number) => {
const cards = page.getByTestId('script-error-card');
return index !== undefined ? cards.nth(index) : cards.first();
},
/** Error title within a card */
title: (card?: Locator) => (card ?? page).getByTestId('script-error-title'),
/** Close button within a card */
closeButton: (card?: Locator) => (card ?? page).getByTestId('script-error-close'),
/** Source label within a card */
sourceLabel: (card?: Locator) => (card ?? page).getByTestId('script-error-source-label'),
/** File path link within a card */
filePath: (card?: Locator) => (card ?? page).getByTestId('script-error-file-path'),
/** Error message within a card */
message: (card?: Locator) => (card ?? page).getByTestId('script-error-message'),
/** Code snippet within a card */
codeSnippet: (card?: Locator) => (card ?? page).getByTestId('code-snippet'),
/** Error-highlighted code line within a card */
errorLine: (card?: Locator) => (card ?? page).getByTestId('code-line-error'),
/** Stack trace toggle within a card */
stackToggle: (card?: Locator) => (card ?? page).getByTestId('script-error-stack-toggle'),
/** Stack trace content within a card */
stack: (card?: Locator) => (card ?? page).getByTestId('script-error-stack'),
/** ScriptErrorIcon (the red alert button shown when card is dismissed) */
errorIcon: () => page.getByTestId('script-error-icon')
});
/**
* Builds locators for sandbox mode settings
* @param page - The Playwright page object
* @returns Object with locators for sandbox elements
*/
export const buildSandboxLocators = (page: Page) => ({
sandboxModeSelector: () => page.getByTestId('sandbox-mode-selector'),
safeModeRadio: () => page.getByTestId('sandbox-mode-safe'),
developerModeRadio: () => page.getByTestId('sandbox-mode-developer'),
jsSandboxHeading: () => page.getByText('JavaScript Sandbox'),
saveButton: () => page.getByRole('button', { name: 'Save' })
});