fix: preserve draft state when creating variables via varInfoToolbar (#6152)

* fix: preserve draft state when creating variables via varInfoToolbar
This commit is contained in:
Pooja
2025-11-21 15:47:23 +05:30
committed by GitHub
parent cc3d6a961a
commit fb420fcea4
6 changed files with 294 additions and 264 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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: '',

View File

@@ -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 {

View File

@@ -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) => {

View File

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