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
26 changes: 5 additions & 21 deletions tests/collection/create/create-collection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createCollection, createRequest } from '../../utils/page';
import { buildCommonLocators, closeAllCollections, createCollection } from '../../utils/page';

test.describe('Create collection', () => {
test.afterEach(async ({ page }) => {
Expand Down Expand Up @@ -53,27 +53,11 @@ test.describe('Create collection', () => {
await createCollectionModal.getByRole('button', { name: 'Cancel' }).click();
});

test('Create collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
test('TC99: Verify user able to Create a new collection', { tag: '@sanity' }, async ({ page, createTmpDir }) => {
const collectionName = 'test-collection';
const requestName = 'ping';

const locators = buildCommonLocators(page);
await createCollection(page, collectionName, await createTmpDir(collectionName));

// Create a new request using the dialog/modal flow
await createRequest(page, requestName, collectionName);

// Set the URL
await page.locator('#request-url .CodeMirror').click();
await page.locator('#request-url').locator('textarea').fill('http://localhost:8081');
await page.locator('#request-actions').getByTitle('Save Request').click();

// Send a request
await page.locator('#request-url .CodeMirror').click();
await page.locator('#request-url').locator('textarea').fill('/ping');
await page.locator('#request-actions').getByTitle('Save Request').click();
await page.getByTestId('send-arrow-icon').click();

// Verify the response
await expect(page.getByRole('main')).toContainText('200 OK');
await expect(locators.toast.collectionCreated()).toBeVisible();
await expect(locators.sidebar.collection(collectionName)).toBeVisible();
Comment on lines +56 to +61

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the collection name unique per test instance.

Line 57 hardcodes 'test-collection', but the cleanup here only closes collections. That leaves this spec vulnerable to stale app state: an existing collection with the same name can satisfy the sidebar assertion, or reruns/workers can trip duplicate-name handling. Please derive the visible collection name from isolated per-test state instead of a fixed literal. As per coding guidelines, "E2E tests must be parallel-safe. No shared user data directories... Each test gets isolated temp paths and unique workspace/project names." As per path instructions, "Each test gets isolated temp paths and unique workspace/project names."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/collection/create/create-collection.spec.ts` around lines 56 - 61, The
collection name in create-collection.spec.ts is hardcoded, which makes TC99
vulnerable to stale state and parallel test collisions. Update the test to
derive the visible collection name from per-test isolated state instead of a
fixed literal, and pass that same unique value through createCollection and the
sidebar assertion in buildCommonLocators/collection.collectionName usage so each
run uses a distinct collection.

Sources: Coding guidelines, Path instructions

});
});
77 changes: 54 additions & 23 deletions tests/import/url-import/github-repository-import.spec.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections } from '../../utils/page';
import { buildCommonLocators, closeAllCollections } from '../../utils/page';

test.describe('GitHub Repository URL Import', () => {
test.describe('Git repository import', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});

test('GitHub repository URL import', async ({ page }) => {
const githubUrl = 'https://github.com/usebruno/github-rest-api-collection';
test('TC114: Verify import collection through Cloning from Git Repository', { tag: '@sanity' }, async ({
page,
electronApp,
createTmpDir
}) => {
const gitUrl = 'https://github.com/usebruno/github-rest-api-collection';
const collectionName = 'github rest api';
const cloneLocation = await createTmpDir('git-clone');
const locators = buildCommonLocators(page);
const importLocators = locators.import;
const { cloneGit } = importLocators;

// Test GitHub repository import
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
await test.step('Step 01: Go to menu click on the + icon', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

please remove step numbers

await locators.plusMenu.button().click();
await expect(locators.plusMenu.importCollection()).toBeVisible();
});

// Wait for import collection modal to be ready
const importModal = page.getByRole('dialog');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await test.step('Step 02: Click on Import a collection', async () => {
await locators.plusMenu.importCollection().click();

// Select the GitHub tab
await page.getByTestId('github-tab').click();
await importLocators.modal().waitFor({ state: 'visible' });
await expect(importLocators.modalTitle()).toContainText('Import Collection');
await expect(importLocators.fileTab()).toBeVisible();
await expect(importLocators.gitRepositoryTab()).toBeVisible();
await expect(importLocators.urlTab()).toBeVisible();
});

// Fill in the URL input
await page.getByTestId('git-url-input').fill(githubUrl);
await page.locator('#clone-git-button').click();
await test.step('Step 03: Go to git repository section and Enter the URL and select the location to save the repo then click on the import button', async () => {
await importLocators.gitRepositoryTab().click();
await importLocators.gitUrlInput().fill(gitUrl);
await importLocators.cloneGitButton().click();
await importLocators.loader().waitFor({ state: 'hidden' });

// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
await expect(cloneGit.modal()).toBeVisible();
await expect(cloneGit.modal()).toContainText(gitUrl);

// Verify that the Clone Git Repository modal is displayed
const cloneModal = page.getByRole('dialog');
await expect(cloneModal.locator('.bruno-modal-header-title')).toContainText('Clone Git Repository');
await electronApp.evaluate(({ dialog }, dir) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: [dir]
});
}, cloneLocation);
Comment on lines +45 to +50

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Restore the Electron dialog stub before the test exits.

This overwrites dialog.showOpenDialog on the worker-scoped electronApp and never puts it back, so later specs in the same worker can inherit the fake dialog and become order-dependent. Save the original function and restore it after the location picker interaction completes.

As per coding guidelines, E2E tests must be parallel-safe with no shared global app state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/import/url-import/github-repository-import.spec.ts` around lines 45 -
50, Restore the worker-scoped Electron dialog stub in the github repository
import spec by saving the original dialog.showOpenDialog before overriding it
inside the electronApp.evaluate block and restoring it immediately after the
location picker interaction completes. Update the test flow around the dialog
stub in github-repository-import.spec.ts so the fake dialog is only active for
the minimal scope and does not leak into later specs that share the same
electronApp worker state.

Source: Coding guidelines


// Cleanup: close any open modals using Cancel button (avoids form validation)
await page.getByRole('button', { name: 'Cancel' }).click();
await cloneGit.locationInput().click();
await expect(cloneGit.locationInput()).toHaveValue(cloneLocation);

await cloneGit.cloneButton().click();
await expect(cloneGit.collectionItemTitle(collectionName)).toBeVisible();
});

await test.step('Step 04: Select the desired collections and click on open', async () => {
await cloneGit.collectionCheckbox(collectionName).check();

await cloneGit.openButton().click();
await cloneGit.modal().waitFor({ state: 'hidden' });

await expect(locators.sidebar.collection(collectionName)).toBeVisible();
await expect(locators.toast.repositoryClonedSuccessfully()).toBeVisible();
});
});
});
87 changes: 51 additions & 36 deletions tests/import/url-import/insomnia-url-import.spec.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,60 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, openCollection } from '../../utils/page';
import { buildCommonLocators, closeAllCollections, openCollection } from '../../utils/page';

test.describe('Insomnia URL Import', () => {
test.afterEach(async ({ page }) => {
// cleanup: close all collections
await closeAllCollections(page);
});

test('Insomnia URL import', async ({ page, createTmpDir }) => {
const insomniaUrl = 'https://raw.githubusercontent.com/usebruno/bruno/refs/heads/main/tests/import/insomnia/fixtures/insomnia-v5.yaml';

await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();

// Wait for import collection modal to be ready
const importModal = page.getByTestId('import-collection-modal');
await importModal.waitFor({ state: 'visible' });

await page.getByTestId('url-tab').click();
await page.getByTestId('url-input').waitFor({ state: 'visible' });
await page.getByTestId('url-input').fill(insomniaUrl);
await page.locator('#import-url-button').click();

// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });

// Verify that the collection location modal appears
const locationModal = page.getByTestId('import-collection-location-modal');
await expect(locationModal.getByText('Test API Collection v5')).toBeVisible();

// Select a location and import
await page.locator('#collection-location').fill(await createTmpDir('test-api-collection-v5'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });

// Verify the collection was imported successfully and configure it
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v5')).toBeVisible();
await openCollection(page, 'Test API Collection v5');

// Verify these folder names are present
await expect(page.locator('.collection-item-name').getByText('API Tests')).toBeVisible();
await expect(page.locator('.collection-item-name').getByText('Data Management')).toBeVisible();
});
test('TC813: Verify Import collection from Valid Insomnia Export from Direct URL',
{ tag: '@sanity' },
async ({ page, createTmpDir }) => {
const insomniaUrl
= 'https://raw.githubusercontent.com/usebruno/bruno/refs/heads/main/tests/import/insomnia/fixtures/insomnia-v5.yaml';
const collectionName = 'Test API Collection v5';
const collectionLocation = await createTmpDir('test-api-collection-v5');
const locators = buildCommonLocators(page);
const importLocators = locators.import;

await test.step('Step 01: Navigate to the Import functionality in Bruno', async () => {
await locators.plusMenu.button().click();
await expect(locators.plusMenu.importCollection()).toBeVisible();
await locators.plusMenu.importCollection().click();

await importLocators.modal().waitFor({ state: 'visible' });
await expect(importLocators.modalTitle()).toContainText('Import Collection');
await expect(importLocators.fileTab()).toBeVisible();
await expect(importLocators.gitRepositoryTab()).toBeVisible();
await expect(importLocators.urlTab()).toBeVisible();
});

await test.step('Step 02: Select \'Import from URL\' option', async () => {
await importLocators.urlTab().click();
await expect(importLocators.urlInput()).toBeVisible();
await expect(importLocators.importUrlButton()).toBeVisible();
});

await test.step('Step 03: Enter a valid Insomnia export URL', async () => {
await importLocators.urlInput().fill(insomniaUrl);
await expect(importLocators.urlInput()).toHaveValue(insomniaUrl);
});

await test.step('Step 04: Initiate the import process', async () => {
await importLocators.importUrlButton().click();
await importLocators.loader().waitFor({ state: 'hidden' });
await expect(importLocators.locationModal()).toBeVisible();
await expect(importLocators.locationModal().getByText(collectionName)).toBeVisible();
});

await test.step('Step 05: Verify successful import of the Insomnia export', async () => {
await importLocators.locationInput().fill(collectionLocation);
await importLocators.importButton(importLocators.locationModal()).click();
await importLocators.locationModal().waitFor({ state: 'hidden' });
await expect(locators.sidebar.collection(collectionName)).toBeVisible();
await openCollection(page, collectionName);
await expect(page.locator('.collection-item-name').getByText('API Tests')).toBeVisible();
await expect(page.locator('.collection-item-name').getByText('Data Management')).toBeVisible();
});
});
});
26 changes: 26 additions & 0 deletions tests/utils/page/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export const buildCommonLocators = (page: Page) => ({
submitButton: () => page.locator('.bruno-modal-footer .submit'),
newRequestMethodOption: (id: string) => page.getByTestId(`method-selector-${id.toLowerCase()}`)
},
toast: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Move toast locators to own file. Add generic method like for success, error, custom etc, no need to create methods for every messages.
eg: toast.success(message)

// merge with common locators
toast: buildToastLocators(page),
// usage
const { toast, sidebar, environment } = buildCommonLocators();

collectionCreated: () => page.getByText('Collection created!'),
repositoryClonedSuccessfully: () => page.getByText('Repository cloned successfully'),
collectionImportedSuccessfully: () => page.getByText('Collection imported successfully')
},
environment: {
selector: () => page.getByTestId('environment-selector-trigger'),
collectionTab: () => page.getByTestId('env-tab-collection'),
Expand Down Expand Up @@ -237,13 +242,34 @@ export const buildCommonLocators = (page: Page) => ({
},
import: {
modal: () => page.locator('[data-testid="import-collection-modal"]'),
modalTitle: () => page.locator('[data-testid="import-collection-modal"] .bruno-modal-header-title'),
fileTab: () => page.getByTestId('file-tab'),
gitRepositoryTab: () => page.getByTestId('github-tab'),
urlTab: () => page.getByTestId('url-tab'),
gitUrlInput: () => page.getByTestId('git-url-input'),
urlInput: () => page.getByTestId('url-input'),
cloneGitButton: () => page.locator('#clone-git-button'),
importUrlButton: () => page.locator('#import-url-button'),
loader: () => page.locator('#import-collection-loader'),
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'),
cloneGit: {
modal: () => page.locator('.bruno-modal-card').filter({ hasText: 'Clone Git Repository' }),
locationInput: () =>
page.locator('.bruno-modal-card').filter({ hasText: 'Clone Git Repository' }).locator('#collection-location'),
cloneButton: () =>
page.locator('.bruno-modal-card').filter({ hasText: 'Clone Git Repository' }).getByRole('button', { name: 'Clone', exact: true }),
openButton: () =>
page.locator('.bruno-modal-card').filter({ hasText: 'Clone Git Repository' }).getByRole('button', { name: 'Open', exact: true }),
collectionItemTitle: (name: string) => page.locator('.selection-item-title').filter({ hasText: name }),
collectionCheckbox: (name: string) =>
page.locator('.selection-item').filter({ hasText: name }).locator('input[type="checkbox"]')
Comment on lines +269 to +271

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Scope the collection-selection locators to the clone modal.

These two locators search the whole page and use partial text matching, so another .selection-item* elsewhere with the same/subset name can satisfy them and make the import flow flaky. Anchor both to the 'Clone Git Repository' modal and match the collection name exactly. As per path instructions, replace brittle selectors with stable user-facing selectors in E2E tests.

Proposed fix
-      collectionItemTitle: (name: string) => page.locator('.selection-item-title').filter({ hasText: name }),
-      collectionCheckbox: (name: string) =>
-        page.locator('.selection-item').filter({ hasText: name }).locator('input[type="checkbox"]')
+      collectionItemTitle: (name: string) =>
+        page
+          .locator('.bruno-modal-card')
+          .filter({ hasText: 'Clone Git Repository' })
+          .locator('.selection-item-title')
+          .getByText(name, { exact: true }),
+      collectionCheckbox: (name: string) =>
+        page
+          .locator('.bruno-modal-card')
+          .filter({ hasText: 'Clone Git Repository' })
+          .locator('.selection-item')
+          .filter({ has: page.getByText(name, { exact: true }) })
+          .locator('input[type="checkbox"]')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
collectionItemTitle: (name: string) => page.locator('.selection-item-title').filter({ hasText: name }),
collectionCheckbox: (name: string) =>
page.locator('.selection-item').filter({ hasText: name }).locator('input[type="checkbox"]')
collectionItemTitle: (name: string) =>
page
.locator('.bruno-modal-card')
.filter({ hasText: 'Clone Git Repository' })
.locator('.selection-item-title')
.getByText(name, { exact: true }),
collectionCheckbox: (name: string) =>
page
.locator('.bruno-modal-card')
.filter({ hasText: 'Clone Git Repository' })
.locator('.selection-item')
.filter({ has: page.getByText(name, { exact: true }) })
.locator('input[type="checkbox"]')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/page/locators.ts` around lines 269 - 271, The
collection-selection locators are too broad and can match elements outside the
clone modal, causing flaky imports. Update the `collectionItemTitle` and
`collectionCheckbox` helpers in `locators.ts` to scope all queries to the
`'Clone Git Repository'` modal and use exact matching for the collection name
instead of partial text. Use the existing locator helpers as the entry point,
and replace the current global `.selection-item*` selectors with stable
user-facing modal-scoped selectors.

Source: Path instructions

},
...(() => {
const issuesToast = () => page.getByTestId('import-issues-toast').last();
return {
Expand Down
Loading