fix: example-request tab collision (#7989)

* fix: prevent response-example tabs from hijacking request sidebar selection

* fix: add selectors for examples with index

* test: better locator

* fix: duplicate name collision

* fix: refactor sidebar example handling functions for better clarity and reusability

* chore: cr comments
This commit is contained in:
Sid
2026-05-13 21:47:39 +05:30
committed by GitHub
parent 4fa882c67c
commit 57d2fc7899
12 changed files with 574 additions and 24 deletions

View File

@@ -335,6 +335,9 @@ const RequestTabPanel = () => {
let example = null;
if (item?.examples) {
example = item.examples.find((ex) => ex.uid === focusedTab.uid);
if (!example && typeof focusedTab.exampleIndex === 'number' && focusedTab.exampleIndex >= 0) {
example = item.examples[focusedTab.exampleIndex] || null;
}
if (!example && focusedTab.exampleName) {
example = item.examples.find((ex) => ex.name === focusedTab.exampleName);
}

View File

@@ -26,11 +26,15 @@ const ExampleTab = ({ tab, collection }) => {
if (!item?.examples) return null;
const byUid = item.examples.find((ex) => ex.uid === tab.uid);
if (byUid) return byUid;
if (typeof tab.exampleIndex === 'number' && tab.exampleIndex >= 0) {
const byIndex = item.examples[tab.exampleIndex];
if (byIndex) return byIndex;
}
if (tab.exampleName) {
return item.examples.find((ex) => ex.name === tab.exampleName);
}
return null;
}, [item?.examples, tab.uid, tab.exampleName]);
}, [item?.examples, tab.uid, tab.exampleIndex, tab.exampleName]);
const hasChanges = useMemo(() => hasExampleChanges(item, example?.uid), [item, example?.uid]);

View File

@@ -37,13 +37,16 @@ const ExampleItem = ({ example, item, collection }) => {
const indents = range((item.depth || 0) + 1);
const handleExampleClick = () => {
const exampleIndex = item?.examples?.findIndex((ex) => ex.uid === example.uid);
dispatch(addTab({
uid: example.uid,
collectionUid: collection.uid,
type: 'response-example',
itemUid: item.uid,
pathname: item.pathname,
exampleName: example.name
exampleName: example.name,
exampleIndex: typeof exampleIndex === 'number' && exampleIndex >= 0 ? exampleIndex : undefined
}));
};

View File

@@ -82,7 +82,8 @@ taskMiddleware.startListening({
type: 'response-example',
itemUid: item.uid,
pathname: item.pathname,
exampleName: example.name
exampleName: example.name,
exampleIndex: task.exampleIndex
}));
}
}

View File

@@ -20,7 +20,7 @@ const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
};
const findTabByPathname = (tabs, { collectionUid, pathname, type, exampleName }) => {
const findTabByPathname = (tabs, { collectionUid, pathname, type, exampleName, exampleIndex }) => {
if (!pathname || !collectionUid || !type) {
return null;
}
@@ -35,6 +35,10 @@ const findTabByPathname = (tabs, { collectionUid, pathname, type, exampleName })
}
if (type === 'response-example') {
if (typeof exampleIndex === 'number' && exampleIndex >= 0 && typeof tab.exampleIndex === 'number' && tab.exampleIndex >= 0) {
return tab.exampleIndex === exampleIndex;
}
return tab.exampleName === exampleName;
}
@@ -47,7 +51,7 @@ export const tabsSlice = createSlice({
initialState,
reducers: {
addTab: (state, action) => {
const { uid, collectionUid, type, requestPaneTab, preview, exampleUid, itemUid, pathname, exampleName, isTransient } = action.payload;
const { uid, collectionUid, type, requestPaneTab, preview, exampleUid, itemUid, pathname, exampleName, exampleIndex, isTransient } = action.payload;
const nonReplaceableTabTypes = [
'variables',
@@ -67,7 +71,7 @@ export const tabsSlice = createSlice({
return;
}
const existingPathnameTab = findTabByPathname(state.tabs, { collectionUid, pathname, type, exampleName });
const existingPathnameTab = findTabByPathname(state.tabs, { collectionUid, pathname, type, exampleName, exampleIndex });
if (existingPathnameTab) {
state.activeTabUid = existingPathnameTab.uid;
return;
@@ -114,6 +118,7 @@ export const tabsSlice = createSlice({
...(exampleUid ? { exampleUid } : {}),
...(itemUid ? { itemUid } : {}),
...(exampleName ? { exampleName } : {}),
...(typeof exampleIndex === 'number' ? { exampleIndex } : {}),
...(isTransient ? { isTransient: true } : {})
};
@@ -149,6 +154,7 @@ export const tabsSlice = createSlice({
...(exampleUid ? { exampleUid } : {}),
...(itemUid ? { itemUid } : {}),
...(exampleName ? { exampleName } : {}),
...(typeof exampleIndex === 'number' ? { exampleIndex } : {}),
...(isTransient ? { isTransient: true } : {})
});
state.activeTabUid = uid;

View File

@@ -12,7 +12,11 @@ export const getTabUidForItem = ({ itemUid, itemPathname, collectionUid }) => cr
return null;
}
const tabByPathname = tabs.find((tab) => tab.pathname === itemPathname && (!collectionUid || tab.collectionUid === collectionUid));
const tabByPathname = tabs.find((tab) => (
tab.type !== 'response-example'
&& tab.pathname === itemPathname
&& (!collectionUid || tab.collectionUid === collectionUid)
));
return tabByPathname?.uid || null;
});
@@ -41,7 +45,7 @@ export const isTabForItemActive = ({ itemUid, itemPathname, collectionUid }) =>
return false;
}
return activeTab.pathname === itemPathname;
return activeTab.type !== 'response-example' && activeTab.pathname === itemPathname;
});
export const isTabForItemPresent = ({ itemUid, itemPathname, collectionUid }) => createSelector([
@@ -51,5 +55,5 @@ export const isTabForItemPresent = ({ itemUid, itemPathname, collectionUid }) =>
return false;
}
return tab.uid === itemUid || (itemPathname && tab.pathname === itemPathname);
return tab.uid === itemUid || (itemPathname && tab.type !== 'response-example' && tab.pathname === itemPathname);
}));

View File

@@ -0,0 +1,90 @@
import { getTabUidForItem, isTabForItemActive, isTabForItemPresent } from './tab';
describe('tab selectors', () => {
const baseState = {
tabs: {
activeTabUid: null,
tabs: []
}
};
it('does not resolve request tab uid from response-example pathname fallback', () => {
const state = {
...baseState,
tabs: {
...baseState.tabs,
tabs: [
{
uid: 'example-1',
type: 'response-example',
pathname: '/c/req.bru',
collectionUid: 'c1'
}
]
}
};
const selector = getTabUidForItem({ itemUid: 'request-1', itemPathname: '/c/req.bru', collectionUid: 'c1' });
expect(selector(state)).toBeNull();
});
it('does not mark request active when only response-example tab is active on same pathname', () => {
const state = {
...baseState,
tabs: {
activeTabUid: 'example-1',
tabs: [
{
uid: 'example-1',
type: 'response-example',
pathname: '/c/req.bru',
collectionUid: 'c1'
}
]
}
};
const selector = isTabForItemActive({ itemUid: 'request-1', itemPathname: '/c/req.bru', collectionUid: 'c1' });
expect(selector(state)).toBe(false);
});
it('does not mark request present when only response-example tab exists for same pathname', () => {
const state = {
...baseState,
tabs: {
...baseState.tabs,
tabs: [
{
uid: 'example-1',
type: 'response-example',
pathname: '/c/req.bru',
collectionUid: 'c1'
}
]
}
};
const selector = isTabForItemPresent({ itemUid: 'request-1', itemPathname: '/c/req.bru', collectionUid: 'c1' });
expect(selector(state)).toBe(false);
});
it('still resolves regular request tab by pathname fallback', () => {
const state = {
...baseState,
tabs: {
...baseState.tabs,
tabs: [
{
uid: 'request-1',
type: 'http-request',
pathname: '/c/req.bru',
collectionUid: 'c1'
}
]
}
};
const selector = getTabUidForItem({ itemUid: 'missing-uid', itemPathname: '/c/req.bru', collectionUid: 'c1' });
expect(selector(state)).toBe('request-1');
});
});

View File

@@ -344,7 +344,7 @@ export const findCollectionEnvironmentFromSnapshot = (collection, snapshotData =
};
const getAccessor = (tab) => {
if (tab.type === 'response-example') return 'pathname::exampleName';
if (tab.type === 'response-example') return 'pathname::exampleIndex';
if (SINGLETON_TAB_TYPES.has(tab.type)) return 'type';
return 'pathname';
};
@@ -379,6 +379,23 @@ export const serializeTab = (tab, collection) => {
const item = findItemInCollection(collection, tab.itemUid);
serialized.pathname = item?.pathname || tab.pathname;
serialized.exampleName = tab.exampleName;
const exampleIndex = item?.examples?.findIndex((example) => example.uid === tab.uid);
if (typeof exampleIndex === 'number' && exampleIndex >= 0) {
serialized.exampleIndex = exampleIndex;
}
serialized.exampleUid = tab.uid;
if (tab.name) {
serialized.name = tab.name;
}
} else if (accessor === 'pathname::exampleIndex') {
const item = findItemInCollection(collection, tab.itemUid);
serialized.pathname = item?.pathname || tab.pathname;
const exampleIndex = item?.examples?.findIndex((example) => example.uid === tab.uid);
if (typeof exampleIndex === 'number' && exampleIndex >= 0) {
serialized.exampleIndex = exampleIndex;
}
serialized.exampleName = tab.exampleName;
serialized.exampleUid = tab.uid;
if (tab.name) {
serialized.name = tab.name;
}
@@ -420,6 +437,22 @@ export const serializeActiveTab = (tab, collection) => {
return { accessor, value: `${pathname}::${tab.exampleName}` };
}
if (accessor === 'pathname::exampleIndex') {
const item = findItemInCollection(collection, tab.itemUid);
const pathname = item?.pathname || tab.pathname;
const exampleIndex = item?.examples?.findIndex((example) => example.uid === tab.uid);
if (typeof exampleIndex === 'number' && exampleIndex >= 0) {
return { accessor, value: `${pathname}::${exampleIndex}` };
}
if (tab.exampleName) {
return { accessor: 'pathname::exampleName', value: `${pathname}::${tab.exampleName}` };
}
return { accessor, value: `${pathname}::-1` };
}
return { accessor: 'type', value: tab.type };
};
@@ -434,7 +467,7 @@ export const isActiveTab = (tab, activeTab, collection) => {
if (accessor === 'pathname') {
const item = findItemInCollection(collection, tab.uid);
return item?.pathname === value || tab.pathname === value;
return tab.type !== 'response-example' && (item?.pathname === value || tab.pathname === value);
}
if (accessor === 'pathname::exampleName') {
@@ -443,11 +476,62 @@ export const isActiveTab = (tab, activeTab, collection) => {
return `${pathname}::${tab.exampleName}` === value;
}
if (accessor === 'pathname::exampleIndex') {
const item = findItemInCollection(collection, tab.itemUid);
const pathname = item?.pathname || tab.pathname;
const exampleIndex = item?.examples?.findIndex((example) => example.uid === tab.uid);
return `${pathname}::${exampleIndex}` === value;
}
return false;
};
const resolveResponseExampleTabState = ({ item, pathname, exampleName, exampleIndex, exampleUid }) => {
const hasExamples = Array.isArray(item?.examples);
const hasProvidedExampleIndex = typeof exampleIndex === 'number' && exampleIndex >= 0;
const hasValidExampleIndex = hasExamples && hasProvidedExampleIndex && exampleIndex < item.examples.length;
let resolvedExample = null;
if (hasExamples) {
if (hasValidExampleIndex) {
resolvedExample = item.examples[exampleIndex] || null;
} else {
if (typeof exampleUid === 'string' && exampleUid.length > 0) {
resolvedExample = item.examples.find((ex) => ex.uid === exampleUid) || null;
}
if (!resolvedExample && exampleName) {
resolvedExample = item.examples.find((ex) => ex.name === exampleName) || null;
}
}
}
const resolvedExampleIndex = hasExamples && resolvedExample?.uid
? item.examples.findIndex((ex) => ex.uid === resolvedExample.uid)
: -1;
const fallbackExampleIdentity = hasProvidedExampleIndex
? `${pathname}::${exampleIndex}`
: `${pathname}::${exampleName}`;
let normalizedExampleIndex = null;
if (resolvedExampleIndex >= 0) {
normalizedExampleIndex = resolvedExampleIndex;
} else if (hasProvidedExampleIndex) {
normalizedExampleIndex = exampleIndex;
}
return {
uid: resolvedExample?.uid || fallbackExampleIdentity,
itemUid: item?.uid || pathname,
exampleName: resolvedExample?.name || exampleName,
exampleIndex: normalizedExampleIndex
};
};
export const deserializeTab = (snapshotTab, collection) => {
const { accessor, pathname, exampleName, type } = snapshotTab;
const { accessor, pathname, exampleName, exampleIndex, exampleUid, type } = snapshotTab;
const restoredRequestPaneTab = typeof snapshotTab.request?.tab === 'string' ? snapshotTab.request.tab : null;
const tab = {
@@ -483,12 +567,13 @@ export const deserializeTab = (snapshotTab, collection) => {
if (type === 'folder-settings') {
tab.folderUid = item?.uid || pathname;
}
} else if (accessor === 'pathname::exampleName' && pathname && exampleName) {
} else if ((accessor === 'pathname::exampleName' || accessor === 'pathname::exampleIndex') && pathname) {
const item = findItemInCollectionByPathname(collection, pathname);
const example = item?.examples?.find((ex) => ex.name === exampleName);
tab.uid = example?.uid || `${pathname}::${exampleName}`;
tab.itemUid = item?.uid || pathname;
tab.exampleName = exampleName;
const resolvedTabState = resolveResponseExampleTabState({ item, pathname, exampleName, exampleIndex, exampleUid });
tab.uid = resolvedTabState.uid;
tab.itemUid = resolvedTabState.itemUid;
tab.exampleName = resolvedTabState.exampleName;
tab.exampleIndex = resolvedTabState.exampleIndex;
} else if (needsTypeBasedFallback) {
const collectionUidFromSnapshot = typeof snapshotTab.collection === 'string' && snapshotTab.collection.length > 0
? snapshotTab.collection
@@ -573,9 +658,20 @@ export const getActiveTabFromSnapshot = async (collectionPathname, collection, s
if (accessor === 'type') {
snapshotTab = tabsSnapshot.tabs.find((t) => t.type === value);
} else if (accessor === 'pathname') {
snapshotTab = tabsSnapshot.tabs.find((t) => t.pathname === value);
snapshotTab = tabsSnapshot.tabs.find((t) => t.pathname === value && t.type !== 'response-example');
} else if (accessor === 'pathname::exampleName') {
snapshotTab = tabsSnapshot.tabs.find((t) => `${t.pathname}::${t.exampleName}` === value);
} else if (accessor === 'pathname::exampleIndex') {
snapshotTab = tabsSnapshot.tabs.find((t) => `${t.pathname}::${t.exampleIndex}` === value);
if (!snapshotTab) {
const [pathname, rawIndex] = value.split('::');
const exampleIndex = Number(rawIndex);
if (pathname && Number.isInteger(exampleIndex) && exampleIndex >= 0) {
const candidateTabs = tabsSnapshot.tabs.filter((t) => t.type === 'response-example' && t.pathname === pathname);
snapshotTab = candidateTabs[exampleIndex] || null;
}
}
}
if (!snapshotTab) return null;

View File

@@ -11,7 +11,13 @@ jest.mock('nanoid', () => {
};
});
const { deserializeTab, hydrateSnapshotLookups, hydrateCollectionTabs } = require('./index');
const {
deserializeTab,
hydrateSnapshotLookups,
hydrateCollectionTabs,
isActiveTab,
getActiveTabFromSnapshot
} = require('./index');
describe('hydrateSnapshotLookups', () => {
it('builds lookup maps from array-based snapshot schema', () => {
@@ -280,6 +286,100 @@ describe('deserializeTab', () => {
expect(tab.uid).toBe('collection-uid-preferences');
});
it('restores response example by index when duplicate names exist', () => {
const collectionWithDuplicateExamples = {
uid: 'collection-uid',
pathname: '/collections/a',
items: [
{
uid: 'request-1',
pathname: '/collections/a/request-1.bru',
examples: [
{ uid: 'example-1', name: 'dup' },
{ uid: 'example-2', name: 'dup' }
]
}
]
};
const snapshotTab = {
type: 'response-example',
accessor: 'pathname::exampleIndex',
pathname: '/collections/a/request-1.bru',
exampleName: 'dup',
exampleIndex: 1,
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithDuplicateExamples);
expect(tab.uid).toBe('example-2');
expect(tab.exampleName).toBe('dup');
expect(tab.exampleIndex).toBe(1);
});
it('falls back to first matching name when example index is missing or invalid', () => {
const collectionWithDuplicateExamples = {
uid: 'collection-uid',
pathname: '/collections/a',
items: [
{
uid: 'request-1',
pathname: '/collections/a/request-1.bru',
examples: [
{ uid: 'example-1', name: 'dup' },
{ uid: 'example-2', name: 'dup' }
]
}
]
};
const snapshotTab = {
type: 'response-example',
accessor: 'pathname::exampleIndex',
pathname: '/collections/a/request-1.bru',
exampleName: 'dup',
exampleIndex: 99,
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithDuplicateExamples);
expect(tab.uid).toBe('example-1');
expect(tab.exampleName).toBe('dup');
expect(tab.exampleIndex).toBe(0);
});
it('keeps example uid and index consistent when uid fallback is used', () => {
const collectionWithDuplicateExamples = {
uid: 'collection-uid',
pathname: '/collections/a',
items: [
{
uid: 'request-1',
pathname: '/collections/a/request-1.bru',
examples: [
{ uid: 'example-1', name: 'dup' },
{ uid: 'example-2', name: 'dup' }
]
}
]
};
const snapshotTab = {
type: 'response-example',
accessor: 'pathname::exampleIndex',
pathname: '/collections/a/request-1.bru',
exampleName: 'dup',
exampleUid: 'example-1',
exampleIndex: 99,
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithDuplicateExamples);
expect(tab.uid).toBe('example-1');
expect(tab.exampleName).toBe('dup');
expect(tab.exampleIndex).toBe(0);
});
it('defaults grpc request pane to body when snapshot request tab is missing', () => {
const snapshotTab = {
type: 'grpc-request',
@@ -315,7 +415,6 @@ describe('deserializeTab', () => {
}
]
};
const snapshotTab = {
type: 'request',
accessor: 'pathname',
@@ -389,6 +488,96 @@ describe('deserializeTab', () => {
});
});
describe('active tab matching', () => {
it('does not mark response example tab as active for pathname accessor', () => {
const collection = {
uid: 'collection-uid',
pathname: '/collections/a',
items: [
{
uid: 'request-1',
pathname: '/collections/a/request-1.bru',
examples: [{ uid: 'example-1', name: 'Sample' }]
}
]
};
const tab = {
uid: 'example-1',
type: 'response-example',
itemUid: 'request-1',
pathname: '/collections/a/request-1.bru',
exampleName: 'Sample'
};
const activeTab = {
accessor: 'pathname',
value: '/collections/a/request-1.bru'
};
expect(isActiveTab(tab, activeTab, collection)).toBe(false);
});
});
describe('getActiveTabFromSnapshot', () => {
beforeEach(() => {
global.window = global.window || {};
global.window.ipcRenderer = {
invoke: jest.fn()
};
});
afterEach(() => {
delete global.window.ipcRenderer;
});
it('resolves response example using index accessor when duplicate names exist', async () => {
const collection = {
uid: 'collection-uid',
pathname: '/collections/a',
items: [
{
uid: 'request-1',
pathname: '/collections/a/request-1.bru',
examples: [
{ uid: 'example-1', name: 'dup' },
{ uid: 'example-2', name: 'dup' }
]
}
]
};
window.ipcRenderer.invoke.mockResolvedValue({
activeTab: {
accessor: 'pathname::exampleIndex',
value: '/collections/a/request-1.bru::1'
},
tabs: [
{
type: 'response-example',
accessor: 'pathname::exampleIndex',
pathname: '/collections/a/request-1.bru',
exampleName: 'dup',
exampleIndex: 0,
permanent: true
},
{
type: 'response-example',
accessor: 'pathname::exampleIndex',
pathname: '/collections/a/request-1.bru',
exampleName: 'dup',
exampleIndex: 1,
permanent: true
}
]
});
const activeTab = await getActiveTabFromSnapshot('/collections/a', collection, null, null);
expect(activeTab.uid).toBe('example-2');
expect(activeTab.exampleIndex).toBe(1);
});
});
describe('hydrateCollectionTabs', () => {
beforeEach(() => {
global.window = {

View File

@@ -28,11 +28,13 @@ const buildWorkspaceCollectionLookupKey = (workspacePathname, collectionPathname
const tabSchema = yup.object({
type: yup.string().required(),
accessor: yup.string().oneOf(['pathname', 'pathname::exampleName', 'type']).required(),
accessor: yup.string().oneOf(['pathname', 'pathname::exampleName', 'pathname::exampleIndex', 'type']).required(),
pathname: yup.string().nullable(),
permanent: yup.boolean().required(),
name: yup.string().optional(),
exampleName: yup.string().optional(),
exampleIndex: yup.number().integer().min(0).optional(),
exampleUid: yup.string().optional(),
request: yup.object({
tab: yup.string(),
width: yup.number().nullable(),
@@ -46,7 +48,7 @@ const tabSchema = yup.object({
});
const activeTabSchema = yup.object({
accessor: yup.string().oneOf(['pathname', 'pathname::exampleName', 'type']).required(),
accessor: yup.string().oneOf(['pathname', 'pathname::exampleName', 'pathname::exampleIndex', 'type']).required(),
value: yup.string().required()
});
@@ -500,7 +502,7 @@ class SnapshotManager {
return null;
}
if (!['pathname', 'pathname::exampleName', 'type'].includes(activeTab.accessor) || typeof activeTab.value !== 'string') {
if (!['pathname', 'pathname::exampleName', 'pathname::exampleIndex', 'type'].includes(activeTab.accessor) || typeof activeTab.value !== 'string') {
return null;
}

View File

@@ -1,7 +1,9 @@
import { test, expect, closeElectronApp, type Page } from '../../playwright';
import {
createCollection,
createExampleFromSidebar,
createRequest,
openExampleFromSidebar,
openRequest
} from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
@@ -77,4 +79,117 @@ test.describe('Snapshot: Sidebar-Tab Restoration', () => {
await closeElectronApp(app2);
});
});
test('when request and example are open, last active request restores as active after restart', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-sidebar-request-active-over-example');
const colPath = await createTmpDir('col');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await test.step('Create request, create example, then make request last active', async () => {
await createCollection(page, 'TestCol', colPath);
await createRequest(page, 'ReqAlpha', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' });
await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true });
await createExampleFromSidebar(page, 'ReqAlpha', 'Example One');
await expect(page.getByTestId('response-example-title')).toHaveText('ReqAlpha / Example One');
await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true });
const locators = buildCommonLocators(page);
await expect(locators.tabs.activeRequestTab()).toContainText('ReqAlpha');
});
await test.step('Close and restart app', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
});
await test.step('Verify request restores as active and request click does not focus example', async () => {
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
const locators = buildCommonLocators(page2);
await expect(locators.tabs.activeRequestTab()).toContainText('ReqAlpha', { timeout: 15000 });
await openRequest(page2, 'TestCol', 'ReqAlpha', { persist: true });
await expect(locators.tabs.requestTab('ReqAlpha')).toHaveCount(1);
await expect(page2.getByTestId('response-example-title')).not.toBeVisible();
await closeElectronApp(app2);
});
});
test('when last active tab is an example, it restores as active after restart', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-sidebar-example-active-restore');
const colPath = await createTmpDir('col');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await test.step('Create request and example, then make example active', async () => {
await createCollection(page, 'TestCol', colPath);
await createRequest(page, 'ReqAlpha', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' });
await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true });
await createExampleFromSidebar(page, 'ReqAlpha', 'Example One');
await openExampleFromSidebar(page, 'ReqAlpha', 'Example One');
await expect(page.getByTestId('response-example-title')).toHaveText('ReqAlpha / Example One');
});
await test.step('Close and restart app', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
});
await test.step('Verify example restores as active', async () => {
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page2.getByTestId('response-example-title')).toHaveText('ReqAlpha / Example One', { timeout: 15000 });
await closeElectronApp(app2);
});
});
test('when duplicate example names exist, snapshot restores the same active example by index', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-sidebar-duplicate-example-restore');
const colPath = await createTmpDir('col');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await test.step('Create request and two examples with duplicate names, then activate second', async () => {
await createCollection(page, 'TestCol', colPath);
await createRequest(page, 'ReqAlpha', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' });
await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true });
await createExampleFromSidebar(page, 'ReqAlpha', 'DupExample', 'first-desc');
await createExampleFromSidebar(page, 'ReqAlpha', 'DupExample', 'second-desc');
await openExampleFromSidebar(page, 'ReqAlpha', 'DupExample', 1);
await expect(page.getByTestId('response-example-description')).toHaveText('second-desc');
});
await test.step('Close and restart app', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
});
await test.step('Verify second duplicate example restores as active', async () => {
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page2.getByTestId('response-example-title')).toHaveText('ReqAlpha / DupExample', { timeout: 15000 });
await expect(page2.getByTestId('response-example-description')).toHaveText('second-desc');
await closeElectronApp(app2);
});
});
});

View File

@@ -1238,6 +1238,41 @@ const sendAndWaitForResponse = async (page: Page) => {
});
};
const createExampleFromSidebar = async (page: Page, requestName: string, exampleName: string, description: string = '') => {
const requestRow = page.locator('.collection-item-name').filter({ hasText: requestName }).first();
await requestRow.hover();
await requestRow.locator('..').locator('.menu-icon').click({ force: true });
await page.locator('.dropdown-item').filter({ hasText: 'Create Example' }).click();
const exampleInput = page.getByTestId('create-example-name-input');
await expect(exampleInput).toBeVisible();
await exampleInput.clear();
await exampleInput.fill(exampleName);
const descriptionInput = page.getByTestId('create-example-description-input');
await descriptionInput.clear();
await descriptionInput.fill(description);
await page.getByRole('button', { name: 'Create Example' }).click();
await expect(page.locator('text=Create Response Example')).not.toBeAttached();
};
const openExampleFromSidebar = async (page: Page, requestName: string, exampleName: string, index: number = 0) => {
const requestRow = page.locator('.collection-item-name').filter({ hasText: requestName }).first();
const requestBranch = requestRow.locator('..');
const exampleRow = requestBranch
.locator('.collection-item-name')
.filter({ has: page.locator('.example-icon') })
.getByText(exampleName, { exact: true })
.nth(index);
if (!(await exampleRow.isVisible())) {
await requestRow.getByTestId('request-item-chevron').click();
}
await expect(exampleRow).toBeVisible();
await exampleRow.click();
};
export {
closeAllCollections,
openCollection,
@@ -1283,7 +1318,9 @@ export {
addPostResponseScript,
addTestScript,
sendAndWaitForErrorCard,
sendAndWaitForResponse
sendAndWaitForResponse,
createExampleFromSidebar,
openExampleFromSidebar
};
export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput };