Merge branch 'feat/test-env-updates'

This commit is contained in:
liyasthomas
2022-02-22 12:59:07 +05:30
28 changed files with 1963 additions and 297 deletions

View File

@@ -38,12 +38,18 @@
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { createEnvironment } from "~/newstore/environments"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { createEnvironment, environments$ } from "~/newstore/environments"
export default defineComponent({
props: {
show: Boolean,
},
setup() {
return {
envList: useReadonlyStream(environments$, []),
}
},
data() {
return {
name: null as string | null,
@@ -56,6 +62,11 @@ export default defineComponent({
return
}
createEnvironment(this.name)
// TODO: find better way to get index of new environment
this.$emit("environment-added", {
name: this.name,
index: this.envList.length - 1,
})
this.hideModal()
},
hideModal() {

View File

@@ -3,7 +3,9 @@
<div
v-if="
testResults &&
(testResults.expectResults.length || testResults.tests.length)
(testResults.expectResults.length ||
testResults.tests.length ||
haveEnvVariables)
"
>
<div
@@ -20,6 +22,78 @@
/>
</div>
<div class="border-b divide-y-4 divide-dividerLight border-dividerLight">
<div v-if="haveEnvVariables" class="flex flex-col">
<details class="flex flex-col divide-y divide-dividerLight" open>
<summary
class="flex items-center justify-between flex-1 min-w-0 cursor-pointer transition focus:outline-none text-secondaryLight text-tiny group"
>
<span
class="px-4 py-2 truncate transition group-hover:text-secondary capitalize-first"
>
{{ t("environment.title") }}
</span>
</summary>
<div class="divide-y divide-dividerLight">
<div
v-if="noEnvSelected && !globalHasAdditions"
class="flex bg-error p-4 text-secondaryDark"
>
<i class="mr-4 material-icons"> warning </i>
<div class="flex flex-col">
<p>
{{ t("environment.no_environment_description") }}
</p>
<p class="space-x-2 flex mt-3">
<ButtonSecondary
:label="t('environment.add_to_global')"
class="text-tiny !bg-primary"
filled
@click.native="addEnvToGlobal()"
/>
<ButtonSecondary
:label="t('environment.create_new')"
class="text-tiny !bg-primary"
filled
@click.native="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.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">
<HttpTestResultEntry
v-for="(result, index) in testResults.tests"
@@ -107,16 +181,97 @@
class="my-4"
/>
</div>
<EnvironmentsAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
@environment-added="createNewEnv($event)"
/>
</div>
</template>
<script setup lang="ts">
import { useReadonlyStream, useI18n } from "~/helpers/utils/composables"
import { computed, Ref, ref } from "@nuxtjs/composition-api"
import isEqual from "lodash/isEqual"
import {
useReadonlyStream,
useI18n,
useStream,
} from "~/helpers/utils/composables"
import {
globalEnv$,
selectedEnvIndex$,
setCurrentEnvironment,
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { restTestResults$, setRESTTestResults } from "~/newstore/RESTSession"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
const t = useI18n()
const testResults = useReadonlyStream(restTestResults$, null)
const showModalAdd = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
}
const testResults = useReadonlyStream(
restTestResults$,
null
) as Ref<HoppTestResult | null>
const clearContent = () => setRESTTestResults(null)
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(
selectedEnvIndex$,
-1,
setCurrentEnvironment
)
const globalEnvVars = useReadonlyStream(globalEnv$, []) as Ref<
Array<{
key: string
value: string
}>
>
const noEnvSelected = computed(() => selectedEnvironmentIndex.value === -1)
const globalHasAdditions = computed(() => {
if (!testResults.value?.envDiff.selected.additions) return false
return (
testResults.value.envDiff.selected.additions.every(
(x) => globalEnvVars.value.findIndex((y) => isEqual(x, y)) !== -1
) ?? false
)
})
const addEnvToGlobal = () => {
if (!testResults.value?.envDiff.selected.additions) return
setGlobalEnvVariables([
...globalEnvVars.value,
...testResults.value.envDiff.selected.additions,
])
}
const createNewEnv = ({ name, index }: { name: string; index: number }) => {
if (!testResults.value?.envDiff.selected.additions) return
updateEnvironment(index, {
name,
variables: testResults.value.envDiff.selected.additions,
})
setCurrentEnvironment(index)
}
</script>

View File

@@ -0,0 +1,83 @@
<template>
<div class="flex items-center justify-between px-4 py-2">
<div class="flex items-center">
<i
v-tippy="{ theme: 'tooltip' }"
class="mr-4 material-icons cursor-help"
:class="getStyle(status)"
:title="`${t(getTooltip(status))}`"
>
{{ getIcon(status) }}
</i>
<span class="text-secondaryDark">
{{ env.key }}
</span>
<span class="text-secondaryDark">
{{ ` \xA0 — \xA0 ${env.value}` }}
</span>
<span v-if="status === 'updations'" class="text-secondaryLight">
{{ ` \xA0 \xA0 ${env.previousValue}` }}
</span>
</div>
<span
v-if="global"
class="bg-accentLight px-1 rounded text-accentContrast text-tiny"
>
Global
</span>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "~/helpers/utils/composables"
type Status = "updations" | "additions" | "deletions"
type Props = {
env: {
key: string
value: string
previousValue?: string
}
status: Status
global: boolean
}
withDefaults(defineProps<Props>(), {
global: false,
})
const t = useI18n()
const getIcon = (status: Status) => {
switch (status) {
case "additions":
return "add_circle"
case "updations":
return "check_circle"
case "deletions":
return "remove_circle"
}
}
const getStyle = (status: Status) => {
switch (status) {
case "additions":
return "text-green-500"
case "updations":
return "text-yellow-500"
case "deletions":
return "text-red-500"
}
}
const getTooltip = (status: Status) => {
switch (status) {
case "additions":
return "environment.added"
case "updations":
return "environment.updated"
case "deletions":
return "environment.deleted"
}
}
</script>

View File

@@ -24,7 +24,11 @@
:label="$t('test.results')"
:indicator="
testResults &&
(testResults.expectResults.length || testResults.tests.length)
(testResults.expectResults.length ||
testResults.tests.length ||
testResults.envDiff.selected.additions.length ||
testResults.envDiff.selected.updations.length ||
testResults.envDiff.global.updations.length)
? true
: false
"

View File

@@ -1,10 +1,17 @@
import { Observable } from "rxjs"
import { filter } from "rxjs/operators"
import { chain, right, TaskEither } from "fp-ts/lib/TaskEither"
import { pipe } from "fp-ts/function"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { runTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"
import * as A from "fp-ts/Array"
import { Environment } from "@hoppscotch/data"
import {
SandboxTestResult,
runTestScript,
TestDescriptor,
} from "@hoppscotch/js-sandbox"
import { isRight } from "fp-ts/Either"
import cloneDeep from "lodash/cloneDeep"
import {
getCombinedEnvVariables,
getFinalEnvsFromPreRequest,
@@ -15,6 +22,14 @@ import { createRESTNetworkRequestStream } from "./network"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { isJSONContentType } from "./utils/contenttypes"
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
import {
environmentsStore,
getCurrentEnvironment,
getEnviroment,
getGlobalVariables,
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
@@ -43,6 +58,11 @@ const getTestableBody = (
return x
}
const combineEnvVariables = (env: {
global: Environment["variables"]
selected: Environment["variables"]
}) => [...env.selected, ...env.global]
export const runRESTRequest$ = (): TaskEither<
string | Error,
Observable<HoppRESTResponse>
@@ -55,7 +75,7 @@ export const runRESTRequest$ = (): TaskEither<
chain((envs) => {
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
name: "Env",
variables: envs,
variables: combineEnvVariables(envs),
})
const stream = createRESTNetworkRequestStream(effectiveRequest)
@@ -65,7 +85,7 @@ export const runRESTRequest$ = (): TaskEither<
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") {
const runResult = await runTestScript(res.req.testScript, {
const runResult = await runTestScript(res.req.testScript, envs, {
status: res.statusCode,
body: getTestableBody(res),
headers: res.headers,
@@ -73,11 +93,38 @@ export const runRESTRequest$ = (): TaskEither<
if (isRight(runResult)) {
setRESTTestResults(translateToSandboxTestResults(runResult.right))
setGlobalEnvVariables(runResult.right.envs.global)
if (environmentsStore.value.currentEnvironmentIndex !== -1) {
const env = getEnviroment(
environmentsStore.value.currentEnvironmentIndex
)
updateEnvironment(
environmentsStore.value.currentEnvironmentIndex,
{
name: env.name,
variables: runResult.right.envs.selected,
}
)
}
} else {
setRESTTestResults({
description: "",
expectResults: [],
tests: [],
envDiff: {
global: {
additions: [],
deletions: [],
updations: [],
},
selected: {
additions: [],
deletions: [],
updations: [],
},
},
scriptError: true,
})
}
@@ -90,8 +137,47 @@ export const runRESTRequest$ = (): TaskEither<
})
)
const getAddedEnvVariables = (
current: Environment["variables"],
updated: Environment["variables"]
) => updated.filter((x) => current.findIndex((y) => y.key === x.key) === -1)
const getRemovedEnvVariables = (
current: Environment["variables"],
updated: Environment["variables"]
) => current.filter((x) => updated.findIndex((y) => y.key === x.key) === -1)
const getUpdatedEnvVariables = (
current: Environment["variables"],
updated: Environment["variables"]
) =>
pipe(
updated,
A.filterMap(
flow(
O.of,
O.bindTo("env"),
O.bind("index", ({ env }) =>
pipe(
current.findIndex((x) => x.key === env.key),
O.fromPredicate((x) => x !== -1)
)
),
O.chain(
O.fromPredicate(
({ env, index }) => env.value !== current[index].value
)
),
O.map(({ env, index }) => ({
...env,
previousValue: current[index].value,
}))
)
)
)
function translateToSandboxTestResults(
testDesc: TestDescriptor
testDesc: SandboxTestResult
): HoppTestResult {
const translateChildTests = (child: TestDescriptor): HoppTestData => {
return {
@@ -100,10 +186,32 @@ function translateToSandboxTestResults(
tests: child.children.map(translateChildTests),
}
}
const globals = cloneDeep(getGlobalVariables())
const env = cloneDeep(getCurrentEnvironment())
return {
description: "",
expectResults: testDesc.expectResults,
tests: testDesc.children.map(translateChildTests),
expectResults: testDesc.tests.expectResults,
tests: testDesc.tests.children.map(translateChildTests),
scriptError: false,
envDiff: {
global: {
additions: getAddedEnvVariables(globals, testDesc.envs.global),
deletions: getRemovedEnvVariables(globals, testDesc.envs.global),
updations: getUpdatedEnvVariables(globals, testDesc.envs.global),
},
selected: {
additions: getAddedEnvVariables(env.variables, testDesc.envs.selected),
deletions: getRemovedEnvVariables(
env.variables,
testDesc.envs.selected
),
updations: getUpdatedEnvVariables(
env.variables,
testDesc.envs.selected
),
},
},
}
}

View File

@@ -1,29 +1,20 @@
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
import { Environment } from "@hoppscotch/data"
import cloneDeep from "lodash/cloneDeep"
import {
getCurrentEnvironment,
getGlobalVariables,
} from "~/newstore/environments"
export const getCombinedEnvVariables = () => {
const variables: { key: string; value: string }[] = [...getGlobalVariables()]
for (const variable of getCurrentEnvironment().variables) {
const index = variables.findIndex((v) => variable.key === v.key)
if (index === -1) {
variables.push({
key: variable.key,
value: variable.value,
})
} else {
variables[index].value = variable.value
}
}
return variables
}
export const getCombinedEnvVariables = () => ({
global: cloneDeep(getGlobalVariables()),
selected: cloneDeep(getCurrentEnvironment().variables),
})
export const getFinalEnvsFromPreRequest = (
script: string,
envs: { key: string; value: string }[]
envs: {
global: Environment["variables"]
selected: Environment["variables"]
}
) => runPreRequestScript(script, envs)

View File

@@ -2,7 +2,10 @@
"!name": "pw-pre",
"pw": {
"env": {
"set": "fn(key: string, value: string)"
"set": "fn(key: string, value: string)",
"get": "fn(key: string) -> string",
"getResolve": "fn(key: string) -> string",
"resolve": "fn(value: string) -> string"
}
}
}

View File

@@ -19,6 +19,12 @@
"headers": "?",
"body": "?"
},
"env": {
"set": "fn(key: string, value: string)",
"get": "fn(key: string) -> string",
"getResolve": "fn(key: string) -> string",
"resolve": "fn(value: string) -> string"
},
"test": "fn(name: string, func: fn())"
}
}

View File

@@ -1,4 +1,9 @@
export default [
{
name: "Environment: Set an environment variable",
script: `\n\n// Set an environment variable
pw.env.set("variable", "value");`,
},
{
name: "Response: Status code is 200",
script: `\n\n// Check status code is 200

View File

@@ -1,3 +1,5 @@
import { Environment } from "@hoppscotch/data"
export type HoppTestExpectResult = {
status: "fail" | "pass" | "error"
message: string
@@ -14,4 +16,21 @@ export type HoppTestResult = {
expectResults: HoppTestExpectResult[]
description: string
scriptError: boolean
envDiff: {
global: {
additions: Environment["variables"]
updations: Array<
Environment["variables"][number] & { previousValue: string }
>
deletions: Environment["variables"]
}
selected: {
additions: Environment["variables"]
updations: Array<
Environment["variables"][number] & { previousValue: string }
>
deletions: Environment["variables"]
}
}
}

View File

@@ -159,14 +159,19 @@
"tests": "There are no tests for this request"
},
"environment": {
"added": "Environment addition",
"add_to_global": "Add to Global",
"create_new": "Create new environment",
"deleted": "Environment deletion",
"edit": "Edit Environment",
"invalid_name": "Please provide a name for the environment",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
"no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"select": "Select environment",
"title": "Environments",
"updated": "Environment updation",
"variable_list": "Variable List"
},
"error": {
@@ -468,7 +473,7 @@
},
"state": {
"bulk_mode": "Bulk edit",
"bulk_mode_placeholder": "Entries are separated by newline\nKeys and values are separated by :\nPrepend # to any row you want to add but keep disabled",
"bulk_mode_placeholder": "Entries are separated by newline Keys and values are separated by : Prepend # to any row you want to add but keep disabled",
"cleared": "Cleared",
"connected": "Connected",
"connected_to": "Connected to {name}",

View File

@@ -57,7 +57,7 @@
"@codemirror/view": "^0.19.0",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0",
"@hoppscotch/data": "workspace:^0.4.0",
"@hoppscotch/js-sandbox": "workspace:^1.0.0",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.31.0",
"@nuxtjs/gtm": "^2.4.0",