diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 8953831c1..27a068d05 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -47,7 +47,13 @@ import { saveRequest as _saveRequest, saveEnvironment as _saveEnvironment, saveCollectionDraft, - saveFolderDraft + saveFolderDraft, + addVar, + updateVar, + addFolderVar, + updateFolderVar, + addCollectionVar, + updateCollectionVar } from './index'; import { each } from 'lodash'; @@ -1712,58 +1718,6 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di }); }; -/** - * Update a variable value directly in the file without affecting draft state - * @param {string} pathname - File path - * @param {Object} variable - Variable object with uid, name, value, type, enabled - * @param {string} scopeType - Type of scope ('request', 'folder', 'collection') - * @param {string} collectionUid - Collection UID - * @param {string} itemUid - Item/Folder UID (for request/folder) - */ -const updateVariableInFile = (pathname, variable, scopeType, collectionUid, itemUid) => (dispatch) => { - return new Promise((resolve, reject) => { - const { ipcRenderer } = window; - - ipcRenderer - .invoke('renderer:update-variable-in-file', pathname, variable, scopeType) - .then(() => { - // Update Redux state to reflect the change - if (scopeType === 'request') { - dispatch({ - type: 'collections/updateRequestVarValue', - payload: { collectionUid, itemUid, variable } - }); - } else if (scopeType === 'folder') { - dispatch({ - type: 'collections/updateFolderVarValue', - payload: { collectionUid, folderUid: itemUid, variable } - }); - } else if (scopeType === 'collection') { - dispatch({ - type: 'collections/updateCollectionVarValue', - payload: { collectionUid, variable } - }); - } - - resolve(); - }) - .catch(reject); - }); -}; - -/** - * Helper: Execute update action with toast notification - * @param {Function} action - The action to dispatch - * @param {string} successMessage - Success toast message - * @returns {Promise} - */ -const executeVariableUpdate = (dispatch, action, successMessage) => { - return dispatch(action) - .then(() => { - toast.success(successMessage); - }); -}; - /** * Update a variable value in its detected scope (inline editing) * @param {string} variableName - Name of the variable to update @@ -1799,54 +1753,101 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect return reject(new Error('Collection not found')); } - let updatePromise; - let successMessage; - switch (type) { case 'environment': { const { environment, variable } = data; - const updatedVariables = variable - ? environment.variables.map((v) => (v.name === variableName ? { ...v, value: newValue } : v)) - : [...environment.variables, { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true }]; - updatePromise = saveEnvironment(updatedVariables, environment.uid, collectionUid); - successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`; - break; + if (!variable) { + return reject(new Error('Variable not found')); + } + + const updatedVariables = environment.variables.map((v) => (v.uid === variable.uid ? { ...v, value: newValue } : v)); + + return dispatch(saveEnvironment(updatedVariables, environment.uid, collectionUid)) + .then(() => { + toast.success(`Variable "${variableName}" updated`); + }) + .then(resolve) + .catch(reject); } case 'collection': { - const { collection: scopeCollection, variable } = data; - const variableToSave = variable - ? { ...variable, value: newValue } - : { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true }; + const { variable } = data; - const collectionFilePath = path.join(scopeCollection.pathname, 'collection.bru'); - updatePromise = updateVariableInFile(collectionFilePath, variableToSave, 'collection', collectionUid, null); - successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`; - break; + if (variable) { + // Update existing variable in draft + dispatch(updateCollectionVar({ + collectionUid, + type: 'request', + var: { ...variable, value: newValue } + })); + } else { + // Create new variable in draft with actual values + dispatch(addCollectionVar({ + collectionUid, + type: 'request', + var: { name: variableName, value: newValue, enabled: true } + })); + } + + // Save collection root to persist the changes + return dispatch(saveCollectionRoot(collectionUid)) + .then(resolve) + .catch(reject); } case 'folder': { const { folder, variable } = data; - const variableToSave = variable - ? { ...variable, value: newValue } - : { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true }; - const folderFilePath = path.join(folder.pathname, 'folder.bru'); - updatePromise = updateVariableInFile(folderFilePath, variableToSave, 'folder', collectionUid, folder.uid); - successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`; - break; + if (variable) { + // Update existing variable in draft + dispatch(updateFolderVar({ + collectionUid, + folderUid: folder.uid, + type: 'request', + var: { ...variable, value: newValue } + })); + } else { + // Create new variable in draft with actual values + dispatch(addFolderVar({ + collectionUid, + folderUid: folder.uid, + type: 'request', + var: { name: variableName, value: newValue, enabled: true } + })); + } + + // Save folder root to persist the changes + return dispatch(saveFolderRoot(collectionUid, folder.uid)) + .then(resolve) + .catch(reject); } case 'request': { const { item, variable } = data; - const variableToSave = variable - ? { ...variable, value: newValue } - : { uid: uuid(), name: variableName, value: newValue, type: 'text', enabled: true }; - updatePromise = updateVariableInFile(item.pathname, variableToSave, 'request', collectionUid, item.uid); - successMessage = `Variable "${variableName}" ${variable ? 'updated' : 'created'}`; - break; + if (variable) { + // Update existing variable in draft + dispatch(updateVar({ + collectionUid, + itemUid: item.uid, + type: 'request', + var: { ...variable, value: newValue } + })); + } else { + // Create new variable in draft with actual values + dispatch(addVar({ + collectionUid, + itemUid: item.uid, + type: 'request', + var: { name: variableName, value: newValue, local: false, enabled: true } + })); + } + + // Save request to persist the changes + return dispatch(saveRequest(item.uid, collectionUid, true)) + .then(resolve) + .catch(reject); } case 'global': { @@ -1858,25 +1859,31 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect } const environment = globalEnvironments.find((env) => env.uid === activeGlobalEnvUid); + if (!environment) { return reject(new Error('Global environment not found')); } - const updatedVariables = environment.variables.map((v) => - v.name === variableName ? { ...v, value: newValue } : v); + const variable = environment.variables.find((v) => v.name === variableName && v.enabled); - updatePromise = saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid }); - successMessage = `Variable "${variableName}" updated`; - break; + if (!variable) { + return reject(new Error('Variable not found')); + } + + const updatedVariables = environment.variables.map((v) => + v.uid === variable.uid ? { ...v, value: newValue } : v); + + return dispatch(saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid })) + .then(() => { + toast.success(`Variable "${variableName}" updated`); + }) + .then(resolve) + .catch(reject); } default: return reject(new Error(`Unknown scope type: ${type}`)); } - - executeVariableUpdate(dispatch, updatePromise, successMessage) - .then(resolve) - .catch(reject); } catch (error) { toast.error(`Failed to update variable: ${error.message}`); reject(error); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index f759d6897..a708db44c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -25,19 +25,6 @@ import path from 'utils/common/path'; import { getUniqueTagsFromItems } from 'utils/collections/index'; import * as exampleReducers from './exampleReducers'; -// Helper: Update or create variable in variables array -const updateOrCreateVariable = (vars, variable) => { - const existingVar = vars.find((v) => v.name === variable.name); - - if (existingVar) { - // Update existing variable - use the passed variable object to preserve UID - return vars.map((v) => (v.name === variable.name ? variable : v)); - } - - // Create new variable - return [...vars, variable]; -}; - // gRPC status code meanings const grpcStatusCodes = { 0: 'OK', @@ -1741,6 +1728,7 @@ export const collectionsSlice = createSlice({ addVar: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const type = action.payload.type; + const varData = action.payload.var || {}; if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); @@ -1754,10 +1742,10 @@ export const collectionsSlice = createSlice({ item.draft.request.vars.req = item.draft.request.vars.req || []; item.draft.request.vars.req.push({ uid: uuid(), - name: '', - value: '', - local: false, - enabled: true + name: varData?.name || '', + value: varData?.value || '', + local: varData?.local !== undefined ? varData.local : false, + enabled: varData?.enabled !== undefined ? varData.enabled : true }); } else if (type === 'response') { item.draft.request.vars = item.draft.request.vars || {}; @@ -2078,6 +2066,7 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null; const type = action.payload.type; + const varData = action.payload.var || {}; if (folder) { if (!folder.draft) { folder.draft = cloneDeep(folder.root); @@ -2086,9 +2075,9 @@ export const collectionsSlice = createSlice({ const vars = get(folder, 'draft.request.vars.req', []); vars.push({ uid: uuid(), - name: '', - value: '', - enabled: true + name: varData.name || '', + value: varData.value || '', + enabled: varData.enabled !== undefined ? varData.enabled : true }); set(folder, 'draft.request.vars.req', vars); } else if (type === 'response') { @@ -2097,6 +2086,7 @@ export const collectionsSlice = createSlice({ uid: uuid(), name: '', value: '', + local: false, enabled: true }); set(folder, 'draft.request.vars.res', vars); @@ -2283,6 +2273,7 @@ export const collectionsSlice = createSlice({ addCollectionVar: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const type = action.payload.type; + const varData = action.payload.var || {}; if (collection) { if (!collection.draft) { collection.draft = { @@ -2293,9 +2284,9 @@ export const collectionsSlice = createSlice({ const vars = get(collection, 'draft.root.request.vars.req', []); vars.push({ uid: uuid(), - name: '', - value: '', - enabled: true + name: varData.name || '', + value: varData.value || '', + enabled: varData.enabled !== undefined ? varData.enabled : true }); set(collection, 'draft.root.request.vars.req', vars); } else if (type === 'response') { @@ -2304,6 +2295,7 @@ export const collectionsSlice = createSlice({ uid: uuid(), name: '', value: '', + local: false, enabled: true }); set(collection, 'draft.root.request.vars.res', vars); @@ -3213,42 +3205,8 @@ export const collectionsSlice = createSlice({ deleteResponseExampleFormUrlEncodedParam: exampleReducers.deleteResponseExampleFormUrlEncodedParam, addResponseExampleMultipartFormParam: exampleReducers.addResponseExampleMultipartFormParam, updateResponseExampleMultipartFormParam: exampleReducers.updateResponseExampleMultipartFormParam, - deleteResponseExampleMultipartFormParam: exampleReducers.deleteResponseExampleMultipartFormParam, + deleteResponseExampleMultipartFormParam: exampleReducers.deleteResponseExampleMultipartFormParam /* End Response Example Actions */ - - updateRequestVarValue: (state, action) => { - const { collectionUid, itemUid, variable } = action.payload; - const collection = findCollectionByUid(state.collections, collectionUid); - if (!collection) return; - - const item = findItemInCollection(collection, itemUid); - if (item) { - const vars = get(item, 'request.vars.req', []); - const updatedVars = updateOrCreateVariable(vars, variable); - set(item, 'request.vars.req', updatedVars); - } - }, - updateFolderVarValue: (state, action) => { - const { collectionUid, folderUid, variable } = action.payload; - const collection = findCollectionByUid(state.collections, collectionUid); - if (!collection) return; - - const folder = findItemInCollection(collection, folderUid); - if (folder) { - const vars = get(folder, 'root.request.vars.req', []); - const updatedVars = updateOrCreateVariable(vars, variable); - set(folder, 'root.request.vars.req', updatedVars); - } - }, - updateCollectionVarValue: (state, action) => { - const { collectionUid, variable } = action.payload; - const collection = findCollectionByUid(state.collections, collectionUid); - if (!collection) return; - - const vars = get(collection, 'root.request.vars.req', []); - const updatedVars = updateOrCreateVariable(vars, variable); - set(collection, 'root.request.vars.req', updatedVars); - } } }); @@ -3422,12 +3380,8 @@ export const { deleteResponseExampleRequestHeader, moveResponseExampleRequestHeader, setResponseExampleRequestHeaders, - setResponseExampleParams, + setResponseExampleParams /* Response Example Actions - End */ - - updateRequestVarValue, - updateFolderVarValue, - updateCollectionVarValue } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index 64ab5b96d..2ae1788ec 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -189,17 +189,36 @@ export const renderVarInfo = (token, options) => { // Detect variable scope scopeInfo = getVariableScope(variableName, collection, item); - // If variable doesn't exist in any scope, default to creating it at request level + // If variable doesn't exist in any scope, determine scope based on context if (!scopeInfo) { if (item) { - // Create as request variable if we have an item context + // Determine if item is a folder or request + const isFolder = item.type === 'folder'; + + if (isFolder) { + // We're in folder settings - create as folder variable + scopeInfo = { + type: 'folder', + value: '', // Empty value for new variable + data: { folder: item, variable: null } // variable is null since it doesn't exist yet + }; + } else { + // We're in a request - create as request variable + scopeInfo = { + type: 'request', + value: '', // Empty value for new variable + data: { item, variable: null } // variable is null since it doesn't exist yet + }; + } + } else if (collection) { + // No item context but we have collection - create as collection variable scopeInfo = { - type: 'request', - value: '', // Empty value for new variable - data: { item, variable: null } // variable is null since it doesn't exist yet + type: 'collection', + value: '', + data: { collection, variable: null } }; } else { - // If no item context, show as undefined + // No context at all, show as undefined scopeInfo = { type: 'undefined', value: '', diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 3120020aa..34509ef99 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1280,15 +1280,21 @@ const mergeVars = (collection, requestTreePath = []) => { } }); for (let i of requestTreePath) { + if (!i) { + continue; + } + if (i.type === 'folder') { - let vars = get(i, 'root.request.vars.req', []); + // Check draft first, then fall back to root + const folderRoot = i.draft || i.root; + let vars = get(folderRoot, 'request.vars.req', []); vars.forEach((_var) => { if (_var.enabled) { folderVariables[_var.name] = _var.value; } }); } else { - let vars = get(i, 'request.vars.req', []); + let vars = i.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []); vars.forEach((_var) => { if (_var.enabled) { requestVariables[_var.name] = _var.value; @@ -1521,8 +1527,9 @@ export const getVariableScope = (variableName, collection, item) => { } // 1. Check Request Variables (highest priority) - if (item && item.request && item.request.vars && item.request.vars.req) { - const requestVar = item.request.vars.req.find((v) => v.name === variableName && v.enabled); + if (item) { + const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []); + const requestVar = requestVars.find((v) => v.name === variableName && v.enabled); if (requestVar) { return { type: 'request', @@ -1536,8 +1543,14 @@ export const getVariableScope = (variableName, collection, item) => { const requestTreePath = getTreePathFromCollectionToItem(collection, item); for (let i = requestTreePath.length - 1; i >= 0; i--) { const pathItem = requestTreePath[i]; + if (!pathItem) { + continue; + } + if (pathItem.type === 'folder') { - const folderVars = get(pathItem, 'root.request.vars.req', []); + // Check draft first, then fall back to root + const folderRoot = pathItem.draft || pathItem.root; + const folderVars = get(folderRoot, 'request.vars.req', []); const folderVar = folderVars.find((v) => v.name === variableName && v.enabled); if (folderVar) { return { @@ -1565,7 +1578,9 @@ export const getVariableScope = (variableName, collection, item) => { } // 4. Check Collection Variables - const collectionVars = get(collection, 'root.request.vars.req', []); + // Check draft first, then fall back to root + const collectionRoot = (collection.draft && collection.draft.root) || collection.root || {}; + const collectionVars = get(collectionRoot, 'request.vars.req', []); const collectionVar = collectionVars.find((v) => v.name === variableName && v.enabled); if (collectionVar) { return { diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c666d8292..e05899c87 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -286,72 +286,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); - // Helper: Parse file content based on scope type - const parseFileByType = async (fileContent, scopeType) => { - switch (scopeType) { - case 'request': - return await parseRequestViaWorker(fileContent); - case 'folder': - return parseFolder(fileContent); - case 'collection': - return parseCollection(fileContent); - default: - throw new Error(`Invalid scope type: ${scopeType}`); - } - }; - - // Helper: Stringify data based on scope type - const stringifyByType = async (data, scopeType) => { - switch (scopeType) { - case 'request': - return await stringifyRequestViaWorker(data); - case 'folder': - return stringifyFolder(data); - case 'collection': - return stringifyCollection(data); - default: - throw new Error(`Invalid scope type: ${scopeType}`); - } - }; - - // Helper: Update or create variable in array - const updateOrCreateVariable = (variables, variable) => { - const existingVar = variables.find((v) => v.name === variable.name); - - if (existingVar) { - // Update existing variable - return variables.map((v) => (v.name === variable.name ? variable : v)); - } - - // Create new variable - return [...variables, variable]; - }; - - // update variable in request/folder/collection file - ipcMain.handle('renderer:update-variable-in-file', async (event, pathname, variable, scopeType) => { - try { - if (!fs.existsSync(pathname)) { - throw new Error(`path: ${pathname} does not exist`); - } - - // Read and parse the file - const fileContent = fs.readFileSync(pathname, 'utf8'); - const parsedData = await parseFileByType(fileContent, scopeType); - - // Update the specific variable or create it if it doesn't exist - const varsPath = 'request.vars.req'; - const variables = _.get(parsedData, varsPath, []); - const updatedVariables = updateOrCreateVariable(variables, variable); - - _.set(parsedData, varsPath, updatedVariables); - - // Stringify and write back - const content = await stringifyByType(parsedData, scopeType); - await writeFile(pathname, content); - } catch (error) { - return Promise.reject(error); - } - }); // create environment ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => { diff --git a/tests/variable-tooltip/variable-tooltip.spec.ts b/tests/variable-tooltip/variable-tooltip.spec.ts index 6b9643a9c..d7dce1172 100644 --- a/tests/variable-tooltip/variable-tooltip.spec.ts +++ b/tests/variable-tooltip/variable-tooltip.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '../../playwright'; -import { createCollection, closeAllCollections } from '../utils/page'; +import { createCollection, closeAllCollections, createRequest } from '../utils/page'; test.describe('Variable Tooltip', () => { test.afterEach(async ({ page }) => { @@ -43,19 +43,15 @@ test.describe('Variable Tooltip', () => { }); await test.step('Create request and test tooltip', async () => { - // Create request - const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName }); - await collectionContainer.locator('.collection-actions').hover(); - await collectionContainer.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); + // Create request using utility method + await createRequest(page, 'Test Request', collectionName); - await page.getByPlaceholder('Request Name').fill('Test Request'); - await page.locator('#new-request-url .CodeMirror').click(); - await page.locator('textarea').fill('https://api.example.com?key={{apiKey}}'); - await page.getByRole('button', { name: 'Create' }).click(); - - // Open request + // 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('Control+s'); }); await test.step('Test basic tooltip', async () => { @@ -153,17 +149,15 @@ test.describe('Variable Tooltip', () => { }); await test.step('Create request with variable references', async () => { - const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName }); - await collectionContainer.locator('.collection-actions').hover(); - await collectionContainer.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); - - await page.getByPlaceholder('Request Name').fill('Ref Test Request'); - await page.locator('#new-request-url .CodeMirror').click(); - await page.locator('textarea').fill('{{endpoint}}'); - await page.getByRole('button', { name: 'Create' }).click(); + // 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('Control+s'); }); await test.step('Test variable referencing other variables', async () => { @@ -272,18 +266,15 @@ test.describe('Variable Tooltip', () => { await page.getByRole('button', { name: 'Save' }).click(); await page.getByText('×').click(); - // Create request - const collectionContainer = page.locator('.collection-name').filter({ hasText: collectionName }); - await collectionContainer.locator('.collection-actions').hover(); - await collectionContainer.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); - - await page.getByPlaceholder('Request Name').fill('Readonly Test'); - await page.locator('#new-request-url .CodeMirror').click(); - await page.locator('textarea').fill('https://example.com'); - await page.getByRole('button', { name: 'Create' }).click(); + // 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'); + await urlEditor.click(); + await page.keyboard.type('https://example.com'); + await page.keyboard.press('Control+s'); }); await test.step('Test process.env variable tooltip', async () => { @@ -314,4 +305,114 @@ test.describe('Variable Tooltip', () => { 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'), { + openWithSandboxMode: 'safe' + }); + + // 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('Control+s'); + }); + + 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 page.getByRole('tab', { name: 'Vars' }).click(); + + // 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').first().locator('input[type="text"]'); + await expect(varNameInput).toBeVisible(); + await expect(varNameInput).toHaveValue('myApiKey'); + + // Check variable value + const varValueTd = varRow.locator('td').nth(1); + const varValue = varValueTd.locator('.CodeMirror'); + await expect(varValue).toBeVisible(); + const varValueContent = await varValue.locator('.CodeMirror-line').textContent(); + expect(varValueContent).toContain('secret-key-123'); + }); + }); });