mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
feat: variable descriptions (#8269)
* feat: add description column to various tables and enhance UI interactions - Introduced a description column in Headers, VarsTable, and other components to allow multi-line descriptions. - Added toggle buttons to show/hide the description column in relevant tables. - Updated EditableTable component to support dynamic row addition with customizable labels. - Enhanced UI for better user experience with consistent button styles and spacing adjustments. * chore: resize fix handles * fix: restrict column check for last index * refactor: remove unwanted style and fix bottom padding of the last child in the table row * tests: improve testability and fix impacted locators * tests: fix locators for value fields * fix: improve newline handling in description prefix * fix: ts changes * chore: remove redundant code and fix description roundtrip * chore: remove redundant check * tests(bru-lang): tests for description and annotation * test: improve locators and tests * tests: descriptions * chore: re-add description setup * fix: re-add description cols * chore: fix the double click reset * fix: account for hidden sidebar in request pane width calculation * refactor: ui/ux fixes for data type toggle * chore: inline common deps into bruno-lang and bruno-schema * chore: tests * chore: fix ux for descriptions * fix: layout for environment vars table * fix: ensure correct row selection for multipart file upload in tests * feat: enhance UID assignment for request headers and variables in collections * chore: simplify * chore: update pkg * chore: abstract the e2e utils * chore: fix exports * tests: fix locators for datatype vars * fix: ux issue with secrets in environment table * tests: update locators for collection headers and vars descriptions * tests: update locators to use data attributes for datatype selectors * chore: update default css width for actions column * chore: remove tooltip for string type warnings * fix: reduce name widths * refactor: update annotation serialization to use single-quote delimiters for descriptions * refactor(tests): simplify description formatting in jsonToEnv tests * feat: add multiline description escaping and unescaping functions with tests * refactor: clean up imports and improve multiline text block handling * refactor: update locators to use new test IDs for environment variable editors * refactor: streamline header input handling and improve environment variable row access * refactor: update e2e-test job to support self-hosted runners * refactor: update E2E test runner configuration to include e2e and macOS environments --------- Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
This commit is contained in:
@@ -458,7 +458,7 @@ test.describe('Scroll Position Persistence', () => {
|
||||
test('Request Headers - scroll persists with many headers across tab switches', async ({ pageWithUserData: page }) => {
|
||||
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
|
||||
const scrollContainer = '.flex-boundary';
|
||||
const firstVisibleRowLocator = () => page.getByTestId('editable-table').locator('table > tbody > tr:nth-child(2)');
|
||||
const firstVisibleRowLocator = () => page.getByTestId('request-headers-table').locator('table > tbody > tr[data-index]').first();
|
||||
|
||||
await test.step('Setup request and navigate to Headers tab', async () => {
|
||||
await openCollection(page, 'scroll-fixtures');
|
||||
@@ -1174,7 +1174,7 @@ test.describe('Scroll Position Persistence', () => {
|
||||
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
|
||||
const locators = buildCommonLocators(page);
|
||||
const scrollContainer = '.collection-settings-content';
|
||||
const firstVisibleRowLocator = () => page.getByTestId('editable-table').locator('table > tbody > tr:nth-child(2)');
|
||||
const firstVisibleRowLocator = () => page.getByTestId('collection-headers').locator('table > tbody > tr[data-index]').first();
|
||||
|
||||
await test.step('Open collection settings and navigate to Headers tab', async () => {
|
||||
await openCollection(page, 'scroll-fixtures');
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "req-description-yml",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
info:
|
||||
name: formurlencoded-with-descriptions
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: https://example.com/form
|
||||
body:
|
||||
type: form-urlencoded
|
||||
data:
|
||||
- name: username
|
||||
value: alice
|
||||
description: Single-line form desc
|
||||
- name: password
|
||||
value: secret
|
||||
description: |-
|
||||
Multi-line form desc line one
|
||||
Multi-line form desc line two
|
||||
- name: plain-field
|
||||
value: no-desc
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@@ -0,0 +1,30 @@
|
||||
info:
|
||||
name: multipart-with-descriptions
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: https://example.com/upload
|
||||
body:
|
||||
type: multipart-form
|
||||
data:
|
||||
- name: username
|
||||
type: text
|
||||
value: alice
|
||||
description: Single-line field desc
|
||||
- name: email
|
||||
type: text
|
||||
value: alice@example.com
|
||||
description: |-
|
||||
Multi-line field desc line one
|
||||
Multi-line field desc line two
|
||||
- name: plain-field
|
||||
type: text
|
||||
value: no-desc
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@@ -0,0 +1,3 @@
|
||||
opencollection: "1.0.0"
|
||||
info:
|
||||
name: req-description-yml
|
||||
@@ -0,0 +1,66 @@
|
||||
info:
|
||||
name: request-with-descriptions
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: https://example.com/api
|
||||
headers:
|
||||
- name: X-Version
|
||||
value: "2.0"
|
||||
description: Single-line header desc
|
||||
- name: X-Multi
|
||||
value: value
|
||||
description: |-
|
||||
Header line one
|
||||
Header line two
|
||||
- name: X-Plain
|
||||
value: plain-value
|
||||
params:
|
||||
- name: q
|
||||
value: search
|
||||
type: query
|
||||
description: Single-line query desc
|
||||
- name: page
|
||||
value: "1"
|
||||
type: query
|
||||
description: |-
|
||||
Multi-line query desc line one
|
||||
Multi-line query desc line two
|
||||
- name: plain-query
|
||||
value: value
|
||||
type: query
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: apiKey
|
||||
value: secret
|
||||
description: Single-line var desc
|
||||
- name: baseUrl
|
||||
value: https://example.com
|
||||
description: |-
|
||||
Multi-line var desc line one
|
||||
Multi-line var desc line two
|
||||
- name: plainVar
|
||||
value: value
|
||||
assertions:
|
||||
- expression: res.status
|
||||
operator: eq
|
||||
value: "200"
|
||||
description: Single-line assert desc
|
||||
- expression: res.body.ok
|
||||
operator: eq
|
||||
value: "true"
|
||||
description: |-
|
||||
Multi-line assert line one
|
||||
Multi-line assert line two
|
||||
- expression: plainAssert
|
||||
operator: eq
|
||||
value: foo
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{collectionPath}}",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}"
|
||||
],
|
||||
"request": {
|
||||
"sslVerification": false,
|
||||
"customCaCertificate": {
|
||||
"enabled": false,
|
||||
"filePath": null
|
||||
}
|
||||
},
|
||||
"font": {
|
||||
"codeFont": "default"
|
||||
},
|
||||
"proxy": {
|
||||
"enabled": false,
|
||||
"protocol": "http",
|
||||
"hostname": "",
|
||||
"port": "",
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"bypassProxy": ""
|
||||
}
|
||||
}
|
||||
106
tests/request/description-yml/read-request-description.spec.ts
Normal file
106
tests/request/description-yml/read-request-description.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
|
||||
test.describe('Request Description (YAML) - Read', () => {
|
||||
test('reads descriptions from request headers in a pre-existing .yml file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const headersTable = page.getByTestId('request-headers-table');
|
||||
const rows = headersTable.locator('tbody tr');
|
||||
|
||||
// row 0: X-Version — single-line description
|
||||
const versionDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
|
||||
|
||||
// row 1: X-Multi — multiline description
|
||||
const multiDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(multiDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Header line one');
|
||||
await expect(multiDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Header line two');
|
||||
|
||||
// row 2: X-Plain — no description (editor is empty)
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from request query params in a pre-existing .yml file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Params');
|
||||
|
||||
const queryTable = page.getByTestId('query-params-table');
|
||||
const rows = queryTable.locator('tbody tr');
|
||||
|
||||
// row 0: q — single-line description
|
||||
const qDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(qDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line query desc');
|
||||
|
||||
// row 1: page — multiline description
|
||||
const pageDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(pageDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line query desc line one');
|
||||
await expect(pageDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line query desc line two');
|
||||
|
||||
// row 2: plain-query — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from multipart form fields in a pre-existing .yml file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const multipartTable = page.getByTestId('multipart-form-table');
|
||||
const rows = multipartTable.locator('tbody tr');
|
||||
|
||||
// row 0: username — single-line description
|
||||
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line field desc');
|
||||
|
||||
// row 1: email — multiline description
|
||||
const emailDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(emailDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line field desc line one');
|
||||
await expect(emailDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line field desc line two');
|
||||
|
||||
// row 2: plain-field — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from form-urlencoded fields in a pre-existing .yml file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const formTable = page.getByTestId('form-urlencoded-table');
|
||||
const rows = formTable.locator('tbody tr');
|
||||
|
||||
// row 0: username — single-line description
|
||||
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line form desc');
|
||||
|
||||
// row 1: password — multiline description
|
||||
const passwordDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line form desc line one');
|
||||
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line form desc line two');
|
||||
|
||||
// row 2: plain-field — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
import { buildCommonLocators } from '../../utils/page/locators';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Assertions)', () => {
|
||||
test('writes a multiline description to a request assertion and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Assert');
|
||||
|
||||
const assertionsTable = buildCommonLocators(page).table('assertions-table');
|
||||
const plainAssertRow = assertionsTable.rowByName('plainAssert');
|
||||
const descCell = plainAssertRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plainAssert description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Body: form-urlencoded)', () => {
|
||||
test('writes a multiline description to a form field and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const formTable = page.getByTestId('form-urlencoded-table');
|
||||
const plainRow = formTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Form line one\nForm line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Form line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Form line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'formurlencoded-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('Form line one');
|
||||
expect(fileContent).toContain('Form line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Body: multipart-form)', () => {
|
||||
test('writes a multiline description to a multipart field and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const multipartTable = page.getByTestId('multipart-form-table');
|
||||
const plainRow = multipartTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Field line one\nField line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Field line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Field line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'multipart-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('Field line one');
|
||||
expect(fileContent).toContain('Field line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Headers)', () => {
|
||||
test('writes a multiline description to a request header and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const headersTable = page.getByTestId('request-headers-table');
|
||||
const xPlainRow = headersTable.locator('tbody tr').filter({
|
||||
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
|
||||
});
|
||||
const descCell = xPlainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Query Params)', () => {
|
||||
test('writes a multiline description to a query param and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Params');
|
||||
|
||||
const queryTable = page.getByTestId('query-params-table');
|
||||
const plainRow = queryTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-query description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Query line one\nQuery line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Query line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Query line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('Query line one');
|
||||
expect(fileContent).toContain('Query line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
import { buildCommonLocators } from '../../utils/page/locators';
|
||||
|
||||
const COLLECTION = 'req-description-yml';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description (YAML) - Write (Vars)', () => {
|
||||
test('writes a multiline description to a request var and persists it to the .yml file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Vars');
|
||||
|
||||
const varsTable = buildCommonLocators(page).table('request-vars-req');
|
||||
const plainVarRow = varsTable.rowByName('plainVar');
|
||||
const descCell = plainVarRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plainVar description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
|
||||
const fileContent = fs.readFileSync(ymlPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('description:');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
5
tests/request/description/fixtures/collection/bruno.json
Normal file
5
tests/request/description/fixtures/collection/bruno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "req-description",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
meta {
|
||||
name: req-description
|
||||
type: collection
|
||||
version: 1.0.0
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: formurlencoded-with-descriptions
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://example.com/form
|
||||
body: formUrlEncoded
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
@description('''Single-line form desc''')
|
||||
username: alice
|
||||
@description('''
|
||||
Multi-line form desc line one
|
||||
Multi-line form desc line two
|
||||
''')
|
||||
password: secret
|
||||
plain-field: no-desc
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: multipart-with-descriptions
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://example.com/upload
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
@description('''Single-line field desc''')
|
||||
username: alice
|
||||
@description('''
|
||||
Multi-line field desc line one
|
||||
Multi-line field desc line two
|
||||
''')
|
||||
email: alice@example.com
|
||||
plain-field: no-desc
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
meta {
|
||||
name: request-with-descriptions
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://example.com/api
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
@description('''Single-line query desc''')
|
||||
q: search
|
||||
@description('''
|
||||
Multi-line query desc line one
|
||||
Multi-line query desc line two
|
||||
''')
|
||||
page: 1
|
||||
plain-query: value
|
||||
}
|
||||
|
||||
headers {
|
||||
@description('''Single-line header desc''')
|
||||
X-Version: 2.0
|
||||
@description('''
|
||||
Header line one
|
||||
Header line two
|
||||
''')
|
||||
X-Multi: value
|
||||
X-Plain: plain-value
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
@description('''Single-line var desc''')
|
||||
apiKey: secret
|
||||
@description('''
|
||||
Multi-line var desc line one
|
||||
Multi-line var desc line two
|
||||
''')
|
||||
baseUrl: https://example.com
|
||||
plainVar: value
|
||||
}
|
||||
|
||||
assert {
|
||||
@description('''Single-line assert desc''')
|
||||
res.status: eq 200
|
||||
@description('''
|
||||
Multi-line assert line one
|
||||
Multi-line assert line two
|
||||
''')
|
||||
res.body.ok: eq true
|
||||
plainAssert: eq foo
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{collectionPath}}",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
28
tests/request/description/init-user-data/preferences.json
Normal file
28
tests/request/description/init-user-data/preferences.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}"
|
||||
],
|
||||
"request": {
|
||||
"sslVerification": false,
|
||||
"customCaCertificate": {
|
||||
"enabled": false,
|
||||
"filePath": null
|
||||
}
|
||||
},
|
||||
"font": {
|
||||
"codeFont": "default"
|
||||
},
|
||||
"proxy": {
|
||||
"enabled": false,
|
||||
"protocol": "http",
|
||||
"hostname": "",
|
||||
"port": "",
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"bypassProxy": ""
|
||||
}
|
||||
}
|
||||
150
tests/request/description/read-request-description.spec.ts
Normal file
150
tests/request/description/read-request-description.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
|
||||
test.describe('Request Description - Read', () => {
|
||||
test('reads descriptions from request headers in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const headersTable = page.getByTestId('request-headers-table');
|
||||
const rows = headersTable.locator('tbody tr');
|
||||
|
||||
// row 0: X-Version — single-line description
|
||||
const versionDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
|
||||
|
||||
// row 1: X-Multi — multiline description
|
||||
const multiDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(multiDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Header line one');
|
||||
await expect(multiDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Header line two');
|
||||
|
||||
// row 2: X-Plain — no description (editor is empty)
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from request query params in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Params');
|
||||
|
||||
const queryTable = page.getByTestId('query-params-table');
|
||||
const rows = queryTable.locator('tbody tr');
|
||||
|
||||
// row 0: q — single-line description
|
||||
const qDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(qDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line query desc');
|
||||
|
||||
// row 1: page — multiline description
|
||||
const pageDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(pageDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line query desc line one');
|
||||
await expect(pageDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line query desc line two');
|
||||
|
||||
// row 2: plain-query — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from multipart form fields in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const multipartTable = page.getByTestId('multipart-form-table');
|
||||
const rows = multipartTable.locator('tbody tr');
|
||||
|
||||
// row 0: username — single-line description
|
||||
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line field desc');
|
||||
|
||||
// row 1: email — multiline description
|
||||
const emailDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(emailDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line field desc line one');
|
||||
await expect(emailDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line field desc line two');
|
||||
|
||||
// row 2: plain-field — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from form-urlencoded fields in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const formTable = page.getByTestId('form-urlencoded-table');
|
||||
const rows = formTable.locator('tbody tr');
|
||||
|
||||
// row 0: username — single-line description
|
||||
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line form desc');
|
||||
|
||||
// row 1: password — multiline description
|
||||
const passwordDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line form desc line one');
|
||||
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line form desc line two');
|
||||
|
||||
// row 2: plain-field — no description
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from request vars in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Vars');
|
||||
|
||||
const varsTable = page.getByTestId('request-vars-req');
|
||||
const rows = varsTable.locator('tbody tr');
|
||||
|
||||
const apiKeyDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(apiKeyDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
|
||||
|
||||
const baseUrlDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(baseUrlDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line var desc line one');
|
||||
await expect(baseUrlDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line var desc line two');
|
||||
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
|
||||
test('reads descriptions from request assertions in a pre-existing .bru file', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Assert');
|
||||
|
||||
const assertionsTable = page.getByTestId('assertions-table');
|
||||
const rows = assertionsTable.locator('tbody tr');
|
||||
|
||||
const statusDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(statusDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line assert desc');
|
||||
|
||||
const bodyDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(bodyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line assert line one');
|
||||
await expect(bodyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line assert line two');
|
||||
|
||||
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
|
||||
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
import { buildCommonLocators } from '../../utils/page/locators';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Assertions)', () => {
|
||||
test('writes a multiline description to a request assertion and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Assert');
|
||||
|
||||
const assertionsTable = buildCommonLocators(page).table('assertions-table');
|
||||
const plainAssertRow = assertionsTable.rowByName('plainAssert');
|
||||
const descCell = plainAssertRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plainAssert description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Body: form-urlencoded)', () => {
|
||||
test('writes a multiline description to a form field and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const formTable = page.getByTestId('form-urlencoded-table');
|
||||
const plainRow = formTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Form line one\nForm line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Form line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Form line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'formurlencoded-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('Form line one');
|
||||
expect(fileContent).toContain('Form line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Body: multipart-form)', () => {
|
||||
test('writes a multiline description to a multipart field and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
|
||||
const multipartTable = page.getByTestId('multipart-form-table');
|
||||
const plainRow = multipartTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Field line one\nField line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Field line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Field line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'multipart-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('Field line one');
|
||||
expect(fileContent).toContain('Field line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
import { buildCommonLocators } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Headers, quote escaping)', () => {
|
||||
test('escapes embedded triple quotes and backslash-quotes in a multiline description and round-trips through disk', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const headersTable = page.getByTestId('request-headers-table');
|
||||
const xPlainRow = headersTable.locator('tbody tr').filter({
|
||||
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
|
||||
});
|
||||
const descCell = xPlainRow.getByTestId('column-description');
|
||||
|
||||
const description = 'Backslash-quote: \\\'end\nTriple quote: \'\'\' embedded\nFinal line';
|
||||
|
||||
await descCell.evaluate((el, value) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue(value);
|
||||
}, description);
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Backslash-quote: \\\'end');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Triple quote: \'\'\' embedded');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(2)).toHaveText('Final line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
// the ''' delimiter is escaped so it can't be mistaken for the closing block delimiter
|
||||
expect(fileContent).toContain('\\\'\\\'\\\' embedded');
|
||||
// the pre-existing backslash-quote is doubled so decoding can tell it apart from the above
|
||||
expect(fileContent).toContain('\\\\\'end');
|
||||
|
||||
// reopen the request from disk to verify the escaped value round-trips back exactly
|
||||
await buildCommonLocators(page).tabs.closeTab('request-with-descriptions').click({ force: true });
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const reopenedRow = page.getByTestId('request-headers-table').locator('tbody tr').filter({
|
||||
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
|
||||
});
|
||||
const reopenedDescCell = reopenedRow.getByTestId('column-description');
|
||||
|
||||
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(0)).toHaveText('Backslash-quote: \\\'end');
|
||||
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(1)).toHaveText('Triple quote: \'\'\' embedded');
|
||||
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(2)).toHaveText('Final line');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Headers)', () => {
|
||||
test('writes a multiline description to a request header and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
|
||||
const headersTable = page.getByTestId('request-headers-table');
|
||||
const xPlainRow = headersTable.locator('tbody tr').filter({
|
||||
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
|
||||
});
|
||||
const descCell = xPlainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Query Params)', () => {
|
||||
test('writes a multiline description to a query param and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Params');
|
||||
|
||||
const queryTable = page.getByTestId('query-params-table');
|
||||
const plainRow = queryTable.locator('tbody tr').nth(2);
|
||||
const descCell = plainRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plain-query description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('Query line one\nQuery line two');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Query line one');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Query line two');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('Query line one');
|
||||
expect(fileContent).toContain('Query line two');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
import { buildCommonLocators } from '../../utils/page/locators';
|
||||
|
||||
const COLLECTION = 'req-description';
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
|
||||
test.describe('Request Description - Write (Vars)', () => {
|
||||
test('writes a multiline description to a request var and persists it to the .bru file', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION, 'request-with-descriptions');
|
||||
await selectRequestPaneTab(page, 'Vars');
|
||||
|
||||
const varsTable = buildCommonLocators(page).table('request-vars-req');
|
||||
const plainVarRow = varsTable.rowByName('plainVar');
|
||||
const descCell = plainVarRow.getByTestId('column-description');
|
||||
|
||||
await descCell.evaluate((el) => {
|
||||
const cmEl = el.querySelector('.CodeMirror');
|
||||
if (!cmEl) throw new Error('No CodeMirror in plainVar description cell');
|
||||
const cm = (cmEl as any).CodeMirror;
|
||||
if (!cm) throw new Error('CodeMirror instance not found');
|
||||
cm.setValue('First line\nSecond line');
|
||||
});
|
||||
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
|
||||
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
|
||||
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
|
||||
const fileContent = fs.readFileSync(bruPath, 'utf8');
|
||||
|
||||
expect(fileContent).toContain('@description');
|
||||
expect(fileContent).toContain('First line');
|
||||
expect(fileContent).toContain('Second line');
|
||||
});
|
||||
});
|
||||
@@ -53,9 +53,10 @@ test.describe.serial('Header Validation', () => {
|
||||
const headerRow = page.locator('table tbody tr').first();
|
||||
const nameCell = getTableCell(headerRow, 0);
|
||||
|
||||
// Clear and enter a valid header name - use triple-click to select all (works cross-platform)
|
||||
await nameCell.locator('.CodeMirror').click({ clickCount: 3 });
|
||||
await nameCell.locator('textarea').fill('Valid-Header');
|
||||
// Clear and enter a valid header name.
|
||||
await nameCell.locator('.CodeMirror').click();
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
|
||||
await page.keyboard.insertText('Valid-Header');
|
||||
|
||||
// Verify the error icon is not visible
|
||||
const errorIcon = headerRow.locator('.text-red-600');
|
||||
|
||||
@@ -148,7 +148,7 @@ value1\r
|
||||
|
||||
// The multipart form has an editable table - find and fill the first row
|
||||
// The name column has placeholder "Key" (defined in MultipartFormParams columns)
|
||||
const nameInput = page.locator('[data-testid="editable-table"] input[placeholder="Key"]').first();
|
||||
const nameInput = page.locator('[data-testid="multipart-form-table"] input[placeholder="Key"]').first();
|
||||
await nameInput.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await nameInput.click();
|
||||
await nameInput.fill('testField');
|
||||
|
||||
@@ -67,7 +67,7 @@ test.describe('Multipart Form - Multiple File Upload', () => {
|
||||
|
||||
// Reset the form to a single empty row before each test.
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const table = buildCommonLocators(page).table('editable-table');
|
||||
const table = buildCommonLocators(page).table('multipart-form-table');
|
||||
await expect(table.container()).toBeVisible();
|
||||
|
||||
let rowCount = await table.allRows().count();
|
||||
@@ -86,7 +86,7 @@ test.describe('Multipart Form - Multiple File Upload', () => {
|
||||
(global as any).__mockFilePaths = paths;
|
||||
}, files);
|
||||
|
||||
const table = buildCommonLocators(page).table('editable-table');
|
||||
const table = buildCommonLocators(page).table('multipart-form-table');
|
||||
await table.allRows().last().getByTestId('multipart-file-upload').click();
|
||||
};
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ test.describe.serial('Multipart Form - File Select Without Key', () => {
|
||||
});
|
||||
|
||||
test('file select should work on empty row without a key', async ({ page }) => {
|
||||
const table = buildCommonLocators(page).table('editable-table');
|
||||
const table = buildCommonLocators(page).table('multipart-form-table');
|
||||
|
||||
await test.step('Click upload on empty last row (no key entered)', async () => {
|
||||
const lastRow = table.allRows().last();
|
||||
const uploadBtn = lastRow.locator('.upload-btn');
|
||||
const uploadBtn = lastRow.getByTestId('multipart-file-upload');
|
||||
await expect(uploadBtn).toBeVisible();
|
||||
await uploadBtn.click();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user