Files
bruno/tests/utils/page/locators.ts
sanish chirayath 87f74262bb feat(variables): persist scripted variable changes by default + re-enable disabled scripting APIs (#8315)
* feat(variables): add variable persistence with scripting

feat(collections): implement script-driven update for collection variables, ensuring direct root modification and draft synchronization

feat(collections): enhance script variable management with baseline tracking and draft preservation

* feat(variables): add runtime variable updates and optimize disk writes by implementing dirty flags

fix(collections): handle errors during environment persistence in script execution

feat(collections): implement baseline clearing for script execution and optimize variable update handling

feat(tests): add default persistence tests for environment variables and update runtime variable handling

refactor(collections): streamline variable update handling and improve draft management by removing redundant comments and optimizing code clarity

test(collection-vars): add verification for draft edits and script variable visibility in collection settings UI

refactor(collection-vars): update header value selection logic for improved clarity and accuracy in draft isolation tests

* feat(global-environments): enhance global environment updates to resolve stale active UIDs and improve persistence logic

- Updated the `updateGlobalEnvironments` reducer to handle stale active UIDs by matching against environment names.
- Improved the logic for setting global environments and active UIDs to ensure consistency after disk reloads.
- Removed outdated tests related to persisted values in favor of more relevant assertions for environment variable handling.

* feat(variables): enhance typed value handling and persistence in global and collection environments

- Added tests to infer data types (number, boolean, object) when setting environment and collection variables.
- Updated the logic to preserve existing data types when variables are not modified by scripts.
- Implemented dirty flags to track changes in typed variables, ensuring accurate persistence across sessions.
- Refactored related tests to verify the correct behavior of typed variables in various scenarios.

* refactor(variables): streamline data type inference and enhance deletion methods

- Removed redundant data type inference logic from global and collection variable updates to simplify the codebase.
- Updated deletion methods in the Bru class to use Object.keys for improved resilience against user-defined properties.
- Added tests to ensure deletion methods function correctly even when properties are shadowed.
- Enhanced clarity in draft merge tests by standardizing keyboard shortcuts for selecting all text.

* fix(tests): correct variable naming and improve environment panel interactions

- Updated test cases to reflect the correct variable name 'wasSaved' instead of 'was-saved'.
- Modified environment panel interaction to remove forced click, enhancing test reliability.
- Added a utility function to close the environment panel in safe mode tests for better readability and maintainability.

* feat(runtime): enhance variable management and cleanup logic

- Introduced a new method to clear script-driven variable baselines for collections, ensuring no stale data leaks into new requests.
- Updated the handling of runtime variables in the Bru class to track changes with a new dirty flag, improving state management.
- Refactored the application of script environment variables to prevent direct mutations, ensuring immutability and cleaner state updates.
- Enhanced the response handling in the script runtime to conditionally include runtime variables based on their dirty state.

* feat(variables): improve request handling and state management for collections and environments

- Enhanced event listeners to clear global environment baselines on both 'testrun-started' and 'request-queued' events, preventing stale data issues.
- Updated global environment and collection variable update events to ignore stale updates from superseded requests, ensuring accurate state management.
- Refactored the Bru class to optimize variable management, including checks for existing keys before updates and deletions, improving performance and reliability.
- Introduced request UID tracking to maintain consistency across variable updates during concurrent requests.

* refactor(collections): update action to clear script variable baselines

- Replaced the dispatch of `_clearScriptGlobalEnvBaseline` with `clearScriptVariableBaselines` to improve clarity and maintainability in the Redux action handling for collections.

* feat(environments): introduce getScriptModifiedKeys utility for improved variable management

- Added a new utility function, `getScriptModifiedKeys`, to identify keys modified by scripts relative to a baseline, enhancing the handling of data types during variable updates.
- Updated the application of script environment variables to prevent overwriting user-defined draft changes during no-op writes.
- Refactored related logic in collections and global environments to utilize the new utility, ensuring accurate state management and improved clarity in the Redux slices.

* refactor(global-environments): simplify active UID resolution logic in updateGlobalEnvironments reducer

- Streamlined the logic for resolving the active global environment UID by consolidating conditions into a more concise format.
- Removed outdated comments to enhance code clarity and maintainability.
- Updated tests to ensure accurate resolution of active UIDs based on incoming environment data.

* refactor(tests): remove outdated comments and streamline environment variable row expectations

- Eliminated comments related to state sync and inference issues to enhance code clarity.
- Adjusted expectations for environment variable row rendering in tests, focusing on relevant assertions.

* feat(tests): add comprehensive tests for secret variable persistence in environments

- Introduced new test cases to validate the preservation of secret variables when updated via scripts in both collection and global environments.
- Implemented tests to ensure that secret values are encrypted before storage and can be correctly decrypted for subsequent requests.
- Added fixtures and environment configurations for testing secret variable behavior in both bru and yml formats.
- Enhanced utility functions for managing environment configurations and interactions within the test suite.

* feat(tests): enhance environment variable tests and add global variable persistence

- Updated MultiLineEditor and SingleLineEditor components to include data-testid for secret reveal toggle buttons, improving testability.
- Introduced new tests for global environment variable persistence, ensuring non-secret variables survive app restarts and are correctly interpolated.
- Added fixtures for workspace and collections to support the new global variable tests, enhancing the overall test coverage for environment management.
- Refactored utility functions to streamline interactions with environment variables in tests.

* refactor(collections): optimize environment and collection saving logic

- Simplified the persistence logic for active environments by directly constructing the environment copy, reducing unnecessary cloning.
- Updated the collection saving process to utilize the fresh collection state, ensuring accurate data is saved without drafts.
- Enhanced error handling during the save operations to improve reliability and maintainability.

* feat(tests): implement collection variable persistence tests

- Added multiple test cases to validate the persistence of collection variables across app restarts, including typed values and multiple variable settings.
- Created new fixtures for collection variables to support the tests, ensuring accurate simulation of variable management scenarios.
- Enhanced the existing collection management logic to ensure that variables are correctly set and deleted as per the test requirements.

* feat(tests): add tests for typed global environment variable persistence

- Introduced a new test suite to validate the persistence of typed global environment variables across app restarts, ensuring correct data types are maintained.
- Created a fixture for the test collection to simulate setting global variables with various data types, including number, boolean, object, and string.
- Enhanced the test logic to verify that the environment file reflects the correct state before and after application restarts.

* fix(tests): update request tab close interaction in variable persistence tests

* fix(tests): improve hover interaction for collection actions in runner tests

- Updated the hover logic for revealing collection actions to handle sidebar re-renders more reliably.
- Replaced one-shot hover with a polling mechanism to ensure visibility of actions, enhancing test stability.

* refactor(environments): streamline environment variable handling and remove ephemeral metadata logic

- Simplified the comparison logic for environment variables by removing unnecessary ephemeral metadata handling.
- Updated the saving process to directly use the environment variables without stripping metadata, enhancing clarity and maintainability.
- Removed outdated comments and unused utility functions related to ephemeral variables, improving code cleanliness.

* fix(ipc): update persistActiveEnvironment to handle requestUid for stale updates

- Modified the persistActiveEnvironment function to accept a requestUid parameter, allowing for better management of stale updates.
- Enhanced the logic to prevent disk writes for superseded requests, improving data integrity during environment persistence.

* refactor(bru): remove unused envName variable in deleteAllEnvVars method

- Eliminated the envName variable from the deleteAllEnvVars method, simplifying the logic for deleting environment variables.
- Cleaned up the method by removing unnecessary checks related to the envName, enhancing code clarity and maintainability.

* fix(bru): prevent deletion of internal __name__ variable in deleteEnvVar method

- Added a check in the deleteEnvVar method to silently ignore attempts to delete the internal __name__ variable, preserving its integrity.
- Updated tests to verify that the __name__ variable remains unchanged when deleteEnvVar is called with this key.
- Enhanced runtime tests to ensure compatibility with QuickJS by confirming that environment variables set with persist options are handled correctly.

* feat(tests): add legacy support test for environment variable persistence

- Introduced a new test suite to validate that the legacy argument for setting environment variables with persistence is still functional in version 4.
- Created a fixture to simulate the legacy syntax, ensuring that the variable is correctly persisted on disk without errors.
- Enhanced integration testing to confirm that the legacy behavior aligns with the current implementation, maintaining backward compatibility.

* test(tests): enhance legacy environment variable persistence tests for safe and developer modes

- Updated the test suite for `bru.setEnvVar` to verify that the legacy persist flag is correctly handled in both safe and developer modes.
- Introduced a helper function to streamline the verification process and ensure consistent behavior across different execution contexts.
- Adjusted the test logic to reset the environment state between mode switches, maintaining test integrity.
- Improved hover interaction in multiple persistent variable tests to ensure reliable visibility of actions during execution.

* fix(EnvironmentVariablesTable): correct change detection logic for environment variables

- Updated the logic for determining changes in environment variables to compare active current and saved values instead of previously used variablesToSave and savedValues.
- This change ensures accurate detection of modifications before saving, improving user feedback when no changes are present.

* test(tests): enhance secret variable persistence tests for environment configurations

- Updated the test suites for `bru.setEnvVar` and `bru.setGlobalEnvVar` to include interactions with the secrets tab, ensuring visibility of secret variables during various states of the environment.
- Added checks to confirm that the eye toggle functionality correctly reveals the values of secret variables after setting and overwriting them.
- Improved test coverage for secret variable persistence, validating that the expected values are displayed in both collection and global environment contexts.
2026-06-26 23:01:37 +05:30

418 lines
23 KiB
TypeScript

import { Page, Locator } from '../../../playwright';
export const buildCommonLocators = (page: Page) => ({
runner: () => page.getByTestId('run-button'),
saveButton: () => page
.locator('.infotip')
.filter({ hasText: /^Save/ }),
openPreferences: () => page.getByRole('button', { name: 'Open Preferences' }),
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 }),
// The sidebar tree wraps each collection in `#collection-<slug>`; scope queries
// to it to disambiguate items that share names across collections.
collectionScope: (name: string) => page.locator(`#collection-${name.replace(/\s+/g, '-').toLowerCase()}`)
},
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 }),
folderTab: (folderName: string) => page.locator('.request-tab .tab-label').filter({ hasText: folderName }),
collectionSettingsTab: () =>
page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }),
activeRequestTab: () => page.locator('.request-tab.active'),
closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).getByTestId('request-tab-close-icon'),
draftIndicator: () => page.locator('.request-tab.active .has-changes-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}`),
folderScriptTab: (key: 'pre-request' | 'post-response') => page.getByTestId(`tab-trigger-${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 }),
listOption: (name: string) => page.locator('.environment-list .dropdown-item', { hasText: name }),
currentEnvironment: () => page.locator('.current-environment'),
configureButton: () => page.locator('#configure-env'),
saveButton: () => page.getByTestId('save-env'),
varRow: (name: string) => page.getByTestId(`env-var-row-${name}`),
// Prefix match — keep as a CSS selector since getByTestId is exact-match only.
varRows: () => page.locator('tbody tr[data-testid^="env-var-row-"]'),
// Rows for `name` whose CodeMirror value matches `value`. Useful when two rows
// share a name (e.g. enabled + disabled twins after a script write).
varRowsByValue: (name: string, value: string | RegExp) =>
page.getByTestId(`env-var-row-${name}`)
.filter({ has: page.locator('.CodeMirror-line', { hasText: value }) }),
// Each env-var row has an `enabled` and a `secret` checkbox; target the latter
// by its `<index>.secret` name (the formik index is dynamic).
varRowSecretCheckbox: (name: string) => page.getByTestId(`env-var-row-${name}`).locator('input[name$=".secret"]'),
// Eye icon that masks/reveals a secret variable's value.
varRowEyeToggle: (name: string) => page.getByTestId(`env-var-row-${name}`).getByTestId('secret-reveal-toggle'),
varRowLine: (name: string) => page.getByTestId(`env-var-row-${name}`).locator('.CodeMirror-line').first(),
addVariableButton: () => page.getByTestId('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"]`) }),
variableRowByName: (name: string) => page.locator('tbody tr').filter({ has: page.locator(`input[value="${name}"]`) }),
// Targets the `.CodeMirror` wrapper (not `.CodeMirror-line`) so single-line and
// multi-line values (e.g. formatted JSON for @object vars) are both covered —
// CodeMirror renders each visual line as a separate `.CodeMirror-line`, so
// matching on the wrapper is the only way to get the full concatenated text.
variableValue: (name: string) => page.locator('tbody tr').filter({ has: page.locator(`input[value="${name}"]`) }).locator('.CodeMirror').first(),
createEnvButton: () => page.locator('button[id="create-env"]'),
envNameInput: () => page.locator('input[name="name"]'),
// Variables and secrets each live on their own tab in the environment editor.
variablesTab: () => page.getByTestId('responsive-tab-variables'),
secretsTab: () => page.getByTestId('responsive-tab-secrets'),
saveTab: () => page.getByTestId('save-env'),
saveAll: () => page.getByTestId('save-all-env'),
collectionEnvTab: () => page.locator('.request-tab').filter({ hasText: /^Environments$/ }),
globalEnvTab: () => page.locator('.request-tab').filter({ hasText: /^Global Environments$/ }),
unsavedModal: {
closeWithoutSave: () => page.getByTestId('env-unsaved-close-without-save'),
cancel: () => page.getByTestId('env-unsaved-cancel'),
saveAndClose: () => page.getByTestId('env-unsaved-save-and-close')
}
},
codeMirror: {
byTestId: (testId: string) => page.getByTestId(testId).locator('.CodeMirror').first()
},
// The DataTypeSelector renders a `.type-label` trigger per row (request/folder/
// collection vars + env vars) and a MenuDropdown (role=menu) at page scope.
dataTypeSelector: {
typeLabel: (row: Locator) => row.locator('.type-label').first(),
// Yellow warning icon shown when a value can't be coerced to its dataType.
mismatchIcon: (row: Locator) => row.locator('svg.text-yellow-600'),
menuItem: (type: string) => page.locator('[role="menu"]').last().getByText(type, { exact: true })
},
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('#request-actions .infotip').first(),
bodyModeSelector: () => page.getByTestId('request-body-mode-selector'),
bodyEditor: () => page.getByTestId('request-body-editor'),
bodyVariableToken: (name: string) =>
page.getByTestId('request-body-editor').locator('.CodeMirror .cm-variable-valid').filter({ hasText: name }),
pane: () => page.getByTestId('request-pane')
},
// The variable-info popup shown when hovering a `{{var}}` token in an editor.
varInfoPopup: {
all: () => page.locator('.CodeMirror-brunoVarInfo'),
byName: (name: string) =>
page.locator('.CodeMirror-brunoVarInfo').filter({ has: page.locator('.var-name').filter({ hasText: new RegExp(`^${name}$`) }) }),
valueDisplay: (popup: Locator) => popup.locator('.var-value-editable-display, .var-value-display').first(),
editableValue: (popup: Locator) => popup.locator('.var-value-editable-display').first(),
secretToggle: (popup: Locator) => popup.locator('.secret-toggle-button'),
editor: (popup: Locator) => popup.locator('.var-value-editor .CodeMirror')
},
auth: {
apiKey: {
placementSelector: () => page.getByTestId('auth-placement-selector'),
placementLabel: () => page.getByTestId('auth-placement-label')
},
oauth2: {
grantTypeDropdown: () => page.getByTestId('grant-type-dropdown'),
tokenHeaderPrefixField: () => page.getByTestId('token-header-prefix'),
tokenQueryParamKeyField: () => page.getByTestId('token-query-param-key')
},
modeSelector: () => page.getByTestId('auth-mode-selector'),
modeLabel: () => page.getByTestId('auth-mode-label'),
inheritedMode: () => page.getByTestId('inherited-auth-mode'),
dropdownItem: (id: string) => page.getByTestId(`auth-mode-dropdown-${id}`)
},
presets: {
requestType: (type: 'http' | 'graphql' | 'grpc' | 'ws') =>
page.getByTestId(`presets-request-type-${type}`),
requestUrl: () => page.getByTestId('presets-request-url'),
saveBtn: () => page.getByTestId('presets-save-btn')
},
tags: {
input: () => page.getByTestId('tag-input').getByRole('textbox'),
item: (tagName: string) => page.locator('.tag-item', { hasText: tagName })
},
generateDocs: {
menuItem: () => page.locator('.dropdown-item').filter({ hasText: 'Generate Docs' }),
modal: () => page.locator('.bruno-modal').filter({
has: page.locator('.bruno-modal-header-title').filter({ hasText: 'Generate Documentation' })
}),
heading: () => page.locator('.bruno-modal').getByText('Interactive API Documentation'),
generateButton: () => page.locator('.bruno-modal').getByRole('button', { name: 'Generate', exact: true }),
cancelButton: () => page.locator('.bruno-modal').getByRole('button', { name: 'Cancel', exact: true }),
// Collection version (read-only) display
versionInfo: () => page.locator('.bruno-modal').getByTestId('version-info'),
versionValue: () => page.locator('.bruno-modal').getByTestId('version-value'),
versionCounts: () => page.locator('.bruno-modal').getByTestId('version-summary'),
// Environment selection list
environmentsTitle: () => page.locator('.bruno-modal').getByTestId('env-section-title'),
// Header controls: tri-state "select all" checkbox + "X/Y selected" count
selectAllCheckbox: () => page.locator('.bruno-modal').getByTestId('env-select-all'),
selectAllLabel: () => page.locator('.bruno-modal').getByTestId('env-select-all-label'),
selectedCount: () => page.locator('.bruno-modal').getByTestId('env-selected-count'),
environmentRows: () => page.locator('.bruno-modal').getByTestId('env-row'),
environmentRow: (name: string) =>
page.locator('.bruno-modal').getByTestId('env-row').filter({ has: page.getByText(name, { exact: true }) }),
// A row has exactly one checkbox; its data-testid is uid-keyed, so select it by role within the named row.
environmentCheckbox: (name: string) =>
page
.locator('.bruno-modal')
.getByTestId('env-row')
.filter({ has: page.getByText(name, { exact: true }) })
.getByRole('checkbox')
},
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'),
// Tests-tab summary line ("Tests (N), Passed: X, Failed: Y") and failure rows.
testSummary: () => page.locator('.test-summary').filter({ hasText: 'Tests' }),
testFailures: () => page.locator('.test-result-item .test-failure')
},
timeline: {
items: () => page.getByTestId('timeline-item'),
lastItem: () => page.getByTestId('timeline-item').last(),
itemHeader: (item: Locator) => item.getByTestId('timeline-item-header'),
clearButton: () => page.getByRole('button', { name: 'Clear Timeline' })
},
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 }),
parsingError: () => page.getByTestId('import-error-message'),
browseLink: (root?: Locator) => (root ?? page).getByTestId('import-collection-browse-link'),
importButton: (root?: Locator) => (root ?? page).getByTestId('import-collection-location-modal-submit-btn'),
...(() => {
const issuesToast = () => page.getByTestId('import-issues-toast').last();
return {
issuesToast,
issuesToastTitle: () => issuesToast().getByTestId('import-issues-toast-title'),
issuesToastCopyBtn: () => issuesToast().getByTestId('import-issues-copy-btn'),
issuesToastReportBtn: () => issuesToast().getByTestId('import-issues-report-btn'),
issuesToastIncludeItemsCheckbox: () => issuesToast().getByTestId('import-issues-include-items-checkbox'),
issuesToastCloseBtn: () => issuesToast().getByTestId('import-issues-toast-close'),
issuesToastUrlTooLongWarning: () => issuesToast().getByTestId('import-issues-url-too-long-warning')
};
})()
},
/**
* 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),
// EditableTable rows carry data-row-name derived from the key column.
rowByName: (name: string) => container().locator(`tbody tr[data-row-name="${name}"]`),
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'),
message: {
label: (index: number) => page.getByTestId(`ws-message-label-${index}`),
nameInput: (index: number) => page.getByTestId(`ws-message-name-input-${index}`),
nameTooltip: () => page.getByTestId('ws-message-name-tooltip')
},
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'),
dropdown: () => page.getByTestId('grpc-methods-dropdown'),
item: (methodName: string) =>
page.getByTestId('grpc-methods-dropdown').getByTestId('grpc-method-item').filter({ hasText: methodName }),
selectedName: () => page.getByTestId('selected-grpc-method-name')
},
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'),
regenerateMessage: (index: number) => page.getByTestId(`grpc-regenerate-message-${index}`),
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' })
});