Merge pull request #5800 from sanish-bruno/add/grpc-make-request-tests

add: tests for grpc requests
This commit is contained in:
Bijin A B
2025-11-13 18:36:11 +05:30
committed by GitHub
21 changed files with 434 additions and 24 deletions

View File

@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex }) => {
const Accordion = ({ children, defaultIndex, dataTestId }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex }) => {
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div>{children}</div>
<div data-testid={dataTestId}>{children}</div>
</AccordionContext.Provider>
);
};

View File

@@ -186,6 +186,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
onClick={onSend}
disabled={!isConnectionActive}
className={`p-1 rounded ${isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'} transition-colors`}
data-testid={`grpc-send-message-${index}`}
>
<IconSend
size={16}
@@ -299,6 +300,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
<div
ref={messagesContainerRef}
id="grpc-messages-container"
data-testid="grpc-messages-container"
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'}`}
>
{body.grpc
@@ -325,6 +327,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
<button
onClick={addNewMessage}
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
data-testid="grpc-add-message-button"
>
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add Message</span>

View File

@@ -73,7 +73,7 @@ const MethodDropdown = ({
const MethodsDropdownIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none">
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none" data-testid="grpc-method-dropdown-trigger">
{selectedGrpcMethod && <div className="mr-2">{getIconForMethodType(selectedGrpcMethod.type)}</div>}
<span className="text-xs">
{selectedGrpcMethod ? (

View File

@@ -389,19 +389,21 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
{isConnectionActive && isStreamingMethod && (
<div className="connection-controls relative flex items-center h-full gap-3">
<div className="infotip" onClick={handleCancelConnection}>
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
<span className="infotip-text text-xs">Cancel</span>
</div>
{isClientStreamingMethod && <div onClick={handleEndConnection}>
<IconCheck
color={theme.colors.text.green}
strokeWidth={2}
size={22}
className="cursor-pointer"
/>
</div>}
{isClientStreamingMethod && (
<div onClick={handleEndConnection} data-testid="grpc-end-connection-button">
<IconCheck
color={theme.colors.text.green}
strokeWidth={2}
size={22}
className="cursor-pointer"
/>
</div>
)}
</div>
)}

View File

@@ -61,13 +61,13 @@ const GrpcQueryResult = ({ item, collection }) => {
}
return (
<StyledWrapper className="w-full h-full relative flex flex-col mt-2">
<StyledWrapper className="w-full h-full relative flex flex-col mt-2" data-testid="grpc-response-content">
{hasError && showErrorMessage && <GrpcError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}
{hasResponses && (
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`}>
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`} data-testid="grpc-responses-container">
{responsesList.length === 1 ? (
// Single message - render directly without accordion
<div className="h-full">
<div className="h-full" data-testid="grpc-single-response">
<CodeEditor
collection={collection}
font={get(preferences, 'font.codeFont', 'default')}
@@ -80,13 +80,13 @@ const GrpcQueryResult = ({ item, collection }) => {
</div>
) : (
// Multiple messages - use accordion
<Accordion defaultIndex={0}>
<Accordion defaultIndex={0} dataTestId="grpc-responses-accordion">
{reversedResponsesList.map((response, index) => {
// Calculate the original response number (for display purposes)
const originalIndex = responsesList.length - index - 1;
return (
<Accordion.Item key={originalIndex} index={index}>
<Accordion.Item key={originalIndex} index={index} data-testid={`grpc-response-item-${originalIndex}`}>
<Accordion.Header index={index} style={{ padding: '8px 12px', minHeight: '40px' }}>
<div className="flex justify-between w-full">
<div className="font-medium">

View File

@@ -108,7 +108,7 @@ const GrpcResponsePane = ({ item, collection }) => {
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist" data-testid="grpc-response-tabs">
{tabConfig.map((tab) => (
<Tab
key={tab.name}

View File

@@ -11,10 +11,11 @@ const Tab = ({ name, label, isActive, onClick, count = 0, className = '', ...pro
className={tabClassName}
role="tab"
onClick={() => onClick(name)}
data-testid={`tab-${name}`}
{...props}
>
{label}
{count > 0 && <sup className="ml-1 font-medium">{count}</sup>}
{count > 0 && <sup className="ml-1 font-medium" data-testid={`tab-${name}-count`}>{count}</sup>}
</div>
);
};

View File

@@ -0,0 +1,31 @@
meta {
name: BidiHello
type: grpc
seq: 4
}
grpc {
url: {{host}}
method: /hello.HelloService/BidiHello
body: grpc
auth: inherit
methodType: bidi-streaming
}
body:grpc {
name: message 1
content: '''
{
"greeting": "cuius"
}
'''
}
body:grpc {
name: message 2
content: '''
{
"greeting": "adfectus"
}
'''
}

View File

@@ -0,0 +1,31 @@
meta {
name: LotOfGreetings
type: grpc
seq: 3
}
grpc {
url: {{host}}
method: /hello.HelloService/LotsOfGreetings
body: grpc
auth: inherit
methodType: client-streaming
}
body:grpc {
name: message 1
content: '''
{
"greeting": "sortitus"
}
'''
}
body:grpc {
name: message 2
content: '''
{
"greeting": "porro"
}
'''
}

View File

@@ -0,0 +1,22 @@
meta {
name: LotOfReplies
type: grpc
seq: 2
}
grpc {
url: {{host}}
method: /hello.HelloService/LotsOfReplies
body: grpc
auth: inherit
methodType: server-streaming
}
body:grpc {
name: message 1
content: '''
{
"greeting": "suadeo"
}
'''
}

View File

@@ -0,0 +1,22 @@
meta {
name: SayHello
type: grpc
seq: 1
}
grpc {
url: {{host}}
method: /hello.HelloService/SayHello
body: grpc
auth: inherit
methodType: unary
}
body:grpc {
name: message 1
content: '''
{
"greeting": "amoveo"
}
'''
}

View File

@@ -0,0 +1,8 @@
meta {
name: HelloService
seq: 2
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,33 @@
{
"version": "1",
"name": "Grpcbin",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"size": 0.001827239990234375,
"filesCount": 10,
"protobuf": {
"protoFiles": [
{
"path": "../protos/services/product.proto",
"type": "file"
},
{
"path": "../protos/services/order.proto",
"type": "file"
}
],
"importPaths": [
{
"path": "../protos/types",
"enabled": false
},
{
"path": ".",
"enabled": true
}
]
}
}

View File

@@ -0,0 +1,3 @@
vars {
host: grpc://grpcb.in:9000
}

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/grpc/make-request/fixtures/collection",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -0,0 +1,11 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/grpc/make-request/fixtures/collection"
],
"preferences": {
"beta": {
"nodevm": false
}
}
}

View File

@@ -0,0 +1,202 @@
import { test, expect } from '../../../playwright';
import { buildGrpcCommonLocators } from '../../utils/page/locators';
test.describe('make grpc requests', () => {
const setupGrpcTest = async (page) => {
const locators = buildGrpcCommonLocators(page);
await test.step('navigate to gRPC collection', async () => {
await locators.sidebar.collection('Grpcbin').click();
await locators.sidebar.folder('HelloService').click();
});
await test.step('select environment', async () => {
await locators.environment.selector().click();
await locators.environment.collectionTab().click();
await locators.environment.envOption('Env').click();
});
};
test('make unary request', async ({ pageWithUserData: page }) => {
await setupGrpcTest(page);
const locators = buildGrpcCommonLocators(page);
await test.step('select unary method', async () => {
await locators.sidebar.request('SayHello').click();
await expect(locators.method.dropdownTrigger()).toContainText('HelloService/SayHello');
});
await test.step('verify gRPC unary request is opened successfully', async () => {
await expect(locators.method.indicator()).toContainText('gRPC');
await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();
await expect(locators.request.sendButton()).toBeVisible();
await expect(locators.request.messagesContainer()).toBeVisible();
});
await test.step('send request', async () => {
await locators.request.sendButton().click();
await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusCode()).toHaveText(/0/);
await expect(locators.response.statusText()).toHaveText(/OK/);
});
await test.step('verify response message count', async () => {
await expect(locators.response.tabCount()).toHaveText('1');
});
await test.step('verify response items are rendered', async () => {
await expect(locators.response.content()).toBeVisible();
await expect(locators.response.container()).toBeVisible();
await expect(locators.response.singleResponse()).toBeVisible();
});
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
});
});
test('make server streaming request', async ({ pageWithUserData: page }) => {
await setupGrpcTest(page);
const locators = buildGrpcCommonLocators(page);
await test.step('select server streaming method', async () => {
await locators.sidebar.request('LotOfReplies').click();
await expect(locators.method.dropdownTrigger()).toContainText('HelloService/LotsOfReplies');
});
await test.step('verify gRPC server streaming request is opened successfully', async () => {
await expect(locators.method.indicator()).toContainText('gRPC');
await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();
await expect(locators.request.messagesContainer()).toBeVisible();
await expect(locators.request.sendButton()).toBeVisible();
});
await test.step('send request', async () => {
await locators.request.sendButton().click();
await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusCode()).toHaveText(/0/);
await expect(locators.response.statusText()).toHaveText(/OK/);
});
await test.step('verify response message count', async () => {
await expect(locators.response.tabCount()).toHaveText('10');
});
await test.step('verify response items are rendered', async () => {
await expect(locators.response.content()).toBeVisible();
await expect(locators.response.container()).toBeVisible();
await expect(locators.response.accordion()).toBeVisible();
await expect(locators.response.responseItems()).toHaveCount(10);
});
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
});
});
test('make client streaming request', async ({ pageWithUserData: page }) => {
await setupGrpcTest(page);
const locators = buildGrpcCommonLocators(page);
await test.step('select client streaming method', async () => {
await locators.sidebar.request('LotOfGreetings').click();
await expect(locators.method.dropdownTrigger()).toContainText('HelloService/LotsOfGreetings');
});
await test.step('verify gRPC client streaming request is opened successfully', async () => {
await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();
await expect(locators.request.messagesContainer()).toBeVisible();
await expect(locators.request.addMessageButton()).toBeVisible();
await expect(locators.request.sendMessage(0)).toBeVisible();
await expect(locators.request.sendButton()).toBeVisible();
});
await test.step('start client streaming connection', async () => {
await locators.request.sendButton().click();
await expect(locators.request.endConnectionButton()).toBeVisible();
});
await test.step('send individual message', async () => {
await locators.request.sendMessage(0).click();
});
await test.step('end client streaming connection', async () => {
await locators.request.endConnectionButton().click();
await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusCode()).toHaveText(/0/);
await expect(locators.response.statusText()).toHaveText(/OK/);
});
await test.step('verify response message count', async () => {
await expect(locators.response.tabCount()).toHaveText('1');
});
await test.step('verify response items are rendered', async () => {
await expect(locators.response.content()).toBeVisible();
await expect(locators.response.container()).toBeVisible();
await expect(locators.response.singleResponse()).toBeVisible();
});
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
});
});
test('make bidi streaming request', async ({ pageWithUserData: page }) => {
await setupGrpcTest(page);
const locators = buildGrpcCommonLocators(page);
await test.step('select bidirectional streaming method', async () => {
await locators.sidebar.request('BidiHello').click();
await expect(locators.method.dropdownTrigger()).toContainText('HelloService/BidiHello');
});
await test.step('verify gRPC bidi streaming request is opened successfully', async () => {
await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();
await expect(locators.request.messagesContainer()).toBeVisible();
await expect(locators.request.addMessageButton()).toBeVisible();
await expect(locators.request.sendMessage(0)).toBeVisible();
await expect(locators.request.sendButton()).toBeVisible();
});
await test.step('start bidirectional streaming connection', async () => {
await locators.request.sendButton().click();
await expect(locators.request.endConnectionButton()).toBeVisible();
});
await test.step('send individual message', async () => {
await locators.request.sendMessage(0).click();
await locators.request.sendMessage(1).click();
});
await test.step('end bidirectional streaming connection', async () => {
await locators.request.endConnectionButton().click();
await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });
await expect(locators.response.statusCode()).toHaveText(/0/);
await expect(locators.response.statusText()).toHaveText(/OK/);
});
await test.step('verify response message count', async () => {
await expect(locators.response.tabCount()).toHaveText('2');
});
await test.step('verify response items are rendered', async () => {
await expect(locators.response.content()).toBeVisible();
await expect(locators.response.container()).toBeVisible();
await expect(locators.response.accordion()).toBeVisible();
await expect(locators.response.responseItems()).toHaveCount(2);
});
/* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */
await test.step('save request via shortcut', async () => {
await page.keyboard.press('Meta+s');
});
});
});

View File

@@ -1,5 +1,5 @@
{
"maximized": true,
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/grpc/metadata/fixtures/collection"
],

View File

@@ -2,8 +2,5 @@
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/grpc/method-search/fixtures/grpc-collection"
],
"beta": {
"grpc": true
}
]
}

View File

@@ -41,6 +41,12 @@ export const buildCommonLocators = (page: Page) => ({
modal: {
title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }),
button: (name: string) => page.getByRole('button', { name: name, exact: true })
},
environment: {
selector: () => page.getByTestId('environment-selector-trigger'),
collectionTab: () => page.getByTestId('env-tab-collection'),
globalTab: () => page.getByTestId('env-tab-global'),
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true })
}
});
@@ -65,3 +71,31 @@ export const buildWebsocketCommonLocators = (page: Page) => ({
clearResponse: () => page.getByRole('button', { name: 'Clear Response' })
}
});
export const buildGrpcCommonLocators = (page: Page) => ({
...buildCommonLocators(page),
method: {
dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),
indicator: () => page.getByTestId('grpc-method-indicator')
},
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'),
sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),
endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),
cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')
},
response: {
statusCode: () => page.getByTestId('grpc-response-status-code'),
statusText: () => page.getByTestId('grpc-response-status-text'),
content: () => page.getByTestId('grpc-response-content'),
container: () => page.getByTestId('grpc-responses-container'),
singleResponse: () => page.getByTestId('grpc-single-response'),
accordion: () => page.getByTestId('grpc-responses-accordion'),
responseItem: (index: number) => page.getByTestId(`grpc-response-item-${index}`),
responseItems: () => page.locator('[data-testid^="grpc-response-item-"]'),
tabCount: () => page.getByTestId('tab-response-count')
}
});