mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
1 Commits
fix/env-ta
...
fix/e2e-he
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b72fb547a4 |
64
package-lock.json
generated
64
package-lock.json
generated
@@ -14555,18 +14555,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/default-shell": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/default-shell/-/default-shell-2.2.0.tgz",
|
||||
"integrity": "sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
@@ -16005,6 +15993,7 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
@@ -16028,6 +16017,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -18300,6 +18290,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.17.0"
|
||||
@@ -21472,6 +21463,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
@@ -22063,6 +22055,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.0.0"
|
||||
@@ -26690,50 +26683,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-env": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/shell-env/-/shell-env-4.0.2.tgz",
|
||||
"integrity": "sha512-8VJLnsyY//uoDJYl7hBcPdX54x0LaKbbfo5htiv8v/jrR4MD7uRUEom6Cb+S54ugMM9GkBbQJSwlLNCI3VXAHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-shell": "^2.0.0",
|
||||
"execa": "^5.1.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-env/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-env/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
|
||||
@@ -26832,6 +26781,7 @@
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
@@ -27439,6 +27389,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -35374,7 +35325,6 @@
|
||||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"is-ip": "^5.0.1",
|
||||
"shell-env": "^4.0.1",
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
||||
@@ -89,7 +89,7 @@ const EnvironmentVariablesTable = ({
|
||||
}, []);
|
||||
|
||||
const handleTotalHeightChanged = useCallback((h) => {
|
||||
setTableHeight(h + 2);
|
||||
setTableHeight(h);
|
||||
}, []);
|
||||
|
||||
const prevEnvUidRef = useRef(null);
|
||||
@@ -495,14 +495,7 @@ const EnvironmentVariablesTable = ({
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => {
|
||||
formik.setFieldValue(`${actualIndex}.value`, newValue, true);
|
||||
// Clear ephemeral metadata when user manually edits the value
|
||||
if (variable.ephemeral) {
|
||||
formik.setFieldValue(`${actualIndex}.ephemeral`, undefined, false);
|
||||
formik.setFieldValue(`${actualIndex}.persistedValue`, undefined, false);
|
||||
}
|
||||
}}
|
||||
onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ const DotEnvTableView = ({
|
||||
isSaving
|
||||
}) => {
|
||||
const handleTotalHeightChanged = useCallback((h) => {
|
||||
onHeightChange(h + 2);
|
||||
onHeightChange(h);
|
||||
}, [onHeightChange]);
|
||||
|
||||
// Use refs for stable access to formik values in callbacks
|
||||
|
||||
@@ -15,20 +15,20 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
margin: 0.67em 0;
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 2.2em;
|
||||
font-size: 1.4em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.7em;
|
||||
font-size: 1.3em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 1.45em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@@ -38,12 +38,12 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
|
||||
h5 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 0.975em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
@@ -17,22 +15,27 @@ const Script = ({ item, collection }) => {
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const scriptPaneTab = focusedTab?.scriptPaneTab;
|
||||
|
||||
// Default to post-response if pre-request script is empty (only when scriptPaneTab is null/undefined)
|
||||
const getDefaultTab = () => {
|
||||
// Default to post-response if pre-request script is empty
|
||||
const getInitialTab = () => {
|
||||
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
|
||||
return hasPreRequestScript ? 'pre-request' : 'post-response';
|
||||
};
|
||||
|
||||
const activeTab = scriptPaneTab || getDefaultTab();
|
||||
const [activeTab, setActiveTab] = useState(getInitialTab);
|
||||
const prevItemUidRef = useRef(item.uid);
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Update active tab only when switching to a different item
|
||||
useEffect(() => {
|
||||
if (prevItemUidRef.current !== item.uid) {
|
||||
prevItemUidRef.current = item.uid;
|
||||
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
|
||||
setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');
|
||||
}
|
||||
}, [item.uid, requestScript]);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
// Small delay to ensure DOM is updated
|
||||
@@ -73,13 +76,9 @@ const Script = ({ item, collection }) => {
|
||||
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
|
||||
const hasPostResponseScript = responseScript && responseScript.trim().length > 0;
|
||||
|
||||
const onScriptTabChange = (tab) => {
|
||||
dispatch(updateScriptPaneTab({ uid: item.uid, scriptPaneTab: tab }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<Tabs value={activeTab} onValueChange={onScriptTabChange}>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">
|
||||
Pre Request
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconBookmark } from '@tabler/icons';
|
||||
import { addResponseExample } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import { uuid, formatResponse } from 'utils/common';
|
||||
import { uuid } from 'utils/common';
|
||||
import toast from 'react-hot-toast';
|
||||
import CreateExampleModal from 'components/ResponseExample/CreateExampleModal';
|
||||
import { getBodyType } from 'utils/responseBodyProcessor';
|
||||
@@ -83,7 +83,7 @@ const ResponseBookmark = forwardRef(({ item, collection, responseSize, children
|
||||
const contentType = contentTypeHeader?.value?.toLowerCase() || '';
|
||||
|
||||
const bodyType = getBodyType(contentType);
|
||||
const content = formatResponse(response.data, response.dataBuffer, bodyType);
|
||||
const content = response.data;
|
||||
|
||||
const exampleData = {
|
||||
name: name,
|
||||
|
||||
@@ -9,7 +9,7 @@ import ActionIcon from 'ui/ActionIcon/index';
|
||||
const ResponseDownload = forwardRef(({ item, children }, ref) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
const isDisabled = !response.dataBuffer || response.stream?.running;
|
||||
const isDisabled = !response.dataBuffer ? true : false;
|
||||
const elementRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
@@ -39,7 +39,6 @@ import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from '
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
@@ -562,14 +561,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
|
||||
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
const [macRenameKey, winRenameKey] = getKeyBindingsForActionAllOS('renameItem');
|
||||
const renameKey = isMac ? macRenameKey : winRenameKey;
|
||||
|
||||
if (e.key.toLowerCase() === renameKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setRenameItemModalOpen(true);
|
||||
} else if (isModifierPressed && e.key.toLowerCase() === 'c') {
|
||||
if (isModifierPressed && e.key.toLowerCase() === 'c') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyItem();
|
||||
|
||||
@@ -172,7 +172,7 @@ class SingleLineEditor extends Component {
|
||||
// TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098
|
||||
const nextValue = String(this.props.value ?? '');
|
||||
const currentValue = this.editor.getValue();
|
||||
if (this.editor.hasFocus?.() && currentValue !== nextValue && nextValue !== '') {
|
||||
if (this.editor.hasFocus?.() && currentValue !== nextValue) {
|
||||
this.cachedValue = currentValue;
|
||||
} else {
|
||||
const cursor = this.editor.getCursor();
|
||||
|
||||
@@ -99,8 +99,6 @@ const VariablesEditor = ({ collection }) => {
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, runtime variables can only be set via the API - <span className="font-medium">getVar()</span>{' '}
|
||||
and <span className="font-medium">setVar()</span>. <br />
|
||||
You can use the <span className="font-medium">var</span> variable with the
|
||||
<span className="font-medium">{'{{var}}'}</span> syntax.<br />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconBox, IconTrash, IconEdit, IconShare, IconDots, IconX, IconFolder } from '@tabler/icons';
|
||||
import { IconBox, IconTrash, IconEdit, IconShare, IconDots, IconX } from '@tabler/icons';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { mountCollection, showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getRevealInFolderLabel } from 'utils/common/platform';
|
||||
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { normalizePath } from 'utils/common/path';
|
||||
import toast from 'react-hot-toast';
|
||||
import RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';
|
||||
@@ -154,14 +153,6 @@ const CollectionsList = ({ workspace }) => {
|
||||
setDeleteCollectionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleShowInFolder = (collection) => {
|
||||
dropdownRefs.current[collection.uid]?.hide();
|
||||
dispatch(showInFolder(collection.pathname)).catch((error) => {
|
||||
console.error('Error opening the folder', error);
|
||||
toast.error('Error opening the folder');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{renameCollectionModalOpen && selectedCollectionUid && (
|
||||
@@ -210,7 +201,9 @@ const CollectionsList = ({ workspace }) => {
|
||||
<div className="empty-state">
|
||||
<IconBox size={32} strokeWidth={1.5} className="empty-icon" />
|
||||
<h3 className="empty-title">No collections yet</h3>
|
||||
<p className="empty-description">Create your first collection or open an existing one to get started.</p>
|
||||
<p className="empty-description">
|
||||
Create your first collection or open an existing one to get started.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
workspaceCollections.map((collection, index) => (
|
||||
@@ -256,16 +249,6 @@ const CollectionsList = ({ workspace }) => {
|
||||
<IconShare size={16} strokeWidth={1.5} />
|
||||
<span>Share</span>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleShowInFolder(collection);
|
||||
}}
|
||||
>
|
||||
<IconFolder size={16} strokeWidth={1.5} />
|
||||
<span>{getRevealInFolderLabel()}</span>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -395,12 +395,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-family: Inter, sans-serif;
|
||||
font-weight: 400;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.25rem;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
min-height: 1.75rem;
|
||||
max-width: 17.1875rem;
|
||||
max-width: 13.1875rem;
|
||||
}
|
||||
|
||||
/* Value Editor (CodeMirror) */
|
||||
|
||||
@@ -218,7 +218,7 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={closeWithSave}>
|
||||
|
||||
@@ -35,7 +35,7 @@ import toast from 'react-hot-toast';
|
||||
import { useDispatch, useStore } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { addLog } from 'providers/ReduxStore/slices/logs';
|
||||
import { updateSystemResources } from 'providers/ReduxStore/slices/performance';
|
||||
import { apiSpecAddFileEvent, apiSpecChangeFileEvent } from 'providers/ReduxStore/slices/apiSpec';
|
||||
@@ -318,10 +318,6 @@ const useIpcEvents = () => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
const removeCollectionOauth2CredentialsClearListener = ipcRenderer.on('main:credentials-clear', (val) => {
|
||||
dispatch(collectionClearOauth2CredentialsByCredentialsId(val));
|
||||
});
|
||||
|
||||
const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => {
|
||||
dispatch(streamDataReceived(val));
|
||||
});
|
||||
@@ -364,7 +360,6 @@ const useIpcEvents = () => {
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
removeCollectionOauth2CredentialsClearListener();
|
||||
removeHttpStreamNewDataListener();
|
||||
removeHttpStreamEndListener();
|
||||
removeCollectionLoadingStateListener();
|
||||
|
||||
@@ -35,8 +35,7 @@ const KeyMapping = {
|
||||
collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' },
|
||||
zoomIn: { mac: 'command+=', windows: 'ctrl+=', name: 'Zoom In' },
|
||||
zoomOut: { mac: 'command+-', windows: 'ctrl+-', name: 'Zoom Out' },
|
||||
resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' },
|
||||
renameItem: { mac: 'enter', windows: 'f2', name: 'Rename Collection Item' }
|
||||
resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrlAndCredentialsId,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
initRunRequestEvent,
|
||||
updateRunnerConfiguration as _updateRunnerConfiguration,
|
||||
updateActiveConnections,
|
||||
@@ -830,7 +830,7 @@ export const renameItem
|
||||
.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename, collectionPathname: collection.pathname })
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw new Error('Duplicate request names are not allowed under the same folder');
|
||||
throw new Error('Failed to rename the file');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2063,14 +2063,7 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect
|
||||
return reject(new Error('Variable not found'));
|
||||
}
|
||||
|
||||
const updatedVariables = environment.variables.map((v) => {
|
||||
if (v.uid === variable.uid) {
|
||||
// Clear ephemeral metadata when user manually edits the value
|
||||
const { ephemeral, persistedValue, ...rest } = v;
|
||||
return { ...rest, value: newValue };
|
||||
}
|
||||
return v;
|
||||
});
|
||||
const updatedVariables = environment.variables.map((v) => (v.uid === variable.uid ? { ...v, value: newValue } : v));
|
||||
|
||||
return dispatch(saveEnvironment(updatedVariables, environment.uid, collectionUid))
|
||||
.then(() => {
|
||||
@@ -2179,14 +2172,8 @@ export const updateVariableInScope = (variableName, newValue, scopeInfo, collect
|
||||
return reject(new Error('Variable not found'));
|
||||
}
|
||||
|
||||
const updatedVariables = environment.variables.map((v) => {
|
||||
if (v.uid === variable.uid) {
|
||||
// Clear ephemeral metadata when user manually edits the value
|
||||
const { ephemeral, persistedValue, ...rest } = v;
|
||||
return { ...rest, value: newValue };
|
||||
}
|
||||
return v;
|
||||
});
|
||||
const updatedVariables = environment.variables.map((v) =>
|
||||
v.uid === variable.uid ? { ...v, value: newValue } : v);
|
||||
|
||||
return dispatch(saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid }))
|
||||
.then(() => {
|
||||
@@ -2739,43 +2726,11 @@ export const importCollectionFromZip = (zipFilePath, collectionLocation) => asyn
|
||||
return collectionPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates Redux collection order and persists it to the active workspace's workspace.yml.
|
||||
*/
|
||||
export const moveCollectionAndPersist
|
||||
= ({ draggedItem, targetItem }) =>
|
||||
(dispatch, getState) => {
|
||||
const state = getState();
|
||||
const activeWorkspace = state.workspaces.workspaces.find(
|
||||
(w) => w.uid === state.workspaces.activeWorkspaceUid
|
||||
);
|
||||
if (!activeWorkspace?.pathname || !activeWorkspace.collections?.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const workspacePathSet = new Set(
|
||||
activeWorkspace.collections.map((wc) => normalizePath(wc.path))
|
||||
);
|
||||
const collectionsInWorkspace = state.collections.collections
|
||||
.filter((c) => workspacePathSet.has(normalizePath(c.pathname)));
|
||||
if (collectionsInWorkspace.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const reordered = collectionsInWorkspace.filter((i) => i.uid !== draggedItem.uid);
|
||||
const targetIndex = reordered.findIndex((i) => i.uid === targetItem.uid);
|
||||
reordered.splice(targetIndex, 0, draggedItem);
|
||||
const collectionPaths = reordered.map((c) => c.pathname);
|
||||
|
||||
return window.ipcRenderer
|
||||
.invoke('renderer:reorder-workspace-collections', activeWorkspace.pathname, collectionPaths)
|
||||
.then(() => {
|
||||
dispatch(moveCollection({ draggedItem, targetItem }));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to reorder workspace collections', err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
dispatch(moveCollection({ draggedItem, targetItem }));
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
|
||||
@@ -2883,10 +2838,9 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
|
||||
.invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
collectionClearOauth2CredentialsByUrlAndCredentialsId({
|
||||
collectionClearOauth2CredentialsByUrl({
|
||||
url,
|
||||
collectionUid,
|
||||
credentialsId
|
||||
collectionUid
|
||||
})
|
||||
);
|
||||
resolve();
|
||||
|
||||
@@ -3217,8 +3217,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
// Clears a specific credential matching url + collectionUid + credentialsId (used by UI "Clear OAuth2 Cache")
|
||||
collectionClearOauth2CredentialsByUrlAndCredentialsId: (state, action) => {
|
||||
collectionClearOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
@@ -3228,23 +3227,21 @@ export const collectionsSlice = createSlice({
|
||||
const filteredOauth2Credentials = filter(
|
||||
collectionOauth2Credentials,
|
||||
(creds) =>
|
||||
!(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)
|
||||
!(creds.url === url && creds.collectionUid === collectionUid)
|
||||
);
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
}
|
||||
},
|
||||
|
||||
// Clears all credentials matching credentialsId regardless of URL (used by script bru.resetOauth2Credential)
|
||||
collectionClearOauth2CredentialsByCredentialsId: (state, action) => {
|
||||
const { collectionUid, credentialsId } = action.payload;
|
||||
collectionGetOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
if (collection.oauth2Credentials) {
|
||||
collection.oauth2Credentials = collection.oauth2Credentials.filter(
|
||||
(creds) => creds.credentialsId !== credentialsId
|
||||
);
|
||||
}
|
||||
const oauth2Credential = find(
|
||||
collection?.oauth2Credentials || [],
|
||||
(creds) =>
|
||||
creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
@@ -3275,9 +3272,10 @@ export const collectionsSlice = createSlice({
|
||||
timestamp: timestamp || Date.now()
|
||||
});
|
||||
}
|
||||
if (data.dataBuffer) {
|
||||
if (item.response.dataBuffer && item.response.dataBuffer.length && data.dataBuffer) {
|
||||
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
|
||||
}
|
||||
|
||||
item.response.size = data.data?.length + (item.response.size || 0);
|
||||
}
|
||||
},
|
||||
@@ -3679,8 +3677,8 @@ export const {
|
||||
moveCollection,
|
||||
streamDataReceived,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrlAndCredentialsId,
|
||||
collectionClearOauth2CredentialsByCredentialsId,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
addRequestTag,
|
||||
|
||||
@@ -63,7 +63,6 @@ export const tabsSlice = createSlice({
|
||||
responsePaneTab: 'response',
|
||||
responseFormat: null,
|
||||
responseViewTab: null,
|
||||
scriptPaneTab: null,
|
||||
type: type || 'request',
|
||||
preview: preview !== undefined
|
||||
? preview
|
||||
@@ -86,7 +85,6 @@ export const tabsSlice = createSlice({
|
||||
responsePaneScrollPosition: null,
|
||||
responseFormat: null,
|
||||
responseViewTab: null,
|
||||
scriptPaneTab: null,
|
||||
type: type || 'request',
|
||||
...(uid ? { folderUid: uid } : {}),
|
||||
preview: preview !== undefined
|
||||
@@ -173,13 +171,6 @@ export const tabsSlice = createSlice({
|
||||
tab.responseViewTab = action.payload.responseViewTab;
|
||||
}
|
||||
},
|
||||
updateScriptPaneTab: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
if (tab) {
|
||||
tab.scriptPaneTab = action.payload.scriptPaneTab;
|
||||
}
|
||||
},
|
||||
closeTabs: (state, action) => {
|
||||
const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
const tabUids = action.payload.tabUids || [];
|
||||
@@ -273,7 +264,6 @@ export const {
|
||||
updateResponsePaneScrollPosition,
|
||||
updateResponseFormat,
|
||||
updateResponseViewTab,
|
||||
updateScriptPaneTab,
|
||||
closeTabs,
|
||||
closeAllCollectionTabs,
|
||||
makeTabPermanent,
|
||||
|
||||
@@ -24,7 +24,6 @@ const STATIC_API_HINTS = {
|
||||
'req.setHeader(name, value)',
|
||||
'req.setHeaders(data)',
|
||||
'req.deleteHeader(name)',
|
||||
'req.deleteHeaders(data)',
|
||||
'req.getBody()',
|
||||
'req.setBody(data)',
|
||||
'req.setMaxRedirects(maxRedirects)',
|
||||
@@ -116,8 +115,7 @@ const STATIC_API_HINTS = {
|
||||
'bru.cookies.jar().deleteCookie(url, name, callback)',
|
||||
'bru.utils',
|
||||
'bru.utils.minifyJson(json)',
|
||||
'bru.utils.minifyXml(xml)',
|
||||
'bru.resetOauth2Credential(credentialId)'
|
||||
'bru.utils.minifyXml(xml)'
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -383,22 +383,6 @@ describe('Bruno Autocomplete', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should provide deleteHeader and deleteHeaders hints for req.delete prefix', () => {
|
||||
const line = 'req.delete';
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: line.length });
|
||||
mockedCodemirror.getLine.mockReturnValue(line);
|
||||
mockedCodemirror.getRange.mockReturnValue(line);
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining(['deleteHeader(name)', 'deleteHeaders(data)'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive matching', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
||||
|
||||
@@ -91,20 +91,9 @@ const getMaskedDisplay = (value) => {
|
||||
const updateValueDisplay = (valueDisplay, value, isSecret, isMasked, isRevealed) => {
|
||||
if ((isSecret || isMasked) && !isRevealed) {
|
||||
valueDisplay.textContent = getMaskedDisplay(value);
|
||||
return;
|
||||
} else {
|
||||
valueDisplay.textContent = value || '';
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
valueDisplay.textContent = value === null ? 'null' : JSON.stringify(value, null, 2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' || value === undefined) {
|
||||
valueDisplay.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
valueDisplay.textContent = value;
|
||||
};
|
||||
|
||||
// Check if the raw value contains references to secret variables
|
||||
@@ -233,7 +222,7 @@ export const renderVarInfo = (token, options) => {
|
||||
|
||||
// If variable doesn't exist in any scope, determine scope based on context
|
||||
if (!scopeInfo) {
|
||||
if (item && item.uid) {
|
||||
if (item) {
|
||||
// Determine if item is a folder or request
|
||||
const isFolder = item.type === 'folder';
|
||||
|
||||
|
||||
@@ -173,8 +173,8 @@ const curlToJson = (curlCommand) => {
|
||||
requestJson.headers = {};
|
||||
}
|
||||
requestJson.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else if (request.isDataBinary && (typeof request.data === 'string' && request.data.startsWith('@'))) {
|
||||
Object.assign(requestJson, getFilesString(request)); // file case
|
||||
} else if (request.isDataBinary) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
Object.assign(requestJson, getDataString(request));
|
||||
}
|
||||
|
||||
@@ -522,8 +522,8 @@ const cleanRequest = (request) => {
|
||||
* Handles escape sequences, line continuations, and method concatenation
|
||||
*/
|
||||
const cleanCurlCommand = (curlCommand) => {
|
||||
// Handle bash ANSI $'..' escapes by decoding common sequences
|
||||
curlCommand = curlCommand.replace(/\$'((?:\\.|[^'])*)'/g, (match, group) => quoteForShell(decodeAnsiEscapes(group)));
|
||||
// Handle escape sequences
|
||||
curlCommand = curlCommand.replace(/\$('.*')/g, (match, group) => group);
|
||||
// Convert escaped single quotes to shell quote pattern
|
||||
curlCommand = curlCommand.replace(/\\'(?!')/g, '\'\\\'\'');
|
||||
// Fix concatenated HTTP methods
|
||||
@@ -555,33 +555,4 @@ const fixConcatenatedMethods = (command) => {
|
||||
return command;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode bash ANSI $'..' escape sequences
|
||||
*/
|
||||
const decodeAnsiEscapes = (value) => {
|
||||
return value.replace(/\\(\\|'|n|r|t|v|f|a|b|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})/g, (match, seq) => {
|
||||
switch (seq[0]) {
|
||||
case '\\': return '\\';
|
||||
case '\'': return '\'';
|
||||
case 'n': return '\n';
|
||||
case 'r': return '\r';
|
||||
case 't': return '\t';
|
||||
case 'v': return '\v';
|
||||
case 'f': return '\f';
|
||||
case 'a': return '\x07';
|
||||
case 'b': return '\b';
|
||||
case 'x': return String.fromCharCode(parseInt(seq.slice(1), 16));
|
||||
case 'u': return String.fromCharCode(parseInt(seq.slice(1), 16));
|
||||
default: return match;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap value in single quotes while preserving embedded single quotes
|
||||
*/
|
||||
const quoteForShell = (value) => {
|
||||
return `'${value.replace(/'/g, '\'\\\'\'')}'`;
|
||||
};
|
||||
|
||||
export default parseCurlCommand;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const yargs = require('yargs');
|
||||
const chalk = require('chalk');
|
||||
const { initializeShellEnv } = require('@usebruno/requests');
|
||||
|
||||
const { CLI_EPILOGUE, CLI_VERSION } = require('./constants');
|
||||
|
||||
@@ -9,9 +8,6 @@ const printBanner = () => {
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
// Fetch shell environment (useful when CLI is run as subprocess from GUI app or cron)
|
||||
await initializeShellEnv();
|
||||
|
||||
const argLength = process.argv.length;
|
||||
const commandsToPrintBanner = ['--help', '-h'];
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2 } = require('@usebruno/requests');
|
||||
const { getCACertificates, transformProxyConfig } = require('@usebruno/requests');
|
||||
const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2');
|
||||
const tokenStore = require('../store/tokenStore');
|
||||
const { getOAuth2Token } = require('../utils/oauth2');
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
@@ -226,12 +225,6 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (result?.oauth2CredentialsToReset?.length) {
|
||||
for (const credentialId of result.oauth2CredentialsToReset) {
|
||||
tokenStore.deleteCredentialById(credentialId);
|
||||
}
|
||||
}
|
||||
|
||||
if (result?.skipRequest) {
|
||||
return {
|
||||
test: {
|
||||
@@ -640,8 +633,6 @@ const runSingleRequest = async function (
|
||||
console.error('OAuth2 token fetch error:', error.message);
|
||||
}
|
||||
|
||||
request.oauth2CredentialVariables = getFormattedOauth2Credentials();
|
||||
|
||||
// Remove oauth2 config from request to prevent it from being sent
|
||||
delete request.oauth2;
|
||||
}
|
||||
@@ -656,8 +647,7 @@ const runSingleRequest = async function (
|
||||
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
requestMaxRedirects: requestMaxRedirects,
|
||||
disableCookies: options.disableCookies,
|
||||
followRedirects: followRedirects
|
||||
disableCookies: options.disableCookies
|
||||
});
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
@@ -796,12 +786,6 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (result?.oauth2CredentialsToReset?.length) {
|
||||
for (const credentialId of result.oauth2CredentialsToReset) {
|
||||
tokenStore.deleteCredentialById(credentialId);
|
||||
}
|
||||
}
|
||||
|
||||
postResponseTestResults = result?.results || [];
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
} catch (error) {
|
||||
@@ -873,12 +857,6 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (result?.oauth2CredentialsToReset?.length) {
|
||||
for (const credentialId of result.oauth2CredentialsToReset) {
|
||||
tokenStore.deleteCredentialById(credentialId);
|
||||
}
|
||||
}
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
} catch (error) {
|
||||
console.error('Test script execution error:', error);
|
||||
|
||||
@@ -29,15 +29,6 @@ const tokenStore = {
|
||||
return false;
|
||||
},
|
||||
|
||||
// Delete all credentials for a given credentialsId (all URLs)
|
||||
deleteCredentialById(credentialsId) {
|
||||
if (this.credentials[credentialsId]) {
|
||||
delete this.credentials[credentialsId];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Get all stored OAuth2 credentials
|
||||
getAllCredentials() {
|
||||
const result = [];
|
||||
|
||||
@@ -71,7 +71,7 @@ const createRedirectConfig = (error, redirectUrl) => {
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedirects = true } = {}) {
|
||||
function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) {
|
||||
let redirectCount = 0;
|
||||
|
||||
/** @type {axios.AxiosInstance} */
|
||||
@@ -113,14 +113,6 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedi
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
|
||||
if (redirectResponseCodes.includes(error.response.status)) {
|
||||
if (!followRedirects) {
|
||||
if (!disableCookies) {
|
||||
saveCookies(error.config.url, error.response.headers);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
// todo: needs to be discussed whether the original error response message should be modified or not
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -11,7 +11,6 @@ const FORMAT_CONFIG = {
|
||||
yml: { ext: '.yml', collectionFile: 'opencollection.yml', folderFile: 'folder.yml' },
|
||||
bru: { ext: '.bru', collectionFile: 'collection.bru', folderFile: 'folder.bru' }
|
||||
};
|
||||
const REQUEST_ITEM_TYPES = ['http-request', 'graphql-request'];
|
||||
|
||||
const getCollectionFormat = (collectionPath) => {
|
||||
if (fs.existsSync(path.join(collectionPath, 'opencollection.yml'))) return 'yml';
|
||||
@@ -500,7 +499,7 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
if (item.seq) {
|
||||
item.root.meta.seq = item.seq;
|
||||
}
|
||||
const folderContent = stringifyFolder(item.root, { format: 'bru' });
|
||||
const folderContent = await stringifyFolder(item.root);
|
||||
safeWriteFileSync(folderBruFilePath, folderContent);
|
||||
}
|
||||
|
||||
@@ -508,16 +507,17 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
if (item.items && item.items.length) {
|
||||
await processCollectionItems(item.items, folderPath);
|
||||
}
|
||||
} else if (REQUEST_ITEM_TYPES.includes(item.type)) {
|
||||
} else if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
// Create request file
|
||||
let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
|
||||
if (!sanitizedFilename.endsWith('.bru')) {
|
||||
sanitizedFilename += '.bru';
|
||||
}
|
||||
|
||||
// Convert JSON to BRU format based on the item type
|
||||
let type = item.type === 'http-request' ? 'http' : 'graphql';
|
||||
const bruJson = {
|
||||
// Keep schema item type so filestore can stringify request correctly
|
||||
type: item.type,
|
||||
type: type,
|
||||
name: item.name,
|
||||
seq: typeof item.seq === 'number' ? item.seq : 1,
|
||||
tags: item.tags || [],
|
||||
@@ -538,10 +538,8 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
};
|
||||
|
||||
// Convert to BRU format and write to file
|
||||
const content = stringifyRequest(bruJson, { format: 'bru' });
|
||||
const content = await stringifyRequest(bruJson);
|
||||
safeWriteFileSync(path.join(currentPath, sanitizedFilename), content);
|
||||
} else {
|
||||
throw new Error(`Unsupported item type: ${item.type}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { describe, it, expect, afterEach } = require('@jest/globals');
|
||||
const { parseRequest, parseFolder } = require('@usebruno/filestore');
|
||||
const { createCollectionFromBrunoObject } = require('../../../src/utils/collection');
|
||||
|
||||
describe('createCollectionFromBrunoObject', () => {
|
||||
let outputDir;
|
||||
const createOutputDir = () => {
|
||||
outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-cli-import-'));
|
||||
return outputDir;
|
||||
};
|
||||
const parseBruRequestFromPath = (filePath) => parseRequest(fs.readFileSync(filePath, 'utf8'), { format: 'bru' });
|
||||
const parseBruFolderFromPath = (filePath) => parseFolder(fs.readFileSync(filePath, 'utf8'), { format: 'bru' });
|
||||
|
||||
afterEach(() => {
|
||||
if (outputDir && fs.existsSync(outputDir)) {
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('writes http and graphql requests from imported collection items', async () => {
|
||||
createOutputDir();
|
||||
|
||||
await createCollectionFromBrunoObject(
|
||||
{
|
||||
name: 'imported-collection',
|
||||
items: [
|
||||
{
|
||||
type: 'http-request',
|
||||
name: 'Get Users',
|
||||
filename: 'get-users.bru',
|
||||
seq: 1,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'graphql-request',
|
||||
name: 'Get Viewer',
|
||||
filename: 'get-viewer.bru',
|
||||
seq: 2,
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/graphql',
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: 'query { viewer { id } }',
|
||||
variables: '{}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
outputDir
|
||||
);
|
||||
|
||||
const httpPath = path.join(outputDir, 'get-users.bru');
|
||||
const graphqlPath = path.join(outputDir, 'get-viewer.bru');
|
||||
|
||||
expect(fs.existsSync(httpPath)).toBe(true);
|
||||
expect(fs.existsSync(graphqlPath)).toBe(true);
|
||||
|
||||
const httpRequest = parseBruRequestFromPath(httpPath);
|
||||
const graphqlRequest = parseBruRequestFromPath(graphqlPath);
|
||||
|
||||
expect(httpRequest).toHaveProperty('type', 'http-request');
|
||||
expect(httpRequest).toHaveProperty('request.method', 'GET');
|
||||
expect(graphqlRequest).toHaveProperty('type', 'graphql-request');
|
||||
expect(graphqlRequest).toHaveProperty('request.method', 'POST');
|
||||
});
|
||||
|
||||
it('writes folder.bru in bru format', async () => {
|
||||
createOutputDir();
|
||||
|
||||
await createCollectionFromBrunoObject(
|
||||
{
|
||||
name: 'folder-collection',
|
||||
items: [
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Users',
|
||||
seq: 3,
|
||||
root: {
|
||||
meta: { name: 'Users' }
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'http-request',
|
||||
name: 'List Users',
|
||||
filename: 'list-users.bru',
|
||||
seq: 1,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
outputDir
|
||||
);
|
||||
|
||||
const folderPath = path.join(outputDir, 'Users');
|
||||
const folderBruPath = path.join(folderPath, 'folder.bru');
|
||||
const nestedRequestPath = path.join(folderPath, 'list-users.bru');
|
||||
|
||||
expect(fs.existsSync(folderBruPath)).toBe(true);
|
||||
expect(fs.existsSync(nestedRequestPath)).toBe(true);
|
||||
|
||||
const folder = parseBruFolderFromPath(folderBruPath);
|
||||
const nestedRequest = parseBruRequestFromPath(nestedRequestPath);
|
||||
|
||||
expect(folder).toHaveProperty('meta.name', 'Users');
|
||||
expect(folder).toHaveProperty('meta.seq', 3);
|
||||
expect(nestedRequest).toHaveProperty('type', 'http-request');
|
||||
expect(nestedRequest).toHaveProperty('request.method', 'GET');
|
||||
});
|
||||
|
||||
it('throws for unsupported item types', async () => {
|
||||
createOutputDir();
|
||||
|
||||
await expect(
|
||||
createCollectionFromBrunoObject(
|
||||
{
|
||||
name: 'invalid-item-type-collection',
|
||||
items: [
|
||||
{
|
||||
type: 'unsupported-type',
|
||||
name: 'Unsupported'
|
||||
}
|
||||
]
|
||||
},
|
||||
outputDir
|
||||
)
|
||||
).rejects.toThrow('Unsupported item type: unsupported-type');
|
||||
});
|
||||
});
|
||||
@@ -301,7 +301,7 @@ const parseInsomniaCollection = (data) => {
|
||||
export const insomniaToBruno = (insomniaCollection) => {
|
||||
try {
|
||||
if (typeof insomniaCollection !== 'object') {
|
||||
insomniaCollection = jsyaml.load(insomniaCollection, { schema: jsyaml.JSON_SCHEMA });
|
||||
insomniaCollection = jsyaml.load(insomniaCollection);
|
||||
}
|
||||
let collection;
|
||||
if (isInsomniaV5Export(insomniaCollection)) {
|
||||
|
||||
@@ -540,7 +540,6 @@ const transformOpenapiRequestItem = (request, usedNames = new Set(), options = {
|
||||
}).filter((tag) => tag.trim())
|
||||
)],
|
||||
request: {
|
||||
docs: _operationObject.description,
|
||||
url: ensureUrl(request.global.server + path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
|
||||
@@ -24,7 +24,7 @@ const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): Collectio
|
||||
if (brunoConfig.protobuf.importPaths?.length) {
|
||||
config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((p) => {
|
||||
const importPath: { path: string; disabled?: boolean } = { path: p.path };
|
||||
if (p.enabled === false) {
|
||||
if (p.disabled) {
|
||||
importPath.disabled = true;
|
||||
}
|
||||
return importPath;
|
||||
|
||||
@@ -48,7 +48,7 @@ const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
|
||||
})),
|
||||
importPaths: config.protobuf.importPaths?.map((p) => ({
|
||||
path: p.path,
|
||||
enabled: p.disabled !== true
|
||||
disabled: p.disabled || false
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export interface BrunoConfig {
|
||||
};
|
||||
protobuf?: {
|
||||
protoFiles?: { path: string }[];
|
||||
importPaths?: { path: string; enabled?: boolean }[];
|
||||
importPaths?: { path: string; disabled?: boolean }[];
|
||||
};
|
||||
proxy?: {
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -70,7 +70,7 @@ const simpleTranslations = {
|
||||
'req.headers': 'pm.request.headers',
|
||||
'req.body': 'pm.request.body',
|
||||
'req.getHeader': 'pm.request.headers.get',
|
||||
// Note: req.setHeader is handled in complexTransformations because it needs arg restructuring (two args -> object)
|
||||
'req.setHeader': 'pm.request.headers.set',
|
||||
'req.deleteHeader': 'pm.request.headers.remove',
|
||||
|
||||
// URL helper methods
|
||||
@@ -318,28 +318,6 @@ const complexTransformations = [
|
||||
return updateCall;
|
||||
}
|
||||
},
|
||||
// req.setHeader(key, value) -> pm.request.headers.upsert({key: key, value: value})
|
||||
{
|
||||
pattern: 'req.setHeader',
|
||||
transform: (path) => {
|
||||
const args = path.value.arguments;
|
||||
if (!args || args.length < 2) {
|
||||
return j.callExpression(
|
||||
buildMemberExpressionFromString('pm.request.headers.upsert'),
|
||||
args || []
|
||||
);
|
||||
}
|
||||
return j.callExpression(
|
||||
buildMemberExpressionFromString('pm.request.headers.upsert'),
|
||||
[
|
||||
j.objectExpression([
|
||||
j.property('init', j.identifier('key'), args[0]),
|
||||
j.property('init', j.identifier('value'), args[1])
|
||||
])
|
||||
]
|
||||
);
|
||||
}
|
||||
},
|
||||
// req.setHeaders(headers) -> loop calling pm.request.headers.upsert() for each header
|
||||
{
|
||||
pattern: 'req.setHeaders',
|
||||
|
||||
@@ -73,22 +73,10 @@ describe('Bruno to Postman Request Translation', () => {
|
||||
expect(translatedCode).toBe('const contentType = pm.request.headers.get("Content-Type");');
|
||||
});
|
||||
|
||||
it('should translate req.setHeader() to pm.request.headers.upsert() with object arg', () => {
|
||||
it('should translate req.setHeader() to pm.request.headers.set()', () => {
|
||||
const code = 'req.setHeader("Authorization", "Bearer token123");';
|
||||
const translatedCode = translateBruToPostman(code);
|
||||
expect(translatedCode).toContain('pm.request.headers.upsert({\n key: "Authorization",\n value: "Bearer token123"\n})');
|
||||
});
|
||||
|
||||
it('should translate req.deleteHeader() to pm.request.headers.remove()', () => {
|
||||
const code = 'req.deleteHeader("Authorization");';
|
||||
const translatedCode = translateBruToPostman(code);
|
||||
expect(translatedCode).toBe('pm.request.headers.remove("Authorization");');
|
||||
});
|
||||
|
||||
it('should handle req.deleteHeader() with a variable argument', () => {
|
||||
const code = 'const headerName = "X-Custom"; req.deleteHeader(headerName);';
|
||||
const translatedCode = translateBruToPostman(code);
|
||||
expect(translatedCode).toBe('const headerName = "X-Custom"; pm.request.headers.remove(headerName);');
|
||||
expect(translatedCode).toBe('pm.request.headers.set("Authorization", "Bearer token123");');
|
||||
});
|
||||
|
||||
it('should handle all request properties together', () => {
|
||||
|
||||
@@ -33,9 +33,6 @@ collection:
|
||||
isPrivate: false
|
||||
sortKey: -1744194421965
|
||||
method: GET
|
||||
parameters:
|
||||
- name: date
|
||||
value: 2022-10-28
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
@@ -130,14 +127,7 @@ const expectedOutput = {
|
||||
},
|
||||
headers: [],
|
||||
method: 'GET',
|
||||
params: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'date',
|
||||
type: 'query',
|
||||
value: '2022-10-28'
|
||||
}
|
||||
],
|
||||
params: [],
|
||||
url: 'https://testbench-sanity.usebruno.com/ping'
|
||||
},
|
||||
seq: 1,
|
||||
|
||||
@@ -36,14 +36,14 @@ const isBrunoConfigFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'bruno.json';
|
||||
return dirname === collectionPath && basename === 'bruno.json';
|
||||
};
|
||||
|
||||
const isEnvironmentsFolder = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
return path.normalize(dirname) === path.normalize(envDirectory);
|
||||
return dirname === envDirectory;
|
||||
};
|
||||
|
||||
const isFolderRootFile = (pathname, collectionPath) => {
|
||||
@@ -64,7 +64,7 @@ const isCollectionRootFile = (pathname, collectionPath) => {
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
// return if we are not at the root of the collection
|
||||
if (path.normalize(dirname) !== path.normalize(collectionPath)) {
|
||||
if (dirname !== collectionPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (path.normalize(pathname) === path.normalize(envDirectory)) {
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
const basename = path.basename(pathname);
|
||||
const dirname = path.dirname(pathname);
|
||||
|
||||
if (basename === 'opencollection.yml' && path.normalize(dirname) === path.normalize(collectionPath)) {
|
||||
if (basename === 'opencollection.yml' && dirname === collectionPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
|
||||
const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
|
||||
if (path.normalize(pathname) === path.normalize(envDirectory)) {
|
||||
if (pathname === envDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ const path = require('path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const isDev = require('electron-is-dev');
|
||||
const os = require('os');
|
||||
const { initializeShellEnv } = require('@usebruno/requests');
|
||||
|
||||
if (isDev) {
|
||||
if (!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {
|
||||
@@ -156,9 +155,6 @@ if (useSingleInstance && !gotTheLock) {
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
// Ensure shell environment is loaded before any operations that need it
|
||||
await initializeShellEnv();
|
||||
|
||||
if (isDev) {
|
||||
const { installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
|
||||
try {
|
||||
|
||||
@@ -84,27 +84,6 @@ const MAX_COLLECTION_SIZE_IN_MB = 20;
|
||||
const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5;
|
||||
const MAX_COLLECTION_FILES_COUNT = 2000;
|
||||
|
||||
// Get the base directory for transient request files (stored in app data directory)
|
||||
const getTransientDirectoryBase = () => {
|
||||
return path.join(app.getPath('userData'), 'tmp', 'transient');
|
||||
};
|
||||
|
||||
// Get the prefix used for transient collection directories
|
||||
const getTransientCollectionPrefix = () => {
|
||||
return path.join(getTransientDirectoryBase(), 'bruno-');
|
||||
};
|
||||
|
||||
// Get the prefix used for scratch collection directories
|
||||
const getTransientScratchPrefix = () => {
|
||||
return path.join(getTransientDirectoryBase(), 'bruno-scratch-');
|
||||
};
|
||||
|
||||
// Check if a path is within the transient directory
|
||||
const isTransientPath = (filePath) => {
|
||||
const transientBase = getTransientDirectoryBase();
|
||||
return filePath.startsWith(transientBase + path.sep) || filePath.startsWith(transientBase);
|
||||
};
|
||||
|
||||
const envHasSecrets = (environment = {}) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||
|
||||
@@ -112,10 +91,11 @@ const envHasSecrets = (environment = {}) => {
|
||||
};
|
||||
|
||||
const findCollectionPathByItemPath = (filePath) => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const parts = filePath.split(path.sep);
|
||||
const index = parts.findIndex((part) => part.startsWith('bruno-'));
|
||||
|
||||
if (isTransientPath(filePath) && index !== -1) {
|
||||
if (filePath.startsWith(tmpDir) && index !== -1) {
|
||||
const transientDirPath = parts.slice(0, index + 1).join(path.sep);
|
||||
const metadataPath = path.join(transientDirPath, 'metadata.json');
|
||||
try {
|
||||
@@ -141,12 +121,8 @@ const findCollectionPathByItemPath = (filePath) => {
|
||||
// Sort by length descending to find the most specific (deepest) match first
|
||||
const sortedPaths = allCollectionPaths.sort((a, b) => b.length - a.length);
|
||||
|
||||
// Normalize the file path for comparison
|
||||
const normalizedFilePath = path.normalize(filePath);
|
||||
|
||||
for (const collectionPath of sortedPaths) {
|
||||
const normalizedCollectionPath = path.normalize(collectionPath);
|
||||
if (normalizedFilePath.startsWith(normalizedCollectionPath + path.sep) || normalizedFilePath === normalizedCollectionPath) {
|
||||
if (filePath.startsWith(collectionPath + path.sep) || filePath === collectionPath) {
|
||||
return collectionPath;
|
||||
}
|
||||
}
|
||||
@@ -1045,10 +1021,10 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
// This is a simpler handler specifically for cleaning up transient requests
|
||||
// tempDirectory: the collection's temp directory path to validate files belong to this collection
|
||||
ipcMain.handle('renderer:delete-transient-requests', async (event, filePaths, tempDirectory) => {
|
||||
const brunoTempPrefix = getTransientCollectionPrefix();
|
||||
const brunoTempPrefix = path.join(os.tmpdir(), 'bruno-');
|
||||
const results = { deleted: [], skipped: [], errors: [] };
|
||||
|
||||
// Validate tempDirectory is within Bruno transient directory
|
||||
// Validate tempDirectory is within Bruno temp prefix
|
||||
const normalizedTempDir = tempDirectory ? path.normalize(tempDirectory) : null;
|
||||
if (!normalizedTempDir || !normalizedTempDir.startsWith(brunoTempPrefix)) {
|
||||
return { deleted: [], skipped: filePaths.map((p) => ({ path: p, reason: 'Invalid temp directory' })), errors: [] };
|
||||
@@ -1921,12 +1897,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
|
||||
let tempDirectoryPath = null;
|
||||
try {
|
||||
// Ensure the transient base directory exists
|
||||
const transientBase = getTransientDirectoryBase();
|
||||
if (!fs.existsSync(transientBase)) {
|
||||
fs.mkdirSync(transientBase, { recursive: true });
|
||||
}
|
||||
tempDirectoryPath = fs.mkdtempSync(getTransientCollectionPrefix());
|
||||
tempDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-'));
|
||||
const metadata = {
|
||||
collectionPath: collectionPathname
|
||||
};
|
||||
@@ -1955,12 +1926,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
|
||||
ipcMain.handle('renderer:mount-workspace-scratch', async (event, { workspaceUid, workspacePath }) => {
|
||||
try {
|
||||
// Ensure the transient base directory exists
|
||||
const transientBase = getTransientDirectoryBase();
|
||||
if (!fs.existsSync(transientBase)) {
|
||||
fs.mkdirSync(transientBase, { recursive: true });
|
||||
}
|
||||
const tempDirectoryPath = fs.mkdtempSync(getTransientScratchPrefix());
|
||||
const tempDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-scratch-'));
|
||||
registerScratchCollectionPath(tempDirectoryPath);
|
||||
|
||||
const collectionRoot = {
|
||||
|
||||
@@ -128,31 +128,15 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalH
|
||||
|
||||
function onWindowRedirect(url) {
|
||||
// Handle redirects as needed
|
||||
let urlObj;
|
||||
let callbackUrlObj;
|
||||
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch (e) {
|
||||
// Invalid redirect URL, skip processing
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callbackUrlObj = new URL(callbackUrl);
|
||||
} catch (e) {
|
||||
// Invalid callback URL, skip matching but still check for errors below
|
||||
callbackUrlObj = null;
|
||||
}
|
||||
|
||||
// Check if redirect is to the callback URL and contains an authorization code
|
||||
if (callbackUrlObj && matchesCallbackUrl(urlObj, callbackUrlObj)) {
|
||||
if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) {
|
||||
finalUrl = url;
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle OAuth error responses
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.searchParams.has('error')) {
|
||||
const error = urlObj.searchParams.get('error');
|
||||
const errorDescription = urlObj.searchParams.get('error_description');
|
||||
|
||||
@@ -76,8 +76,7 @@ function makeAxiosInstance({
|
||||
proxyConfig = {},
|
||||
requestMaxRedirects = 5,
|
||||
httpsAgentRequestFields = {},
|
||||
interpolationOptions = {},
|
||||
followRedirects = true
|
||||
interpolationOptions = {}
|
||||
} = {}) {
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
@@ -278,14 +277,6 @@ function makeAxiosInstance({
|
||||
// Attach the timeline to the response
|
||||
error.response.timeline = timeline;
|
||||
|
||||
if (!followRedirects) {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
saveCookies(error.config.url, error.response.headers);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
const errorResponseData = error.response.data;
|
||||
timeline?.push({
|
||||
|
||||
@@ -25,7 +25,7 @@ const { chooseFileToSave, writeFile, getCollectionFormat, hasRequestExtension }
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials, clearOauth2CredentialsByCredentialsId } = require('../../utils/oauth2');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
@@ -149,8 +149,7 @@ const configureRequest = async (
|
||||
proxyConfig,
|
||||
requestMaxRedirects,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
followRedirects
|
||||
interpolationOptions
|
||||
});
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
@@ -477,25 +476,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
};
|
||||
};
|
||||
|
||||
const resetOauth2Credentials = ({ oauth2CredentialsToReset, request, collectionUid }) => {
|
||||
if (!oauth2CredentialsToReset?.length) return;
|
||||
for (const credentialId of oauth2CredentialsToReset) {
|
||||
clearOauth2CredentialsByCredentialsId({ collectionUid, credentialsId: credentialId });
|
||||
if (request?.oauth2Credentials?.credentialsId === credentialId) {
|
||||
request.oauth2Credentials = null;
|
||||
}
|
||||
const prefix = `$oauth2.${credentialId}.`;
|
||||
if (request.oauth2CredentialVariables) {
|
||||
for (const key of Object.keys(request.oauth2CredentialVariables)) {
|
||||
if (key.startsWith(prefix)) {
|
||||
delete request.oauth2CredentialVariables[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
mainWindow.webContents.send('main:credentials-clear', { collectionUid, credentialsId: credentialId });
|
||||
}
|
||||
};
|
||||
|
||||
const runPreRequest = async (
|
||||
request,
|
||||
requestUid,
|
||||
@@ -547,8 +527,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid });
|
||||
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
}
|
||||
@@ -687,8 +665,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid });
|
||||
|
||||
const domainsWithCookiesPost = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost)));
|
||||
}
|
||||
@@ -839,7 +815,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) {
|
||||
if (request?.oauth2Credentials) {
|
||||
mainWindow.webContents.send('main:credentials-update', {
|
||||
credentials: request?.oauth2Credentials?.credentials,
|
||||
url: request?.oauth2Credentials?.url,
|
||||
@@ -848,12 +824,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo
|
||||
});
|
||||
|
||||
const { credentialsId, credentials } = request.oauth2Credentials;
|
||||
request.oauth2CredentialVariables = request.oauth2CredentialVariables || {};
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
}
|
||||
|
||||
let response, responseTime, axiosDataStream;
|
||||
@@ -1061,8 +1031,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid });
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
@@ -1518,7 +1486,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collection.globalEnvironmentVariables
|
||||
);
|
||||
|
||||
if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) {
|
||||
if (request?.oauth2Credentials) {
|
||||
mainWindow.webContents.send('main:credentials-update', {
|
||||
credentials: request?.oauth2Credentials?.credentials,
|
||||
url: request?.oauth2Credentials?.url,
|
||||
@@ -1528,12 +1496,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo
|
||||
});
|
||||
|
||||
const { credentialsId, credentials } = request.oauth2Credentials;
|
||||
request.oauth2CredentialVariables = request.oauth2CredentialVariables || {};
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
|
||||
collection.oauth2Credentials = updateCollectionOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
collectionUid,
|
||||
@@ -1775,8 +1737,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid });
|
||||
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-folder-event',
|
||||
basePayload: eventData,
|
||||
|
||||
@@ -20,7 +20,6 @@ const {
|
||||
updateWorkspaceDocs,
|
||||
addCollectionToWorkspace,
|
||||
removeCollectionFromWorkspace,
|
||||
reorderWorkspaceCollections,
|
||||
getWorkspaceCollections,
|
||||
normalizeCollectionEntry,
|
||||
validateWorkspacePath,
|
||||
@@ -191,18 +190,6 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:reorder-workspace-collections', async (event, workspacePath, collectionPaths) => {
|
||||
try {
|
||||
if (!workspacePath) {
|
||||
throw new Error('Workspace path is undefined');
|
||||
}
|
||||
validateWorkspacePath(workspacePath);
|
||||
await reorderWorkspaceCollections(workspacePath, collectionPaths);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-workspace-apispecs', async (event, workspacePath) => {
|
||||
try {
|
||||
if (!workspacePath) {
|
||||
|
||||
@@ -150,23 +150,6 @@ class Oauth2Store {
|
||||
}
|
||||
}
|
||||
|
||||
clearCredentialsByCredentialsId({ collectionUid, credentialsId }) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid });
|
||||
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(
|
||||
(c) => c?.credentialsId !== credentialsId
|
||||
);
|
||||
let newOauth2DataForCollection = {
|
||||
...oauth2DataForCollection,
|
||||
credentials: filteredCredentials
|
||||
};
|
||||
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
|
||||
return newOauth2DataForCollection;
|
||||
} catch (err) {
|
||||
console.log('error clearing oauth2 credentials by credentialsId from cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
clearCredentialsForCollection({ collectionUid, url, credentialsId }) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
|
||||
@@ -442,7 +442,7 @@ const isDotEnvFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return path.normalize(dirname) === path.normalize(collectionPath) && basename === '.env';
|
||||
return dirname === collectionPath && basename === '.env';
|
||||
};
|
||||
|
||||
const isValidDotEnvFilename = (filename) => {
|
||||
@@ -456,7 +456,7 @@ const isBrunoConfigFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'bruno.json';
|
||||
return dirname === collectionPath && basename === 'bruno.json';
|
||||
};
|
||||
|
||||
const isBruEnvironmentConfig = (pathname, collectionPath) => {
|
||||
@@ -464,14 +464,14 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
|
||||
const envDirectory = path.join(collectionPath, 'environments');
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return path.normalize(dirname) === path.normalize(envDirectory) && hasBruExtension(basename);
|
||||
return dirname === envDirectory && hasBruExtension(basename);
|
||||
};
|
||||
|
||||
const isCollectionRootBruFile = (pathname, collectionPath) => {
|
||||
const dirname = path.dirname(pathname);
|
||||
const basename = path.basename(pathname);
|
||||
|
||||
return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'collection.bru';
|
||||
return dirname === collectionPath && basename === 'collection.bru';
|
||||
};
|
||||
|
||||
const scanForBrunoFiles = async (dir) => {
|
||||
|
||||
@@ -25,10 +25,6 @@ const clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
|
||||
oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId });
|
||||
};
|
||||
|
||||
const clearOauth2CredentialsByCredentialsId = ({ collectionUid, credentialsId }) => {
|
||||
oauth2Store.clearCredentialsByCredentialsId({ collectionUid, credentialsId });
|
||||
};
|
||||
|
||||
const getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
|
||||
try {
|
||||
const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });
|
||||
@@ -945,7 +941,6 @@ const updateCollectionOauth2Credentials = ({ collectionUid, itemUid, collectionO
|
||||
module.exports = {
|
||||
persistOauth2Credentials,
|
||||
clearOauth2Credentials,
|
||||
clearOauth2CredentialsByCredentialsId,
|
||||
getStoredOauth2Credentials,
|
||||
getOAuth2TokenUsingAuthorizationCode,
|
||||
getOAuth2TokenUsingClientCredentials,
|
||||
|
||||
@@ -5,9 +5,6 @@ const { writeFile, validateName, isValidCollectionDirectory } = require('./files
|
||||
const { generateUidBasedOnHash } = require('./common');
|
||||
const { withLock, getWorkspaceLockKey } = require('./workspace-lock');
|
||||
|
||||
// Normalize Windows backslash paths to forward slashes for cross-platform compatibility.
|
||||
const posixifyPath = (p) => p.replace(/\\/g, '/');
|
||||
|
||||
const WORKSPACE_TYPE = 'workspace';
|
||||
const OPENCOLLECTION_VERSION = '1.0.0';
|
||||
|
||||
@@ -97,7 +94,7 @@ const sanitizeCollections = (collections) => {
|
||||
}).map((collection) => {
|
||||
const sanitized = {
|
||||
name: collection.name.trim(),
|
||||
path: posixifyPath(collection.path.trim())
|
||||
path: collection.path.trim()
|
||||
};
|
||||
|
||||
if (collection.remote && typeof collection.remote === 'string') {
|
||||
@@ -121,32 +118,26 @@ const sanitizeSpecs = (specs) => {
|
||||
return true;
|
||||
}).map((spec) => ({
|
||||
name: spec.name.trim(),
|
||||
path: posixifyPath(spec.path.trim())
|
||||
path: spec.path.trim()
|
||||
}));
|
||||
};
|
||||
|
||||
const makeRelativePath = (workspacePath, absolutePath) => {
|
||||
if (!path.isAbsolute(absolutePath)) {
|
||||
return posixifyPath(absolutePath);
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
try {
|
||||
const relativePath = path.relative(workspacePath, absolutePath);
|
||||
if (relativePath.startsWith('..') && relativePath.split(path.sep).filter((s) => s === '..').length > 2) {
|
||||
return posixifyPath(absolutePath);
|
||||
return absolutePath;
|
||||
}
|
||||
return posixifyPath(relativePath);
|
||||
return relativePath;
|
||||
} catch (error) {
|
||||
return posixifyPath(absolutePath);
|
||||
return absolutePath;
|
||||
}
|
||||
};
|
||||
|
||||
const getNormalizedAbsoluteCollectionPath = (workspacePath, collection) => {
|
||||
if (!collection?.path) return null;
|
||||
const resolved = path.isAbsolute(collection.path) ? collection.path : path.resolve(workspacePath, collection.path);
|
||||
return path.normalize(resolved);
|
||||
};
|
||||
|
||||
const normalizeCollectionEntry = (workspacePath, collection) => {
|
||||
const relativePath = makeRelativePath(workspacePath, collection.path);
|
||||
|
||||
@@ -344,14 +335,14 @@ const addCollectionToWorkspace = async (workspacePath, collection) => {
|
||||
|
||||
const normalizedCollection = {
|
||||
name: collection.name.trim(),
|
||||
path: posixifyPath(collection.path.trim())
|
||||
path: collection.path.trim()
|
||||
};
|
||||
|
||||
if (collection.remote && typeof collection.remote === 'string') {
|
||||
normalizedCollection.remote = collection.remote.trim();
|
||||
}
|
||||
|
||||
const existingIndex = config.collections.findIndex((c) => c.path && posixifyPath(c.path) === normalizedCollection.path);
|
||||
const existingIndex = config.collections.findIndex((c) => c.path === normalizedCollection.path);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
config.collections[existingIndex] = normalizedCollection;
|
||||
@@ -372,7 +363,7 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
let removedCollection = null;
|
||||
|
||||
config.collections = (config.collections || []).filter((c) => {
|
||||
const collectionPathFromYml = c.path ? posixifyPath(c.path) : c.path;
|
||||
const collectionPathFromYml = c.path;
|
||||
|
||||
if (!collectionPathFromYml) {
|
||||
return true;
|
||||
@@ -400,43 +391,6 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reorders the collections array in the workspace's workspace.yml to match the given path list.
|
||||
* Entries not in the list are appended at the end.
|
||||
* @param {string} workspacePath - Absolute path to the workspace directory
|
||||
* @param {string[]} collectionPaths - Absolute collection pathnames in the desired order
|
||||
*/
|
||||
const reorderWorkspaceCollections = async (workspacePath, collectionPaths) => {
|
||||
if (!Array.isArray(collectionPaths)) {
|
||||
throw new Error('collectionPaths must be an array');
|
||||
}
|
||||
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
const existing = config.collections || [];
|
||||
|
||||
const inNewOrder = [];
|
||||
const matched = new Set();
|
||||
|
||||
for (const absolutePath of collectionPaths) {
|
||||
const targetPath = posixifyPath(path.normalize(absolutePath));
|
||||
const entry = existing.find(
|
||||
(c) => posixifyPath(getNormalizedAbsoluteCollectionPath(workspacePath, c)) === targetPath
|
||||
);
|
||||
if (entry && !matched.has(entry)) {
|
||||
inNewOrder.push(entry);
|
||||
matched.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const notInList = existing.filter((c) => !matched.has(c));
|
||||
config.collections = [...inNewOrder, ...notInList];
|
||||
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkspaceCollections = (workspacePath) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
const collections = config.collections || [];
|
||||
@@ -444,14 +398,13 @@ const getWorkspaceCollections = (workspacePath) => {
|
||||
const seenPaths = new Set();
|
||||
return collections
|
||||
.map((collection) => {
|
||||
const collectionPath = collection.path ? posixifyPath(collection.path) : collection.path;
|
||||
if (collectionPath && !path.isAbsolute(collectionPath)) {
|
||||
if (collection.path && !path.isAbsolute(collection.path)) {
|
||||
return {
|
||||
...collection,
|
||||
path: path.resolve(workspacePath, collectionPath)
|
||||
path: path.resolve(workspacePath, collection.path)
|
||||
};
|
||||
}
|
||||
return { ...collection, path: collectionPath };
|
||||
return collection;
|
||||
})
|
||||
.filter((collection) => {
|
||||
if (!collection.path) {
|
||||
@@ -474,14 +427,13 @@ const getWorkspaceApiSpecs = (workspacePath) => {
|
||||
const specs = config.specs || [];
|
||||
|
||||
return specs.map((spec) => {
|
||||
const specPath = spec.path ? posixifyPath(spec.path) : spec.path;
|
||||
if (specPath && !path.isAbsolute(specPath)) {
|
||||
if (spec.path && !path.isAbsolute(spec.path)) {
|
||||
return {
|
||||
...spec,
|
||||
path: path.join(workspacePath, specPath)
|
||||
path: path.join(workspacePath, spec.path)
|
||||
};
|
||||
}
|
||||
return { ...spec, path: specPath };
|
||||
return spec;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -503,7 +455,7 @@ const addApiSpecToWorkspace = async (workspacePath, apiSpec) => {
|
||||
};
|
||||
|
||||
const existingIndex = config.specs.findIndex(
|
||||
(a) => a.name === normalizedSpec.name || (a.path && posixifyPath(a.path) === normalizedSpec.path)
|
||||
(a) => a.name === normalizedSpec.name || a.path === normalizedSpec.path
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
@@ -529,7 +481,7 @@ const removeApiSpecFromWorkspace = async (workspacePath, apiSpecPath) => {
|
||||
let removedApiSpec = null;
|
||||
|
||||
config.specs = config.specs.filter((a) => {
|
||||
const specPathFromYml = a.path ? posixifyPath(a.path) : a.path;
|
||||
const specPathFromYml = a.path;
|
||||
if (!specPathFromYml) return true;
|
||||
|
||||
const absoluteSpecPath = path.isAbsolute(specPathFromYml)
|
||||
@@ -576,7 +528,6 @@ module.exports = {
|
||||
updateWorkspaceDocs,
|
||||
addCollectionToWorkspace,
|
||||
removeCollectionFromWorkspace,
|
||||
reorderWorkspaceCollections,
|
||||
getWorkspaceCollections,
|
||||
getWorkspaceApiSpecs,
|
||||
addApiSpecToWorkspace,
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const yaml = require('js-yaml');
|
||||
const { reorderWorkspaceCollections } = require('../../src/utils/workspace-config');
|
||||
|
||||
const collection = (name, pathSegment) => ({ name, path: pathSegment });
|
||||
|
||||
describe('reorderWorkspaceCollections', () => {
|
||||
let workspacePath;
|
||||
|
||||
/** Writes workspace.yml with the given collections (relative paths). */
|
||||
const writeWorkspaceYml = (collections) => {
|
||||
const content = [
|
||||
'opencollection: 1.0.0',
|
||||
'info:',
|
||||
' name: Test',
|
||||
' type: workspace',
|
||||
'collections:',
|
||||
...collections.flatMap((c) => [` - name: ${c.name}`, ` path: ${c.path}`]),
|
||||
'specs: []',
|
||||
'docs: \'\''
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), content);
|
||||
};
|
||||
|
||||
/** Returns collection paths (relative) in order as stored in workspace.yml. */
|
||||
const getCollectionPathsFromYml = () => {
|
||||
const raw = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');
|
||||
const config = yaml.load(raw);
|
||||
return (config.collections || []).map((c) => c.path);
|
||||
};
|
||||
|
||||
/** Resolves a relative collection path segment to an absolute path under the current workspace. */
|
||||
const absPath = (relativePath) => path.resolve(workspacePath, relativePath);
|
||||
|
||||
beforeEach(() => {
|
||||
workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-ws-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(workspacePath, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('reorders collections to match given path list', async () => {
|
||||
writeWorkspaceYml([
|
||||
collection('API', 'collections/api'),
|
||||
collection('Backend', 'collections/backend'),
|
||||
collection('Frontend', 'collections/frontend')
|
||||
]);
|
||||
|
||||
await reorderWorkspaceCollections(workspacePath, [
|
||||
absPath('collections/frontend'),
|
||||
absPath('collections/api'),
|
||||
absPath('collections/backend')
|
||||
]);
|
||||
|
||||
expect(getCollectionPathsFromYml()).toEqual(['collections/frontend', 'collections/api', 'collections/backend']);
|
||||
});
|
||||
|
||||
test('deduplicates when reorder list contains duplicate paths', async () => {
|
||||
writeWorkspaceYml([
|
||||
collection('API', 'collections/api'),
|
||||
collection('Backend', 'collections/backend')
|
||||
]);
|
||||
|
||||
await reorderWorkspaceCollections(workspacePath, [
|
||||
absPath('collections/api'),
|
||||
absPath('collections/backend'),
|
||||
absPath('collections/api'),
|
||||
absPath('collections/api')
|
||||
]);
|
||||
|
||||
expect(getCollectionPathsFromYml()).toEqual(['collections/api', 'collections/backend']);
|
||||
});
|
||||
});
|
||||
@@ -58,12 +58,12 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
||||
// protobuf
|
||||
if (oc.config?.protobuf) {
|
||||
brunoConfig.protobuf = {
|
||||
protoFiles: oc.config.protobuf.protoFiles?.map((protoFile: any) => ({
|
||||
protofFiles: oc.config.protobuf.protoFiles?.map((protoFile: any) => ({
|
||||
path: protoFile.path
|
||||
})) || [],
|
||||
importPaths: oc.config.protobuf.importPaths?.map((importPath: any) => ({
|
||||
path: importPath.path,
|
||||
enabled: importPath.disabled !== true
|
||||
disabled: importPath.disabled || false
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { Auth } from '@opencollection/types/common/auth';
|
||||
const hasCollectionConfig = (brunoConfig: any): boolean => {
|
||||
// protobuf
|
||||
const hasProtobuf = (
|
||||
brunoConfig.protobuf?.protoFiles?.length > 0
|
||||
brunoConfig.protobuf?.protofFiles?.length > 0
|
||||
|| brunoConfig.protobuf?.importPaths?.length > 0
|
||||
);
|
||||
|
||||
@@ -77,25 +77,17 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
||||
if (hasCollectionConfig(brunoConfig)) {
|
||||
oc.config = {};
|
||||
|
||||
if (brunoConfig.protobuf?.protoFiles?.length || brunoConfig.protobuf?.importPaths?.length) {
|
||||
oc.config.protobuf = {};
|
||||
|
||||
if (brunoConfig.protobuf.protoFiles?.length) {
|
||||
oc.config.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((protoFile: any): ProtoFileItem => ({
|
||||
if (brunoConfig.protobuf?.protofFiles?.length) {
|
||||
oc.config.protobuf = {
|
||||
protoFiles: brunoConfig.protobuf.protofFiles.map((protoFile: any): ProtoFileItem => ({
|
||||
type: 'file' as const,
|
||||
path: protoFile.path
|
||||
}));
|
||||
}
|
||||
|
||||
if (brunoConfig.protobuf.importPaths?.length) {
|
||||
oc.config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((importPath: any): ProtoFileImportPath => {
|
||||
const item: ProtoFileImportPath = { path: importPath.path };
|
||||
if (importPath.enabled === false) {
|
||||
item.disabled = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
})),
|
||||
importPaths: brunoConfig.protobuf.importPaths.map((importPath: any): ProtoFileImportPath => ({
|
||||
path: importPath.path,
|
||||
disabled: importPath.disabled
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// proxy - only write newer format
|
||||
|
||||
@@ -92,8 +92,6 @@ class Bru {
|
||||
};
|
||||
// Holds variables that are marked as persistent by scripts
|
||||
this.persistentEnvVariables = {};
|
||||
// Holds credential IDs to be reset after script execution
|
||||
this.oauth2CredentialsToReset = [];
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
@@ -277,24 +275,6 @@ class Bru {
|
||||
return this.interpolate(this.oauth2CredentialVariables[key]);
|
||||
}
|
||||
|
||||
resetOauth2Credential(credentialId) {
|
||||
if (!credentialId || typeof credentialId !== 'string') {
|
||||
throw new Error('credentialId must be a non-empty string');
|
||||
}
|
||||
|
||||
if (!this.oauth2CredentialsToReset.includes(credentialId)) {
|
||||
this.oauth2CredentialsToReset.push(credentialId);
|
||||
}
|
||||
|
||||
// Remove matching credential variables so subsequent getOauth2CredentialVar() calls return undefined
|
||||
const prefix = `$oauth2.${credentialId}.`;
|
||||
for (const key of Object.keys(this.oauth2CredentialVariables)) {
|
||||
if (key.startsWith(prefix)) {
|
||||
delete this.oauth2CredentialVariables[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasVar(key) {
|
||||
return Object.hasOwn(this.runtimeVariables, key);
|
||||
}
|
||||
|
||||
@@ -125,13 +125,6 @@ class BrunoRequest {
|
||||
this.req.headers = headers;
|
||||
}
|
||||
|
||||
deleteHeaders(headers) {
|
||||
headers.forEach((name) => {
|
||||
delete this.headers[name];
|
||||
delete this.req.headers[name];
|
||||
});
|
||||
}
|
||||
|
||||
getHeader(name) {
|
||||
return this.req.headers[name];
|
||||
}
|
||||
|
||||
@@ -13,12 +13,7 @@ chai.use(function (chai, utils) {
|
||||
// Custom assertion for checking if a variable is JSON
|
||||
chai.Assertion.addProperty('json', function () {
|
||||
const obj = this._obj;
|
||||
// Use Object.prototype.toString instead of constructor check for cross-realm compatibility.
|
||||
// Objects created inside Node's vm.createContext() have a different Object constructor,
|
||||
// so obj.constructor === Object fails for objects passed via res.setBody() from scripts.
|
||||
// Note: toString check is more permissive than constructor check — custom class instances
|
||||
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
||||
&& Object.prototype.toString.call(obj) === '[object Object]';
|
||||
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) && obj.constructor === Object;
|
||||
|
||||
this.assert(isJson, `expected ${utils.inspect(obj)} to be JSON`, `expected ${utils.inspect(obj)} not to be JSON`);
|
||||
});
|
||||
|
||||
@@ -76,7 +76,6 @@ class ScriptRuntime {
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
@@ -194,7 +193,6 @@ class ScriptRuntime {
|
||||
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
|
||||
@@ -27,14 +27,13 @@ class TestRuntime {
|
||||
collectionName
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables, certsAndProxyConfig);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
@@ -110,7 +109,6 @@ class TestRuntime {
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
||||
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest
|
||||
};
|
||||
|
||||
@@ -89,12 +89,6 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar);
|
||||
getOauth2CredentialVar.dispose();
|
||||
|
||||
let resetOauth2Credential = vm.newFunction('resetOauth2Credential', function (credentialId) {
|
||||
bru.resetOauth2Credential(vm.dump(credentialId));
|
||||
});
|
||||
vm.setProp(bruObject, 'resetOauth2Credential', resetOauth2Credential);
|
||||
resetOauth2Credential.dispose();
|
||||
|
||||
let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
|
||||
bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));
|
||||
});
|
||||
|
||||
@@ -102,12 +102,6 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
||||
vm.setProp(reqObject, 'setHeaders', setHeaders);
|
||||
setHeaders.dispose();
|
||||
|
||||
let deleteHeaders = vm.newFunction('deleteHeaders', function (headers) {
|
||||
req.deleteHeaders(vm.dump(headers));
|
||||
});
|
||||
vm.setProp(reqObject, 'deleteHeaders', deleteHeaders);
|
||||
deleteHeaders.dispose();
|
||||
|
||||
let getHeader = vm.newFunction('getHeader', function (name) {
|
||||
return marshallToVm(req.getHeader(vm.dump(name)), vm);
|
||||
});
|
||||
@@ -120,8 +114,8 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
||||
vm.setProp(reqObject, 'setHeader', setHeader);
|
||||
setHeader.dispose();
|
||||
|
||||
let deleteHeader = vm.newFunction('deleteHeader', function (header) {
|
||||
req.deleteHeader(vm.dump(header));
|
||||
let deleteHeader = vm.newFunction('deleteHeader', function (name) {
|
||||
req.deleteHeader(vm.dump(name));
|
||||
});
|
||||
vm.setProp(reqObject, 'deleteHeader', deleteHeader);
|
||||
deleteHeader.dispose();
|
||||
|
||||
@@ -58,27 +58,6 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
||||
globalThis.test = Test(__brunoTestResults);
|
||||
`
|
||||
);
|
||||
|
||||
// Register custom chai assertion for isJson (expect(...).to.be.json)
|
||||
// The bundled chai only exposes { expect, assert } — no Assertion class.
|
||||
// Access the prototype through an expect() instance instead.
|
||||
vm.evalCode(
|
||||
`
|
||||
(function() {
|
||||
var proto = Object.getPrototypeOf(expect(null));
|
||||
Object.defineProperty(proto, 'json', {
|
||||
get: function () {
|
||||
var obj = this._obj;
|
||||
var isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) &&
|
||||
Object.prototype.toString.call(obj) === '[object Object]';
|
||||
this.assert(isJson, 'expected #{this} to be JSON', 'expected #{this} not to be JSON');
|
||||
return this;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
})();
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const TestRuntime = require('../src/runtime/test-runtime');
|
||||
const ScriptRuntime = require('../src/runtime/script-runtime');
|
||||
const AssertRuntime = require('../src/runtime/assert-runtime');
|
||||
const Bru = require('../src/bru');
|
||||
const VarsRuntime = require('../src/runtime/vars-runtime');
|
||||
|
||||
@@ -259,87 +258,4 @@ describe('runtime', () => {
|
||||
expect(result.runtimeVariables.title).toBe('{{$randomFirstName}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assert-runtime', () => {
|
||||
const baseRequest = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:3000/',
|
||||
headers: {},
|
||||
data: undefined
|
||||
};
|
||||
|
||||
const makeResponse = (data) => ({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
data,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
const runAssertions = (assertions, response, runtime = 'nodevm') => {
|
||||
const assertRuntime = new AssertRuntime({ runtime });
|
||||
return assertRuntime.runAssertions(assertions, { ...baseRequest }, response, {}, {}, process.env);
|
||||
};
|
||||
|
||||
describe('isJson', () => {
|
||||
it('should pass for a plain object', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse({ id: 1, name: 'test' })
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should pass for a nested object', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse({ user: { id: 1, tags: ['a', 'b'] } })
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should pass for objects from a different realm (e.g. after res.setBody in node-vm)', async () => {
|
||||
const response = makeResponse({ id: 1, name: 'original' });
|
||||
|
||||
// res.setBody() inside node-vm creates a cross-realm object whose
|
||||
// constructor is the VM's Object, not the host's Object
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: 'nodevm' });
|
||||
await scriptRuntime.runResponseScript(
|
||||
`res.setBody({ id: 2, name: 'updated' });`,
|
||||
{ ...baseRequest },
|
||||
response,
|
||||
{}, {}, '.', null, process.env
|
||||
);
|
||||
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
response
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should fail for an array', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse([1, 2, 3])
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
|
||||
it('should fail for a string', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse('hello')
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
|
||||
it('should fail for null', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse(null)
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3",
|
||||
"shell-env": "^4.0.1"
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
|
||||
@@ -39,6 +39,6 @@ module.exports = [
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
],
|
||||
external: (id) => isBuiltin(id) || ['axios', 'qs', 'ws', 'debug', 'shell-env'].includes(id)
|
||||
external: (id) => isBuiltin(id) || ['axios', 'qs', 'ws', 'debug'].includes(id)
|
||||
}
|
||||
];
|
||||
|
||||
@@ -8,7 +8,6 @@ export { transformProxyConfig } from './utils/proxy-util';
|
||||
export { default as createVaultClient, VaultError } from './utils/node-vault';
|
||||
export type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault';
|
||||
export { getHttpHttpsAgents } from './utils/http-https-agents';
|
||||
export { initializeShellEnv } from './utils/shell-env';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Shell Environment Utility
|
||||
*
|
||||
* Fetches environment variables from the user's shell configuration files (e.g., .zshenv, .bashrc)
|
||||
*/
|
||||
|
||||
const fetchShellEnv = async (): Promise<Record<string, string>> => {
|
||||
// Windows handles environment variables differently - skip
|
||||
if (process.platform === 'win32') {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
// shell-env is ESM-only, so we use dynamic import
|
||||
const { shellEnv } = await import('shell-env');
|
||||
const env = await shellEnv();
|
||||
return env;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes process.env with shell environment variables.
|
||||
* Should be called early in the app startup.
|
||||
*
|
||||
* @returns The fetched shell environment variables
|
||||
*/
|
||||
export const initializeShellEnv = async (): Promise<Record<string, string>> => {
|
||||
const shellEnvVars = await fetchShellEnv();
|
||||
Object.assign(process.env, shellEnvVars);
|
||||
return shellEnvVars;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
meta {
|
||||
name: deleteHeaders
|
||||
type: http
|
||||
seq: 13
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
X-Frame-Options: 1
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.body: eq pong
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
req.deleteHeaders(['X-Frame-Options']);
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.deleteHeaders(names)", function() {
|
||||
const h = req.getHeaders();
|
||||
expect(h["x-frame-options"]).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
meta {
|
||||
name: isJson after setBody
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "bruno"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.body: isJson
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
res.setBody({ id: 1, name: "updated", nested: { key: "value" } });
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.body should be json after setBody with object", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.be.json;
|
||||
expect(body.id).to.eql(1);
|
||||
expect(body.name).to.eql("updated");
|
||||
expect(body.nested.key).to.eql("value");
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test as baseTest, BrowserContext, ElectronApplication, Page, TestInfo } from '@playwright/test';
|
||||
import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
@@ -25,89 +25,19 @@ async function recursiveCopy(src: string, dest: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const TRACING_OPTIONS = { screenshots: true, snapshots: true, sources: true };
|
||||
|
||||
function isTracingEnabled(testInfo: TestInfo): boolean {
|
||||
return !!(testInfo as any)._tracing.traceOptions();
|
||||
}
|
||||
|
||||
async function usePageWithTracing(
|
||||
context: BrowserContext,
|
||||
page: Page,
|
||||
testInfo: TestInfo,
|
||||
use: (page: Page) => Promise<void>,
|
||||
options: { initTracing?: boolean; useChunks?: boolean } = {}
|
||||
) {
|
||||
const { initTracing = false, useChunks = true } = options;
|
||||
|
||||
if (!isTracingEnabled(testInfo)) {
|
||||
await use(page);
|
||||
return;
|
||||
}
|
||||
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
|
||||
if (initTracing) {
|
||||
try {
|
||||
await context.tracing.start(TRACING_OPTIONS);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (useChunks) {
|
||||
await context.tracing.startChunk();
|
||||
await use(page);
|
||||
try { await context.tracing.stopChunk({ path: tracePath }); } catch { }
|
||||
} else {
|
||||
await use(page);
|
||||
try { await context.tracing.stop({ path: tracePath }); } catch { }
|
||||
}
|
||||
|
||||
try { await testInfo.attach('trace', { path: tracePath }); } catch { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully close an Electron app by telling it to exit with code 0.
|
||||
* This avoids the macOS "quit unexpectedly" crash dialog that appears when
|
||||
* app.context().close() kills subprocesses (renderer/GPU) abruptly before
|
||||
* the main process can shut down cleanly.
|
||||
*
|
||||
* Emits 'before-quit' first so cleanup handlers run (e.g., saving cookies to disk),
|
||||
* since app.exit() bypasses all lifecycle events.
|
||||
*/
|
||||
export async function closeElectronApp(app: ElectronApplication) {
|
||||
try {
|
||||
await app.evaluate(async ({ app }) => {
|
||||
app.emit('before-quit');
|
||||
|
||||
// Add a delay to ensure the app is fully closed
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
app.exit(0);
|
||||
});
|
||||
} catch {
|
||||
// Expected: process exited before the CDP response was sent
|
||||
}
|
||||
|
||||
try {
|
||||
await app.close();
|
||||
} catch {
|
||||
// Process already exited
|
||||
}
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<
|
||||
{
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
newPage: Page;
|
||||
pageWithUserData: Page;
|
||||
collectionFixturePath: string | null;
|
||||
restartApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
|
||||
},
|
||||
{
|
||||
createTmpDir: (tag?: string) => Promise<string>;
|
||||
launchElectronApp: (options?: { initUserDataPath?: string; userDataPath?: string; dotEnv?: Record<string, string>; templateVars?: Record<string, string> }) => Promise<ElectronApplication>;
|
||||
launchElectronApp: (options?: { initUserDataPath?: string; userDataPath?: string; dotEnv?: Record<string, string> }) => Promise<ElectronApplication>;
|
||||
electronApp: ElectronApplication;
|
||||
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string; testFile?: string; userDataPath?: string; dotEnv?: Record<string, string>; templateVars?: Record<string, string>; closePrevious?: boolean }) => Promise<ElectronApplication>;
|
||||
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string; testFile?: string; userDataPath?: string; dotEnv?: Record<string, string> }) => Promise<ElectronApplication>;
|
||||
}
|
||||
>({
|
||||
createTmpDir: [
|
||||
@@ -125,27 +55,10 @@ export const test = baseTest.extend<
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
collectionFixturePath: async ({ createTmpDir }, use, testInfo) => {
|
||||
const testDir = path.dirname(testInfo.file);
|
||||
const fixturesDir = path.join(testDir, 'fixtures');
|
||||
// fixtures/collections — multiple named collections (subdirs with bruno.json/opencollection.yml)
|
||||
// fixtures/collection — single collection (single dir with bruno.json/opencollection.yml)
|
||||
const srcPath = [path.join(fixturesDir, 'collections'), path.join(fixturesDir, 'collection')]
|
||||
.find((p) => fs.existsSync(p));
|
||||
|
||||
if (srcPath) {
|
||||
const tmpDir = await createTmpDir(path.basename(srcPath));
|
||||
await fs.promises.cp(srcPath, tmpDir, { recursive: true });
|
||||
await use(tmpDir);
|
||||
} else {
|
||||
await use(null);
|
||||
}
|
||||
},
|
||||
|
||||
launchElectronApp: [
|
||||
async ({ playwright, createTmpDir }, use, workerInfo) => {
|
||||
const apps: ElectronApplication[] = [];
|
||||
await use(async ({ initUserDataPath, userDataPath: providedUserDataPath, dotEnv = {}, templateVars = {} } = {}) => {
|
||||
await use(async ({ initUserDataPath, userDataPath: providedUserDataPath, dotEnv = {} } = {}) => {
|
||||
const userDataPath = providedUserDataPath || (await createTmpDir('electron-userdata'));
|
||||
|
||||
// Ensure dir exists when caller supplies their own path
|
||||
@@ -154,9 +67,8 @@ export const test = baseTest.extend<
|
||||
}
|
||||
|
||||
if (initUserDataPath) {
|
||||
const replacements: Record<string, string> = {
|
||||
projectRoot: path.posix.join(__dirname, '..'),
|
||||
...templateVars
|
||||
const replacements = {
|
||||
projectRoot: path.posix.join(__dirname, '..')
|
||||
};
|
||||
|
||||
for (const file of await fs.promises.readdir(initUserDataPath)) {
|
||||
@@ -201,7 +113,8 @@ export const test = baseTest.extend<
|
||||
return app;
|
||||
});
|
||||
for (const app of apps) {
|
||||
await closeElectronApp(app);
|
||||
await app.context().close();
|
||||
await app.close();
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
@@ -217,9 +130,10 @@ export const test = baseTest.extend<
|
||||
|
||||
context: async ({ electronApp }, use, testInfo) => {
|
||||
const context = await electronApp.context();
|
||||
if (isTracingEnabled(testInfo)) {
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
try {
|
||||
await context.tracing.start(TRACING_OPTIONS);
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
} catch (e) { }
|
||||
}
|
||||
await use(context);
|
||||
@@ -227,39 +141,43 @@ export const test = baseTest.extend<
|
||||
|
||||
page: async ({ electronApp, context }, use, testInfo) => {
|
||||
const page = await electronApp.firstWindow();
|
||||
await usePageWithTracing(context, page, testInfo, use);
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
await context.tracing.startChunk();
|
||||
await use(page);
|
||||
await context.tracing.stopChunk({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
},
|
||||
|
||||
newPage: async ({ launchElectronApp }, use, testInfo) => {
|
||||
const app = await launchElectronApp();
|
||||
const context = await app.context();
|
||||
const page = await app.firstWindow();
|
||||
await usePageWithTracing(context, page, testInfo, use, { initTracing: true, useChunks: false });
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
await use(page);
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
},
|
||||
|
||||
reuseOrLaunchElectronApp: [
|
||||
async ({ launchElectronApp }, use, testInfo) => {
|
||||
const apps: Record<string, ElectronApplication> = {};
|
||||
await use(async ({ initUserDataPath, testFile, userDataPath, dotEnv = {}, templateVars = {}, closePrevious = false } = {}) => {
|
||||
await use(async ({ initUserDataPath, testFile, userDataPath, dotEnv = {} } = {}) => {
|
||||
const key = testFile || userDataPath || initUserDataPath;
|
||||
if (key && apps[key]) {
|
||||
if (closePrevious) {
|
||||
await closeElectronApp(apps[key]);
|
||||
delete apps[key];
|
||||
} else {
|
||||
return apps[key];
|
||||
}
|
||||
return apps[key];
|
||||
}
|
||||
|
||||
// Close other cached apps to prevent resource accumulation across test files
|
||||
for (const existingKey of Object.keys(apps)) {
|
||||
if (existingKey !== key) {
|
||||
await closeElectronApp(apps[existingKey]);
|
||||
delete apps[existingKey];
|
||||
}
|
||||
}
|
||||
|
||||
const app = await launchElectronApp({ initUserDataPath, userDataPath, dotEnv, templateVars });
|
||||
const app = await launchElectronApp({ initUserDataPath, userDataPath, dotEnv });
|
||||
if (key) {
|
||||
apps[key] = app;
|
||||
}
|
||||
@@ -269,39 +187,33 @@ export const test = baseTest.extend<
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
restartApp: async ({ reuseOrLaunchElectronApp, createTmpDir, collectionFixturePath }, use, testInfo) => {
|
||||
restartApp: async ({ launchElectronApp }, use, testInfo) => {
|
||||
const appInstances: Array<{ app: ElectronApplication; initUserDataPath?: string }> = [];
|
||||
await use(async ({ initUserDataPath } = {}) => {
|
||||
// Get the test directory and check for init-user-data folder
|
||||
const testDir = path.dirname(testInfo.file);
|
||||
const defaultInitUserDataPath = path.join(testDir, 'init-user-data');
|
||||
|
||||
let srcUserDataPath = initUserDataPath;
|
||||
if (!srcUserDataPath) {
|
||||
// Use provided initUserDataPath, or check if default path exists, or use undefined
|
||||
let userDataPath = initUserDataPath;
|
||||
if (!userDataPath) {
|
||||
const hasInitUserData = await fs.promises.stat(defaultInitUserDataPath).catch(() => false);
|
||||
srcUserDataPath = hasInitUserData ? defaultInitUserDataPath : undefined;
|
||||
userDataPath = hasInitUserData ? defaultInitUserDataPath : undefined;
|
||||
}
|
||||
|
||||
// Copy init-user-data to a fresh tmp dir (same as pageWithUserData)
|
||||
const tmpAppDataDir = await createTmpDir();
|
||||
if (srcUserDataPath) {
|
||||
await recursiveCopy(srcUserDataPath, tmpAppDataDir);
|
||||
}
|
||||
|
||||
const templateVars: Record<string, string> = {};
|
||||
if (collectionFixturePath) {
|
||||
templateVars.collectionPath = collectionFixturePath;
|
||||
}
|
||||
|
||||
// Close the previous app (from pageWithUserData) before launching a new one
|
||||
return await reuseOrLaunchElectronApp({
|
||||
initUserDataPath: tmpAppDataDir,
|
||||
testFile: testInfo.file,
|
||||
templateVars,
|
||||
closePrevious: true
|
||||
});
|
||||
const app = await launchElectronApp({ initUserDataPath: userDataPath });
|
||||
appInstances.push({ app, initUserDataPath: userDataPath });
|
||||
return app;
|
||||
});
|
||||
|
||||
// Clean up all app instances
|
||||
for (const { app } of appInstances) {
|
||||
await app.context().close();
|
||||
await app.close();
|
||||
}
|
||||
},
|
||||
|
||||
pageWithUserData: async ({ reuseOrLaunchElectronApp, createTmpDir, collectionFixturePath }, use, testInfo) => {
|
||||
pageWithUserData: async ({ reuseOrLaunchElectronApp, createTmpDir }, use, testInfo) => {
|
||||
const testDir = path.dirname(testInfo.file);
|
||||
const initUserDataPath = path.join(testDir, 'init-user-data');
|
||||
|
||||
@@ -315,19 +227,23 @@ export const test = baseTest.extend<
|
||||
throw err;
|
||||
}
|
||||
|
||||
const templateVars: Record<string, string> = {};
|
||||
if (collectionFixturePath) {
|
||||
templateVars.collectionPath = collectionFixturePath;
|
||||
}
|
||||
|
||||
const app = await reuseOrLaunchElectronApp({ initUserDataPath: tmpAppDataDir, testFile: testInfo.file, templateVars });
|
||||
const app = await reuseOrLaunchElectronApp({ initUserDataPath: tmpAppDataDir, testFile: testInfo.file });
|
||||
|
||||
const context = await app.context();
|
||||
const page = await app.firstWindow();
|
||||
|
||||
// Wait for app to be ready
|
||||
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
|
||||
await usePageWithTracing(context, page, testInfo, use, { initTracing: true });
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
try {
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
} catch (e) { }
|
||||
await context.tracing.startChunk();
|
||||
await use(page);
|
||||
await context.tracing.stopChunk({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,19 @@ test.describe('Create GraphQL Requests', () => {
|
||||
});
|
||||
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
// Clean up Root GraphQL Request
|
||||
await locators.sidebar.request('Root GraphQL Request').hover();
|
||||
await locators.actions.collectionItemActions('Root GraphQL Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder GraphQL Request
|
||||
await locators.sidebar.request('Folder GraphQL Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder GraphQL Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up collection
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,21 @@ test.describe('Create gRPC Requests', () => {
|
||||
});
|
||||
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Root gRPC Request
|
||||
await locators.sidebar.request('Root gRPC Request').hover();
|
||||
await locators.actions.collectionItemActions('Root gRPC Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder gRPC Request
|
||||
await locators.sidebar.request('Folder gRPC Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder gRPC Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up collection
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,21 @@ test.describe('Create HTTP Requests', () => {
|
||||
});
|
||||
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Root HTTP Request
|
||||
await locators.sidebar.request('Root HTTP Request').hover();
|
||||
await locators.actions.collectionItemActions('Root HTTP Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder HTTP Request
|
||||
await locators.sidebar.request('Folder HTTP Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder HTTP Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up collection
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{collectionPath}}",
|
||||
"path": "{{projectRoot}}/tests/collection/create-requests/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}"
|
||||
"{{projectRoot}}/tests/collection/create-requests/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,21 @@ test.describe('Create WebSocket Requests', () => {
|
||||
});
|
||||
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Folder WebSocket Request
|
||||
await locators.sidebar.request('Folder WebSocket Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder WebSocket Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Root WebSocket Request
|
||||
await locators.sidebar.request('Root WebSocket Request').hover();
|
||||
await locators.actions.collectionItemActions('Root WebSocket Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up collection
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ test.describe('Draft values are used in requests', () => {
|
||||
|
||||
const nameEditor = headerRow.locator('.CodeMirror').first();
|
||||
await nameEditor.click();
|
||||
await headerRow.locator('textarea').first().fill('X-Draft-Header');
|
||||
await page.keyboard.type('X-Draft-Header');
|
||||
|
||||
const valueEditor = headerRow.locator('.CodeMirror').nth(1);
|
||||
await valueEditor.click();
|
||||
await headerRow.locator('textarea').nth(1).fill('draft-value-123');
|
||||
await page.keyboard.type('draft-value-123');
|
||||
|
||||
// Verify draft indicator appears (header is not saved yet)
|
||||
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
|
||||
@@ -51,11 +51,11 @@ test.describe('Draft values are used in requests', () => {
|
||||
|
||||
const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first();
|
||||
await folderNameEditor.click();
|
||||
await folderHeaderRow.locator('textarea').first().fill('X-Folder-Draft-Header');
|
||||
await page.keyboard.type('X-Folder-Draft-Header');
|
||||
|
||||
const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1);
|
||||
await folderValueEditor.click();
|
||||
await folderHeaderRow.locator('textarea').nth(1).fill('folder-draft-value-123');
|
||||
await page.keyboard.type('folder-draft-value-123');
|
||||
|
||||
// Create a request in the collection
|
||||
// Create a new request via collection menu
|
||||
@@ -122,7 +122,7 @@ test.describe('Draft values are used in requests', () => {
|
||||
// Create a new request from collection menu
|
||||
const collection = page.locator('.collection-name').filter({ hasText: collectionName });
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click({ force: true });
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByTestId('request-name').fill('Test Request');
|
||||
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
|
||||
|
||||
@@ -29,7 +29,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
|
||||
// Add a request to the folder to make it more realistic
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click({ force: true });
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('test-request-in-folder');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
@@ -126,7 +126,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
|
||||
// Add a request to the folder to make it more realistic
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click({ force: true });
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('http-request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
|
||||
@@ -94,8 +94,8 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible();
|
||||
|
||||
// source and target collection request should remain unchanged
|
||||
await expect(sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first()).toBeVisible();
|
||||
await expect(sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first()).toBeVisible();
|
||||
await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,8 +99,7 @@ test.describe('Tag persistence', () => {
|
||||
await locators.tags.input().fill('smoke');
|
||||
await locators.tags.input().press('Enter');
|
||||
await expect(locators.tags.item('smoke')).toBeVisible();
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await page.keyboard.press('Meta+s');
|
||||
|
||||
// Create another folder
|
||||
await locators.sidebar.collectionRow('test-collection').hover();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, closeElectronApp } from '../../playwright';
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {
|
||||
// Create a temporary user-data directory so we control where the cookies store file is written.
|
||||
@@ -26,7 +26,7 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await closeElectronApp(app1);
|
||||
await app1.close();
|
||||
|
||||
// Second launch – verify the cookie was persisted and re-loaded
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
@@ -39,5 +39,5 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch
|
||||
// The domain we added earlier should still be present.
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await closeElectronApp(app2);
|
||||
await app2.close();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, closeElectronApp } from '../../playwright';
|
||||
import { test, expect } from '../../playwright';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
@@ -24,7 +24,7 @@ test('should handle corrupted passkey and still display saved cookie list', asyn
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await closeElectronApp(app1);
|
||||
await app1.close();
|
||||
|
||||
// 2. Corrupt the encryptedPasskey in cookies.json
|
||||
const cookiesFilePath = path.join(userDataPath, 'cookies.json');
|
||||
@@ -43,5 +43,5 @@ test('should handle corrupted passkey and still display saved cookie list', asyn
|
||||
// The domain row should still be visible (even if cookie values are blank).
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await closeElectronApp(app2);
|
||||
await app2.close();
|
||||
});
|
||||
|
||||
@@ -33,8 +33,7 @@ test.describe('EditableTable - Focus and Placeholder', () => {
|
||||
await expect(nameInput).toBeFocused();
|
||||
|
||||
// Save the request
|
||||
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
||||
await page.keyboard.press(saveShortcut);
|
||||
await page.keyboard.press('Meta+s');
|
||||
|
||||
// Wait for save toast
|
||||
await expect(page.getByText('Request saved successfully').last()).toBeVisible();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect, closeElectronApp } from '../../../playwright';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { sendRequest } from '../../utils/page';
|
||||
|
||||
test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
|
||||
test('set env var with persist using script', async ({ pageWithUserData: page, restartApp }) => {
|
||||
// Keep a copy of the original Stage.bru file
|
||||
const originalStageBruPath = path.join(__dirname, 'fixtures/collection/environments/Stage.bru');
|
||||
const originalStageBruContent = fs.readFileSync(originalStageBruPath, 'utf8');
|
||||
|
||||
// Select the collection and request
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByText('api-setEnvVar-with-persist', { exact: true }).click();
|
||||
@@ -32,7 +38,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
|
||||
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// we restart the app to confirm that the environment variable is persisted
|
||||
const newApp = await restartApp();
|
||||
@@ -53,8 +59,10 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
|
||||
await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
|
||||
await newEnvTab.hover();
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
await closeElectronApp(newApp);
|
||||
// Restore the original Stage.bru file
|
||||
fs.writeFileSync(originalStageBruPath, originalStageBruContent);
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, closeElectronApp } from '../../../playwright';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { sendRequest } from '../../utils/page';
|
||||
|
||||
test.describe.serial('bru.setEnvVar(name, value)', () => {
|
||||
@@ -28,7 +28,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
|
||||
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// we restart the app to confirm that the environment variable is not persisted
|
||||
const newApp = await restartApp();
|
||||
@@ -48,7 +48,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
|
||||
await expect(newPage.locator('.table-container tbody')).not.toContainText('token');
|
||||
|
||||
await newEnvTab.hover();
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await closeElectronApp(newApp);
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click();
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{collectionPath}}",
|
||||
"path": "{{projectRoot}}/tests/environments/api-setEnvVar/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}"
|
||||
"{{projectRoot}}/tests/environments/api-setEnvVar/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -27,7 +27,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
}
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors to avoid masking test failures
|
||||
@@ -35,7 +35,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should persist multiple environment variables from different requests', async ({ pageWithUserData: page, collectionFixturePath }) => {
|
||||
test('should persist multiple environment variables from different requests', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Select collection', async () => {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
// The collection name should be 'collection' based on the test setup
|
||||
@@ -85,12 +85,12 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'value2' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Verify variables are persisted to file', async () => {
|
||||
// Check that the variables are written to the Stage.bru file
|
||||
const stageBruPath = path.join(collectionFixturePath!, 'environments', 'Stage.bru');
|
||||
const stageBruPath = path.join(__dirname, 'fixtures/collection/environments/Stage.bru');
|
||||
const stageBruContent = fs.readFileSync(stageBruPath, 'utf8');
|
||||
|
||||
// Both variables should be present in the file
|
||||
|
||||
@@ -35,6 +35,6 @@ test.describe('Collection Environment Configuration Selection Tests', () => {
|
||||
await expect(activeEnvItem).toContainText('prod');
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/collection-env-config-selection/collection"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/global-env-config-selection/collection"
|
||||
]
|
||||
|
||||
@@ -28,6 +28,6 @@ test.describe('Global Environment Configuration Selection Tests', () => {
|
||||
await expect(activeEnvItem).toContainText(currentEnvName);
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/environments/global-env-config-selection/collection"
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@ test.describe.serial('Collection Environment Import Tests', () => {
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
@@ -128,7 +128,7 @@ test.describe.serial('Collection Environment Import Tests', () => {
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
|
||||
@@ -62,7 +62,7 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,7 +145,7 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ test.describe('Collection Environment Import Tests', () => {
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('collection-env-import-test'));
|
||||
await locationModal.getByRole('button', { name: 'Import' }).click();
|
||||
await locationModal.waitFor({ state: 'hidden' });
|
||||
|
||||
await expect(
|
||||
page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })).toBeVisible({ timeout: 10000 });
|
||||
@@ -62,7 +61,7 @@ test.describe('Collection Environment Import Tests', () => {
|
||||
await expect(page.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(page.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
await page.locator('.collection-item-name').first().click();
|
||||
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}"
|
||||
"{{projectRoot}}/tests/environments/import-environment/env-color-import/fixtures/collection"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('Global Environment Import Tests', () => {
|
||||
await expect(variablesTable.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(variablesTable.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
await page.locator('#collection-environment-test-collection .collection-item-name').first().click();
|
||||
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user