Compare commits

...

3 Commits

Author SHA1 Message Date
Bijin A B
5ef8867635 chore: fix flaky tests 2026-02-14 20:46:08 +05:30
Bijin A B
1d126dcb65 fix: flaky tests - standardize save keyboard shortcut across tests (#7141) 2026-02-14 03:58:02 +05:30
Bijin A B
0c3b828b09 fix: update header validation test to use triple-click for selecting all text (#7140) 2026-02-14 01:40:51 +05:30
58 changed files with 267 additions and 251 deletions

View File

@@ -1,4 +1,4 @@
import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test';
import { test as baseTest, BrowserContext, ElectronApplication, Page, TestInfo } from '@playwright/test';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
@@ -25,6 +25,71 @@ async function recursiveCopy(src: string, dest: string) {
}
}
const TRACING_OPTIONS = { screenshots: true, snapshots: true, sources: true };
function isTracingEnabled(testInfo: TestInfo): boolean {
return !!(testInfo as any)._tracing.traceOptions();
}
async function usePageWithTracing(
context: BrowserContext,
page: Page,
testInfo: TestInfo,
use: (page: Page) => Promise<void>,
options: { initTracing?: boolean; useChunks?: boolean } = {}
) {
const { initTracing = false, useChunks = true } = options;
if (!isTracingEnabled(testInfo)) {
await use(page);
return;
}
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
if (initTracing) {
try {
await context.tracing.start(TRACING_OPTIONS);
} catch (e) { }
}
if (useChunks) {
await context.tracing.startChunk();
await use(page);
await context.tracing.stopChunk({ path: tracePath });
} else {
await use(page);
await context.tracing.stop({ path: tracePath });
}
await testInfo.attach('trace', { path: tracePath });
}
/**
* Gracefully close an Electron app by telling it to exit with code 0.
* This avoids the macOS "quit unexpectedly" crash dialog that appears when
* app.context().close() kills subprocesses (renderer/GPU) abruptly before
* the main process can shut down cleanly.
*
* Emits 'before-quit' first so cleanup handlers run (e.g., saving cookies to disk),
* since app.exit() bypasses all lifecycle events.
*/
export async function closeElectronApp(app: ElectronApplication) {
try {
await app.evaluate(({ app }) => {
app.emit('before-quit');
app.exit(0);
});
} catch {
// Expected: process exited before the CDP response was sent
}
try {
await app.close();
} catch {
// Process already exited
}
}
export const test = baseTest.extend<
{
context: BrowserContext;
@@ -113,8 +178,7 @@ export const test = baseTest.extend<
return app;
});
for (const app of apps) {
await app.context().close();
await app.close();
await closeElectronApp(app);
}
},
{ scope: 'worker' }
@@ -130,10 +194,9 @@ export const test = baseTest.extend<
context: async ({ electronApp }, use, testInfo) => {
const context = await electronApp.context();
const tracingOptions = (testInfo as any)._tracing.traceOptions();
if (tracingOptions) {
if (isTracingEnabled(testInfo)) {
try {
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
await context.tracing.start(TRACING_OPTIONS);
} catch (e) { }
}
await use(context);
@@ -141,32 +204,14 @@ export const test = baseTest.extend<
page: async ({ electronApp, context }, use, testInfo) => {
const page = await electronApp.firstWindow();
const tracingOptions = (testInfo as any)._tracing.traceOptions();
if (tracingOptions) {
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
await context.tracing.startChunk();
await use(page);
await context.tracing.stopChunk({ path: tracePath });
await testInfo.attach('trace', { path: tracePath });
} else {
await use(page);
}
await usePageWithTracing(context, page, testInfo, use);
},
newPage: async ({ launchElectronApp }, use, testInfo) => {
const app = await launchElectronApp();
const context = await app.context();
const page = await app.firstWindow();
const tracingOptions = (testInfo as any)._tracing.traceOptions();
if (tracingOptions) {
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
await use(page);
await context.tracing.stop({ path: tracePath });
await testInfo.attach('trace', { path: tracePath });
} else {
await use(page);
}
await usePageWithTracing(context, page, testInfo, use, { initTracing: true, useChunks: false });
},
reuseOrLaunchElectronApp: [
@@ -208,8 +253,7 @@ export const test = baseTest.extend<
// Clean up all app instances
for (const { app } of appInstances) {
await app.context().close();
await app.close();
await closeElectronApp(app);
}
},
@@ -231,19 +275,7 @@ export const test = baseTest.extend<
const context = await app.context();
const page = await app.firstWindow();
const tracingOptions = (testInfo as any)._tracing.traceOptions();
if (tracingOptions) {
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
try {
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
} catch (e) { }
await context.tracing.startChunk();
await use(page);
await context.tracing.stopChunk({ path: tracePath });
await testInfo.attach('trace', { path: tracePath });
} else {
await use(page);
}
await usePageWithTracing(context, page, testInfo, use, { initTracing: true });
}
});

View File

@@ -23,11 +23,11 @@ test.describe('Draft values are used in requests', () => {
const nameEditor = headerRow.locator('.CodeMirror').first();
await nameEditor.click();
await page.keyboard.type('X-Draft-Header');
await headerRow.locator('textarea').first().fill('X-Draft-Header');
const valueEditor = headerRow.locator('.CodeMirror').nth(1);
await valueEditor.click();
await page.keyboard.type('draft-value-123');
await headerRow.locator('textarea').nth(1).fill('draft-value-123');
// Verify draft indicator appears (header is not saved yet)
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
@@ -51,11 +51,11 @@ test.describe('Draft values are used in requests', () => {
const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first();
await folderNameEditor.click();
await page.keyboard.type('X-Folder-Draft-Header');
await folderHeaderRow.locator('textarea').first().fill('X-Folder-Draft-Header');
const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1);
await folderValueEditor.click();
await page.keyboard.type('folder-draft-value-123');
await folderHeaderRow.locator('textarea').nth(1).fill('folder-draft-value-123');
// Create a request in the collection
// Create a new request via collection menu
@@ -122,7 +122,7 @@ test.describe('Draft values are used in requests', () => {
// Create a new request from collection menu
const collection = page.locator('.collection-name').filter({ hasText: collectionName });
await collection.hover();
await collection.locator('.collection-actions .icon').click();
await collection.locator('.collection-actions .icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByTestId('request-name').fill('Test Request');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();

View File

@@ -29,7 +29,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
// Add a request to the folder to make it more realistic
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).hover();
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click();
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('test-request-in-folder');
await page.locator('#new-request-url .CodeMirror').click();
@@ -126,7 +126,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
// Add a request to the folder to make it more realistic
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover();
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click();
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('http-request');
await page.locator('#new-request-url .CodeMirror').click();

View File

@@ -99,7 +99,8 @@ test.describe('Tag persistence', () => {
await locators.tags.input().fill('smoke');
await locators.tags.input().press('Enter');
await expect(locators.tags.item('smoke')).toBeVisible();
await page.keyboard.press('Meta+s');
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
// Create another folder
await locators.sidebar.collectionRow('test-collection').hover();

View File

@@ -1,4 +1,4 @@
import { test, expect } from '../../playwright';
import { test, expect, closeElectronApp } from '../../playwright';
test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {
// Create a temporary user-data directory so we control where the cookies store file is written.
@@ -26,7 +26,7 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch
await expect(page1.getByText('example.com')).toBeVisible();
await app1.close();
await closeElectronApp(app1);
// Second launch verify the cookie was persisted and re-loaded
const app2 = await launchElectronApp({ userDataPath });
@@ -39,5 +39,5 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch
// The domain we added earlier should still be present.
await expect(page2.getByText('example.com')).toBeVisible();
await app2.close();
await closeElectronApp(app2);
});

View File

@@ -1,4 +1,4 @@
import { test, expect } from '../../playwright';
import { test, expect, closeElectronApp } from '../../playwright';
import * as path from 'path';
import * as fs from 'fs/promises';
@@ -24,7 +24,7 @@ test('should handle corrupted passkey and still display saved cookie list', asyn
await expect(page1.getByText('example.com')).toBeVisible();
await app1.close();
await closeElectronApp(app1);
// 2. Corrupt the encryptedPasskey in cookies.json
const cookiesFilePath = path.join(userDataPath, 'cookies.json');
@@ -43,5 +43,5 @@ test('should handle corrupted passkey and still display saved cookie list', asyn
// The domain row should still be visible (even if cookie values are blank).
await expect(page2.getByText('example.com')).toBeVisible();
await app2.close();
await closeElectronApp(app2);
});

View File

@@ -33,7 +33,8 @@ test.describe('EditableTable - Focus and Placeholder', () => {
await expect(nameInput).toBeFocused();
// Save the request
await page.keyboard.press('Meta+s');
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
// Wait for save toast
await expect(page.getByText('Request saved successfully').last()).toBeVisible();

View File

@@ -38,7 +38,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
// we restart the app to confirm that the environment variable is persisted
const newApp = await restartApp();
@@ -59,7 +59,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
await newEnvTab.hover();
await newEnvTab.getByTestId('request-tab-close-icon').click();
await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });
// Restore the original Stage.bru file
fs.writeFileSync(originalStageBruPath, originalStageBruContent);

View File

@@ -28,7 +28,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
// we restart the app to confirm that the environment variable is not persisted
const newApp = await restartApp();
@@ -48,7 +48,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
await expect(newPage.locator('.table-container tbody')).not.toContainText('token');
await newEnvTab.hover();
await newEnvTab.getByTestId('request-tab-close-icon').click();
await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });
await newPage.close();
});
});

View File

@@ -27,7 +27,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
}
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
}
} catch (error) {
// Ignore cleanup errors to avoid masking test failures
@@ -85,7 +85,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible();
await expect(page.getByRole('row', { name: 'value2' }).getByRole('cell').nth(2)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
await test.step('Verify variables are persisted to file', async () => {

View File

@@ -35,6 +35,6 @@ test.describe('Collection Environment Configuration Selection Tests', () => {
await expect(activeEnvItem).toContainText('prod');
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});

View File

@@ -28,6 +28,6 @@ test.describe('Global Environment Configuration Selection Tests', () => {
await expect(activeEnvItem).toContainText(currentEnvName);
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});

View File

@@ -47,7 +47,7 @@ test.describe.serial('Collection Environment Import Tests', () => {
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
await test.step('Clean up after test', async () => {
@@ -128,7 +128,7 @@ test.describe.serial('Collection Environment Import Tests', () => {
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
await test.step('Clean up after test', async () => {

View File

@@ -62,7 +62,7 @@ test.describe.serial('Global Environment Import Tests', () => {
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
@@ -145,7 +145,7 @@ test.describe.serial('Global Environment Import Tests', () => {
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
});

View File

@@ -27,6 +27,7 @@ test.describe('Collection Environment Import Tests', () => {
// Select a location and import
await page.locator('#collection-location').fill(await createTmpDir('collection-env-import-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
await expect(
page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })).toBeVisible({ timeout: 10000 });
@@ -61,7 +62,7 @@ test.describe('Collection Environment Import Tests', () => {
await expect(page.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
await expect(page.locator('input[name="5.secret"]')).toBeChecked();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.locator('.collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');

View File

@@ -57,7 +57,7 @@ test.describe('Global Environment Import Tests', () => {
await expect(variablesTable.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
await expect(variablesTable.locator('input[name="5.secret"]')).toBeChecked();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.locator('#collection-environment-test-collection .collection-item-name').first().click();
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');

View File

@@ -33,6 +33,12 @@ test.describe('Multiline Variables - Write Test', () => {
await expect(emptyRowNameInput).toBeVisible();
await emptyRowNameInput.fill('multiline_data_json');
// After filling the name, the table appends a new empty row causing persistent layout shifts.
// Use force:true to bypass Playwright's stability check on the CodeMirror click.
const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') });
await expect(variableRow).toBeVisible();
const codeMirror = variableRow.locator('.CodeMirror');
const jsonValue = `{
"user": {
"name": "John Doe",
@@ -48,15 +54,13 @@ test.describe('Multiline Variables - Write Test', () => {
}
}`;
const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') });
const codeMirror = variableRow.locator('.CodeMirror');
await codeMirror.click();
await codeMirror.click({ force: true });
await page.keyboard.insertText(jsonValue);
await page.getByTestId('save-env').click();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.getByTestId('send-arrow-icon').click();

View File

@@ -58,7 +58,7 @@ test.describe('Global Environment Variable Update via Script', () => {
await test.step('Close the global environment config tab.', async () => {
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
});

View File

@@ -35,7 +35,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
await page.getByTestId('save-env').click();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
// Request contains a script that sets the non-string global variables.
@@ -124,7 +124,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
});

View File

@@ -1,6 +1,8 @@
import { test, expect } from '../../../playwright';
import { buildGrpcCommonLocators } from '../../utils/page/locators';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('make grpc requests', () => {
const setupGrpcTest = async (page) => {
const locators = buildGrpcCommonLocators(page);
@@ -53,7 +55,7 @@ test.describe('make grpc requests', () => {
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
});
});
@@ -94,7 +96,7 @@ test.describe('make grpc requests', () => {
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
});
});
@@ -144,7 +146,7 @@ test.describe('make grpc requests', () => {
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
});
});
@@ -196,7 +198,7 @@ test.describe('make grpc requests', () => {
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
});
});
});

View File

@@ -26,7 +26,8 @@ test.describe('grpc metadata', () => {
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
});
});
});

View File

@@ -15,7 +15,7 @@ test.describe('Grpc Collection - Method Search Functionality', () => {
test.afterEach(async ({ pageWithUserData: page }) => {
await test.step('Close the gRPC sayHello tab without saving changes', async () => {
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click();
await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click({ force: true });
await page.getByRole('button', { name: 'Don\'t Save' }).click();
});
});

View File

@@ -18,8 +18,8 @@ test.describe('Invalid File Handling', () => {
// Wait for the loader to disappear
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const hasError = await page.getByText('Failed to parse the file ensure it is valid JSON or YAML').first().isVisible();
expect(hasError).toBe(true);
// Use auto-retrying assertion instead of snapshot isVisible() check
await expect(page.getByText('Failed to parse the file ensure it is valid JSON or YAML').first()).toBeVisible();
// Cleanup: close any open modals
await page.getByTestId('modal-close-button').click();

View File

@@ -39,6 +39,7 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => {
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-env-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4 with Environments')).toBeVisible();
@@ -199,7 +200,7 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => {
await test.step('Close environment tab', async () => {
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
});

View File

@@ -38,6 +38,7 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => {
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-env-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
await openCollection(page, 'Test API Collection v5 with Environments');
});
@@ -224,7 +225,7 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => {
await test.step('Close environment tab', async () => {
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
});
});

View File

@@ -16,8 +16,8 @@ test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
await page.setInputFiles('input[type="file"]', insomniaFile);
// Check for error message - this should fail during JSON parsing
const hasError = await page.getByText('Failed to parse the file').first().isVisible();
expect(hasError).toBe(true);
// Use auto-retrying assertion instead of snapshot isVisible() check
await expect(page.getByText('Failed to parse the file').first()).toBeVisible();
// Cleanup: close any open modals
await page.getByTestId('modal-close-button').click();

View File

@@ -31,6 +31,7 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
// select a location
await page.locator('#collection-location').fill(await createTmpDir('duplicate-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
// verify the collection was imported successfully
await expect(page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection')).toBeVisible();

View File

@@ -75,6 +75,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
await test.step('Complete import by clicking import button', async () => {
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
});
await test.step('Handle sandbox modal', async () => {
@@ -203,6 +204,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
await test.step('Complete import by clicking import button', async () => {
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
});
await test.step('Handle sandbox modal', async () => {

View File

@@ -30,6 +30,7 @@ test.describe('OpenAPI Newline Handling', () => {
// select a location
await page.locator('#collection-location').fill(await createTmpDir('newline-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
// verify the collection was imported successfully
await expect(page.locator('#sidebar-collection-name').getByText('Newline Test Collection')).toBeVisible();

View File

@@ -34,6 +34,7 @@ test.describe('OpenAPI Path-Based Grouping', () => {
// Select a location and import
await page.locator('#collection-location').fill(await createTmpDir('path-grouping-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
// Verify the collection was imported successfully
await expect(page.locator('#sidebar-collection-name').getByText('Path Grouping Test API')).toBeVisible();

View File

@@ -79,6 +79,7 @@ test.describe('Import Postman Collection with Examples', () => {
await test.step('Complete import by clicking import button', async () => {
const locationModal = page.locator('[data-testid="import-collection-location-modal"]');
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
});
await test.step('Open collection', async () => {

View File

@@ -16,9 +16,7 @@ test.describe('Invalid Postman Collection - Invalid JSON', () => {
await page.setInputFiles('input[type="file"]', postmanFile);
// Check for error message
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
expect(hasError).toBe(true);
await expect(page.getByText('Unsupported collection format').first()).toBeVisible();
// Cleanup: close any open modals
await page.getByTestId('modal-close-button').click();

View File

@@ -16,8 +16,7 @@ test.describe('Invalid Postman Collection - Missing Info', () => {
await page.setInputFiles('input[type="file"]', postmanFile);
// Check for error message
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
expect(hasError).toBe(true);
await expect(page.getByText('Unsupported collection format').first()).toBeVisible();
// Cleanup: close any open modals
await page.getByTestId('modal-close-button').click();

View File

@@ -16,8 +16,7 @@ test.describe('Invalid Postman Collection - Invalid Schema', () => {
await page.setInputFiles('input[type="file"]', postmanFile);
// Check for error message
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
expect(hasError).toBe(true);
await expect(page.getByText('Unsupported collection format').first()).toBeVisible();
// Cleanup: close any open modals
await page.getByTestId('modal-close-button').click();

View File

@@ -32,6 +32,7 @@ test.describe('Insomnia URL Import', () => {
// 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();

View File

@@ -36,6 +36,7 @@ test.describe('OpenAPI URL Import', () => {
// Select a location and import with default grouping (tags)
await page.locator('#collection-location').fill(await createTmpDir('swagger-petstore'));
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('Swagger Petstore')).toBeVisible();
@@ -82,6 +83,7 @@ test.describe('OpenAPI URL Import', () => {
// Select a location and import with path-based grouping
await page.locator('#collection-location').fill(await createTmpDir('swagger-petstore-path'));
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('Swagger Petstore')).toBeVisible();

View File

@@ -32,6 +32,7 @@ test.describe('Postman URL Import', () => {
// Select a location and import
await page.locator('#collection-location').fill(await createTmpDir('postman-v21-collection'));
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('Postman v2.1 Collection')).toBeVisible();

View File

@@ -37,6 +37,7 @@ test.describe('Import WSDL Collection', () => {
// select a location
await page.locator('#collection-location').fill(await createTmpDir('wsdl-xml-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();
});
@@ -98,6 +99,7 @@ test.describe('Import WSDL Collection', () => {
// select a location
await page.locator('#collection-location').fill(await createTmpDir('wsdl-json-test'));
await locationModal.getByRole('button', { name: 'Import' }).click();
await locationModal.waitFor({ state: 'hidden' });
});
await test.step('Verify that the collection was imported successfully', async () => {

View File

@@ -1,5 +1,5 @@
import path from 'path';
import { test, expect, errors } from '../../playwright';
import { test, expect, errors, closeElectronApp } from '../../playwright';
const env = {
DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'
@@ -28,7 +28,7 @@ test.describe('Onboarding', () => {
await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');
// Clean up
await app.close();
await closeElectronApp(app);
});
test('should not create duplicate collections on subsequent launches', async ({ launchElectronApp, createTmpDir }) => {
@@ -51,7 +51,7 @@ test.describe('Onboarding', () => {
await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');
// Close the first app instance
await app.close();
await closeElectronApp(app);
// Restart app - should not create sample collection again
const newApp = await launchElectronApp({ userDataPath, dotEnv: env });
@@ -71,7 +71,7 @@ test.describe('Onboarding', () => {
await expect(newPage.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');
// Clean up
await newApp.close();
await closeElectronApp(newApp);
});
test('should not recreate sample collection after user deletes it', async ({ launchElectronApp, reuseOrLaunchElectronApp, createTmpDir }) => {

View File

@@ -52,7 +52,7 @@ test.describe('Autosave', () => {
// Close preferences tab using the close icon
const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });
await preferencesTab.hover();
await preferencesTab.locator('.close-icon').click();
await preferencesTab.locator('.close-icon').click({ force: true });
// Click on the request to make it active again
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
@@ -77,7 +77,7 @@ test.describe('Autosave', () => {
// Close and reopen the request tab to verify persistence
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });
// Reopen request
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
@@ -108,7 +108,7 @@ test.describe('Autosave', () => {
// Close preferences tab using the close icon
const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });
await preferencesTab.hover();
await preferencesTab.locator('.close-icon').click();
await preferencesTab.locator('.close-icon').click({ force: true });
// Click on the request to make it active again
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
@@ -192,7 +192,7 @@ test.describe('Autosave', () => {
// Close preferences tab using the close icon
const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });
await preferencesTab.hover();
await preferencesTab.locator('.close-icon').click();
await preferencesTab.locator('.close-icon').click({ force: true });
// Click on the request to make it active again
await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();
@@ -207,7 +207,7 @@ test.describe('Autosave', () => {
// Close and reopen the request tab to verify persistence
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });
// Reopen request
await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();

View File

@@ -108,7 +108,7 @@ test.describe('manage protofile', () => {
await method.click();
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.getByRole('button', { name: 'Don\'t Save' }).click();
});
@@ -127,8 +127,8 @@ test.describe('manage protofile', () => {
// Use more specific selector for proto file selection
await page.locator('div').filter({ hasText: /^product\.proto\.\.\/protos\/services\/product\.proto$/ }).first().click();
const loadedMethodsMessage = await page.getByText('Failed to load gRPC methods: Unknown error').first().isVisible();
expect(loadedMethodsMessage).toBe(true);
// Verify the error message is visible (auto-retrying)
await expect(page.getByText('Failed to load gRPC methods: Unknown error').first()).toBeVisible();
// Check that methods dropdown is not visible when loading fails
const methodsDropdown = page.getByTestId('grpc-methods-dropdown');
@@ -136,7 +136,7 @@ test.describe('manage protofile', () => {
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.getByRole('button', { name: 'Don\'t Save' }).click();
});
@@ -180,7 +180,7 @@ test.describe('manage protofile', () => {
// Clean up
const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });
await requestTab.hover();
await requestTab.getByTestId('request-tab-close-icon').click();
await requestTab.getByTestId('request-tab-close-icon').click({ force: true });
await page.getByRole('button', { name: 'Don\'t Save' }).click();
});
});

View File

@@ -22,7 +22,7 @@ test.describe('Copy and Paste Folders', () => {
// Add a request to the folder
await folder.hover();
await folder.locator('.menu-icon').click();
await folder.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('request-in-folder');
await page.locator('#new-request-url .CodeMirror').click();
@@ -34,7 +34,7 @@ test.describe('Copy and Paste Folders', () => {
// Copy the folder
await folder.hover();
await folder.locator('.menu-icon').click();
await folder.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
// Paste into the collection root
@@ -77,13 +77,13 @@ test.describe('Copy and Paste Folders', () => {
// Copy folder-to-copy
await folderToCopy.hover();
await folderToCopy.locator('.menu-icon').click();
await folderToCopy.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
await folderToCopy.click();
// Paste into target folder
await targetFolder.hover();
await targetFolder.locator('.menu-icon').click();
await targetFolder.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
// Verify folder was pasted inside target folder

View File

@@ -24,7 +24,7 @@ test.describe('Copy and Paste Requests', () => {
// Copy the request
const requestItem = page.locator('.collection-item-name').filter({ hasText: 'original-request' });
await requestItem.hover();
await requestItem.locator('.menu-icon').click();
await requestItem.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
// Paste into the collection root
@@ -48,7 +48,7 @@ test.describe('Copy and Paste Requests', () => {
const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });
await folder.click();
await folder.hover();
await folder.locator('.menu-icon').click();
await folder.locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(3);

View File

@@ -51,9 +51,8 @@ test.describe.serial('Header Validation', () => {
const headerRow = page.locator('table tbody tr').first();
const nameCell = getTableCell(headerRow, 0);
// Clear and enter a valid header name
await nameCell.locator('.CodeMirror').click();
await page.keyboard.press('Meta+a');
// Clear and enter a valid header name - use triple-click to select all (works cross-platform)
await nameCell.locator('.CodeMirror').click({ clickCount: 3 });
await nameCell.locator('textarea').fill('Valid-Header');
// Verify the error icon is not visible

View File

@@ -1,4 +1,4 @@
import { test, expect } from '../../../playwright';
import { test, expect, closeElectronApp } from '../../../playwright';
import { createCollection, openCollection } from '../../utils/page';
import { getTableCell } from '../../utils/page/locators';
@@ -39,16 +39,22 @@ test('should persist request with newlines across app restarts', async ({ create
await page.getByRole('tab', { name: 'Vars' }).click();
const preReqRow = page.locator('table').first().locator('tbody tr').first();
await getTableCell(preReqRow, 0).getByRole('textbox').fill('preRequestVar');
// Wait for table to stabilize after fill (new empty row may be appended)
await expect(getTableCell(preReqRow, 0).getByRole('textbox')).toHaveValue('preRequestVar');
await getTableCell(preReqRow, 1).locator('.CodeMirror').click();
await getTableCell(preReqRow, 1).locator('textarea').fill('pre\nRequest\nValue');
const postResRow = page.locator('table').nth(1).locator('tbody tr').first();
await getTableCell(postResRow, 0).getByRole('textbox').fill('postResponseVar');
// Wait for table to stabilize after fill (new empty row may be appended)
await expect(getTableCell(postResRow, 0).getByRole('textbox')).toHaveValue('postResponseVar');
await getTableCell(postResRow, 1).locator('.CodeMirror').click();
await getTableCell(postResRow, 1).locator('textarea').fill('post\nResponse\nValue');
await page.keyboard.press('Meta+s');
await app1.close();
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible();
await closeElectronApp(app1);
// Verify persistence after restart
const app2 = await launchElectronApp({ userDataPath });
@@ -70,5 +76,5 @@ test('should persist request with newlines across app restarts', async ({ create
await expect(page2.locator('table').first().locator('tbody tr')).toHaveCount(2);
await expect(page2.locator('table').nth(1).locator('tbody tr')).toHaveCount(2);
await app2.close();
await closeElectronApp(app2);
});

View File

@@ -39,7 +39,7 @@ test.describe('Max Redirects Settings Tests', () => {
await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });
// Close without saving to avoid modifying the .bru file
await page.locator('.close-icon-container').click();
await page.locator('.close-icon-container').click({ force: true });
await page.locator('button:has-text("Don\'t Save")').first().click();
});
});

View File

@@ -43,7 +43,7 @@ test.describe('No Redirects Settings Tests', () => {
await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });
// Close without saving to avoid modifying the .bru file
await page.locator('.close-icon-container').click();
await page.locator('.close-icon-container').click({ force: true });
await page.locator('button:has-text("Don\'t Save")').first().click();
});
});

View File

@@ -45,7 +45,7 @@ test.describe('Timeout Settings Tests', () => {
await expect(responsePane).toContainText('302');
// Close without saving to avoid modifying the .bru file
await page.locator('.close-icon-container').click();
await page.locator('.close-icon-container').click({ force: true });
await page.locator('button:has-text("Don\'t Save")').first().click();
});

View File

@@ -1,5 +1,7 @@
import { test, expect } from '../../../../playwright';
const findShortcut = process.platform === 'darwin' ? 'Meta+f' : 'Control+f';
test.describe('Custom Search Functionality in Scripts Tab', () => {
test('should open search box when Cmd+F or Ctrl+F is pressed in scripts tab', async ({ pageWithUserData: page }) => {
await page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'custom-search' }).click();
@@ -22,7 +24,7 @@ test.describe('Custom Search Functionality in Scripts Tab', () => {
const preContent = await preRequestEditor.textContent();
console.log('Pre Request content loaded:', preContent?.substring(0, 100));
await page.keyboard.press('Meta+f');
await page.keyboard.press(findShortcut);
// Verify search box appears
const preEditorSearchBar = page.getByTestId('pre-request-script-editor');
@@ -74,7 +76,7 @@ test.describe('Custom Search Functionality in Scripts Tab', () => {
const preRequestEditor = page.getByTestId('pre-request-script-editor').locator('.CodeMirror').first();
const preTextarea = preRequestEditor.locator('textarea[tabindex="0"]');
await preTextarea.focus();
await page.keyboard.press('Meta+f');
await page.keyboard.press(findShortcut);
const preSearchInput = page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar input[placeholder="Search..."]');
await preSearchInput.fill('uniquePreVar');
@@ -87,7 +89,7 @@ test.describe('Custom Search Functionality in Scripts Tab', () => {
const postResponseEditor = page.getByTestId('post-response-script-editor').locator('.CodeMirror').first();
const postTextarea = postResponseEditor.locator('textarea[tabindex="0"]');
await postTextarea.focus();
await page.keyboard.press('Meta+f');
await page.keyboard.press(findShortcut);
const postSearchInput = page.getByTestId('post-response-script-editor').locator('.bruno-search-bar input[placeholder="Search..."]');
await postSearchInput.fill('uniquePostVar');
@@ -108,7 +110,7 @@ test.describe('Custom Search Functionality in Scripts Tab', () => {
const preRequestEditor = page.getByTestId('pre-request-script-editor').locator('.CodeMirror').first();
const preTextarea = preRequestEditor.locator('textarea[tabindex="0"]');
await preTextarea.focus();
await page.keyboard.press('Meta+f');
await page.keyboard.press(findShortcut);
const preSearchInput = page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar input[placeholder="Search..."]');
await preSearchInput.fill('commonVar');
@@ -121,7 +123,7 @@ test.describe('Custom Search Functionality in Scripts Tab', () => {
const postResponseEditor = page.getByTestId('post-response-script-editor').locator('.CodeMirror').first();
const postTextarea = postResponseEditor.locator('textarea[tabindex="0"]');
await postTextarea.focus();
await page.keyboard.press('Meta+f');
await page.keyboard.press(findShortcut);
const postSearchInput = page.getByTestId('post-response-script-editor').locator('.bruno-search-bar input[placeholder="Search..."]');
await postSearchInput.fill('postVar');

View File

@@ -176,7 +176,8 @@ test.describe.serial('Edit Response Examples', () => {
await page.getByTestId('response-example-edit-btn').click();
await page.getByTestId('response-example-name-input').clear();
await page.getByTestId('response-example-name-input').fill('Keyboard Shortcut Test');
await page.keyboard.press('Meta+s');
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
await page.keyboard.press(saveShortcut);
await expect(page.getByTestId('response-example-title')).toHaveText('edit-example / Keyboard Shortcut Test');
});
});

View File

@@ -1,7 +1,7 @@
import { test, expect } from '../../playwright';
import { execSync } from 'child_process';
import path from 'path';
import { clickResponseAction } from '../utils/page/actions';
import { clickResponseAction, sendRequest } from '../utils/page/actions';
test.describe.serial('Response Example Menu Operations', () => {
test.setTimeout(1 * 60 * 1000); // 1 minute for all tests in this describe block, default is 30 seconds.
@@ -17,7 +17,7 @@ test.describe.serial('Response Example Menu Operations', () => {
});
await test.step('Create example', async () => {
await page.getByTestId('send-arrow-icon').click();
await sendRequest(page, 200);
await clickResponseAction(page, 'response-bookmark-btn');
await page.getByTestId('create-example-name-input').clear();
await page.getByTestId('create-example-name-input').fill('Example to Clone');
@@ -33,7 +33,7 @@ test.describe.serial('Response Example Menu Operations', () => {
await test.step('Clone example', async () => {
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });
await exampleRow.hover();
await exampleRow.locator('.menu-icon').click();
await exampleRow.locator('.menu-icon').click({ force: true });
await page.getByTestId('response-example-menu-clone').click();
const clonedExampleItem = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone (Copy)' });
@@ -48,7 +48,7 @@ test.describe.serial('Response Example Menu Operations', () => {
});
await test.step('Create example to delete', async () => {
await page.getByTestId('send-arrow-icon').click();
await sendRequest(page, 200);
await clickResponseAction(page, 'response-bookmark-btn');
await page.getByTestId('create-example-name-input').clear();
await page.getByTestId('create-example-name-input').fill('Example to Delete');
@@ -65,7 +65,7 @@ test.describe.serial('Response Example Menu Operations', () => {
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });
await expect(exampleRow).toBeVisible();
await exampleRow.hover();
await exampleRow.locator('.menu-icon').click();
await exampleRow.locator('.menu-icon').click({ force: true });
await page.getByTestId('response-example-menu-delete').click();
await expect(page.getByText('Delete Example')).toBeVisible();
@@ -81,7 +81,7 @@ test.describe.serial('Response Example Menu Operations', () => {
});
await test.step('Create example to rename', async () => {
await page.getByTestId('send-arrow-icon').click();
await sendRequest(page, 200);
await clickResponseAction(page, 'response-bookmark-btn');
await page.getByTestId('create-example-name-input').clear();
await page.getByTestId('create-example-name-input').fill('Example to Rename');
@@ -98,7 +98,7 @@ test.describe.serial('Response Example Menu Operations', () => {
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });
await expect(exampleRow).toBeVisible();
await exampleRow.hover();
await exampleRow.locator('.menu-icon').click();
await exampleRow.locator('.menu-icon').click({ force: true });
await page.getByTestId('response-example-menu-rename').click();
await expect(page.getByText('Rename Example')).toBeVisible();
const renameExampleNameInput = page.getByTestId('rename-example-name-input');

View File

@@ -2,6 +2,8 @@ import { test, expect } from '../../playwright';
import { createTransientRequest, fillRequestUrl, closeAllCollections, createCollection, sendRequest, clickResponseAction, selectRequestPaneTab } from '../utils/page';
import { buildCommonLocators, buildWebsocketCommonLocators } from '../utils/page/locators';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe.serial('Transient Requests', () => {
let locators: ReturnType<typeof buildCommonLocators>;
@@ -123,7 +125,7 @@ test.describe.serial('Transient Requests', () => {
await test.step('Trigger save action using keyboard shortcut', async () => {
// Try to save using Cmd+S (Mac) or Ctrl+S (other platforms)
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
await page.waitForTimeout(500);
});
@@ -171,7 +173,7 @@ test.describe.serial('Transient Requests', () => {
});
await test.step('Trigger save action using keyboard shortcut', async () => {
await page.keyboard.press('Meta+s');
await page.keyboard.press(saveShortcut);
await page.waitForTimeout(500);
});

View File

@@ -569,7 +569,7 @@ const closeEnvironmentPanel = async (page: Page, type: EnvironmentType = 'collec
const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments';
const envTab = page.locator('.request-tab').filter({ hasText: tabLabel });
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
});
};

View File

@@ -2,9 +2,9 @@ import { expect, Locator, test } from '../../playwright';
import { buildWebsocketCommonLocators } from '../utils/page/locators';
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { waitForPredicate } from '../utils/wait';
const BRU_REQ_NAME = /^base$/;
const BRU_PATH = join(__dirname, 'fixtures/collection/base.bru');
// TODO: reaper move to someplace common
const isRequestSaved = async (saveButton: Locator) => {
@@ -14,53 +14,43 @@ const isRequestSaved = async (saveButton: Locator) => {
test.describe.serial('persistence', () => {
let originalUrl = '';
let originalContext = {
path: join(__dirname, 'fixtures/collection/base.bru'),
data: ''
};
let originalData = '';
test.beforeAll(async () => {
// Store original request data to simplify test consistency
originalContext.data = await readFile(originalContext.path, 'utf8');
const originalUrlMatch = originalContext.data.match(`(url)\s*\:\s*(.+)`);
originalData = await readFile(BRU_PATH, 'utf8');
const originalUrlMatch = originalData.match(`(url)\s*\:\s*(.+)`);
if (!originalUrlMatch) {
throw new Error('url not found in bru file for websocket');
}
originalUrl = originalUrlMatch[0].replace(/url\:/, '');
// Trim to remove leading/trailing whitespace from the regex capture
originalUrl = originalUrlMatch[0].replace(/url\:/, '').trim();
});
test.afterAll(async () => {
// Write back the original request information
await writeFile(originalContext.path, originalContext.data, 'utf8');
// Restore original fixture since pageWithUserData does not isolate collection files
await writeFile(BRU_PATH, originalData, 'utf8');
});
test('save new websocket url', async ({ pageWithUserData: page }) => {
const replacementUrl = 'ws://localhost:8083';
const locators = buildWebsocketCommonLocators(page);
const clearText = async (text: string) => {
for (let i = text.length; i > 0; i--) {
await page.keyboard.press('Backspace');
}
};
const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';
await page.locator('#sidebar-collection-name').click();
await page.getByTitle(BRU_REQ_NAME).click();
// remove the original url from the request
// Select all text in the URL input and replace with new URL
await page.locator('.input-container').filter({ hasText: originalUrl }).first().click();
await clearText(originalUrl);
// replace it with an arbritrary url
await page.keyboard.press(selectAllShortcut);
await page.keyboard.insertText(replacementUrl);
// check if the request is now unsaved
await expect(await isRequestSaved(locators.saveButton())).toBe(false);
// Use auto-retrying assertion to check if the request is now unsaved
await expect.poll(() => isRequestSaved(locators.saveButton())).toBe(false);
await locators.saveButton().click();
const result = await waitForPredicate(() => isRequestSaved(locators.saveButton()));
await expect(result).toBe(true);
// Use auto-retrying assertion to verify save completed
await expect.poll(() => isRequestSaved(locators.saveButton())).toBe(true);
// check if the replacementUrl is now visually available
await expect(page.locator('.input-container').filter({ hasText: replacementUrl }).first()).toBeAttached();

View File

@@ -26,9 +26,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();
await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();
// Wait a bit for environment to be applied
await page.waitForTimeout(200);
// Connect WebSocket
await locators.connectionControls.connect().click();
@@ -52,7 +49,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
// Click to expand the collection
await page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' }).click();
await page.waitForTimeout(300);
// Open the request
await expect(page.getByTitle(BRU_REQ_NAME)).toBeVisible();
@@ -60,7 +56,9 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
// Select the test environment (which has data: test-data)
await page.locator('div.current-environment').click();
await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();
await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();
await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();
// Clear any previous messages
await locators.toolbar.clearResponse().click();
@@ -73,9 +71,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
timeout: MAX_CONNECTION_TIME
});
// Wait a bit for messages to be sent and received (echo server echoes back)
await page.waitForTimeout(1000);
// Verify the sent message contains interpolated value
// Should send {"test": "test-data"} (not {"test": "{{data}}"})
const messages = locators.messages();
@@ -83,14 +78,14 @@ test.describe.serial('WebSocket Variable Interpolation', () => {
// Find the outgoing message with interpolated content
// The echo server will echo back the same message, so we should see it twice
const sentMessage = messages.filter({ hasText: 'test-data' }).first();
await expect(sentMessage).toBeAttached({ timeout: 2000 });
await expect(sentMessage).toBeAttached({ timeout: MAX_CONNECTION_TIME });
// Verify the message content shows interpolated value, not literal variable
const messageText = await sentMessage.locator('.text-ellipsis').textContent();
expect(messageText).toContain('test-data');
expect(messageText).not.toContain('{{data}}');
const messageContent = sentMessage.locator('.text-ellipsis');
await expect(messageContent).toContainText('test-data');
await expect(messageContent).not.toContainText('{{data}}');
// Verify JSON structure is correct
expect(messageText).toMatch(/\{[\s\S]*"test"[\s\S]*"test-data"[\s\S]*\}/);
await expect(messageContent).toContainText('"test"');
});
});

View File

@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs';
import { test, expect } from '../../../playwright';
import { test, expect, closeElectronApp } from '../../../playwright';
test.describe('Default Workspace', () => {
test.describe('First Launch', () => {
@@ -15,8 +15,7 @@ test.describe('Default Workspace', () => {
const workspaceName = page.getByTestId('workspace-name');
await expect(workspaceName).toHaveText('My Workspace');
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -30,7 +29,7 @@ test.describe('Default Workspace', () => {
await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page1.getByTestId('workspace-name')).toHaveText('My Workspace');
await app1.close();
await closeElectronApp(app1);
// Second launch - same workspace should be loaded
const app2 = await launchElectronApp({ userDataPath });
@@ -38,8 +37,7 @@ test.describe('Default Workspace', () => {
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace');
await app2.context().close();
await app2.close();
await closeElectronApp(app2);
});
});
@@ -79,8 +77,7 @@ test.describe('Default Workspace', () => {
expect(fs.existsSync(newWorkspacePath)).toBe(true);
expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should create NEW workspace when workspace.yml has invalid YAML', async ({ launchElectronApp, createTmpDir }) => {
@@ -116,8 +113,7 @@ test.describe('Default Workspace', () => {
const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should create NEW workspace when workspace.yml has wrong type', async ({ launchElectronApp, createTmpDir }) => {
@@ -156,8 +152,7 @@ docs: ''
const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should create NEW workspace when directory does not exist', async ({ launchElectronApp, createTmpDir }) => {
@@ -186,8 +181,7 @@ docs: ''
expect(fs.existsSync(newWorkspacePath)).toBe(true);
expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -206,8 +200,7 @@ docs: ''
const workspaceItem = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'My Workspace' });
await expect(workspaceItem.first()).toBeVisible();
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should not show pin button for default workspace', async ({ launchElectronApp, createTmpDir }) => {
@@ -223,8 +216,7 @@ docs: ''
// Default workspace should NOT have pin button
await expect(workspaceItem.locator('.pin-btn')).not.toBeVisible();
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
});

View File

@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs';
import { test, expect } from '../../../playwright';
import { test, expect, closeElectronApp } from '../../../playwright';
const env = {
DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'
@@ -50,8 +50,7 @@ test.describe('Default Workspace Migration', () => {
});
await test.step('Cleanup', async () => {
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -97,8 +96,7 @@ test.describe('Default Workspace Migration', () => {
expect(workspaceYml).toContain('collection-1');
expect(workspaceYml).toContain('collection-2');
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -138,8 +136,7 @@ test.describe('Default Workspace Migration', () => {
const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');
await expect(sampleCollection).not.toBeVisible();
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -158,8 +155,7 @@ test.describe('Default Workspace Migration', () => {
expect(fs.existsSync(workspacePath)).toBe(true);
const originalYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');
await app1.context().close();
await app1.close();
await closeElectronApp(app1);
// Second launch - should reuse existing workspace
const app2 = await launchElectronApp({ userDataPath });
@@ -174,8 +170,7 @@ test.describe('Default Workspace Migration', () => {
// No new workspace should have been created
expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(false);
await app2.context().close();
await app2.close();
await closeElectronApp(app2);
});
});
@@ -201,8 +196,7 @@ test.describe('Default Workspace Migration', () => {
// Collections should be empty (just the key)
expect(workspaceYml).toMatch(/collections:\s*\n/);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
});

View File

@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs';
import { test, expect } from '../../../playwright';
import { test, expect, closeElectronApp } from '../../../playwright';
test.describe('Default Workspace Recovery and Backup', () => {
test.describe('Global Environments Backup', () => {
@@ -61,8 +61,7 @@ test.describe('Default Workspace Recovery and Backup', () => {
expect(backup.activeGlobalEnvironmentUid).toBe('env1abcdefghijk123456');
expect(backup.backupDate).toBeDefined();
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should preserve global environments backup across multiple app restarts', async ({ launchElectronApp, createTmpDir }) => {
@@ -96,7 +95,7 @@ test.describe('Default Workspace Recovery and Backup', () => {
const app1 = await launchElectronApp({ userDataPath });
const page1 = await app1.firstWindow();
await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await app1.close();
await closeElectronApp(app1);
// Verify backup exists
const backupPath = path.join(userDataPath, 'global-environments-backup.json');
@@ -113,8 +112,7 @@ test.describe('Default Workspace Recovery and Backup', () => {
const backupContentAfterSecond = fs.readFileSync(backupPath, 'utf8');
expect(backupContentAfterSecond).toBe(backupContentAfterFirst);
await app2.context().close();
await app2.close();
await closeElectronApp(app2);
});
});
@@ -140,7 +138,7 @@ test.describe('Default Workspace Recovery and Backup', () => {
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await app.close();
await closeElectronApp(app);
// Verify lastOpenedCollections is still in preferences
const prefsPath = path.join(userDataPath, 'preferences.json');
@@ -192,8 +190,7 @@ docs: ''
const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));
expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspacePath);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should find latest numbered workspace when multiple exist and path not in preferences', async ({ launchElectronApp, createTmpDir }) => {
@@ -240,8 +237,7 @@ docs: ''
// No new workspace should be created
expect(fs.existsSync(path.join(userDataPath, 'default-workspace-3'))).toBe(false);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should skip invalid workspaces and use latest valid one', async ({ launchElectronApp, createTmpDir }) => {
@@ -301,8 +297,7 @@ docs: ''
const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));
expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspace1);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -357,8 +352,7 @@ docs: ''
const newWorkspace = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspace)).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should recover environments from broken workspace to new workspace', async ({ launchElectronApp, createTmpDir }) => {
@@ -432,8 +426,7 @@ docs: ''
expect(fs.existsSync(path.join(newEnvDir, 'production.yml'))).toBe(true);
expect(fs.existsSync(path.join(newEnvDir, 'staging.yml'))).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should use lastOpenedCollections as fallback when workspace config parsing fails', async ({ launchElectronApp, createTmpDir }) => {
@@ -473,8 +466,7 @@ docs: ''
const workspaceYml = fs.readFileSync(path.join(newWorkspace, 'workspace.yml'), 'utf8');
expect(workspaceYml).toContain('fallback-collection');
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -531,8 +523,7 @@ docs: ''
const createdNew = fs.existsSync(path.join(userDataPath, 'default-workspace-1'));
expect(usedExisting || createdNew).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should recover from latest workspace when path does not exist and multiple workspaces exist', async ({ launchElectronApp, createTmpDir }) => {
@@ -611,8 +602,7 @@ docs: ''
const createdWorkspace2 = fs.existsSync(path.join(userDataPath, 'default-workspace-2'));
expect(usedWorkspace1 || createdWorkspace2).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
@@ -637,7 +627,7 @@ docs: ''
const workspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(workspacePath)).toBe(true);
await app1.close();
await closeElectronApp(app1);
// Now add collection to the workspace
const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');
@@ -686,8 +676,7 @@ variables:
// Environment should be recovered
expect(fs.existsSync(path.join(newWorkspace, 'environments', 'myenv.yml'))).toBe(true);
await app2.context().close();
await app2.close();
await closeElectronApp(app2);
});
test('should handle workspace deleted between app restarts', async ({ launchElectronApp, createTmpDir }) => {
@@ -701,7 +690,7 @@ variables:
const workspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(workspacePath)).toBe(true);
await app1.close();
await closeElectronApp(app1);
// DELETE the workspace directory
fs.rmSync(workspacePath, { recursive: true, force: true });
@@ -716,8 +705,7 @@ variables:
expect(fs.existsSync(workspacePath)).toBe(true);
expect(fs.existsSync(path.join(workspacePath, 'workspace.yml'))).toBe(true);
await app2.context().close();
await app2.close();
await closeElectronApp(app2);
});
test('should preserve all data through multiple corruption and recovery cycles', async ({ launchElectronApp, createTmpDir }) => {
@@ -741,7 +729,7 @@ variables:
const app1 = await launchElectronApp({ userDataPath });
const page1 = await app1.firstWindow();
await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await app1.close();
await closeElectronApp(app1);
// Verify workspace-0 created
const ws0 = path.join(userDataPath, 'default-workspace');
@@ -764,7 +752,7 @@ variables: []
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await app2.close();
await closeElectronApp(app2);
// Verify workspace-1 created with recovered data
const ws1 = path.join(userDataPath, 'default-workspace-1');
@@ -790,8 +778,7 @@ variables: []
const ws2Yml = fs.readFileSync(path.join(ws2, 'workspace.yml'), 'utf8');
expect(ws2Yml).toContain('persistent-collection');
await app3.context().close();
await app3.close();
await closeElectronApp(app3);
});
});
@@ -818,8 +805,7 @@ variables: []
const newWorkspace = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspace)).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should handle missing environments directory during recovery', async ({ launchElectronApp, createTmpDir }) => {
@@ -842,8 +828,7 @@ variables: []
// Should not crash
expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(true);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should deduplicate collections between recovered and preference sources', async ({ launchElectronApp, createTmpDir }) => {
@@ -885,8 +870,7 @@ variables: []
const collectionEntries = yml.match(/- name:/g);
expect(collectionEntries).toHaveLength(1);
await app.context().close();
await app.close();
await closeElectronApp(app);
});
test('should not overwrite recovered environments with global environments of same name', async ({ launchElectronApp, createTmpDir }) => {
@@ -943,8 +927,7 @@ variables:
expect(envContent).toContain('workspace-value');
expect(envContent).not.toContain('global-value');
await app.context().close();
await app.close();
await closeElectronApp(app);
});
});
});