feat: collection runner (#3600)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
*.env
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "19.5.0",
|
||||
"@commitlint/config-conventional": "19.5.0",
|
||||
"@hoppscotch/ui": "0.2.1",
|
||||
"@hoppscotch/ui": "0.2.2",
|
||||
"@types/node": "22.7.6",
|
||||
"cross-env": "7.0.3",
|
||||
"http-server": "14.1.1",
|
||||
|
||||
@@ -79,7 +79,7 @@ export const WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Workspa
|
||||
collectionID: "clx1ldkzs005t10f8rp5u60q7",
|
||||
teamID: "clws3hg58000011o8h07glsb1",
|
||||
title: "RequestA",
|
||||
request: `{"v":"${RESTReqSchemaVersion}","id":"clpttpdq00003qp16kut6doqv","auth":{"authType":"inherit","authActive":true},"body":{"body":null,"contentType":null},"name":"RequestA","method":"GET","params":[],"headers":[],"endpoint":"https://echo.hoppscotch.io","testScript":"pw.test(\\"Correctly inherits auth and headers from the root collection\\", ()=> {\\n pw.expect(pw.response.body.headers[\\"x-test-header\\"]).toBe(\\"Set at root collection\\");\\n pw.expect(pw.response.body.headers[\\"authorization\\"]).toBe(\\"Bearer BearerToken\\");\\n});","preRequestScript":"","requestVariables":[]}`,
|
||||
request: `{"v":"${RESTReqSchemaVersion}","id":"clpttpdq00003qp16kut6doqv","auth":{"authType":"inherit","authActive":true},"body":{"body":null,"contentType":null},"name":"RequestA","method":"GET","params":[],"headers":[],"endpoint":"https://echo.hoppscotch.io","testScript":"pw.test(\\"Correctly inherits auth and headers from the root collection\\", ()=> {\\n pw.expect(pw.response.body.headers[\\"x-test-header\\"]).toBe(\\"Set at root collection\\");\\n pw.expect(pw.response.body.headers[\\"authorization\\"]).toBe(\\"Bearer BearerToken\\");\\n});","preRequestScript":"","requestVariables":[],"responses":{}}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -233,6 +233,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
|
||||
'pw.test("Correctly inherits auth and headers from the root collection", ()=> {\n pw.expect(pw.response.body.headers["x-test-header"]).toBe("Set at root collection");\n pw.expect(pw.response.body.headers["authorization"]).toBe("Bearer BearerToken");\n});',
|
||||
preRequestScript: "",
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
],
|
||||
auth: {
|
||||
|
||||
@@ -31,12 +31,14 @@ const migrateCollections = (collections: unknown[]): HoppCollection[] => {
|
||||
);
|
||||
}
|
||||
|
||||
return collectionSchemaParsedResult.data.map((collection) => {
|
||||
return {
|
||||
...collection,
|
||||
folders: migrateCollections(collection.folders),
|
||||
};
|
||||
});
|
||||
return collectionSchemaParsedResult.data.map(
|
||||
({ _ref_id, folders, ...rest }) => {
|
||||
return {
|
||||
...rest,
|
||||
folders: migrateCollections(folders),
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
describe("workspace-access", () => {
|
||||
|
||||
@@ -344,26 +344,44 @@ pre.ace_editor {
|
||||
|
||||
.info-response {
|
||||
color: var(--status-info-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-info-color);
|
||||
}
|
||||
}
|
||||
|
||||
.success-response {
|
||||
color: var(--status-success-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.redirect-response {
|
||||
color: var(--status-redirect-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-redirect-color);
|
||||
}
|
||||
}
|
||||
|
||||
.critical-error-response {
|
||||
color: var(--status-critical-error-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-critical-error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.server-error-response {
|
||||
color: var(--status-server-error-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-server-error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
color: var(--status-missing-data-color);
|
||||
&.outlined {
|
||||
border: 1px solid var(--status-missing-data-color);
|
||||
}
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
--font-size-tiny: 0.625rem;
|
||||
--line-height-body: 1rem;
|
||||
--upper-primary-sticky-fold: 4.125rem;
|
||||
--upper-runner-sticky-fold: 4.125rem;
|
||||
--upper-secondary-sticky-fold: 6.188rem;
|
||||
--upper-runner-sticky-fold: 4.5rem;
|
||||
--upper-tertiary-sticky-fold: 8.25rem;
|
||||
--upper-fourth-sticky-fold: 10.2rem;
|
||||
--upper-mobile-primary-sticky-fold: 6.75rem;
|
||||
|
||||
@@ -231,6 +231,8 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"title": "Collection",
|
||||
"run": "Run Collection",
|
||||
"created": "Collection created",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Edit Collection",
|
||||
@@ -341,6 +343,7 @@
|
||||
"response": "No response received"
|
||||
},
|
||||
"environment": {
|
||||
"heading": "Environment",
|
||||
"add_to_global": "Add to Global",
|
||||
"added": "Environment addition",
|
||||
"create_new": "Create new environment",
|
||||
@@ -448,6 +451,7 @@
|
||||
"invalid_name": "Please provide a name for the folder",
|
||||
"name_length_insufficient": "Folder name should be at least 3 characters long",
|
||||
"new": "New Folder",
|
||||
"run": "Run Folder",
|
||||
"renamed": "Folder renamed"
|
||||
},
|
||||
"graphql": {
|
||||
@@ -1069,7 +1073,10 @@
|
||||
"tests": "Tests",
|
||||
"types": "Types",
|
||||
"variables": "Variables",
|
||||
"websocket": "WebSocket"
|
||||
"websocket": "WebSocket",
|
||||
"all_tests": "All Tests",
|
||||
"passed": "Passed",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"team": {
|
||||
"already_member": "This email is associated with an existing user.",
|
||||
@@ -1137,6 +1144,8 @@
|
||||
"not_found": "Environment not found."
|
||||
},
|
||||
"test": {
|
||||
"requests": "Requests",
|
||||
"selection": "Selection",
|
||||
"failed": "test failed",
|
||||
"javascript_code": "JavaScript Code",
|
||||
"learn": "Read documentation",
|
||||
@@ -1144,7 +1153,14 @@
|
||||
"report": "Test Report",
|
||||
"results": "Test Results",
|
||||
"script": "Script",
|
||||
"snippets": "Snippets"
|
||||
"snippets": "Snippets",
|
||||
"run": "Run",
|
||||
"run_again": "Run again",
|
||||
"stop": "Stop",
|
||||
"new_run": "New Run",
|
||||
"iterations": "Iterations",
|
||||
"duration": "Duration",
|
||||
"avg_resp": "Avg. Response Time"
|
||||
},
|
||||
"websocket": {
|
||||
"communication": "Communication",
|
||||
@@ -1194,7 +1210,17 @@
|
||||
"cli_environment_id_description": "This environment ID will be used by the CLI collection runner for Hoppscotch.",
|
||||
"include_active_environment": "Include active environment:",
|
||||
"cli": "CLI",
|
||||
"ui": "Runner (coming soon)",
|
||||
"delay": "Delay",
|
||||
"ui": "Runner",
|
||||
"running_collection": "Running collection",
|
||||
"run_config": "Run Configuration",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"stop_on_error": "Stop run if an error occurs",
|
||||
"persist_responses": "Persist responses",
|
||||
"collection_not_found": "Collection not found. May be deleted or moved.",
|
||||
"empty_collection": "Collection is empty. Add requests to run.",
|
||||
"no_response_persist": "The collection runner is presently configured not to persist responses. This setting prevents showing the response data. To modify this behavior, initiate a new run configuration.",
|
||||
"select_request": "Select a request to see response and test results",
|
||||
"cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.",
|
||||
"cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.",
|
||||
"cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/httpsnippet": "3.0.6",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "0.2.1",
|
||||
"@hoppscotch/ui": "0.2.2",
|
||||
"@hoppscotch/vue-toasted": "0.1.0",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@noble/curves": "1.6.0",
|
||||
|
||||
68
packages/hoppscotch-common/src/components.d.ts
vendored
68
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -7,6 +7,9 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
'(chore': fix broken runner for user collection)
|
||||
'(feat': collection runner config in modal)
|
||||
'(fix': run again function)
|
||||
AccessTokens: typeof import('./components/accessTokens/index.vue')['default']
|
||||
AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default']
|
||||
AccessTokensList: typeof import('./components/accessTokens/List.vue')['default']
|
||||
@@ -14,6 +17,7 @@ declare module 'vue' {
|
||||
AiexperimentsMergeView: typeof import('./components/aiexperiments/MergeView.vue')['default']
|
||||
AiexperimentsModifyBodyModal: typeof import('./components/aiexperiments/ModifyBodyModal.vue')['default']
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppAnnouncement: (typeof import("./components/app/Announcement.vue"))["default"]
|
||||
AppBanner: typeof import('./components/app/Banner.vue')['default']
|
||||
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||
@@ -41,6 +45,8 @@ declare module 'vue' {
|
||||
AppSpotlightSearch: typeof import('./components/app/SpotlightSearch.vue')['default']
|
||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||
AppWhatsNewDialog: typeof import('./components/app/WhatsNewDialog.vue')['default']
|
||||
ButtonPrimary: (typeof import("./../../hoppscotch-ui/src/components/button/Primary.vue"))["default"]
|
||||
ButtonSecondary: (typeof import("./../../hoppscotch-ui/src/components/button/Secondary.vue"))["default"]
|
||||
Collections: typeof import('./components/collections/index.vue')['default']
|
||||
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
|
||||
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
|
||||
@@ -66,7 +72,7 @@ declare module 'vue' {
|
||||
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
||||
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsRunner: typeof import('./components/collections/Runner.vue')['default']
|
||||
CollectionsRunner: (typeof import("./components/collections/Runner.vue"))["default"]
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||
@@ -107,8 +113,10 @@ declare module 'vue' {
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartAutoComplete: (typeof import("@hoppscotch/ui"))["HoppSmartAutoComplete"]
|
||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartExpand: (typeof import("@hoppscotch/ui"))["HoppSmartExpand"]
|
||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||
@@ -129,6 +137,8 @@ declare module 'vue' {
|
||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||
HoppTestEnv: (typeof import("@hoppscotch/ui"))["HoppTestEnv"]
|
||||
HoppTestRunnerModal: (typeof import("@hoppscotch/ui"))["HoppTestRunnerModal"]
|
||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||
HttpAuthorizationAkamaiEG: typeof import('./components/http/authorization/AkamaiEG.vue')['default']
|
||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||
@@ -143,6 +153,7 @@ declare module 'vue' {
|
||||
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
||||
HttpCodegen: typeof import('./components/http/Codegen.vue')['default']
|
||||
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
||||
HttpCollectionRunner: (typeof import("./components/http/CollectionRunner.vue"))["default"]
|
||||
HttpExampleLenseBodyRenderer: typeof import('./components/http/example/LenseBodyRenderer.vue')['default']
|
||||
HttpExampleResponse: typeof import('./components/http/example/Response.vue')['default']
|
||||
HttpExampleResponseMeta: typeof import('./components/http/example/ResponseMeta.vue')['default']
|
||||
@@ -151,6 +162,7 @@ declare module 'vue' {
|
||||
HttpHeaders: typeof import('./components/http/Headers.vue')['default']
|
||||
HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
|
||||
HttpKeyValue: typeof import('./components/http/KeyValue.vue')['default']
|
||||
HttpOAuth2Authorization: (typeof import("./components/http/OAuth2Authorization.vue"))["default"]
|
||||
HttpParameters: typeof import('./components/http/Parameters.vue')['default']
|
||||
HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
|
||||
HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
|
||||
@@ -162,19 +174,36 @@ declare module 'vue' {
|
||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||
HttpResponseInterface: typeof import('./components/http/ResponseInterface.vue')['default']
|
||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||
HttpRunner: (typeof import("./components/http/Runner.vue"))["default"]
|
||||
HttpSaveResponseName: typeof import('./components/http/SaveResponseName.vue')['default']
|
||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
|
||||
HttpTestEnv: typeof import('./components/http/test/Env.vue')['default']
|
||||
HttpTestFolder: typeof import('./components/http/test/Folder.vue')['default']
|
||||
HttpTestRequest: typeof import('./components/http/test/Request.vue')['default']
|
||||
HttpTestResponse: typeof import('./components/http/test/Response.vue')['default']
|
||||
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
||||
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
||||
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
||||
HttpTestResultFolder: typeof import('./components/http/test/ResultFolder.vue')['default']
|
||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||
HttpTestResultRequest: typeof import('./components/http/test/ResultRequest.vue')['default']
|
||||
HttpTestRunner: typeof import('./components/http/test/Runner.vue')['default']
|
||||
HttpTestRunnerConfig: typeof import('./components/http/test/RunnerConfig.vue')['default']
|
||||
HttpTestRunnerMeta: typeof import('./components/http/test/RunnerMeta.vue')['default']
|
||||
HttpTestRunnerModal: typeof import('./components/http/test/RunnerModal.vue')['default']
|
||||
HttpTestRunnerResult: typeof import('./components/http/test/RunnerResult.vue')['default']
|
||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||
HttpTestSelector: (typeof import("./components/http/test/Selector.vue"))["default"]
|
||||
HttpTestSelectRequest: (typeof import("./components/http/test/SelectRequest.vue"))["default"]
|
||||
HttpTestTestResult: typeof import('./components/http/test/TestResult.vue')['default']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
IconLucideAlertCircle: (typeof import("~icons/lucide/alert-circle"))["default"]
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"]
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
@@ -184,10 +213,12 @@ declare module 'vue' {
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucidePlay: (typeof import("~icons/lucide/play"))["default"]
|
||||
IconLucidePlaySquare: (typeof import("~icons/lucide/play-square"))["default"]
|
||||
IconLucideRss: (typeof import("~icons/lucide/rss"))["default"]
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
IconLucideVerified: (typeof import("~icons/lucide/verified"))["default"]
|
||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||
@@ -214,6 +245,8 @@ declare module 'vue' {
|
||||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfileShortcode: (typeof import("./components/profile/Shortcode.vue"))["default"]
|
||||
ProfileShortcodes: (typeof import("./components/profile/Shortcodes.vue"))["default"]
|
||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
||||
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
|
||||
@@ -228,14 +261,43 @@ declare module 'vue' {
|
||||
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
|
||||
ShareModal: typeof import('./components/share/Modal.vue')['default']
|
||||
ShareRequest: typeof import('./components/share/Request.vue')['default']
|
||||
ShareRequestModal: (typeof import("./components/share/RequestModal.vue"))["default"]
|
||||
ShareShareRequestModal: (typeof import("./components/share/ShareRequestModal.vue"))["default"]
|
||||
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
|
||||
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
|
||||
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
|
||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||
SmartAnchor: (typeof import("./../../hoppscotch-ui/src/components/smart/Anchor.vue"))["default"]
|
||||
SmartAutoComplete: (typeof import("./../../hoppscotch-ui/src/components/smart/AutoComplete.vue"))["default"]
|
||||
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
|
||||
SmartCheckbox: (typeof import("./../../hoppscotch-ui/src/components/smart/Checkbox.vue"))["default"]
|
||||
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
|
||||
SmartConfirmModal: (typeof import("./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue"))["default"]
|
||||
SmartEncodingPicker: typeof import('./components/smart/EncodingPicker.vue')['default']
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
SmartExpand: (typeof import("./../../hoppscotch-ui/src/components/smart/Expand.vue"))["default"]
|
||||
SmartFileChip: (typeof import("./../../hoppscotch-ui/src/components/smart/FileChip.vue"))["default"]
|
||||
SmartInput: (typeof import("./../../hoppscotch-ui/src/components/smart/Input.vue"))["default"]
|
||||
SmartIntersection: (typeof import("./../../hoppscotch-ui/src/components/smart/Intersection.vue"))["default"]
|
||||
SmartItem: (typeof import("./../../hoppscotch-ui/src/components/smart/Item.vue"))["default"]
|
||||
SmartLink: (typeof import("./../../hoppscotch-ui/src/components/smart/Link.vue"))["default"]
|
||||
SmartModal: (typeof import("./../../hoppscotch-ui/src/components/smart/Modal.vue"))["default"]
|
||||
SmartPicture: (typeof import("./../../hoppscotch-ui/src/components/smart/Picture.vue"))["default"]
|
||||
SmartPlaceholder: (typeof import("./../../hoppscotch-ui/src/components/smart/Placeholder.vue"))["default"]
|
||||
SmartProgressRing: (typeof import("./../../hoppscotch-ui/src/components/smart/ProgressRing.vue"))["default"]
|
||||
SmartRadio: (typeof import("./../../hoppscotch-ui/src/components/smart/Radio.vue"))["default"]
|
||||
SmartRadioGroup: (typeof import("./../../hoppscotch-ui/src/components/smart/RadioGroup.vue"))["default"]
|
||||
SmartSelectWrapper: (typeof import("./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue"))["default"]
|
||||
SmartSlideOver: (typeof import("./../../hoppscotch-ui/src/components/smart/SlideOver.vue"))["default"]
|
||||
SmartSpinner: (typeof import("./../../hoppscotch-ui/src/components/smart/Spinner.vue"))["default"]
|
||||
SmartTab: (typeof import("./../../hoppscotch-ui/src/components/smart/Tab.vue"))["default"]
|
||||
SmartTable: (typeof import("./../../hoppscotch-ui/src/components/smart/Table.vue"))["default"]
|
||||
SmartTabs: (typeof import("./../../hoppscotch-ui/src/components/smart/Tabs.vue"))["default"]
|
||||
SmartToggle: (typeof import("./../../hoppscotch-ui/src/components/smart/Toggle.vue"))["default"]
|
||||
SmartTree: (typeof import("./../../hoppscotch-ui/src/components/smart/Tree.vue"))["default"]
|
||||
SmartTreeBranch: (typeof import("./../../hoppscotch-ui/src/components/smart/TreeBranch.vue"))["default"]
|
||||
SmartWindow: (typeof import("./../../hoppscotch-ui/src/components/smart/Window.vue"))["default"]
|
||||
SmartWindows: (typeof import("./../../hoppscotch-ui/src/components/smart/Windows.vue"))["default"]
|
||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||
Teams: typeof import('./components/teams/index.vue')['default']
|
||||
|
||||
@@ -157,10 +157,8 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
if (tabs.currentActiveTab.value.document.type === "example-response")
|
||||
return
|
||||
|
||||
editingName.value = tabs.currentActiveTab.value.document.request.name
|
||||
if (tabs.currentActiveTab.value.document.type === "request")
|
||||
editingName.value = tabs.currentActiveTab.value.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
@click="emit('add-folder')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="collectionsType === 'team-collections'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlaySquare"
|
||||
:title="t('collection_runner.run_collection')"
|
||||
@@ -133,6 +132,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="runCollectionAction"
|
||||
:icon="IconPlaySquare"
|
||||
:label="t('collection_runner.run_collection')"
|
||||
:shortcut="['T']"
|
||||
@click="
|
||||
() => {
|
||||
emit('run-collection', props.id)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
@@ -195,19 +206,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="collectionsType === 'team-collections'"
|
||||
ref="runCollectionAction"
|
||||
:icon="IconPlaySquare"
|
||||
:label="t('collection_runner.run_collection')"
|
||||
:shortcut="['T']"
|
||||
@click="
|
||||
() => {
|
||||
emit('run-collection', props.id)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
@@ -298,6 +296,7 @@ const emit = defineEmits<{
|
||||
(event: "toggle-children"): void
|
||||
(event: "add-request"): void
|
||||
(event: "add-folder"): void
|
||||
(event: "run-collection"): void
|
||||
(event: "edit-collection"): void
|
||||
(event: "edit-properties"): void
|
||||
(event: "duplicate-collection"): void
|
||||
|
||||
@@ -114,7 +114,7 @@ const t = useI18n()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
loadingState: boolean
|
||||
loadingState?: boolean
|
||||
modelValue?: string
|
||||
requestContext: HoppRESTRequest | null
|
||||
}>(),
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@run-collection="
|
||||
emit('run-collection', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@edit-collection="
|
||||
node.data.type === 'collections' &&
|
||||
emit('edit-collection', {
|
||||
@@ -133,6 +139,12 @@
|
||||
})
|
||||
"
|
||||
folder-type="folder"
|
||||
@run-collection="
|
||||
emit('run-collection', {
|
||||
collectionIndex: node.id,
|
||||
collection: node.data.data.data,
|
||||
})
|
||||
"
|
||||
@add-request="
|
||||
node.data.type === 'folders' &&
|
||||
emit('add-request', {
|
||||
@@ -493,6 +505,13 @@ const emit = defineEmits<{
|
||||
folder: HoppCollection
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "run-collection",
|
||||
payload: {
|
||||
collectionIndex: string
|
||||
collection: HoppCollection
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "edit-collection",
|
||||
payload: {
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
dialog
|
||||
:title="t('collection_runner.run_collection')"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartTabs v-model="activeTab">
|
||||
<HoppSmartTab id="cli" :label="t('collection_runner.cli')">
|
||||
<div class="space-y-4 p-4">
|
||||
<p
|
||||
class="p-4 mb-4 border rounded-md text-amber-500 border-amber-600"
|
||||
>
|
||||
{{ cliCommandGenerationDescription }}
|
||||
</p>
|
||||
|
||||
<div v-if="environmentID" class="flex gap-x-2 items-center">
|
||||
<HoppSmartCheckbox
|
||||
:on="includeEnvironmentID"
|
||||
@change="toggleIncludeEnvironment"
|
||||
/>
|
||||
<span class="truncate"
|
||||
>{{ t("collection_runner.include_active_environment") }}
|
||||
<span class="text-secondaryDark">{{
|
||||
activeEnvironment
|
||||
}}</span></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 rounded-md bg-primaryLight text-secondaryDark select-text"
|
||||
>
|
||||
{{ generatedCLICommand }}
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab id="runner" disabled :label="t('collection_runner.ui')" />
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.copy')}`"
|
||||
:icon="copyIcon"
|
||||
outline
|
||||
@click="copyCLICommandToClipboard"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.close')}`"
|
||||
outline
|
||||
filled
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { SelectedEnvironmentIndex } from "~/newstore/environments"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
collectionID: string
|
||||
environmentID?: string | null
|
||||
selectedEnvironmentIndex: SelectedEnvironmentIndex
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const includeEnvironmentID = ref(false)
|
||||
const activeTab = ref("cli")
|
||||
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const activeEnvironment = computed(() => {
|
||||
const selectedEnv = props.selectedEnvironmentIndex
|
||||
|
||||
if (selectedEnv.type === "TEAM_ENV") {
|
||||
return selectedEnv.environment.name
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const isCloudInstance = window.location.hostname === "hoppscotch.io"
|
||||
|
||||
const cliCommandGenerationDescription = computed(() => {
|
||||
if (isCloudInstance) {
|
||||
return t("collection_runner.cli_command_generation_description_cloud")
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_BACKEND_API_URL) {
|
||||
return t("collection_runner.cli_command_generation_description_sh")
|
||||
}
|
||||
|
||||
return t(
|
||||
"collection_runner.cli_command_generation_description_sh_with_server_url_placeholder"
|
||||
)
|
||||
})
|
||||
|
||||
const generatedCLICommand = computed(() => {
|
||||
const { collectionID, environmentID } = props
|
||||
|
||||
const environmentFlag =
|
||||
includeEnvironmentID.value && environmentID ? `-e ${environmentID}` : ""
|
||||
|
||||
const serverUrl = import.meta.env.VITE_BACKEND_API_URL?.endsWith("/v1")
|
||||
? // Removing `/v1` prefix
|
||||
import.meta.env.VITE_BACKEND_API_URL.slice(0, -3)
|
||||
: "<server_url>"
|
||||
|
||||
const serverFlag = isCloudInstance ? "" : `--server ${serverUrl}`
|
||||
|
||||
return `hopp test ${collectionID} ${environmentFlag} --token <access_token> ${serverFlag}`
|
||||
})
|
||||
|
||||
const toggleIncludeEnvironment = () => {
|
||||
includeEnvironmentID.value = !includeEnvironmentID.value
|
||||
}
|
||||
|
||||
const copyCLICommandToClipboard = () => {
|
||||
copyToClipboard(generatedCLICommand.value)
|
||||
copyIcon.value = IconCheck
|
||||
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -33,6 +33,13 @@
|
||||
:filter-text="filterTexts"
|
||||
:save-request="saveRequest"
|
||||
:picked="picked"
|
||||
@run-collection="
|
||||
runCollectionHandler({
|
||||
type: 'my-collections',
|
||||
collectionID: $event.collection._ref_id,
|
||||
collectionIndex: $event.collectionIndex,
|
||||
})
|
||||
"
|
||||
@add-folder="addFolder"
|
||||
@add-request="addRequest"
|
||||
@edit-request="editRequest"
|
||||
@@ -99,7 +106,12 @@
|
||||
@remove-folder="removeFolder"
|
||||
@remove-request="removeRequest"
|
||||
@remove-response="removeResponse"
|
||||
@run-collection="runCollectionHandler"
|
||||
@run-collection="
|
||||
runCollectionHandler({
|
||||
type: 'team-collections',
|
||||
collectionID: $event,
|
||||
})
|
||||
"
|
||||
@share-request="shareRequest"
|
||||
@select-request="selectRequest"
|
||||
@select-response="selectResponse"
|
||||
@@ -193,11 +205,9 @@
|
||||
/>
|
||||
|
||||
<!-- `selectedCollectionID` is guaranteed to be a string when `showCollectionsRunnerModal` is `true` -->
|
||||
<CollectionsRunner
|
||||
v-if="showCollectionsRunnerModal"
|
||||
:collection-i-d="selectedCollectionID!"
|
||||
:environment-i-d="activeEnvironmentID"
|
||||
:selected-environment-index="selectedEnvironmentIndex"
|
||||
<HttpTestRunnerModal
|
||||
v-if="showCollectionsRunnerModal && collectionRunnerData"
|
||||
:collection-runner-data="collectionRunnerData"
|
||||
@hide-modal="showCollectionsRunnerModal = false"
|
||||
/>
|
||||
</div>
|
||||
@@ -207,6 +217,7 @@
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
getDefaultRESTRequest,
|
||||
HoppCollection,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeaders,
|
||||
@@ -218,7 +229,7 @@ import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { cloneDeep, debounce, isEqual } from "lodash-es"
|
||||
import { PropType, computed, nextTick, onMounted, ref, watch } from "vue"
|
||||
import { useReadonlyStream, useStream } from "~/composables/stream"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
@@ -278,10 +289,7 @@ import {
|
||||
updateRESTCollectionOrder,
|
||||
updateRESTRequestOrder,
|
||||
} from "~/newstore/collections"
|
||||
import {
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
import { platform } from "~/platform"
|
||||
@@ -292,7 +300,7 @@ import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
|
||||
import { RESTOptionTabs } from "../http/RequestOptions.vue"
|
||||
import { Collection as NodeCollection } from "./MyCollections.vue"
|
||||
import { EditingProperties } from "./Properties.vue"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { CollectionRunnerData } from "../http/test/RunnerModal.vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -383,15 +391,6 @@ const teamLoadingCollections = useReadonlyStream(
|
||||
[]
|
||||
)
|
||||
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(undefined)
|
||||
const teamEnvironmentList = useReadonlyStream(
|
||||
teamEnvironmentAdapter.teamEnvironmentList$,
|
||||
[]
|
||||
)
|
||||
const selectedEnvironmentIndex = useStream(
|
||||
selectedEnvironmentIndex$,
|
||||
{ type: "NO_ENV_SELECTED" },
|
||||
setSelectedEnvironmentIndex
|
||||
)
|
||||
|
||||
const {
|
||||
cascadeParentCollectionForHeaderAuthForSearchResults,
|
||||
@@ -692,8 +691,7 @@ const showConfirmModal = ref(false)
|
||||
const showTeamModalAdd = ref(false)
|
||||
|
||||
const showCollectionsRunnerModal = ref(false)
|
||||
const selectedCollectionID = ref<string | null>(null)
|
||||
const activeEnvironmentID = ref<string | null | undefined>(null)
|
||||
const collectionRunnerData = ref<CollectionRunnerData | null>(null)
|
||||
|
||||
const displayModalAdd = (show: boolean) => {
|
||||
showModalAdd.value = show
|
||||
@@ -837,7 +835,9 @@ const onAddRequest = (requestName: string) => {
|
||||
if (!request) return
|
||||
|
||||
const newRequest = {
|
||||
...cloneDeep(request),
|
||||
...(tabs.currentActiveTab.value.document.type === "request"
|
||||
? cloneDeep(tabs.currentActiveTab.value.document.request)
|
||||
: getDefaultRESTRequest()),
|
||||
name: requestName,
|
||||
}
|
||||
|
||||
@@ -849,9 +849,9 @@ const onAddRequest = (requestName: string) => {
|
||||
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
|
||||
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
@@ -904,9 +904,9 @@ const onAddRequest = (requestName: string) => {
|
||||
const { auth, headers } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
saveContext: {
|
||||
originLocation: "team-collection",
|
||||
requestID: createRequestInCollection.id,
|
||||
@@ -1973,13 +1973,13 @@ const selectRequest = (selectedRequest: {
|
||||
requestID: requestIndex,
|
||||
})
|
||||
|
||||
if (possibleTab) {
|
||||
if (possibleTab && possibleTab.value.document.type === "request") {
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
} else {
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: cloneDeep(request),
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
saveContext: {
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
@@ -2004,9 +2004,9 @@ const selectRequest = (selectedRequest: {
|
||||
} else {
|
||||
// If not, open the request in a new tab
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: cloneDeep(request),
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath!,
|
||||
@@ -2866,25 +2866,9 @@ const setCollectionProperties = (newCollection: {
|
||||
displayModalEditProperties(false)
|
||||
}
|
||||
|
||||
const runCollectionHandler = (collectionID: string) => {
|
||||
selectedCollectionID.value = collectionID
|
||||
const runCollectionHandler = (payload: CollectionRunnerData) => {
|
||||
collectionRunnerData.value = payload
|
||||
showCollectionsRunnerModal.value = true
|
||||
|
||||
const activeWorkspace = workspace.value
|
||||
const currentEnv = selectedEnvironmentIndex.value
|
||||
|
||||
if (["NO_ENV_SELECTED", "MY_ENV"].includes(currentEnv.type)) {
|
||||
activeEnvironmentID.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (activeWorkspace.type === "team" && currentEnv.type === "TEAM_ENV") {
|
||||
activeEnvironmentID.value = teamEnvironmentList.value.find(
|
||||
(env) =>
|
||||
env.teamID === activeWorkspace.teamID &&
|
||||
env.environment.id === currentEnv.environment.id
|
||||
)?.environment.id
|
||||
}
|
||||
}
|
||||
|
||||
const resolveConfirmModal = (title: string | null) => {
|
||||
|
||||
@@ -298,9 +298,9 @@ const clearHistory = () => {
|
||||
const tabs = useService(RESTTabService)
|
||||
const useHistory = (entry: RESTHistoryEntry) => {
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: entry.request,
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -135,9 +135,7 @@
|
||||
:key="`result-${index}`"
|
||||
class="flex items-center px-4 py-2"
|
||||
>
|
||||
<div
|
||||
class="flex flex-shrink flex-shrink-0 items-center overflow-x-auto"
|
||||
>
|
||||
<div class="flex flex-shrink-0 items-center overflow-x-auto">
|
||||
<component
|
||||
:is="result.status === 'pass' ? IconCheck : IconClose"
|
||||
class="svg-icons mr-4"
|
||||
@@ -146,7 +144,7 @@
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
class="flex flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
>
|
||||
<span
|
||||
v-if="result.message"
|
||||
@@ -237,9 +235,15 @@ import { useColorMode } from "~/composables/theming"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
showEmptyMessage?: boolean
|
||||
}>(),
|
||||
{
|
||||
showEmptyMessage: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTestResult | null | undefined): void
|
||||
|
||||
@@ -8,57 +8,87 @@
|
||||
</span>
|
||||
<div v-if="testResults.expectResults" class="divide-y divide-dividerLight">
|
||||
<HttpTestResultReport
|
||||
v-if="testResults.expectResults.length"
|
||||
v-if="testResults.expectResults.length && !shouldHideResultReport"
|
||||
:test-results="testResults"
|
||||
/>
|
||||
<div
|
||||
|
||||
<template
|
||||
v-for="(result, index) in testResults.expectResults"
|
||||
:key="`result-${index}`"
|
||||
class="flex items-center px-4 py-2"
|
||||
>
|
||||
<div
|
||||
class="flex flex-shrink flex-shrink-0 items-center overflow-x-auto"
|
||||
v-if="shouldShowResult(result.status)"
|
||||
class="flex items-center px-4 py-2"
|
||||
>
|
||||
<component
|
||||
:is="result.status === 'pass' ? IconCheck : IconClose"
|
||||
class="svg-icons mr-4"
|
||||
:class="
|
||||
result.status === 'pass' ? 'text-green-500' : 'text-red-500'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
>
|
||||
<span v-if="result.message" class="inline-flex text-secondaryDark">
|
||||
{{ result.message }}
|
||||
</span>
|
||||
<span class="inline-flex text-secondaryLight">
|
||||
<icon-lucide-minus class="svg-icons mr-2" />
|
||||
{{
|
||||
result.status === "pass" ? t("test.passed") : t("test.failed")
|
||||
}}
|
||||
</span>
|
||||
<div class="flex flex-shrink-0 items-center overflow-x-auto">
|
||||
<component
|
||||
:is="result.status === 'pass' ? IconCheck : IconClose"
|
||||
class="svg-icons mr-4"
|
||||
:class="
|
||||
result.status === 'pass' ? 'text-green-500' : 'text-red-500'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
>
|
||||
<span
|
||||
v-if="result.message"
|
||||
class="inline-flex text-secondaryDark"
|
||||
>
|
||||
{{ result.message }}
|
||||
</span>
|
||||
<span class="inline-flex text-secondaryLight">
|
||||
<icon-lucide-minus class="svg-icons mr-2" />
|
||||
{{
|
||||
result.status === "pass" ? t("test.passed") : t("test.failed")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { computed } from "vue"
|
||||
import {
|
||||
HoppTestResult,
|
||||
HoppTestExpectResult,
|
||||
} from "~/helpers/types/HoppTestResult"
|
||||
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconClose from "~icons/lucide/x"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps({
|
||||
testResults: {
|
||||
type: Object as PropType<HoppTestResult>,
|
||||
required: true,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
testResults: HoppTestResult
|
||||
showTestType: "all" | "passed" | "failed"
|
||||
}>(),
|
||||
{
|
||||
showTestType: "all",
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Determines if a test result should be displayed based on the filter type
|
||||
*/
|
||||
function shouldShowResult(status: HoppTestExpectResult["status"]): boolean {
|
||||
if (props.showTestType === "all") return true
|
||||
if (props.showTestType === "passed" && status === "pass") return true
|
||||
if (props.showTestType === "failed" && status === "fail") return true
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldHideResultReport = computed(() => {
|
||||
if (props.showTestType === "all") return false
|
||||
|
||||
return props.testResults.expectResults.some(
|
||||
(result) => result.status === "pass" || result.status === "fail"
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-4 py-2">
|
||||
<div class="flex flex-shrink flex-shrink-0 items-center overflow-x-auto">
|
||||
<div class="flex flex-shrink items-center overflow-x-auto">
|
||||
<component
|
||||
:is="getIcon(status)"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -8,9 +8,7 @@
|
||||
:class="getStyle(status)"
|
||||
:title="`${t(getTooltip(status))}`"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
>
|
||||
<div class="flex flex-shrink items-center space-x-2 overflow-x-auto">
|
||||
<span class="inline-flex text-secondaryDark">
|
||||
{{ env.key }}
|
||||
</span>
|
||||
@@ -51,7 +49,7 @@ type Props = {
|
||||
previousValue?: string
|
||||
}
|
||||
status: Status
|
||||
global: boolean
|
||||
global?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
|
||||
104
packages/hoppscotch-common/src/components/http/test/Env.vue
Normal file
104
packages/hoppscotch-common/src/components/http/test/Env.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<span v-if="show">
|
||||
{{ envName ?? t("filter.none") }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useService } from "dioc/vue"
|
||||
import { computed, watch } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useReadonlyStream, useStream } from "~/composables/stream"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import {
|
||||
environments$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
show?: boolean
|
||||
}>(),
|
||||
{
|
||||
show: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select-env", data: any): void
|
||||
}>()
|
||||
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const envName = computed(() => selectedEnv.value?.name ?? null)
|
||||
|
||||
const currentEnv = useStream(
|
||||
selectedEnvironmentIndex$,
|
||||
{ type: "NO_ENV_SELECTED" },
|
||||
setSelectedEnvironmentIndex
|
||||
)
|
||||
|
||||
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(
|
||||
workspace.value.type === "team" ? workspace.value.teamID : undefined
|
||||
)
|
||||
const teamEnvironmentList = useReadonlyStream(
|
||||
teamEnvironmentAdapter.teamEnvironmentList$,
|
||||
[]
|
||||
)
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
const activeWorkspace = workspace.value
|
||||
|
||||
export type CurrentEnv =
|
||||
| {
|
||||
type: "MY_ENV"
|
||||
index: number
|
||||
name: string
|
||||
}
|
||||
| { type: "TEAM_ENV"; name: string; teamEnvID: string }
|
||||
| null
|
||||
|
||||
const selectedEnv = computed<CurrentEnv>(() => {
|
||||
if (
|
||||
activeWorkspace.type === "personal" &&
|
||||
currentEnv.value.type === "MY_ENV"
|
||||
) {
|
||||
const environment = myEnvironments.value[currentEnv.value.index]
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: currentEnv.value.index,
|
||||
name: environment.name,
|
||||
}
|
||||
}
|
||||
|
||||
if (activeWorkspace.type === "team" && currentEnv.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find((env) => {
|
||||
return (
|
||||
env.id ===
|
||||
(currentEnv.value.type === "TEAM_ENV" && currentEnv.value.teamEnvID)
|
||||
)
|
||||
})
|
||||
|
||||
if (teamEnv) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: currentEnv.value.teamEnvID,
|
||||
}
|
||||
}
|
||||
}
|
||||
return null // Return null or a default value if no environment is selected
|
||||
})
|
||||
|
||||
watch(
|
||||
() => selectedEnv.value,
|
||||
(newVal) => {
|
||||
if (newVal) emit("select-env", newVal)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="h-1 w-full transition"></div>
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-1 min-w-0">
|
||||
<span
|
||||
class="flex items-center justify-center px-4 pointer-events-none"
|
||||
>
|
||||
<HoppSmartCheckbox
|
||||
v-if="showSelection"
|
||||
:on="isSelected"
|
||||
class="mr-2"
|
||||
/>
|
||||
<component :is="IconFolderOpen" class="svg-icons" />
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ collectionName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
|
||||
type FolderType = "collection" | "folder"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string
|
||||
parentID?: string | null
|
||||
data: HoppCollection | TeamCollection
|
||||
/**
|
||||
* Collection component can be used for both collections and folders.
|
||||
* folderType is used to determine which one it is.
|
||||
*/
|
||||
folderType: FolderType
|
||||
isOpen: boolean
|
||||
isSelected?: boolean
|
||||
exportLoading?: boolean
|
||||
hasNoTeamAccess?: boolean
|
||||
collectionMoveLoading?: string[]
|
||||
isLastItem?: boolean
|
||||
showSelection?: boolean
|
||||
}>(),
|
||||
{
|
||||
id: "",
|
||||
parentID: null,
|
||||
folderType: "collection",
|
||||
isOpen: false,
|
||||
isSelected: false,
|
||||
exportLoading: false,
|
||||
hasNoTeamAccess: false,
|
||||
isLastItem: false,
|
||||
showSelection: false,
|
||||
}
|
||||
)
|
||||
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const collectionName = computed(() => {
|
||||
if ((props.data as HoppCollection).name)
|
||||
return (props.data as HoppCollection).name
|
||||
return (props.data as TeamCollection).title
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.exportLoading,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
options.value!.tippy?.hide()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="h-1 w-full transition"></div>
|
||||
<div class="flex items-stretch group">
|
||||
<div
|
||||
class="flex items-center justify-center flex-1 min-w-0 cursor-pointer pointer-events-auto"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-2 truncate pointer-events-none"
|
||||
:style="{ color: requestLabelColor }"
|
||||
>
|
||||
<HoppSmartCheckbox
|
||||
v-if="showSelection"
|
||||
:on="isSelected"
|
||||
:name="`request-${requestID}`"
|
||||
class="mx-2 ml-4"
|
||||
@change="selectRequest()"
|
||||
/>
|
||||
<span class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 pointer-events-none transition group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isActive"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
||||
:title="`${t('collection.request_in_use')}`"
|
||||
>
|
||||
<span
|
||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
request: HoppRESTRequest
|
||||
requestID?: string
|
||||
parentID: string | null
|
||||
isActive?: boolean
|
||||
isSelected?: boolean
|
||||
showSelection?: boolean
|
||||
}>(),
|
||||
{
|
||||
parentID: null,
|
||||
isActive: false,
|
||||
isSelected: false,
|
||||
showSelection: false,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select-request"): void
|
||||
}>()
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(props.request.method)
|
||||
)
|
||||
|
||||
const selectRequest = () => {
|
||||
emit("select-request")
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="relative flex flex-1 flex-col">
|
||||
<HttpResponseMeta :response="doc.response" :is-embed="false" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="hasResponse"
|
||||
v-model:document="doc"
|
||||
:is-editable="false"
|
||||
:show-response="showResponse"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed } from "vue"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
const props = defineProps<{
|
||||
showResponse: boolean
|
||||
document: TestRunnerRequest
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRequestDocument): void
|
||||
}>()
|
||||
|
||||
const doc = useVModel(props, "document", emit)
|
||||
|
||||
const hasResponse = computed(
|
||||
() =>
|
||||
doc.value.response?.type === "success" ||
|
||||
doc.value.response?.type === "fail"
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="h-1 w-full transition"></div>
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-1 min-w-0">
|
||||
<span
|
||||
class="flex items-center justify-center px-4 pointer-events-none"
|
||||
>
|
||||
<HoppSmartCheckbox
|
||||
v-if="showSelection"
|
||||
:on="isSelected"
|
||||
class="mr-2"
|
||||
/>
|
||||
<component :is="IconFolderOpen" class="svg-icons" />
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ collectionName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
|
||||
type FolderType = "collection" | "folder"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string
|
||||
parentID?: string | null
|
||||
data: HoppCollection | TeamCollection
|
||||
/**
|
||||
* Collection component can be used for both collections and folders.
|
||||
* folderType is used to determine which one it is.
|
||||
*/
|
||||
folderType: FolderType
|
||||
isOpen: boolean
|
||||
isSelected?: boolean
|
||||
exportLoading?: boolean
|
||||
hasNoTeamAccess?: boolean
|
||||
collectionMoveLoading?: string[]
|
||||
isLastItem?: boolean
|
||||
showSelection?: boolean
|
||||
}>(),
|
||||
{
|
||||
id: "",
|
||||
parentID: null,
|
||||
folderType: "collection",
|
||||
isOpen: false,
|
||||
isSelected: false,
|
||||
exportLoading: false,
|
||||
hasNoTeamAccess: false,
|
||||
isLastItem: false,
|
||||
showSelection: false,
|
||||
}
|
||||
)
|
||||
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
|
||||
const collectionName = computed(() => {
|
||||
if ((props.data as HoppCollection).name)
|
||||
return (props.data as HoppCollection).name
|
||||
return (props.data as TeamCollection).title
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.exportLoading,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
options.value!.tippy?.hide()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="flex items-stretch group ml-4 flex-col">
|
||||
<button
|
||||
class="w-full rounded px-4 py-3 transition cursor-pointer focus:outline-none hover:active hover:bg-primaryLight hover:text-secondaryDark"
|
||||
@click="selectRequest()"
|
||||
>
|
||||
<div class="flex gap-4 mb-1">
|
||||
<span
|
||||
class="flex items-center justify-center truncate pointer-events-none"
|
||||
:style="{ color: requestLabelColor }"
|
||||
>
|
||||
<span class="font-bold truncate">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="truncate text-sm text-secondaryDark">
|
||||
{{ request.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="request.response?.statusCode"
|
||||
:class="[
|
||||
statusCategory.className,
|
||||
'outlined text-xs rounded-md px-2 flex items-center',
|
||||
]"
|
||||
>
|
||||
{{ `${request.response?.statusCode}` }}
|
||||
</span>
|
||||
<span v-if="isLoading" class="flex flex-col items-center">
|
||||
<HoppSmartSpinner />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-left text-secondaryLight text-sm">
|
||||
{{ request.endpoint }}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="request.error"
|
||||
class="py-2 pl-4 ml-4 mb-2 border-l text-red-500 border-red-500"
|
||||
>
|
||||
<span> {{ request.error }} </span>
|
||||
</div>
|
||||
<HttpTestTestResult
|
||||
v-if="request.testResults"
|
||||
:model-value="request.testResults"
|
||||
:show-test-type="showTestType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
request: TestRunnerRequest
|
||||
requestID?: string
|
||||
parentID: string | null
|
||||
isActive?: boolean
|
||||
isSelected?: boolean
|
||||
showSelection?: boolean
|
||||
showTestType: "all" | "passed" | "failed"
|
||||
}>(),
|
||||
{
|
||||
parentID: null,
|
||||
isActive: false,
|
||||
isSelected: false,
|
||||
showSelection: false,
|
||||
requestID: "",
|
||||
}
|
||||
)
|
||||
|
||||
const isLoading = computed(() => props.request?.isLoading)
|
||||
|
||||
const statusCategory = computed(() => {
|
||||
if (
|
||||
props.request?.response === null ||
|
||||
props.request?.response === undefined ||
|
||||
props.request?.response.type === "loading" ||
|
||||
props.request?.response.type === "network_fail" ||
|
||||
props.request?.response.type === "script_fail" ||
|
||||
props.request?.response.type === "fail" ||
|
||||
props.request?.response.type === "extension_error"
|
||||
)
|
||||
return {
|
||||
name: "error",
|
||||
className: "text-red-500",
|
||||
}
|
||||
return findStatusGroup(props.request?.response.statusCode)
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select-request"): void
|
||||
}>()
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(props.request.method)
|
||||
)
|
||||
|
||||
const selectRequest = () => {
|
||||
emit("select-request")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active {
|
||||
@apply after:bg-accentLight;
|
||||
}
|
||||
</style>
|
||||
335
packages/hoppscotch-common/src/components/http/test/Runner.vue
Normal file
335
packages/hoppscotch-common/src/components/http/test/Runner.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="test-runner-primary">
|
||||
<template #primary>
|
||||
<div
|
||||
class="flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary sticky top-0 z-20"
|
||||
>
|
||||
<div class="inline-flex flex-1 gap-8">
|
||||
<HttpTestRunnerMeta
|
||||
:heading="t('collection.title')"
|
||||
:text="collectionName"
|
||||
/>
|
||||
|
||||
<template v-if="showResult">
|
||||
<HttpTestRunnerMeta :heading="t('environment.heading')">
|
||||
<HttpTestEnv />
|
||||
</HttpTestRunnerMeta>
|
||||
<!-- <HttpTestRunnerMeta :heading="t('test.iterations')" :text="'1'" /> -->
|
||||
<HttpTestRunnerMeta
|
||||
:heading="t('test.duration')"
|
||||
:text="duration ? msToHumanReadable(duration) : '...'"
|
||||
/>
|
||||
<HttpTestRunnerMeta
|
||||
:heading="t('test.avg_resp')"
|
||||
:text="
|
||||
avgResponseTime ? msToHumanReadable(avgResponseTime) : '...'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HoppButtonPrimary
|
||||
v-if="showResult && tab.document.status === 'running'"
|
||||
:label="t('test.stop')"
|
||||
class="w-32"
|
||||
@click="stopTests()"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
:label="t('test.run_again')"
|
||||
class="w-32"
|
||||
@click="runAgain()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="showResult && tab.document.status !== 'running'"
|
||||
:icon="IconPlus"
|
||||
:label="t('test.new_run')"
|
||||
filled
|
||||
outline
|
||||
@click="newRun()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HttpTestRunnerResult
|
||||
v-if="showResult"
|
||||
:tab="tab"
|
||||
:collection-adapter="collectionAdapter"
|
||||
:is-running="tab.document.status === 'running'"
|
||||
@on-change-tab="showTestsType = $event as 'all' | 'passed' | 'failed'"
|
||||
@on-select-request="onSelectRequest"
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpTestResponse
|
||||
v-if="selectedRequest && selectedRequest.response"
|
||||
v-model:document="selectedRequest"
|
||||
:show-response="tab.document.config.persistResponses"
|
||||
/>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="
|
||||
!testRunnerConfig.persistResponses && !selectedRequest?.response
|
||||
"
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
:alt="`${t('collection_runner.no_response_persist')}`"
|
||||
:text="`${t('collection_runner.no_response_persist')}`"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonPrimary
|
||||
:label="t('test.new_run')"
|
||||
@click="showCollectionsRunnerModal = true"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<div
|
||||
v-else-if="tab.document.status === 'running'"
|
||||
class="flex flex-col items-center gap-4 justify-center h-full"
|
||||
>
|
||||
<HoppSmartSpinner />
|
||||
<span> {{ t("collection_runner.running_collection") }}... </span>
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="!selectedRequest"
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
:alt="`${t('collection_runner.select_request')}`"
|
||||
:text="`${t('collection_runner.select_request')}`"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
|
||||
<HttpTestRunnerModal
|
||||
v-if="showCollectionsRunnerModal"
|
||||
:same-tab="true"
|
||||
:collection-runner-data="
|
||||
tab.document.collectionType === 'my-collections'
|
||||
? {
|
||||
type: 'my-collections',
|
||||
collectionID: tab.document.collectionID,
|
||||
}
|
||||
: {
|
||||
type: 'team-collections',
|
||||
collectionID: tab.document.collectionID,
|
||||
}
|
||||
"
|
||||
@hide-modal="showCollectionsRunnerModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { SmartTreeAdapter } from "@hoppscotch/ui"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { computed, nextTick, onMounted, ref } from "vue"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
getCompleteCollectionTree,
|
||||
teamCollToHoppRESTColl,
|
||||
} from "~/helpers/backend/helpers"
|
||||
import { HoppTestRunnerDocument } from "~/helpers/rest/document"
|
||||
import {
|
||||
CollectionNode,
|
||||
TestRunnerCollectionsAdapter,
|
||||
} from "~/helpers/runner/adapter"
|
||||
import { getErrorMessage } from "~/helpers/runner/collection-tree"
|
||||
import { getRESTCollectionByRefId } from "~/newstore/collections"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import {
|
||||
TestRunnerRequest,
|
||||
TestRunnerService,
|
||||
} from "~/services/test-runner/test-runner.service"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppTestRunnerDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTab<HoppTestRunnerDocument>): void
|
||||
}>()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
const duration = computed(() => tab.value.document.testRunnerMeta.totalTime)
|
||||
const avgResponseTime = computed(() =>
|
||||
calculateAverageTime(
|
||||
tab.value.document.testRunnerMeta.totalTime,
|
||||
tab.value.document.testRunnerMeta.completedRequests
|
||||
)
|
||||
)
|
||||
|
||||
function msToHumanReadable(ms: number) {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const milliseconds = ms % 1000
|
||||
|
||||
let result = ""
|
||||
if (seconds > 0) {
|
||||
result += `${seconds}s `
|
||||
}
|
||||
result += `${milliseconds}ms`
|
||||
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
const selectedRequest = computed(() => tab.value.document.request)
|
||||
|
||||
const onSelectRequest = async (request: TestRunnerRequest) => {
|
||||
tab.value.document.request = null
|
||||
await nextTick() // HACK: To ensure the request is cleared before setting the new request. there is a bug in the response component that doesn't change to the valid lens when the response is changed.
|
||||
tab.value.document.request = request
|
||||
}
|
||||
|
||||
const collectionName = computed(() =>
|
||||
props.modelValue.document.type === "test-runner"
|
||||
? props.modelValue.document.collection.name
|
||||
: ""
|
||||
)
|
||||
|
||||
const testRunnerConfig = computed(() => tab.value.document.config)
|
||||
|
||||
const collection = computed(() => {
|
||||
return tab.value.document.collection
|
||||
})
|
||||
|
||||
// for re-run config
|
||||
const showCollectionsRunnerModal = ref(false)
|
||||
const selectedCollectionID = ref<string>()
|
||||
|
||||
const testRunnerStopRef = ref(false)
|
||||
const showResult = computed(() => {
|
||||
return (
|
||||
tab.value.document.status === "running" ||
|
||||
tab.value.document.status === "stopped" ||
|
||||
tab.value.document.status === "error"
|
||||
)
|
||||
})
|
||||
|
||||
const runTests = async () => {
|
||||
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
|
||||
testRunnerService.runTests(tab, collection.value, {
|
||||
...testRunnerConfig.value,
|
||||
stopRef: testRunnerStopRef,
|
||||
})
|
||||
}
|
||||
|
||||
const stopTests = () => {
|
||||
testRunnerStopRef.value = true
|
||||
// when we manually stop the test runner, we need to update the tab document with the current state
|
||||
tab.value.document.testRunnerMeta = {
|
||||
...tab.value.document.testRunnerMeta,
|
||||
}
|
||||
}
|
||||
|
||||
const runAgain = async () => {
|
||||
tab.value.document.resultCollection = undefined
|
||||
await nextTick()
|
||||
resetRunnerState()
|
||||
const updatedCollection = await refetchCollectionTree()
|
||||
|
||||
if (updatedCollection) {
|
||||
if (checkIfCollectionIsEmpty(updatedCollection)) {
|
||||
tabs.closeTab(tab.value.id)
|
||||
toast.error(t("collection_runner.empty_collection"))
|
||||
return
|
||||
}
|
||||
|
||||
tab.value.document.collection = updatedCollection
|
||||
await nextTick()
|
||||
runTests()
|
||||
} else {
|
||||
tabs.closeTab(tab.value.id)
|
||||
toast.error(t("collection_runner.collection_not_found"))
|
||||
}
|
||||
}
|
||||
|
||||
const resetRunnerState = () => {
|
||||
tab.value.document.testRunnerMeta = {
|
||||
failedTests: 0,
|
||||
passedTests: 0,
|
||||
totalTests: 0,
|
||||
totalRequests: 0,
|
||||
totalTime: 0,
|
||||
completedRequests: 0,
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (tab.value.document.status === "idle") runTests()
|
||||
if (
|
||||
tab.value.document.status === "stopped" ||
|
||||
tab.value.document.status === "error"
|
||||
) {
|
||||
}
|
||||
})
|
||||
|
||||
function calculateAverageTime(
|
||||
totalTime: number,
|
||||
completedRequests: number
|
||||
): number {
|
||||
return completedRequests > 0 ? Math.round(totalTime / completedRequests) : 0
|
||||
}
|
||||
|
||||
const newRun = () => {
|
||||
showCollectionsRunnerModal.value = true
|
||||
selectedCollectionID.value = collection.value.id
|
||||
}
|
||||
|
||||
const testRunnerService = useService(TestRunnerService)
|
||||
|
||||
const result = computed(() => {
|
||||
return tab.value.document.resultCollection
|
||||
? [tab.value.document.resultCollection]
|
||||
: []
|
||||
})
|
||||
|
||||
const showTestsType = ref<"all" | "passed" | "failed">("all")
|
||||
|
||||
const collectionAdapter: SmartTreeAdapter<CollectionNode> =
|
||||
new TestRunnerCollectionsAdapter(result, showTestsType)
|
||||
|
||||
/**
|
||||
* refetches the collection tree from the backend
|
||||
* @returns collection tree
|
||||
*/
|
||||
const refetchCollectionTree = async () => {
|
||||
if (!tab.value.document.collectionID) return
|
||||
const type = tab.value.document.collectionType
|
||||
if (type === "my-collections") {
|
||||
return getRESTCollectionByRefId(tab.value.document.collectionID)
|
||||
}
|
||||
|
||||
return pipe(
|
||||
getCompleteCollectionTree(tab.value.document.collectionID),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err, t)}`)
|
||||
return
|
||||
},
|
||||
async (coll) => {
|
||||
return teamCollToHoppRESTColl(coll)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
function checkIfCollectionIsEmpty(collection: HoppCollection): boolean {
|
||||
// Check if the collection has requests or if any child collection is non-empty
|
||||
return (
|
||||
collection.requests.length === 0 &&
|
||||
collection.folders.every((folder) => checkIfCollectionIsEmpty(folder))
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs text-secondaryLight mb-1 truncate">
|
||||
{{ heading }}
|
||||
</span>
|
||||
<span class="text-sm font-bold text-secondaryDark truncate">
|
||||
<slot>
|
||||
{{ text }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
heading: string
|
||||
text?: string
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
dialog
|
||||
:title="t('collection_runner.run_collection')"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartTabs v-model="activeTab">
|
||||
<HoppSmartTab id="test-runner" :label="t('collection_runner.ui')">
|
||||
<div
|
||||
class="flex-shrink-0 w-full h-full p-4 overflow-auto overflow-x-auto bg-primary"
|
||||
>
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("collection_runner.run_config") }}
|
||||
</h4>
|
||||
<div class="mt-4">
|
||||
<!-- TODO: fix input component types. so that it accepts number -->
|
||||
<HoppSmartInput
|
||||
v-model="config.delay as any"
|
||||
type="number"
|
||||
:label="t('collection_runner.delay')"
|
||||
class="!rounded-r-none !border-r-0"
|
||||
input-styles="floating-input !rounded-r-none !border-r-0"
|
||||
>
|
||||
<template #button>
|
||||
<span
|
||||
class="px-4 py-2 font-semibold border rounded-r bg-primaryLight border-divider text-secondaryLight"
|
||||
>
|
||||
ms
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6">
|
||||
<span class="text-xs text-secondaryLight">
|
||||
{{ t("collection_runner.advanced_settings") }}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-col gap-4 mt-4 items-start">
|
||||
<HoppSmartCheckbox
|
||||
class="pr-2"
|
||||
:on="config.stopOnError"
|
||||
@change="config.stopOnError = !config.stopOnError"
|
||||
>
|
||||
<span>
|
||||
{{ t("collection_runner.stop_on_error") }}
|
||||
</span>
|
||||
</HoppSmartCheckbox>
|
||||
|
||||
<HoppSmartCheckbox
|
||||
class="pr-2"
|
||||
:on="config.persistResponses"
|
||||
@change="config.persistResponses = !config.persistResponses"
|
||||
>
|
||||
<span>
|
||||
{{ t("collection_runner.persist_responses") }}
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
class="!py-0 pl-2"
|
||||
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
</HoppSmartCheckbox>
|
||||
|
||||
<!-- <HoppSmartCheckbox
|
||||
class="pr-2"
|
||||
:on="config.keepVariableValues"
|
||||
@change="
|
||||
config.keepVariableValues = !config.keepVariableValues
|
||||
"
|
||||
>
|
||||
<span>Keep variable values</span>
|
||||
<HoppButtonSecondary
|
||||
class="!py-0 pl-2"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
</HoppSmartCheckbox> -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
id="cli"
|
||||
:label="`${t('collection_runner.cli')} ${
|
||||
!CLICommand ? '(Team Collections Only)' : ''
|
||||
}`"
|
||||
:disabled="!CLICommand"
|
||||
>
|
||||
<HttpTestEnv :show="false" @select-env="setCurrentEnv" />
|
||||
|
||||
<div class="space-y-4 p-4">
|
||||
<p
|
||||
class="p-4 mb-4 border rounded-md text-amber-500 border-amber-600"
|
||||
>
|
||||
{{ cliCommandGenerationDescription }}
|
||||
</p>
|
||||
|
||||
<div v-if="environmentID" class="flex gap-x-2 items-center">
|
||||
<HoppSmartCheckbox
|
||||
:on="includeEnvironmentID"
|
||||
@change="toggleIncludeEnvironment"
|
||||
/>
|
||||
<span class="truncate"
|
||||
>{{ t("collection_runner.include_active_environment") }}
|
||||
<span class="text-secondaryDark">
|
||||
{{ currentEnv?.name }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 rounded-md bg-primaryLight text-secondaryDark select-text"
|
||||
>
|
||||
{{ CLICommand }}
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-if="activeTab === 'test-runner'"
|
||||
:label="`${t('test.run')}`"
|
||||
:icon="IconPlay"
|
||||
outline
|
||||
@click="runTests"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
:label="`${t('action.copy')}`"
|
||||
:icon="copyIcon"
|
||||
outline
|
||||
@click="copyCLICommandToClipboard"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('action.close')}`"
|
||||
outline
|
||||
filled
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { TestRunnerConfig } from "~/helpers/rest/document"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import { CurrentEnv } from "./Env.vue"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import {
|
||||
getCompleteCollectionTree,
|
||||
teamCollToHoppRESTColl,
|
||||
} from "~/helpers/backend/helpers"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { getErrorMessage } from "~/helpers/runner/collection-tree"
|
||||
import { getRESTCollectionByRefId } from "~/newstore/collections"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const loadingCollection = ref(false)
|
||||
|
||||
export type CollectionRunnerData =
|
||||
| {
|
||||
type: "my-collections"
|
||||
// for my-collections it's actually _ref_id
|
||||
collectionID: string
|
||||
collectionIndex?: string
|
||||
}
|
||||
| {
|
||||
type: "team-collections"
|
||||
collectionID: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sameTab?: boolean
|
||||
collectionRunnerData: CollectionRunnerData
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const includeEnvironmentID = ref(false)
|
||||
const activeTab = ref("test-runner")
|
||||
|
||||
const environmentID = ref("")
|
||||
const currentEnv = ref<CurrentEnv>(null)
|
||||
|
||||
function setCurrentEnv(payload: CurrentEnv) {
|
||||
currentEnv.value = payload
|
||||
if (payload?.type === "TEAM_ENV") {
|
||||
environmentID.value = payload.teamEnvID
|
||||
}
|
||||
}
|
||||
|
||||
const config = ref<TestRunnerConfig>({
|
||||
iterations: 1,
|
||||
delay: 500,
|
||||
stopOnError: false,
|
||||
persistResponses: true,
|
||||
keepVariableValues: false,
|
||||
})
|
||||
|
||||
const runTests = async () => {
|
||||
const collectionTree = await getCollectionTree(
|
||||
props.collectionRunnerData.type,
|
||||
props.collectionRunnerData.collectionID
|
||||
)
|
||||
|
||||
if (!collectionTree) {
|
||||
toast.error(t("collection_runner.collection_not_found"))
|
||||
return
|
||||
}
|
||||
|
||||
if (checkIfCollectionIsEmpty(collectionTree)) {
|
||||
toast.error(t("collection_runner.empty_collection"))
|
||||
return
|
||||
}
|
||||
|
||||
let tabIdToClose = null
|
||||
if (props.sameTab) tabIdToClose = cloneDeep(tabs.currentTabID.value)
|
||||
|
||||
tabs.createNewTab({
|
||||
type: "test-runner",
|
||||
collectionType: props.collectionRunnerData.type,
|
||||
collectionID: props.collectionRunnerData.collectionID,
|
||||
collection: collectionTree as HoppCollection,
|
||||
isDirty: false,
|
||||
config: config.value,
|
||||
status: "idle",
|
||||
request: null,
|
||||
testRunnerMeta: {
|
||||
completedRequests: 0,
|
||||
totalRequests: 0,
|
||||
totalTime: 0,
|
||||
failedTests: 0,
|
||||
passedTests: 0,
|
||||
totalTests: 0,
|
||||
},
|
||||
})
|
||||
|
||||
if (tabIdToClose) tabs.closeTab(tabIdToClose)
|
||||
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the collection tree from the backend
|
||||
* @param collection
|
||||
* @returns collection tree
|
||||
*/
|
||||
const getCollectionTree = async (
|
||||
type: CollectionRunnerData["type"],
|
||||
collectionID: string
|
||||
) => {
|
||||
if (!collectionID) return
|
||||
if (type === "my-collections") {
|
||||
return await getRESTCollectionByRefId(collectionID)
|
||||
}
|
||||
loadingCollection.value = true
|
||||
return pipe(
|
||||
getCompleteCollectionTree(collectionID),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err, t)}`)
|
||||
loadingCollection.value = false
|
||||
return
|
||||
},
|
||||
async (coll) => {
|
||||
loadingCollection.value = false
|
||||
return teamCollToHoppRESTColl(coll)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
function checkIfCollectionIsEmpty(collection: HoppCollection): boolean {
|
||||
// Check if the collection has requests or if any child collection is non-empty
|
||||
return (
|
||||
collection.requests.length === 0 &&
|
||||
collection.folders.every((folder) => checkIfCollectionIsEmpty(folder))
|
||||
)
|
||||
}
|
||||
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const isCloudInstance = window.location.hostname === "hoppscotch.io"
|
||||
|
||||
const cliCommandGenerationDescription = computed(() => {
|
||||
if (isCloudInstance) {
|
||||
return t("collection_runner.cli_command_generation_description_cloud")
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_BACKEND_API_URL) {
|
||||
return t("collection_runner.cli_command_generation_description_sh")
|
||||
}
|
||||
|
||||
return t(
|
||||
"collection_runner.cli_command_generation_description_sh_with_server_url_placeholder"
|
||||
)
|
||||
})
|
||||
|
||||
const CLICommand = computed(() => {
|
||||
if (props.collectionRunnerData.type === "team-collections") {
|
||||
const collectionID = props.collectionRunnerData.collectionID
|
||||
const environmentFlag =
|
||||
includeEnvironmentID.value && environmentID.value
|
||||
? `-e ${environmentID.value}`
|
||||
: ""
|
||||
|
||||
const serverUrl = import.meta.env.VITE_BACKEND_API_URL?.endsWith("/v1")
|
||||
? // Removing `/v1` prefix
|
||||
import.meta.env.VITE_BACKEND_API_URL.slice(0, -3)
|
||||
: "<server_url>"
|
||||
|
||||
const serverFlag = isCloudInstance ? "" : `--server ${serverUrl}`
|
||||
|
||||
return `hopp test ${collectionID} ${environmentFlag} --token <access_token> ${serverFlag}`
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const toggleIncludeEnvironment = () => {
|
||||
includeEnvironmentID.value = !includeEnvironmentID.value
|
||||
}
|
||||
|
||||
const copyCLICommandToClipboard = () => {
|
||||
copyToClipboard(CLICommand.value ?? "")
|
||||
copyIcon.value = IconCheck
|
||||
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="sticky top-upperRunnerStickyFold z-10">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedTestTab"
|
||||
styles="overflow-x-auto flex-shrink-0 bg-primary"
|
||||
render-inactive-tabs
|
||||
@update:model-value="emit('onChangeTab', $event)"
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'all'"
|
||||
:label="`${t('tab.all_tests')}`"
|
||||
:info="tab.document.testRunnerMeta.totalTests.toString()"
|
||||
>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'passed'"
|
||||
:label="`${t('tab.passed')}`"
|
||||
:info="tab.document.testRunnerMeta.passedTests.toString()"
|
||||
>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'failed'"
|
||||
:label="`${t('tab.failed')}`"
|
||||
:info="tab.document.testRunnerMeta.failedTests.toString()"
|
||||
>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center test-runner pr-2">
|
||||
<HoppSmartTree :expand-all="true" :adapter="collectionAdapter">
|
||||
<template #content="{ node }">
|
||||
<HttpTestResultFolder
|
||||
v-if="
|
||||
node.data.type === 'folders' &&
|
||||
node.data.data.data.requests.length > 0
|
||||
"
|
||||
:id="node.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:data="node.data.data.data"
|
||||
:is-open="true"
|
||||
:is-selected="node.data.isSelected"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
:show-selection="showCheckbox"
|
||||
folder-type="folder"
|
||||
/>
|
||||
|
||||
<HttpTestResultRequest
|
||||
v-if="node.data.type === 'requests' && !node.data.hidden"
|
||||
class="runner-request"
|
||||
:show-test-type="selectedTestTab"
|
||||
:request="node.data.data.data"
|
||||
:request-i-d="node.id"
|
||||
:parent-i-d="node.data.data.parentIndex"
|
||||
:is-selected="node.data.isSelected"
|
||||
:show-selection="showCheckbox"
|
||||
:is-last-item="node.data.isLastItem"
|
||||
@select-request="selectRequest(node.data.data.data)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SmartTreeAdapter } from "@hoppscotch/ui"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { HoppTestRunnerDocument } from "~/helpers/rest/document"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
tab: HoppTab<HoppTestRunnerDocument>
|
||||
collectionAdapter: SmartTreeAdapter<any>
|
||||
isRunning: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onSelectRequest", request: TestRunnerRequest): void
|
||||
(e: "onChangeTab", event: string): void
|
||||
}>()
|
||||
|
||||
const selectedTestTab = ref<"all" | "passed" | "failed">("all")
|
||||
|
||||
const showCheckbox = ref(false)
|
||||
|
||||
const selectRequest = (request: TestRunnerRequest) => {
|
||||
emit("onSelectRequest", request)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.test-runner > div > div > div > div > div {
|
||||
margin-left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.test-runner .runner-request {
|
||||
@apply ml-2;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="
|
||||
testResults &&
|
||||
(testResults.expectResults.length ||
|
||||
testResults.tests.length ||
|
||||
haveEnvVariables)
|
||||
"
|
||||
>
|
||||
<div class="divide-y-4 divide-dividerLight border-b border-dividerLight">
|
||||
<div v-if="haveEnvVariables" class="flex flex-col">
|
||||
<details class="flex flex-col divide-y divide-dividerLight" open>
|
||||
<summary
|
||||
class="group flex min-w-0 flex-1 cursor-pointer items-center justify-between text-tiny text-secondaryLight transition focus:outline-none"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center truncate px-4 py-2 transition group-hover:text-secondary"
|
||||
>
|
||||
<icon-lucide-chevron-right
|
||||
class="indicator mr-2 flex flex-shrink-0"
|
||||
/>
|
||||
<span class="capitalize-first truncate">
|
||||
{{ t("environment.title") }}
|
||||
</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-if="noEnvSelected && !globalHasAdditions"
|
||||
class="flex bg-bannerInfo p-4 text-secondaryDark"
|
||||
role="alert"
|
||||
>
|
||||
<icon-lucide-alert-triangle class="svg-icons mr-4" />
|
||||
<div class="flex flex-col">
|
||||
<p>
|
||||
{{ t("environment.no_environment_description") }}
|
||||
</p>
|
||||
<p class="mt-3 flex space-x-2">
|
||||
<HoppButtonSecondary
|
||||
:label="t('environment.add_to_global')"
|
||||
class="!bg-primary text-tiny"
|
||||
filled
|
||||
@click="addEnvToGlobal()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('environment.create_new')"
|
||||
class="!bg-primary text-tiny"
|
||||
filled
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.global.additions"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="additions"
|
||||
global
|
||||
/>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.global.updations"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="updations"
|
||||
global
|
||||
/>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.global.deletions"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="deletions"
|
||||
global
|
||||
/>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.selected.additions"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="additions"
|
||||
/>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.selected.updations"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="updations"
|
||||
/>
|
||||
<HttpTestResultEnv
|
||||
v-for="(env, index) in testResults.envDiff.selected.deletions"
|
||||
:key="`env-${env.key}-${index}`"
|
||||
:env="env"
|
||||
status="deletions"
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
v-if="testResults.tests"
|
||||
class="divide-y-4 divide-dividerLight test-results-entry"
|
||||
>
|
||||
<template
|
||||
v-for="(result, index) in testResults.tests"
|
||||
:key="`result-${index}`"
|
||||
>
|
||||
<HttpTestResultEntry
|
||||
v-if="shouldShowEntry(result)"
|
||||
:test-results="result"
|
||||
:show-test-type="props.showTestType"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="testResults.expectResults"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<HttpTestResultReport
|
||||
v-if="testResults.expectResults.length"
|
||||
:test-results="testResults"
|
||||
/>
|
||||
<div
|
||||
v-for="(result, index) in testResults.expectResults"
|
||||
:key="`result-${index}`"
|
||||
class="flex items-center px-4 py-2"
|
||||
>
|
||||
<div class="flex flex-shrink-0 items-center overflow-x-auto">
|
||||
<component
|
||||
:is="result.status === 'pass' ? IconCheck : IconClose"
|
||||
class="svg-icons mr-4"
|
||||
:class="
|
||||
result.status === 'pass' ? 'text-green-500' : 'text-red-500'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center space-x-2 overflow-x-auto"
|
||||
>
|
||||
<span
|
||||
v-if="result.message"
|
||||
class="inline-flex text-secondaryDark"
|
||||
>
|
||||
{{ result.message }}
|
||||
</span>
|
||||
<span class="inline-flex text-secondaryLight">
|
||||
<icon-lucide-minus class="svg-icons mr-2" />
|
||||
{{
|
||||
result.status === "pass"
|
||||
? t("test.passed")
|
||||
: t("test.failed")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="testResults && testResults.scriptError"
|
||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
||||
:alt="`${t('error.test_script_fail')}`"
|
||||
:heading="t('error.test_script_fail')"
|
||||
:text="t('helpers.test_script_fail')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<template v-else>
|
||||
<div
|
||||
class="py-2 pl-4 ml-4 mb-2 text-secondaryLight border-secondaryLight border-l"
|
||||
>
|
||||
{{ t("empty.tests") }}
|
||||
</div>
|
||||
</template>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showMyEnvironmentDetailsModal"
|
||||
action="new"
|
||||
:env-vars="getAdditionVars"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
/>
|
||||
<EnvironmentsTeamsDetails
|
||||
:show="showTeamEnvironmentDetailsModal"
|
||||
action="new"
|
||||
:env-vars="getAdditionVars"
|
||||
:editing-team-id="
|
||||
workspace.type === 'team' ? workspace.teamID : undefined
|
||||
"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppTestData, HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
import {
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconClose from "~icons/lucide/x"
|
||||
|
||||
import { GlobalEnvironment } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
showTestType: "all" | "passed" | "failed"
|
||||
showEmptyMessage?: boolean
|
||||
}>(),
|
||||
{
|
||||
showEmptyMessage: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppTestResult | null | undefined): void
|
||||
}>()
|
||||
|
||||
const testResults = useVModel(props, "modelValue", emit)
|
||||
|
||||
const shouldShowEntry = (result: HoppTestData) => {
|
||||
if (props.showTestType === "all") return true
|
||||
if (props.showTestType === "passed")
|
||||
return result.expectResults.some((x) => x.status === "pass")
|
||||
if (props.showTestType === "failed")
|
||||
return result.expectResults.some((x) => x.status === "fail")
|
||||
return false
|
||||
}
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const showMyEnvironmentDetailsModal = ref(false)
|
||||
const showTeamEnvironmentDetailsModal = ref(false)
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
if (workspace.value.type === "personal")
|
||||
showMyEnvironmentDetailsModal.value = shouldDisplay
|
||||
else showTeamEnvironmentDetailsModal.value = shouldDisplay
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "addition" environment variables
|
||||
* @returns Array of objects with key-value pairs of arguments
|
||||
*/
|
||||
const getAdditionVars = () =>
|
||||
testResults?.value?.envDiff?.selected?.additions
|
||||
? testResults.value.envDiff.selected.additions
|
||||
: []
|
||||
|
||||
const haveEnvVariables = computed(() => {
|
||||
if (!testResults.value) return false
|
||||
return (
|
||||
testResults.value.envDiff.global.additions.length ||
|
||||
testResults.value.envDiff.global.updations.length ||
|
||||
testResults.value.envDiff.global.deletions.length ||
|
||||
testResults.value.envDiff.selected.additions.length ||
|
||||
testResults.value.envDiff.selected.updations.length ||
|
||||
testResults.value.envDiff.selected.deletions.length
|
||||
)
|
||||
})
|
||||
|
||||
const selectedEnvironmentIndex = useStream(
|
||||
selectedEnvironmentIndex$,
|
||||
{ type: "NO_ENV_SELECTED" },
|
||||
setSelectedEnvironmentIndex
|
||||
)
|
||||
|
||||
const globalEnvVars = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
|
||||
|
||||
const noEnvSelected = computed(
|
||||
() => selectedEnvironmentIndex.value.type === "NO_ENV_SELECTED"
|
||||
)
|
||||
|
||||
const globalHasAdditions = computed(() => {
|
||||
if (!testResults.value?.envDiff.selected.additions) return false
|
||||
return (
|
||||
testResults.value.envDiff.selected.additions.every(
|
||||
(x) =>
|
||||
globalEnvVars.value.variables.findIndex((y) => isEqual(x, y)) !== -1
|
||||
) ?? false
|
||||
)
|
||||
})
|
||||
|
||||
const addEnvToGlobal = () => {
|
||||
if (!testResults.value?.envDiff.selected.additions) return
|
||||
|
||||
invokeAction("modals.global.environment.update", {
|
||||
variables: testResults.value.envDiff.selected.additions,
|
||||
isSecret: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -50,9 +50,10 @@ import {
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppRequestDocument
|
||||
document: HoppRequestDocument | TestRunnerRequest
|
||||
isEditable: boolean
|
||||
}>()
|
||||
|
||||
@@ -64,6 +65,7 @@ const emit = defineEmits<{
|
||||
const doc = useVModel(props, "document", emit)
|
||||
|
||||
const isSavable = computed(() => {
|
||||
if (doc.value.type === "test-response") return false
|
||||
return doc.value.response?.type === "success" && doc.value.saveContext
|
||||
})
|
||||
|
||||
@@ -118,6 +120,8 @@ watch(
|
||||
"results",
|
||||
]
|
||||
|
||||
if (doc.value.type === "test-response") return
|
||||
|
||||
const { responseTabPreference } = doc.value
|
||||
|
||||
if (
|
||||
@@ -133,6 +137,7 @@ watch(
|
||||
)
|
||||
|
||||
watch(selectedLensTab, (newLensID) => {
|
||||
if (doc.value.type === "test-response") return
|
||||
doc.value.responseTabPreference = newLensID
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -381,7 +381,9 @@ const envVars = computed(() => {
|
||||
tabs.currentActiveTab.value.document.type === "example-response"
|
||||
? tabs.currentActiveTab.value.document.response.originalRequest
|
||||
.requestVariables
|
||||
: tabs.currentActiveTab.value.document.request.requestVariables
|
||||
: tabs.currentActiveTab.value.document.type === "request"
|
||||
? tabs.currentActiveTab.value.document.request.requestVariables
|
||||
: []
|
||||
|
||||
return [
|
||||
...requestVariables.map(({ active, key, value }) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Environment,
|
||||
HoppRESTHeaders,
|
||||
HoppRESTRequest,
|
||||
HoppRESTRequestVariable,
|
||||
} from "@hoppscotch/data"
|
||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||
@@ -14,6 +15,7 @@ import { Observable, Subject } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import { Ref } from "vue"
|
||||
|
||||
import { getService } from "~/modules/dioc"
|
||||
import {
|
||||
environmentsStore,
|
||||
getCurrentEnvironment,
|
||||
@@ -22,6 +24,10 @@ import {
|
||||
setGlobalEnvVariables,
|
||||
updateEnvironment,
|
||||
} from "~/newstore/environments"
|
||||
import {
|
||||
SecretEnvironmentService,
|
||||
SecretVariable,
|
||||
} from "~/services/secret-environment.service"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
@@ -34,15 +40,10 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import {
|
||||
SecretEnvironmentService,
|
||||
SecretVariable,
|
||||
} from "~/services/secret-environment.service"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const getTestableBody = (
|
||||
export const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
) => {
|
||||
const contentTypeHeader = res.headers.find(
|
||||
@@ -69,7 +70,7 @@ const getTestableBody = (
|
||||
return x
|
||||
}
|
||||
|
||||
const combineEnvVariables = (variables: {
|
||||
export const combineEnvVariables = (variables: {
|
||||
environments: {
|
||||
selected: Environment["variables"]
|
||||
global: Environment["variables"]
|
||||
@@ -279,70 +280,12 @@ export function runRESTRequest$(
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
|
||||
cloneDeep(runResult.right.envs.global),
|
||||
"global"
|
||||
)
|
||||
|
||||
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
|
||||
cloneDeep(runResult.right.envs.selected),
|
||||
"selected"
|
||||
)
|
||||
|
||||
// set the response in the tab so that multiple tabs can run request simultaneously
|
||||
tab.value.document.response = res
|
||||
|
||||
const updatedRunResult = {
|
||||
...runResult.right,
|
||||
envs: {
|
||||
global: updatedGlobalEnvVariables,
|
||||
selected: updatedSelectedEnvVariables,
|
||||
},
|
||||
}
|
||||
|
||||
const updatedRunResult = updateEnvsAfterTestScript(runResult)
|
||||
tab.value.document.testResults =
|
||||
// @ts-expect-error Typescript can't figure out this inference for some reason
|
||||
translateToSandboxTestResults(updatedRunResult)
|
||||
|
||||
const globalEnvVariables = updateEnvironmentsWithSecret(
|
||||
runResult.right.envs.global,
|
||||
"global"
|
||||
)
|
||||
|
||||
setGlobalEnvVariables({
|
||||
v: 1,
|
||||
variables: globalEnvVariables,
|
||||
})
|
||||
if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
const env = getEnvironment({
|
||||
type: "MY_ENV",
|
||||
index: environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
})
|
||||
updateEnvironment(
|
||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
{
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: "id" in env ? env.id : "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type ===
|
||||
"TEAM_ENV"
|
||||
) {
|
||||
const env = getEnvironment({
|
||||
type: "TEAM_ENV",
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
)()
|
||||
}
|
||||
} else {
|
||||
tab.value.document.testResults = {
|
||||
description: "",
|
||||
@@ -374,6 +317,160 @@ export function runRESTRequest$(
|
||||
return [cancel, res]
|
||||
}
|
||||
|
||||
function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
|
||||
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
|
||||
// @ts-expect-error Typescript can't figure out this inference for some reason
|
||||
cloneDeep(runResult.right.envs.global),
|
||||
"global"
|
||||
)
|
||||
|
||||
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
|
||||
// @ts-expect-error Typescript can't figure out this inference for some reason
|
||||
cloneDeep(runResult.right.envs.selected),
|
||||
"selected"
|
||||
)
|
||||
|
||||
const updatedRunResult = {
|
||||
...runResult.right,
|
||||
envs: {
|
||||
global: updatedGlobalEnvVariables,
|
||||
selected: updatedSelectedEnvVariables,
|
||||
},
|
||||
}
|
||||
|
||||
const globalEnvVariables = updateEnvironmentsWithSecret(
|
||||
// @ts-expect-error Typescript can't figure out this inference for some reason
|
||||
runResult.right.envs.global,
|
||||
"global"
|
||||
)
|
||||
|
||||
setGlobalEnvVariables({
|
||||
v: 1,
|
||||
variables: globalEnvVariables,
|
||||
})
|
||||
if (environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
const env = getEnvironment({
|
||||
type: "MY_ENV",
|
||||
index: environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
})
|
||||
updateEnvironment(environmentsStore.value.selectedEnvironmentIndex.index, {
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: "id" in env ? env.id : "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
})
|
||||
} else if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV"
|
||||
) {
|
||||
const env = getEnvironment({
|
||||
type: "TEAM_ENV",
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
return updatedRunResult
|
||||
}
|
||||
|
||||
export function runTestRunnerRequest(request: HoppRESTRequest): Promise<
|
||||
| E.Left<"script_fail">
|
||||
| E.Right<{
|
||||
response: HoppRESTResponse
|
||||
testResult: HoppTestResult
|
||||
}>
|
||||
| undefined
|
||||
> {
|
||||
return getFinalEnvsFromPreRequest(
|
||||
request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
).then(async (envs) => {
|
||||
if (E.isLeft(envs)) {
|
||||
console.error(envs.left)
|
||||
return E.left("script_fail" as const)
|
||||
}
|
||||
|
||||
const effectiveRequest = await getEffectiveRESTRequest(request, {
|
||||
id: "env-id",
|
||||
v: 1,
|
||||
name: "Env",
|
||||
variables: combineEnvVariables({
|
||||
environments: envs.right,
|
||||
requestVariables: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const [stream] = createRESTNetworkRequestStream(effectiveRequest)
|
||||
|
||||
const requestResult = stream
|
||||
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
|
||||
.toPromise()
|
||||
.then(async (res) => {
|
||||
if (res?.type === "success" || res?.type === "fail") {
|
||||
executedResponses$.next(
|
||||
// @ts-expect-error Typescript can't figure out this inference for some reason
|
||||
res
|
||||
)
|
||||
|
||||
const runResult = await runTestScript(
|
||||
res.req.testScript,
|
||||
envs.right,
|
||||
{
|
||||
status: res.statusCode,
|
||||
body: getTestableBody(res),
|
||||
headers: res.headers,
|
||||
}
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
const sandboxTestResult = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
)
|
||||
|
||||
updateEnvsAfterTestScript(runResult)
|
||||
|
||||
return E.right({
|
||||
response: res,
|
||||
testResult: sandboxTestResult,
|
||||
})
|
||||
}
|
||||
const sandboxTestResult = {
|
||||
description: "",
|
||||
expectResults: [],
|
||||
tests: [],
|
||||
envDiff: {
|
||||
global: {
|
||||
additions: [],
|
||||
deletions: [],
|
||||
updations: [],
|
||||
},
|
||||
selected: {
|
||||
additions: [],
|
||||
deletions: [],
|
||||
updations: [],
|
||||
},
|
||||
},
|
||||
scriptError: true,
|
||||
}
|
||||
return E.right({
|
||||
response: res,
|
||||
testResult: sandboxTestResult,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (requestResult) {
|
||||
return requestResult
|
||||
}
|
||||
|
||||
return E.left("script_fail")
|
||||
})
|
||||
}
|
||||
|
||||
const getAddedEnvVariables = (
|
||||
current: Environment["variables"],
|
||||
updated: Environment["variables"]
|
||||
|
||||
@@ -281,15 +281,19 @@ export class HoppEnvironmentPlugin {
|
||||
const currentTab = restTabs.currentActiveTab.value
|
||||
|
||||
const currentTabRequest =
|
||||
currentTab.document.type === "request"
|
||||
? currentTab.document.request
|
||||
: currentTab.document.response.originalRequest
|
||||
currentTab.document.type === "example-response"
|
||||
? currentTab.document.response.originalRequest
|
||||
: currentTab.document.request
|
||||
|
||||
watch(
|
||||
currentTabRequest,
|
||||
(reqVariables) => {
|
||||
(request) => {
|
||||
const requestVariables = request?.requestVariables
|
||||
? request.requestVariables
|
||||
: []
|
||||
|
||||
this.envs = [
|
||||
...reqVariables.requestVariables.map(({ key, value }) => ({
|
||||
...requestVariables.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
sourceEnv: "RequestVariable",
|
||||
@@ -308,9 +312,11 @@ export class HoppEnvironmentPlugin {
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const requestVariables = currentTabRequest?.requestVariables ?? []
|
||||
|
||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||
this.envs = [
|
||||
...currentTabRequest.requestVariables.map(({ key, value }) => ({
|
||||
...requestVariables.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
sourceEnv: "RequestVariable",
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { HoppRESTRequest, HoppRESTRequestResponse } from "@hoppscotch/data"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
HoppRESTRequestResponse,
|
||||
} from "@hoppscotch/data"
|
||||
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
export type HoppRESTSaveContext =
|
||||
| {
|
||||
@@ -50,9 +55,122 @@ export type HoppRESTSaveContext =
|
||||
/**
|
||||
* Defines a live 'document' (something that is open and being edited) in the app
|
||||
*/
|
||||
|
||||
export type HoppCollectionSaveContext =
|
||||
| {
|
||||
/**
|
||||
* The origin source of the request
|
||||
*/
|
||||
originLocation: "user-collection"
|
||||
/**
|
||||
* Path to the request folder
|
||||
*/
|
||||
folderPath: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The origin source of the request
|
||||
*/
|
||||
originLocation: "team-collection"
|
||||
/**
|
||||
* ID of the team
|
||||
*/
|
||||
teamID?: string
|
||||
/**
|
||||
* ID of the collection loaded
|
||||
*/
|
||||
collectionID?: string
|
||||
/**
|
||||
* ID of the request in the team
|
||||
*/
|
||||
requestID: string
|
||||
}
|
||||
| null
|
||||
|
||||
export type TestRunnerConfig = {
|
||||
iterations: number
|
||||
delay: number
|
||||
stopOnError: boolean
|
||||
persistResponses: boolean
|
||||
keepVariableValues: boolean
|
||||
}
|
||||
|
||||
export type HoppTestRunnerDocument = {
|
||||
/**
|
||||
* The document type
|
||||
*/
|
||||
type: "test-runner"
|
||||
|
||||
/**
|
||||
* The test runner configuration
|
||||
*/
|
||||
config: TestRunnerConfig
|
||||
|
||||
/**
|
||||
* initiate test runner on tab open
|
||||
*/
|
||||
status: "idle" | "running" | "stopped" | "error"
|
||||
|
||||
/**
|
||||
* The collection as it is in the document
|
||||
*/
|
||||
collection: HoppCollection
|
||||
|
||||
/**
|
||||
* The type of the collection
|
||||
*/
|
||||
collectionType: "my-collections" | "team-collections"
|
||||
|
||||
/**
|
||||
* collection ID to be used for team collections
|
||||
* (if it's my-collections, the _ref_id will be used as collectionID)
|
||||
*/
|
||||
collectionID: string
|
||||
|
||||
/**
|
||||
* The request as it is in the document
|
||||
*/
|
||||
resultCollection?: HoppCollection
|
||||
|
||||
/**
|
||||
* The test runner meta information
|
||||
*/
|
||||
testRunnerMeta: {
|
||||
totalRequests: number
|
||||
completedRequests: number
|
||||
totalTests: number
|
||||
passedTests: number
|
||||
failedTests: number
|
||||
totalTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected test runner request
|
||||
*/
|
||||
request: TestRunnerRequest | null
|
||||
|
||||
/**
|
||||
* The response of the selected request in collections after running the test
|
||||
* (if any)
|
||||
*/
|
||||
response?: HoppRESTResponse | null
|
||||
|
||||
/**
|
||||
* The test results of the selected request in collections after running the test
|
||||
* (if any)
|
||||
*/
|
||||
testResults?: HoppTestResult | null
|
||||
|
||||
/**
|
||||
* Whether the request has any unsaved changes
|
||||
* (atleast as far as we can say)
|
||||
*/
|
||||
isDirty: boolean
|
||||
}
|
||||
|
||||
export type HoppRequestDocument = {
|
||||
/**
|
||||
* The type of the document
|
||||
* The document type
|
||||
*/
|
||||
type: "request"
|
||||
|
||||
@@ -134,4 +252,7 @@ export type HoppSavedExampleDocument = {
|
||||
/**
|
||||
* Defines a live 'document' (something that is open and being edited) in the app
|
||||
*/
|
||||
export type HoppTabDocument = HoppSavedExampleDocument | HoppRequestDocument
|
||||
export type HoppTabDocument =
|
||||
| HoppSavedExampleDocument
|
||||
| HoppRequestDocument
|
||||
| HoppTestRunnerDocument
|
||||
|
||||
179
packages/hoppscotch-common/src/helpers/runner/adapter.ts
Normal file
179
packages/hoppscotch-common/src/helpers/runner/adapter.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui/helpers"
|
||||
import { computed, Ref } from "vue"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
export type Collection = {
|
||||
type: "collections"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: null
|
||||
data: HoppCollection
|
||||
}
|
||||
}
|
||||
|
||||
type Folder = {
|
||||
type: "folders"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: string
|
||||
data: HoppCollection
|
||||
}
|
||||
}
|
||||
|
||||
type Requests = {
|
||||
type: "requests"
|
||||
isLastItem: boolean
|
||||
data: {
|
||||
parentIndex: string
|
||||
data: TestRunnerRequest
|
||||
}
|
||||
}
|
||||
|
||||
export type CollectionNode = Collection | Folder | Requests
|
||||
|
||||
export class TestRunnerCollectionsAdapter
|
||||
implements SmartTreeAdapter<CollectionNode>
|
||||
{
|
||||
constructor(
|
||||
public data: Ref<HoppCollection[]>,
|
||||
private show: Ref<"all" | "passed" | "failed">
|
||||
) {}
|
||||
|
||||
private shouldShowRequest(request: TestRunnerRequest): boolean {
|
||||
// Always show requests that are still loading or haven't run yet
|
||||
if (!request.testResults || request.isLoading) return true
|
||||
|
||||
const { passed, failed } = this.countTestResults(request.testResults)
|
||||
|
||||
switch (this.show.value) {
|
||||
case "passed":
|
||||
return passed > 0
|
||||
case "failed":
|
||||
return failed > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private countTestResults(testResult: any) {
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
|
||||
// Count direct expect results
|
||||
if (testResult.expectResults) {
|
||||
for (const result of testResult.expectResults) {
|
||||
if (result.status === "pass") passed++
|
||||
else if (result.status === "fail") failed++
|
||||
}
|
||||
}
|
||||
|
||||
// Count nested test results
|
||||
if (testResult.tests) {
|
||||
for (const test of testResult.tests) {
|
||||
const counts = this.countTestResults(test)
|
||||
passed += counts.passed
|
||||
failed += counts.failed
|
||||
}
|
||||
}
|
||||
|
||||
return { passed, failed }
|
||||
}
|
||||
|
||||
navigateToFolderWithIndexPath(
|
||||
collections: HoppCollection[],
|
||||
indexPaths: number[]
|
||||
) {
|
||||
if (indexPaths.length === 0) return null
|
||||
|
||||
let target = collections[indexPaths.shift() as number]
|
||||
|
||||
while (indexPaths.length > 0)
|
||||
target = target.folders[indexPaths.shift() as number]
|
||||
|
||||
return target !== undefined ? target : null
|
||||
}
|
||||
|
||||
getChildren(id: string | null): Ref<ChildrenResult<any>> {
|
||||
return computed(() => {
|
||||
if (id === null) {
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: `folder-${index.toString()}`,
|
||||
data: {
|
||||
type: "collections",
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
data: {
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
},
|
||||
},
|
||||
}))
|
||||
return {
|
||||
status: "loaded",
|
||||
data: data,
|
||||
} as ChildrenResult<Collection>
|
||||
}
|
||||
|
||||
const childType = id.split("-")[0]
|
||||
|
||||
if (childType === "request") {
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
const folderId = id.split("-")[1]
|
||||
const indexPath = folderId.split("/").map((x) => parseInt(x))
|
||||
const item = this.navigateToFolderWithIndexPath(
|
||||
this.data.value,
|
||||
indexPath
|
||||
)
|
||||
|
||||
if (item && Object.keys(item).length) {
|
||||
// Always include all folders for smooth transitions
|
||||
const folderData = item.folders.map((folder, index) => ({
|
||||
id: `folder-${folderId}/${index}`,
|
||||
data: {
|
||||
isLastItem:
|
||||
index === item.folders.length - 1 && item.requests.length === 0,
|
||||
type: "folders",
|
||||
isSelected: true,
|
||||
data: {
|
||||
parentIndex: id,
|
||||
data: folder,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const requestData = item.requests.map((request, index) => {
|
||||
const shouldShow = this.shouldShowRequest(
|
||||
request as TestRunnerRequest
|
||||
)
|
||||
return {
|
||||
id: `request-${id}/${index}`,
|
||||
data: {
|
||||
isLastItem: index === item.requests.length - 1,
|
||||
type: "requests",
|
||||
isSelected: true,
|
||||
hidden: !shouldShow,
|
||||
data: {
|
||||
parentIndex: id,
|
||||
data: request,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [...folderData, ...requestData],
|
||||
} as ChildrenResult<Folder | Request>
|
||||
}
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ComposerTranslation } from "vue-i18n"
|
||||
import { GQLError } from "../backend/GQLClient"
|
||||
|
||||
export const getErrorMessage = (
|
||||
err: GQLError<string>,
|
||||
t: ComposerTranslation
|
||||
) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
case "bug/team_coll/no_coll_id":
|
||||
case "team_req/invalid_target_id":
|
||||
return t("team.invalid_coll_id")
|
||||
case "team/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_found":
|
||||
return t("team.no_request_found")
|
||||
case "bug/team_req/no_req_id":
|
||||
return t("team.no_request_found")
|
||||
case "team/collection_is_parent_coll":
|
||||
return t("team.parent_coll_move")
|
||||
case "team/target_and_destination_collection_are_same":
|
||||
return t("team.same_target_destination")
|
||||
case "team/target_collection_is_already_root_collection":
|
||||
return t("collection.invalid_root_move")
|
||||
case "team_req/requests_not_from_same_collection":
|
||||
return t("request.different_collection")
|
||||
case "team/team_collections_have_different_parents":
|
||||
return t("collection.different_parent")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
generateUniqueRefId,
|
||||
HoppCollection,
|
||||
HoppGQLAuth,
|
||||
HoppGQLRequest,
|
||||
@@ -514,6 +515,21 @@ const restCollectionDispatchers = defineDispatchers({
|
||||
if (collection) {
|
||||
const name = `${collection.name} - ${t("action.duplicate")}`
|
||||
|
||||
function recursiveChangeRefIdToAvoidConflicts(
|
||||
collection: HoppCollection
|
||||
): HoppCollection {
|
||||
const newCollection = {
|
||||
...collection,
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
newCollection.folders = newCollection.folders.map((folder) =>
|
||||
recursiveChangeRefIdToAvoidConflicts(folder)
|
||||
)
|
||||
|
||||
return newCollection
|
||||
}
|
||||
|
||||
const duplicatedCollection = {
|
||||
...cloneDeep(collection),
|
||||
name,
|
||||
@@ -522,15 +538,18 @@ const restCollectionDispatchers = defineDispatchers({
|
||||
: {}),
|
||||
}
|
||||
|
||||
const duplicatedCollectionWithNewRefId =
|
||||
recursiveChangeRefIdToAvoidConflicts(duplicatedCollection)
|
||||
|
||||
if (isRootCollection) {
|
||||
newState.push(duplicatedCollection)
|
||||
newState.push(duplicatedCollectionWithNewRefId)
|
||||
} else {
|
||||
const parentCollectionIndexPath = indexPaths.slice(0, -1)
|
||||
|
||||
const parentCollection = navigateToFolderWithIndexPath(state, [
|
||||
...parentCollectionIndexPath,
|
||||
])
|
||||
parentCollection?.folders.push(duplicatedCollection)
|
||||
parentCollection?.folders.push(duplicatedCollectionWithNewRefId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,6 +1217,30 @@ export function getRESTCollection(collectionIndex: number) {
|
||||
return restCollectionStore.value.state[collectionIndex]
|
||||
}
|
||||
|
||||
export function getRESTCollectionByRefId(ref_id: string) {
|
||||
function findCollection(
|
||||
collection: HoppCollection,
|
||||
ref_id: string
|
||||
): HoppCollection | null {
|
||||
if (collection._ref_id === ref_id) {
|
||||
return collection
|
||||
}
|
||||
for (const folder of collection.folders) {
|
||||
const found = findCollection(folder, ref_id)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
for (const collection of restCollectionStore.value.state) {
|
||||
const found = findCollection(collection, ref_id)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function editRESTCollection(
|
||||
collectionIndex: number,
|
||||
partialCollection: Partial<HoppCollection>
|
||||
|
||||
@@ -88,7 +88,6 @@ import { usePageHead } from "@composables/head"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useService } from "dioc/vue"
|
||||
import { computed, onBeforeUnmount, ref } from "vue"
|
||||
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { connection, disconnect } from "~/helpers/graphql/connection"
|
||||
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:is-removable="activeTabs.length > 1"
|
||||
:close-visibility="'hover'"
|
||||
>
|
||||
<template #tabhead>
|
||||
<template v-if="tab.document.type === 'request'" #tabhead>
|
||||
<HttpTabHead
|
||||
:tab="tab"
|
||||
:is-removable="activeTabs.length > 1"
|
||||
@@ -44,16 +44,24 @@
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
<HttpExampleResponseTab
|
||||
v-if="tab.document.type === 'example-response'"
|
||||
:model-value="tab"
|
||||
@update:model-value="onTabUpdate"
|
||||
/>
|
||||
<!-- Render TabContents -->
|
||||
<HttpTestRunner
|
||||
v-if="tab.document.type === 'test-runner'"
|
||||
:model-value="tab"
|
||||
@update:model-value="onTabUpdate"
|
||||
/>
|
||||
<!-- When document.type === 'request' the tab type is HoppTab<HoppRequestDocument>-->
|
||||
<HttpRequestTab
|
||||
v-if="tab.document.type === 'request'"
|
||||
:model-value="tab"
|
||||
@update:model-value="onTabUpdate"
|
||||
/>
|
||||
<HttpExampleResponseTab
|
||||
v-else-if="tab.document.type === 'example-response'"
|
||||
:model-value="tab"
|
||||
@update:model-value="onTabUpdate"
|
||||
/>
|
||||
<!-- END Render TabContents -->
|
||||
</HoppSmartWindow>
|
||||
<template #actions>
|
||||
<EnvironmentsSelector class="h-full" />
|
||||
@@ -211,9 +219,9 @@ const onTabUpdate = (tab: HoppTab<HoppRequestDocument>) => {
|
||||
|
||||
const addNewTab = () => {
|
||||
const tab = tabs.createNewTab({
|
||||
type: "request",
|
||||
request: getDefaultRESTRequest(),
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
})
|
||||
|
||||
tabs.setActiveTab(tab.id)
|
||||
@@ -222,6 +230,18 @@ const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||
tabs.updateTabOrdering(e.oldIndex, e.newIndex)
|
||||
}
|
||||
|
||||
const getTabName = (tab: HoppTab<HoppTabDocument>) => {
|
||||
if (tab.document.type === "request") {
|
||||
return tab.document.request.name
|
||||
} else if (tab.document.type === "test-runner") {
|
||||
return tab.document.collection.name
|
||||
} else if (tab.document.type === "example-response") {
|
||||
return tab.document.response.name
|
||||
}
|
||||
|
||||
return "Unnamed tab"
|
||||
}
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const removeTab = (tabID: string) => {
|
||||
@@ -255,9 +275,9 @@ const duplicateTab = (tabID: string) => {
|
||||
const tab = tabs.getTabRef(tabID)
|
||||
if (tab.value && tab.value.document.type === "request") {
|
||||
const newTab = tabs.createNewTab({
|
||||
type: "request",
|
||||
request: cloneDeep(tab.value.document.request),
|
||||
isDirty: true,
|
||||
type: "request",
|
||||
})
|
||||
tabs.setActiveTab(newTab.id)
|
||||
}
|
||||
@@ -268,14 +288,6 @@ const onResolveConfirmCloseAllTabs = () => {
|
||||
confirmingCloseAllTabs.value = false
|
||||
}
|
||||
|
||||
const getTabName = (tab: HoppTab<HoppTabDocument>) => {
|
||||
if (tab.document.type === "request") {
|
||||
return tab.document.request.name
|
||||
} else if (tab.document.type === "example-response") {
|
||||
return tab.document.response.name
|
||||
}
|
||||
}
|
||||
|
||||
const requestToRename = computed(() => {
|
||||
if (!renameTabID.value) return null
|
||||
const tab = tabs.getTabRef(renameTabID.value)
|
||||
@@ -386,7 +398,10 @@ defineActionHandler("rest.request.open", ({ doc }) => {
|
||||
tabs.createNewTab(doc)
|
||||
})
|
||||
|
||||
defineActionHandler("request.rename", openReqRenameModal)
|
||||
defineActionHandler("request.rename", () => {
|
||||
if (tabs.currentActiveTab.value.document.type === "request")
|
||||
openReqRenameModal(tabs.currentActiveTab.value.id)
|
||||
})
|
||||
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
|
||||
duplicateTab(tabID ?? currentTabID.value)
|
||||
})
|
||||
|
||||
@@ -136,6 +136,7 @@ const addRequestToTab = () => {
|
||||
const request: unknown = JSON.parse(data.right.shortcode?.request as string)
|
||||
|
||||
tabs.createNewTab({
|
||||
type: "request",
|
||||
request: safelyExtractRESTRequest(request, getDefaultRESTRequest()),
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ExtensionInspectorService extends Service implements Inspector {
|
||||
this.inspection.registerInspector(this)
|
||||
}
|
||||
|
||||
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
|
||||
getInspections(req: Readonly<Ref<HoppRESTRequest | null>>) {
|
||||
const currentExtensionStatus = this.extensionService.extensionStatus
|
||||
|
||||
const isExtensionInstalled = computed(
|
||||
@@ -55,6 +55,8 @@ export class ExtensionInspectorService extends Service implements Inspector {
|
||||
return computed(() => {
|
||||
const results: InspectorResult[] = []
|
||||
|
||||
if (!req.value) return results
|
||||
|
||||
const url = req.value.endpoint
|
||||
const localHostURLs = ["localhost", "127.0.0.1"]
|
||||
|
||||
|
||||
11
packages/hoppscotch-common/src/platform/tab.ts
Normal file
11
packages/hoppscotch-common/src/platform/tab.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PersistableTabState } from "~/services/tab"
|
||||
import { HoppUser } from "./auth"
|
||||
import { HoppTabDocument } from "~/helpers/rest/document"
|
||||
|
||||
export type TabStatePlatformDef = {
|
||||
loadTabStateFromSync: () => Promise<PersistableTabState<HoppTabDocument> | null>
|
||||
writeCurrentTabState: (
|
||||
user: HoppUser,
|
||||
persistableTabState: PersistableTabState<HoppTabDocument>
|
||||
) => Promise<void>
|
||||
}
|
||||
@@ -89,6 +89,9 @@ export class ParameterMenuService extends Service implements ContextMenu {
|
||||
|
||||
const tabService = getService(RESTTabService)
|
||||
|
||||
if (tabService.currentActiveTab.value.document.type === "test-runner")
|
||||
return
|
||||
|
||||
const currentActiveRequest =
|
||||
tabService.currentActiveTab.value.document.type === "request"
|
||||
? tabService.currentActiveTab.value.document.request
|
||||
|
||||
@@ -55,9 +55,9 @@ export class URLMenuService extends Service implements ContextMenu {
|
||||
}
|
||||
|
||||
this.restTab.createNewTab({
|
||||
type: "request",
|
||||
request: request,
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { computed, markRaw, reactive } from "vue"
|
||||
import { Component, Ref, ref, watch } from "vue"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { RESTTabService } from "../tab/rest"
|
||||
|
||||
/**
|
||||
* Defines how to render the text in an Inspector Result
|
||||
*/
|
||||
@@ -127,18 +126,24 @@ export class InspectionService extends Service {
|
||||
watch(
|
||||
() => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id],
|
||||
() => {
|
||||
const currentTabRequest = computed(() =>
|
||||
this.restTab.currentActiveTab.value.document.type === "request"
|
||||
const currentTabRequest = computed(() => {
|
||||
if (
|
||||
this.restTab.currentActiveTab.value.document.type === "test-runner"
|
||||
)
|
||||
return null
|
||||
|
||||
return this.restTab.currentActiveTab.value.document.type === "request"
|
||||
? this.restTab.currentActiveTab.value.document.request
|
||||
: this.restTab.currentActiveTab.value.document.response
|
||||
.originalRequest
|
||||
)
|
||||
})
|
||||
|
||||
const currentTabResponse = computed(() =>
|
||||
this.restTab.currentActiveTab.value.document.type === "request"
|
||||
? this.restTab.currentActiveTab.value.document.response
|
||||
: null
|
||||
)
|
||||
const currentTabResponse = computed(() => {
|
||||
if (this.restTab.currentActiveTab.value.document.type === "request") {
|
||||
return this.restTab.currentActiveTab.value.document.response
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const reqRef = computed(() => currentTabRequest.value)
|
||||
const resRef = computed(() => currentTabResponse.value)
|
||||
@@ -147,6 +152,7 @@ export class InspectionService extends Service {
|
||||
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })
|
||||
|
||||
const inspectorRefs = Array.from(this.inspectors.values()).map((x) =>
|
||||
// @ts-expect-error - This is a valid call
|
||||
x.getInspections(debouncedReq, debouncedRes)
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,10 @@ export class AuthorizationInspectorService
|
||||
const activeTabDocument =
|
||||
this.restTabService.currentActiveTab.value.document
|
||||
|
||||
if (activeTabDocument.type === "example-response") {
|
||||
if (
|
||||
activeTabDocument.type === "example-response" ||
|
||||
activeTabDocument.type === "test-runner"
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -60,6 +63,7 @@ export class AuthorizationInspectorService
|
||||
req: Readonly<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>
|
||||
) {
|
||||
return computed(() => {
|
||||
if (!req.value) return []
|
||||
const currentInterceptorIDValue =
|
||||
this.interceptorService.currentInterceptorID.value
|
||||
|
||||
|
||||
@@ -77,10 +77,12 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
const currentTabRequest =
|
||||
currentTab.document.type === "request"
|
||||
? currentTab.document.request
|
||||
: currentTab.document.response.originalRequest
|
||||
: currentTab.document.type === "example-response"
|
||||
? currentTab.document.response.originalRequest
|
||||
: null
|
||||
|
||||
const environmentVariables = [
|
||||
...currentTabRequest.requestVariables,
|
||||
...(currentTabRequest?.requestVariables ?? []),
|
||||
...this.aggregateEnvsWithSecrets.value,
|
||||
]
|
||||
|
||||
@@ -191,11 +193,13 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
const currentTabRequest =
|
||||
currentTab.document.type === "request"
|
||||
? currentTab.document.request
|
||||
: currentTab.document.response.originalRequest
|
||||
: currentTab.document.type === "example-response"
|
||||
? currentTab.document.response.originalRequest
|
||||
: null
|
||||
|
||||
const environmentVariables =
|
||||
this.filterNonEmptyEnvironmentVariables([
|
||||
...currentTabRequest.requestVariables.map((env) => ({
|
||||
...(currentTabRequest?.requestVariables ?? []).map((env) => ({
|
||||
...env,
|
||||
secret: false,
|
||||
sourceEnv: "RequestVariable",
|
||||
@@ -300,6 +304,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
return computed(() => {
|
||||
const results: InspectorResult[] = []
|
||||
|
||||
if (!req.value) return results
|
||||
|
||||
const headers = req.value.headers
|
||||
|
||||
const params = req.value.params
|
||||
|
||||
@@ -40,6 +40,9 @@ export class HeaderInspectorService extends Service implements Inspector {
|
||||
) {
|
||||
return computed(() => {
|
||||
const results: InspectorResult[] = []
|
||||
|
||||
if (!req.value) return results
|
||||
|
||||
const headers = req.value.headers
|
||||
|
||||
const headerKeys = Object.values(headers).map((header) => header.key)
|
||||
|
||||
@@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
||||
|
||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 4,
|
||||
v: 5,
|
||||
name: "Echo",
|
||||
folders: [],
|
||||
requests: [
|
||||
@@ -51,7 +51,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
|
||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 4,
|
||||
v: 5,
|
||||
name: "Echo",
|
||||
folders: [],
|
||||
requests: [
|
||||
|
||||
@@ -698,22 +698,22 @@ export class PersistenceService extends Service {
|
||||
|
||||
try {
|
||||
if (restTabStateData) {
|
||||
let parsedGqlTabStateData = JSON.parse(restTabStateData)
|
||||
let parsedRESTTabStateData = JSON.parse(restTabStateData)
|
||||
|
||||
// Validate data read from localStorage
|
||||
const result = REST_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)
|
||||
const result = REST_TAB_STATE_SCHEMA.safeParse(parsedRESTTabStateData)
|
||||
|
||||
if (result.success) {
|
||||
parsedGqlTabStateData = result.data
|
||||
parsedRESTTabStateData = result.data
|
||||
} else {
|
||||
this.showErrorToast(restTabStateKey)
|
||||
window.localStorage.setItem(
|
||||
`${restTabStateKey}-backup`,
|
||||
JSON.stringify(parsedGqlTabStateData)
|
||||
JSON.stringify(parsedRESTTabStateData)
|
||||
)
|
||||
}
|
||||
|
||||
this.restTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
|
||||
this.restTabService.loadTabsFromPersistedState(parsedRESTTabStateData)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HoppRESTRequest,
|
||||
HoppRESTHeaders,
|
||||
HoppRESTRequestResponse,
|
||||
HoppCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { entityReference } from "verzod"
|
||||
import { z } from "zod"
|
||||
@@ -75,36 +76,13 @@ const SettingsDefSchema = z.object({
|
||||
ENABLE_AI_EXPERIMENTS: z.optional(z.boolean()),
|
||||
})
|
||||
|
||||
// Common properties shared across REST & GQL collections
|
||||
const HoppCollectionSchemaCommonProps = z
|
||||
.object({
|
||||
v: z.number(),
|
||||
name: z.string(),
|
||||
id: z.optional(z.string()),
|
||||
})
|
||||
.strict()
|
||||
|
||||
const HoppRESTRequestSchema = entityReference(HoppRESTRequest)
|
||||
|
||||
const HoppGQLRequestSchema = entityReference(HoppGQLRequest)
|
||||
|
||||
// @ts-expect-error recursive schema
|
||||
const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({
|
||||
folders: z.array(z.lazy(() => HoppRESTCollectionSchema)),
|
||||
requests: z.optional(z.array(HoppRESTRequestSchema)),
|
||||
const HoppRESTCollectionSchema = entityReference(HoppCollection)
|
||||
|
||||
auth: z.optional(HoppRESTAuth),
|
||||
headers: z.optional(HoppRESTHeaders),
|
||||
}).strict()
|
||||
|
||||
// @ts-expect-error recursive schema
|
||||
const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({
|
||||
folders: z.array(z.lazy(() => HoppGQLCollectionSchema)),
|
||||
requests: z.optional(z.array(HoppGQLRequestSchema)),
|
||||
|
||||
auth: z.optional(HoppGQLAuth),
|
||||
headers: z.optional(z.array(GQLHeader)),
|
||||
}).strict()
|
||||
const HoppGQLCollectionSchema = entityReference(HoppCollection)
|
||||
|
||||
export const VUEX_SCHEMA = z.object({
|
||||
postwoman: z.optional(
|
||||
@@ -551,6 +529,33 @@ export const REST_TAB_STATE_SCHEMA = z
|
||||
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||
isDirty: z.boolean(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("test-runner").catch("test-runner"),
|
||||
config: z.object({
|
||||
delay: z.number(),
|
||||
iterations: z.number(),
|
||||
keepVariableValues: z.boolean(),
|
||||
persistResponses: z.boolean(),
|
||||
stopOnError: z.boolean(),
|
||||
}),
|
||||
status: z.enum(["idle", "running", "stopped", "error"]),
|
||||
collection: HoppRESTCollectionSchema,
|
||||
collectionType: z.enum(["my-collections", "team-collections"]),
|
||||
collectionID: z.optional(z.string()),
|
||||
resultCollection: z.optional(HoppRESTCollectionSchema),
|
||||
testRunnerMeta: z.object({
|
||||
totalRequests: z.number(),
|
||||
completedRequests: z.number(),
|
||||
totalTests: z.number(),
|
||||
passedTests: z.number(),
|
||||
failedTests: z.number(),
|
||||
totalTime: z.number(),
|
||||
}),
|
||||
request: z.nullable(entityReference(HoppRESTRequest)),
|
||||
response: z.nullable(HoppRESTResponseSchema),
|
||||
testResults: z.optional(z.nullable(HoppTestResultSchema)),
|
||||
isDirty: z.boolean(),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
),
|
||||
|
||||
@@ -325,9 +325,9 @@ export class CollectionsSpotlightSearcherService
|
||||
|
||||
this.restTab.createNewTab(
|
||||
{
|
||||
type: "request",
|
||||
request: req,
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath.join("/"),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Container } from "dioc"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { computed } from "vue"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { HoppRESTSaveContext, HoppTabDocument } from "~/helpers/rest/document"
|
||||
import { TabService } from "./tab"
|
||||
import { Container } from "dioc"
|
||||
|
||||
export class RESTTabService extends TabService<HoppTabDocument> {
|
||||
public static readonly ID = "REST_TAB_SERVICE"
|
||||
@@ -52,6 +52,8 @@ export class RESTTabService extends TabService<HoppTabDocument> {
|
||||
public getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
|
||||
for (const tab of this.tabMap.values()) {
|
||||
// For `team-collection` request id can be considered unique
|
||||
if (tab.document.type === "test-runner") continue
|
||||
|
||||
if (ctx?.originLocation === "team-collection") {
|
||||
if (
|
||||
tab.document.saveContext?.originLocation === "team-collection" &&
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import {
|
||||
HoppCollection,
|
||||
HoppRESTHeaders,
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { Service } from "dioc"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { Ref } from "vue"
|
||||
import { runTestRunnerRequest } from "~/helpers/RequestRunner"
|
||||
import {
|
||||
HoppTestRunnerDocument,
|
||||
TestRunnerConfig,
|
||||
} from "~/helpers/rest/document"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
import { HoppTab } from "../tab"
|
||||
|
||||
export type TestRunnerOptions = {
|
||||
stopRef: Ref<boolean>
|
||||
} & TestRunnerConfig
|
||||
|
||||
export type TestRunnerRequest = HoppRESTRequest & {
|
||||
type: "test-response"
|
||||
response?: HoppRESTResponse | null
|
||||
testResults?: HoppTestResult | null
|
||||
isLoading?: boolean
|
||||
error?: string
|
||||
renderResults?: boolean
|
||||
passedTests: number
|
||||
failedTests: number
|
||||
}
|
||||
|
||||
function delay(timeMS: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(resolve, timeMS)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation cancelled"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class TestRunnerService extends Service {
|
||||
public static readonly ID = "TEST_RUNNER_SERVICE"
|
||||
|
||||
public runTests(
|
||||
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
||||
collection: HoppCollection,
|
||||
options: TestRunnerOptions
|
||||
) {
|
||||
// Reset the result collection
|
||||
tab.value.document.status = "running"
|
||||
tab.value.document.resultCollection = {
|
||||
v: collection.v,
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
auth: collection.auth,
|
||||
headers: collection.headers,
|
||||
folders: [],
|
||||
requests: [],
|
||||
}
|
||||
|
||||
this.runTestCollection(tab, collection, options)
|
||||
.then(() => {
|
||||
tab.value.document.status = "stopped"
|
||||
})
|
||||
.catch((error) => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "Test execution stopped"
|
||||
) {
|
||||
tab.value.document.status = "stopped"
|
||||
} else {
|
||||
tab.value.document.status = "error"
|
||||
console.error("Test runner failed:", error)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
tab.value.document.status = "stopped"
|
||||
})
|
||||
}
|
||||
|
||||
private async runTestCollection(
|
||||
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
||||
collection: HoppCollection,
|
||||
options: TestRunnerOptions,
|
||||
parentPath: number[] = [],
|
||||
parentHeaders?: HoppRESTHeaders,
|
||||
parentAuth?: HoppRESTRequest["auth"]
|
||||
) {
|
||||
try {
|
||||
// Compute inherited auth and headers for this collection
|
||||
const inheritedAuth =
|
||||
collection.auth?.authType === "inherit" && collection.auth.authActive
|
||||
? parentAuth || { authType: "none", authActive: false }
|
||||
: collection.auth || { authType: "none", authActive: false }
|
||||
|
||||
const inheritedHeaders: HoppRESTHeaders = [
|
||||
...(parentHeaders || []),
|
||||
...collection.headers,
|
||||
]
|
||||
|
||||
// Process folders progressively
|
||||
for (let i = 0; i < collection.folders.length; i++) {
|
||||
if (options.stopRef?.value) {
|
||||
tab.value.document.status = "stopped"
|
||||
throw new Error("Test execution stopped")
|
||||
}
|
||||
|
||||
const folder = collection.folders[i]
|
||||
const currentPath = [...parentPath, i]
|
||||
|
||||
// Add folder to the result collection
|
||||
this.addFolderToPath(
|
||||
tab.value.document.resultCollection!,
|
||||
currentPath,
|
||||
{
|
||||
...cloneDeep(folder),
|
||||
folders: [],
|
||||
requests: [],
|
||||
}
|
||||
)
|
||||
|
||||
// Pass inherited headers and auth to the folder
|
||||
await this.runTestCollection(
|
||||
tab,
|
||||
folder,
|
||||
options,
|
||||
currentPath,
|
||||
inheritedHeaders,
|
||||
inheritedAuth
|
||||
)
|
||||
}
|
||||
|
||||
// Process requests progressively
|
||||
for (let i = 0; i < collection.requests.length; i++) {
|
||||
if (options.stopRef?.value) {
|
||||
tab.value.document.status = "stopped"
|
||||
throw new Error("Test execution stopped")
|
||||
}
|
||||
|
||||
const request = collection.requests[i] as TestRunnerRequest
|
||||
const currentPath = [...parentPath, i]
|
||||
|
||||
// Add request to the result collection before execution
|
||||
this.addRequestToPath(
|
||||
tab.value.document.resultCollection!,
|
||||
currentPath,
|
||||
cloneDeep(request)
|
||||
)
|
||||
|
||||
// Update the request with inherited headers and auth before execution
|
||||
const finalRequest = {
|
||||
...request,
|
||||
auth:
|
||||
request.auth.authType === "inherit" && request.auth.authActive
|
||||
? inheritedAuth
|
||||
: request.auth,
|
||||
headers: [...inheritedHeaders, ...request.headers],
|
||||
}
|
||||
|
||||
await this.runTestRequest(
|
||||
tab,
|
||||
finalRequest,
|
||||
collection,
|
||||
options,
|
||||
currentPath
|
||||
)
|
||||
|
||||
if (options.delay && options.delay > 0) {
|
||||
try {
|
||||
await delay(options.delay)
|
||||
} catch (error) {
|
||||
if (options.stopRef?.value) {
|
||||
tab.value.document.status = "stopped"
|
||||
throw new Error("Test execution stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "Test execution stopped"
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
tab.value.document.status = "error"
|
||||
console.error("Collection execution failed:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private addFolderToPath(
|
||||
collection: HoppCollection,
|
||||
path: number[],
|
||||
folder: HoppCollection
|
||||
) {
|
||||
let current = collection
|
||||
|
||||
// Navigate to the parent folder
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
current = current.folders[path[i]]
|
||||
}
|
||||
|
||||
// Add the folder at the specified index
|
||||
if (path.length > 0) {
|
||||
current.folders[path[path.length - 1]] = folder
|
||||
}
|
||||
}
|
||||
|
||||
private addRequestToPath(
|
||||
collection: HoppCollection,
|
||||
path: number[],
|
||||
request: TestRunnerRequest
|
||||
) {
|
||||
let current = collection
|
||||
|
||||
// Navigate to the parent folder
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
current = current.folders[path[i]]
|
||||
}
|
||||
|
||||
// Add the request at the specified index
|
||||
if (path.length > 0) {
|
||||
current.requests[path[path.length - 1]] = request
|
||||
}
|
||||
}
|
||||
|
||||
private updateRequestAtPath(
|
||||
collection: HoppCollection,
|
||||
path: number[],
|
||||
updates: Partial<TestRunnerRequest>
|
||||
) {
|
||||
let current = collection
|
||||
|
||||
// Navigate to the parent folder
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
current = current.folders[path[i]]
|
||||
}
|
||||
|
||||
// Update the request at the specified index
|
||||
if (path.length > 0) {
|
||||
const index = path[path.length - 1]
|
||||
current.requests[index] = {
|
||||
...current.requests[index],
|
||||
...updates,
|
||||
} as TestRunnerRequest
|
||||
}
|
||||
}
|
||||
|
||||
private async runTestRequest(
|
||||
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
||||
request: TestRunnerRequest,
|
||||
collection: HoppCollection,
|
||||
options: TestRunnerOptions,
|
||||
path: number[]
|
||||
) {
|
||||
if (options.stopRef?.value) {
|
||||
throw new Error("Test execution stopped")
|
||||
}
|
||||
|
||||
try {
|
||||
// Update request status in the result collection
|
||||
this.updateRequestAtPath(tab.value.document.resultCollection!, path, {
|
||||
isLoading: true,
|
||||
error: undefined,
|
||||
})
|
||||
|
||||
const results = await runTestRunnerRequest(request)
|
||||
|
||||
if (options.stopRef?.value) {
|
||||
throw new Error("Test execution stopped")
|
||||
}
|
||||
|
||||
if (results && E.isRight(results)) {
|
||||
const { response, testResult } = results.right
|
||||
const { passed, failed } = this.getTestResultInfo(testResult)
|
||||
|
||||
tab.value.document.testRunnerMeta.totalTests += passed + failed
|
||||
tab.value.document.testRunnerMeta.passedTests += passed
|
||||
tab.value.document.testRunnerMeta.failedTests += failed
|
||||
|
||||
// Update request with results in the result collection
|
||||
this.updateRequestAtPath(tab.value.document.resultCollection!, path, {
|
||||
testResults: testResult,
|
||||
response: options.persistResponses ? response : null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
if (response.type === "success" || response.type === "fail") {
|
||||
tab.value.document.testRunnerMeta.totalTime +=
|
||||
response.meta.responseDuration
|
||||
tab.value.document.testRunnerMeta.completedRequests += 1
|
||||
}
|
||||
} else {
|
||||
const errorMsg = "Request execution failed"
|
||||
|
||||
// Update request with error in the result collection
|
||||
this.updateRequestAtPath(tab.value.document.resultCollection!, path, {
|
||||
error: errorMsg,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
if (options.stopOnError) {
|
||||
tab.value.document.status = "stopped"
|
||||
throw new Error("Test execution stopped due to error")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "Test execution stopped"
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
// Update request with error in the result collection
|
||||
this.updateRequestAtPath(tab.value.document.resultCollection!, path, {
|
||||
error: errorMsg,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
if (options.stopOnError) {
|
||||
tab.value.document.status = "stopped"
|
||||
throw new Error("Test execution stopped due to error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getTestResultInfo(testResult: HoppTestData) {
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
|
||||
for (const result of testResult.expectResults) {
|
||||
if (result.status === "pass") {
|
||||
passed++
|
||||
} else if (result.status === "fail") {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
for (const nestedTest of testResult.tests) {
|
||||
const nestedResult = this.getTestResultInfo(nestedTest)
|
||||
passed += nestedResult.passed
|
||||
failed += nestedResult.failed
|
||||
}
|
||||
|
||||
return { passed, failed }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"homepage": "https://github.com/hoppscotch/hoppscotch#readme",
|
||||
"devDependencies": {
|
||||
"@types/lodash": "4.17.10",
|
||||
"@types/uuid": "10.0.0",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "5.4.9"
|
||||
},
|
||||
@@ -44,6 +45,7 @@
|
||||
"io-ts": "2.2.21",
|
||||
"lodash": "4.17.21",
|
||||
"parser-ts": "0.7.0",
|
||||
"uuid": "10.0.0",
|
||||
"verzod": "0.2.2",
|
||||
"zod": "3.23.8"
|
||||
}
|
||||
|
||||
@@ -4,22 +4,25 @@ import V1_VERSION from "./v/1"
|
||||
import V2_VERSION from "./v/2"
|
||||
import V3_VERSION from "./v/3"
|
||||
import V4_VERSION from "./v/4"
|
||||
import V5_VERSION from "./v/5"
|
||||
|
||||
import { z } from "zod"
|
||||
import { translateToNewRequest } from "../rest"
|
||||
import { translateToGQLRequest } from "../graphql"
|
||||
import { generateUniqueRefId } from "../utils/collection"
|
||||
|
||||
const versionedObject = z.object({
|
||||
v: z.number(),
|
||||
})
|
||||
|
||||
export const HoppCollection = createVersionedEntity({
|
||||
latestVersion: 4,
|
||||
latestVersion: 5,
|
||||
versionMap: {
|
||||
1: V1_VERSION,
|
||||
2: V2_VERSION,
|
||||
3: V3_VERSION,
|
||||
4: V4_VERSION,
|
||||
5: V5_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
const versionCheck = versionedObject.safeParse(data)
|
||||
@@ -35,7 +38,7 @@ export const HoppCollection = createVersionedEntity({
|
||||
|
||||
export type HoppCollection = InferredEntity<typeof HoppCollection>
|
||||
|
||||
export const CollectionSchemaVersion = 4
|
||||
export const CollectionSchemaVersion = 5
|
||||
|
||||
/**
|
||||
* Generates a Collection object. This ignores the version number object
|
||||
@@ -46,6 +49,7 @@ export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
|
||||
return {
|
||||
v: CollectionSchemaVersion,
|
||||
...x,
|
||||
_ref_id: x._ref_id ? x._ref_id : generateUniqueRefId("coll"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +76,7 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
if (x._ref_id) obj._ref_id = x._ref_id
|
||||
|
||||
return obj
|
||||
}
|
||||
@@ -99,6 +104,7 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
if (x._ref_id) obj._ref_id = x._ref_id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { HoppRESTAuth } from "../../rest/v/8"
|
||||
|
||||
import { V3_SCHEMA, v3_baseCollectionSchema } from "./3"
|
||||
|
||||
const v4_baseCollectionSchema = v3_baseCollectionSchema.extend({
|
||||
export const v4_baseCollectionSchema = v3_baseCollectionSchema.extend({
|
||||
v: z.literal(4),
|
||||
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
|
||||
})
|
||||
@@ -19,7 +19,7 @@ type Output = z.output<typeof v4_baseCollectionSchema> & {
|
||||
folders: Output[]
|
||||
}
|
||||
|
||||
const V4_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> =
|
||||
export const V4_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> =
|
||||
v4_baseCollectionSchema.extend({
|
||||
folders: z.lazy(() => z.array(V4_SCHEMA)),
|
||||
})
|
||||
|
||||
36
packages/hoppscotch-data/src/collection/v/5.ts
Normal file
36
packages/hoppscotch-data/src/collection/v/5.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineVersion } from "verzod"
|
||||
import { z } from "zod"
|
||||
|
||||
import { V4_SCHEMA, v4_baseCollectionSchema } from "./4"
|
||||
import { generateUniqueRefId } from "../../utils/collection"
|
||||
|
||||
const v5_baseCollectionSchema = v4_baseCollectionSchema.extend({
|
||||
v: z.literal(5),
|
||||
_ref_id: z.string().optional(),
|
||||
})
|
||||
|
||||
type Input = z.input<typeof v5_baseCollectionSchema> & {
|
||||
folders: Input[]
|
||||
}
|
||||
|
||||
type Output = z.output<typeof v5_baseCollectionSchema> & {
|
||||
folders: Output[]
|
||||
}
|
||||
|
||||
const V5_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> =
|
||||
v5_baseCollectionSchema.extend({
|
||||
folders: z.lazy(() => z.array(V5_SCHEMA)),
|
||||
})
|
||||
|
||||
export default defineVersion({
|
||||
initial: false,
|
||||
schema: V5_SCHEMA,
|
||||
// @ts-expect-error
|
||||
up(old: z.infer<typeof V4_SCHEMA>) {
|
||||
return {
|
||||
...old,
|
||||
v: 5 as const,
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -5,3 +5,4 @@ export * from "./rawKeyValue"
|
||||
export * from "./environment"
|
||||
export * from "./global-environment"
|
||||
export * from "./predefinedVariables"
|
||||
export * from "./utils/collection"
|
||||
|
||||
13
packages/hoppscotch-data/src/utils/collection.ts
Normal file
13
packages/hoppscotch-data/src/utils/collection.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { v4 as uuidV4 } from "uuid"
|
||||
|
||||
/**
|
||||
* Generate a unique reference ID
|
||||
* @param prefix Prefix to add to the generated ID
|
||||
* @returns The generated reference ID
|
||||
*/
|
||||
export const generateUniqueRefId = (prefix = "") => {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const randomPart = uuidV4()
|
||||
|
||||
return `${prefix}_${timestamp}_${randomPart}`
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
updateRESTRequestOrder,
|
||||
} from "@hoppscotch/common/newstore/collections"
|
||||
import {
|
||||
generateUniqueRefId,
|
||||
GQLHeader,
|
||||
HoppCollection,
|
||||
HoppGQLRequest,
|
||||
@@ -70,6 +71,7 @@ function initCollectionsSync() {
|
||||
|
||||
gqlCollectionsSyncer.startStoreSync()
|
||||
|
||||
// TODO: fix collection schema transformation on backend maybe?
|
||||
loadUserCollections("REST")
|
||||
loadUserCollections("GQL")
|
||||
|
||||
@@ -94,6 +96,7 @@ function initCollectionsSync() {
|
||||
|
||||
type ExportedUserCollectionREST = {
|
||||
id?: string
|
||||
_ref_id?: string
|
||||
folders: ExportedUserCollectionREST[]
|
||||
requests: Array<HoppRESTRequest & { id: string }>
|
||||
name: string
|
||||
@@ -102,6 +105,7 @@ type ExportedUserCollectionREST = {
|
||||
|
||||
type ExportedUserCollectionGQL = {
|
||||
id?: string
|
||||
_ref_id?: string
|
||||
folders: ExportedUserCollectionGQL[]
|
||||
requests: Array<HoppGQLRequest & { id: string }>
|
||||
name: string
|
||||
@@ -130,11 +134,13 @@ function exportedCollectionToHoppCollection(
|
||||
: {
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
return {
|
||||
id: restCollection.id,
|
||||
v: 4,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 5,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
@@ -192,11 +198,13 @@ function exportedCollectionToHoppCollection(
|
||||
: {
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
return {
|
||||
id: gqlCollection.id,
|
||||
v: 4,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 5,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
@@ -366,6 +374,7 @@ function setupUserCollectionCreatedSubscription() {
|
||||
: {
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
runDispatchWithOutSyncing(() => {
|
||||
@@ -374,7 +383,8 @@ function setupUserCollectionCreatedSubscription() {
|
||||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 4,
|
||||
v: 5,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
})
|
||||
@@ -382,7 +392,8 @@ function setupUserCollectionCreatedSubscription() {
|
||||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 4,
|
||||
v: 5,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
})
|
||||
@@ -587,12 +598,13 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||
)
|
||||
|
||||
// Incoming data transformed to the respective internal representations
|
||||
const { auth, headers } =
|
||||
const { auth, headers, _ref_id } =
|
||||
data && data != "null"
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
const folders = transformDuplicatedCollections(childCollectionsJSONStr)
|
||||
@@ -607,7 +619,8 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 3,
|
||||
v: 5,
|
||||
_ref_id,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
@@ -1037,7 +1050,7 @@ function transformDuplicatedCollections(
|
||||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 3,
|
||||
v: 5,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
settingsStore,
|
||||
} from "@hoppscotch/common/newstore/settings"
|
||||
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import {
|
||||
generateUniqueRefId,
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
|
||||
import { getSyncInitFunction, StoreSyncDefinitionOf } from "../../lib/sync"
|
||||
import { createMapper } from "../../lib/sync/mapper"
|
||||
@@ -53,6 +57,7 @@ const recursivelySyncCollections = async (
|
||||
authActive: true,
|
||||
},
|
||||
headers: collection.headers ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
}
|
||||
const res = await createRESTRootUserCollection(
|
||||
collection.name,
|
||||
@@ -69,9 +74,11 @@ const recursivelySyncCollections = async (
|
||||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
collection.id = parentCollectionID
|
||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||
collection.auth = returnedData.auth
|
||||
collection.headers = returnedData.headers
|
||||
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
|
||||
@@ -86,6 +93,7 @@ const recursivelySyncCollections = async (
|
||||
authActive: true,
|
||||
},
|
||||
headers: collection.headers ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
}
|
||||
|
||||
const res = await createRESTChildUserCollection(
|
||||
@@ -105,9 +113,11 @@ const recursivelySyncCollections = async (
|
||||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
}
|
||||
|
||||
collection.id = childCollectionId
|
||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||
collection.auth = returnedData.auth
|
||||
collection.headers = returnedData.headers
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PersistableTabState } from "@hoppscotch/common/services/tab"
|
||||
import { HoppTabDocument } from "@hoppscotch/common/helpers/rest/document"
|
||||
import { HoppUser } from "@hoppscotch/common/platform/auth"
|
||||
import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab"
|
||||
import { def as platformAuth } from "@platform/auth"
|
||||
import { getCurrentRestSession, updateUserSession } from "./tabState.api"
|
||||
import { SessionType } from "../../api/generated/graphql"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
async function writeCurrentTabState(
|
||||
_: HoppUser,
|
||||
persistableTabState: PersistableTabState<HoppTabDocument>
|
||||
) {
|
||||
await updateUserSession(JSON.stringify(persistableTabState), SessionType.Rest)
|
||||
}
|
||||
|
||||
async function loadTabStateFromSync(): Promise<PersistableTabState<HoppTabDocument> | null> {
|
||||
const currentUser = platformAuth.getCurrentUser()
|
||||
|
||||
if (!currentUser)
|
||||
throw new Error("Cannot load request from sync without login")
|
||||
|
||||
const res = await getCurrentRestSession()
|
||||
|
||||
if (E.isRight(res)) {
|
||||
const currentRESTSession = res.right.me.currentRESTSession
|
||||
|
||||
return currentRESTSession ? JSON.parse(currentRESTSession) : null
|
||||
} else {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const def: TabStatePlatformDef = {
|
||||
loadTabStateFromSync,
|
||||
writeCurrentTabState,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
upperSecondaryStickyFold: "var(--upper-secondary-sticky-fold)",
|
||||
upperTertiaryStickyFold: "var(--upper-tertiary-sticky-fold)",
|
||||
upperFourthStickyFold: "var(--upper-fourth-sticky-fold)",
|
||||
upperRunnerStickyFold: "var(--upper-runner-sticky-fold)",
|
||||
upperMobilePrimaryStickyFold: "var(--upper-mobile-primary-sticky-fold)",
|
||||
upperMobileSecondaryStickyFold:
|
||||
"var(--upper-mobile-secondary-sticky-fold)",
|
||||
|
||||
145
pnpm-lock.yaml
generated
145
pnpm-lock.yaml
generated
@@ -30,8 +30,8 @@ importers:
|
||||
specifier: 19.5.0
|
||||
version: 19.5.0
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3))
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3))
|
||||
'@types/node':
|
||||
specifier: 22.7.6
|
||||
version: 22.7.6
|
||||
@@ -499,8 +499,8 @@ importers:
|
||||
specifier: workspace:^
|
||||
version: link:../hoppscotch-js-sandbox
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))
|
||||
'@hoppscotch/vue-toasted':
|
||||
specifier: 0.1.0
|
||||
version: 0.1.0(vue@3.5.12(typescript@5.3.3))
|
||||
@@ -904,6 +904,9 @@ importers:
|
||||
parser-ts:
|
||||
specifier: 0.7.0
|
||||
version: 0.7.0(fp-ts@2.16.9)
|
||||
uuid:
|
||||
specifier: 10.0.0
|
||||
version: 10.0.0
|
||||
verzod:
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2(zod@3.23.8)
|
||||
@@ -914,6 +917,9 @@ importers:
|
||||
'@types/lodash':
|
||||
specifier: 4.17.10
|
||||
version: 4.17.10
|
||||
'@types/uuid':
|
||||
specifier: 10.0.0
|
||||
version: 10.0.0
|
||||
typescript:
|
||||
specifier: 5.6.3
|
||||
version: 5.6.3
|
||||
@@ -3760,6 +3766,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: 3.5.12
|
||||
|
||||
'@hoppscotch/ui@0.2.2':
|
||||
resolution: {integrity: sha512-rDRfG9onpmlDCO2KjJZN6UIlFC5Ewif689guvtVCZh9a+soy9nUUTbwMHI9913oBIJpbZ4GTLHGpdCl1YHUiVQ==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
vue: 3.5.12
|
||||
|
||||
'@hoppscotch/vue-sonner@1.2.3':
|
||||
resolution: {integrity: sha512-P1gyvHHLsPeB8lsLP5SrqwQatuwOKtbsP83sKhyIV3WL2rJj3+DiFfqo2ErNBa+Sl0gM68o1V+wuOS7zbR//6g==}
|
||||
|
||||
@@ -3882,26 +3894,26 @@ packages:
|
||||
resolution: {integrity: sha512-GG428DkrrWCMhxRMRQZjuS7zmSUzarYcaHJqG9VB8dXAxw4iQDoKVQ7ChJRB6ZtsCsX3Jse1PEUlHrJiyQrOTg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@10.0.0':
|
||||
resolution: {integrity: sha512-OcaWc63NC/9p1cMdgoNKBj4d61BH8sUW1Hfs6YijTd9656ZR4rNqXAlRnBrfS5ABq0vjQjpa8VnyvH9hK49yBw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@10.0.4':
|
||||
resolution: {integrity: sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@11.0.0-beta.1':
|
||||
resolution: {integrity: sha512-yMXfN4hg/EeSdtWfmoMrwB9X4TXwkBoZlTIpNydQaW9y0tSJHGnUPRoahtkbsyACCm9leSJINLY4jQ0rK6BK0Q==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@9.3.0-beta.20':
|
||||
resolution: {integrity: sha512-hwqQXyTnDzAVZ300SU31jO0+3OJbpOdfVU6iBkrmNpS7t2HRnVACo0EwcEXzJa++4EVDreqz5OeqJbt+PeSGGA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@10.0.0':
|
||||
resolution: {integrity: sha512-6ngLfI7DOTew2dcF9WMJx+NnMWghMBhIiHbGg+wRvngpzD5KZJZiJVuzMsUQE1a5YebEmtpTEfUrDp/NqVGdiw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@10.0.4':
|
||||
resolution: {integrity: sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@11.0.0-beta.1':
|
||||
resolution: {integrity: sha512-Md/4T/QOx7wZ7zqVzSsMx2M/9Mx/1nsgsjXS5SFIowFKydqUhMz7K+y7pMFh781aNYz+rGXYwad8E9/+InK9SA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@9.3.0-beta.20':
|
||||
resolution: {integrity: sha512-RucSPqh8O9FFxlYUysQTerSw0b9HIRpyoN1Zjogpm0qLiHK+lBNSa5sh1nCJ4wSsNcjphzgpLQCyR60GZlRV8g==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -10381,6 +10393,7 @@ packages:
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
deprecated: |-
|
||||
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
qs@6.11.0:
|
||||
@@ -15394,30 +15407,6 @@ snapshots:
|
||||
stringify-object: 3.3.0
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hoppscotch/ui@0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))':
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3))
|
||||
'@fontsource-variable/inter': 5.1.0
|
||||
'@fontsource-variable/material-symbols-rounded': 5.1.3
|
||||
'@fontsource-variable/roboto-mono': 5.1.0
|
||||
'@hoppscotch/vue-sonner': 1.2.3
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.3.3))
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))
|
||||
'@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.3.3))
|
||||
fp-ts: 2.16.9
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))
|
||||
vue: 3.5.12(typescript@5.3.3)
|
||||
vue-promise-modals: 0.1.0(typescript@5.3.3)
|
||||
vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.3.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- typescript
|
||||
- vite
|
||||
|
||||
'@hoppscotch/ui@0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))':
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3))
|
||||
@@ -15490,6 +15479,54 @@ snapshots:
|
||||
- typescript
|
||||
- vite
|
||||
|
||||
'@hoppscotch/ui@0.2.2(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))':
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3))
|
||||
'@fontsource-variable/inter': 5.0.15
|
||||
'@fontsource-variable/material-symbols-rounded': 5.0.16
|
||||
'@fontsource-variable/roboto-mono': 5.0.16
|
||||
'@hoppscotch/vue-sonner': 1.2.3
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.3.3))
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))
|
||||
'@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.3.3))
|
||||
fp-ts: 2.16.9
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))
|
||||
vue: 3.5.12(typescript@5.3.3)
|
||||
vue-promise-modals: 0.1.0(typescript@5.3.3)
|
||||
vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.3.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- typescript
|
||||
- vite
|
||||
|
||||
'@hoppscotch/ui@0.2.2(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
'@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.6.3))
|
||||
'@fontsource-variable/inter': 5.0.15
|
||||
'@fontsource-variable/material-symbols-rounded': 5.0.16
|
||||
'@fontsource-variable/roboto-mono': 5.0.16
|
||||
'@hoppscotch/vue-sonner': 1.2.3
|
||||
'@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.6.3))
|
||||
'@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))
|
||||
'@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.6.3))
|
||||
fp-ts: 2.16.9
|
||||
lodash-es: 4.17.21
|
||||
path: 0.12.7
|
||||
vite-plugin-eslint: 1.8.1(eslint@9.12.0(jiti@2.3.3))(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))
|
||||
vue: 3.5.12(typescript@5.6.3)
|
||||
vue-promise-modals: 0.1.0(typescript@5.6.3)
|
||||
vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.6.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- eslint
|
||||
- terser
|
||||
- typescript
|
||||
- vite
|
||||
|
||||
'@hoppscotch/vue-sonner@1.2.3': {}
|
||||
|
||||
'@hoppscotch/vue-toasted@0.1.0(vue@3.5.12(typescript@5.3.3))':
|
||||
@@ -15588,8 +15625,8 @@ snapshots:
|
||||
|
||||
'@intlify/bundle-utils@3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 10.0.0
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/message-compiler': 11.0.0-beta.1
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
jsonc-eslint-parser: 1.4.1
|
||||
source-map: 0.6.1
|
||||
yaml-eslint-parser: 0.3.2
|
||||
@@ -15613,8 +15650,8 @@ snapshots:
|
||||
|
||||
'@intlify/bundle-utils@9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 10.0.0
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/message-compiler': 11.0.0-beta.1
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
acorn: 8.12.1
|
||||
escodegen: 2.1.0
|
||||
estree-walker: 2.0.2
|
||||
@@ -15630,33 +15667,33 @@ snapshots:
|
||||
'@intlify/message-compiler': 10.0.4
|
||||
'@intlify/shared': 10.0.4
|
||||
|
||||
'@intlify/message-compiler@10.0.0':
|
||||
dependencies:
|
||||
'@intlify/shared': 10.0.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/message-compiler@10.0.4':
|
||||
dependencies:
|
||||
'@intlify/shared': 10.0.4
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/message-compiler@11.0.0-beta.1':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/message-compiler@9.3.0-beta.20':
|
||||
dependencies:
|
||||
'@intlify/shared': 9.3.0-beta.20
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@10.0.0': {}
|
||||
|
||||
'@intlify/shared@10.0.4': {}
|
||||
|
||||
'@intlify/shared@11.0.0-beta.1': {}
|
||||
|
||||
'@intlify/shared@9.3.0-beta.20': {}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@5.2.0(@vue/compiler-dom@3.5.12)(eslint@9.12.0(jiti@2.3.3))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@2.3.3))
|
||||
'@intlify/bundle-utils': 9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
|
||||
'@rollup/pluginutils': 5.1.2(rollup@4.24.0)
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3)
|
||||
@@ -15682,7 +15719,7 @@ snapshots:
|
||||
'@intlify/vite-plugin-vue-i18n@6.0.1(vite@4.5.0(@types/node@18.18.8)(sass@1.80.3)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@4.9.5)))':
|
||||
dependencies:
|
||||
'@intlify/bundle-utils': 7.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@4.9.5)))
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
debug: 4.3.7
|
||||
fast-glob: 3.3.2
|
||||
@@ -15696,7 +15733,7 @@ snapshots:
|
||||
'@intlify/vite-plugin-vue-i18n@7.0.0(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))':
|
||||
dependencies:
|
||||
'@intlify/bundle-utils': 3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
debug: 4.3.7
|
||||
fast-glob: 3.3.2
|
||||
@@ -15710,7 +15747,7 @@ snapshots:
|
||||
'@intlify/vite-plugin-vue-i18n@7.0.0(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))':
|
||||
dependencies:
|
||||
'@intlify/bundle-utils': 3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
debug: 4.3.7
|
||||
fast-glob: 3.3.2
|
||||
@@ -15721,11 +15758,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
|
||||
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
'@babel/parser': 7.25.7
|
||||
optionalDependencies:
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/shared': 11.0.0-beta.1
|
||||
'@vue/compiler-dom': 3.5.12
|
||||
vue: 3.5.12(typescript@5.6.3)
|
||||
vue-i18n: 10.0.4(vue@3.5.12(typescript@5.6.3))
|
||||
|
||||
Reference in New Issue
Block a user