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'); }); }); });