fix(grpc): gRPC request loses all messages except the first on save for yaml collection (#8203)

* fix(grpc): gRPC request loses all messages except the first on save for yaml collection

* fix(grpc): enhance gRPC locators and improve message handling in tests
This commit is contained in:
sharan-bruno
2026-06-09 14:55:04 +05:30
committed by GitHub
parent 6f47218a81
commit 240826ebc1
7 changed files with 139 additions and 19 deletions

View File

@@ -39,7 +39,7 @@ const MessageToolbar = ({
</ToolHint>
<ToolHint text="Generate sample" toolhintId={`regenerate-msg-${index}`}>
<button onClick={onRegenerateMessage} className="toolbar-btn">
<button onClick={onRegenerateMessage} className="toolbar-btn" data-testid={`grpc-regenerate-message-${index}`}>
<IconRefresh size={16} strokeWidth={1.5} />
</button>
</ToolHint>

View File

@@ -281,6 +281,7 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initi
<div
className="dropdown-item"
key="show-file-format"
data-testid="show-file-format-toggle"
onClick={(e) => {
dropdownTippyRef.current.hide();
setShowFileFormat(!showFileFormat);

View File

@@ -1,6 +1,6 @@
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
import type { GrpcRequest, GrpcMetadata } from '@opencollection/types/requests/grpc';
import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant } from '@opencollection/types/requests/grpc';
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
import { toBrunoAuth } from '../common/auth';
import { toBrunoVariables } from '../common/variables';
@@ -57,10 +57,16 @@ const parseGrpcRequest = (ocRequest: GrpcRequest): BrunoItem => {
};
// message
if (isNonEmptyString(grpc?.message)) {
const rawMessage = grpc?.message;
if (Array.isArray(rawMessage)) {
brunoRequest.body.grpc = (rawMessage as GrpcMessageVariant[]).map(({ title, message }, index) => ({
name: title || `message ${index + 1}`,
content: ensureString(message)
}));
} else if (isNonEmptyString(rawMessage)) {
brunoRequest.body.grpc = [{
name: '',
content: grpc?.message as string
content: rawMessage as string
}];
}

View File

@@ -1,7 +1,7 @@
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
import type { GrpcRequest, GrpcMetadata, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
import type { Auth } from '@opencollection/types/common/auth';
import type { Scripts } from '@opencollection/types/common/scripts';
import type { Variable } from '@opencollection/types/common/variables';
@@ -73,16 +73,11 @@ const stringifyGrpcRequest = (item: BrunoItem): string => {
// message
if (brunoRequest.body?.mode === 'grpc' && brunoRequest.body.grpc?.length) {
const messages = brunoRequest.body.grpc;
// todo: bruno app supports only one message for now
// update this when bruno app supports multiple messages
if (messages.length) {
const message: GrpcMessage = messages[0].content || '';
if (message.trim().length) {
grpc.message = message;
}
}
const messages: GrpcMessageVariant[] = brunoRequest.body.grpc.map(({ name, content }, index) => ({
title: name || `message ${index + 1}`,
message: content || ''
}));
grpc.message = messages;
}
// auth

View File

@@ -0,0 +1,70 @@
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { bruToJsonV2 } from '@usebruno/lang';
import { expect, test } from '../../../playwright';
import {
addGrpcMessage,
createCollection,
createRequest,
generateGrpcSampleMessage,
saveRequest,
selectGrpcMethod
} from '../../utils/page/actions';
const REQUEST_NAME = 'grpc-multi-msg';
const GRPC_URL = 'grpcb.in:9000';
const GRPC_METHOD = 'BidiHello';
type GrpcRequestYml = {
grpc?: {
message?: { title: string; message: string }[];
};
};
const FORMATS = [
{ format: 'yml', collectionName: 'grpc-yml-multi-msg', tmpDirPrefix: 'grpc-yml-collection' },
{ format: 'bru', collectionName: 'grpc-bru-multi-msg', tmpDirPrefix: 'grpc-bru-collection' }
] as const;
for (const { format, collectionName, tmpDirPrefix } of FORMATS) {
test.describe.serial(`grpc multi-message (${format} format)`, () => {
let collectionPath: string;
test('creates a gRPC request with multiple messages and saves it', async ({ page, createTmpDir }) => {
collectionPath = await createTmpDir(tmpDirPrefix);
await createCollection(page, collectionName, collectionPath, format);
await createRequest(page, REQUEST_NAME, collectionName, { url: GRPC_URL, requestType: 'grpc' });
await selectGrpcMethod(page, GRPC_METHOD);
await addGrpcMessage(page);
await addGrpcMessage(page);
await generateGrpcSampleMessage(page, 0);
await generateGrpcSampleMessage(page, 1);
await generateGrpcSampleMessage(page, 2);
await saveRequest(page);
const messageContainers = page.getByTestId('grpc-messages-container').locator('.message-container');
await expect(messageContainers).toHaveCount(3, { timeout: 5000 });
});
test(`verifies all messages are saved in the request .${format} file`, async () => {
const requestFilePath = path.join(collectionPath, collectionName, `${REQUEST_NAME}.${format}`);
expect(fs.existsSync(requestFilePath)).toBe(true);
const fileContent = fs.readFileSync(requestFilePath, 'utf8');
if (format === 'yml') {
const parsed = yaml.load(fileContent) as GrpcRequestYml;
const messages = parsed.grpc?.message ?? [];
expect(messages.length).toBe(3);
} else if (format === 'bru') {
const parsed = bruToJsonV2(fileContent) as { body?: { grpc?: { name: string; content: string }[] } };
const messages = parsed.body?.grpc ?? [];
expect(messages.length).toBe(3);
}
});
});
}

View File

@@ -1,10 +1,12 @@
import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright';
import process from 'node:process';
import * as path from 'path';
import { buildCommonLocators, buildScriptErrorLocators } from './locators';
import { buildCommonLocators, buildScriptErrorLocators, buildGrpcCommonLocators } from './locators';
type SandboxMode = 'safe' | 'developer';
type CollectionFormat = 'bru' | 'yml';
type WaitForAppReadyOptions = {
timeout?: number;
};
@@ -101,9 +103,9 @@ const createCollection = async (
page,
collectionName: string,
collectionLocation: string,
format?: 'bru' | 'yml'
format: CollectionFormat = 'yml'
) => {
await test.step(`Create collection "${collectionName}"`, async () => {
await test.step(`Create ${format} collection "${collectionName}"`, async () => {
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
@@ -1335,6 +1337,44 @@ const saveRequest = async (page: Page) => {
});
};
/**
* Click the gRPC "Add Message" button to append a new message to the request
* @param page - The page object
*/
const addGrpcMessage = async (page: Page) => {
await test.step('Add gRPC message', async () => {
const locators = buildGrpcCommonLocators(page);
await locators.request.addMessageButton().click();
});
};
/**
* Click the "Generate sample" button on a gRPC message to populate it with a sample payload
* @param page - The page object
* @param index - The 0-based index of the message (default: 0)
*/
const generateGrpcSampleMessage = async (page: Page, index: number = 0) => {
await test.step(`Generate sample for gRPC message #${index}`, async () => {
const locators = buildGrpcCommonLocators(page);
await locators.request.regenerateMessage(index).click();
});
};
/**
* Open the gRPC method dropdown and select a method by name
* @param page - The page object
* @param methodName - The name of the gRPC method to select (e.g. "BidiHello")
*/
const selectGrpcMethod = async (page: Page, methodName: string) => {
await test.step(`Select gRPC method "${methodName}"`, async () => {
const locators = buildGrpcCommonLocators(page);
await locators.method.dropdownTrigger().click();
await locators.method.dropdown().waitFor({ state: 'visible', timeout: 5000 });
await locators.method.item(methodName).first().click();
await expect(locators.method.selectedName()).toContainText(methodName);
});
};
/**
* Close all open request tabs using the right-click context menu
* @param page - The page object
@@ -1756,6 +1796,9 @@ export {
editAssertion,
deleteAssertion,
saveRequest,
addGrpcMessage,
generateGrpcSampleMessage,
selectGrpcMethod,
closeAllTabs,
createWorkspace,
switchWorkspace,

View File

@@ -237,13 +237,18 @@ export const buildGrpcCommonLocators = (page: Page) => ({
...buildCommonLocators(page),
method: {
dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),
indicator: () => page.getByTestId('grpc-method-indicator')
indicator: () => page.getByTestId('grpc-method-indicator'),
dropdown: () => page.getByTestId('grpc-methods-dropdown'),
item: (methodName: string) =>
page.getByTestId('grpc-methods-dropdown').getByTestId('grpc-method-item').filter({ hasText: methodName }),
selectedName: () => page.getByTestId('selected-grpc-method-name')
},
request: {
queryUrlContainer: () => page.getByTestId('grpc-query-url-container'),
sendButton: () => page.getByTestId('grpc-send-request-button'),
messagesContainer: () => page.getByTestId('grpc-messages-container'),
addMessageButton: () => page.getByTestId('grpc-add-message-button'),
regenerateMessage: (index: number) => page.getByTestId(`grpc-regenerate-message-${index}`),
sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),
endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),
cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')