Skip to content

Commit 2c9dc9d

Browse files
authored
fix: correct the request type tabs in the snapshot (usebruno#7994)
1 parent 9df06e1 commit 2c9dc9d

6 files changed

Lines changed: 392 additions & 26 deletions

File tree

packages/bruno-app/src/components/OpenAPISyncTab/hooks/useOpenAPISync.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const useOpenAPISync = (collection) => {
9393
uid: itemUid,
9494
collectionUid: collection.uid,
9595
requestPaneTab: item ? getDefaultRequestPaneTab(item) : undefined,
96-
type: 'request'
96+
type: item?.type ?? 'request'
9797
}));
9898
}
9999
};

packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ taskMiddleware.startListening({
3434
addTab({
3535
uid: item.uid,
3636
collectionUid: collection.uid,
37+
type: item.type,
38+
pathname: item.pathname,
3739
requestPaneTab: getDefaultRequestPaneTab(item),
3840
preview: task?.preview ?? true,
3941
...(item.isTransient ? { isTransient: true } : {})

packages/bruno-app/src/utils/snapshot/index.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,18 @@ const getAccessor = (tab) => {
349349
return 'pathname';
350350
};
351351

352+
const getDefaultRequestPaneTabForType = (type) => {
353+
if (type === 'grpc-request' || type === 'ws-request') {
354+
return 'body';
355+
}
356+
357+
if (type === 'graphql-request') {
358+
return 'query';
359+
}
360+
361+
return 'params';
362+
};
363+
352364
export const serializeTab = (tab, collection) => {
353365
const accessor = getAccessor(tab);
354366
const serialized = {
@@ -436,14 +448,15 @@ export const isActiveTab = (tab, activeTab, collection) => {
436448

437449
export const deserializeTab = (snapshotTab, collection) => {
438450
const { accessor, pathname, exampleName, type } = snapshotTab;
451+
const restoredRequestPaneTab = typeof snapshotTab.request?.tab === 'string' ? snapshotTab.request.tab : null;
439452

440453
const tab = {
441454
collectionUid: collection.uid,
442455
type,
443456
preview: !snapshotTab.permanent,
444457
name: snapshotTab.name || null,
445458
pathname: pathname || null,
446-
requestPaneTab: snapshotTab.request?.tab || 'params',
459+
requestPaneTab: restoredRequestPaneTab || getDefaultRequestPaneTabForType(type),
447460
requestPaneWidth: snapshotTab.request?.width || null,
448461
requestPaneHeight: snapshotTab.request?.height || null,
449462
responsePaneTab: snapshotTab.response?.tab || 'response',
@@ -461,6 +474,11 @@ export const deserializeTab = (snapshotTab, collection) => {
461474

462475
if (accessor === 'pathname' && pathname) {
463476
const item = findItemInCollectionByPathname(collection, pathname);
477+
const resolvedType = item?.type || type;
478+
tab.type = resolvedType;
479+
if (!restoredRequestPaneTab) {
480+
tab.requestPaneTab = getDefaultRequestPaneTabForType(resolvedType);
481+
}
464482
tab.uid = item?.uid || pathname;
465483
if (type === 'folder-settings') {
466484
tab.folderUid = item?.uid || pathname;

packages/bruno-app/src/utils/snapshot/index.spec.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,114 @@ describe('deserializeTab', () => {
279279
const tab = deserializeTab(snapshotTab, collection);
280280
expect(tab.uid).toBe('collection-uid-preferences');
281281
});
282+
283+
it('defaults grpc request pane to body when snapshot request tab is missing', () => {
284+
const snapshotTab = {
285+
type: 'grpc-request',
286+
accessor: 'pathname',
287+
pathname: '/collections/a/grpc-request.bru',
288+
permanent: true
289+
};
290+
291+
const tab = deserializeTab(snapshotTab, collection);
292+
expect(tab.requestPaneTab).toBe('body');
293+
});
294+
295+
it('defaults websocket request pane to body when snapshot request tab is missing', () => {
296+
const snapshotTab = {
297+
type: 'ws-request',
298+
accessor: 'pathname',
299+
pathname: '/collections/a/ws-request.bru',
300+
permanent: true
301+
};
302+
303+
const tab = deserializeTab(snapshotTab, collection);
304+
expect(tab.requestPaneTab).toBe('body');
305+
});
306+
307+
it('resolves generic request snapshot type to item type using pathname', () => {
308+
const collectionWithGrpcItem = {
309+
...collection,
310+
items: [
311+
{
312+
uid: 'grpc-item-1',
313+
pathname: '/collections/a/grpc-item.bru',
314+
type: 'grpc-request'
315+
}
316+
]
317+
};
318+
319+
const snapshotTab = {
320+
type: 'request',
321+
accessor: 'pathname',
322+
pathname: '/collections/a/grpc-item.bru',
323+
permanent: true
324+
};
325+
326+
const tab = deserializeTab(snapshotTab, collectionWithGrpcItem);
327+
expect(tab.type).toBe('grpc-request');
328+
expect(tab.requestPaneTab).toBe('body');
329+
});
330+
331+
it('defaults to body for resolved websocket item type when generic snapshot request tab is missing', () => {
332+
const collectionWithWsItem = {
333+
...collection,
334+
items: [
335+
{
336+
uid: 'ws-item-1',
337+
pathname: '/collections/a/ws-item.bru',
338+
type: 'ws-request'
339+
}
340+
]
341+
};
342+
343+
const snapshotTab = {
344+
type: 'request',
345+
accessor: 'pathname',
346+
pathname: '/collections/a/ws-item.bru',
347+
permanent: true
348+
};
349+
350+
const tab = deserializeTab(snapshotTab, collectionWithWsItem);
351+
expect(tab.type).toBe('ws-request');
352+
expect(tab.requestPaneTab).toBe('body');
353+
});
354+
355+
it('defaults graphql request pane to query when snapshot request tab is missing', () => {
356+
const snapshotTab = {
357+
type: 'graphql-request',
358+
accessor: 'pathname',
359+
pathname: '/collections/a/graphql-request.bru',
360+
permanent: true
361+
};
362+
363+
const tab = deserializeTab(snapshotTab, collection);
364+
expect(tab.requestPaneTab).toBe('query');
365+
});
366+
367+
it('resolves generic request snapshot type to graphql-request item type using pathname', () => {
368+
const collectionWithGraphqlItem = {
369+
...collection,
370+
items: [
371+
{
372+
uid: 'graphql-item-1',
373+
pathname: '/collections/a/graphql-item.bru',
374+
type: 'graphql-request'
375+
}
376+
]
377+
};
378+
379+
const snapshotTab = {
380+
type: 'request',
381+
accessor: 'pathname',
382+
pathname: '/collections/a/graphql-item.bru',
383+
permanent: true
384+
};
385+
386+
const tab = deserializeTab(snapshotTab, collectionWithGraphqlItem);
387+
expect(tab.type).toBe('graphql-request');
388+
expect(tab.requestPaneTab).toBe('query');
389+
});
282390
});
283391

284392
describe('hydrateCollectionTabs', () => {

tests/snapshots/request-pane-interactivity.spec.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import path from 'path';
2+
import fs from 'fs';
13
import { test, expect, closeElectronApp } from '../../playwright';
24
import {
35
createCollection,
@@ -6,6 +8,34 @@ import {
68
} from '../utils/page';
79
import { buildCommonLocators } from '../utils/page/locators';
810

11+
const readSnapshot = (userDataPath: string) => {
12+
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
13+
if (!fs.existsSync(snapshotPath)) return null;
14+
return JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
15+
};
16+
17+
const findSnapshotRequestTab = (snapshot: any, requestName: string) => {
18+
if (!snapshot || !Array.isArray(snapshot.collections)) {
19+
return null;
20+
}
21+
22+
for (const collection of snapshot.collections) {
23+
if (!Array.isArray(collection?.tabs)) continue;
24+
25+
const tab = collection.tabs.find((candidate) => (
26+
candidate?.accessor === 'pathname'
27+
&& typeof candidate?.pathname === 'string'
28+
&& candidate.pathname.includes(requestName)
29+
));
30+
31+
if (tab) {
32+
return tab;
33+
}
34+
}
35+
36+
return null;
37+
};
38+
939
test.describe('Snapshot: Request Pane Interactivity', () => {
1040
test('grpc request pane tab interactivity is restored after restart', async ({ launchElectronApp, createTmpDir }) => {
1141
const userDataPath = await createTmpDir('snap-grpc-interactivity');
@@ -105,4 +135,172 @@ test.describe('Snapshot: Request Pane Interactivity', () => {
105135
await closeElectronApp(app2);
106136
});
107137
});
138+
139+
test('grpc snapshot stores concrete type and body tab key', async ({ launchElectronApp, createTmpDir }) => {
140+
const userDataPath = await createTmpDir('snap-grpc-snapshot-type-tab-key');
141+
const colPath = await createTmpDir('col');
142+
143+
const app = await launchElectronApp({ userDataPath });
144+
const page = await app.firstWindow();
145+
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
146+
147+
await test.step('Create collection and gRPC request', async () => {
148+
await createCollection(page, 'TestCol', colPath);
149+
150+
const locators = buildCommonLocators(page);
151+
await locators.sidebar.collection('TestCol').hover();
152+
await locators.actions.collectionActions('TestCol').click();
153+
await locators.dropdown.item('New Request').click();
154+
155+
await page.getByTestId('grpc-request').click();
156+
await page.getByTestId('request-name').fill('ReqGrpcSnapshot');
157+
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
158+
await page.keyboard.type('grpc://localhost:50051');
159+
await locators.modal.button('Create').click();
160+
161+
await openRequest(page, 'TestCol', 'ReqGrpcSnapshot', { persist: true });
162+
await selectRequestPaneTab(page, 'Message');
163+
});
164+
165+
await test.step('Close app and verify snapshot stores grpc-request/body', async () => {
166+
await page.waitForTimeout(2000);
167+
await closeElectronApp(app);
168+
169+
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
170+
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
171+
172+
const snapshot = readSnapshot(userDataPath);
173+
const tab = findSnapshotRequestTab(snapshot, 'ReqGrpcSnapshot');
174+
expect(tab).toBeTruthy();
175+
expect(tab.type).toBe('grpc-request');
176+
expect(tab.request?.tab).toBe('body');
177+
});
178+
179+
await test.step('Verify restore opens Message tab and avoids 404', async () => {
180+
const app2 = await launchElectronApp({ userDataPath });
181+
const page2 = await app2.firstWindow();
182+
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
183+
184+
const locators = buildCommonLocators(page2);
185+
await expect(locators.tabs.requestTab('ReqGrpcSnapshot')).toBeVisible({ timeout: 15000 });
186+
await locators.tabs.requestTab('ReqGrpcSnapshot').click({ force: true });
187+
188+
await expect(page2.getByTestId('responsive-tab-body')).toHaveAttribute('aria-selected', 'true');
189+
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
190+
191+
await closeElectronApp(app2);
192+
});
193+
});
194+
195+
test('websocket snapshot stores concrete type and body tab key', async ({ launchElectronApp, createTmpDir }) => {
196+
const userDataPath = await createTmpDir('snap-ws-snapshot-type-tab-key');
197+
const colPath = await createTmpDir('col');
198+
199+
const app = await launchElectronApp({ userDataPath });
200+
const page = await app.firstWindow();
201+
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
202+
203+
await test.step('Create collection and WebSocket request', async () => {
204+
await createCollection(page, 'TestCol', colPath);
205+
206+
const locators = buildCommonLocators(page);
207+
await locators.sidebar.collection('TestCol').hover();
208+
await locators.actions.collectionActions('TestCol').click();
209+
await locators.dropdown.item('New Request').click();
210+
211+
await page.getByTestId('ws-request').click();
212+
await page.getByTestId('request-name').fill('ReqWsSnapshot');
213+
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
214+
await page.keyboard.type('ws://localhost:8080');
215+
await locators.modal.button('Create').click();
216+
217+
await openRequest(page, 'TestCol', 'ReqWsSnapshot', { persist: true });
218+
await selectRequestPaneTab(page, 'Message');
219+
});
220+
221+
await test.step('Close app and verify snapshot stores ws-request/body', async () => {
222+
await page.waitForTimeout(2000);
223+
await closeElectronApp(app);
224+
225+
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
226+
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
227+
228+
const snapshot = readSnapshot(userDataPath);
229+
const tab = findSnapshotRequestTab(snapshot, 'ReqWsSnapshot');
230+
expect(tab).toBeTruthy();
231+
expect(tab.type).toBe('ws-request');
232+
expect(tab.request?.tab).toBe('body');
233+
});
234+
235+
await test.step('Verify restore opens Message tab and avoids 404', async () => {
236+
const app2 = await launchElectronApp({ userDataPath });
237+
const page2 = await app2.firstWindow();
238+
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
239+
240+
const locators = buildCommonLocators(page2);
241+
await expect(locators.tabs.requestTab('ReqWsSnapshot')).toBeVisible({ timeout: 15000 });
242+
await locators.tabs.requestTab('ReqWsSnapshot').click({ force: true });
243+
244+
await expect(page2.getByTestId('responsive-tab-body')).toHaveAttribute('aria-selected', 'true');
245+
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
246+
247+
await closeElectronApp(app2);
248+
});
249+
});
250+
251+
test('graphql snapshot stores concrete type and query tab key', async ({ launchElectronApp, createTmpDir }) => {
252+
const userDataPath = await createTmpDir('snap-graphql-snapshot-type-tab-key');
253+
const colPath = await createTmpDir('col');
254+
255+
const app = await launchElectronApp({ userDataPath });
256+
const page = await app.firstWindow();
257+
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
258+
259+
await test.step('Create collection and GraphQL request', async () => {
260+
await createCollection(page, 'TestCol', colPath);
261+
262+
const locators = buildCommonLocators(page);
263+
await locators.sidebar.collection('TestCol').hover();
264+
await locators.actions.collectionActions('TestCol').click();
265+
await locators.dropdown.item('New Request').click();
266+
267+
await page.getByTestId('graphql-request').click();
268+
await page.getByTestId('request-name').fill('ReqGraphSnapshot');
269+
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
270+
await page.keyboard.type('https://echo.usebruno.com/graphql');
271+
await locators.modal.button('Create').click();
272+
273+
await openRequest(page, 'TestCol', 'ReqGraphSnapshot', { persist: true });
274+
await selectRequestPaneTab(page, 'Headers');
275+
});
276+
277+
await test.step('Close app and verify snapshot stores graphql-request/headers', async () => {
278+
await page.waitForTimeout(2000);
279+
await closeElectronApp(app);
280+
281+
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
282+
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
283+
284+
const snapshot = readSnapshot(userDataPath);
285+
const tab = findSnapshotRequestTab(snapshot, 'ReqGraphSnapshot');
286+
expect(tab).toBeTruthy();
287+
expect(tab.type).toBe('graphql-request');
288+
expect(tab.request?.tab).toBe('headers');
289+
});
290+
291+
await test.step('Verify restore opens Headers tab and avoids 404', async () => {
292+
const app2 = await launchElectronApp({ userDataPath });
293+
const page2 = await app2.firstWindow();
294+
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
295+
296+
const locators = buildCommonLocators(page2);
297+
await expect(locators.tabs.requestTab('ReqGraphSnapshot')).toBeVisible({ timeout: 15000 });
298+
await locators.tabs.requestTab('ReqGraphSnapshot').click({ force: true });
299+
300+
await expect(page2.getByTestId('responsive-tab-headers')).toHaveAttribute('aria-selected', 'true');
301+
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
302+
303+
await closeElectronApp(app2);
304+
});
305+
});
108306
});

0 commit comments

Comments
 (0)