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

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