-
Notifications
You must be signed in to change notification settings - Fork 2.4k
fix: gRPC request loses all messages except the first on save for yaml collection #7910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b222870
8cdbadf
282e66b
b2de654
bb32631
c299dae
07bb456
9666862
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| REQUEST_TYPE, | ||
| 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: REQUEST_TYPE.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); | ||
| }); | ||
|
|
||
| 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); | ||
|
Check failure on line 55 in tests/grpc/multi-message-yml/multi-message.spec.ts
|
||
|
|
||
| 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); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,8 +15,8 @@ | |
|
|
||
| for (let i = 0; i < numberOfCollections; i++) { | ||
| const firstCollection = page.locator('[data-testid="collections"] .collection-name').first(); | ||
| await firstCollection.hover(); | ||
|
Check failure on line 18 in tests/utils/page/actions.ts
|
||
| await firstCollection.locator('.collection-actions .icon').click(); | ||
|
Check failure on line 19 in tests/utils/page/actions.ts
|
||
| await page.locator('.dropdown-item').getByText('Remove').click(); | ||
|
|
||
| // Wait for modal to appear - could be either regular remove or drafts confirmation | ||
|
|
@@ -28,7 +28,7 @@ | |
|
|
||
| if (hasDiscardButton) { | ||
| // Drafts modal - click "Discard All and Remove" | ||
| await page.getByRole('button', { name: 'Discard All and Remove' }).click(); | ||
|
Check failure on line 31 in tests/utils/page/actions.ts
|
||
| } else { | ||
| // Regular modal - click the submit button | ||
| await page.locator('.bruno-modal-footer .submit').click(); | ||
|
|
@@ -55,17 +55,30 @@ | |
| }); | ||
| }; | ||
|
|
||
| type CollectionFormat = 'yml' | 'bru'; | ||
|
|
||
| type CreateCollectionOptions = { | ||
| format?: CollectionFormat; | ||
| }; | ||
|
|
||
| /** | ||
| * Create a collection | ||
| * @param page - The page object | ||
| * @param collectionName - The name of the collection to create | ||
| * @param collectionLocation - The location of the collection to create (eg) | ||
| * @param options - The options for creating the collection | ||
| * @param options - Optional settings (format: 'yml' (default) or 'bru') | ||
| * | ||
| * @returns void | ||
| */ | ||
| const createCollection = async (page, collectionName: string, collectionLocation: string) => { | ||
| await test.step(`Create collection "${collectionName}"`, async () => { | ||
| const createCollection = async ( | ||
| page, | ||
| collectionName: string, | ||
| collectionLocation: string, | ||
| options: CreateCollectionOptions = {} | ||
| ) => { | ||
| const { format = 'yml' } = options; | ||
|
|
||
| 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(); | ||
|
|
||
|
|
@@ -93,9 +106,18 @@ | |
| await nameInput.fill(collectionName); | ||
| // Verify the name is correct before creating | ||
| await expect(nameInput).toHaveValue(collectionName, { timeout: 2000 }); | ||
|
|
||
| // The File Format dropdown is hidden by default. Open the Advanced Options menu | ||
| // and toggle "Show File Format" before selecting a non-default format. | ||
| if (format === 'bru') { | ||
| await createCollectionModal.locator('.advanced-options .btn-advanced').click(); | ||
| await page.getByTestId('show-file-format-toggle').click(); | ||
| await createCollectionModal.locator('select#format').selectOption(format); | ||
| } | ||
|
|
||
| await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click(); | ||
|
|
||
| await createCollectionModal.waitFor({ state: 'detached', timeout: 15000 }); | ||
|
Check failure on line 120 in tests/utils/page/actions.ts
|
||
| // Wait for the collection name to appear in the sidebar before proceeding | ||
| await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).waitFor({ state: 'visible', timeout: 5000 }); | ||
| await openCollection(page, collectionName); | ||
|
|
@@ -104,14 +126,31 @@ | |
|
|
||
| const STANDARD_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT']; | ||
|
|
||
| export const REQUEST_TYPE = Object.freeze({ | ||
| HTTP: 'HTTP', | ||
| GRAPHQL: 'GraphQL', | ||
| GRPC: 'gRPC', | ||
| WEBSOCKET: 'WebSocket' | ||
| } as const); | ||
|
|
||
| export type RequestType = typeof REQUEST_TYPE[keyof typeof REQUEST_TYPE]; | ||
|
|
||
| type CreateRequestOptions = { | ||
| url?: string; | ||
| method?: string; | ||
| inFolder?: boolean; | ||
| requestType?: RequestType; | ||
| }; | ||
|
|
||
| const REQUEST_TYPE_TESTID = Object.freeze({ | ||
| [REQUEST_TYPE.HTTP]: 'http-request', | ||
| [REQUEST_TYPE.GRAPHQL]: 'graphql-request', | ||
| [REQUEST_TYPE.GRPC]: 'grpc-request', | ||
| [REQUEST_TYPE.WEBSOCKET]: 'ws-request' | ||
| } as const); | ||
|
|
||
| type CreateUntitledRequestOptions = { | ||
| requestType?: 'HTTP' | 'GraphQL' | 'WebSocket' | 'gRPC'; | ||
| requestType?: RequestType; | ||
| requestName?: string; | ||
| url?: string; | ||
| tag?: string; | ||
|
|
@@ -174,7 +213,7 @@ | |
| }; | ||
|
|
||
| type CreateTransientRequestOptions = { | ||
| requestType?: 'HTTP' | 'GraphQL' | 'gRPC' | 'WebSocket'; | ||
| requestType?: RequestType; | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -258,10 +297,10 @@ | |
| parentName: string, | ||
| options: CreateRequestOptions = {} | ||
| ) => { | ||
| const { url, method, inFolder = false } = options; | ||
| const { url, method, inFolder = false, requestType = 'HTTP' } = options; | ||
| const parentType = inFolder ? 'folder' : 'collection'; | ||
|
|
||
| await test.step(`Create request "${requestName}" in ${parentType} "${parentName}"`, async () => { | ||
| await test.step(`Create ${requestType} request "${requestName}" in ${parentType} "${parentName}"`, async () => { | ||
| const locators = buildCommonLocators(page); | ||
|
|
||
| if (inFolder) { | ||
|
|
@@ -275,9 +314,14 @@ | |
| } | ||
|
|
||
| await locators.dropdown.item('New Request').click(); | ||
|
|
||
| if (requestType !== REQUEST_TYPE.HTTP) { | ||
| await page.getByTestId(REQUEST_TYPE_TESTID[requestType]).click(); | ||
| } | ||
|
|
||
| await page.getByPlaceholder('Request Name').fill(requestName); | ||
|
|
||
| if (method) { | ||
| if (method && requestType === 'HTTP') { | ||
| await page.locator('.bruno-modal .method-selector').click(); | ||
| const isStandardMethod = STANDARD_HTTP_METHODS.includes(method.toUpperCase()); | ||
| if (isStandardMethod) { | ||
|
|
@@ -822,7 +866,7 @@ | |
|
|
||
| // Check if tab is directly visible | ||
| if (await visibleTab.isVisible()) { | ||
| await visibleTab.click(); | ||
|
Check failure on line 869 in tests/utils/page/actions.ts
|
||
| await expect(visibleTab).toContainClass('active'); | ||
| return; | ||
| } | ||
|
|
@@ -1029,6 +1073,7 @@ | |
| */ | ||
| const saveRequest = async (page: Page) => { | ||
| await test.step('Save request', async () => { | ||
| await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sharan-bruno do we really need this change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes we need this line, without that the playwrite test cases are failing |
||
| const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s'; | ||
| await page.keyboard.press(saveShortcut); | ||
| await expect(page.getByText('Request saved successfully').last()).toBeVisible({ timeout: 3000 }); | ||
|
|
@@ -1198,6 +1243,42 @@ | |
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * 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 () => { | ||
| await page.getByTestId('grpc-add-message-button').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 () => { | ||
| await page.getByTestId(`grpc-regenerate-message-${index}`).click(); | ||
| }); | ||
| }; | ||
|
sharan-bruno marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * 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 () => { | ||
| await page.getByTestId('grpc-method-dropdown-trigger').click(); | ||
| const dropdown = page.getByTestId('grpc-methods-dropdown'); | ||
| await dropdown.waitFor({ state: 'visible', timeout: 5000 }); | ||
| await dropdown.getByTestId('grpc-method-item').filter({ hasText: methodName }).first().click(); | ||
| await expect(page.getByTestId('selected-grpc-method-name')).toContainText(methodName); | ||
| }); | ||
| }; | ||
|
|
||
| export { | ||
| closeAllCollections, | ||
| openCollection, | ||
|
|
@@ -1243,7 +1324,10 @@ | |
| addPostResponseScript, | ||
| addTestScript, | ||
| sendAndWaitForErrorCard, | ||
| sendAndWaitForResponse | ||
| sendAndWaitForResponse, | ||
| selectGrpcMethod, | ||
| addGrpcMessage, | ||
| generateGrpcSampleMessage | ||
| }; | ||
|
|
||
| export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput }; | ||
Uh oh!
There was an error while loading. Please reload this page.