Files
bruno/tests/websockets/multi-message-bru/multi-message.spec.ts
Pooja b9d8bdf2ec feat(ws): multiple messages support in websockets (#8115)
* feat: ws multi message

* fix

* fix

* fix

* improve: UX

* improve: new message ui

* fix

* fix

* fix

* fix

* fix

* fix: rename message title

* chore: cleanup

* change: add message color

* fix(websocket): correct cursor and truncate long message names

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2026-06-08 16:03:43 +05:30

257 lines
10 KiB
TypeScript

import { expect, test } from '../../../playwright';
import { buildWebsocketCommonLocators } from '../../utils/page/locators';
import { openRequest, saveRequest, closeAllCollections } from '../../utils/page/actions';
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
const COLLECTION_NAME = 'ws-multi-message';
const MULTI_MSG_REQ = 'ws-multi-msg';
const SINGLE_MSG_REQ = 'ws-single-msg';
const MULTI_MSG_BRU_PATH = join(__dirname, 'fixtures/collection/ws-multi-msg.bru');
const SINGLE_MSG_BRU_PATH = join(__dirname, 'fixtures/collection/ws-single-msg.bru');
const MAX_CONNECTION_TIME = 3000;
test.describe('websocket multi-message (bru format)', () => {
let originalMultiMsgData = '';
let originalSingleMsgData = '';
test.beforeAll(async () => {
originalMultiMsgData = await readFile(MULTI_MSG_BRU_PATH, 'utf8');
originalSingleMsgData = await readFile(SINGLE_MSG_BRU_PATH, 'utf8');
});
test.afterEach(async () => {
await writeFile(MULTI_MSG_BRU_PATH, originalMultiMsgData, 'utf8');
await writeFile(SINGLE_MSG_BRU_PATH, originalSingleMsgData, 'utf8');
});
test.afterAll(async ({ pageWithUserData: page }) => {
await closeAllCollections(page);
});
test('add a new message and save', async ({ pageWithUserData: page }) => {
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
await page.getByTestId('ws-add-message').click();
const nameInput = page.getByTestId(/^ws-message-name-input-/);
await expect(nameInput).toBeVisible();
await nameInput.selectText();
await page.keyboard.type('ping message');
await nameInput.press('Enter');
await expect(page.getByTestId(/^ws-message-label-/).filter({ hasText: 'ping message' })).toBeVisible();
await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(3);
await saveRequest(page);
const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8');
expect(bruContent).toContain('name: ping message');
});
test('edit message content and verify persistence', async ({ pageWithUserData: page }) => {
const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';
await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ);
// Expand the first message if not already expanded
const editorBody = page.getByTestId('ws-message-body-0');
if (!(await editorBody.isVisible())) {
await page.getByTestId('ws-message-header-0').click();
}
const editor = editorBody.locator('.CodeMirror');
await editor.click();
const textarea = editor.locator('textarea');
await textarea.focus();
await page.keyboard.press(selectAllShortcut);
await page.keyboard.insertText('{"updated": "content"}');
await saveRequest(page);
const bruContent = await readFile(SINGLE_MSG_BRU_PATH, 'utf8');
expect(bruContent).toContain('{"updated": "content"}');
});
test('messages with different types persist correctly', async ({ pageWithUserData: page }) => {
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
const firstHeader = page.getByTestId('ws-message-header-0');
await expect(firstHeader.locator('.selected-body-mode')).toContainText('JSON');
const secondHeader = page.getByTestId('ws-message-header-1');
await expect(secondHeader.locator('.selected-body-mode')).toContainText('TEXT');
// Change message 1 type from json to xml
await firstHeader.locator('.body-mode-selector').click();
await page.locator('.dropdown-item').filter({ hasText: 'XML' }).click();
await expect(firstHeader.locator('.selected-body-mode')).toContainText('XML');
await saveRequest(page);
const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8');
expect(bruContent).toContain('type: xml');
expect(bruContent).toContain('type: text');
// Re-open to verify persistence
await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ);
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
await expect(page.getByTestId('ws-message-header-0').locator('.selected-body-mode')).toContainText('XML');
await expect(page.getByTestId('ws-message-header-1').locator('.selected-body-mode')).toContainText('TEXT');
});
test('send selected message to active connection', async ({ pageWithUserData: page }) => {
const locators = buildWebsocketCommonLocators(page);
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
await locators.connectionControls.connect().click();
await expect(locators.connectionControls.disconnect()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
const messageItems = locators.messages().locator('.text-ellipsis');
const beforeCount = await messageItems.count();
// Click the main send button — sends the currently selected message
await page.getByTestId('run-button').click();
// Expect at least one new message (outgoing + echo response from server)
await expect.poll(() => messageItems.count(), { timeout: MAX_CONNECTION_TIME }).toBeGreaterThan(beforeCount);
await locators.connectionControls.disconnect().click();
await expect(locators.connectionControls.connect()).toBeVisible();
});
test('first message is implicitly selected when no message is marked selected', async ({ pageWithUserData: page }) => {
const locators = buildWebsocketCommonLocators(page);
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
// ws-multi-msg.bru has two messages with no `selected: true` flag. The
// main send button should therefore dispatch the first message.
await locators.connectionControls.connect().click();
await expect(locators.connectionControls.disconnect()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await page.getByTestId('run-button').click();
// the first message's content ("subscribe"), and none should carry the
// second message's content ("hello world").
await expect(locators.messages().filter({ hasText: 'subscribe' }).first()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await expect(locators.messages().filter({ hasText: 'hello world' })).toHaveCount(0);
await locators.connectionControls.disconnect().click();
});
test('selecting a different message routes run-button to that message', async ({ pageWithUserData: page }) => {
const locators = buildWebsocketCommonLocators(page);
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
// Select the second message by clicking its header
await page.getByTestId('ws-message-header-1').click();
await locators.connectionControls.connect().click();
await expect(locators.connectionControls.disconnect()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await page.getByTestId('run-button').click();
await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await expect(locators.messages().filter({ hasText: 'subscribe' })).toHaveCount(0);
await locators.connectionControls.disconnect().click();
});
test('per-message send button sends that specific message', async ({ pageWithUserData: page }) => {
const locators = buildWebsocketCommonLocators(page);
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
// Hover the header to reveal hover-actions, then click the second
await page.getByTestId('ws-message-header-1').hover();
await page.getByTestId('ws-send-msg-1').click();
await expect(locators.connectionControls.disconnect()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({
timeout: MAX_CONNECTION_TIME
});
await locators.connectionControls.disconnect().click();
});
test('prettify json message content', async ({ pageWithUserData: page }) => {
const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';
await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ);
// Expand the first message if not already expanded
const editorBody = page.getByTestId('ws-message-body-0');
if (!(await editorBody.isVisible())) {
await page.getByTestId('ws-message-header-0').click();
}
const editor = editorBody.locator('.CodeMirror');
await editor.click();
const textarea = editor.locator('textarea');
await textarea.focus();
await page.keyboard.press(selectAllShortcut);
await page.keyboard.insertText('{"name":"bruno","version":"1.0"}');
await page.getByTestId('ws-prettify-all').click();
// Verify prettification split single line into multiple lines
const lineNumbers = await editor.locator('.CodeMirror-linenumber').count();
expect(lineNumbers).toBeGreaterThan(1);
});
test('delete a message', async ({ pageWithUserData: page }) => {
await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ);
await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(2);
// Hover over the message header to reveal the delete button
await page.getByTestId('ws-message-header-1').hover();
await page.getByTestId('ws-delete-msg-1').click();
await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(1);
await saveRequest(page);
const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8');
const bodyWsCount = (bruContent.match(/body:ws/g) || []).length;
expect(bodyWsCount).toBe(1);
});
test('rename a message via double-click', async ({ pageWithUserData: page }) => {
await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ);
const messageLabel = page.getByTestId('ws-message-label-0');
await messageLabel.dblclick();
const nameInput = page.getByTestId('ws-message-name-input-0');
await expect(nameInput).toBeVisible();
await nameInput.selectText();
await page.keyboard.type('subscribe request');
await nameInput.press('Enter');
await expect(page.getByTestId('ws-message-label-0').filter({ hasText: 'subscribe request' })).toBeVisible();
await saveRequest(page);
const bruContent = await readFile(SINGLE_MSG_BRU_PATH, 'utf8');
expect(bruContent).toContain('name: subscribe request');
});
});