mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: correct the request type tabs in the snapshot (#7994)
This commit is contained in:
@@ -93,7 +93,7 @@ const useOpenAPISync = (collection) => {
|
||||
uid: itemUid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: item ? getDefaultRequestPaneTab(item) : undefined,
|
||||
type: 'request'
|
||||
type: item?.type ?? 'request'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 } : {})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user