mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-30 08:04:09 +00:00
feat: support newlines in headers, params, and variables (#5795)
* feat: support newlines in headers, params, and variables * add: collectin unit test * fix: assertion and additional header multiline * fix: assert * rm: useEffect for header validation * rm: comments * fix: already encoded url * rm: new line changes * handle new line in url * fix: lint error * add: unit test for multi line test * change: unit test * mv: functions in util * fix: drag icon position * improve: arrow height * improvements * rm: getKeyString from assert * fix: single line editor * fix: import MultiLineEditor * import getKeyString and getValueUrl * add: getTableCell in utils * rm: multiline key logic * fix * mv: getTableCell in locators.ts
This commit is contained in:
@@ -45,7 +45,8 @@ const Headers = ({ collection }) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
// Strip newlines from header keys
|
||||
header.name = e.target.value.replace(/[\r\n]/g, '');
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -114,7 +114,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -41,7 +41,8 @@ const Headers = ({ collection, folder }) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
// Strip newlines from header keys
|
||||
header.name = e.target.value.replace(/[\r\n]/g, '');
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -113,7 +113,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -79,7 +79,7 @@ const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||
<>
|
||||
<div
|
||||
draggable
|
||||
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
|
||||
className="group drag-handle absolute z-10 left-[-17px] top-1/2 -translate-y-[80%] p-2.5 cursor-grab"
|
||||
>
|
||||
{hoveredRow === index && (
|
||||
<>
|
||||
|
||||
@@ -4,7 +4,8 @@ import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { cloneDeep } from "lodash";
|
||||
import SingleLineEditor from "components/SingleLineEditor/index";
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Table from "components/Table/index";
|
||||
|
||||
@@ -205,7 +206,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={param?.value || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
updatePathParam,
|
||||
setQueryParams
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -168,7 +168,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
@@ -244,7 +244,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={path.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -116,6 +116,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
collection={collection}
|
||||
highlightPathParams={true}
|
||||
item={item}
|
||||
showNewlineArrow={true}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
|
||||
@@ -20,7 +20,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -36,9 +36,11 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
// Strip newlines from header keys
|
||||
header.name = e.target.value.replace(/[\r\n]/g, '');
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
@@ -50,6 +52,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateRequestHeader({
|
||||
header: header,
|
||||
@@ -154,7 +157,6 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addVar, updateVar, deleteVar, moveVar } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -122,7 +122,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -99,6 +99,11 @@ class SingleLineEditor extends Component {
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
|
||||
// Add newline arrow markers if enabled
|
||||
if (this.props.showNewlineArrow) {
|
||||
this._updateNewlineMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/** Enable or disable masking the rendered content of the editor */
|
||||
@@ -123,6 +128,11 @@ class SingleLineEditor extends Component {
|
||||
if (this.props.onChange && (this.props.value !== this.cachedValue)) {
|
||||
this.props.onChange(this.cachedValue);
|
||||
}
|
||||
|
||||
// Update newline markers after edit
|
||||
if (this.props.showNewlineArrow) {
|
||||
this._updateNewlineMarkers();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,6 +155,11 @@ class SingleLineEditor extends Component {
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
|
||||
// Update newline markers after value change
|
||||
if (this.props.showNewlineArrow) {
|
||||
this._updateNewlineMarkers();
|
||||
}
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
@@ -162,6 +177,7 @@ class SingleLineEditor extends Component {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('paste', this._onPaste);
|
||||
this._clearNewlineMarkers();
|
||||
this.editor.getWrapperElement().remove();
|
||||
this.editor = null;
|
||||
}
|
||||
@@ -180,6 +196,63 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update markers to show arrows for newlines
|
||||
*/
|
||||
_updateNewlineMarkers = () => {
|
||||
if (!this.editor) return;
|
||||
|
||||
// Clear existing markers
|
||||
this._clearNewlineMarkers();
|
||||
|
||||
this.newlineMarkers = [];
|
||||
const content = this.editor.getValue();
|
||||
|
||||
// Find all newlines and replace them with arrow widgets
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] === '\n') {
|
||||
const pos = this.editor.posFromIndex(i);
|
||||
const nextPos = this.editor.posFromIndex(i + 1);
|
||||
|
||||
// Create a widget to display the arrow
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'newline-arrow';
|
||||
arrow.textContent = '↲';
|
||||
arrow.style.cssText = `
|
||||
color: #888;
|
||||
font-size: 8px;
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
// Mark the newline character and replace it with the arrow widget
|
||||
const marker = this.editor.markText(pos, nextPos, {
|
||||
replacedWith: arrow,
|
||||
handleMouseEvents: true
|
||||
});
|
||||
|
||||
this.newlineMarkers.push(marker);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all newline markers
|
||||
*/
|
||||
_clearNewlineMarkers = () => {
|
||||
if (this.newlineMarkers) {
|
||||
this.newlineMarkers.forEach((marker) => {
|
||||
try {
|
||||
marker.clear();
|
||||
} catch (e) {
|
||||
// Marker might already be cleared
|
||||
}
|
||||
});
|
||||
this.newlineMarkers = [];
|
||||
}
|
||||
};
|
||||
|
||||
toggleVisibleSecret = () => {
|
||||
const isVisible = !this.state.maskInput;
|
||||
this.setState({ maskInput: isVisible });
|
||||
@@ -204,13 +277,15 @@ class SingleLineEditor extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>
|
||||
<div className={`flex flex-row items-center w-full overflow-x-auto ${this.props.className}`}>
|
||||
<StyledWrapper
|
||||
ref={this.editorRef}
|
||||
className={`single-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`}
|
||||
{...(this.props['data-testid'] ? { 'data-testid': this.props['data-testid'] } : {})}
|
||||
/>
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
<div className="flex items-center">
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,12 +20,22 @@ const grammar = ohm.grammar(`Bru {
|
||||
keychar = ~(tagend | st | nl | ":") any
|
||||
valuechar = ~(nl | tagend) any
|
||||
|
||||
// Multiline text block surrounded by '''
|
||||
multilinetextblockdelimiter = "'''"
|
||||
multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
|
||||
|
||||
// Dictionary Blocks
|
||||
dictionary = st* "{" pairlist? tagend
|
||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||
pair = st* key st* ":" st* value st*
|
||||
pair = st* (quoted_key | key) st* ":" st* value st*
|
||||
disable_char = "~"
|
||||
quote_char = "\\""
|
||||
esc_char = "\\\\"
|
||||
esc_quote_char = esc_char quote_char
|
||||
quoted_key_char = ~(quote_char | esc_quote_char | nl) any
|
||||
quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char
|
||||
key = keychar*
|
||||
value = valuechar*
|
||||
value = multilinetextblock | valuechar*
|
||||
|
||||
// Text Blocks
|
||||
textblock = textline (~tagend nl textline)*
|
||||
@@ -137,10 +147,38 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
quoted_key(disabled, _1, chars, _2) {
|
||||
// unquote and handle disabled prefix
|
||||
return (disabled ? disabled.sourceString : '') + chars.ast.join('');
|
||||
},
|
||||
esc_quote_char(_1, quote) {
|
||||
// unescape
|
||||
return quote.sourceString;
|
||||
},
|
||||
quoted_key_char(char) {
|
||||
// return the character itself
|
||||
return char.sourceString;
|
||||
},
|
||||
key(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
value(chars) {
|
||||
if (chars.ctorName === 'list') {
|
||||
return chars.ast;
|
||||
}
|
||||
try {
|
||||
let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
|
||||
if (isMultiline) {
|
||||
const multilineString = chars.sourceString?.replace(/^'''|'''$/g, '');
|
||||
return multilineString
|
||||
.split('\n')
|
||||
.map((line) => line.slice(4))
|
||||
.join('\n');
|
||||
}
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
textblock(line, _1, rest) {
|
||||
@@ -152,6 +190,10 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
textchar(char) {
|
||||
return char.sourceString;
|
||||
},
|
||||
multilinetextblock(_1, content, _2) {
|
||||
// Join all the content between the triple quotes and trim it
|
||||
return content.sourceString.trim();
|
||||
},
|
||||
nl(_1, _2) {
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString, getValueString } = require('./utils');
|
||||
const { indentString, getValueString, getKeyString, getValueUrl } = require('./utils');
|
||||
const jsonToExampleBru = require('./example/jsonToBru');
|
||||
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
const quoteKey = (key) => {
|
||||
const quotableChars = [':', '"', '{', '}', ' '];
|
||||
return quotableChars.some(char => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key;
|
||||
}
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -50,7 +46,7 @@ const jsonToBru = (json) => {
|
||||
const isStandard = standardMethods.has(method);
|
||||
|
||||
bru += isStandard ? `${method} {` : `http {\n method: ${method}`;
|
||||
bru += `\n url: ${url}`;
|
||||
bru += `\n url: ${getValueUrl(url)}`;
|
||||
|
||||
if (body?.length) {
|
||||
bru += `\n body: ${body}`;
|
||||
@@ -133,7 +129,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(queryParams)
|
||||
.map((item) => `${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -141,7 +137,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(queryParams)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -152,7 +148,7 @@ const jsonToBru = (json) => {
|
||||
if (pathParams.length) {
|
||||
bru += 'params:path {';
|
||||
|
||||
bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
@@ -163,7 +159,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(headers)
|
||||
.map((item) => `${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -171,7 +167,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(headers)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${item.value}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -184,7 +180,7 @@ const jsonToBru = (json) => {
|
||||
if (enabled(metadata).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(metadata)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -192,7 +188,7 @@ const jsonToBru = (json) => {
|
||||
if (disabled(metadata).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(metadata)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.map((item) => `~${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -360,7 +356,7 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr
|
||||
${indentString(
|
||||
authorizationHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -373,7 +369,7 @@ ${indentString(
|
||||
${indentString(
|
||||
authorizationQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -386,7 +382,7 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -399,7 +395,7 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -412,7 +408,7 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -425,7 +421,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -438,7 +434,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -451,7 +447,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -508,14 +504,14 @@ ${indentString(body.sparql)}
|
||||
|
||||
if (enabled(body.formUrlEncoded).length) {
|
||||
const enabledValues = enabled(body.formUrlEncoded)
|
||||
.map((item) => `${quoteKey(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(enabledValues)}\n`;
|
||||
}
|
||||
|
||||
if (disabled(body.formUrlEncoded).length) {
|
||||
const disabledValues = disabled(body.formUrlEncoded)
|
||||
.map((item) => `~${quoteKey(item.name)}: ${getValueString(item.value)}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n');
|
||||
bru += `${indentString(disabledValues)}\n`;
|
||||
}
|
||||
@@ -536,7 +532,7 @@ ${indentString(body.sparql)}
|
||||
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${quoteKey(item.name)}: ${getValueString(item.value)}${contentType}`;
|
||||
return `${enabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
@@ -544,7 +540,7 @@ ${indentString(body.sparql)}
|
||||
const filestr = filepaths.join('|');
|
||||
|
||||
const value = `@file(${filestr})`;
|
||||
return `${enabled}${quoteKey(item.name)}: ${value}${contentType}`;
|
||||
return `${enabled}${getKeyString(item.name)}: ${value}${contentType}`;
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
@@ -644,19 +640,19 @@ ${indentString(body.sparql)}
|
||||
bru += `vars:pre-request {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -670,19 +666,19 @@ ${indentString(body.sparql)}
|
||||
bru += `vars:post-response {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -694,7 +690,7 @@ ${indentString(body.sparql)}
|
||||
if (enabled(assertions).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(assertions)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.name}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -702,7 +698,7 @@ ${indentString(body.sparql)}
|
||||
if (disabled(assertions).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(assertions)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('./utils');
|
||||
const { indentString, getValueString, getKeyString } = require('./utils');
|
||||
|
||||
const enabled = (items = []) => items.filter((item) => item.enabled);
|
||||
const disabled = (items = []) => items.filter((item) => !item.enabled);
|
||||
@@ -30,7 +30,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (enabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(query)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (disabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(query)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (enabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(headers)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ const jsonToCollectionBru = (json) => {
|
||||
if (disabled(headers).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(headers)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
@@ -243,7 +243,7 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
|
||||
${indentString(
|
||||
authorizationHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ ${indentString(
|
||||
${indentString(
|
||||
authorizationQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -282,9 +282,8 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n'))}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -295,7 +294,7 @@ ${indentString(
|
||||
${indentString(
|
||||
tokenBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -308,7 +307,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -321,7 +320,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -334,7 +333,7 @@ ${indentString(
|
||||
${indentString(
|
||||
refreshBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
|
||||
.join('\n')
|
||||
)}
|
||||
}
|
||||
@@ -355,19 +354,19 @@ ${indentString(
|
||||
bru += `vars:pre-request {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
@@ -381,19 +380,19 @@ ${indentString(
|
||||
bru += `vars:post-response {`;
|
||||
|
||||
if (varsEnabled.length) {
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalEnabled.length) {
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsDisabled.length) {
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
if (varsLocalDisabled.length) {
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
|
||||
bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
|
||||
@@ -8,14 +8,15 @@ const safeParseJson = (json) => {
|
||||
};
|
||||
|
||||
|
||||
const indentString = (str) => {
|
||||
const indentString = (str, levels = 1) => {
|
||||
if (!str || !str.length) {
|
||||
return str || '';
|
||||
}
|
||||
|
||||
const indent = ' '.repeat(levels);
|
||||
return str
|
||||
.split(/\r\n|\r|\n/)
|
||||
.map((line) => ' ' + line)
|
||||
.map((line) => indent + line)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
@@ -37,7 +38,7 @@ const getValueString = (value) => {
|
||||
return '';
|
||||
}
|
||||
|
||||
const hasNewLines = value?.includes('\n') || value?.includes('\r');
|
||||
const hasNewLines = value.includes('\n') || value.includes('\r');
|
||||
|
||||
if (!hasNewLines) {
|
||||
return value;
|
||||
@@ -47,9 +48,32 @@ const getValueString = (value) => {
|
||||
return `'''\n${indentString(value)}\n'''`;
|
||||
};
|
||||
|
||||
const getKeyString = (key) => {
|
||||
const quotableChars = [':', '"', '{', '}', ' '];
|
||||
return quotableChars.some((char) => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key;
|
||||
};
|
||||
|
||||
const getValueUrl = (url) => {
|
||||
// Handle null, undefined, and empty strings
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const hasNewLines = url.includes('\n') || url.includes('\r');
|
||||
|
||||
if (!hasNewLines) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Wrap multiline values in triple quotes with 4-space indentation (2 levels)
|
||||
return `'''\n${indentString(url, 2)}\n'''`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
safeParseJson,
|
||||
indentString,
|
||||
outdentString,
|
||||
getValueString
|
||||
getValueString,
|
||||
getKeyString,
|
||||
getValueUrl
|
||||
};
|
||||
|
||||
@@ -38,4 +38,88 @@ settings {
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-line values', () => {
|
||||
it('parses multi-line values in URL, headers, params, and vars', () => {
|
||||
const input = `
|
||||
meta {
|
||||
name: new-line
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: '''
|
||||
https://httpbin.io/anything?foo=hello
|
||||
world
|
||||
'''
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
params:query {
|
||||
foo: '''
|
||||
hello
|
||||
world
|
||||
'''
|
||||
}
|
||||
|
||||
headers {
|
||||
"test header": '''
|
||||
t1
|
||||
t2
|
||||
'''
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
test-var: '''
|
||||
t1
|
||||
t2
|
||||
'''
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = {
|
||||
meta: {
|
||||
name: 'new-line',
|
||||
type: 'http',
|
||||
seq: '1'
|
||||
},
|
||||
http: {
|
||||
method: 'get',
|
||||
url: 'https://httpbin.io/anything?foo=hello\nworld',
|
||||
body: 'none',
|
||||
auth: 'oauth2'
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: 'hello\nworld',
|
||||
enabled: true,
|
||||
type: 'query'
|
||||
}
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
name: 'test header',
|
||||
value: 't1\nt2',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
vars: {
|
||||
req: [
|
||||
{
|
||||
name: 'test-var',
|
||||
value: 't1\nt2',
|
||||
enabled: true,
|
||||
local: false
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const output = parser(input);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
56
packages/bruno-lang/v2/tests/getKeyString.spec.js
Normal file
56
packages/bruno-lang/v2/tests/getKeyString.spec.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const { getKeyString } = require('../src/utils');
|
||||
|
||||
describe('getKeyString', () => {
|
||||
describe('should not quote keys without special characters', () => {
|
||||
it('should return simple alphanumeric keys as-is', () => {
|
||||
expect(getKeyString('hello')).toBe('hello');
|
||||
expect(getKeyString('world123')).toBe('world123');
|
||||
expect(getKeyString('API')).toBe('API');
|
||||
});
|
||||
|
||||
it('should return keys with hyphens as-is', () => {
|
||||
expect(getKeyString('api-key')).toBe('api-key');
|
||||
expect(getKeyString('content-type')).toBe('content-type');
|
||||
});
|
||||
|
||||
it('should return keys with underscores as-is', () => {
|
||||
expect(getKeyString('api_key')).toBe('api_key');
|
||||
expect(getKeyString('user_name')).toBe('user_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should quote keys with special characters', () => {
|
||||
it('should quote keys with colons', () => {
|
||||
expect(getKeyString('key:value')).toBe('"key:value"');
|
||||
expect(getKeyString('disabled:colon:header')).toBe('"disabled:colon:header"');
|
||||
expect(getKeyString(':startsWithColon')).toBe('":startsWithColon"');
|
||||
expect(getKeyString('endsWithColon:')).toBe('"endsWithColon:"');
|
||||
});
|
||||
|
||||
it('should quote keys with spaces', () => {
|
||||
expect(getKeyString('key with spaces')).toBe('"key with spaces"');
|
||||
expect(getKeyString(' leadingSpace')).toBe('" leadingSpace"');
|
||||
expect(getKeyString('trailingSpace ')).toBe('"trailingSpace "');
|
||||
expect(getKeyString('multiple spaces')).toBe('"multiple spaces"');
|
||||
});
|
||||
|
||||
it('should quote keys with curly braces', () => {
|
||||
expect(getKeyString('{braces}')).toBe('"{braces}"');
|
||||
expect(getKeyString('{only-open')).toBe('"{only-open"');
|
||||
expect(getKeyString('only-close}')).toBe('"only-close}"');
|
||||
expect(getKeyString('nested{brace}here')).toBe('"nested{brace}here"');
|
||||
});
|
||||
|
||||
it('should quote keys with double quotes and escape them', () => {
|
||||
expect(getKeyString('nested "quote"')).toBe('"nested \\"quote\\""');
|
||||
expect(getKeyString('"quoted"')).toBe('"\\"quoted\\""');
|
||||
expect(getKeyString('multiple "quotes" here "too"')).toBe('"multiple \\"quotes\\" here \\"too\\""');
|
||||
});
|
||||
|
||||
it('should quote keys with multiple special characters', () => {
|
||||
expect(getKeyString('key: value')).toBe('"key: value"');
|
||||
expect(getKeyString('{key}: "value"')).toBe('"{key}: \\"value\\""');
|
||||
expect(getKeyString('complex:key with {braces}')).toBe('"complex:key with {braces}"');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,4 +53,87 @@ describe('jsonToBru stringify', () => {
|
||||
expect(output).toMatch(new RegExp(`timeout: ${input.settings.timeout}`));
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-line values', () => {
|
||||
it('handles multi-line values in URL, headers, params, and vars', () => {
|
||||
const input = {
|
||||
meta: {
|
||||
name: 'new-line',
|
||||
type: 'http',
|
||||
seq: 1
|
||||
},
|
||||
http: {
|
||||
method: 'get',
|
||||
url: 'https://httpbin.io/anything?foo=hello\nworld',
|
||||
body: 'none',
|
||||
auth: 'oauth2'
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: 'hello\nworld',
|
||||
enabled: true,
|
||||
type: 'query'
|
||||
}
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
name: 'test header',
|
||||
value: 't1\nt2',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
vars: {
|
||||
req: [
|
||||
{
|
||||
name: 'test-var',
|
||||
value: 't1\nt2',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const output = stringify(input);
|
||||
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"meta {
|
||||
name: new-line
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: '''
|
||||
https://httpbin.io/anything?foo=hello
|
||||
world
|
||||
'''
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
params:query {
|
||||
foo: '''
|
||||
hello
|
||||
world
|
||||
'''
|
||||
}
|
||||
|
||||
headers {
|
||||
"test header": '''
|
||||
t1
|
||||
t2
|
||||
'''
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
test-var: '''
|
||||
t1
|
||||
t2
|
||||
'''
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
87
tests/request/newlines/newlines-persistence.spec.ts
Normal file
87
tests/request/newlines/newlines-persistence.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { openCollectionAndAcceptSandbox } from '../../utils/page/actions';
|
||||
import { getTableCell } from '../../utils/page/locators';
|
||||
|
||||
test('should persist request with newlines across app restarts', async ({ createTmpDir, launchElectronApp }) => {
|
||||
const userDataPath = await createTmpDir('newlines-persistence-userdata');
|
||||
const collectionPath = await createTmpDir('newlines-persistence-collection');
|
||||
|
||||
// Create collection and request
|
||||
const app1 = await launchElectronApp({ userDataPath });
|
||||
const page = await app1.firstWindow();
|
||||
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('newlines-persistence');
|
||||
await page.getByLabel('Location').fill(collectionPath);
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
const collection = page.locator('.collection-name').filter({ hasText: 'newlines-persistence' });
|
||||
await collection.locator('.collection-actions').hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('persistence-test');
|
||||
await page.locator('#new-request-url').locator('.CodeMirror').click();
|
||||
await page.locator('#new-request-url').locator('textarea').fill('https://httpbin.org/get');
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
await openCollectionAndAcceptSandbox(page, 'newlines-persistence', 'safe');
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
|
||||
|
||||
// Add query param
|
||||
await page.getByRole('tab', { name: 'Params' }).click();
|
||||
await page.getByRole('button', { name: /Add.*Param/i }).click();
|
||||
|
||||
const paramRow = page.locator('table tbody tr').last();
|
||||
await getTableCell(paramRow, 0).locator('input[type="text"]').fill('queryParamKey');
|
||||
|
||||
// Add header with newlines
|
||||
await page.getByRole('tab', { name: 'Headers' }).click();
|
||||
await page.getByRole('button', { name: /Add.*Header/i }).click();
|
||||
|
||||
const headerRow = page.locator('table tbody tr').last();
|
||||
await getTableCell(headerRow, 0).locator('.CodeMirror').click();
|
||||
await getTableCell(headerRow, 0).locator('textarea').fill('headerKey');
|
||||
await getTableCell(headerRow, 1).locator('.CodeMirror').click();
|
||||
await getTableCell(headerRow, 1).locator('textarea').fill('header\nValue');
|
||||
|
||||
// Add Pre Request var with newlines
|
||||
await page.getByRole('tab', { name: 'Vars' }).click();
|
||||
await page.locator('.btn-add-var').first().click();
|
||||
const preReqRow = page.locator('table').first().locator('tbody tr').first();
|
||||
await getTableCell(preReqRow, 0).locator('input[type="text"]').fill('preRequestVar');
|
||||
await getTableCell(preReqRow, 1).locator('.CodeMirror').click();
|
||||
await getTableCell(preReqRow, 1).locator('textarea').fill('pre\nRequest\nValue');
|
||||
|
||||
// Add Post Response var with newlines
|
||||
await page.locator('.btn-add-var').last().click();
|
||||
const postResRow = page.locator('table').nth(1).locator('tbody tr').first();
|
||||
await getTableCell(postResRow, 0).locator('input[type="text"]').fill('postResponseVar');
|
||||
await getTableCell(postResRow, 1).locator('.CodeMirror').click();
|
||||
await getTableCell(postResRow, 1).locator('textarea').fill('post\nResponse\nValue');
|
||||
|
||||
await page.keyboard.press('Meta+s');
|
||||
await app1.close();
|
||||
|
||||
// Verify persistence after restart
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await app2.firstWindow();
|
||||
|
||||
await page2.locator('.collection-name').filter({ hasText: 'newlines-persistence' }).click();
|
||||
await page2.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
|
||||
|
||||
// Verify params persisted
|
||||
await page2.getByRole('tab', { name: 'Params' }).click();
|
||||
await expect(page2.locator('table tbody tr')).toHaveCount(1);
|
||||
|
||||
// Verify headers persisted
|
||||
await page2.getByRole('tab', { name: 'Headers' }).click();
|
||||
await expect(page2.locator('table tbody tr')).toHaveCount(1);
|
||||
|
||||
// Verify vars persisted
|
||||
await page2.getByRole('tab', { name: 'Vars' }).click();
|
||||
await expect(page2.locator('table').first().locator('tbody tr')).toHaveCount(1);
|
||||
await expect(page2.locator('table').nth(1).locator('tbody tr')).toHaveCount(1);
|
||||
|
||||
await app2.close();
|
||||
});
|
||||
@@ -72,6 +72,8 @@ export const buildWebsocketCommonLocators = (page: Page) => ({
|
||||
}
|
||||
});
|
||||
|
||||
export const getTableCell = (row, index) => row.locator('td').nth(index);
|
||||
|
||||
export const buildGrpcCommonLocators = (page: Page) => ({
|
||||
...buildCommonLocators(page),
|
||||
method: {
|
||||
|
||||
Reference in New Issue
Block a user