mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 11:51:30 +00:00
@@ -37,6 +37,25 @@ const General = ({ close }) => {
|
||||
.test('isValidTimeout', 'Request Timeout must be equal or greater than 0', (value) => {
|
||||
return value === undefined || Number(value) >= 0;
|
||||
}),
|
||||
autoSave: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
interval: Yup.mixed()
|
||||
.transform((value, originalValue) => {
|
||||
return originalValue === '' ? undefined : value;
|
||||
})
|
||||
.test('isNumber', 'Save Delay must be a number', (value) => {
|
||||
return value === undefined || !isNaN(value);
|
||||
})
|
||||
.test('isValidInterval', 'Save Delay must be at least 100ms', (value) => {
|
||||
return value === undefined || Number(value) >= 100;
|
||||
})
|
||||
}).test('intervalRequired', 'Save Delay is required when Auto Save is enabled', (value) => {
|
||||
// If autosave is enabled, interval must be provided
|
||||
if (value.enabled && (value.interval === undefined || value.interval === '')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
defaultCollectionLocation: Yup.string().max(1024)
|
||||
});
|
||||
|
||||
@@ -53,6 +72,10 @@ const General = ({ close }) => {
|
||||
timeout: preferences.request.timeout,
|
||||
storeCookies: get(preferences, 'request.storeCookies', true),
|
||||
sendCookies: get(preferences, 'request.sendCookies', true),
|
||||
autoSave: {
|
||||
enabled: get(preferences, 'autoSave.enabled', false),
|
||||
interval: get(preferences, 'autoSave.interval', 1000)
|
||||
},
|
||||
defaultCollectionLocation: get(preferences, 'general.defaultCollectionLocation', '')
|
||||
},
|
||||
validationSchema: preferencesSchema,
|
||||
@@ -83,12 +106,16 @@ const General = ({ close }) => {
|
||||
storeCookies: newPreferences.storeCookies,
|
||||
sendCookies: newPreferences.sendCookies
|
||||
},
|
||||
autoSave: {
|
||||
enabled: newPreferences.autoSave.enabled,
|
||||
interval: newPreferences.autoSave.interval
|
||||
},
|
||||
general: {
|
||||
defaultCollectionLocation: newPreferences.defaultCollectionLocation
|
||||
}
|
||||
}))
|
||||
.then(() => {
|
||||
toast.success('Preferences saved successfully')
|
||||
toast.success('Preferences saved successfully');
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||
@@ -250,6 +277,43 @@ const General = ({ close }) => {
|
||||
{formik.touched.timeout && formik.errors.timeout ? (
|
||||
<div className="text-red-500">{formik.errors.timeout}</div>
|
||||
) : null}
|
||||
<div className="flex items-center mt-6">
|
||||
<input
|
||||
id="autoSaveEnabled"
|
||||
type="checkbox"
|
||||
name="autoSave.enabled"
|
||||
checked={formik.values.autoSave.enabled}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="autoSaveEnabled">
|
||||
Enable Auto Save
|
||||
</label>
|
||||
</div>
|
||||
<div className={`flex flex-col mt-2 ${!formik.values.autoSave.enabled ? 'opacity-50' : ''}`}>
|
||||
<label className="block select-none" htmlFor="autoSaveInterval">
|
||||
Save Delay (in ms)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="autoSave.interval"
|
||||
id="autoSaveInterval"
|
||||
className="block textbox mt-2 w-24"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.autoSave.interval}
|
||||
disabled={!formik.values.autoSave.enabled}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.autoSave && formik.errors.autoSave && typeof formik.errors.autoSave === 'string' && (
|
||||
<div className="text-red-500">{formik.errors.autoSave}</div>
|
||||
)}
|
||||
{formik.touched.autoSave?.interval && formik.errors.autoSave?.interval && (
|
||||
<div className="text-red-500">{formik.errors.autoSave.interval}</div>
|
||||
)}
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="block select-none default-collection-location-label" htmlFor="defaultCollectionLocation">
|
||||
Default Collection Location
|
||||
@@ -257,6 +321,7 @@ const General = ({ close }) => {
|
||||
<input
|
||||
type="text"
|
||||
name="defaultCollectionLocation"
|
||||
id="defaultCollectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer default-collection-location-input"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
|
||||
@@ -33,7 +33,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = (finalValue) => {
|
||||
const onSave = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ import globalEnvironmentsReducer from './slices/global-environments';
|
||||
import logsReducer from './slices/logs';
|
||||
import performanceReducer from './slices/performance';
|
||||
import { draftDetectMiddleware } from './middlewares/draft/middleware';
|
||||
import { autosaveMiddleware } from './middlewares/autosave/middleware';
|
||||
|
||||
const isDevEnv = () => {
|
||||
return import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
let middleware = [tasksMiddleware.middleware, draftDetectMiddleware];
|
||||
let middleware = [tasksMiddleware.middleware, draftDetectMiddleware, autosaveMiddleware];
|
||||
if (isDevEnv()) {
|
||||
middleware = [...middleware, debugMiddleware.middleware];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { saveRequest, saveCollectionSettings, saveFolderRoot } from '../../slices/collections/actions';
|
||||
import { flattenItems, isItemARequest, isItemAFolder } from 'utils/collections';
|
||||
|
||||
const actionsToIntercept = [
|
||||
// Request-level actions
|
||||
'collections/requestUrlChanged',
|
||||
'collections/updateAuth',
|
||||
'collections/addQueryParam',
|
||||
'collections/moveQueryParam',
|
||||
'collections/updateQueryParam',
|
||||
'collections/deleteQueryParam',
|
||||
'collections/updatePathParam',
|
||||
'collections/addRequestHeader',
|
||||
'collections/updateRequestHeader',
|
||||
'collections/deleteRequestHeader',
|
||||
'collections/moveRequestHeader',
|
||||
'collections/addFormUrlEncodedParam',
|
||||
'collections/updateFormUrlEncodedParam',
|
||||
'collections/deleteFormUrlEncodedParam',
|
||||
'collections/moveFormUrlEncodedParam',
|
||||
'collections/addMultipartFormParam',
|
||||
'collections/updateMultipartFormParam',
|
||||
'collections/deleteMultipartFormParam',
|
||||
'collections/moveMultipartFormParam',
|
||||
'collections/updateRequestAuthMode',
|
||||
'collections/updateRequestBodyMode',
|
||||
'collections/updateRequestBody',
|
||||
'collections/updateRequestGraphqlQuery',
|
||||
'collections/updateRequestGraphqlVariables',
|
||||
'collections/updateRequestScript',
|
||||
'collections/updateResponseScript',
|
||||
'collections/updateRequestTests',
|
||||
'collections/updateRequestMethod',
|
||||
'collections/addAssertion',
|
||||
'collections/updateAssertion',
|
||||
'collections/deleteAssertion',
|
||||
'collections/moveAssertion',
|
||||
'collections/addVar',
|
||||
'collections/updateVar',
|
||||
'collections/deleteVar',
|
||||
'collections/moveVar',
|
||||
'collections/updateRequestDocs',
|
||||
'collections/runRequestEvent',
|
||||
|
||||
// Folder-level actions
|
||||
'collections/addFolderHeader',
|
||||
'collections/updateFolderHeader',
|
||||
'collections/deleteFolderHeader',
|
||||
'collections/addFolderVar',
|
||||
'collections/updateFolderVar',
|
||||
'collections/deleteFolderVar',
|
||||
'collections/updateFolderRequestScript',
|
||||
'collections/updateFolderResponseScript',
|
||||
'collections/updateFolderTests',
|
||||
'collections/updateFolderAuth',
|
||||
'collections/updateFolderAuthMode',
|
||||
'collections/updateFolderDocs',
|
||||
|
||||
// Collection-level actions
|
||||
'collections/addCollectionHeader',
|
||||
'collections/updateCollectionHeader',
|
||||
'collections/deleteCollectionHeader',
|
||||
'collections/addCollectionVar',
|
||||
'collections/updateCollectionVar',
|
||||
'collections/deleteCollectionVar',
|
||||
'collections/updateCollectionAuth',
|
||||
'collections/updateCollectionAuthMode',
|
||||
'collections/updateCollectionRequestScript',
|
||||
'collections/updateCollectionResponseScript',
|
||||
'collections/updateCollectionTests',
|
||||
'collections/updateCollectionDocs',
|
||||
'collections/updateCollectionClientCertificates',
|
||||
'collections/updateCollectionProtobuf',
|
||||
'collections/updateCollectionProxy'
|
||||
];
|
||||
|
||||
// Simple object to track pending save timers
|
||||
const pendingTimers = {};
|
||||
|
||||
// Helper to schedule autosave for an item
|
||||
const scheduleAutoSave = (key, save, interval) => {
|
||||
// Clear any existing timer for this entity
|
||||
clearTimeout(pendingTimers[key]);
|
||||
|
||||
// Schedule a new save
|
||||
pendingTimers[key] = setTimeout(() => {
|
||||
save();
|
||||
delete pendingTimers[key];
|
||||
}, interval);
|
||||
};
|
||||
|
||||
// Helper to find and schedule saves for all existing drafts
|
||||
const saveExistingDrafts = (dispatch, getState, interval) => {
|
||||
const collections = getState().collections.collections;
|
||||
|
||||
collections.forEach((collection) => {
|
||||
// Check collection-level draft
|
||||
if (collection.draft) {
|
||||
const key = `collection-${collection.uid}`;
|
||||
scheduleAutoSave(key, () => dispatch(saveCollectionSettings(collection.uid, null, true)), interval);
|
||||
}
|
||||
|
||||
// Check all items (requests and folders) for drafts
|
||||
const allItems = flattenItems(collection.items);
|
||||
allItems.forEach((item) => {
|
||||
if (item.draft) {
|
||||
if (isItemARequest(item)) {
|
||||
const key = `request-${item.uid}`;
|
||||
scheduleAutoSave(key, () => dispatch(saveRequest(item.uid, collection.uid, true)), interval);
|
||||
} else if (isItemAFolder(item)) {
|
||||
const key = `folder-${item.uid}`;
|
||||
scheduleAutoSave(key, () => dispatch(saveFolderRoot(collection.uid, item.uid, true)), interval);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action) => {
|
||||
// Let the action update the state first
|
||||
const result = next(action);
|
||||
|
||||
// Check if autosave is enabled
|
||||
const { autoSave } = getState().app.preferences;
|
||||
if (!autoSave?.enabled) return result;
|
||||
|
||||
// When autosave is enabled (or settings change), save any existing drafts
|
||||
if (action.type === 'app/updatePreferences' && action.payload?.autoSave?.enabled) {
|
||||
saveExistingDrafts(dispatch, getState, autoSave.interval);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (action.type === 'app/updatePreferences' && action.payload?.autoSave?.enabled === false) {
|
||||
Object.keys(pendingTimers).forEach((key) => {
|
||||
clearTimeout(pendingTimers[key]);
|
||||
delete pendingTimers[key];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Only handle actions that create dirty state
|
||||
if (!actionsToIntercept.includes(action.type)) return result;
|
||||
|
||||
const { itemUid, folderUid, collectionUid } = action.payload;
|
||||
const interval = autoSave.interval;
|
||||
|
||||
// Determine what to save based on what IDs are present
|
||||
let key, save;
|
||||
|
||||
if (itemUid) {
|
||||
// Request change
|
||||
key = `request-${itemUid}`;
|
||||
save = () => dispatch(saveRequest(itemUid, collectionUid, true));
|
||||
} else if (folderUid) {
|
||||
// Folder change
|
||||
key = `folder-${folderUid}`;
|
||||
save = () => dispatch(saveFolderRoot(collectionUid, folderUid, true));
|
||||
} else if (collectionUid) {
|
||||
// Collection change
|
||||
key = `collection-${collectionUid}`;
|
||||
save = () => dispatch(saveCollectionSettings(collectionUid, null, true));
|
||||
}
|
||||
|
||||
if (key && save) {
|
||||
scheduleAutoSave(key, save, interval);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -28,6 +28,10 @@ const initialState = {
|
||||
},
|
||||
general: {
|
||||
defaultCollectionLocation: ''
|
||||
},
|
||||
autoSave: {
|
||||
enabled: false,
|
||||
interval: 1000
|
||||
}
|
||||
},
|
||||
generateCode: {
|
||||
|
||||
@@ -94,7 +94,7 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
|
||||
});
|
||||
};
|
||||
|
||||
export const saveRequest = (itemUid, collectionUid, saveSilently) => (dispatch, getState) => {
|
||||
export const saveRequest = (itemUid, collectionUid, silent = false) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -116,7 +116,7 @@ export const saveRequest = (itemUid, collectionUid, saveSilently) => (dispatch,
|
||||
.validate(itemToSave)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave, collection.format))
|
||||
.then(() => {
|
||||
if (!saveSilently) {
|
||||
if (!silent) {
|
||||
toast.success('Request saved successfully');
|
||||
}
|
||||
dispatch(
|
||||
@@ -196,7 +196,7 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) => {
|
||||
export const saveFolderRoot = (collectionUid, folderUid, silent = false) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
@@ -225,7 +225,9 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(() => {
|
||||
toast.success('Folder Settings saved successfully');
|
||||
if (!silent) {
|
||||
toast.success('Folder Settings saved successfully');
|
||||
}
|
||||
// If there was a draft, save it to root and clear the draft
|
||||
if (folder.draft) {
|
||||
dispatch(saveFolderDraft({ collectionUid, folderUid }));
|
||||
@@ -2128,7 +2130,7 @@ export const browseFiles = (filters, properties) => (_dispatch, _getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const saveCollectionSettings = (collectionUid, brunoConfig = null) => (dispatch, getState) => {
|
||||
export const saveCollectionSettings = (collectionUid, brunoConfig = null, silent = false) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -2156,7 +2158,9 @@ export const saveCollectionSettings = (collectionUid, brunoConfig = null) => (di
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(() => {
|
||||
toast.success('Collection Settings saved successfully');
|
||||
if (!silent) {
|
||||
toast.success('Collection Settings saved successfully');
|
||||
}
|
||||
dispatch(saveCollectionDraft({ collectionUid }));
|
||||
})
|
||||
.then(resolve)
|
||||
|
||||
@@ -545,6 +545,12 @@ const darkTheme = {
|
||||
text: '#B8B8B8'
|
||||
},
|
||||
|
||||
preferences: {
|
||||
sidebar: {
|
||||
border: '#444444'
|
||||
}
|
||||
},
|
||||
|
||||
examples: {
|
||||
buttonBg: '#d9a3421A',
|
||||
buttonColor: '#d9a342',
|
||||
|
||||
@@ -555,6 +555,12 @@ const lightTheme = {
|
||||
text: '#343434'
|
||||
},
|
||||
|
||||
preferences: {
|
||||
sidebar: {
|
||||
border: '#EFEFEF'
|
||||
}
|
||||
},
|
||||
|
||||
examples: {
|
||||
buttonBg: '#D977061A',
|
||||
buttonColor: '#D97706',
|
||||
|
||||
@@ -47,6 +47,10 @@ const defaultPreferences = {
|
||||
},
|
||||
general: {
|
||||
defaultCollectionLocation: ''
|
||||
},
|
||||
autoSave: {
|
||||
enabled: false,
|
||||
interval: 1000
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,6 +94,10 @@ const preferencesSchema = Yup.object().shape({
|
||||
}),
|
||||
general: Yup.object({
|
||||
defaultCollectionLocation: Yup.string().max(1024).nullable()
|
||||
}),
|
||||
autoSave: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
interval: Yup.number().min(100)
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
198
tests/preferences/autosave/autosave.spec.ts
Normal file
198
tests/preferences/autosave/autosave.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { createCollection, closeAllCollections, createRequest } from '../../utils/page';
|
||||
|
||||
test.describe('Autosave', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('should automatically save request changes when autosave is enabled', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'autosave-test';
|
||||
|
||||
await test.step('Create collection and request', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir('autosave-collection'), {
|
||||
openWithSandboxMode: 'safe'
|
||||
});
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
|
||||
|
||||
await createRequest(page, 'Test Request', collectionName);
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
|
||||
|
||||
// Set initial URL and save
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('https://api.example.com');
|
||||
await page.keyboard.press('Control+s');
|
||||
|
||||
// Verify no draft indicator
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Enable autosave in preferences', async () => {
|
||||
// Open preferences
|
||||
await page.locator('.status-bar button[data-trigger="preferences"]').click();
|
||||
|
||||
const preferencesModal = page.locator('.bruno-modal-card').filter({ hasText: 'Preferences' });
|
||||
await expect(preferencesModal).toBeVisible();
|
||||
|
||||
// Enable autosave checkbox
|
||||
const autoSaveCheckbox = preferencesModal.locator('#autoSaveEnabled');
|
||||
await autoSaveCheckbox.check();
|
||||
|
||||
// Save preferences
|
||||
await preferencesModal.locator('button[type="submit"]').click();
|
||||
|
||||
// Wait for preferences to close
|
||||
await expect(preferencesModal).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Make changes and verify autosave', async () => {
|
||||
// Make a change to the URL
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/users');
|
||||
|
||||
// Verify draft indicator appears
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
|
||||
// Wait for autosave to trigger (interval + some buffer)
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify draft indicator disappears after autosave
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify changes persisted', async () => {
|
||||
// Close and reopen the request tab to verify persistence
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await requestTab.locator('.close-icon').click();
|
||||
|
||||
// Reopen request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
|
||||
|
||||
// Verify URL contains our changes
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();
|
||||
expect(urlContent).toContain('api.example.com/users');
|
||||
});
|
||||
|
||||
await test.step('Disable autosave in preferences', async () => {
|
||||
// Open preferences from status bar
|
||||
await page.locator('.status-bar button[data-trigger="preferences"]').click();
|
||||
|
||||
// Wait for preferences modal
|
||||
const preferencesModal = page.locator('.bruno-modal-card').filter({ hasText: 'Preferences' });
|
||||
await expect(preferencesModal).toBeVisible();
|
||||
|
||||
// Disable autosave checkbox
|
||||
const autoSaveCheckbox = preferencesModal.locator('#autoSaveEnabled');
|
||||
await autoSaveCheckbox.uncheck();
|
||||
|
||||
// Save preferences
|
||||
await preferencesModal.locator('button[type="submit"]').click();
|
||||
|
||||
// Wait for preferences to close
|
||||
await expect(preferencesModal).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Make changes and verify no autosave when disabled', async () => {
|
||||
// Make a change to the URL
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/posts');
|
||||
|
||||
// Verify draft indicator appears
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
|
||||
// Wait a bit (longer than autosave interval would be)
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Draft indicator should still be visible (autosave is disabled)
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
|
||||
// Save the request
|
||||
await page.keyboard.press('Control+s');
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('should autosave existing drafts when autosave is enabled', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'autosave-existing-drafts-test';
|
||||
|
||||
await test.step('Create collection and request with initial URL', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir('autosave-existing-drafts-collection'), {
|
||||
openWithSandboxMode: 'safe'
|
||||
});
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
|
||||
|
||||
await createRequest(page, 'Draft Request', collectionName);
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();
|
||||
|
||||
// Set initial URL and save
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.type('https://api.example.com');
|
||||
await page.keyboard.press('Control+s');
|
||||
|
||||
// Verify no draft indicator
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Make changes to create a draft (without saving)', async () => {
|
||||
// Make a change to the URL
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
await urlEditor.click();
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.type('/existing-draft');
|
||||
|
||||
// Verify draft indicator appears
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Enable autosave and verify existing draft is saved', async () => {
|
||||
// Open preferences
|
||||
await page.locator('.status-bar button[data-trigger="preferences"]').click();
|
||||
|
||||
const preferencesModal = page.locator('.bruno-modal-card').filter({ hasText: 'Preferences' });
|
||||
await expect(preferencesModal).toBeVisible();
|
||||
|
||||
// Enable autosave checkbox
|
||||
const autoSaveCheckbox = preferencesModal.locator('#autoSaveEnabled');
|
||||
await autoSaveCheckbox.check();
|
||||
|
||||
// Save preferences
|
||||
await preferencesModal.locator('button[type="submit"]').click();
|
||||
|
||||
// Wait for preferences to close
|
||||
await expect(preferencesModal).not.toBeVisible();
|
||||
|
||||
// Wait for autosave to trigger for existing draft
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify draft indicator disappears (existing draft was auto-saved)
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify changes persisted', async () => {
|
||||
// Close and reopen the request tab to verify persistence
|
||||
const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });
|
||||
await requestTab.locator('.close-icon').click();
|
||||
|
||||
// Reopen request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();
|
||||
|
||||
// Verify URL contains our changes
|
||||
const urlEditor = page.locator('#request-url .CodeMirror');
|
||||
const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();
|
||||
expect(urlContent).toContain('api.example.com/existing-draft');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user