diff --git a/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts b/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts index 8245cf491..04a796616 100644 --- a/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts +++ b/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '../../../playwright'; import fs from 'fs'; import path from 'path'; +import { sendRequest } from '../../utils/page'; test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => { test('set env var with persist using script', async ({ pageWithUserData: page, restartApp }) => { @@ -21,9 +22,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => { await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible(); // Send the request - await page.getByTestId('send-arrow-icon').click(); - await page.getByTestId('response-status-code').getByText(/200/).waitFor({ state: 'visible' }); - await page.waitForTimeout(100); + await sendRequest(page, 200); // confirm that the environment variable is set await page.getByTestId('environment-selector-trigger').click(); diff --git a/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts b/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts index 32c95c556..2972a738f 100644 --- a/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts +++ b/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from '../../../playwright'; +import { sendRequest } from '../../utils/page'; test.describe.serial('bru.setEnvVar(name, value)', () => { test('set env var using script', async ({ pageWithUserData: page, restartApp }) => { @@ -15,9 +16,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => { await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible(); // Send the request - await page.getByTestId('send-arrow-icon').click(); - await page.getByTestId('response-status-code').getByText(/200/).waitFor({ state: 'visible' }); - await page.waitForTimeout(100); + await sendRequest(page, 200); // confirm that the environment variable is set await page.getByTestId('environment-selector-trigger').click(); diff --git a/tests/environments/create-environment/collection-env-create.spec.ts b/tests/environments/create-environment/collection-env-create.spec.ts index f44439f2d..01b0bd7cb 100644 --- a/tests/environments/create-environment/collection-env-create.spec.ts +++ b/tests/environments/create-environment/collection-env-create.spec.ts @@ -1,126 +1,65 @@ import { test, expect } from '../../../playwright'; import path from 'path'; +import { + importCollection, + createEnvironment, + addEnvironmentVariables, + saveEnvironment, + closeEnvironmentPanel, + sendRequest, + expectResponseContains, + removeCollection +} from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; test.describe('Collection Environment Create Tests', () => { test('should import collection and create environment for request usage', async ({ page, createTmpDir }) => { - const openApiFile = path.join(__dirname, 'fixtures', 'bruno-collection.json'); + const collectionFile = path.join(__dirname, 'fixtures', 'bruno-collection.json'); + const locators = buildCommonLocators(page); - // Import test collection - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); + await test.step('Import collection', async () => { + await importCollection(page, collectionFile, await createTmpDir('env-test'), { + expectedCollectionName: 'test_collection', + openWithSandboxMode: 'safe' + }); + }); - const importModal = page.locator('[data-testid="import-collection-modal"]'); - await importModal.waitFor({ state: 'visible' }); + await test.step('Create environment with variables', async () => { + await createEnvironment(page, 'Test Environment', 'collection'); - await page.setInputFiles('input[type="file"]', openApiFile); + await addEnvironmentVariables(page, [ + { name: 'host', value: 'https://echo.usebruno.com' }, + { name: 'userId', value: '1' }, + { name: 'postTitle', value: 'Test Post from Environment' }, + { name: 'postBody', value: 'This is a test post body with environment variables' }, + { name: 'secretApiToken', value: 'super-secret-token-12345', isSecret: true } + ]); - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - await expect(locationModal.getByText('test_collection')).toBeVisible(); + await saveEnvironment(page); + await closeEnvironmentPanel(page); + await expect(locators.environment.currentEnvironment()).toContainText('Test Environment'); + }); - await page.locator('#collection-location').fill(await createTmpDir('env-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); + await test.step('Test GET request with environment variables', async () => { + await page.locator('.collection-item-name').first().click(); + await expect(locators.request.urlLine()).toContainText('{{host}}'); + await sendRequest(page, 200); + }); - await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' })).toBeVisible(); + await test.step('Verify response contains environment variables', async () => { + await expectResponseContains(page, [ + '"userId": 1', + '"title": "Test Post from Environment"', + '"body": "This is a test post body with environment variables"', + '"apiToken": "super-secret-token-12345"' + ]); + }); - // Configure collection - await page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' }).click(); - await page.getByLabel('Safe Mode').check(); - await page.getByRole('button', { name: 'Save' }).click(); - - // Create environment - await page.locator('[data-testid="environment-selector-trigger"]').click(); - await expect(page.locator('[data-testid="env-tab-collection"]')).toHaveClass(/active/); - - // Create new environment - await page.locator('button[id="create-env"]').click(); - - // Fill environment name - const environmentNameInput = page.locator('input[name="name"]'); - await expect(environmentNameInput).toBeVisible(); - await environmentNameInput.fill('Test Environment'); - await page.getByRole('button', { name: 'Create' }).click(); - - // Add environment variables - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="0.name"]').fill('host'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="0.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('https://echo.usebruno.com'); - - // Add userId - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="1.name"]').fill('userId'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="1.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('1'); - - // Add postTitle - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="2.name"]').fill('postTitle'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="2.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('Test Post from Environment'); - - // Add postBody - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="3.name"]').fill('postBody'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="3.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('This is a test post body with environment variables'); - - // Add secret token - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="4.name"]').fill('secretApiToken'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="4.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('super-secret-token-12345'); - await page.locator('input[name="4.secret"]').check(); - - // Save environment - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByText('×').click(); - await expect(page.locator('.current-environment')).toContainText('Test Environment'); - - // Test GET request with environment variables - await page.locator('.collection-item-name').first().click(); - await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}'); - await page.locator('[data-testid="send-arrow-icon"]').click(); - await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' }); - await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200'); - - // Verify the JSON response contains the environment variables - const responsePane = page.locator('.response-pane'); - await expect(responsePane).toContainText('"userId": 1'); - await expect(responsePane).toContainText('"title": "Test Post from Environment"'); - await expect(responsePane).toContainText('"body": "This is a test post body with environment variables"'); - await expect(responsePane).toContainText('"apiToken": "super-secret-token-12345"'); - - // Cleanup - use new "Remove" action in workspace UI - const collectionRow = page.locator('.collection-name').filter({ has: page.locator('#sidebar-collection-name:has-text("test_collection")') }); - await collectionRow.hover(); - await collectionRow.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'Remove' }).click(); - - const closeModal = page.getByRole('dialog').filter({ has: page.getByText('Remove Collection') }); - await closeModal.getByRole('button', { name: 'Remove' }).click(); + await test.step('Cleanup', async () => { + await removeCollection(page, 'test_collection'); + }); }); }); diff --git a/tests/environments/create-environment/global-env-create.spec.ts b/tests/environments/create-environment/global-env-create.spec.ts index 32ffa6641..49051f020 100644 --- a/tests/environments/create-environment/global-env-create.spec.ts +++ b/tests/environments/create-environment/global-env-create.spec.ts @@ -1,125 +1,67 @@ import { test, expect } from '../../../playwright'; import path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { + importCollection, + createEnvironment, + addEnvironmentVariables, + saveEnvironment, + closeEnvironmentPanel, + sendRequest, + expectResponseContains, + closeAllCollections +} from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; test.describe('Global Environment Create Tests', () => { + test.setTimeout(60000); + test('should import collection and create global environment for request usage', async ({ page, createTmpDir }) => { - const openApiFile = path.join(__dirname, 'fixtures', 'bruno-collection.json'); + const collectionFile = path.join(__dirname, 'fixtures', 'bruno-collection.json'); + const locators = buildCommonLocators(page); - // Import test collection - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); + await test.step('Import collection', async () => { + await importCollection(page, collectionFile, await createTmpDir('global-env-test'), { + expectedCollectionName: 'test_collection', + openWithSandboxMode: 'safe' + }); + }); - const importModal = page.locator('[data-testid="import-collection-modal"]'); - await importModal.waitFor({ state: 'visible' }); + await test.step('Create global environment with variables', async () => { + await createEnvironment(page, 'Test Global Environment', 'global'); - await page.setInputFiles('input[type="file"]', openApiFile); + await addEnvironmentVariables(page, [ + { name: 'host', value: 'https://echo.usebruno.com' }, + { name: 'userId', value: '1' }, + { name: 'postTitle', value: 'Global Test Post from Environment' }, + { name: 'postBody', value: 'This is a global test post body with environment variables' }, + { name: 'secretApiToken', value: 'global-secret-token-12345', isSecret: true } + ]); - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - await expect(locationModal.getByText('test_collection')).toBeVisible(); + await saveEnvironment(page); + await closeEnvironmentPanel(page); + await expect(locators.environment.currentEnvironment()).toContainText('Test Global Environment'); + }); - await page.locator('#collection-location').fill(await createTmpDir('global-env-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); + await test.step('Test GET request with environment variables', async () => { + await page.locator('.collection-item-name').first().click(); + await expect(locators.request.urlLine()).toContainText('{{host}}'); + await sendRequest(page, 200); + }); - await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' })).toBeVisible(); + await test.step('Verify response contains environment variables', async () => { + await expectResponseContains(page, [ + '"userId": 1', + '"title": "Global Test Post from Environment"', + '"body": "This is a global test post body with environment variables"', + '"apiToken": "global-secret-token-12345"' + ]); + }); - // Configure collection - await page.locator('#sidebar-collection-name').filter({ hasText: 'test_collection' }).click(); - await page.getByLabel('Safe Mode').check(); - await page.getByRole('button', { name: 'Save' }).click(); - - // Create global environment - await page.locator('[data-testid="environment-selector-trigger"]').click(); - await page.locator('[data-testid="env-tab-global"]').click(); - await expect(page.locator('[data-testid="env-tab-global"]')).toHaveClass(/active/); - - // Create new global environment - await page.locator('button[id="create-env"]').click(); - - // Fill environment name - const environmentNameInput = page.locator('input[name="name"]'); - await expect(environmentNameInput).toBeVisible(); - await environmentNameInput.fill('Test Global Environment'); - await page.getByRole('button', { name: 'Create' }).click(); - - // Add environment variables - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="0.name"]').fill('host'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="0.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('https://echo.usebruno.com'); - - // Add userId - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="1.name"]').fill('userId'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="1.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('1'); - - // Add postTitle - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="2.name"]').fill('postTitle'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="2.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('Global Test Post from Environment'); - - // Add postBody - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="3.name"]').fill('postBody'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="3.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('This is a global test post body with environment variables'); - - // Add secret token - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="4.name"]').fill('secretApiToken'); - await page - .locator('tr') - .filter({ has: page.locator('input[name="4.name"]') }) - .locator('.CodeMirror') - .click(); - await page.keyboard.type('global-secret-token-12345'); - await page.locator('input[name="4.secret"]').check(); - await expect(page.locator('input[name="4.secret"]')).toBeChecked(); - - // Save environment - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByText('×').click(); - await expect(page.locator('.current-environment')).toContainText('Test Global Environment'); - - // Test GET request with environment variables - await page.locator('.collection-item-name').first().click(); - await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}'); - await page.locator('[data-testid="send-arrow-icon"]').click(); - await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' }); - await expect(page.locator('[data-testid="response-status-code"]')).toContainText('200'); - - // Verify the JSON response contains the environment variables - const responsePane = page.locator('.response-pane'); - await expect(responsePane).toContainText('"userId": 1'); - await expect(responsePane).toContainText('"title": "Global Test Post from Environment"'); - await expect(responsePane).toContainText('"body": "This is a global test post body with environment variables"'); - await expect(responsePane).toContainText('"apiToken": "global-secret-token-12345"'); - - // cleanup: close all collections - await closeAllCollections(page); + await test.step('Cleanup', async () => { + await closeAllCollections(page); + }); }); }); diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts index 8dda2f31a..a7e544646 100644 --- a/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts @@ -101,11 +101,13 @@ test.describe.serial('Collection Environment Import Tests', () => { await test.step('Verify both environments are available in selector', async () => { // Check that both environments are available in the selector await page.getByText('×').click(); // Close environment settings modal + + await page.waitForTimeout(500); await page.getByTestId('environment-selector-trigger').click(); - // Verify both environments are in the dropdown - await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible(); - await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible(); + await page.waitForTimeout(300); + await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible({ timeout: 10000 }); }); await test.step('Test switching to prod environment and verify variables', async () => { diff --git a/tests/global-environments/non-string-values.spec.ts b/tests/global-environments/non-string-values.spec.ts index 73fff4e83..4fe5e8774 100644 --- a/tests/global-environments/non-string-values.spec.ts +++ b/tests/global-environments/non-string-values.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from '../../playwright'; -import { openCollectionAndAcceptSandbox, closeAllCollections } from '../utils/page'; +import { openCollectionAndAcceptSandbox, closeAllCollections, sendRequest } from '../utils/page'; +import { buildCommonLocators } from '../utils/page/locators'; test.describe('Global Environment Variables - Non-string Values', () => { test.afterEach(async ({ pageWithUserData: page }) => { @@ -40,12 +41,9 @@ test.describe('Global Environment Variables - Non-string Values', () => { // Request contains a script that sets the non-string global variables. await test.step('Run the request to seed non-string global variables via post-script', async () => { - await page.getByText('set-global-nonstring').click(); - await page.getByTestId('send-arrow-icon').click(); - - // wait for the response to arrive - await page.getByTestId('response-status-code').waitFor({ state: 'visible' }); - await expect(page.getByTestId('response-status-code')).toHaveText(/200/); + const locators = buildCommonLocators(page); + await locators.sidebar.request('set-global-nonstring').click(); + await sendRequest(page, 200); }); await test.step('Re-open Global Environments to see the seeded variables', async () => { diff --git a/tests/import/bruno/import-bruno-corrupted-fails.spec.ts b/tests/import/bruno/import-bruno-corrupted-fails.spec.ts index 76da05539..f10f1fa67 100644 --- a/tests/import/bruno/import-bruno-corrupted-fails.spec.ts +++ b/tests/import/bruno/import-bruno-corrupted-fails.spec.ts @@ -15,11 +15,8 @@ test.describe('Import Corrupted Bruno Collection - Should Fail', () => { await page.setInputFiles('input[type="file"]', brunoFile); - // Check for JSON parsing error - const hasImportError = await page.getByText('Failed to parse the file – ensure it is valid JSON or YAML').first().isVisible({ timeout: 5000 }); - - // Either parsing error or import error should be shown - expect(hasImportError).toBe(true); + const errorLocator = page.getByText(/Failed to parse the file|Unsupported collection format|Invalid|Error/).first(); + await expect(errorLocator).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals await page.getByTestId('modal-close-button').click(); diff --git a/tests/import/bruno/import-bruno-missing-required-schema.spec.ts b/tests/import/bruno/import-bruno-missing-required-schema.spec.ts index f9a131bf2..a41cf7ba9 100644 --- a/tests/import/bruno/import-bruno-missing-required-schema.spec.ts +++ b/tests/import/bruno/import-bruno-missing-required-schema.spec.ts @@ -15,10 +15,8 @@ test.describe('Import Bruno Collection - Missing Required Schema Fields', () => await page.setInputFiles('input[type="file"]', brunoFile); - // Check for schema validation error messages - const hasImportError = await page.getByText('Unsupported collection format').first().isVisible({ timeout: 5000 }); - - expect(hasImportError).toBe(true); + const errorMessage = page.getByText('Unsupported collection format').first(); + await expect(errorMessage).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals await page.getByTestId('modal-close-button').click(); diff --git a/tests/import/bruno/import-bruno-testbench.spec.ts b/tests/import/bruno/import-bruno-testbench.spec.ts index 3ddf3d484..e87d6799c 100644 --- a/tests/import/bruno/import-bruno-testbench.spec.ts +++ b/tests/import/bruno/import-bruno-testbench.spec.ts @@ -1,6 +1,6 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import Bruno Testbench Collection', () => { test.afterAll(async ({ page }) => { @@ -10,27 +10,8 @@ test.describe('Import Bruno Testbench Collection', () => { test('Import Bruno Testbench collection successfully', async ({ page, createTmpDir }) => { const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-testbench.json'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', brunoFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - // Wait for collection to appear in the location modal - await expect(locationModal.getByText('bruno-testbench')).toBeVisible(); - - await page.locator('#collection-location').fill(await createTmpDir('bruno-testbench-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - await expect(page.locator('#sidebar-collection-name').getByText('bruno-testbench')).toBeVisible(); + await importCollection(page, brunoFile, await createTmpDir('bruno-testbench-test'), { + expectedCollectionName: 'bruno-testbench' + }); }); }); diff --git a/tests/import/bruno/import-bruno-with-examples.spec.ts b/tests/import/bruno/import-bruno-with-examples.spec.ts index 71078e8c5..fade75046 100644 --- a/tests/import/bruno/import-bruno-with-examples.spec.ts +++ b/tests/import/bruno/import-bruno-with-examples.spec.ts @@ -21,19 +21,24 @@ test.describe('Import Bruno Collection with Examples', () => { await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); }); - await test.step('Upload collection file', async () => { + await test.step('Upload collection file and verify location modal appears', async () => { await page.setInputFiles('input[type="file"]', brunoFile); - }); - await test.step('Verify no parsing errors occurred', async () => { - const hasError = await page.getByText('Failed to parse the file').isVisible().catch(() => false); - if (hasError) { + const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); + const errorMessage = page.getByText('Failed to parse the file'); + + const result = await Promise.race([ + locationModal.waitFor({ state: 'visible', timeout: 15000 }).then(() => 'success'), + errorMessage.waitFor({ state: 'visible', timeout: 15000 }).then(() => 'error') + ]).catch(() => 'timeout'); + + if (result === 'error') { throw new Error('Collection import failed with parsing error'); } - }); + if (result === 'timeout') { + throw new Error('Import timed out - neither success nor error state was reached'); + } - await test.step('Verify location selection modal appears', async () => { - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); }); diff --git a/tests/import/insomnia/import-insomnia-v4.spec.ts b/tests/import/insomnia/import-insomnia-v4.spec.ts index 1cb0a55ec..12828aa93 100644 --- a/tests/import/insomnia/import-insomnia-v4.spec.ts +++ b/tests/import/insomnia/import-insomnia-v4.spec.ts @@ -1,34 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import Insomnia Collection v4', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import Insomnia Collection v4 successfully', async ({ page, createTmpDir }) => { const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4.json'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', insomniaFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4')).toBeVisible(); + await importCollection(page, insomniaFile, await createTmpDir('insomnia-v4-test'), { + expectedCollectionName: 'Test API Collection v4' + }); }); }); diff --git a/tests/import/insomnia/import-insomnia-v5.spec.ts b/tests/import/insomnia/import-insomnia-v5.spec.ts index 7a00b6ebf..c4b5eaacb 100644 --- a/tests/import/insomnia/import-insomnia-v5.spec.ts +++ b/tests/import/insomnia/import-insomnia-v5.spec.ts @@ -1,34 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import Insomnia Collection v5', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import Insomnia Collection v5 successfully', async ({ page, createTmpDir }) => { const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5.yaml'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', insomniaFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v5')).toBeVisible(); + await importCollection(page, insomniaFile, await createTmpDir('insomnia-v5-test'), { + expectedCollectionName: 'Test API Collection v5' + }); }); }); diff --git a/tests/import/insomnia/invalid-missing-collection.spec.ts b/tests/import/insomnia/invalid-missing-collection.spec.ts index db1f2cfea..1a20e6d72 100644 --- a/tests/import/insomnia/invalid-missing-collection.spec.ts +++ b/tests/import/insomnia/invalid-missing-collection.spec.ts @@ -15,11 +15,10 @@ test.describe('Invalid Insomnia Collection - Missing Collection Array', () => { await page.setInputFiles('input[type="file"]', insomniaFile); - // Check for error message - const hasError = await page.getByText('Unsupported collection format').first().isVisible(); - expect(hasError).toBe(true); + const errorLocator = page.getByText('Unsupported collection format').first(); + await expect(errorLocator).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals - await page.locator('[data-test-id="modal-close-button"]').click(); + await page.getByTestId('modal-close-button').click(); }); }); diff --git a/tests/import/openapi/import-openapi-json.spec.ts b/tests/import/openapi/import-openapi-json.spec.ts index 77f455939..785ec2184 100644 --- a/tests/import/openapi/import-openapi-json.spec.ts +++ b/tests/import/openapi/import-openapi-json.spec.ts @@ -1,39 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import OpenAPI v3 JSON Collection', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import simple OpenAPI v3 JSON successfully', async ({ page, createTmpDir }) => { const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', openApiFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - // Wait for collection to appear in the location modal - await expect(locationModal.getByText('Simple Test API')).toBeVisible(); - - // Select a location and import - await page.locator('#collection-location').fill(await createTmpDir('simple-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - // Verify the collection was imported successfully - await expect(page.locator('#sidebar-collection-name').getByText('Simple Test API')).toBeVisible(); + await importCollection(page, openApiFile, await createTmpDir('simple-test'), { + expectedCollectionName: 'Simple Test API' + }); }); }); diff --git a/tests/import/openapi/import-openapi-yaml.spec.ts b/tests/import/openapi/import-openapi-yaml.spec.ts index b6948a18c..185970a29 100644 --- a/tests/import/openapi/import-openapi-yaml.spec.ts +++ b/tests/import/openapi/import-openapi-yaml.spec.ts @@ -1,39 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import OpenAPI v3 YAML Collection', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page, createTmpDir }) => { const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-comprehensive.yaml'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', openApiFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - // Wait for collection to appear in the location modal - await expect(locationModal.getByText('Comprehensive API Test Collection')).toBeVisible(); - - // Select a location and import - await page.locator('#collection-location').fill(await createTmpDir('comprehensive-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - // Verify the collection was imported successfully - await expect(page.locator('#sidebar-collection-name').getByText('Comprehensive API Test Collection')).toBeVisible(); + await importCollection(page, openApiFile, await createTmpDir('comprehensive-test'), { + expectedCollectionName: 'Comprehensive API Test Collection' + }); }); }); diff --git a/tests/import/openapi/malformed-yaml.spec.ts b/tests/import/openapi/malformed-yaml.spec.ts index 5e0a1f5a8..0a43cbaf5 100644 --- a/tests/import/openapi/malformed-yaml.spec.ts +++ b/tests/import/openapi/malformed-yaml.spec.ts @@ -15,12 +15,11 @@ test.describe('Invalid OpenAPI - Malformed YAML', () => { await page.setInputFiles('input[type="file"]', openApiFile); - // Check for error message - this should fail during YAML parsing - const hasParseError = await page.getByText('Failed to parse the file').isVisible(); - const hasImportError = await page.getByText('Import collection failed').isVisible(); + const parseError = page.getByText('Failed to parse the file'); + const importError = page.getByText('Import collection failed'); - // Either parsing error or import error should be shown - expect(hasParseError || hasImportError).toBe(true); + // Wait for at least one error message to be visible + await expect(parseError.or(importError)).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals await page.locator('[data-test-id="modal-close-button"]').click(); diff --git a/tests/import/openapi/missing-info.spec.ts b/tests/import/openapi/missing-info.spec.ts index e58607426..5dddba729 100644 --- a/tests/import/openapi/missing-info.spec.ts +++ b/tests/import/openapi/missing-info.spec.ts @@ -15,11 +15,8 @@ test.describe('Invalid OpenAPI - Missing Info Section', () => { await page.setInputFiles('input[type="file"]', openApiFile); - // The OpenAPI parser might handle missing info gracefully with defaults - const hasError = await page.getByText('Unsupported collection format').first().isVisible(); - - // Either should show an error or create an "Untitled Collection" - expect(hasError).toBe(true); + const errorMessage = page.getByText('Unsupported collection format').first(); + await expect(errorMessage).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals await page.locator('[data-test-id="modal-close-button"]').click(); diff --git a/tests/import/postman/import-postman-v20.spec.ts b/tests/import/postman/import-postman-v20.spec.ts index 4611405a1..168ddc81f 100644 --- a/tests/import/postman/import-postman-v20.spec.ts +++ b/tests/import/postman/import-postman-v20.spec.ts @@ -1,39 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import Postman Collection v2.0', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import Postman Collection v2.0 successfully', async ({ page, createTmpDir }) => { const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v20.json'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', postmanFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - // Wait for collection to appear in the location modal - await expect(locationModal.getByText('Postman v2.0 Collection')).toBeVisible(); - - // Select a location and import - await page.locator('#collection-location').fill(await createTmpDir('postman-v20-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - // Verify the collection was imported successfully - await expect(page.locator('#sidebar-collection-name').getByText('Postman v2.0 Collection')).toBeVisible(); + await importCollection(page, postmanFile, await createTmpDir('postman-v20-test'), { + expectedCollectionName: 'Postman v2.0 Collection' + }); }); }); diff --git a/tests/import/postman/import-postman-v21.spec.ts b/tests/import/postman/import-postman-v21.spec.ts index be2c3f986..740c18bc9 100644 --- a/tests/import/postman/import-postman-v21.spec.ts +++ b/tests/import/postman/import-postman-v21.spec.ts @@ -1,39 +1,17 @@ -import { test, expect } from '../../../playwright'; +import { test } from '../../../playwright'; import * as path from 'path'; -import { closeAllCollections } from '../../utils/page'; +import { closeAllCollections, importCollection } from '../../utils/page'; test.describe('Import Postman Collection v2.1', () => { test.afterEach(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Import Postman Collection v2.1 successfully', async ({ page, createTmpDir }) => { const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v21.json'); - await page.locator('.plus-icon-button').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); - - // 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 page.setInputFiles('input[type="file"]', postmanFile); - - // Wait for location modal to appear after file processing - const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); - await locationModal.waitFor({ state: 'visible', timeout: 10000 }); - await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); - - // Wait for collection to appear in the location modal - await expect(locationModal.getByText('Postman v2.1 Collection')).toBeVisible(); - - // Select a location and import - await page.locator('#collection-location').fill(await createTmpDir('postman-v21-test')); - await locationModal.getByRole('button', { name: 'Import' }).click(); - - // Verify the collection was imported successfully - await expect(page.locator('#sidebar-collection-name').getByText('Postman v2.1 Collection')).toBeVisible(); + await importCollection(page, postmanFile, await createTmpDir('postman-v21-test'), { + expectedCollectionName: 'Postman v2.1 Collection' + }); }); }); diff --git a/tests/import/postman/malformed-structure.spec.ts b/tests/import/postman/malformed-structure.spec.ts index 30cf4c540..846bf9c2a 100644 --- a/tests/import/postman/malformed-structure.spec.ts +++ b/tests/import/postman/malformed-structure.spec.ts @@ -15,11 +15,10 @@ test.describe('Invalid Postman Collection - Malformed Structure', () => { 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); + const errorLocator = page.getByText(/Unsupported collection format|Failed to parse|Invalid|Error/).first(); + await expect(errorLocator).toBeVisible({ timeout: 10000 }); // Cleanup: close any open modals - await page.locator('[data-test-id="modal-close-button"]').click(); + await page.getByTestId('modal-close-button').click(); }); }); diff --git a/tests/interpolation/dynamic-variable/set-var-dynamic-variable.spec.ts b/tests/interpolation/dynamic-variable/set-var-dynamic-variable.spec.ts index e9b24b8ca..8f1b3d307 100644 --- a/tests/interpolation/dynamic-variable/set-var-dynamic-variable.spec.ts +++ b/tests/interpolation/dynamic-variable/set-var-dynamic-variable.spec.ts @@ -1,24 +1,23 @@ import { test, expect } from '../../../playwright'; -import { closeAllCollections, openCollectionAndAcceptSandbox } from '../../utils/page'; +import { closeAllCollections, openCollectionAndAcceptSandbox, sendRequest } from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; test.describe.serial('Dynamic Variable Interpolation', () => { test.afterEach(async ({ pageWithUserData: page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Verifying if the bru.setVar method interpolates random generator functions properly', async ({ pageWithUserData: page }) => { + const locators = buildCommonLocators(page); + // Open collection and accept sandbox mode await openCollectionAndAcceptSandbox(page, 'dynamic-variable-interpolation', 'safe'); // Navigate to the request - await page.getByRole('complementary').getByText('set-var-dynamic-variable').click(); + await locators.sidebar.request('set-var-dynamic-variable').click(); // Send the request - await page.getByTestId('send-arrow-icon').click(); - - // Wait for the response and verify status code - await expect(page.getByTestId('response-status-code')).toHaveText(/200/); + await sendRequest(page, 200); // Verify response contains the title field and that it's not the literal interpolation string const responsePane = page.locator('.response-pane'); diff --git a/tests/interpolation/interpolate-request-url.spec.ts b/tests/interpolation/interpolate-request-url.spec.ts index 3ed080889..1177ecd32 100644 --- a/tests/interpolation/interpolate-request-url.spec.ts +++ b/tests/interpolation/interpolate-request-url.spec.ts @@ -1,28 +1,26 @@ import { test, expect } from '../../playwright'; -import { closeAllCollections } from '../utils/page'; +import { closeAllCollections, sendRequest } from '../utils/page'; +import { buildCommonLocators } from '../utils/page/locators'; test.describe.serial('URL Interpolation', () => { test.afterAll(async ({ pageWithUserData: page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('Interpolate basic path params', async ({ pageWithUserData: page }) => { - await page.locator('#sidebar-collection-name').click(); - await page.getByRole('complementary').getByText('echo-request-url').click(); - await page.getByTestId('send-arrow-icon').click(); - - await expect(page.getByTestId('response-status-code')).toHaveText(/200/); + const locators = buildCommonLocators(page); + await locators.sidebar.collection('interpolation').click(); + await locators.sidebar.request('echo-request-url').click(); + await sendRequest(page, 200); const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts(); await expect(texts.some((d) => d.includes(`"url": "/path/some-data"`))).toBe(true); }); test('Interpolate oData path params', async ({ pageWithUserData: page }) => { - await page.getByRole('complementary').getByText('echo-request-odata').click(); - await page.getByTestId('send-arrow-icon').click(); - - await expect(page.getByTestId('response-status-code')).toHaveText(/200/); + const locators = buildCommonLocators(page); + await locators.sidebar.request('echo-request-odata').click(); + await sendRequest(page, 200); const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts(); await expect(texts.some((d) => d.includes(`"url": "/path/Category('category123')/Item(item456)/foobar/Tags(%22tag%20test%22)"`))).toBe(true); diff --git a/tests/preferences/autosave/autosave.spec.ts b/tests/preferences/autosave/autosave.spec.ts index 7be6572bb..a7e13a7b4 100644 --- a/tests/preferences/autosave/autosave.spec.ts +++ b/tests/preferences/autosave/autosave.spec.ts @@ -2,8 +2,13 @@ import { test, expect } from '../../../playwright'; import { createCollection, closeAllCollections, createRequest } from '../../utils/page'; test.describe('Autosave', () => { + test.setTimeout(60000); + test.afterEach(async ({ page }) => { - await closeAllCollections(page); + // Only try to cleanup if page is still open + if (!page.isClosed()) { + await closeAllCollections(page); + } }); test('should automatically save request changes when autosave is enabled', async ({ page, createTmpDir }) => { @@ -58,11 +63,8 @@ test.describe('Autosave', () => { const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); - // Wait for autosave to trigger (interval + some buffer) - await page.waitForTimeout(1000); - // Verify draft indicator disappears after autosave - await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 5000 }); }); await test.step('Verify changes persisted', async () => { @@ -109,11 +111,7 @@ test.describe('Autosave', () => { const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); - // Wait a bit (longer than autosave interval would be) - await page.waitForTimeout(1500); - - // Draft indicator should still be visible (autosave is disabled) - await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).toBeVisible({ timeout: 2000 }); // Save the request await page.keyboard.press('Control+s'); @@ -173,12 +171,10 @@ test.describe('Autosave', () => { // Wait for preferences to close await expect(preferencesModal).not.toBeVisible(); - // Wait for autosave to trigger for existing draft await page.waitForTimeout(1000); - // Verify draft indicator disappears (existing draft was auto-saved) const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) }); - await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 10000 }); }); await test.step('Verify changes persisted', async () => { diff --git a/tests/response/response-actions.spec.ts b/tests/response/response-actions.spec.ts index 750b63641..a6945ee80 100644 --- a/tests/response/response-actions.spec.ts +++ b/tests/response/response-actions.spec.ts @@ -1,50 +1,33 @@ import { test, expect } from '../../playwright'; -import { closeAllCollections, createCollection } from '../utils/page/actions'; +import { + closeAllCollections, + createCollection, + createRequest, + sendRequest +} from '../utils/page/actions'; +import { buildCommonLocators } from '../utils/page/locators'; test.describe('Response Pane Actions', () => { test.afterAll(async ({ page }) => { - // cleanup: close all collections await closeAllCollections(page); }); test('should copy response to clipboard', async ({ page, createTmpDir }) => { const collectionName = 'response-copy-test'; + const locators = buildCommonLocators(page); await test.step('Create collection and request', async () => { - // Create collection await createCollection(page, collectionName, await createTmpDir(collectionName), { openWithSandboxMode: 'safe' }); - - // Create request - const collection = page.locator('.collection-name').filter({ hasText: collectionName }); - await collection.locator('.collection-actions').hover(); - await collection.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); - - await page.getByPlaceholder('Request Name').fill('copy-test'); - await page.locator('#new-request-url .CodeMirror').click(); - // Using httpbin.org for a simple JSON response - await page.locator('textarea').fill('https://www.httpfaker.org/api/random/json?size=1kb'); - await page.getByRole('button', { name: 'Create' }).click(); + await createRequest(page, 'copy-test', collectionName, { url: 'https://httpbin.org/json' }); }); await test.step('Send request and wait for response', async () => { - // Send request - const sendButton = page.getByTestId('send-arrow-icon'); - await sendButton.click(); - - // Wait for response - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 30000 }); + await sendRequest(page, 200); }); - await test.step('should copy response to clipboard', async () => { - // Find the copy button - const copyButton = page.locator('button[title="Copy response to clipboard"]'); - await expect(copyButton).toBeVisible(); - - // Click the copy button - await copyButton.click(); - - // Verify toast notification appears + await test.step('Copy response to clipboard', async () => { + await expect(locators.response.copyButton()).toBeVisible(); + await locators.response.copyButton().click(); await expect(page.getByText('Response copied to clipboard')).toBeVisible(); }); }); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index fd41eab73..813030f6c 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -1,6 +1,8 @@ -import { test, expect } from '../../../playwright'; +import { test, expect, Page } from '../../../playwright'; import { buildCommonLocators } from './locators'; +type SandboxMode = 'safe' | 'developer'; + /** * Close all collections * @param page - The page object @@ -82,24 +84,54 @@ const createCollection = async (page, collectionName: string, collectionLocation }); }; +type CreateRequestOptions = { + url?: string; + inFolder?: boolean; +}; + /** - * Create a request in a collection + * Create a request in a collection or folder * @param page - The page object * @param requestName - The name of the request to create - * @param collectionName - The name of the collection + * @param parentName - The name of the collection or folder + * @param options - Optional settings (url, inFolder) * @returns void */ -const createRequest = async (page, requestName: string, collectionName: string) => { - await test.step(`Create request "${requestName}" in collection "${collectionName}"`, async () => { - const locators = buildCommonLocators(page); - const collection = locators.sidebar.collection(collectionName); +const createRequest = async ( + page: Page, + requestName: string, + parentName: string, + options: CreateRequestOptions = {} +) => { + const { url, inFolder = false } = options; + const parentType = inFolder ? 'folder' : 'collection'; + + await test.step(`Create request "${requestName}" in ${parentType} "${parentName}"`, async () => { + const locators = buildCommonLocators(page); + + if (inFolder) { + await locators.sidebar.folder(parentName).hover(); + await locators.actions.collectionItemActions(parentName).click(); + } else { + await locators.sidebar.collection(parentName).hover(); + await locators.actions.collectionActions(parentName).click(); + } - await collection.hover(); - await locators.actions.collectionActions(collectionName).click(); await locators.dropdown.item('New Request').click(); await page.getByPlaceholder('Request Name').fill(requestName); + + if (url) { + await page.locator('#new-request-url .CodeMirror').click(); + await page.keyboard.type(url); + } + await locators.modal.button('Create').click(); - await expect(locators.sidebar.request(requestName)).toBeVisible(); + + if (inFolder) { + await expect(locators.sidebar.folderRequest(parentName, requestName)).toBeVisible(); + } else { + await expect(locators.sidebar.request(requestName)).toBeVisible(); + } }); }; @@ -130,4 +162,381 @@ const deleteRequest = async (page, requestName: string, collectionName: string) }); }; -export { closeAllCollections, openCollectionAndAcceptSandbox, createCollection, createRequest, deleteRequest }; +/** + * Import a collection from a file + * @param page - The page object + * @param filePath - The path to the collection file to import + * @param collectionLocation - The directory where the collection will be saved + * @param options - Optional settings for import + * @returns void + */ +type ImportCollectionOptions = { + expectedCollectionName?: string; + openWithSandboxMode?: SandboxMode; +}; + +const importCollection = async ( + page: Page, + filePath: string, + collectionLocation: string, + options: ImportCollectionOptions = {} +) => { + await test.step(`Import collection from "${filePath}"`, async () => { + const locators = buildCommonLocators(page); + + await page.locator('.plus-icon-button').click(); + await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click(); + + // Wait for import modal + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Set the file + await page.setInputFiles('input[type="file"]', filePath); + + // Wait for location modal to appear + const locationModal = page.locator('[data-testid="import-collection-location-modal"]'); + await locationModal.waitFor({ state: 'visible', timeout: 10000 }); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Verify expected collection name if provided + if (options.expectedCollectionName) { + await expect(locationModal.getByText(options.expectedCollectionName)).toBeVisible(); + } + + // Set location and import + await page.locator('#collection-location').fill(collectionLocation); + await locationModal.getByRole('button', { name: 'Import' }).click(); + + // Wait for collection to appear in sidebar + if (options.expectedCollectionName) { + await expect( + page.locator('#sidebar-collection-name').filter({ hasText: options.expectedCollectionName }) + ).toBeVisible(); + + // Configure sandbox mode if requested + if (options.openWithSandboxMode) { + await openCollectionAndAcceptSandbox(page, options.expectedCollectionName, options.openWithSandboxMode); + } + } + }); +}; + +/** + * Remove a specific collection from the sidebar + * @param page - The page object + * @param collectionName - The name of the collection to remove + * @returns void + */ +const removeCollection = async (page: Page, collectionName: string) => { + await test.step(`Remove collection "${collectionName}"`, async () => { + const locators = buildCommonLocators(page); + const collectionRow = page.locator('.collection-name').filter({ + has: page.locator('#sidebar-collection-name', { hasText: collectionName }) + }); + + await collectionRow.hover(); + await collectionRow.locator('.collection-actions .icon').click(); + await locators.dropdown.item('Remove').click(); + + // Wait for and confirm modal + await locators.modal.title('Remove Collection').waitFor({ state: 'visible' }); + await locators.modal.button('Remove').click(); + await locators.modal.title('Remove Collection').waitFor({ state: 'hidden' }); + + // Verify collection is removed + await expect( + page.locator('#sidebar-collection-name').filter({ hasText: collectionName }) + ).not.toBeVisible(); + }); +}; + +/** + * Create a folder inside a collection or another folder + * @param page - The page object + * @param folderName - The name of the folder to create + * @param parentName - The name of the parent collection or folder + * @param isCollection - Whether the parent is a collection (true) or folder (false) + * @returns void + */ +const createFolder = async ( + page: Page, + folderName: string, + parentName: string, + isCollection: boolean = true +) => { + await test.step(`Create folder "${folderName}" in "${parentName}"`, async () => { + const locators = buildCommonLocators(page); + + if (isCollection) { + await locators.sidebar.collection(parentName).hover(); + await locators.actions.collectionActions(parentName).click(); + } else { + await locators.sidebar.folder(parentName).hover(); + await locators.actions.collectionItemActions(parentName).click(); + } + + await locators.dropdown.item('New Folder').click(); + await page.getByPlaceholder('Folder Name').fill(folderName); + await locators.modal.button('Create').click(); + await expect(locators.sidebar.folder(folderName)).toBeVisible(); + }); +}; + +type EnvironmentType = 'collection' | 'global'; + +/** + * Open the environment selector panel + * @param page - The page object + * @param type - The type of environment tab to select + * @returns void + */ +const openEnvironmentSelector = async (page: Page, type: EnvironmentType = 'collection') => { + await test.step(`Open ${type} environment selector`, async () => { + const locators = buildCommonLocators(page); + + await locators.environment.selector().click(); + + if (type === 'global') { + await locators.environment.globalTab().click(); + await expect(locators.environment.globalTab()).toHaveClass(/active/); + } else { + await expect(locators.environment.collectionTab()).toHaveClass(/active/); + } + }); +}; + +/** + * Create a new environment + * @param page - The page object + * @param environmentName - The name of the environment + * @param type - The type of environment (collection or global) + * @returns void + */ +const createEnvironment = async ( + page: Page, + environmentName: string, + type: EnvironmentType = 'collection' +) => { + await test.step(`Create ${type} environment "${environmentName}"`, async () => { + await openEnvironmentSelector(page, type); + + await page.locator('button[id="create-env"]').click(); + + const nameInput = page.locator('input[name="name"]'); + await expect(nameInput).toBeVisible(); + await nameInput.fill(environmentName); + await page.getByRole('button', { name: 'Create' }).click(); + }); +}; + +type EnvironmentVariable = { + name: string; + value: string; + isSecret?: boolean; +}; + +/** + * Add an environment variable to the currently open environment + * @param page - The page object + * @param variable - The variable to add (name, value, and optional secret flag) + * @param index - The index of the variable (0-based) + * @returns void + */ +const addEnvironmentVariable = async ( + page: Page, + variable: EnvironmentVariable, + index: number +) => { + await test.step(`Add environment variable "${variable.name}"`, async () => { + const addButton = page.locator('button[data-testid="add-variable"]'); + await addButton.waitFor({ state: 'visible' }); + await addButton.click(); + + // Wait for the new row to be added and the name input to be visible + const nameInput = page.locator(`input[name="${index}.name"]`); + await nameInput.waitFor({ state: 'visible' }); + await nameInput.fill(variable.name); + + // Wait for the CodeMirror editor in the row to be ready + const variableRow = page.locator('tr').filter({ has: page.locator(`input[name="${index}.name"]`) }); + const codeMirror = variableRow.locator('.CodeMirror'); + await codeMirror.waitFor({ state: 'visible' }); + await codeMirror.click(); + await page.keyboard.type(variable.value); + + if (variable.isSecret) { + const secretCheckbox = page.locator(`input[name="${index}.secret"]`); + await secretCheckbox.waitFor({ state: 'visible' }); + await secretCheckbox.check(); + } + }); +}; + +/** + * Add multiple environment variables to the currently open environment + * @param page - The page object + * @param variables - Array of variables to add + * @returns void + */ +const addEnvironmentVariables = async (page: Page, variables: EnvironmentVariable[]) => { + await test.step(`Add ${variables.length} environment variables`, async () => { + for (let i = 0; i < variables.length; i++) { + await addEnvironmentVariable(page, variables[i], i); + } + }); +}; + +/** + * Save the current environment settings + * @param page - The page object + * @returns void + */ +const saveEnvironment = async (page: Page) => { + await test.step('Save environment', async () => { + await page.getByRole('button', { name: 'Save' }).click(); + }); +}; + +/** + * Close the environment modal/panel + * @param page - The page object + * @returns void + */ +const closeEnvironmentPanel = async (page: Page) => { + await test.step('Close environment panel', async () => { + await page.getByText('×').click(); + }); +}; + +/** + * Select an environment from the dropdown + * @param page - The page object + * @param environmentName - The name of the environment to select + * @param type - The type of environment (collection or global) + * @returns void + */ +const selectEnvironment = async ( + page: Page, + environmentName: string, + type: EnvironmentType = 'collection' +) => { + await test.step(`Select ${type} environment "${environmentName}"`, async () => { + const locators = buildCommonLocators(page); + + await locators.environment.selector().click(); + + if (type === 'global') { + await locators.environment.globalTab().click(); + } + + await locators.environment.envOption(environmentName).click(); + + // Verify selection + await expect(page.locator('.current-environment')).toContainText(environmentName); + }); +}; + +/** + * Send the current request and wait for response + * @param page - The page object + * @param expectedStatusCode - Optional expected status code to wait for + * @param timeout - Timeout in milliseconds (default: 30000) + * @returns void + */ +const sendRequest = async ( + page: Page, + expectedStatusCode?: number | string, + timeout: number = 30000 +) => { + await test.step('Send request', async () => { + await page.getByTestId('send-arrow-icon').click(); + await page.getByTestId('response-status-code').waitFor({ state: 'visible', timeout }); + + if (expectedStatusCode !== undefined) { + await expect(page.getByTestId('response-status-code')).toContainText( + String(expectedStatusCode), + { timeout } + ); + } + }); +}; + +/** + * Open a request by clicking on it in the sidebar + * @param page - The page object + * @param requestName - The name of the request to open + * @returns void + */ +const openRequest = async (page: Page, requestName: string) => { + await test.step(`Open request "${requestName}"`, async () => { + const locators = buildCommonLocators(page); + await locators.sidebar.request(requestName).click(); + await expect(locators.tabs.activeRequestTab()).toContainText(requestName); + }); +}; + +/** + * Open a request within a folder + * @param page - The page object + * @param folderName - The name of the folder + * @param requestName - The name of the request + * @returns void + */ +const openFolderRequest = async (page: Page, folderName: string, requestName: string) => { + await test.step(`Open request "${requestName}" in folder "${folderName}"`, async () => { + const locators = buildCommonLocators(page); + await locators.sidebar.folderRequest(folderName, requestName).click(); + await expect(locators.tabs.activeRequestTab()).toContainText(requestName); + }); +}; + +/** + * Get the response body text + * @param page - The page object + * @returns The response body text + */ +const getResponseBody = async (page: Page): Promise => { + return await page.locator('.response-pane').innerText(); +}; + +/** + * Verify response contains specific text + * @param page - The page object + * @param texts - Array of texts to verify in the response + * @returns void + */ +const expectResponseContains = async (page: Page, texts: string[]) => { + await test.step('Verify response content', async () => { + const responsePane = page.locator('.response-pane'); + for (const text of texts) { + await expect(responsePane).toContainText(text); + } + }); +}; + +export { + closeAllCollections, + openCollectionAndAcceptSandbox, + createCollection, + createRequest, + deleteRequest, + importCollection, + removeCollection, + createFolder, + openEnvironmentSelector, + createEnvironment, + addEnvironmentVariable, + addEnvironmentVariables, + saveEnvironment, + closeEnvironmentPanel, + selectEnvironment, + sendRequest, + openRequest, + openFolderRequest, + getResponseBody, + expectResponseContains +}; + +export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions }; diff --git a/tests/utils/page/index.ts b/tests/utils/page/index.ts index 69b3d32f1..5eecb558f 100644 --- a/tests/utils/page/index.ts +++ b/tests/utils/page/index.ts @@ -1,2 +1,3 @@ export * from './actions'; export * from './runner'; +export * from './locators'; diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index 833913ef3..a1a72c942 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -17,7 +17,10 @@ export const buildCommonLocators = (page: Page) => ({ const folderWrapper = page.locator('.collection-item-name').filter({ hasText: folderName }).locator('..'); return folderWrapper.locator('.collection-item-name').filter({ hasText: requestName }); }, - closeAllCollectionsButton: () => page.getByTestId('close-all-collections-button') + closeAllCollectionsButton: () => page.getByTestId('close-all-collections-button'), + collectionRow: (name: string) => page.locator('.collection-name').filter({ + has: page.locator('#sidebar-collection-name', { hasText: name }) + }) }, actions: { collectionActions: (collectionName: string) => @@ -28,14 +31,15 @@ export const buildCommonLocators = (page: Page) => ({ page.locator('.collection-item-name') .filter({ hasText: itemName }) .locator('.menu-icon') - }, dropdown: { - item: (text: string) => page.locator('.dropdown-item').filter({ hasText: text }) + item: (text: string) => page.locator('.dropdown-item').filter({ hasText: text }), + tippyItem: (text: string) => page.locator('.tippy-box .dropdown-item').filter({ hasText: text }) }, tabs: { requestTab: (requestName: string) => page.locator('.request-tab .tab-label').filter({ hasText: requestName }), - activeRequestTab: () => page.locator('.request-tab.active') + activeRequestTab: () => page.locator('.request-tab.active'), + closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).locator('.close-icon') }, folder: { chevron: (folderName: string) => page.locator('.collection-item-name').filter({ hasText: folderName }).getByTestId('folder-chevron') @@ -44,13 +48,48 @@ export const buildCommonLocators = (page: Page) => ({ title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }), byTitle: (title: string) => page.locator('.bruno-modal').filter({ has: page.locator('.bruno-modal-header-title').filter({ hasText: title }) }), button: (name: string) => page.locator('.bruno-modal').getByRole('button', { name: name, exact: true }), - closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button') + closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button'), + card: () => page.locator('.bruno-modal-card'), + footer: () => page.locator('.bruno-modal-footer'), + submitButton: () => page.locator('.bruno-modal-footer .submit') }, environment: { selector: () => page.getByTestId('environment-selector-trigger'), collectionTab: () => page.getByTestId('env-tab-collection'), globalTab: () => page.getByTestId('env-tab-global'), - envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }) + envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }), + currentEnvironment: () => page.locator('.current-environment'), + addVariableButton: () => page.locator('button[data-testid="add-variable"]'), + variableNameInput: (index: number) => page.locator(`input[name="${index}.name"]`), + variableSecretCheckbox: (index: number) => page.locator(`input[name="${index}.secret"]`), + variableRow: (index: number) => page.locator('tr').filter({ has: page.locator(`input[name="${index}.name"]`) }), + createEnvButton: () => page.locator('button[id="create-env"]'), + envNameInput: () => page.locator('input[name="name"]') + }, + request: { + urlInput: () => page.locator('#request-url .CodeMirror'), + urlLine: () => page.locator('#request-url .CodeMirror-line'), + sendButton: () => page.getByTestId('send-arrow-icon'), + methodDropdown: () => page.getByTestId('request-method-selector'), + newRequestUrl: () => page.locator('#new-request-url .CodeMirror'), + requestNameInput: () => page.getByPlaceholder('Request Name'), + requestTestId: () => page.getByTestId('request-name') + }, + response: { + statusCode: () => page.getByTestId('response-status-code'), + pane: () => page.locator('.response-pane'), + copyButton: () => page.locator('button[title="Copy response to clipboard"]') + }, + plusMenu: { + button: () => page.locator('.plus-icon-button'), + createCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }), + importCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }) + }, + import: { + modal: () => page.locator('[data-testid="import-collection-modal"]'), + locationModal: () => page.locator('[data-testid="import-collection-location-modal"]'), + locationInput: () => page.locator('#collection-location'), + fileInput: () => page.locator('input[type="file"]') } }); diff --git a/tests/utils/page/runner.ts b/tests/utils/page/runner.ts index a3aa1ae23..c7db38080 100644 --- a/tests/utils/page/runner.ts +++ b/tests/utils/page/runner.ts @@ -1,4 +1,19 @@ -import { Page, expect } from '../../../playwright'; +import { Page, expect, test } from '../../../playwright'; + +/** + * Builds locators for the runner results view + * @param page - The Playwright page object + * @returns Object with locators for runner elements + */ +export const buildRunnerLocators = (page: Page) => ({ + allButton: () => page.locator('button').filter({ hasText: /^All/ }), + passedButton: () => page.locator('button').filter({ hasText: /^Passed/ }), + failedButton: () => page.locator('button').filter({ hasText: /^Failed/ }), + skippedButton: () => page.locator('button').filter({ hasText: /^Skipped/ }), + resetButton: () => page.getByRole('button', { name: 'Reset' }), + runCollectionButton: () => page.getByRole('button', { name: 'Run Collection' }), + runAgainButton: () => page.getByRole('button', { name: 'Run Again' }) +}); /** * Reads test result counts from the filter buttons in the runner results view @@ -6,15 +21,12 @@ import { Page, expect } from '../../../playwright'; * @returns An object with totalRequests, passed, failed, and skipped counts */ export const getRunnerResultCounts = async (page: Page) => { - const allButton = page.locator('button').filter({ hasText: /^All/ }); - const passedButton = page.locator('button').filter({ hasText: /^Passed/ }); - const failedButton = page.locator('button').filter({ hasText: /^Failed/ }); - const skippedButton = page.locator('button').filter({ hasText: /^Skipped/ }); + const locators = buildRunnerLocators(page); - const totalRequests = parseInt(await allButton.locator('span').innerText()); - const passed = parseInt(await passedButton.locator('span').innerText()); - const failed = parseInt(await failedButton.locator('span').innerText()); - const skipped = parseInt(await skippedButton.locator('span').innerText()); + const totalRequests = parseInt(await locators.allButton().locator('span').innerText()); + const passed = parseInt(await locators.passedButton().locator('span').innerText()); + const failed = parseInt(await locators.failedButton().locator('span').innerText()); + const skipped = parseInt(await locators.skippedButton().locator('span').innerText()); return { totalRequests, passed, failed, skipped }; }; @@ -27,42 +39,59 @@ export const getRunnerResultCounts = async (page: Page) => { * @returns void */ export const runCollection = async (page: Page, collectionName: string) => { - // Ensure collection is visible and loaded (scope to sidebar) - const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName }); - await collectionContainer.waitFor({ state: 'visible' }); - // Wait a bit for the UI to stabilize - await page.waitForTimeout(300); + await test.step(`Run collection "${collectionName}"`, async () => { + // Ensure collection is visible and loaded (scope to sidebar) + const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName }); + await collectionContainer.waitFor({ state: 'visible' }); - // Open collection actions menu - await collectionContainer.locator('.collection-actions').hover(); - const icon = collectionContainer.locator('.collection-actions .icon'); - await icon.waitFor({ state: 'visible', timeout: 5000 }); - await page.waitForTimeout(200); // Small delay to ensure hover state is stable - await icon.click(); + // Open collection actions menu - wait for the actions container to be actionable + const actionsContainer = collectionContainer.locator('.collection-actions'); + await actionsContainer.waitFor({ state: 'visible' }); + await actionsContainer.hover(); - // Click Run menu item - await page.getByText('Run', { exact: true }).click(); + const icon = actionsContainer.locator('.icon'); + await icon.waitFor({ state: 'visible', timeout: 5000 }); + await icon.click(); - // Handle runner tab - reset if needed, then run - const resetButton = page.getByRole('button', { name: 'Reset' }); - const runCollectionButton = page.getByRole('button', { name: 'Run Collection' }); + // Click Run menu item + const runMenuItem = page.getByText('Run', { exact: true }); + await runMenuItem.waitFor({ state: 'visible' }); + await runMenuItem.click(); - // Check if Reset button is visible (means there are existing results) - const resetVisible = await resetButton.isVisible().catch(() => false); - if (resetVisible) { - await resetButton.click(); - // Wait a bit for the reset to complete - await page.waitForTimeout(500); - } + // Handle runner tab - reset if needed, then run + const locators = buildRunnerLocators(page); - // Now wait for and click Run Collection button - await runCollectionButton.waitFor({ state: 'visible', timeout: 10000 }); - await runCollectionButton.click(); + // Check if Reset button is visible (means there are existing results) + const resetVisible = await locators.resetButton().isVisible({ timeout: 1000 }).catch(() => false); + if (resetVisible) { + await locators.resetButton().click(); + // Wait for the Run Collection button to become visible after reset + await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 5000 }); + } - // Wait for the run to complete - await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + // Now wait for and click Run Collection button + await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 10000 }); + await locators.runCollectionButton().click(); + + // Wait for the run to complete + await locators.runAgainButton().waitFor({ timeout: 2 * 60 * 1000 }); + }); }; +/** + * Builds locators for sandbox mode settings + * @param page - The Playwright page object + * @returns Object with locators for sandbox elements + */ +export const buildSandboxLocators = (page: Page) => ({ + developerModeBadge: () => page.locator('.developer-mode').filter({ hasText: 'Developer Mode' }), + safeModeBadge: () => page.locator('.safe-mode').filter({ hasText: 'Safe Mode' }), + safeModeRadio: () => page.getByLabel('Safe Mode'), + developerModeRadio: () => page.getByLabel('Developer Mode(use only if'), + jsSandboxHeading: () => page.getByText('JavaScript Sandbox'), + saveButton: () => page.getByRole('button', { name: 'Save' }) +}); + /** * Sets up the JavaScript sandbox mode for a collection * @param page - The Playwright page object @@ -71,81 +100,59 @@ export const runCollection = async (page: Page, collectionName: string) => { * @returns void */ export const setSandboxMode = async (page: Page, collectionName: string, mode: 'developer' | 'safe') => { - // Click on the collection name in the sidebar - // Use the collections testid to scope to the sidebar, then find the specific collection - const sidebarCollection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: collectionName }).first(); + await test.step(`Set sandbox mode to "${mode}" for "${collectionName}"`, async () => { + const sandboxLocators = buildSandboxLocators(page); - // Wait for the sidebar to be loaded - await page.waitForTimeout(500); + // Click on the collection name in the sidebar + const sidebarCollection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: collectionName }).first(); + await sidebarCollection.waitFor({ state: 'visible' }); + await sidebarCollection.click(); - await sidebarCollection.click(); + // Check if there's already a mode selected - if so, we need to click the badge to open settings tab + const developerBadgeVisible = await sandboxLocators.developerModeBadge().isVisible().catch(() => false); + const safeBadgeVisible = await sandboxLocators.safeModeBadge().isVisible().catch(() => false); - // Wait a moment for the UI to load - await page.waitForTimeout(300); + // If a badge exists, click it to open the security settings tab + if (developerBadgeVisible || safeBadgeVisible) { + if (developerBadgeVisible) { + await sandboxLocators.developerModeBadge().click(); + } else { + await sandboxLocators.safeModeBadge().click(); + } - // Check if there's already a mode selected - if so, we need to click the badge to open settings tab - // Look for the Developer Mode or Safe Mode badge/button - const developerModeBadge = page.locator('.developer-mode').filter({ hasText: 'Developer Mode' }); - const safeModeBadge = page.locator('.safe-mode').filter({ hasText: 'Safe Mode' }); + // Wait for the security settings tab to be active + await sandboxLocators.jsSandboxHeading().waitFor({ state: 'visible', timeout: 10000 }); + } + // If no badge exists, the modal should have appeared automatically (first time selection) - const developerBadgeExists = await developerModeBadge.count().then((count) => count > 0).catch(() => false); - const safeBadgeExists = await safeModeBadge.count().then((count) => count > 0).catch(() => false); + // Wait for security settings form to be visible - wait for either radio button + await Promise.race([ + sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}), + sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}) + ]); - // If a badge exists, click it to open the security settings tab - if (developerBadgeExists || safeBadgeExists) { - // Click the appropriate badge to open the security settings tab - if (developerBadgeExists) { - await developerModeBadge.click(); + if (mode === 'developer') { + await sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 5000 }); + await sandboxLocators.developerModeRadio().check(); } else { - await safeModeBadge.click(); + // For safe mode, check if developer mode is currently selected + const developerModeChecked = await sandboxLocators.developerModeRadio().isChecked().catch(() => false); + + if (developerModeChecked) { + // Click the Developer Mode label text inside the security settings form + const securityForm = page.locator('div').filter({ hasText: 'JavaScript Sandbox' }).locator('..').first(); + const developerLabel = securityForm.locator('label').filter({ hasText: /^Developer Mode/ }).first(); + await developerLabel.waitFor({ state: 'visible', timeout: 5000 }); + await developerLabel.click(); + } + + // Ensure Safe Mode radio is visible and check it + await sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 5000 }); + await sandboxLocators.safeModeRadio().check(); } - // Wait for the security settings tab to be active and form to be visible - // Look for the security settings content - it should have "JavaScript Sandbox" heading - await page.getByText('JavaScript Sandbox').waitFor({ state: 'visible', timeout: 10000 }); - await page.waitForTimeout(300); - } - // If no badge exists, the modal should have appeared automatically (first time selection) - - // Wait for security settings form to be visible - wait for either radio button - // These should be in the active tab (either modal or tab) - const safeModeRadio = page.getByLabel('Safe Mode'); - const developerRadio = page.getByLabel('Developer Mode(use only if'); - - // Wait for at least one of them to be visible - await Promise.race([ - safeModeRadio.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}), - developerRadio.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}) - ]); - - // Additional wait to ensure UI is stable - await page.waitForTimeout(300); - - if (mode === 'developer') { - await developerRadio.waitFor({ state: 'visible', timeout: 5000 }); - await developerRadio.check(); - } else { - // For safe mode, check if developer mode is currently selected - const developerModeChecked = await developerRadio.isChecked().catch(() => false); - - if (developerModeChecked) { - // Click the Developer Mode label text inside the security settings form - // Scope to the form container to avoid clicking the badge - // The form should have the "JavaScript Sandbox" heading, so scope to that container - const securityForm = page.locator('div').filter({ hasText: 'JavaScript Sandbox' }).locator('..').first(); - const developerLabel = securityForm.locator('label').filter({ hasText: /^Developer Mode/ }).first(); - await developerLabel.waitFor({ state: 'visible', timeout: 5000 }); - await developerLabel.click(); - // Wait for UI to update - await page.waitForTimeout(300); - } - - // Ensure Safe Mode radio is visible and check it - await safeModeRadio.waitFor({ state: 'visible', timeout: 5000 }); - await safeModeRadio.check(); - } - - await page.getByRole('button', { name: 'Save' }).click(); + await sandboxLocators.saveButton().click(); + }); }; /** diff --git a/tests/variable-tooltip/variable-tooltip.spec.ts b/tests/variable-tooltip/variable-tooltip.spec.ts index c89eabb47..c2b39cc6c 100644 --- a/tests/variable-tooltip/variable-tooltip.spec.ts +++ b/tests/variable-tooltip/variable-tooltip.spec.ts @@ -1,9 +1,20 @@ import { test, expect } from '../../playwright'; -import { createCollection, closeAllCollections, createRequest } from '../utils/page'; +import { + createCollection, + closeAllCollections, + createRequest, + createEnvironment, + addEnvironmentVariables, + saveEnvironment, + closeEnvironmentPanel +} from '../utils/page'; +import { buildCommonLocators } from '../utils/page/locators'; test.describe('Variable Tooltip', () => { test.afterEach(async ({ page }) => { - await closeAllCollections(page); + if (!page.isClosed()) { + await closeAllCollections(page); + } }); test('should test tooltip functionality with environment variables', async ({ page, createTmpDir }) => { @@ -13,33 +24,16 @@ test.describe('Variable Tooltip', () => { await createCollection(page, collectionName, await createTmpDir('tooltip-collection'), { openWithSandboxMode: 'safe' }); - await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible(); - // Open environment settings - await page.locator('[data-testid="environment-selector-trigger"]').click(); - await expect(page.locator('[data-testid="env-tab-collection"]')).toHaveClass(/active/); + await createEnvironment(page, 'Test Env', 'collection'); - // Create environment - await page.locator('button[id="create-env"]').click(); - await page.locator('input[name="name"]').fill('Test Env'); - await page.getByRole('button', { name: 'Create' }).click(); + await addEnvironmentVariables(page, [ + { name: 'apiKey', value: 'test-key-123' }, + { name: 'secretToken', value: 'secret-xyz', isSecret: true } + ]); - // Add apiKey variable - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="0.name"]').fill('apiKey'); - await page.locator('tr').filter({ has: page.locator('input[name="0.name"]') }).locator('.CodeMirror').click(); - await page.keyboard.type('test-key-123'); - - // Add secretToken variable - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="1.name"]').fill('secretToken'); - await page.locator('tr').filter({ has: page.locator('input[name="1.name"]') }).locator('.CodeMirror').click(); - await page.keyboard.type('secret-xyz'); - await page.locator('input[name="1.secret"]').check(); - - // Save and close - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByText('×').click(); + await saveEnvironment(page); + await closeEnvironmentPanel(page); }); await test.step('Create request and test tooltip', async () => { @@ -120,32 +114,16 @@ test.describe('Variable Tooltip', () => { await createCollection(page, collectionName, await createTmpDir('tooltip-ref-collection'), { openWithSandboxMode: 'safe' }); - await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible(); - // Open environment settings - await page.locator('[data-testid="environment-selector-trigger"]').click(); - await expect(page.locator('[data-testid="env-tab-collection"]')).toHaveClass(/active/); + await createEnvironment(page, 'Ref Test Env', 'collection'); - // Create environment - await page.locator('button[id="create-env"]').click(); - await page.locator('input[name="name"]').fill('Ref Test Env'); - await page.getByRole('button', { name: 'Create' }).click(); + await addEnvironmentVariables(page, [ + { name: 'host', value: 'api.example.com' }, + { name: 'endpoint', value: 'https://{{host}}/users' } + ]); - // Add host variable - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="0.name"]').fill('host'); - await page.locator('tr').filter({ has: page.locator('input[name="0.name"]') }).locator('.CodeMirror').click(); - await page.keyboard.type('api.example.com'); - - // Add endpoint that references host - await page.locator('button[data-testid="add-variable"]').click(); - await page.locator('input[name="1.name"]').fill('endpoint'); - await page.locator('tr').filter({ has: page.locator('input[name="1.name"]') }).locator('.CodeMirror').click(); - await page.keyboard.type('https://{{host}}/users'); - - // Save and close - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByText('×').click(); + await saveEnvironment(page); + await closeEnvironmentPanel(page); }); await test.step('Create request with variable references', async () => { @@ -256,22 +234,18 @@ test.describe('Variable Tooltip', () => { await createCollection(page, collectionName, await createTmpDir('tooltip-readonly-collection'), { openWithSandboxMode: 'safe' }); - await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible(); - // Create environment - await page.locator('[data-testid="environment-selector-trigger"]').click(); - await page.locator('button[id="create-env"]').click(); - await page.locator('input[name="name"]').fill('Readonly Env'); - await page.getByRole('button', { name: 'Create' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByText('×').click(); + await createEnvironment(page, 'Readonly Env', 'collection'); + await saveEnvironment(page); + await closeEnvironmentPanel(page); // Create request using utility method await createRequest(page, 'Readonly Test', collectionName); // Set the URL - await page.locator('.collection-item-name').filter({ hasText: 'Readonly Test' }).click(); - const urlEditor = page.locator('#request-url .CodeMirror'); + const locators = buildCommonLocators(page); + await locators.sidebar.request('Readonly Test').click(); + const urlEditor = locators.request.urlInput(); await urlEditor.click(); await page.keyboard.type('https://example.com'); await page.keyboard.press('Control+s');