mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
HTTP stream enhancements (#6077)
* feat: add stop request button in api url bar * docs: add farsi translation * fix: handle escaped forward slashes by fast-json-format library upgrade * refactor: change ui to use one from Websockets * chore: cleanup * fix: lint issues * Replace IconPlayerStop with IconSquareRoundedX * update json request and response formatting logic * chore: format changes * chore: remove un-needed diffs * chore: sanitize * bugfix(#5939): curl import fails for custom content-types * chore: remove un-needed diffs * chore: enhance response handling for streaming * fix: disable requestid check for tests and assertions to be updated after streaming result * chore: housekeeping * fix: streamline loading and cancel request icon logic * chore: formatting * fix: multiple co-pilot changes * fix: handle in folders * feat: add WaitGroup utility for managing concurrent tasks * refactor: remove WaitGroup utility and clean up network IPC logic * refactor: remove unused setTimeout import and clean up post script execution * refactor: clean up post-response script execution logic * undiff * re-align * refactor: streamline post-response script execution - Cleaned up formatting and improved readability of the post-response script execution logic. - Consolidated parameters in function calls for consistency. * fix: keep original dataBuffer for saving response --------- Co-authored-by: adarshajit <adarshajit@gmail.com> Co-authored-by: sajadoncode <sajadoncode@gmail.com> Co-authored-by: lohit-bruno <lohit@usebruno.com> Co-authored-by: Bijin A B <bijin@usebruno.com> Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com> Co-authored-by: Anoop M D <anoop@usebruno.com> Co-authored-by: Anoop M D <anoop.md1421@gmail.com> Co-authored-by: Dawid Góra <dawidgora@icloud.com>
This commit is contained in:
committed by
GitHub
parent
fc5093eab4
commit
efad149afc
@@ -16,6 +16,7 @@
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
| [فارسی](docs/contributing/contributing_fa.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
@@ -74,6 +75,7 @@ npm run build:bruno-filestore
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
@@ -94,18 +96,22 @@ npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
92
docs/contributing/contributing_fa.md
Normal file
92
docs/contributing/contributing_fa.md
Normal file
@@ -0,0 +1,92 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## با هم، Bruno را بهتر میکنیم!
|
||||
|
||||
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راهاندازی Bruno روی سیستم شما آورده شده است.
|
||||
|
||||
### فناوریهای استفادهشده
|
||||
|
||||
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بستهبندی نسخه دسکتاپ (که امکان مجموعههای محلی را فراهم میکند) استفاده میکنیم.
|
||||
|
||||
کتابخانههایی که استفاده میکنیم:
|
||||
|
||||
- CSS - Tailwind استایل
|
||||
- Codemirror - ویرایشگر کد
|
||||
- Redux - مدیریت وضعیت
|
||||
- Tabler Icons - آیکونها
|
||||
- formik - فرمها
|
||||
- Yup اعتبارسنجی اسکیمـا
|
||||
- axios - کلاینت درخواست
|
||||
- chokidar - پایشگر سیستم فایل
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده میکنیم.
|
||||
|
||||
### شروع به کدنویسی
|
||||
|
||||
برای راهاندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
|
||||
|
||||
### ارسال Pull Request
|
||||
|
||||
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
|
||||
2 - لطفاً از فرمت نامگذاری شاخهها استفاده کنید:
|
||||
|
||||
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
|
||||
- feature/dark-mode : مثال
|
||||
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
|
||||
- bugfix/bug-1 : مثال
|
||||
|
||||
## توسعه
|
||||
|
||||
به فارسی برونو یا Bruno بهصورت یک اپلیکیشن «سنگین» توسعه داده میشود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راهاندازی نمایید.
|
||||
|
||||
### نیازمندی توسعه
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### اجرای محلی
|
||||
|
||||
```bash
|
||||
# از ورژن NodeJS 18 استفاده کنید
|
||||
nvm use
|
||||
|
||||
# نصب وابستگیها
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# ساخت مستندات GraphQL
|
||||
npm run build:graphql-docs
|
||||
|
||||
# ساخت bruno-query
|
||||
npm run build:bruno-query
|
||||
|
||||
# اجرای اپ Next (ترمینال 1)
|
||||
npm run dev:web
|
||||
|
||||
# اجرای اپ Electron (ترمینال 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### عیبیابی
|
||||
|
||||
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیجهای لازم را نصب میکند.
|
||||
|
||||
```shell
|
||||
# حذف پوشه node_modules در زیردایرکتوریها
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# حذف فایل package-lock.json در زیردایرکتوریها
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### تستها
|
||||
|
||||
```bash
|
||||
# اجرای تستهای schema مربوط به bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# اجرای تستها در همه فضاهای کاری (در صورت وجود)
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
8
docs/publishing/publishing_fa.md
Normal file
8
docs/publishing/publishing_fa.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### انتشار Bruno در یک پکیج منیجر جدید
|
||||
|
||||
اگرچه کد ما متنباز است و همه میتوانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بستههای جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیتهاب ثبت کنید.
|
||||
|
||||
اگرچه بیشتر قابلیتهای ما رایگان و متنباز هستند (شامل REST و GraphQL Apis)،
|
||||
ما تلاش میکنیم بین اصول متنباز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269
|
||||
143
docs/readme/readme_fa.md
Normal file
143
docs/readme/readme_fa.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| **فارسی**
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
|
||||
|
||||
برونو مجموعههای شما را مستقیماً در یک پوشه روی فایلسیستم شما ذخیره میکند. ما از یک زبان نشانهگذاری ساده به نام Bru برای ذخیره اطلاعات درخواستهای API استفاده میکنیم.
|
||||
|
||||
شما میتوانید برای همکاری روی مجموعههای API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
|
||||
|
||||
برونو فقط به صورت آفلاین کار میکند. هیچ برنامهای برای اضافه کردن همگامسازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی دادههای شما اهمیت میدهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. میتوانید چشمانداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
|
||||
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### نصب
|
||||
|
||||
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
|
||||
|
||||
همچنین میتوانید برونو را از طریق مدیر بستههایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
|
||||
|
||||
```sh
|
||||
# بر روی مک از طریق brew
|
||||
brew install bruno
|
||||
|
||||
# بر روی ویندوز از طریق Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Snap
|
||||
snap install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### روی پلتفرمهای مختلف کار میکند 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### همکاری از طریق گیت 👩💻🧑💻
|
||||
|
||||
یا هر سیستم کنترل نسخهای که ترجیح میدهید
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### لینکهای مهم 📌
|
||||
|
||||
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [مستندات](https://docs.usebruno.com)
|
||||
- [وبسایت](https://www.usebruno.com)
|
||||
- [اشتراک ها](https://www.usebruno.com/pricing)
|
||||
- [دانلود](https://www.usebruno.com/downloads)
|
||||
|
||||
### ویدیوها 🎥
|
||||
|
||||
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### حمایت ❤️
|
||||
|
||||
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
|
||||
|
||||
### تجربههای به اشتراک گذاشتهشده 📣
|
||||
|
||||
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربههای خود را به اشتراک بگذارید. [تجربههای خود را در بحث گیتهاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### انتشار برونو در یک پکیچ منیجر جدید
|
||||
|
||||
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
|
||||
|
||||
### مشارکت 👩💻🧑💻
|
||||
|
||||
خوشحالم که میخواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
|
||||
|
||||
حتی اگر نمیتوانید از طریق کدنویسی مشارکت کنید، در گزارش باگها و درخواست قابلیتهای جدید که به حل نیازهای شما کمک میکند تردید نکنید.
|
||||
|
||||
### نویسنده ها
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### در ارتباط باشید 🌐
|
||||
|
||||
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
|
||||
[وبسایت](https://www.usebruno.com) <br />
|
||||
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[لینکدین](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### برند
|
||||
|
||||
**نام**
|
||||
|
||||
به فارسی برونو - `Bruno` یک علامت تجاری ثبتشده متعلق به [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**لوگو**
|
||||
|
||||
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### مجوز 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -14235,9 +14235,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-format": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.3.0.tgz",
|
||||
"integrity": "sha512-B95psGYXJ5XItmxLR6JFcQRQafDyfy8ecHiV/jWCJF9oCIA9/o+wt89cGW61D04xf07yCpIaevvCQbgeJ9w8lQ==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.4.0.tgz",
|
||||
"integrity": "sha512-HEomBtr2fYaVX3iaRdcVLU7Qd3SQhCYvXlMMM9RNaihfIaj5bIC7ADqw/bAPSg/uyX6FIBPq69ioXq0B4Cb6eA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
@@ -26874,7 +26874,7 @@
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.3.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -26883,6 +26883,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.3.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -37,6 +37,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { IconWand } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -19,8 +19,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const onPrettify = () => {
|
||||
if (!variables) return;
|
||||
try {
|
||||
const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyVariables = applyEdits(variables, edits);
|
||||
const prettyVariables = prettifyJsonString(variables);
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: prettyVariables,
|
||||
|
||||
@@ -12,9 +12,9 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import toast from 'react-hot-toast'
|
||||
import { getAbsoluteFilePath } from 'utils/common/path';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
|
||||
const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCollapsed, onToggleCollapse, handleRun, canClientSendMultipleMessages }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -130,8 +130,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
|
||||
const onPrettify = () => {
|
||||
try {
|
||||
const edits = format(content, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyBodyJson = applyEdits(content, edits);
|
||||
const prettyBodyJson = prettifyJsonString(content);
|
||||
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
currentMessages[index] = {
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cancelRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode, IconX } from '@tabler/icons';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode, IconSquareRoundedX } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import { hasRequestChanges } from 'utils/collections';
|
||||
@@ -22,6 +22,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
const editorRef = useRef(null);
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
@@ -80,6 +81,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRequest = () => {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex flex-1 items-center h-full method-selector-container">
|
||||
@@ -87,7 +92,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center justify-center h-full w-16">
|
||||
<span className="text-xs text-indigo-500 font-bold">gRPC</span>
|
||||
</div>
|
||||
|
||||
|
||||
) : (
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
)}
|
||||
@@ -149,11 +154,23 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
item.response?.hasStreamRunning ? (
|
||||
<IconX color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
isLoading || item.response?.stream?.running ? (
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
data-testid="cancel-request-icon"
|
||||
onClick={handleCancelRequest}
|
||||
/>
|
||||
) : (
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
|
||||
<IconArrowRight
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
data-testid="send-arrow-icon"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,8 @@ const RequestTabPanel = () => {
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 is intentional to make both panes appear to be of equal width
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
|
||||
); // 2.2 is intentional to make both panes appear to be of equal width
|
||||
const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
@@ -140,10 +141,12 @@ const RequestTabPanel = () => {
|
||||
setDragging(false);
|
||||
if (!isVerticalLayout) {
|
||||
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
||||
dispatch(updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - mainRect.left
|
||||
}));
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - mainRect.left
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -260,7 +263,7 @@ const RequestTabPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.response?.hasStreamRunning) {
|
||||
if (item.response?.stream?.running) {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
|
||||
@@ -12,12 +12,25 @@ import { getInitialExampleName } from 'utils/collections/index';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getTitleText = ({ isResponseTooLarge, isStreamingResponse }) => {
|
||||
if (isStreamingResponse) {
|
||||
return 'Response Examples aren\'t supported in streaming responses yet.';
|
||||
}
|
||||
|
||||
if (isResponseTooLarge) {
|
||||
return 'Response size exceeds 5MB limit. Cannot save as example.';
|
||||
}
|
||||
|
||||
return 'Save current response as example';
|
||||
};
|
||||
|
||||
const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showSaveResponseExampleModal, setShowSaveResponseExampleModal] = useState(false);
|
||||
const response = item.response || {};
|
||||
|
||||
const isResponseTooLarge = responseSize >= 5 * 1024 * 1024; // 5 MB
|
||||
const isStreamingResponse = response.stream;
|
||||
|
||||
// Only show for HTTP requests
|
||||
if (item.type !== 'http-request') {
|
||||
@@ -96,19 +109,22 @@ const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
toast.success(`Example "${name}" created successfully`);
|
||||
};
|
||||
|
||||
const disabledMessage = getTitleText({
|
||||
isResponseTooLarge,
|
||||
isStreamingResponse
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={handleSaveClick}
|
||||
disabled={isResponseTooLarge}
|
||||
disabled={isResponseTooLarge || isStreamingResponse}
|
||||
title={
|
||||
isResponseTooLarge
|
||||
? 'Response size exceeds 5MB limit. Cannot save as example.'
|
||||
: 'Save current response as example'
|
||||
disabledMessage
|
||||
}
|
||||
className={classnames('p-1', {
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge || isStreamingResponse
|
||||
})}
|
||||
data-testid="response-bookmark-btn"
|
||||
>
|
||||
|
||||
@@ -6,22 +6,22 @@ const ResponseStopWatch = ({ startMillis }) => {
|
||||
|
||||
const tickInterval = 100;
|
||||
const tick = () => {
|
||||
setMilliseconds(_milliseconds => _milliseconds + tickInterval);
|
||||
setMilliseconds((_milliseconds) => _milliseconds + tickInterval);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timerID = setInterval(() => {
|
||||
tick()
|
||||
tick();
|
||||
}, tickInterval);
|
||||
return () => {
|
||||
clearTimeout(timerID);
|
||||
clearInterval(timerID);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
let secondsFormatted = `${seconds.toFixed(1)}s`;
|
||||
let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast
|
||||
return <StyledWrapper className="ml-4" style={{width: `${width}rem`}}>{secondsFormatted}</StyledWrapper>;
|
||||
return <StyledWrapper className="ml-4" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;
|
||||
};
|
||||
|
||||
export default React.memo(ResponseStopWatch);
|
||||
|
||||
@@ -195,7 +195,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
<StyledWrapper className="ws-messages-list mt-1 flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem inFocus={inFocus} id={idx} message={msg} />;
|
||||
return <WSMessageItem key={msg.timestamp} inFocus={inFocus} id={idx} message={msg} />;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -21,10 +21,10 @@ import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import ResponseBookmark from 'src/components/ResponsePane/ResponseBookmark';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import StopWatch from 'components/StopWatch';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import WSMessagesList from './WsResponsePane/WSMessagesList';
|
||||
|
||||
const ResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -73,6 +73,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
const isStream = item.response?.stream ?? false;
|
||||
if (isStream) {
|
||||
return <WSMessagesList order={-1} messages={item.response.data} />;
|
||||
}
|
||||
return (
|
||||
<QueryResult
|
||||
item={item}
|
||||
@@ -89,17 +93,15 @@ const ResponsePane = ({ item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return (
|
||||
<TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>
|
||||
);
|
||||
return <TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -188,11 +190,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.hasStreamRunning} />
|
||||
{item.response?.hasStreamRunning ? (
|
||||
<ResponseStopWatch startMillis={response.duration} />
|
||||
) : <ResponseTime duration={response.duration} />}
|
||||
<ResponseTime duration={response.duration} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.stream?.running} />
|
||||
{item.response?.stream?.running
|
||||
? <ResponseStopWatch startMillis={response.duration} />
|
||||
: <ResponseTime duration={response.duration} />}
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
@@ -200,7 +201,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className="flex flex-col min-h-0 relative px-4 auto overflow-auto"
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
@@ -213,9 +214,9 @@ const ResponsePane = ({ item, collection }) => {
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { parseQueryParams, buildQueryString as stringifyQueryParams } from '@use
|
||||
import { uuid } from 'utils/common';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { hexy as hexdump } from 'hexy';
|
||||
import {
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
@@ -83,8 +84,8 @@ const initiatedGrpcResponse = {
|
||||
isError: false,
|
||||
duration: 0,
|
||||
responses: [],
|
||||
timestamp: Date.now()
|
||||
};
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
const initiatedWsResponse = {
|
||||
status: 'PENDING',
|
||||
@@ -380,39 +381,39 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
if (item.response?.hasStreamRunning) {
|
||||
item.response.hasStreamRunning = null;
|
||||
if (item.response?.stream?.running) {
|
||||
item.response.stream.running = null;
|
||||
|
||||
const startTimestamp = item.requestSent.timestamp;
|
||||
item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration;
|
||||
item.response.data = [{ type: 'info', timestamp: Date.now(), message: 'Connection Closed' }].concat(item.response.data);
|
||||
} else {
|
||||
item.response = null;
|
||||
item.requestUid = null;
|
||||
}
|
||||
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = item.response.hasStreamRunning ? item.cancelTokenUid : null;
|
||||
item.cancelTokenUid = item.response.stream?.running ? item.cancelTokenUid : null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure timestamp is a number (milliseconds since epoch)
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
|
||||
// Append the new timeline entry with numeric timestamp
|
||||
@@ -435,7 +436,7 @@ export const collectionsSlice = createSlice({
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
@@ -455,7 +456,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
collection.timeline.push({
|
||||
type: 'request',
|
||||
type: "request",
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -464,34 +465,36 @@ export const collectionsSlice = createSlice({
|
||||
data: {
|
||||
request: eventData || item.requestSent || item.request,
|
||||
timestamp: Date.now(),
|
||||
eventData: eventData
|
||||
eventData: eventData,
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
grpcResponseReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
|
||||
// Get current response state or create initial state
|
||||
const currentResponse = item.response || initiatedGrpcResponse;
|
||||
const currentResponse = item.response || initiatedGrpcResponse
|
||||
const timestamp = item?.requestSent?.timestamp;
|
||||
let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };
|
||||
|
||||
|
||||
// Process based on event type
|
||||
switch (eventType) {
|
||||
case 'response':
|
||||
const { error, res } = eventData;
|
||||
|
||||
|
||||
// Handle error if present
|
||||
if (error) {
|
||||
const errorCode = error.code || 2; // Default to UNKNOWN if no code
|
||||
|
||||
|
||||
updatedResponse.error = error.details || 'gRPC error occurred';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
@@ -500,72 +503,72 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
// Add response to list
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
: [...(currentResponse?.responses || [])];
|
||||
break;
|
||||
|
||||
|
||||
case 'metadata':
|
||||
updatedResponse.headers = eventData.metadata;
|
||||
updatedResponse.metadata = eventData.metadata;
|
||||
break;
|
||||
|
||||
|
||||
case 'status':
|
||||
// Extract status info
|
||||
const statusCode = eventData.status?.code;
|
||||
const statusDetails = eventData.status?.details;
|
||||
const statusMetadata = eventData.status?.metadata;
|
||||
|
||||
|
||||
// Set status based on actual code and details
|
||||
updatedResponse.statusCode = statusCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = statusDetails;
|
||||
updatedResponse.statusDetails = eventData.status;
|
||||
|
||||
|
||||
// Store trailers (status metadata)
|
||||
if (statusMetadata) {
|
||||
updatedResponse.trailers = statusMetadata;
|
||||
}
|
||||
|
||||
|
||||
// Handle error status (non-zero code)
|
||||
if (statusCode !== 0) {
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
// Extract error details
|
||||
const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code
|
||||
const errorDetails = eventData.error?.details || eventData.error?.message;
|
||||
const errorMetadata = eventData.error?.metadata;
|
||||
|
||||
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = errorDetails || 'Unknown gRPC error';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = errorDetails;
|
||||
|
||||
|
||||
// Store error metadata as trailers if present
|
||||
if (errorMetadata) {
|
||||
updatedResponse.trailers = errorMetadata;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'end':
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
break;
|
||||
|
||||
|
||||
case 'cancel':
|
||||
updatedResponse.statusCode = 1; // CANCELLED
|
||||
updatedResponse.statusText = 'CANCELLED';
|
||||
updatedResponse.statusDescription = 'Stream cancelled by client or server';
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
item.requestState = 'received';
|
||||
item.response = updatedResponse;
|
||||
|
||||
@@ -576,7 +579,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Append the new timeline entry with specific gRPC event type
|
||||
collection.timeline.push({
|
||||
type: 'request',
|
||||
type: "request",
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -586,7 +589,7 @@ export const collectionsSlice = createSlice({
|
||||
request: item.requestSent || item.request,
|
||||
response: updatedResponse,
|
||||
eventData: eventData, // Store the original event data
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -596,12 +599,11 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
if (item.response && item.response.hasStreamRunning) {
|
||||
if (item.response && item.response.stream?.running) {
|
||||
item.response.data = '';
|
||||
item.response.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
item.response = null;
|
||||
}
|
||||
}
|
||||
@@ -928,7 +930,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || [];
|
||||
const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || [];
|
||||
const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name,
|
||||
@@ -942,7 +944,9 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Update the request URL to reflect the new query params
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||
const query = stringifyQueryParams(
|
||||
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
|
||||
);
|
||||
|
||||
// If there are enabled query params, append them to the URL
|
||||
if (query && query.length) {
|
||||
@@ -1173,7 +1177,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1215,8 +1219,8 @@ export const collectionsSlice = createSlice({
|
||||
if (!folder || !isItemAFolder(folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
|
||||
folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1497,7 +1501,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
|
||||
switch (item.draft.request.body.mode) {
|
||||
case 'json': {
|
||||
item.draft.request.body.json = action.payload.content;
|
||||
@@ -1634,7 +1638,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
@@ -1885,7 +1889,7 @@ export const collectionsSlice = createSlice({
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(collection, 'draft.root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
break;
|
||||
case 'oauth2':
|
||||
set(collection, 'draft.root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
@@ -2614,7 +2618,7 @@ export const collectionsSlice = createSlice({
|
||||
const { requestUid, itemUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
|
||||
@@ -2647,7 +2651,7 @@ export const collectionsSlice = createSlice({
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'test-script-execution') {
|
||||
if(type === 'test-script-execution') {
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
@@ -2662,7 +2666,7 @@ export const collectionsSlice = createSlice({
|
||||
if (type === 'request-sent') {
|
||||
const { cancelTokenUid, requestSent } = action.payload;
|
||||
item.requestSent = requestSent;
|
||||
|
||||
|
||||
// sometimes the response is received before the request-sent event arrives
|
||||
if (item.requestState === 'queued') {
|
||||
item.requestState = 'sending';
|
||||
@@ -2679,12 +2683,12 @@ export const collectionsSlice = createSlice({
|
||||
const { results } = action.payload;
|
||||
item.testResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-pre-request') {
|
||||
const { results } = action.payload;
|
||||
item.preRequestTestResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-post-response') {
|
||||
const { results } = action.payload;
|
||||
item.postResponseTestResults = results;
|
||||
@@ -2798,7 +2802,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.runnerTags = { include: [], exclude: [] };
|
||||
collection.runnerTags = { include: [], exclude: [] }
|
||||
collection.runnerTagsEnabled = false;
|
||||
collection.runnerConfiguration = null;
|
||||
}
|
||||
@@ -2937,7 +2941,7 @@ export const collectionsSlice = createSlice({
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
@@ -2952,7 +2956,16 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
item.response.data = data.data + (item.response.data || '');
|
||||
if (data.data) {
|
||||
item.response.data ||= [];
|
||||
item.response.data = [{
|
||||
type: 'incoming',
|
||||
message: data.data,
|
||||
messageHexdump: hexdump(data.data),
|
||||
timestamp: Date.now()
|
||||
}].concat(item.response.data);
|
||||
}
|
||||
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
|
||||
item.response.size = data.data?.length + (item.response.size || 0);
|
||||
}
|
||||
},
|
||||
@@ -2997,7 +3010,7 @@ export const collectionsSlice = createSlice({
|
||||
updateCollectionTagsList: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { customAlphabet } from 'nanoid';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import fastJsonFormat from 'fast-json-format';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { patternHasher } from '@usebruno/common/utils';
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
@@ -294,7 +295,7 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
|
||||
}
|
||||
|
||||
try {
|
||||
return prettifyJsonString(rawData);
|
||||
return fastJsonFormat(rawData);
|
||||
} catch (error) {}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
@@ -326,9 +327,11 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
|
||||
|
||||
export const prettifyJsonString = (jsonDataString) => {
|
||||
if (typeof jsonDataString !== 'string') return jsonDataString;
|
||||
|
||||
try {
|
||||
const { hashed, restore } = patternHasher(jsonDataString);
|
||||
const formattedJsonDataStringHashed = fastJsonFormat(hashed);
|
||||
const edits = format(hashed, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const formattedJsonDataStringHashed = applyEdits(hashed, edits);
|
||||
const formattedJsonDataString = restore(formattedJsonDataStringHashed);
|
||||
return formattedJsonDataString;
|
||||
} catch (error) {
|
||||
|
||||
@@ -218,16 +218,16 @@ describe('common utils', () => {
|
||||
});
|
||||
|
||||
test('should format complex json string', () => {
|
||||
const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with "quotes" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
|
||||
const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with \"quotes\" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
|
||||
const expectedOutput = `{
|
||||
"id": 123456789123456789123456789,
|
||||
"name": "Test 'JSON' Data with "quotes" — Pretty Print ",
|
||||
"name": "Test 'JSON' Data with \"quotes\" — Pretty Print ",
|
||||
"active": true,
|
||||
"price": 199.9999999,
|
||||
"decimals": 1.00,
|
||||
"nullValue": null,
|
||||
"unicodeText": "こんにちは世界 ",
|
||||
"escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
|
||||
"escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
|
||||
"nestedObject": {
|
||||
"level1": {
|
||||
"level2": {
|
||||
@@ -280,7 +280,7 @@ describe('common utils', () => {
|
||||
"website": "https://example.com?param='value'&flag='true'",
|
||||
"escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"
|
||||
},
|
||||
"multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "
|
||||
"multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "
|
||||
}`;
|
||||
expect(prettifyJsonString(input)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
29
packages/bruno-app/src/utils/curl/content-type.js
Normal file
29
packages/bruno-app/src/utils/curl/content-type.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const normalizeContentType = (contentType) => {
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return contentType.toLowerCase();
|
||||
};
|
||||
|
||||
export const isJsonLikeContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('application/json') || normalized.includes('+json');
|
||||
};
|
||||
|
||||
export const isXmlLikeContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('application/xml') || normalized.includes('+xml') || normalized.includes('text/xml');
|
||||
};
|
||||
|
||||
export const isPlainTextContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('text/plain');
|
||||
};
|
||||
|
||||
export const isStructuredContentType = (contentType) => {
|
||||
return isJsonLikeContentType(contentType) || isXmlLikeContentType(contentType) || isPlainTextContentType(contentType);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import parseCurlCommand from './parse-curl';
|
||||
import * as querystring from 'query-string';
|
||||
import * as jsesc from 'jsesc';
|
||||
import { buildQueryString } from '@usebruno/common/utils';
|
||||
import { isStructuredContentType } from './content-type';
|
||||
|
||||
function getContentType(headers = {}) {
|
||||
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
||||
@@ -34,7 +35,7 @@ function getDataString(request) {
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType && (contentType.includes('application/json') || contentType.includes('application/xml') || contentType.includes('text/plain'))) {
|
||||
if (isStructuredContentType(contentType)) {
|
||||
return { data: request.data };
|
||||
}
|
||||
|
||||
|
||||
@@ -120,4 +120,37 @@ describe('curlToJson', () => {
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse custom json content-types', () => {
|
||||
const curlCommand = `curl 'https://api.example.com/test'
|
||||
-H 'content-type: application/x.custom+json;version=1'
|
||||
--data-raw '{"test":"data"}'
|
||||
`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/test',
|
||||
raw_url: 'https://api.example.com/test',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'content-type': 'application/x.custom+json;version=1'
|
||||
},
|
||||
data: '{"test":"data"}'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse vendor tree json content-types', () => {
|
||||
const curlCommand = `curl --request POST \\
|
||||
--url https://api.example.com/orders/42/preferences \\
|
||||
--header 'accept: */*' \\
|
||||
--header 'content-type: application/vnd.vendor+json' \\
|
||||
--data '{\\n "data": {\\n "type": "order-preferences",\\n "attributes": {\\n "notes": "Leave at door",\\n "priority": true\\n }\\n }\\n}'`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
expect(result.data).toContain('"type": "order-preferences"');
|
||||
expect(result.data).toContain('"notes": "Leave at door"');
|
||||
expect(result.data).toContain('"priority": true');
|
||||
expect(result.headers['content-type']).toBe('application/vnd.vendor+json');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { forOwn } from 'lodash';
|
||||
import curlToJson from './curl-to-json';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type';
|
||||
|
||||
export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {
|
||||
const parseFormData = (parsedBody) => {
|
||||
@@ -59,25 +60,27 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
|
||||
};
|
||||
|
||||
if (parsedBody && contentType && typeof contentType === 'string') {
|
||||
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
|
||||
const normalizedContentType = contentType.toLowerCase();
|
||||
|
||||
if (requestType === 'graphql-request' && (isJsonLikeContentType(contentType) || normalizedContentType.includes('application/graphql'))) {
|
||||
body.mode = 'graphql';
|
||||
body.graphql = parseGraphQL(parsedBody);
|
||||
} else if (requestType === 'http-request' && request.isDataBinary) {
|
||||
body.mode = 'file';
|
||||
body.file = parsedBody;
|
||||
}else if (contentType.includes('application/json')) {
|
||||
} else if (isJsonLikeContentType(contentType)) {
|
||||
body.mode = 'json';
|
||||
body.json = prettifyJsonString(parsedBody);
|
||||
} else if (contentType.includes('xml')) {
|
||||
} else if (isXmlLikeContentType(contentType) || normalizedContentType.includes('xml')) {
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
} else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
||||
body.mode = 'formUrlEncoded';
|
||||
body.formUrlEncoded = parseFormData(parsedBody);
|
||||
} else if (contentType.includes('multipart/form-data')) {
|
||||
} else if (normalizedContentType.includes('multipart/form-data')) {
|
||||
body.mode = 'multipartForm';
|
||||
body.multipartForm = parsedBody;
|
||||
} else if (contentType.includes('text/plain')) {
|
||||
} else if (isPlainTextContentType(contentType)) {
|
||||
body.mode = 'text';
|
||||
body.text = parsedBody;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
if (response?.error) {
|
||||
resolve(response)
|
||||
}
|
||||
|
||||
resolve({
|
||||
state: 'success',
|
||||
data: response.data,
|
||||
@@ -21,7 +22,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
statusText: response.statusText,
|
||||
duration: response.duration,
|
||||
timeline: response.timeline,
|
||||
hasStreamRunning: response.hasStreamRunning
|
||||
stream: response.stream
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
@@ -32,17 +33,19 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
startGrpcRequest(item, collection, environment, runtimeVariables)
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -82,19 +85,19 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const request = item.draft ? item.draft : item;
|
||||
|
||||
|
||||
ipcRenderer.invoke('grpc:start-connection', {
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
runtimeVariables
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -187,7 +190,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
resolve(response.isActive);
|
||||
} else {
|
||||
@@ -196,7 +199,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
console.error('Failed to check connection status:', err);
|
||||
// On error, assume the connection is not active
|
||||
resolve(false);
|
||||
@@ -214,14 +217,14 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
|
||||
ipcRenderer.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -69,8 +69,9 @@ const getJsSandboxRuntime = (collection) => {
|
||||
return 'vm2';
|
||||
};
|
||||
|
||||
const isStream = (headers) => {
|
||||
return headers.get('content-type') === 'text/event-stream';
|
||||
const hasStreamHeaders = (headers) => {
|
||||
const headerSplit = (headers.get('content-type') ?? '').split(';').map((d) => d.trim());
|
||||
return headerSplit.indexOf('text/event-stream') > -1;
|
||||
};
|
||||
|
||||
const promisifyStream = async (stream, abortController, closeOnFirst) => {
|
||||
@@ -95,18 +96,20 @@ const promisifyStream = async (stream, abortController, closeOnFirst) => {
|
||||
});
|
||||
|
||||
stream.on('close', doResolve);
|
||||
stream.on('error', err => reject(err));
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
const configureRequest = async (collectionUid,
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
collection,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath,
|
||||
globalEnvironmentVariables) => {
|
||||
globalEnvironmentVariables
|
||||
) => {
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
@@ -125,7 +128,7 @@ const configureRequest = async (collectionUid,
|
||||
|
||||
// Get followRedirects setting, default to true for backward compatibility
|
||||
const followRedirects = request.settings?.followRedirects ?? true;
|
||||
|
||||
|
||||
// Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
|
||||
let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;
|
||||
|
||||
@@ -166,12 +169,14 @@ const configureRequest = async (collectionUid,
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
} catch (error) {}
|
||||
}
|
||||
catch(error) {}
|
||||
}
|
||||
break;
|
||||
case 'implicit':
|
||||
@@ -180,7 +185,8 @@ const configureRequest = async (collectionUid,
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
@@ -242,7 +248,8 @@ const configureRequest = async (collectionUid,
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie'
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'cookie'
|
||||
);
|
||||
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
||||
|
||||
@@ -306,7 +313,8 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
|
||||
|
||||
// Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars
|
||||
const processEnvVars = getProcessEnvVars(collection.uid);
|
||||
const resolvedVars = merge({},
|
||||
const resolvedVars = merge(
|
||||
{},
|
||||
globalEnvironmentVars,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
@@ -319,7 +327,8 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
|
||||
@@ -336,14 +345,16 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
|
||||
|
||||
const collectionPath = collection.pathname;
|
||||
|
||||
const axiosInstance = await configureRequest(collection.uid,
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
collection,
|
||||
request,
|
||||
envVars,
|
||||
collection.runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath,
|
||||
collection.globalEnvironmentVariables);
|
||||
collection.globalEnvironmentVariables
|
||||
);
|
||||
|
||||
const response = await axiosInstance(request);
|
||||
|
||||
@@ -378,10 +389,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
};
|
||||
|
||||
const notifyScriptExecution = ({
|
||||
channel, // 'main:run-request-event' | 'main:run-folder-event'
|
||||
basePayload, // request-level or runner-level identifiers
|
||||
scriptType, // 'pre-request' | 'post-response' | 'test'
|
||||
error // optional Error
|
||||
channel, // 'main:run-request-event' | 'main:run-folder-event'
|
||||
basePayload, // request-level or runner-level identifiers
|
||||
scriptType, // 'pre-request' | 'post-response' | 'test'
|
||||
error // optional Error
|
||||
}) => {
|
||||
mainWindow.webContents.send(channel, {
|
||||
type: `${scriptType}-script-execution`,
|
||||
@@ -390,7 +401,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
};
|
||||
|
||||
const runPreRequest = async (request,
|
||||
const runPreRequest = async (
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
@@ -399,10 +411,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname) => {
|
||||
runRequestByItemPathname
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name;
|
||||
const collectionName = collection?.name
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@@ -480,7 +493,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return scriptResult;
|
||||
};
|
||||
|
||||
const runPostResponse = async (request,
|
||||
const runPostResponse = async (
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
@@ -537,7 +551,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run post-response script
|
||||
const responseScript = get(request, 'script.res');
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name;
|
||||
const collectionName = collection?.name
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runResponseScript(
|
||||
@@ -612,9 +626,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const abortController = new AbortController();
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
request.responseType = "stream";
|
||||
request.responseType = 'stream';
|
||||
// flag to see if the stream needs to be handled as an actual stream or
|
||||
// is it just a data stream from axios
|
||||
let isResponseStream = false;
|
||||
const brunoConfig = getBrunoConfig(collectionUid, collection);
|
||||
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
@@ -625,7 +641,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
let preRequestScriptResult = null;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
preRequestScriptResult = await runPreRequest(request,
|
||||
preRequestScriptResult = await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
@@ -634,7 +651,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname);
|
||||
runRequestByItemPathname
|
||||
);
|
||||
} catch (error) {
|
||||
preRequestError = error;
|
||||
}
|
||||
@@ -659,14 +677,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (preRequestError) {
|
||||
return Promise.reject(preRequestError);
|
||||
}
|
||||
const axiosInstance = await configureRequest(collectionUid,
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
collection,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath,
|
||||
collection.globalEnvironmentVariables);
|
||||
collection.globalEnvironmentVariables
|
||||
);
|
||||
|
||||
const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
|
||||
|
||||
@@ -685,7 +705,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
@@ -707,13 +727,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
let response, responseTime;
|
||||
let response, responseTime, axiosDataStream;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
request.isStream = isStream(response.headers);
|
||||
isResponseStream = hasStreamHeaders(response.headers);
|
||||
|
||||
if (!request.isStream) {
|
||||
if (!isResponseStream) {
|
||||
response.data = await promisifyStream(response.data);
|
||||
}
|
||||
|
||||
@@ -740,9 +760,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
|
||||
request.isStream = isStream(response.headers);
|
||||
if (!request.isStream) {
|
||||
isResponseStream = hasStreamHeaders(response.headers);
|
||||
if (!isResponseStream) {
|
||||
response.data = await promisifyStream(response.data);
|
||||
}
|
||||
} else {
|
||||
@@ -755,21 +774,21 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
statusText: error.statusText,
|
||||
error: error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST,
|
||||
timeline: error.timeline
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
||||
|
||||
if (request.isStream) {
|
||||
response.stream = response.data;
|
||||
if (isResponseStream) {
|
||||
axiosDataStream = response.data;
|
||||
}
|
||||
|
||||
const { data, dataBuffer } = request.isStream
|
||||
? { data: '', dataBuffer: new ArrayBuffer(0) }
|
||||
const { data, dataBuffer } = isResponseStream
|
||||
? { data: '', dataBuffer: Buffer.alloc(0) }
|
||||
: parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
|
||||
response.data = data;
|
||||
response.dataBuffer = dataBuffer;
|
||||
|
||||
response.responseTime = responseTime;
|
||||
|
||||
// save cookies
|
||||
@@ -783,9 +802,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
const runPostScripts = async () => {
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
try {
|
||||
postResponseScriptResult = await runPostResponse(request,
|
||||
response,
|
||||
@@ -914,9 +933,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cookiesStore.saveCookieJar();
|
||||
}
|
||||
};
|
||||
|
||||
if (request.isStream) {
|
||||
response.stream.on('close', () => runPostScripts().then());
|
||||
if (isResponseStream) {
|
||||
axiosDataStream.on('close', () => runPostScripts().then());
|
||||
} else {
|
||||
await runPostScripts();
|
||||
}
|
||||
@@ -926,8 +944,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
stream: request.isStream ? response.stream : null,
|
||||
dataBuffer: response.dataBuffer.toString('base64'),
|
||||
stream: isResponseStream ? axiosDataStream : null,
|
||||
cancelTokenUid: cancelTokenUid,
|
||||
size: Buffer.byteLength(response.dataBuffer),
|
||||
duration: responseTime ?? 0,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
||||
timeline: response.timeline
|
||||
@@ -953,12 +973,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const response = await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
|
||||
if (response.stream) {
|
||||
const stream = response.stream;
|
||||
response.stream = undefined;
|
||||
response.hasStreamRunning = response.status >= 200 && response.status < 300;
|
||||
response.stream = { running: response.status >= 200 && response.status < 300 };
|
||||
|
||||
stream.on('data', newData => {
|
||||
stream.on('data', (newData) => {
|
||||
const parsed = parseDataFromResponse({ data: newData, headers: {} });
|
||||
mainWindow.webContents.send('main:http-stream-new-data', {collectionUid, itemUid: item.uid, data: parsed});
|
||||
mainWindow.webContents.send('main:http-stream-new-data', { collectionUid, itemUid: item.uid, data: parsed });
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
@@ -966,7 +985,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:http-stream-end', {collectionUid, itemUid: item.uid});
|
||||
mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid });
|
||||
deleteCancelToken(response.cancelTokenUid);
|
||||
});
|
||||
}
|
||||
@@ -990,7 +1009,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {
|
||||
const abortController = cancelTokens[cancelTokenUid];
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
abortController.abort(); // Ensure the on stream end event is called after the token is deleted
|
||||
abortController.abort();
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('cancel token not found'));
|
||||
@@ -999,7 +1018,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// handler for fetch-gql-schema
|
||||
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler);
|
||||
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler)
|
||||
|
||||
ipcMain.handle(
|
||||
'renderer:run-collection-folder',
|
||||
@@ -1033,7 +1052,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
@@ -1065,8 +1084,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// sort requests by seq property
|
||||
folderRequests = sortByNameThenSequence(folderRequests);
|
||||
folderRequests = sortByNameThenSequence(folderRequests)
|
||||
}
|
||||
|
||||
// Filter requests based on tags
|
||||
@@ -1075,7 +1095,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const excludeTags = tags.exclude ? tags.exclude : [];
|
||||
folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => {
|
||||
requestTags = draft?.tags || requestTags || [];
|
||||
return isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
return isRequestTagsIncluded(requestTags, includeTags, excludeTags)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1128,14 +1148,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
|
||||
const requestUid = uuid();
|
||||
|
||||
try {
|
||||
let preRequestScriptResult;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
preRequestScriptResult = await runPreRequest(request,
|
||||
preRequestScriptResult = await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
@@ -1144,7 +1165,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname);
|
||||
runRequestByItemPathname
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Pre-request script error:', error);
|
||||
preRequestError = error;
|
||||
@@ -1214,7 +1236,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// todo:
|
||||
// i have no clue why electron can't send the request object
|
||||
@@ -1228,8 +1250,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
currentAbortController = new AbortController();
|
||||
request.signal = currentAbortController.signal;
|
||||
request.responseType = 'stream';
|
||||
|
||||
const axiosInstance = await configureRequest(collectionUid,
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
collection,
|
||||
request,
|
||||
envVars,
|
||||
@@ -1246,7 +1268,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId,
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo,
|
||||
});
|
||||
|
||||
collection.oauth2Credentials = updateCollectionOauth2Credentials({
|
||||
@@ -1274,10 +1296,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
request.isStream = isStream(response.headers);
|
||||
response.data = await promisifyStream(response.data, currentAbortController, true);
|
||||
|
||||
response.data = await promisifyStream(response.data, currentAbortController, false);
|
||||
timeEnd = Date.now();
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
@@ -1319,9 +1338,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
request.isStream = isStream(error.response.headers);
|
||||
error.response.data = await promisifyStream(error.response.data, currentAbortController, true);
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(error.response);
|
||||
error.response.responseTime = error.response.headers.get('request-duration');
|
||||
error.response.headers.delete('request-duration');
|
||||
@@ -1338,7 +1355,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
data: error.response.data,
|
||||
responseTime: error.response.responseTime,
|
||||
timeline: error.response.timeline
|
||||
timeline: error.response.timeline,
|
||||
};
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
@@ -1359,7 +1376,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
let postResponseScriptResult;
|
||||
let postResponseError = null;
|
||||
try {
|
||||
postResponseScriptResult = await runPostResponse(request,
|
||||
postResponseScriptResult = await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
@@ -1369,7 +1387,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname);
|
||||
runRequestByItemPathname
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Post-response script error:', error);
|
||||
postResponseError = error;
|
||||
@@ -1406,12 +1425,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const results = assertRuntime.runAssertions(assertions,
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars);
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'assertion-results',
|
||||
@@ -1422,14 +1443,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name;
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
let testResults = null;
|
||||
let testError = null;
|
||||
|
||||
try {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
testResults = await testRuntime.runTests(decomment(testFile),
|
||||
testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
@@ -1439,10 +1461,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName);
|
||||
collectionName
|
||||
);
|
||||
} catch (error) {
|
||||
testError = error;
|
||||
|
||||
|
||||
if (error.partialResults) {
|
||||
testResults = error.partialResults;
|
||||
} else {
|
||||
@@ -1476,7 +1499,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
notifyScriptExecution({
|
||||
@@ -1505,7 +1528,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid,
|
||||
folderUid,
|
||||
statusText: 'collection run was terminated!',
|
||||
runCompletionTime: new Date().toISOString()
|
||||
runCompletionTime: new Date().toISOString(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1522,7 +1545,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (nextRequestIdx >= 0) {
|
||||
currentRequestIndex = nextRequestIdx;
|
||||
} else {
|
||||
console.error('Could not find request with name \'' + nextRequestName + '\'');
|
||||
console.error("Could not find request with name '" + nextRequestName + "'");
|
||||
currentRequestIndex++;
|
||||
}
|
||||
} else {
|
||||
@@ -1535,10 +1558,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
type: 'testrun-ended',
|
||||
collectionUid,
|
||||
folderUid,
|
||||
runCompletionTime: new Date().toISOString()
|
||||
runCompletionTime: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
console.log("error", error);
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
@@ -1635,13 +1658,14 @@ const executeRequestOnFailHandler = async (request, error) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const registerAllNetworkIpc = (mainWindow) => {
|
||||
registerNetworkIpc(mainWindow);
|
||||
registerGrpcEventHandlers(mainWindow);
|
||||
registerWsEventHandlers(mainWindow);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = registerAllNetworkIpc;
|
||||
module.exports = registerAllNetworkIpc
|
||||
module.exports.configureRequest = configureRequest;
|
||||
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
|
||||
module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
| [正體中文](docs/publishing/publishing_zhtw.md)
|
||||
| [日本語](docs/publishing/publishing_ja.md)
|
||||
| [Nederlands](docs/publishing/publishing_nl.md)
|
||||
| [فارسی](docs/publishing/publishing_fa.md)
|
||||
|
||||
### Publishing Bruno to a new package manager
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
| [日本語](docs/readme/readme_ja.md)
|
||||
| [ქართული](docs/readme/readme_ka.md)
|
||||
| [Nederlands](docs/readme/readme_nl.md)
|
||||
| [فارسی](docs/readme/readme_fa.md)
|
||||
|
||||
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
|
||||
|
||||
@@ -52,6 +53,7 @@ We strive to strike a harmonious balance between [open-source principles and sus
|
||||
You can explore our [paid versions](https://www.usebruno.com/pricing) to see if there are additional features that you or your team may find useful! <br/>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Features](#features)
|
||||
- [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
|
||||
|
||||
@@ -13,6 +13,7 @@ post {
|
||||
body:json {
|
||||
{
|
||||
"bigint": 1736184243098437392,
|
||||
"unicode": ["\u4e00","\u4e8c","\u4e09"]
|
||||
"unicode": ["\u4e00","\u4e8c","\u4e09"],
|
||||
"forwardslashes": "\/url\/path\/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ test.describe.serial('JSON Response Formatting', () => {
|
||||
await expect(responseBody).toContainText('一');
|
||||
await expect(responseBody).toContainText('二');
|
||||
await expect(responseBody).toContainText('三');
|
||||
|
||||
// The response should handle escaped forward slashes
|
||||
await expect(responseBody).toContainText('/url/path/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user