fix: correct the request type tabs in the snapshot (#7994)

This commit is contained in:
Sid
2026-05-13 19:05:34 +05:30
committed by GitHub
parent 9df06e152a
commit 2c9dc9dcf8
6 changed files with 393 additions and 27 deletions

View File

@@ -93,7 +93,7 @@ const useOpenAPISync = (collection) => {
uid: itemUid,
collectionUid: collection.uid,
requestPaneTab: item ? getDefaultRequestPaneTab(item) : undefined,
type: 'request'
type: item?.type ?? 'request'
}));
}
};

View File

@@ -34,6 +34,8 @@ taskMiddleware.startListening({
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: item.type,
pathname: item.pathname,
requestPaneTab: getDefaultRequestPaneTab(item),
preview: task?.preview ?? true,
...(item.isTransient ? { isTransient: true } : {})

View File

@@ -349,6 +349,18 @@ const getAccessor = (tab) => {
return 'pathname';
};
const getDefaultRequestPaneTabForType = (type) => {
if (type === 'grpc-request' || type === 'ws-request') {
return 'body';
}
if (type === 'graphql-request') {
return 'query';
}
return 'params';
};
export const serializeTab = (tab, collection) => {
const accessor = getAccessor(tab);
const serialized = {
@@ -436,6 +448,7 @@ export const isActiveTab = (tab, activeTab, collection) => {
export const deserializeTab = (snapshotTab, collection) => {
const { accessor, pathname, exampleName, type } = snapshotTab;
const restoredRequestPaneTab = typeof snapshotTab.request?.tab === 'string' ? snapshotTab.request.tab : null;
const tab = {
collectionUid: collection.uid,
@@ -443,7 +456,7 @@ export const deserializeTab = (snapshotTab, collection) => {
preview: !snapshotTab.permanent,
name: snapshotTab.name || null,
pathname: pathname || null,
requestPaneTab: snapshotTab.request?.tab || 'params',
requestPaneTab: restoredRequestPaneTab || getDefaultRequestPaneTabForType(type),
requestPaneWidth: snapshotTab.request?.width || null,
requestPaneHeight: snapshotTab.request?.height || null,
responsePaneTab: snapshotTab.response?.tab || 'response',
@@ -461,6 +474,11 @@ export const deserializeTab = (snapshotTab, collection) => {
if (accessor === 'pathname' && pathname) {
const item = findItemInCollectionByPathname(collection, pathname);
const resolvedType = item?.type || type;
tab.type = resolvedType;
if (!restoredRequestPaneTab) {
tab.requestPaneTab = getDefaultRequestPaneTabForType(resolvedType);
}
tab.uid = item?.uid || pathname;
if (type === 'folder-settings') {
tab.folderUid = item?.uid || pathname;

View File

@@ -279,6 +279,114 @@ describe('deserializeTab', () => {
const tab = deserializeTab(snapshotTab, collection);
expect(tab.uid).toBe('collection-uid-preferences');
});
it('defaults grpc request pane to body when snapshot request tab is missing', () => {
const snapshotTab = {
type: 'grpc-request',
accessor: 'pathname',
pathname: '/collections/a/grpc-request.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.requestPaneTab).toBe('body');
});
it('defaults websocket request pane to body when snapshot request tab is missing', () => {
const snapshotTab = {
type: 'ws-request',
accessor: 'pathname',
pathname: '/collections/a/ws-request.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.requestPaneTab).toBe('body');
});
it('resolves generic request snapshot type to item type using pathname', () => {
const collectionWithGrpcItem = {
...collection,
items: [
{
uid: 'grpc-item-1',
pathname: '/collections/a/grpc-item.bru',
type: 'grpc-request'
}
]
};
const snapshotTab = {
type: 'request',
accessor: 'pathname',
pathname: '/collections/a/grpc-item.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithGrpcItem);
expect(tab.type).toBe('grpc-request');
expect(tab.requestPaneTab).toBe('body');
});
it('defaults to body for resolved websocket item type when generic snapshot request tab is missing', () => {
const collectionWithWsItem = {
...collection,
items: [
{
uid: 'ws-item-1',
pathname: '/collections/a/ws-item.bru',
type: 'ws-request'
}
]
};
const snapshotTab = {
type: 'request',
accessor: 'pathname',
pathname: '/collections/a/ws-item.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithWsItem);
expect(tab.type).toBe('ws-request');
expect(tab.requestPaneTab).toBe('body');
});
it('defaults graphql request pane to query when snapshot request tab is missing', () => {
const snapshotTab = {
type: 'graphql-request',
accessor: 'pathname',
pathname: '/collections/a/graphql-request.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collection);
expect(tab.requestPaneTab).toBe('query');
});
it('resolves generic request snapshot type to graphql-request item type using pathname', () => {
const collectionWithGraphqlItem = {
...collection,
items: [
{
uid: 'graphql-item-1',
pathname: '/collections/a/graphql-item.bru',
type: 'graphql-request'
}
]
};
const snapshotTab = {
type: 'request',
accessor: 'pathname',
pathname: '/collections/a/graphql-item.bru',
permanent: true
};
const tab = deserializeTab(snapshotTab, collectionWithGraphqlItem);
expect(tab.type).toBe('graphql-request');
expect(tab.requestPaneTab).toBe('query');
});
});
describe('hydrateCollectionTabs', () => {

View File

@@ -1,3 +1,5 @@
import path from 'path';
import fs from 'fs';
import { test, expect, closeElectronApp } from '../../playwright';
import {
createCollection,
@@ -6,6 +8,34 @@ import {
} from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
const readSnapshot = (userDataPath: string) => {
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
if (!fs.existsSync(snapshotPath)) return null;
return JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
};
const findSnapshotRequestTab = (snapshot: any, requestName: string) => {
if (!snapshot || !Array.isArray(snapshot.collections)) {
return null;
}
for (const collection of snapshot.collections) {
if (!Array.isArray(collection?.tabs)) continue;
const tab = collection.tabs.find((candidate) => (
candidate?.accessor === 'pathname'
&& typeof candidate?.pathname === 'string'
&& candidate.pathname.includes(requestName)
));
if (tab) {
return tab;
}
}
return null;
};
test.describe('Snapshot: Request Pane Interactivity', () => {
test('grpc request pane tab interactivity is restored after restart', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-grpc-interactivity');
@@ -105,4 +135,172 @@ test.describe('Snapshot: Request Pane Interactivity', () => {
await closeElectronApp(app2);
});
});
test('grpc snapshot stores concrete type and body tab key', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-grpc-snapshot-type-tab-key');
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 collection and gRPC request', async () => {
await createCollection(page, 'TestCol', colPath);
const locators = buildCommonLocators(page);
await locators.sidebar.collection('TestCol').hover();
await locators.actions.collectionActions('TestCol').click();
await locators.dropdown.item('New Request').click();
await page.getByTestId('grpc-request').click();
await page.getByTestId('request-name').fill('ReqGrpcSnapshot');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
await page.keyboard.type('grpc://localhost:50051');
await locators.modal.button('Create').click();
await openRequest(page, 'TestCol', 'ReqGrpcSnapshot', { persist: true });
await selectRequestPaneTab(page, 'Message');
});
await test.step('Close app and verify snapshot stores grpc-request/body', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
const snapshot = readSnapshot(userDataPath);
const tab = findSnapshotRequestTab(snapshot, 'ReqGrpcSnapshot');
expect(tab).toBeTruthy();
expect(tab.type).toBe('grpc-request');
expect(tab.request?.tab).toBe('body');
});
await test.step('Verify restore opens Message tab and avoids 404', 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.requestTab('ReqGrpcSnapshot')).toBeVisible({ timeout: 15000 });
await locators.tabs.requestTab('ReqGrpcSnapshot').click({ force: true });
await expect(page2.getByTestId('responsive-tab-body')).toHaveAttribute('aria-selected', 'true');
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
await closeElectronApp(app2);
});
});
test('websocket snapshot stores concrete type and body tab key', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-ws-snapshot-type-tab-key');
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 collection and WebSocket request', async () => {
await createCollection(page, 'TestCol', colPath);
const locators = buildCommonLocators(page);
await locators.sidebar.collection('TestCol').hover();
await locators.actions.collectionActions('TestCol').click();
await locators.dropdown.item('New Request').click();
await page.getByTestId('ws-request').click();
await page.getByTestId('request-name').fill('ReqWsSnapshot');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
await page.keyboard.type('ws://localhost:8080');
await locators.modal.button('Create').click();
await openRequest(page, 'TestCol', 'ReqWsSnapshot', { persist: true });
await selectRequestPaneTab(page, 'Message');
});
await test.step('Close app and verify snapshot stores ws-request/body', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
const snapshot = readSnapshot(userDataPath);
const tab = findSnapshotRequestTab(snapshot, 'ReqWsSnapshot');
expect(tab).toBeTruthy();
expect(tab.type).toBe('ws-request');
expect(tab.request?.tab).toBe('body');
});
await test.step('Verify restore opens Message tab and avoids 404', 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.requestTab('ReqWsSnapshot')).toBeVisible({ timeout: 15000 });
await locators.tabs.requestTab('ReqWsSnapshot').click({ force: true });
await expect(page2.getByTestId('responsive-tab-body')).toHaveAttribute('aria-selected', 'true');
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
await closeElectronApp(app2);
});
});
test('graphql snapshot stores concrete type and query tab key', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('snap-graphql-snapshot-type-tab-key');
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 collection and GraphQL request', async () => {
await createCollection(page, 'TestCol', colPath);
const locators = buildCommonLocators(page);
await locators.sidebar.collection('TestCol').hover();
await locators.actions.collectionActions('TestCol').click();
await locators.dropdown.item('New Request').click();
await page.getByTestId('graphql-request').click();
await page.getByTestId('request-name').fill('ReqGraphSnapshot');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
await page.keyboard.type('https://echo.usebruno.com/graphql');
await locators.modal.button('Create').click();
await openRequest(page, 'TestCol', 'ReqGraphSnapshot', { persist: true });
await selectRequestPaneTab(page, 'Headers');
});
await test.step('Close app and verify snapshot stores graphql-request/headers', async () => {
await page.waitForTimeout(2000);
await closeElectronApp(app);
const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json');
await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true);
const snapshot = readSnapshot(userDataPath);
const tab = findSnapshotRequestTab(snapshot, 'ReqGraphSnapshot');
expect(tab).toBeTruthy();
expect(tab.type).toBe('graphql-request');
expect(tab.request?.tab).toBe('headers');
});
await test.step('Verify restore opens Headers tab and avoids 404', 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.requestTab('ReqGraphSnapshot')).toBeVisible({ timeout: 15000 });
await locators.tabs.requestTab('ReqGraphSnapshot').click({ force: true });
await expect(page2.getByTestId('responsive-tab-headers')).toHaveAttribute('aria-selected', 'true');
await expect(page2.locator('text=404 | Not found')).not.toBeVisible();
await closeElectronApp(app2);
});
});
});

View File

@@ -812,37 +812,77 @@ const getResponseBody = async (page: Page): Promise<string> => {
return await page.locator('.response-pane').innerText();
};
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const trySelectPaneTabOnce = async (page: Page, paneSelector: string, tabName: string) => {
const pane = page.locator(paneSelector);
const visibleTab = pane.locator('.tabs').getByRole('tab', { name: tabName });
if (await visibleTab.isVisible().catch(() => false)) {
try {
await visibleTab.click({ timeout: 2000 });
await expect(visibleTab).toContainClass('active', { timeout: 500 });
return true;
} catch {
return false;
}
}
const overflowButton = pane.locator('.tabs .more-tabs');
if (!(await overflowButton.isVisible().catch(() => false))) {
return false;
}
try {
await overflowButton.click({ force: true, timeout: 1000 });
} catch {
return false;
}
const dropdownItem = page
.getByRole('menuitem', { name: new RegExp(escapeRegExp(tabName), 'i') })
.first();
if (await dropdownItem.isVisible({ timeout: 1500 }).catch(() => false)) {
try {
await dropdownItem.click({ force: true, timeout: 2000 });
await expect(visibleTab).toContainClass('active', { timeout: 500 });
return true;
} catch {
return false;
}
}
const fallbackDropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName }).first();
if (await fallbackDropdownItem.isVisible({ timeout: 1500 }).catch(() => false)) {
try {
await fallbackDropdownItem.click({ force: true, timeout: 2000 });
await expect(visibleTab).toContainClass('active', { timeout: 500 });
return true;
} catch {
return false;
}
}
return false;
};
const selectPaneTab = async (page: Page, paneSelector: string, tabName: string) => {
await test.step(`Select tab "${tabName}" in ${paneSelector}`, async () => {
const pane = page.locator(paneSelector);
await expect(pane).toBeVisible();
await expect(pane.locator('.tabs')).toBeVisible();
const visibleTab = pane.locator('.tabs').getByRole('tab', { name: tabName });
// Check if tab is directly visible
if (await visibleTab.isVisible()) {
await visibleTab.click();
await expect(visibleTab).toContainClass('active');
return;
}
const overflowButton = pane.locator('.tabs .more-tabs');
// Check if there's an overflow dropdown
if (await overflowButton.isVisible()) {
await overflowButton.click();
// Wait for dropdown to appear and click the menu item
const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName });
await dropdownItem.waitFor({ state: 'visible' });
await page.waitForTimeout(50);
await dropdownItem.click({ force: true });
await expect(visibleTab).toContainClass('active');
return;
}
throw new Error(`Tab "${tabName}" not found in visible tabs or overflow dropdown`);
await expect
.poll(
async () => trySelectPaneTabOnce(page, paneSelector, tabName),
{
message: `Tab "${tabName}" not found in visible tabs or overflow dropdown`,
timeout: 8000,
intervals: [100, 150, 200, 250]
}
)
.toBe(true);
});
};