diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb92ae7..52c6340 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,116 @@ # Contributing to Yasumu -TODO +We appreciate your interest in contributing to Yasumu! Whether you're fixing bugs, adding new features, improving documentation, or anything else, your contributions are valuable. Follow the steps below to set up your development environment and start contributing. + +## Getting Started + +### 1. Clone the Repository + +Begin by cloning the Yasumu repository to your local machine. This will create a local copy of the project where you can work on your contributions. + +```sh +git clone https://github.com/yasumu-org/yasumu.git +``` + +### 2. Navigate to the Project Directory + +Once the repository is cloned, change into the project directory. + +```sh +cd yasumu +``` + +### 3. Install Dependencies + +Before working on the project, you will need to install all necessary dependencies. Yasumu uses Yarn v4 as its package manager. Run the following command to install dependencies: + +```sh +yarn install +``` + +This will install all the required packages and dependencies specified in the `package.json` file. Make sure you have [Yarn](https://yarnpkg.com/getting-started/install) installed on your machine before proceeding. + +### 4. Build the Project + +Yasumu is structured as a multi-package repository (monorepo), meaning it contains several packages. You need to build the project before running it. Building ensures that all source files are compiled and ready for execution. + +```sh +yarn build +``` + +The build process compiles the TypeScript files and ensures everything is in sync across packages. If you make any changes to the source code, be sure to run this command again to reflect those updates. + +### 5. Run the Yasumu Application + +To start the Yasumu application, use the following command: + +```sh +yarn workspace @yasumu/app app +``` + +This command runs the application located in the `@yasumu/app` workspace, which is the main application of the Yasumu project. + +## Running Tests + +To ensure that everything works correctly, you should run the test suite. Yasumu has a set of automated tests to maintain code quality and catch potential issues. To run the tests, use: + +```sh +yarn test +``` + +This will execute all the unit tests and report any errors. Make sure to run the tests after making changes to ensure that your contribution doesn’t break existing functionality. + +## Code Style and Linting + +Yasumu follows specific code formatting and linting rules to maintain a clean and readable codebase. Please make sure that your code adheres to these standards. + +To check for linting errors and automatically fix them (where possible), run: + +```sh +yarn lint --fix +``` + +This will run ESLint on the project and fix any auto-fixable issues. Be sure to fix any remaining linting errors manually before submitting your changes. + +## Submitting a Pull Request + +Once you've made your changes, you're ready to submit a pull request (PR). Follow these steps: + +1. **Create a New Branch** + It's a good practice to create a new branch for each feature or bug fix you work on. Use descriptive branch names that explain the purpose of the branch, e.g., `fix-bug-123` or `feature-new-component`. + + ```sh + git checkout -b feature/your-feature-name + ``` + +2. **Commit Your Changes** + Make sure your commit messages are descriptive and follow conventional commit guidelines (e.g., `feat: add new feature`, `fix: resolve issue with something`). You can take a look at [Conventional Commits](https://www.conventionalcommits.org/) for more information. + + ```sh + git add . + git commit -m "feat: add feature X" + ``` + +3. **Push the Changes to Your Fork** + Push your branch to your forked repository. + + ```sh + git push origin feature/your-feature-name + ``` + +4. **Open a Pull Request** + Go to the Yasumu repository on GitHub and open a new pull request. Provide a detailed description of your changes, the problem you're solving, and any additional context that may be helpful. + + Once the PR is submitted, it will be reviewed by the maintainers. If any changes are requested, please address them in a timely manner to help get your contribution merged quickly. + +## Additional Resources + +- **Documentation**: Ensure you're familiar with the [Yasumu documentation](https://docs.yasumu.dev) to understand how the system works and where your contributions will fit. +- **Issue Tracker**: If you're unsure what to work on, check the open issues on the [GitHub Issue Tracker](https://github.com/yasumu-org/yasumu/issues). Feel free to claim any issue or start a discussion if you need more clarification. +- **Coding Guidelines**: Make sure to review the project's coding standards and best practices to ensure consistency across the codebase. + +## Need Help? + +If you have any questions, feel free to reach out by opening an issue or joining the discussion in the project's community forum or [Discord](https://discord.yasumu.dev) server. + +We look forward to your contributions! diff --git a/README.md b/README.md index 58012b9..74fc579 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ Yasumu is a customizable, free and open-source application to test various types The following table shows the status of the features in the project. -| Feature | Status | -| ----------------- | ----------- | -| REST API Testing | In Progress | -| SMTP Server | In Progress | -| GraphQL Testing | In Progress | -| WebSocket Testing | Planned | -| gRPC Testing | Planned | -| Custom Plugins | Planned | -| Custom Themes | Planned | +| Feature | Status | +| ----------------- | ------- | +| REST API Testing | ✅ | +| SMTP Server | ✅ | +| GraphQL Testing | Planned | +| WebSocket Testing | Planned | +| gRPC Testing | Planned | +| Custom Plugins | Planned | +| Custom Themes | Planned | ## About @@ -77,3 +77,4 @@ For any queries, you can contact the maintainers at [contact@yasumu.dev](mailto: | Bibek Raj Ghimire | Developer | [GitHub](https://github.com/ghimirebibek) | | Samir Paudyal | Developer | [GitHub](https://github.com/samir-byte) | | Sulav Niroula | Frontend Developer | [GitHub](https://github.com/sulav7) | +| Anish | Frontend Developer | [GitHub](https://github.com/novanish) | diff --git a/apps/yasumu/src-tauri/Cargo.lock b/apps/yasumu/src-tauri/Cargo.lock index 215cc43..ea13fc6 100644 --- a/apps/yasumu/src-tauri/Cargo.lock +++ b/apps/yasumu/src-tauri/Cargo.lock @@ -1864,8 +1864,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -3026,6 +3028,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nid" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abdf1789932b85dc39446e27f45a1064a30f9e19a2b872b1d09bd59283f85f3" +dependencies = [ + "rand 0.8.5", + "thiserror", +] + [[package]] name = "nix" version = "0.27.1" @@ -5339,9 +5351,9 @@ dependencies = [ [[package]] name = "tanxium" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a78074a6843e5de930769336a5b70aa7916f7b9874edd5d53ca36bb2e53e4c" +checksum = "c87760f2c28d2e1046a8bfbd8367c094e94d2e7d067338fbb9606553cdd4031c" dependencies = [ "boa_engine", "boa_gc", @@ -5349,8 +5361,11 @@ dependencies = [ "boa_runtime", "deno_ast", "futures-util", + "nid", + "rand 0.8.5", "reqwest", "smol", + "ulid", "uuid", ] @@ -6223,6 +6238,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "ulid" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289" +dependencies = [ + "getrandom 0.2.15", + "rand 0.8.5", + "web-time", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -6552,6 +6578,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" diff --git a/apps/yasumu/src-tauri/Cargo.toml b/apps/yasumu/src-tauri/Cargo.toml index 39a7c8e..5134bbf 100644 --- a/apps/yasumu/src-tauri/Cargo.toml +++ b/apps/yasumu/src-tauri/Cargo.toml @@ -38,5 +38,5 @@ tauri-plugin-shell = "2.0.0-rc.1" tauri-plugin-process = "2.0.0-rc.0" mailparse = "0.15.0" smol = "2.0.2" -tanxium = "0.1.2" +tanxium = "0.1.3" uuid = { version = "1.10.0", features = ["fast-rng"] } diff --git a/apps/yasumu/src-tauri/capabilities/main.json b/apps/yasumu/src-tauri/capabilities/main.json index 7a67f3c..f8f697a 100644 --- a/apps/yasumu/src-tauri/capabilities/main.json +++ b/apps/yasumu/src-tauri/capabilities/main.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Capability for main window", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:app:allow-version", "core:app:allow-name", @@ -54,6 +52,14 @@ "store:allow-set", "store:allow-save", "store:allow-load", + "store:allow-clear", + "store:allow-delete", + "store:allow-entries", + "store:allow-has", + "store:allow-keys", + "store:allow-length", + "store:allow-reset", + "store:allow-values", { "identifier": "fs:allow-write-text-file", "allow": [ @@ -153,4 +159,4 @@ }, "process:default" ] -} \ No newline at end of file +} diff --git a/apps/yasumu/src-tauri/runtime/02_runtime.ts b/apps/yasumu/src-tauri/runtime/02_runtime.ts index c128df0..c469522 100644 --- a/apps/yasumu/src-tauri/runtime/02_runtime.ts +++ b/apps/yasumu/src-tauri/runtime/02_runtime.ts @@ -6,6 +6,10 @@ return Yasumu.context.data; } + public get id(): string { + return this.data.request.id; + } + public get url(): string { return this.data.request.url; } @@ -15,12 +19,16 @@ } public get headers(): Headers { - Yasumu.context.__meta.requestHeaders = new Headers(this.data.request.headers); - return Yasumu.context.__meta.requestHeaders; + const headers = new Headers(this.data.request.headers as HeadersInit); + + // @ts-ignore + Yasumu.context.__meta.request.headers = headers; + + return headers; } public cancel(): void { - Yasumu.context.__meta.requestCanceled = true; + Yasumu.context.__meta.request.canceled = true; } } @@ -29,6 +37,26 @@ return Yasumu.context.data; } + public get contentLength(): number { + return this.data.response.contentLength; + } + + public get redirected(): boolean { + return this.data.response.redirected; + } + + public get type(): string { + return this.data.response.type; + } + + public get ok(): boolean { + return this.data.response.ok; + } + + public get cookies(): Cookie[] { + return this.data.response.cookies; + } + public get url(): string { return this.data.response.url; } @@ -38,7 +66,7 @@ } public get headers(): Headers { - return new Headers(this.data.response.headers); + return new Headers(this.data.response.headers as HeadersInit); } public get status(): number { @@ -58,39 +86,73 @@ } } - class YasumuStoreModel implements YasumuStore { - private get store() { - return Yasumu.context.__meta.store; + const getStore = () => { + const val = Yasumu.context.data.store; + + if (!val) { + return (Yasumu.context.data.store = {}); + } + + return val; + }; + const getChanges = () => { + const val = Yasumu.context.__meta.store; + + if (!Array.isArray(val)) { + return (Yasumu.context.__meta.store = []); } + return val; + }; + + class YasumuStoreModel implements YasumuStore { public get(key: string): any { - return this.store[key]; + return getStore()[key] ?? getChanges().find((change) => change.key === key)?.value; } public set(key: string, value: any): void { - this.store[key] = value; + if (value === undefined) { + throw new Error('Value cannot be undefined'); + } + + getStore()[key] = value; + getChanges().push({ op: 'set', key, value }); } public remove(key: string): void { - delete this.store[key]; + delete getStore()[key]; + getChanges().push({ op: 'delete', key }); } public has(key: string): boolean { - return key in this.store; + return key in getStore() || getChanges().some((change) => change.key === key); } public count(): number { - return Object.keys(this.store).length; + const keys = Object.keys(getStore()); + return keys.length + getChanges().filter((change) => change.op === 'set' && !keys.includes(change.key)).length; } public clear(): void { - for (const key in this.store) { - delete this.store[key]; + const store = getStore(); + const changes = getChanges(); + for (const key in store) { + changes.push({ op: 'delete', key }); + delete store[key]; } } public entries(): [string, any][] { - return Object.entries(this.store); + const main = Object.entries(getStore()); + const changes = getChanges(); + + const res = [ + changes + .filter((change) => change.op === 'set' && !main.some((v) => v[0] === change.key)) + .map((v) => [v.key, v.value]), + ]; + + return res.flat() as [string, any][]; } } @@ -98,10 +160,12 @@ context: { data: {}, __meta: { - store: {}, - requestHeaders: {}, + store: [], + request: {} as any, + response: {} as any, console: [], - }, + test: [], + } satisfies YasumuContextMeta, }, setContextData(data: string | Record) { if (typeof data === 'string') { @@ -115,8 +179,24 @@ store: new YasumuStoreModel(), }; + function deeplyClean(obj: any) { + if (!obj) return; + + for (const key in obj) { + if (obj[key] === undefined) { + delete obj[key]; + } else if (typeof obj[key] === 'object') { + deeplyClean(obj[key]); + } + } + } + Yasumu.serialize = function () { - return JSON.stringify(this.context.__meta); + const obj = this.context.__meta; + + deeplyClean(obj); + + return JSON.stringify(obj); }; Object.assign(Yasumu, context); diff --git a/apps/yasumu/src-tauri/runtime/03_console.ts b/apps/yasumu/src-tauri/runtime/03_console.ts index 8583abc..408d52a 100644 --- a/apps/yasumu/src-tauri/runtime/03_console.ts +++ b/apps/yasumu/src-tauri/runtime/03_console.ts @@ -1,10 +1,13 @@ /// (() => { const COMMON_OBJECTS = new Set(['Map', 'Set', 'WeakMap', 'WeakSet']); - function inspect(value: any, visited = new WeakSet(), indentLevel = 0): string { + const MAX_DEPTH = 2; + + function inspect(value: any, visited = new WeakSet(), indentLevel = 0, root = true, depth = 0): string { try { if (typeof value === 'string') { - return `"${value}"`; + if (!root) return `"${value}"`; + return value; } if (typeof value === 'bigint') { @@ -27,14 +30,27 @@ } if (typeof value === 'object') { + if (depth > MAX_DEPTH) { + return '{ ... }'; + } if (visited.has(value)) { return '[Circular]'; } visited.add(value); + if (value instanceof Error) { + let msg = value.stack || String(value); + + if (!value.stack) { + msg += `\n${Yasumu.utils.getStackTrace()}`; + } + + return msg; + } + if (Array.isArray(value)) { - return `[${value.map((item) => inspect(item, visited, indentLevel + 2)).join(', ')}]`; + return `[${value.map((item) => inspect(item, visited, indentLevel + 2, false, depth + 1)).join(', ')}]`; } if (value && 'constructor' in value && value.constructor && COMMON_OBJECTS.has(value.constructor.name)) { @@ -50,7 +66,7 @@ for (const key in value) { if (Object.prototype.propertyIsEnumerable.call(value, key)) { enumerableFound++; - result += `${nestedIndent}${key}: ${inspect(value[key], visited, indentLevel + 2)},\n`; + result += `${nestedIndent}${key}: ${inspect(value[key], visited, indentLevel + 2, false, depth + 1)},\n`; } } @@ -71,7 +87,7 @@ } } - const CONSOLE_METHODS: LogType = ['log', 'error', 'warn', 'info', 'clear']; + const CONSOLE_METHODS: LogType = ['log', 'warn', 'error', 'info', 'clear']; const console = new Proxy({} as YasumuConsole, { get(target, prop, receiver) { diff --git a/apps/yasumu/src-tauri/runtime/04_test.ts b/apps/yasumu/src-tauri/runtime/04_test.ts index 5ff0f2e..de3dedf 100644 --- a/apps/yasumu/src-tauri/runtime/04_test.ts +++ b/apps/yasumu/src-tauri/runtime/04_test.ts @@ -1,5 +1,7 @@ /// +import type { TestResult } from '@yasumu/core'; + (() => { const isTestEnvironment = Yasumu.features.test; @@ -15,12 +17,16 @@ return; } - const warnLog = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'warn', args: [arg], timestamp: Date.now(), test: true }); - const writeSuccess = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'log', args: [arg], timestamp: Date.now(), test: true }); - const writeError = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'error', args: [arg], timestamp: Date.now(), test: true }); + const addTestResult = (result: Omit) => { + Yasumu.context.__meta.test.push({ ...result, id: crypto.randomUUID() }); + }; + + const writeSuccess = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'pass', time, message }); + const writeError = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'fail', time, message }); + const writeSkip = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'skip', time, message }); const YASUMU_ASSERTION_ERROR = 'YasumuAssertionError'; const TEST_CONTEXT_THROWAWAY_ERROR = 'TestContextThrowawayError'; @@ -218,21 +224,16 @@ }, }; - const formatTime = (duration: number) => { - if (duration < 1000) return `${duration.toFixed(2)}ms`; - return `${duration.toFixed(2)}s`; - }; - - const evaluateState = (duration: string) => { + const evaluateState = (duration: number) => { switch (state) { case TestState.Passed: - writeSuccess(`[PASSED] (${duration}) ${name}: ${stateReason}`); + writeSuccess(name, duration, stateReason || null); break; case TestState.Failed: - writeError(`[FAILED] (${duration}) Yasumu.test(${name}): ${stateReason}`); + writeError(name, duration, stateReason || null); break; case TestState.Skipped: - warnLog(`[SKIPPED] (${duration}) Yasumu.test(${name}): ${stateReason}`); + writeSkip(name, duration, stateReason || null); break; default: break; @@ -245,20 +246,18 @@ try { fn(testContext); end = performance.now(); - writeSuccess(`[PASSED] (${formatTime(end - start)}) ${name}`); + writeSuccess(name, end - start, null); } catch (_error) { end = performance.now(); - const timeTaken = formatTime(end - start); + const timeTaken = end - start; if (_error && _error instanceof TestContextThrowawayError && state != null) return void evaluateState(timeTaken); if (_error && _error instanceof YasumuAssertionError) { - writeError(`[FAILED] (${timeTaken}) Yasumu.test(${name}):\n${_error.message}`); + writeError(name, timeTaken, _error.message || String(_error)); return; } - writeError( - `[ERROR] (${timeTaken}) Yasumu.test(${name}) failed with error:\n${(_error as Error).message || _error}`, - ); + writeError(name, timeTaken, (_error as Error).message || String(_error)); } } diff --git a/apps/yasumu/src-tauri/runtime/_common.ts b/apps/yasumu/src-tauri/runtime/_common.ts index 5f1f726..dc555ec 100644 --- a/apps/yasumu/src-tauri/runtime/_common.ts +++ b/apps/yasumu/src-tauri/runtime/_common.ts @@ -1,3 +1,13 @@ +import { + LogType as YasumuLogType, + LogStream as YasumuLogStream, + YasumuResponseContextData, + YasumuRequestContextData, + YasumuContextData as YasumuContextDataCore, + YasumuContextMeta as YasumuContextMetaCore, + YasumuCookie, +} from '@yasumu/core'; + declare global { interface YasumuRequire { (id: string): any; @@ -5,6 +15,8 @@ declare global { cache: Record; } + type Cookie = YasumuCookie; + var __require: YasumuRequire; // @ts-ignore @@ -19,14 +31,8 @@ declare global { // @ts-ignore var crypto: YasumuCrypto; - type LogType = readonly ['log', 'error', 'warn', 'info', 'clear']; - - type LogStream = { - type: LogType[number]; - args: any[]; - timestamp: number; - test?: boolean; - }; + type LogType = YasumuLogType; + type LogStream = YasumuLogStream; interface NamedConsoleMethods { clear(): void; @@ -40,23 +46,6 @@ declare global { > & NamedConsoleMethods; - interface YasumuRequest { - url: string; - method: string; - headers: Headers; - cancel(): void; - } - - interface YasumuResponse { - url: string; - method: string; - headers: Headers; - status: number; - statusText: string; - bodyText: string; - responseTime: number; - } - interface YasumuStore { get(key: string): any; set(key: string, value: any): void; @@ -67,28 +56,16 @@ declare global { entries(): [string, any][]; } - interface YasumuContextData { - request: { - url: string; - method: string; - headers: Record; - }; - response: { - url: string; - method: string; - headers: Record; - status: number; - statusText: string; - bodyText: string; - responseTime: number; - }; + type YasumuContextData = YasumuContextDataCore; + type YasumuContextMeta = YasumuContextMetaCore; + + interface YasumuRequest extends Omit { + headers: Headers; + cancel(): void; } - interface YasumuContextMeta { - store: Record; - console: LogStream[]; - requestHeaders: Headers; - requestCanceled: boolean; + interface YasumuResponse extends Omit { + headers: Headers; } interface YasumuFeatures { @@ -105,6 +82,9 @@ declare global { response: YasumuResponse; store: YasumuStore; features: YasumuFeatures; + utils: { + getStackTrace(): string; + }; serialize(): string; nanoseconds(): bigint; sleep(ms: number): Promise; diff --git a/apps/yasumu/src-tauri/src/commands/scripts.rs b/apps/yasumu/src-tauri/src/commands/scripts.rs index 159cd38..9cab5b2 100644 --- a/apps/yasumu/src-tauri/src/commands/scripts.rs +++ b/apps/yasumu/src-tauri/src/commands/scripts.rs @@ -2,7 +2,7 @@ use boa_engine::{ js_str, js_string, object::ObjectInitializer, property::{Attribute, PropertyDescriptorBuilder}, - JsValue, + JsString, JsValue, NativeFunction, }; use tanxium::tanxium; use tauri::{path::BaseDirectory, Manager}; @@ -19,6 +19,7 @@ fn polyfill_yasumu_api( workspace_id: String, ) { let ctx = &mut tanxium.context; + // Yasumu object let package = app.package_info(); let app_version = format!( @@ -74,6 +75,31 @@ fn polyfill_yasumu_api( .configurable(false); yasumu_obj.insert_property(js_str!("workspace"), workspace); + + let yasumu_utils = ObjectInitializer::new(ctx) + .function( + NativeFunction::from_fn_ptr(|_, _, context| { + let stack = context + .stack_trace() + .map(|frame| format!(" at {}", frame.code_block().name().to_std_string_escaped())) + .collect::>() + .join("\n"); + + Ok(JsValue::String(JsString::from(stack))) + }), + js_string!("getStackTrace"), + 0, + ) + .build(); + + let yasumu_utils_obj = PropertyDescriptorBuilder::new() + .value(yasumu_utils) + .enumerable(true) + .writable(false) + .configurable(false) + .build(); + + yasumu_obj.insert_property(js_str!("utils"), yasumu_utils_obj); } #[tauri::command] @@ -157,7 +183,6 @@ pub async fn evaluate_javascript( .unwrap(); tanxium.initialize_runtime().unwrap(); - tanxium.load_extensions(extensions).unwrap(); polyfill_yasumu_api( &mut tanxium, @@ -167,6 +192,8 @@ pub async fn evaluate_javascript( id, ); + tanxium.load_extensions(extensions).unwrap(); + let prepare_script = if ts_supported { let res = tanxium.transpile(&prepare); diff --git a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx index a413520..0c91af8 100644 --- a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx +++ b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { BodyMode, HttpMethods, HttpMethodsArray } from '@yasumu/core'; +import { BodyMode, HttpMethods, HttpMethodsArray, YasumuResponseContextData } from '@yasumu/core'; import { cn, IS_AUDIO, IS_BINARY_DATA, IS_IMAGE, IS_VIDEO } from '@/lib/utils'; import { useRequestConfig, useRequestStore } from '@/stores/api-testing/request-config.store'; import { ICookie, useResponse } from '@/stores/api-testing/response.store'; @@ -14,10 +14,14 @@ import { HttpMethodColors } from '@/lib/constants'; import { Yasumu } from '@/lib/yasumu'; import { useConsole } from '@/stores/api-testing/console.store'; import { canEvaluateResult } from '@/lib/scripts/script'; +import { useTest } from '@/stores/api-testing/test.store'; +import { useScriptTime } from '@/stores/api-testing/script-time.store'; export default function RequestInput() { const { current } = useRequestStore(); const { add } = useConsole(); + const { add: addTest } = useTest(); + const { setPostResponse, setPreRequest, setTestScript } = useScriptTime(); const { method, setMethod, url, setUrl, body, headers, bodyMode, id, script: preRequestScript } = useRequestConfig(); const save = useDebounceCallback(() => { @@ -87,19 +91,31 @@ export default function RequestInput() { const dispatchRequest = useCallback(async () => { try { + if (!Yasumu.workspace) return; const controller = new AbortController(); responseStore.setAbortController(controller); const h = new Headers(); - const contextData: Record = { - response: {}, + const kv = Yasumu.workspace.openKV(); + const storeData = await kv.entries(); + const storeRecord = storeData.reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as Record, + ); + + const contextData: YasumuContextData = { + response: {} as YasumuResponseContextData, request: { id, url, method, - headers: h, + headers: h as any, }, + store: storeRecord, }; headers.forEach((header) => { @@ -171,52 +187,63 @@ export default function RequestInput() { break; } - contextData.request.body = bodyData; - if (!!preRequestScript?.trim().length) { + const preScriptStart = performance.now(); const result = await Yasumu.scripts.run(preRequestScript, Yasumu.scripts.createContextData(contextData), { test: false, }); + const preScriptFinish = Math.abs(performance.now() - preScriptStart); - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); - } else if (result && typeof result === 'object' && '$error' in result) { - add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); - } else if (canEvaluateResult(result)) { - if (result.requestHeaders?.length) { + setPreRequest(preScriptFinish); + + if (canEvaluateResult(result)) { + if (result.request.canceled) { + controller.abort(); + } + + if (result.console && result.console.length) { + add(result.console); + } + + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); + + if (result.request.headers) { try { - result.requestHeaders.forEach(([key, value]) => { - h.set(key, value); + const headers = Object.entries(result.request.headers); + headers.forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => h.append(key, v)); + } else { + h.set(key, value); + } }); } catch { // } } - // if (Object.keys(result.store).length) { - // const id = Yasumu.workspace?.metadata.id; - - // if (id) { - // const store = Yasumu.createStore(id); - - // for (const key in result.store) { - // await store.set(key, result.store[key]); - // } - // } - // } + if (result.request.url && typeof result.request.url === 'string') { + setUrl(result.request.url); + } + } else if (result && typeof result === 'object' && '$error' in result) { + add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } } + const finalUrl = contextData.request.url || url; - const start = Date.now(); + if (!finalUrl) { + throw new Error('No url provided'); + } - const res = await Yasumu.fetch(url, { + const start = performance.now(); + + const res = await Yasumu.fetch(finalUrl, { method: method.toUpperCase(), body: bodyData, redirect: 'follow', // @ts-ignore - maxRedirections: 20, + maxRedirections: 10, cache: 'no-cache', - credentials: 'omit', connectTimeout: 60_000, headers: h, signal: controller.signal, @@ -224,7 +251,7 @@ export default function RequestInput() { if (!res) throw new Error('Failed to fetch response'); - const end = Math.abs(Date.now() - start); + const end = Math.abs(performance.now() - start); responseStore.setUrl(res.url); responseStore.setResponseStatus(res.status); @@ -265,7 +292,7 @@ export default function RequestInput() { const bodySize = Number(res.headers.get('Content-Length')) || value.byteLength; - contextData.response.body = str; + contextData.response.bodyText = str; contextData.response.contentLength = bodySize; responseStore.setBody(str); @@ -277,7 +304,7 @@ export default function RequestInput() { responseStore.setResponseSize(len); } - contextData.response.headers = res.headers; + contextData.response.headers = Object.fromEntries(res.headers.entries()); contextData.response.status = res.status; contextData.response.statusText = res.statusText; contextData.response.redirected = res.redirected; @@ -288,6 +315,7 @@ export default function RequestInput() { contextData.response.responseTime = end; if (!!responseStore.postRequestScript?.trim().length) { + const postScriptStart = performance.now(); const result = await Yasumu.scripts.run( responseStore.postRequestScript, Yasumu.scripts.createContextData(contextData), @@ -295,21 +323,40 @@ export default function RequestInput() { test: false, }, ); + const postScriptFinish = Math.abs(performance.now() - postScriptStart); + + setPostResponse(postScriptFinish); - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); + if (canEvaluateResult(result)) { + if (result.console && result.console.length) { + add(result.console); + } + + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); } else if (result && typeof result === 'object' && '$error' in result) { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } } if (!!responseStore.test?.trim().length) { + const testScriptStart = performance.now(); const result = await Yasumu.scripts.run(responseStore.test, Yasumu.scripts.createContextData(contextData), { test: true, }); + const testScriptFinish = Math.abs(performance.now() - testScriptStart); + + setTestScript(testScriptFinish); + + if (canEvaluateResult(result)) { + if (result.console && result.console.length) { + add(result.console); + } + + if (result.test.length) { + addTest(result.test); + } - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); } else if (result && typeof result === 'object' && '$error' in result) { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } diff --git a/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx b/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx index cd76a0e..93c80d0 100644 --- a/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx +++ b/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx @@ -12,12 +12,19 @@ export function ResponseAttachmentGuard({ const { url } = useResponse(); if (!contentType) return <>{children}; - if (IS_AUDIO(contentType)) return