chore: collection runner enhancements and ui clean up (#4564)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam
2024-11-28 18:25:00 +06:00
committed by GitHub
parent d2401d6ceb
commit e2e769db71
10 changed files with 215 additions and 51 deletions

View File

@@ -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",

View File

@@ -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']

View File

@@ -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}` }}

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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>

View File

@@ -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,
}
)
)

View File

@@ -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) {

View File

@@ -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(),
}),
]),
})
),

View File

@@ -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: {