Files
bruno/tests/utils/page/runner.ts
sanish chirayath bef4b6bbee feat(cookies): add direct cookie access methods and update translations (#7073)
* feat(cookies): add direct cookie access methods and update translations

- Introduced new methods for direct cookie access: `bru.cookies.get`, `bru.cookies.has`, and `bru.cookies.toObject`.
- Updated translation mappings in `bruno-to-postman-translator` and `postman-to-bruno-translator` to support these new methods.
- Enhanced tests to verify correct translation between `bru` and `pm` cookie methods, including mixed usage scenarios.
- Updated `Bru` class to handle cookie access based on the current request URL.

* feat(cookies): enhance cookie management with new methods and refactor

- Added new cookie methods: `toString`, `clear`, `delete`, `one`, `all`, `idx`, `count`, `indexOf`, `find`, `filter`, `each`, `map`, and `reduce` to `bru.cookies`.
- Refactored `Bru` class to utilize a new `CookieList` for cookie management, improving structure and readability.
- Updated translation mappings in `bruno-to-postman-translator` and `postman-to-bruno-translator` to include new cookie methods.
- Introduced `PropertyList` and `ReadOnlyPropertyList` classes for better data structure management.
- Enhanced tests for comprehensive coverage of new cookie functionalities and their interactions.

* docs(readonly-property-list): clarify array usage in constructor comments

* feat(cookies): add direct cookie manipulation tests and methods

* feat(cookies): add hasCookie method for checking cookie existence

* fix

* refactor(cookies): simplify cookie method translations

* feat(cookies): expand cookie API with new methods and tests

- Added new cookie methods: `get`, `has`, `toString`, `clear`, `upsert`, `remove`, `idx`, and `indexOf` to enhance cookie management.
- Updated translation mappings for `bru.cookies` to include new methods in `bruno-to-postman-translator` and `postman-to-bruno-translator`.
- Introduced tests for new methods and their interactions, ensuring comprehensive coverage of cookie functionalities.
- Enhanced existing tests to validate correct behavior of cookie methods across different scenarios.

* refactor(cookies): update CookieList to extend PropertyList and improve error handling

* test(cookies): add regression tests for jar and direct cookie patterns

- Introduced regression tests to ensure that jar patterns are correctly prioritized over direct cookie access patterns in translations.
- Updated `CookieList` to extend `ReadOnlyPropertyList` instead of `PropertyList`, clarifying its functionality.
- Refactored cookie method handling in the `bru` shim to utilize a new asynchronous bridge for improved error handling and consistency.

* refactor(cookies): update translations and remove PropertyList

- Enhanced comments in `postman-translations.js` to clarify the order of cookie jar translations.
- Updated `cookie-list.js` comments to better describe the factory function for the cookie jar.
- Removed the `PropertyList` class and its associated tests, streamlining the codebase and focusing on `ReadOnlyPropertyList` and `CookieList` for cookie management.

* fix(cookies): normalize tough-cookie objects and improve remove method comments

- Updated `CookieList` to normalize tough-cookie instances to plain objects, preventing circular references and exposing internal structures.
- Enhanced comments in the `remove` method to clarify behavior when removing non-existent or empty-named cookies.

* test(cookies): update tests to use async/await for consistency

* test(cookies): use async/await in cookie tests for consistency

* refactor(readonly-property-list): update get and reduce methods for improved behavior

* fix(cookies): update cookie method signature in autocomplete hints and enhance translation comments

- Modified the autocomplete hint for `bru.cookies.has` to include the new signature with an optional value parameter.
- Improved comments in `postman-translations.js` to clarify the order of cookie jar translation patterns for better understanding.

* refactor(cookies): introduce PropertyList for enhanced cookie management

* refactor(property-list): simplify repopulate method and enhance item handling logic

* feat(cookies): implement PropertyList bridge for enhanced cookie management

- Introduced a new `createPropertyListBridge` utility to streamline the integration of cookie methods into the QuickJS VM.
- Replaced the previous async cookie bridge with a more flexible approach, allowing for both synchronous and asynchronous cookie operations.
- Added comprehensive tests to validate the functionality of the new cookie methods in both developer and safe modes.
- Updated existing cookie tests to ensure compatibility with the new PropertyList structure.

* fix(tests): correct expected passed requests in cookie tests

- Updated the expected number of passed requests in the cookie tests from 34 to 6 to reflect the correct validation results.
- Ensured consistency in test assertions across multiple test cases for the PropertyList API.

* fix(cookies): update cookie URLs to use localhost for testing

- Changed all cookie-related test scripts to use `{{localhost}}` instead of `{{host}}` for the ping URL, ensuring consistency in local testing environments.
- Updated the cookie test suite to reflect the new URL structure, enhancing the reliability of the tests.
- Removed outdated cookie test files to streamline the test suite.

* refactor(cookies): standardize cookie handling with localhost variable

- Updated cookie test scripts to utilize the `{{localhost}}` variable for setting and retrieving cookies, ensuring consistency across tests.
- Enhanced clarity in comments regarding cookie behavior for different domains.
- Improved test assertions to validate cookie management functionality more effectively.

* refactor(property-list, readonly-property-list): update methods to use private class fields

* feat(cookies): enhance CookieList API with detailed documentation and method improvements

- Updated the `CookieList` class to provide comprehensive documentation on cookie management methods, including `add`, `upsert`, `remove`, and `delete`.
- Improved method signatures to support both callback and Promise-based usage for asynchronous operations.
- Added detailed descriptions for read and write methods, including examples and expected behavior.
- Enhanced the integration of the `CookieList` with the QuickJS VM by updating the property list bridge to include `toJSON` in sync read object methods.

* feat(cookies): add detailed examples and improve async bridge documentation

- Enhanced the `createPropertyListBridge` function documentation with comprehensive examples for setting up cookie methods in QuickJS.
- Clarified the two-phase setup process for async write methods, detailing the registration of bridge functions and the generation of JavaScript code for method wrappers.
- Added a new test case for the `toJSON()` method to ensure it returns a cloned array of all cookies, validating the expected structure and properties.

* fix(assert-runtime): correct syntax error in response parser assignment

- Added a semicolon at the end of the response parser assignment to ensure proper syntax in the AssertRuntime class.
2026-03-27 19:42:23 +05:30

253 lines
11 KiB
TypeScript

import { Page, expect, test } from '../../../playwright';
import { buildSandboxLocators } from './locators';
/**
* Builds locators for the runner results view
* @param page - The Playwright page object
* @returns Object with locators for runner elements
*/
export const buildRunnerLocators = (page: Page) => ({
allButton: () => page.locator('button').filter({ hasText: /^All/ }),
passedButton: () => page.locator('button').filter({ hasText: /^Passed/ }),
failedButton: () => page.locator('button').filter({ hasText: /^Failed/ }),
skippedButton: () => page.locator('button').filter({ hasText: /^Skipped/ }),
resetButton: () => page.getByRole('button', { name: 'Reset' }),
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')
});
/**
* Reads test result counts from the filter buttons in the runner results view
* @param page - The Playwright page object
* @returns An object with totalRequests, passed, failed, and skipped counts
*/
export const getRunnerResultCounts = async (page: Page) => {
const locators = buildRunnerLocators(page);
const totalRequests = parseInt(await locators.allButton().locator('span').innerText());
const passed = parseInt(await locators.passedButton().locator('span').innerText());
const failed = parseInt(await locators.failedButton().locator('span').innerText());
const skipped = parseInt(await locators.skippedButton().locator('span').innerText());
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
* @param page - The Playwright page object
* @param collectionName - The name of the collection to run
* @returns void
*/
export const runCollection = async (page: Page, collectionName: string) => {
await test.step(`Run collection "${collectionName}"`, async () => {
// Ensure collection is visible and loaded (scope to sidebar)
const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName });
await collectionContainer.waitFor({ state: 'visible' });
// Open collection actions menu - hover first to reveal the hidden actions button
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();
// Click Run menu item
const runMenuItem = page.getByText('Run', { exact: true });
await runMenuItem.waitFor({ state: 'visible' });
await runMenuItem.click();
// Handle runner tab - reset if needed, then run
const locators = buildRunnerLocators(page);
// Check if Reset button is visible (means there are existing results)
const resetVisible = await locators.resetButton().isVisible({ timeout: 1000 }).catch(() => false);
if (resetVisible) {
await locators.resetButton().click();
// Wait for the Run Collection button to become visible after reset
await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 5000 });
}
// Now wait for and click Run Collection button
await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 10000 });
await locators.runCollectionButton().click();
// Wait for the run to complete
await locators.runAgainButton().waitFor({ timeout: 2 * 60 * 1000 });
});
};
/**
* Runs a specific folder within a collection by navigating to it in the sidebar,
* opening its context menu, and clicking "Run" followed by "Recursive Run".
* @param page - The Playwright page object
* @param collectionName - The name of the collection containing the folder
* @param folderPath - Array of folder names forming the path (e.g. ['scripting', 'api', 'bru', 'cookies'])
*/
export const runFolder = async (page: Page, collectionName: string, folderPath: string[]) => {
await test.step(`Run folder "${folderPath.join('/')}" in "${collectionName}"`, async () => {
// Scope to the specific collection by its DOM id (collection-<name-kebab>)
const collectionId = `collection-${collectionName.replace(/\s+/g, '-').toLowerCase()}`;
const collectionContainer = page.locator(`#${collectionId}`);
await collectionContainer.waitFor({ state: 'visible', timeout: 5000 });
// Walk down the folder path, scoping each step to the previous folder's container.
// Each CollectionItem renders as a StyledWrapper div containing:
// - div.collection-item-name (the row with chevron, name, menu)
// - div (children container when expanded)
// We scope to the parent wrapper so the next folder lookup is unambiguous.
let scope = collectionContainer;
for (const folderName of folderPath) {
const row = scope.locator('.collection-item-name').filter({ hasText: folderName }).first();
await row.waitFor({ state: 'visible', timeout: 5000 });
// Click the chevron to expand (skip if already expanded)
const chevron = row.getByTestId('folder-chevron');
const isExpanded = await chevron.evaluate((el: HTMLElement) => el.classList.contains('rotate-90'));
if (!isExpanded) {
await chevron.click();
}
// Scope to this folder's wrapper (parent of the row) for the next iteration
scope = row.locator('..');
}
// The target folder row is the last one we found — hover to reveal menu
const targetRow = scope.locator('.collection-item-name').filter({ hasText: folderPath[folderPath.length - 1] }).first();
await targetRow.hover();
// Click the menu icon
const menuIcon = targetRow.locator('.menu-icon');
await menuIcon.waitFor({ state: 'visible', timeout: 5000 });
await menuIcon.click();
// Click "Run" in the dropdown
const runMenuItem = page.locator('.dropdown-item').filter({ hasText: 'Run' });
await runMenuItem.waitFor({ state: 'visible' });
await runMenuItem.click();
// In the RunCollectionItem modal, click "Recursive Run"
const recursiveRunButton = page.getByRole('button', { name: 'Recursive Run' });
await recursiveRunButton.waitFor({ state: 'visible', timeout: 5000 });
await recursiveRunButton.click();
// Wait for the run to complete
const runnerLocators = buildRunnerLocators(page);
await runnerLocators.runAgainButton().waitFor({ timeout: 2 * 60 * 1000 });
});
};
/**
* Sets up the JavaScript sandbox mode for a collection
* @param page - The Playwright page object
* @param collectionName - The name of the collection (can be title or text)
* @param mode - 'developer' or 'safe' mode
* @returns void
*/
export const setSandboxMode = async (page: Page, collectionName: string, mode: 'developer' | 'safe') => {
await test.step(`Set sandbox mode to "${mode}" for "${collectionName}"`, async () => {
const sandboxLocators = buildSandboxLocators(page);
// Click on the collection name in the sidebar
const sidebarCollection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: collectionName }).first();
await sidebarCollection.waitFor({ state: 'visible' });
await sidebarCollection.click();
// Check if there's already a mode selected - if so, we need to click the badge to open settings tab
const sandboxBadgeVisible = await sandboxLocators.sandboxModeSelector().isVisible().catch(() => false);
// If a badge exists, click it to open the security settings tab
if (sandboxBadgeVisible) {
await sandboxLocators.sandboxModeSelector().click();
// Wait for the security settings tab to be active
await sandboxLocators.jsSandboxHeading().waitFor({ state: 'visible', timeout: 10000 });
}
// If no badge exists, the modal should have appeared automatically (first time selection)
// Wait for security settings form to be visible - wait for either radio button
await Promise.race([
sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}),
sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {})
]);
if (mode === 'developer') {
await sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 5000 });
await sandboxLocators.developerModeRadio().click();
} else {
await sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 5000 });
await sandboxLocators.safeModeRadio().click();
}
await page.keyboard.press('Escape');
});
};
/**
* Validates runner results against expected counts
* @param page - The Playwright page object
* @param expected - Expected counts
* @returns void
*/
export const validateRunnerResults = async (page: Page,
expected: {
totalRequests?: number;
passed?: number;
failed?: number;
skipped?: number;
}) => {
const { totalRequests, passed, failed, skipped } = await getRunnerResultCounts(page);
if (expected.totalRequests !== undefined) {
await expect(totalRequests).toBe(expected.totalRequests);
}
if (expected.passed !== undefined) {
await expect(passed).toBe(expected.passed);
}
if (expected.failed !== undefined) {
await expect(failed).toBe(expected.failed);
}
if (expected.skipped !== undefined) {
await expect(skipped).toBe(expected.skipped);
}
// Validate that passed + failed + skipped = totalRequests
await expect(passed).toBe(totalRequests - skipped - failed);
};