mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
* 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>
230 lines
9.1 KiB
JavaScript
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);
|
|
});
|
|
});
|