chore: collection runner enhancements and ui clean up (#4564)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -1215,6 +1215,7 @@
|
||||
"include_active_environment": "Include active environment:",
|
||||
"cli": "CLI",
|
||||
"delay": "Delay",
|
||||
"negative_delay": "Delay cannot be negative",
|
||||
"ui": "Runner",
|
||||
"running_collection": "Running collection",
|
||||
"run_config": "Run Configuration",
|
||||
@@ -1228,7 +1229,9 @@
|
||||
"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.",
|
||||
"run_collection": "Run collection"
|
||||
"run_collection": "Run collection",
|
||||
"no_passed_tests": "No tests passed",
|
||||
"no_failed_tests": "No tests failed"
|
||||
},
|
||||
"ai_experiments": {
|
||||
"generate_request_name": "Generate Request Name Using AI",
|
||||
|
||||
@@ -150,6 +150,7 @@ declare module 'vue' {
|
||||
HttpAuthorizationNTLM: typeof import('./components/http/authorization/NTLM.vue')['default']
|
||||
HttpAuthorizationOAuth2: typeof import('./components/http/authorization/OAuth2.vue')['default']
|
||||
HttpBody: typeof import('./components/http/Body.vue')['default']
|
||||
HttpBodyBinary: typeof import('./components/http/BodyBinary.vue')['default']
|
||||
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
||||
HttpCodegen: typeof import('./components/http/Codegen.vue')['default']
|
||||
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
||||
@@ -225,6 +226,7 @@ declare module 'vue' {
|
||||
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
||||
ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default']
|
||||
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
|
||||
ImportExportImportExportStepsImportSummary: typeof import('./components/importExport/ImportExportSteps/ImportSummary.vue')['default']
|
||||
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
|
||||
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
|
||||
InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
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">
|
||||
<div class="flex gap-4 mb-1 items-center">
|
||||
<span
|
||||
class="flex items-center justify-center truncate pointer-events-none"
|
||||
:style="{ color: requestLabelColor }"
|
||||
@@ -20,7 +20,7 @@
|
||||
v-if="request.response?.statusCode"
|
||||
:class="[
|
||||
statusCategory.className,
|
||||
'outlined text-xs rounded-md px-2 flex items-center',
|
||||
'outlined text-[10px] rounded px-2 flex items-center',
|
||||
]"
|
||||
>
|
||||
{{ `${request.response?.statusCode}` }}
|
||||
|
||||
@@ -31,13 +31,11 @@
|
||||
<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
|
||||
@@ -115,6 +113,7 @@
|
||||
collectionID: tab.document.collectionID,
|
||||
}
|
||||
"
|
||||
:prev-config="testRunnerConfig"
|
||||
@hide-modal="showCollectionsRunnerModal = false"
|
||||
/>
|
||||
</template>
|
||||
@@ -127,7 +126,8 @@ 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 { computed, nextTick, onMounted, ref, watch } from "vue"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
@@ -141,7 +141,12 @@ import {
|
||||
TestRunnerCollectionsAdapter,
|
||||
} from "~/helpers/runner/adapter"
|
||||
import { getErrorMessage } from "~/helpers/runner/collection-tree"
|
||||
import { getRESTCollectionByRefId } from "~/newstore/collections"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import {
|
||||
getRESTCollectionByRefId,
|
||||
getRESTCollectionInheritedProps,
|
||||
restCollectionStore,
|
||||
} from "~/newstore/collections"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import {
|
||||
@@ -154,6 +159,12 @@ const t = useI18n()
|
||||
const toast = useToast()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const teamCollectionAdapter = new TeamCollectionAdapter(null)
|
||||
const teamCollectionList = useReadonlyStream(
|
||||
teamCollectionAdapter.collections$,
|
||||
[]
|
||||
)
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppTestRunnerDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -218,8 +229,33 @@ const showResult = computed(() => {
|
||||
})
|
||||
|
||||
const runTests = async () => {
|
||||
const { collectionID, collectionType } = tab.value.document
|
||||
|
||||
const isPersonalWorkspace = collectionType === "my-collections"
|
||||
|
||||
const collections = isPersonalWorkspace
|
||||
? restCollectionStore.value.state
|
||||
: teamCollectionList.value.map(teamCollToHoppRESTColl)
|
||||
|
||||
const collectionInheritedProps = getRESTCollectionInheritedProps(
|
||||
collectionID,
|
||||
collections,
|
||||
collectionType
|
||||
)
|
||||
|
||||
const { auth, headers } = collectionInheritedProps ?? {
|
||||
auth: { authActive: true, authType: "none" },
|
||||
headers: [],
|
||||
}
|
||||
|
||||
// Accommodate collection properties for personal workspace
|
||||
// TODO: Resolve the collection properties computation for team workspaces
|
||||
const resolvedCollection = isPersonalWorkspace
|
||||
? { ...collection.value, auth, headers }
|
||||
: collection.value
|
||||
|
||||
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
|
||||
testRunnerService.runTests(tab, collection.value, {
|
||||
testRunnerService.runTests(tab, resolvedCollection, {
|
||||
...testRunnerConfig.value,
|
||||
stopRef: testRunnerStopRef,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type="number"
|
||||
:label="t('collection_runner.delay')"
|
||||
class="!rounded-r-none !border-r-0"
|
||||
:class="{ 'border-red-500': config.delay < 0 }"
|
||||
input-styles="floating-input !rounded-r-none !border-r-0"
|
||||
>
|
||||
<template #button>
|
||||
@@ -31,6 +32,9 @@
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
<p v-if="config.delay < 0" class="text-xs text-red-500 mt-1">
|
||||
{{ t("collection_runner.negative_delay") }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -133,6 +137,7 @@
|
||||
<HoppButtonPrimary
|
||||
v-if="activeTab === 'test-runner'"
|
||||
:label="`${t('test.run')}`"
|
||||
:disabled="config.delay < 0"
|
||||
:icon="IconPlay"
|
||||
outline
|
||||
@click="runTests"
|
||||
@@ -157,7 +162,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
import { HoppCollection } from "@hoppscotch/data"
|
||||
@@ -203,6 +208,7 @@ export type CollectionRunnerData =
|
||||
const props = defineProps<{
|
||||
sameTab?: boolean
|
||||
collectionRunnerData: CollectionRunnerData
|
||||
prevConfig?: Partial<TestRunnerConfig>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -230,6 +236,12 @@ const config = ref<TestRunnerConfig>({
|
||||
keepVariableValues: false,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.prevConfig) {
|
||||
config.value = { ...config.value, ...props.prevConfig }
|
||||
}
|
||||
})
|
||||
|
||||
const runTests = async () => {
|
||||
const collectionTree = await getCollectionTree(
|
||||
props.collectionRunnerData.type,
|
||||
|
||||
@@ -27,7 +27,16 @@
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center test-runner pr-2">
|
||||
<div
|
||||
class="flex flex-col justify-center test-runner pr-2"
|
||||
:class="{
|
||||
hidden:
|
||||
(selectedTestTab === 'passed' &&
|
||||
tab.document.testRunnerMeta.passedTests === 0) ||
|
||||
(selectedTestTab === 'failed' &&
|
||||
tab.document.testRunnerMeta.failedTests === 0),
|
||||
}"
|
||||
>
|
||||
<HoppSmartTree :expand-all="true" :adapter="collectionAdapter">
|
||||
<template #content="{ node }">
|
||||
<HttpTestResultFolder
|
||||
@@ -60,17 +69,34 @@
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
(selectedTestTab === 'passed' &&
|
||||
tab.document.testRunnerMeta.passedTests === 0) ||
|
||||
(selectedTestTab === 'failed' &&
|
||||
tab.document.testRunnerMeta.failedTests === 0)
|
||||
"
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:text="
|
||||
selectedTestTab === 'passed'
|
||||
? `${t('collection_runner.no_passed_tests')}`
|
||||
: `${t('collection_runner.no_failed_tests')}`
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SmartTreeAdapter } from "@hoppscotch/ui"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { HoppTestRunnerDocument } from "~/helpers/rest/document"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
defineProps<{
|
||||
tab: HoppTab<HoppTestRunnerDocument>
|
||||
|
||||
@@ -51,9 +51,12 @@ const getCollectionChildrenIDs = async (collID: string) => {
|
||||
return E.left(data.left)
|
||||
}
|
||||
|
||||
collsList.push(...data.right.collection!.children.map((x) => x.id))
|
||||
if (!data.right.collection) return E.right([])
|
||||
|
||||
if (data.right.collection!.children.length !== BACKEND_PAGE_SIZE) break
|
||||
const children = data.right.collection.children || []
|
||||
collsList.push(...children.map((x) => x.id))
|
||||
|
||||
if (children.length !== BACKEND_PAGE_SIZE) break
|
||||
}
|
||||
|
||||
return E.right(collsList)
|
||||
@@ -170,10 +173,14 @@ export const getCompleteCollectionTree = (
|
||||
collectionID: collID,
|
||||
},
|
||||
}),
|
||||
TE.map((result) => ({
|
||||
title: result.collection!.title,
|
||||
data: result.collection!.data,
|
||||
}))
|
||||
TE.map((result) =>
|
||||
result.collection
|
||||
? {
|
||||
title: result.collection!.title,
|
||||
data: result.collection!.data,
|
||||
}
|
||||
: null
|
||||
)
|
||||
)
|
||||
),
|
||||
TE.bind("children", () =>
|
||||
@@ -192,8 +199,8 @@ export const getCompleteCollectionTree = (
|
||||
id: collID,
|
||||
children,
|
||||
requests,
|
||||
title: titleAndData.title,
|
||||
data: titleAndData.data,
|
||||
title: titleAndData?.title,
|
||||
data: titleAndData?.data,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1217,22 +1217,89 @@ 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
|
||||
function computeCollectionInheritedProps(
|
||||
collection: HoppCollection,
|
||||
ref_id: string,
|
||||
type: "my-collections" | "team-collections" = "my-collections",
|
||||
parentAuth: HoppRESTAuth | null = null,
|
||||
parentHeaders: HoppRESTHeaders | null = null
|
||||
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
|
||||
// Determine the inherited authentication and headers
|
||||
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,
|
||||
]
|
||||
|
||||
// Check if the current collection matches the target reference ID
|
||||
const isTargetCollection =
|
||||
type === "my-collections"
|
||||
? collection._ref_id === ref_id
|
||||
: collection.id === ref_id
|
||||
|
||||
if (isTargetCollection) {
|
||||
return {
|
||||
auth: inheritedAuth,
|
||||
headers: inheritedHeaders,
|
||||
}
|
||||
for (const folder of collection.folders) {
|
||||
const found = findCollection(folder, ref_id)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Recursively search in folders
|
||||
for (const folder of collection.folders) {
|
||||
const result = computeCollectionInheritedProps(
|
||||
folder,
|
||||
ref_id,
|
||||
type,
|
||||
inheritedAuth,
|
||||
inheritedHeaders
|
||||
)
|
||||
if (result) return result // Return as soon as a match is found
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getRESTCollectionInheritedProps(
|
||||
collectionID: string,
|
||||
collections: HoppCollection[] = restCollectionStore.value.state,
|
||||
type: "my-collections" | "team-collections" = "my-collections"
|
||||
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
|
||||
for (const collection of collections) {
|
||||
const result = computeCollectionInheritedProps(
|
||||
collection,
|
||||
collectionID,
|
||||
type
|
||||
)
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function getRESTCollectionByRefId(ref_id: string) {
|
||||
for (const collection of restCollectionStore.value.state) {
|
||||
const found = findCollection(collection, ref_id)
|
||||
if (found) {
|
||||
|
||||
@@ -510,25 +510,6 @@ export const REST_TAB_STATE_SCHEMA = z
|
||||
z.object({
|
||||
tabID: z.string(),
|
||||
doc: z.union([
|
||||
z.object({
|
||||
// !Versioned entity
|
||||
request: entityReference(HoppRESTRequest),
|
||||
type: z.literal("request").catch("request"),
|
||||
isDirty: z.boolean(),
|
||||
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||
response: z.optional(z.nullable(HoppRESTResponseSchema)),
|
||||
testResults: z.optional(z.nullable(HoppTestResultSchema)),
|
||||
responseTabPreference: z.optional(z.string()),
|
||||
optionTabPreference: z.optional(z.enum(validRestOperations)),
|
||||
inheritedProperties: z.optional(HoppInheritedPropertySchema),
|
||||
cancelFunction: z.optional(z.function()),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("example-response").catch("example-response"),
|
||||
response: HoppRESTRequestResponse,
|
||||
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||
isDirty: z.boolean(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("test-runner").catch("test-runner"),
|
||||
config: z.object({
|
||||
@@ -556,6 +537,25 @@ export const REST_TAB_STATE_SCHEMA = z
|
||||
testResults: z.optional(z.nullable(HoppTestResultSchema)),
|
||||
isDirty: z.boolean(),
|
||||
}),
|
||||
z.object({
|
||||
// !Versioned entity
|
||||
request: entityReference(HoppRESTRequest),
|
||||
type: z.literal("request").catch("request"),
|
||||
isDirty: z.boolean(),
|
||||
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||
response: z.optional(z.nullable(HoppRESTResponseSchema)),
|
||||
testResults: z.optional(z.nullable(HoppTestResultSchema)),
|
||||
responseTabPreference: z.optional(z.string()),
|
||||
optionTabPreference: z.optional(z.enum(validRestOperations)),
|
||||
inheritedProperties: z.optional(HoppInheritedPropertySchema),
|
||||
cancelFunction: z.optional(z.function()),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("example-response").catch("example-response"),
|
||||
response: HoppRESTRequestResponse,
|
||||
saveContext: z.optional(HoppRESTSaveContextSchema),
|
||||
isDirty: z.boolean(),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
),
|
||||
|
||||
@@ -39,6 +39,17 @@ export class RESTTabService extends TabService<HoppTabDocument> {
|
||||
}
|
||||
}
|
||||
|
||||
if (tab.document.type === "test-runner") {
|
||||
return {
|
||||
tabID: tab.id,
|
||||
doc: {
|
||||
...tab.document,
|
||||
request: null,
|
||||
response: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tabID: tab.id,
|
||||
doc: {
|
||||
|
||||
Reference in New Issue
Block a user