mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
543 lines
18 KiB
TypeScript
543 lines
18 KiB
TypeScript
import { test, expect, Page } from '../../../playwright';
|
||
import { buildCommonLocators } from './locators';
|
||
|
||
type SandboxMode = 'safe' | 'developer';
|
||
|
||
/**
|
||
* Close all collections
|
||
* @param page - The page object
|
||
* @returns void
|
||
*/
|
||
const closeAllCollections = async (page) => {
|
||
await test.step('Close all collections', async () => {
|
||
const numberOfCollections = await page.locator('[data-testid="collections"] .collection-name').count();
|
||
|
||
for (let i = 0; i < numberOfCollections; i++) {
|
||
await page.locator('[data-testid="collections"] .collection-name').first().locator('.collection-actions').click();
|
||
await page.locator('.dropdown-item').getByText('Remove').click();
|
||
// Wait for the remove collection modal to be visible
|
||
await page.locator('.bruno-modal-header-title', { hasText: 'Remove Collection' }).waitFor({ state: 'visible' });
|
||
await page.locator('.bruno-modal-footer .submit').click();
|
||
// Wait for the remove collection modal to be hidden
|
||
await page.locator('.bruno-modal-header-title', { hasText: 'Remove Collection' }).waitFor({ state: 'hidden' });
|
||
}
|
||
|
||
// Wait until no collections are left open (check sidebar only)
|
||
await expect(page.getByTestId('collections').locator('.collection-name')).toHaveCount(0);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Open a collection from the sidebar and accept the JavaScript Sandbox modal
|
||
* @param page - The page object
|
||
* @param collectionName - The name of the collection to open
|
||
* @param sandboxMode - The mode to accept the sandbox modal
|
||
* @returns void
|
||
*/
|
||
const openCollectionAndAcceptSandbox = async (page, collectionName: string, sandboxMode: 'safe' | 'developer' = 'safe') => {
|
||
await test.step(`Open collection "${collectionName}" and accept sandbox "${sandboxMode}" mode`, async () => {
|
||
await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();
|
||
|
||
const sandboxModal = page
|
||
.locator('.bruno-modal-card')
|
||
.filter({ has: page.locator('.bruno-modal-header-title', { hasText: 'JavaScript Sandbox' }) });
|
||
|
||
const modeLabel = sandboxMode === 'safe' ? 'Safe Mode' : 'Developer Mode';
|
||
await sandboxModal.getByLabel(modeLabel).check();
|
||
await sandboxModal.locator('.bruno-modal-footer .submit').click();
|
||
await sandboxModal.waitFor({ state: 'detached' });
|
||
});
|
||
};
|
||
|
||
type CreateCollectionOptions = {
|
||
openWithSandboxMode?: 'safe' | 'developer';
|
||
};
|
||
|
||
/**
|
||
* Create a collection
|
||
* @param page - The page object
|
||
* @param collectionName - The name of the collection to create
|
||
* @param collectionLocation - The location of the collection to create (eg)
|
||
* @param options - The options for creating the collection
|
||
*
|
||
* @returns void
|
||
*/
|
||
const createCollection = async (page, collectionName: string, collectionLocation: string, options: CreateCollectionOptions = {}) => {
|
||
await test.step(`Create collection "${collectionName}"`, async () => {
|
||
await page.locator('.plus-icon-button').click();
|
||
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
||
|
||
const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' });
|
||
|
||
await createCollectionModal.getByLabel('Name').fill(collectionName);
|
||
const locationInput = createCollectionModal.getByLabel('Location');
|
||
if (await locationInput.isVisible()) {
|
||
await locationInput.fill(collectionLocation);
|
||
}
|
||
await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click();
|
||
|
||
await createCollectionModal.waitFor({ state: 'detached' });
|
||
|
||
if (options.openWithSandboxMode != undefined) {
|
||
await openCollectionAndAcceptSandbox(page, collectionName, options.openWithSandboxMode);
|
||
}
|
||
});
|
||
};
|
||
|
||
type CreateRequestOptions = {
|
||
url?: string;
|
||
inFolder?: boolean;
|
||
};
|
||
|
||
/**
|
||
* Create a request in a collection or folder
|
||
* @param page - The page object
|
||
* @param requestName - The name of the request to create
|
||
* @param parentName - The name of the collection or folder
|
||
* @param options - Optional settings (url, inFolder)
|
||
* @returns void
|
||
*/
|
||
const createRequest = async (
|
||
page: Page,
|
||
requestName: string,
|
||
parentName: string,
|
||
options: CreateRequestOptions = {}
|
||
) => {
|
||
const { url, inFolder = false } = options;
|
||
const parentType = inFolder ? 'folder' : 'collection';
|
||
|
||
await test.step(`Create request "${requestName}" in ${parentType} "${parentName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
if (inFolder) {
|
||
await locators.sidebar.folder(parentName).hover();
|
||
await locators.actions.collectionItemActions(parentName).click();
|
||
} else {
|
||
await locators.sidebar.collection(parentName).hover();
|
||
await locators.actions.collectionActions(parentName).click();
|
||
}
|
||
|
||
await locators.dropdown.item('New Request').click();
|
||
await page.getByPlaceholder('Request Name').fill(requestName);
|
||
|
||
if (url) {
|
||
await page.locator('#new-request-url .CodeMirror').click();
|
||
await page.keyboard.type(url);
|
||
}
|
||
|
||
await locators.modal.button('Create').click();
|
||
|
||
if (inFolder) {
|
||
await expect(locators.sidebar.folderRequest(parentName, requestName)).toBeVisible();
|
||
} else {
|
||
await expect(locators.sidebar.request(requestName)).toBeVisible();
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Delete a request from a collection
|
||
* @param page - The page object
|
||
* @param requestName - The name of the request to delete
|
||
* @param collectionName - The name of the collection
|
||
* @returns void
|
||
*/
|
||
const deleteRequest = async (page, requestName: string, collectionName: string) => {
|
||
await test.step(`Delete request "${requestName}" from collection "${collectionName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
// Click on the collection first to open it if it's closed
|
||
await locators.sidebar.collection(collectionName).click();
|
||
|
||
// Find the request within the collection's context
|
||
// Use the collection container (.collection-name) scoped to sidebar to scope the search
|
||
const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName });
|
||
const collectionWrapper = collectionContainer.locator('..');
|
||
const request = collectionWrapper.locator('.collection-item-name').filter({ hasText: requestName });
|
||
|
||
await request.locator('.menu-icon').click();
|
||
await locators.dropdown.item('Delete').click();
|
||
await locators.modal.button('Delete').click();
|
||
await expect(request).not.toBeVisible();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Import a collection from a file
|
||
* @param page - The page object
|
||
* @param filePath - The path to the collection file to import
|
||
* @param collectionLocation - The directory where the collection will be saved
|
||
* @param options - Optional settings for import
|
||
* @returns void
|
||
*/
|
||
type ImportCollectionOptions = {
|
||
expectedCollectionName?: string;
|
||
openWithSandboxMode?: SandboxMode;
|
||
};
|
||
|
||
const importCollection = async (
|
||
page: Page,
|
||
filePath: string,
|
||
collectionLocation: string,
|
||
options: ImportCollectionOptions = {}
|
||
) => {
|
||
await test.step(`Import collection from "${filePath}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
await page.locator('.plus-icon-button').click();
|
||
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
|
||
|
||
// Wait for import modal
|
||
const importModal = page.getByRole('dialog');
|
||
await importModal.waitFor({ state: 'visible' });
|
||
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||
|
||
// Set the file
|
||
await page.setInputFiles('input[type="file"]', filePath);
|
||
|
||
// Wait for location modal to appear
|
||
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
|
||
await locationModal.waitFor({ state: 'visible', timeout: 10000 });
|
||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||
|
||
// Verify expected collection name if provided
|
||
if (options.expectedCollectionName) {
|
||
await expect(locationModal.getByText(options.expectedCollectionName)).toBeVisible();
|
||
}
|
||
|
||
// Set location and import
|
||
await page.locator('#collection-location').fill(collectionLocation);
|
||
await locationModal.getByRole('button', { name: 'Import' }).click();
|
||
|
||
// Wait for collection to appear in sidebar
|
||
if (options.expectedCollectionName) {
|
||
await expect(
|
||
page.locator('#sidebar-collection-name').filter({ hasText: options.expectedCollectionName })
|
||
).toBeVisible();
|
||
|
||
// Configure sandbox mode if requested
|
||
if (options.openWithSandboxMode) {
|
||
await openCollectionAndAcceptSandbox(page, options.expectedCollectionName, options.openWithSandboxMode);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Remove a specific collection from the sidebar
|
||
* @param page - The page object
|
||
* @param collectionName - The name of the collection to remove
|
||
* @returns void
|
||
*/
|
||
const removeCollection = async (page: Page, collectionName: string) => {
|
||
await test.step(`Remove collection "${collectionName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
const collectionRow = page.locator('.collection-name').filter({
|
||
has: page.locator('#sidebar-collection-name', { hasText: collectionName })
|
||
});
|
||
|
||
await collectionRow.hover();
|
||
await collectionRow.locator('.collection-actions .icon').click();
|
||
await locators.dropdown.item('Remove').click();
|
||
|
||
// Wait for and confirm modal
|
||
await locators.modal.title('Remove Collection').waitFor({ state: 'visible' });
|
||
await locators.modal.button('Remove').click();
|
||
await locators.modal.title('Remove Collection').waitFor({ state: 'hidden' });
|
||
|
||
// Verify collection is removed
|
||
await expect(
|
||
page.locator('#sidebar-collection-name').filter({ hasText: collectionName })
|
||
).not.toBeVisible();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Create a folder inside a collection or another folder
|
||
* @param page - The page object
|
||
* @param folderName - The name of the folder to create
|
||
* @param parentName - The name of the parent collection or folder
|
||
* @param isCollection - Whether the parent is a collection (true) or folder (false)
|
||
* @returns void
|
||
*/
|
||
const createFolder = async (
|
||
page: Page,
|
||
folderName: string,
|
||
parentName: string,
|
||
isCollection: boolean = true
|
||
) => {
|
||
await test.step(`Create folder "${folderName}" in "${parentName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
if (isCollection) {
|
||
await locators.sidebar.collection(parentName).hover();
|
||
await locators.actions.collectionActions(parentName).click();
|
||
} else {
|
||
await locators.sidebar.folder(parentName).hover();
|
||
await locators.actions.collectionItemActions(parentName).click();
|
||
}
|
||
|
||
await locators.dropdown.item('New Folder').click();
|
||
await page.getByPlaceholder('Folder Name').fill(folderName);
|
||
await locators.modal.button('Create').click();
|
||
await expect(locators.sidebar.folder(folderName)).toBeVisible();
|
||
});
|
||
};
|
||
|
||
type EnvironmentType = 'collection' | 'global';
|
||
|
||
/**
|
||
* Open the environment selector panel
|
||
* @param page - The page object
|
||
* @param type - The type of environment tab to select
|
||
* @returns void
|
||
*/
|
||
const openEnvironmentSelector = async (page: Page, type: EnvironmentType = 'collection') => {
|
||
await test.step(`Open ${type} environment selector`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
await locators.environment.selector().click();
|
||
|
||
if (type === 'global') {
|
||
await locators.environment.globalTab().click();
|
||
await expect(locators.environment.globalTab()).toHaveClass(/active/);
|
||
} else {
|
||
await expect(locators.environment.collectionTab()).toHaveClass(/active/);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Create a new environment
|
||
* @param page - The page object
|
||
* @param environmentName - The name of the environment
|
||
* @param type - The type of environment (collection or global)
|
||
* @returns void
|
||
*/
|
||
const createEnvironment = async (
|
||
page: Page,
|
||
environmentName: string,
|
||
type: EnvironmentType = 'collection'
|
||
) => {
|
||
await test.step(`Create ${type} environment "${environmentName}"`, async () => {
|
||
await openEnvironmentSelector(page, type);
|
||
|
||
await page.locator('button[id="create-env"]').click();
|
||
|
||
const nameInput = page.locator('input[name="name"]');
|
||
await expect(nameInput).toBeVisible();
|
||
await nameInput.fill(environmentName);
|
||
await page.getByRole('button', { name: 'Create' }).click();
|
||
});
|
||
};
|
||
|
||
type EnvironmentVariable = {
|
||
name: string;
|
||
value: string;
|
||
isSecret?: boolean;
|
||
};
|
||
|
||
/**
|
||
* Add an environment variable to the currently open environment
|
||
* @param page - The page object
|
||
* @param variable - The variable to add (name, value, and optional secret flag)
|
||
* @param index - The index of the variable (0-based)
|
||
* @returns void
|
||
*/
|
||
const addEnvironmentVariable = async (
|
||
page: Page,
|
||
variable: EnvironmentVariable,
|
||
index: number
|
||
) => {
|
||
await test.step(`Add environment variable "${variable.name}"`, async () => {
|
||
const addButton = page.locator('button[data-testid="add-variable"]');
|
||
await addButton.waitFor({ state: 'visible' });
|
||
await addButton.click();
|
||
|
||
// Wait for the new row to be added and the name input to be visible
|
||
const nameInput = page.locator(`input[name="${index}.name"]`);
|
||
await nameInput.waitFor({ state: 'visible' });
|
||
await nameInput.fill(variable.name);
|
||
|
||
// Wait for the CodeMirror editor in the row to be ready
|
||
const variableRow = page.locator('tr').filter({ has: page.locator(`input[name="${index}.name"]`) });
|
||
const codeMirror = variableRow.locator('.CodeMirror');
|
||
await codeMirror.waitFor({ state: 'visible' });
|
||
await codeMirror.click();
|
||
await page.keyboard.type(variable.value);
|
||
|
||
if (variable.isSecret) {
|
||
const secretCheckbox = page.locator(`input[name="${index}.secret"]`);
|
||
await secretCheckbox.waitFor({ state: 'visible' });
|
||
await secretCheckbox.check();
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Add multiple environment variables to the currently open environment
|
||
* @param page - The page object
|
||
* @param variables - Array of variables to add
|
||
* @returns void
|
||
*/
|
||
const addEnvironmentVariables = async (page: Page, variables: EnvironmentVariable[]) => {
|
||
await test.step(`Add ${variables.length} environment variables`, async () => {
|
||
for (let i = 0; i < variables.length; i++) {
|
||
await addEnvironmentVariable(page, variables[i], i);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Save the current environment settings
|
||
* @param page - The page object
|
||
* @returns void
|
||
*/
|
||
const saveEnvironment = async (page: Page) => {
|
||
await test.step('Save environment', async () => {
|
||
await page.getByRole('button', { name: 'Save' }).click();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Close the environment modal/panel
|
||
* @param page - The page object
|
||
* @returns void
|
||
*/
|
||
const closeEnvironmentPanel = async (page: Page) => {
|
||
await test.step('Close environment panel', async () => {
|
||
await page.getByText('×').click();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Select an environment from the dropdown
|
||
* @param page - The page object
|
||
* @param environmentName - The name of the environment to select
|
||
* @param type - The type of environment (collection or global)
|
||
* @returns void
|
||
*/
|
||
const selectEnvironment = async (
|
||
page: Page,
|
||
environmentName: string,
|
||
type: EnvironmentType = 'collection'
|
||
) => {
|
||
await test.step(`Select ${type} environment "${environmentName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
|
||
await locators.environment.selector().click();
|
||
|
||
if (type === 'global') {
|
||
await locators.environment.globalTab().click();
|
||
}
|
||
|
||
await locators.environment.envOption(environmentName).click();
|
||
|
||
// Verify selection
|
||
await expect(page.locator('.current-environment')).toContainText(environmentName);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Send the current request and wait for response
|
||
* @param page - The page object
|
||
* @param expectedStatusCode - Optional expected status code to wait for
|
||
* @param timeout - Timeout in milliseconds (default: 30000)
|
||
* @returns void
|
||
*/
|
||
const sendRequest = async (
|
||
page: Page,
|
||
expectedStatusCode?: number | string,
|
||
timeout: number = 30000
|
||
) => {
|
||
await test.step('Send request', async () => {
|
||
await page.getByTestId('send-arrow-icon').click();
|
||
await page.getByTestId('response-status-code').waitFor({ state: 'visible', timeout });
|
||
|
||
if (expectedStatusCode !== undefined) {
|
||
await expect(page.getByTestId('response-status-code')).toContainText(
|
||
String(expectedStatusCode),
|
||
{ timeout }
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Open a request by clicking on it in the sidebar
|
||
* @param page - The page object
|
||
* @param requestName - The name of the request to open
|
||
* @returns void
|
||
*/
|
||
const openRequest = async (page: Page, requestName: string) => {
|
||
await test.step(`Open request "${requestName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
await locators.sidebar.request(requestName).click();
|
||
await expect(locators.tabs.activeRequestTab()).toContainText(requestName);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Open a request within a folder
|
||
* @param page - The page object
|
||
* @param folderName - The name of the folder
|
||
* @param requestName - The name of the request
|
||
* @returns void
|
||
*/
|
||
const openFolderRequest = async (page: Page, folderName: string, requestName: string) => {
|
||
await test.step(`Open request "${requestName}" in folder "${folderName}"`, async () => {
|
||
const locators = buildCommonLocators(page);
|
||
await locators.sidebar.folderRequest(folderName, requestName).click();
|
||
await expect(locators.tabs.activeRequestTab()).toContainText(requestName);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Get the response body text
|
||
* @param page - The page object
|
||
* @returns The response body text
|
||
*/
|
||
const getResponseBody = async (page: Page): Promise<string> => {
|
||
return await page.locator('.response-pane').innerText();
|
||
};
|
||
|
||
/**
|
||
* Verify response contains specific text
|
||
* @param page - The page object
|
||
* @param texts - Array of texts to verify in the response
|
||
* @returns void
|
||
*/
|
||
const expectResponseContains = async (page: Page, texts: string[]) => {
|
||
await test.step('Verify response content', async () => {
|
||
const responsePane = page.locator('.response-pane');
|
||
for (const text of texts) {
|
||
await expect(responsePane).toContainText(text);
|
||
}
|
||
});
|
||
};
|
||
|
||
export {
|
||
closeAllCollections,
|
||
openCollectionAndAcceptSandbox,
|
||
createCollection,
|
||
createRequest,
|
||
deleteRequest,
|
||
importCollection,
|
||
removeCollection,
|
||
createFolder,
|
||
openEnvironmentSelector,
|
||
createEnvironment,
|
||
addEnvironmentVariable,
|
||
addEnvironmentVariables,
|
||
saveEnvironment,
|
||
closeEnvironmentPanel,
|
||
selectEnvironment,
|
||
sendRequest,
|
||
openRequest,
|
||
openFolderRequest,
|
||
getResponseBody,
|
||
expectResponseContains
|
||
};
|
||
|
||
export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions };
|