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:
@@ -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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user