Files
bruno/packages/bruno-lang/v2/tests/utils.spec.js
Sid 0bba66a590 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>
2026-07-07 21:43:39 +05:30

230 lines
9.1 KiB
JavaScript

const {
getValueString,
extractTypedAnnotations,
buildAnnotationsFromVariable,
serializeAnnotations,
escapeMultilineDescription,
unescapeMultilineDescription
} = require('../src/utils');
describe('getValueString', () => {
it('returns single line value as-is', () => {
expect(getValueString('hello world')).toBe('hello world');
});
it('wraps multiline value in triple quotes with indentation', () => {
expect(getValueString('line1\nline2\nline3')).toBe('\'\'\'\n line1\n line2\n line3\n\'\'\'');
});
it('normalizes different newline types', () => {
expect(getValueString('line1\r\nline2\rline3\nline4')).toBe('\'\'\'\n line1\n line2\n line3\n line4\n\'\'\'');
});
it('returns empty string for empty/null/undefined', () => {
expect(getValueString('')).toBe('');
expect(getValueString(null)).toBe('');
expect(getValueString(undefined)).toBe('');
});
it('returns "0" for the number 0 (truthy guard)', () => {
expect(getValueString(0)).toBe('0');
});
it('returns "false" for boolean false (truthy guard)', () => {
expect(getValueString(false)).toBe('false');
});
it('stringifies numbers and booleans to their primitive form', () => {
expect(getValueString(42)).toBe('42');
expect(getValueString(true)).toBe('true');
});
it('JSON-stringifies object values and wraps multiline output in triple quotes', () => {
const out = getValueString({ a: 1, b: 'x' });
expect(out).toContain('"a": 1');
expect(out).toContain('"b": "x"');
});
});
describe('extractTypedAnnotations', () => {
it('sets dataType and coerces value when a dataType annotation is present', () => {
const result = { value: '42' };
extractTypedAnnotations([{ name: 'number' }], result);
expect(result.dataType).toBe('number');
expect(result.value).toBe(42);
});
it('does nothing when no dataType annotation is present', () => {
const result = { value: 'abc' };
extractTypedAnnotations([{ name: 'description', value: 'doc' }], result);
expect(result.dataType).toBeUndefined();
expect(result.value).toBe('abc');
});
it('does not materialize @string as an explicit dataType', () => {
const result = { value: 'abc' };
extractTypedAnnotations([{ name: 'string' }], result);
expect(result.dataType).toBeUndefined();
});
it('picks the last dataType annotation when multiple are stacked', () => {
const result = { value: '99' };
extractTypedAnnotations([{ name: 'object' }, { name: 'number' }], result);
expect(result.dataType).toBe('number');
expect(result.value).toBe(99);
});
it('preserves the declared dataType and leaves the raw value when coercion is impossible', () => {
// The DataTypeSelector relies on this: when dataType is preserved but the
// coerced value's type doesn't match, the UI surfaces a warning icon.
const result = { value: 'not-a-number' };
extractTypedAnnotations([{ name: 'number' }], result);
expect(result.dataType).toBe('number');
expect(result.value).toBe('not-a-number');
});
it('preserves @boolean even when the literal is not a boolean string', () => {
const result = { value: 'maybe' };
extractTypedAnnotations([{ name: 'boolean' }], result);
expect(result.dataType).toBe('boolean');
expect(result.value).toBe('maybe');
});
it('preserves @object even when the value is not parseable JSON', () => {
const result = { value: 'plain text' };
extractTypedAnnotations([{ name: 'object' }], result);
expect(result.dataType).toBe('object');
expect(result.value).toBe('plain text');
});
it('handles null / empty annotations safely', () => {
const result = { value: 'abc' };
extractTypedAnnotations(null, result);
extractTypedAnnotations([], result);
expect(result.dataType).toBeUndefined();
});
});
describe('buildAnnotationsFromVariable', () => {
it('returns an empty array when no dataType and no annotations', () => {
expect(buildAnnotationsFromVariable({})).toEqual([]);
});
it('prepends a dataType annotation from the dataType field', () => {
expect(buildAnnotationsFromVariable({ dataType: 'number' })).toEqual([{ name: 'number' }]);
});
it('drops any existing dataType annotation and rebuilds from the dataType field', () => {
const out = buildAnnotationsFromVariable({
dataType: 'number',
annotations: [{ name: 'string' }, { name: 'description', value: 'doc' }]
});
expect(out).toEqual([{ name: 'number' }, { name: 'description', value: 'doc' }]);
});
it('does not emit a dataType annotation for the string default', () => {
expect(buildAnnotationsFromVariable({ dataType: 'string' })).toEqual([]);
});
it('preserves non-dataType annotations when dataType is absent', () => {
expect(buildAnnotationsFromVariable({
annotations: [{ name: 'description', value: 'doc' }]
})).toEqual([{ name: 'description', value: 'doc' }]);
});
});
describe('serializeAnnotations', () => {
it('returns an empty string for null/undefined/empty input', () => {
expect(serializeAnnotations(null)).toBe('');
expect(serializeAnnotations(undefined)).toBe('');
expect(serializeAnnotations([])).toBe('');
});
it('serializes a valueless annotation as @name with a trailing newline', () => {
expect(serializeAnnotations([{ name: 'number' }])).toBe('@number\n');
});
it('serializes a non-empty description annotation using single-quote delimiters', () => {
expect(serializeAnnotations([{ name: 'description', value: 'a doc' }])).toBe('@description(\'a doc\')\n');
});
it('uses double-quote delimiters for description when value contains a single quote', () => {
expect(serializeAnnotations([{ name: 'description', value: 'O\'Reilly' }])).toBe('@description("O\'Reilly")\n');
});
it('uses single-quote delimiters for description when value contains a double quote', () => {
expect(serializeAnnotations([{ name: 'description', value: 'say "hi"' }])).toBe('@description(\'say "hi"\')\n');
});
it('wraps multiline values in triple-quote delimiters with 2-space indentation', () => {
expect(serializeAnnotations([{ name: 'description', value: 'line1\nline2' }])).toBe(
'@description(\'\'\'\n line1\n line2\n\'\'\')\n'
);
});
it('joins multiple annotations with newlines and adds a single trailing newline', () => {
expect(
serializeAnnotations([
{ name: 'number' },
{ name: 'description', value: 'doc' }
])
).toBe('@number\n@description(\'doc\')\n');
});
it('coerces non-string values to strings via String() before quoting', () => {
expect(serializeAnnotations([{ name: 'count', value: 42 }])).toBe('@count(\'42\')\n');
expect(serializeAnnotations([{ name: 'enabled', value: false }])).toBe('@enabled(\'false\')\n');
});
it('treats null/empty-string values as present (not as missing)', () => {
// null coerces to 'null' (non-empty), so gets single-quote format
expect(serializeAnnotations([{ name: 'description', value: null }])).toBe('@description(\'null\')\n');
// empty string falls through to standard single-quote format
expect(serializeAnnotations([{ name: 'description', value: '' }])).toBe('@description(\'\')\n');
});
it('escapes embedded triple quotes in a multiline description', () => {
expect(serializeAnnotations([{ name: 'description', value: 'line1\nline2 with \'\'\' inside\nline3' }])).toBe(
'@description(\'\'\'\n line1\n line2 with \\\'\\\'\\\' inside\n line3\n\'\'\')\n'
);
});
it('doubles a pre-existing backslash-quote before escaping triple quotes in a multiline description', () => {
expect(serializeAnnotations([{ name: 'description', value: 'it\\\'s multiline\nand has \'\'\' too' }])).toBe(
'@description(\'\'\'\n it\\\\\'s multiline\n and has \\\'\\\'\\\' too\n\'\'\')\n'
);
});
it('does not escape triple quotes on other (non-description) multiline annotations', () => {
expect(serializeAnnotations([{ name: 'note', value: 'line1 \'\'\'\nline2' }])).toBe(
'@note(\'\'\'\n line1 \'\'\'\n line2\n\'\'\')\n'
);
});
});
describe('escapeMultilineDescription / unescapeMultilineDescription', () => {
it('escapes a literal triple-quote sequence', () => {
expect(escapeMultilineDescription('has \'\'\' inside')).toBe('has \\\'\\\'\\\' inside');
});
it('doubles a pre-existing backslash-quote sequence', () => {
expect(escapeMultilineDescription('it\\\'s here')).toBe('it\\\\\'s here');
});
it('round-trips a value containing both a backslash-quote and a triple-quote', () => {
const original = 'it\\\'s \'\'\' here';
const escaped = escapeMultilineDescription(original);
expect(unescapeMultilineDescription(escaped)).toBe(original);
});
it('leaves plain text untouched', () => {
expect(escapeMultilineDescription('plain text')).toBe('plain text');
expect(unescapeMultilineDescription('plain text')).toBe('plain text');
});
it('unescapeMultilineDescription reverses escapeMultilineDescription', () => {
const original = 'line1\nline2 with \'\'\' and \\\'quoted\\\' bits\nline3';
expect(unescapeMultilineDescription(escapeMultilineDescription(original))).toBe(original);
});
});