Files
bruno/tests/variable-tooltip/variable-tooltip.spec.ts
2026-05-12 15:18:06 +05:30

638 lines
26 KiB
TypeScript

import { test, expect } from '../../playwright';
import {
createCollection,
closeAllCollections,
createRequest,
createEnvironment,
addEnvironmentVariables,
saveEnvironment,
selectRequestPaneTab,
closeEnvironmentPanel
} from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Variable Tooltip', () => {
test.afterEach(async ({ page }) => {
if (!page.isClosed()) {
await closeAllCollections(page);
}
});
test('should test tooltip functionality with environment variables', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-test';
await test.step('Create collection and add environment variables', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-collection'));
await createEnvironment(page, 'Test Env', 'collection');
await addEnvironmentVariables(page, [
{ name: 'apiKey', value: 'test-key-123' },
{ name: 'secretToken', value: 'secret-xyz', isSecret: true }
]);
await saveEnvironment(page);
await closeEnvironmentPanel(page);
});
await test.step('Create request and test tooltip', async () => {
// Create request using utility method
await createRequest(page, 'Test Request', collectionName);
// Set the URL
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com?key={{apiKey}}');
await page.keyboard.press(saveShortcut);
});
await test.step('Test basic tooltip', async () => {
const urlEditor = page.locator('#request-url .CodeMirror');
const apiKeyVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'apiKey' }).first();
await apiKeyVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('apiKey');
await expect(tooltip.locator('.var-scope-badge')).toContainText('Environment');
await expect(tooltip.locator('.var-value-editable-display')).toContainText('test-key-123');
await expect(tooltip.locator('.copy-button')).toBeVisible();
});
await test.step('Test secret variable with toggle', async () => {
await page.mouse.move(0, 0);
await selectRequestPaneTab(page, 'Headers');
const headerTable = page.locator('table').first();
const headerRow = headerTable.locator('tbody tr').first();
const headerNameEditor = headerRow.locator('.CodeMirror').first();
await headerNameEditor.click();
await page.keyboard.type('Authorization');
const headerValueEditor = headerRow.locator('.CodeMirror').nth(1);
await headerValueEditor.click();
await page.keyboard.type('Bearer {{secretToken}}');
await page.keyboard.press(saveShortcut);
// Test tooltip with secret
const secretVar = headerValueEditor.locator('.cm-variable-valid').filter({ hasText: 'secretToken' }).first();
await secretVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
// Verify masked
const valueDisplay = tooltip.locator('.var-value-editable-display');
const maskedText = await valueDisplay.textContent();
// Check that value is masked (contains bullet points and not the actual value)
expect(maskedText).not.toContain('secret-xyz');
expect(maskedText?.length).toBeGreaterThan(0);
// Test toggle
const toggleButton = tooltip.locator('.secret-toggle-button');
await expect(toggleButton).toBeVisible();
await toggleButton.click();
await expect(valueDisplay).toContainText('secret-xyz');
// Toggle back
await toggleButton.click();
const remaskedText = await valueDisplay.textContent();
expect(remaskedText).not.toContain('secret-xyz');
expect(remaskedText?.length).toBeGreaterThan(0);
});
});
test('should test tooltip with variable references', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-reference-test';
await test.step('Create collection with interdependent variables', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-ref-collection'));
await createEnvironment(page, 'Ref Test Env', 'collection');
await addEnvironmentVariables(page, [
{ name: 'host', value: 'api.example.com' },
{ name: 'endpoint', value: 'https://{{host}}/users' }
]);
await saveEnvironment(page);
await closeEnvironmentPanel(page);
});
await test.step('Create request with variable references', async () => {
// Create request using utility method
await createRequest(page, 'Ref Test Request', collectionName);
// Set the URL
await page.locator('.collection-item-name').filter({ hasText: 'Ref Test Request' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('{{endpoint}}');
await page.keyboard.press(saveShortcut);
});
await test.step('Test variable referencing other variables', async () => {
const urlEditor = page.locator('#request-url .CodeMirror');
const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();
await endpointVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('endpoint');
// Should show resolved value
await expect(tooltip.locator('.var-value-editable-display')).toContainText('https://api.example.com/users');
// Should have copy button
await expect(tooltip.locator('.copy-button')).toBeVisible();
});
await test.step('Test editing variable with references', async () => {
// Move mouse away to dismiss any active tooltip
await page.mouse.move(0, 0);
// URL editor is always visible at the top
const urlEditor = page.locator('#request-url .CodeMirror');
const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();
await endpointVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
// Click on value to edit
const valueDisplay = tooltip.locator('.var-value-editable-display');
await valueDisplay.click();
// Should show editor with raw value (not resolved)
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
// Verify it shows the raw value with variable references
// focus on the editor
const editorContent = await editor.locator('.CodeMirror-line').textContent();
expect(editorContent).toContain('{{host}}');
// Edit the value
await page.keyboard.press('End');
await page.keyboard.type('/posts');
// Click outside to save
await page.locator('body').click();
// Move mouse away and back to get fresh tooltip
await page.mouse.move(0, 0);
// Hover again to verify the change
await endpointVar.hover();
const newTooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(newTooltip).toBeVisible();
// Should show updated resolved value
await expect(newTooltip.locator('.var-value-editable-display')).toContainText('https://api.example.com/users/posts');
});
await test.step('Test copy button', async () => {
// Move mouse away to dismiss any active tooltip
await page.mouse.move(0, 0);
const urlEditor = page.locator('#request-url .CodeMirror');
const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();
await endpointVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
const copyButton = tooltip.locator('.copy-button');
await expect(copyButton).toBeVisible();
// Click copy button
await copyButton.click();
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText()), { timeout: 1000 }).toBe('https://api.example.com/users/posts');
});
});
test('should handle runtime and process.env variables', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-readonly-test';
await test.step('Create collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-readonly-collection'));
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
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(saveShortcut);
});
await test.step('Test process.env variable tooltip', async () => {
// Move mouse away to dismiss any active tooltip
await page.mouse.move(0, 0);
// Add a process.env variable in URL (URL editor is always visible at the top)
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.press('End');
await page.keyboard.type('?env={{process.env.HOME}}');
await page.keyboard.press(saveShortcut);
// Hover over process.env variable
const processEnvVar = urlEditor.locator('.cm-variable-valid, .cm-variable-invalid').filter({ hasText: 'process.env.HOME' }).first();
await processEnvVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('process.env.HOME');
await expect(tooltip.locator('.var-scope-badge')).toContainText('Process Env');
// Should show read-only note
await expect(tooltip.locator('.var-readonly-note')).toContainText('read-only');
// Should have copy button but not be editable
await expect(tooltip.locator('.copy-button')).toBeVisible();
await expect(tooltip.locator('.var-value-editor')).not.toBeVisible();
});
});
test('should auto-save request when creating variable via tooltip', async ({ page, createTmpDir }) => {
const collectionName = 'draft-autosave-test';
await test.step('Setup collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('draft-autosave'));
// Create request using utility method
await createRequest(page, 'Autosave Test', collectionName);
// Set the URL
await page.locator('.collection-item-name').filter({ hasText: 'Autosave Test' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com');
await page.keyboard.press(saveShortcut);
});
await test.step('Edit URL to create draft with undefined variable', async () => {
// Edit the URL to add a variable reference
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.press('End');
await page.keyboard.type('/users/{{myApiKey}}');
// Verify draft indicator appears (unsaved changes) in the request tab
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
});
await test.step('Create variable via tooltip - should auto-save entire request', async () => {
// Hover over the undefined variable {{myApiKey}}
const urlEditor = page.locator('#request-url .CodeMirror');
const undefinedVar = urlEditor.locator('.cm-variable-invalid').filter({ hasText: 'myApiKey' }).first();
await undefinedVar.hover();
// Tooltip should appear
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('myApiKey');
await expect(tooltip.locator('.var-scope-badge')).toContainText('Request');
// Click to edit the variable
const valueDisplay = tooltip.locator('.var-value-editable-display');
await valueDisplay.click();
// Type value
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
await page.keyboard.type('secret-key-123');
// Click outside to close editor - this will auto-save the entire request
await page.locator('body').click();
});
await test.step('Verify request was auto-saved with URL changes and new variable', async () => {
// Move mouse away
await page.mouse.move(0, 0);
// Verify variable is now valid (green) in the URL
const urlEditor = page.locator('#request-url .CodeMirror');
const validVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'myApiKey' });
await expect(validVar).toBeVisible();
// Hover to verify value was saved
await validVar.first().hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-value-editable-display')).toContainText('secret-key-123');
// Move mouse away
await page.mouse.move(0, 0);
// Verify the URL changes were also saved
const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();
expect(urlContent).toContain('api.example.com/users');
expect(urlContent).toContain('myApiKey');
// Verify draft indicator is GONE (everything was auto-saved)
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
await expect(requestTab.locator('.close-icon')).toBeVisible();
});
await test.step('Verify variable exists in Vars tab', async () => {
// Check variable is saved to file - should appear in the Vars tab
await selectRequestPaneTab(page, 'Vars');
// The variable should exist in the saved file
const varsTable = page.locator('table').first();
await expect(varsTable).toBeVisible();
const varRow = varsTable.locator('tbody tr').first();
await expect(varRow).toBeVisible();
// Check variable name
const varNameInput = varRow.locator('td').nth(1).getByRole('textbox');
await expect(varNameInput).toBeVisible();
await expect(varNameInput).toHaveValue('myApiKey');
// Check variable value
const varValueTd = varRow.locator('td').nth(2);
const varValue = varValueTd.locator('.CodeMirror');
await expect(varValue).toBeVisible();
const varValueContent = await varValue.locator('.CodeMirror-line').textContent();
expect(varValueContent).toContain('secret-key-123');
});
});
test('should handle invalid variable names with warning', async ({ page, createTmpDir }) => {
const collectionName = 'invalid-var-test';
await test.step('Setup collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('invalid-var-collection'));
// Create request using utility method
await createRequest(page, 'Invalid Var Test', collectionName);
// Set the URL
await page.locator('.collection-item-name').filter({ hasText: 'Invalid Var Test' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com');
await page.keyboard.press(saveShortcut);
});
await test.step('Test invalid variable name with space', async () => {
await selectRequestPaneTab(page, 'Body');
// Select JSON body mode
await page.locator('.body-mode-selector').click();
await page.locator('.dropdown-item').filter({ hasText: 'JSON' }).click();
const bodyEditor = page.locator('.CodeMirror').last();
await bodyEditor.click();
await bodyEditor.evaluate((el: any) => {
const cm = el.CodeMirror;
cm.setValue('{\n "userId": "{{user id}}"\n}');
});
await page.keyboard.press(saveShortcut);
// Hover over the invalid variable
await page.mouse.move(0, 0);
const invalidVar = bodyEditor.locator('.cm-variable-invalid, .cm-variable-valid').filter({ hasText: 'user id' }).first();
await invalidVar.hover();
// Verify tooltip shows warning and hides input
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('user id');
await expect(tooltip.locator('.var-warning-note')).toBeVisible();
await expect(tooltip.locator('.var-value-editable-display')).not.toBeVisible();
});
});
test('should keep tooltip open while editing when mouse leaves popup area', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-pin-test';
await test.step('Setup collection, environment variable, and request', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-pin-collection'));
await createEnvironment(page, 'Pin Env', 'collection');
await addEnvironmentVariables(page, [{ name: 'pinVar', value: 'pin-value' }]);
await saveEnvironment(page);
await closeEnvironmentPanel(page);
await createRequest(page, 'Pin Test Request', collectionName);
await page.locator('.collection-item-name').filter({ hasText: 'Pin Test Request' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com?key={{pinVar}}');
await page.keyboard.press(saveShortcut);
});
await test.step('Tooltip stays open and accepts input while mouse is outside popup', async () => {
await page.mouse.move(0, 0);
const urlEditor = page.locator('#request-url .CodeMirror');
const pinVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'pinVar' }).first();
await pinVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
// Click value display to enter edit mode (this also pins the popup)
const valueDisplay = tooltip.locator('.var-value-editable-display');
await valueDisplay.click();
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
// Move mouse far outside the popup
await page.mouse.move(0, 0);
// Type with a per-keystroke delay so the typing window spans past the internal
// 500ms hide timer. If the popup were not pinned, it would hide mid-typing and
// the keystrokes would never reach the editor — the assertion below would fail.
// This validates pinning via real editor activity instead of a fixed sleep.
await page.keyboard.press('End');
await page.keyboard.type('-still-editable-after-mouse-left', { delay: 25 });
await expect(editor.locator('.CodeMirror-line')).toContainText(
'pin-value-still-editable-after-mouse-left'
);
await expect(tooltip).toBeVisible();
});
});
test('should persist subsequent edits while popup stays open', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-subsequent-edit-test';
await test.step('Setup collection, environment variable, and request', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-subsequent-collection'));
await createEnvironment(page, 'Edit Env', 'collection');
await addEnvironmentVariables(page, [{ name: 'editVar', value: 'initial' }]);
await saveEnvironment(page);
await closeEnvironmentPanel(page);
await createRequest(page, 'Edit Test Request', collectionName);
await page.locator('.collection-item-name').filter({ hasText: 'Edit Test Request' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com?key={{editVar}}');
await page.keyboard.press(saveShortcut);
});
await test.step('First edit saves via Enter and keeps popup open', async () => {
await page.mouse.move(0, 0);
const urlEditor = page.locator('#request-url .CodeMirror');
const editVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'editVar' }).first();
await editVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
const valueDisplay = tooltip.locator('.var-value-editable-display');
await expect(valueDisplay).toContainText('initial');
await valueDisplay.click();
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
await page.keyboard.press('End');
await page.keyboard.type('-one');
// Pressing Enter saves and keeps the popup open (does not click outside)
await page.keyboard.press('Enter');
// Display reflects the saved value, and tooltip is still visible
await expect(valueDisplay).toContainText('initial-one');
await expect(tooltip).toBeVisible();
});
await test.step('Second edit on the same popup also saves', async () => {
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
const valueDisplay = tooltip.locator('.var-value-editable-display');
await valueDisplay.click();
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
await page.keyboard.press('End');
await page.keyboard.type('-two');
await page.keyboard.press('Enter');
await expect(valueDisplay).toContainText('initial-one-two');
});
await test.step('Reopen tooltip and verify the second edit persisted', async () => {
// Close the existing tooltip with an outside click, then re-hover to get a fresh one
await page.locator('body').click();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).not.toBeVisible();
await page.mouse.move(0, 0);
const urlEditor = page.locator('#request-url .CodeMirror');
const editVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'editVar' }).first();
await editVar.hover();
const newTooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(newTooltip).toBeVisible();
await expect(newTooltip.locator('.var-value-editable-display')).toContainText('initial-one-two');
});
});
test('should copy latest value after editing within the same tooltip', async ({ page, createTmpDir }) => {
const collectionName = 'tooltip-copy-latest-test';
await test.step('Setup collection, environment variable, and request', async () => {
await createCollection(page, collectionName, await createTmpDir('tooltip-copy-latest-collection'));
await createEnvironment(page, 'Copy Env', 'collection');
await addEnvironmentVariables(page, [{ name: 'copyVar', value: 'original-copy' }]);
await saveEnvironment(page);
await closeEnvironmentPanel(page);
await createRequest(page, 'Copy Test Request', collectionName);
await page.locator('.collection-item-name').filter({ hasText: 'Copy Test Request' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com?key={{copyVar}}');
await page.keyboard.press(saveShortcut);
});
await test.step('Copy button copies the initial value', async () => {
await page.mouse.move(0, 0);
const urlEditor = page.locator('#request-url .CodeMirror');
const copyVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'copyVar' }).first();
await copyVar.hover();
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
const copyButton = tooltip.locator('.copy-button');
await copyButton.click();
// Success state confirms writeText resolved before we read the clipboard
await expect(copyButton.locator('svg polyline')).toBeVisible({ timeout: 1000 });
const initialClipboard = await page.evaluate(() => navigator.clipboard.readText());
expect(initialClipboard).toBe('original-copy');
// Wait for the icon to revert so the next click is allowed
await expect(copyButton.locator('svg rect')).toBeVisible();
});
await test.step('Edit value, save with Enter, then copy without re-hovering', async () => {
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
const valueDisplay = tooltip.locator('.var-value-editable-display');
await valueDisplay.click();
const editor = tooltip.locator('.var-value-editor .CodeMirror');
await expect(editor).toBeVisible();
await page.keyboard.press('End');
await page.keyboard.type('-edited');
await page.keyboard.press('Enter');
// Wait for the display to reflect the saved value before clicking copy
await expect(valueDisplay).toContainText('original-copy-edited');
const copyButton = tooltip.locator('.copy-button');
await copyButton.click();
await expect(copyButton.locator('svg polyline')).toBeVisible({ timeout: 1000 });
const updatedClipboard = await page.evaluate(() => navigator.clipboard.readText());
expect(updatedClipboard).toBe('original-copy-edited');
});
});
});