feat: introduce APIs to update envs from tests and recursive resolution

This commit is contained in:
Andrew Bastin
2022-02-19 02:20:28 +05:30
parent 3cb47d3812
commit 59c6e21636
20 changed files with 1392 additions and 168 deletions

View File

@@ -1,8 +1,10 @@
import { Observable } from "rxjs" import { Observable } from "rxjs"
import { filter } from "rxjs/operators" import { filter } from "rxjs/operators"
import { chain, right, TaskEither } from "fp-ts/lib/TaskEither" 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 * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { Environment } from "@hoppscotch/data"
import { runTestScript, TestDescriptor } from "@hoppscotch/js-sandbox" import { runTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"
import { isRight } from "fp-ts/Either" import { isRight } from "fp-ts/Either"
import { import {
@@ -15,6 +17,15 @@ import { createRESTNetworkRequestStream } from "./network"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult" import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { isJSONContentType } from "./utils/contenttypes" import { isJSONContentType } from "./utils/contenttypes"
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession" import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
import {
environmentsStore,
getCurrentEnvironment,
getEnviroment,
getGlobalVariables,
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { TestResult } from "~/../hoppscotch-js-sandbox/lib/test-runner"
const getTestableBody = ( const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" } res: HoppRESTResponse & { type: "success" | "fail" }
@@ -43,6 +54,11 @@ const getTestableBody = (
return x return x
} }
const combineEnvVariables = (env: {
global: Environment["variables"]
selected: Environment["variables"]
}) => [...env.selected, ...env.global]
export const runRESTRequest$ = (): TaskEither< export const runRESTRequest$ = (): TaskEither<
string | Error, string | Error,
Observable<HoppRESTResponse> Observable<HoppRESTResponse>
@@ -55,7 +71,7 @@ export const runRESTRequest$ = (): TaskEither<
chain((envs) => { chain((envs) => {
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), { const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
name: "Env", name: "Env",
variables: envs, variables: combineEnvVariables(envs),
}) })
const stream = createRESTNetworkRequestStream(effectiveRequest) const stream = createRESTNetworkRequestStream(effectiveRequest)
@@ -65,7 +81,7 @@ export const runRESTRequest$ = (): TaskEither<
.pipe(filter((res) => res.type === "success" || res.type === "fail")) .pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => { .subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") { 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, status: res.statusCode,
body: getTestableBody(res), body: getTestableBody(res),
headers: res.headers, headers: res.headers,
@@ -73,11 +89,37 @@ export const runRESTRequest$ = (): TaskEither<
if (isRight(runResult)) { if (isRight(runResult)) {
setRESTTestResults(translateToSandboxTestResults(runResult.right)) 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 { } else {
setRESTTestResults({ setRESTTestResults({
description: "", description: "",
expectResults: [], expectResults: [],
tests: [], tests: [],
envDiff: {
global: {
additions: [],
deletions: [],
updations: [],
},
selected: {
additions: [],
deletions: [],
updations: [],
},
},
scriptError: true, scriptError: true,
}) })
} }
@@ -90,8 +132,37 @@ 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.fromPredicate(
(x) => current.findIndex((y) => y.key === x.key) !== -1
),
O.map((x) => ({
...x,
previousValue: current.find((y) => x.key === y.key)!.value,
}))
)
)
)
function translateToSandboxTestResults( function translateToSandboxTestResults(
testDesc: TestDescriptor testDesc: TestResult & { tests: TestDescriptor }
): HoppTestResult { ): HoppTestResult {
const translateChildTests = (child: TestDescriptor): HoppTestData => { const translateChildTests = (child: TestDescriptor): HoppTestData => {
return { return {
@@ -100,10 +171,32 @@ function translateToSandboxTestResults(
tests: child.children.map(translateChildTests), tests: child.children.map(translateChildTests),
} }
} }
const globals = getGlobalVariables()
const env = getCurrentEnvironment()
return { return {
description: "", description: "",
expectResults: testDesc.expectResults, expectResults: testDesc.tests.expectResults,
tests: testDesc.children.map(translateChildTests), tests: testDesc.tests.children.map(translateChildTests),
scriptError: false, 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,19 @@
import { runPreRequestScript } from "@hoppscotch/js-sandbox" import { runPreRequestScript } from "@hoppscotch/js-sandbox"
import { Environment } from "@hoppscotch/data"
import { import {
getCurrentEnvironment, getCurrentEnvironment,
getGlobalVariables, getGlobalVariables,
} from "~/newstore/environments" } from "~/newstore/environments"
export const getCombinedEnvVariables = () => { export const getCombinedEnvVariables = () => ({
const variables: { key: string; value: string }[] = [...getGlobalVariables()] global: getGlobalVariables(),
selected: getCurrentEnvironment().variables,
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 getFinalEnvsFromPreRequest = ( export const getFinalEnvsFromPreRequest = (
script: string, script: string,
envs: { key: string; value: string }[] envs: {
global: Environment["variables"]
selected: Environment["variables"]
}
) => runPreRequestScript(script, envs) ) => runPreRequestScript(script, envs)

View File

@@ -1,3 +1,5 @@
import { Environment } from "@hoppscotch/data"
export type HoppTestExpectResult = { export type HoppTestExpectResult = {
status: "fail" | "pass" | "error" status: "fail" | "pass" | "error"
message: string message: string
@@ -14,4 +16,21 @@ export type HoppTestResult = {
expectResults: HoppTestExpectResult[] expectResults: HoppTestExpectResult[]
description: string description: string
scriptError: boolean 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

@@ -57,7 +57,7 @@
"@codemirror/view": "^0.19.0", "@codemirror/view": "^0.19.0",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0", "@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0",
"@hoppscotch/data": "workspace:^0.4.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/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.31.0", "@nuxtjs/composition-api": "^0.31.0",
"@nuxtjs/gtm": "^2.4.0", "@nuxtjs/gtm": "^2.4.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/js-sandbox", "name": "@hoppscotch/js-sandbox",
"version": "1.0.0", "version": "2.0.0",
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients", "description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/", "types": "./lib/",
@@ -35,7 +35,8 @@
"dependencies": { "dependencies": {
"fp-ts": "^2.11.8", "fp-ts": "^2.11.8",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"quickjs-emscripten": "^0.15.0" "quickjs-emscripten": "^0.15.0",
"@hoppscotch/data": "workspace:^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@digitak/esrun": "^3.1.2", "@digitak/esrun": "^3.1.2",

View File

@@ -8,15 +8,21 @@ describe("execPreRequestScript", () => {
` `
pw.env.set("bob", "newbob") pw.env.set("bob", "newbob")
`, `,
[ {
{ key: "bob", value: "oldbob" }, global: [],
{ key: "foo", value: "bar" }, selected: [
] { key: "bob", value: "oldbob" },
{ key: "foo", value: "bar" },
],
}
)() )()
).resolves.toEqualRight([ ).resolves.toEqualRight({
{ key: "bob", value: "newbob" }, global: [],
{ key: "foo", value: "bar" }, selected: [
]) { key: "bob", value: "newbob" },
{ key: "foo", value: "bar" },
],
})
}) })
test("fails if the key is not a string", () => { test("fails if the key is not a string", () => {
@@ -25,10 +31,13 @@ describe("execPreRequestScript", () => {
` `
pw.env.set(10, "newbob") pw.env.set(10, "newbob")
`, `,
[ {
{ key: "bob", value: "oldbob" }, global: [],
{ key: "foo", value: "bar" }, selected: [
] { key: "bob", value: "oldbob" },
{ key: "foo", value: "bar" },
],
}
)() )()
).resolves.toBeLeft() ).resolves.toBeLeft()
}) })
@@ -39,10 +48,13 @@ describe("execPreRequestScript", () => {
` `
pw.env.set("bob", 10) pw.env.set("bob", 10)
`, `,
[ {
{ key: "bob", value: "oldbob" }, global: [],
{ key: "foo", value: "bar" }, selected: [
] { key: "bob", value: "oldbob" },
{ key: "foo", value: "bar" },
],
}
)() )()
).resolves.toBeLeft() ).resolves.toBeLeft()
}) })
@@ -51,12 +63,15 @@ describe("execPreRequestScript", () => {
return expect( return expect(
execPreRequestScript( execPreRequestScript(
` `
pw.env.set("bob", pw.env.set("bob",
`, `,
[ {
{ key: "bob", value: "oldbob" }, global: [],
{ key: "foo", value: "bar" }, selected: [
] { key: "bob", value: "oldbob" },
{ key: "foo", value: "bar" },
],
}
)() )()
).resolves.toBeLeft() ).resolves.toBeLeft()
}) })
@@ -67,8 +82,11 @@ describe("execPreRequestScript", () => {
` `
pw.env.set("foo", "bar") pw.env.set("foo", "bar")
`, `,
[] { selected: [], global: [] }
)() )()
).resolves.toEqualRight([{ key: "foo", value: "bar" }]) ).resolves.toEqualRight({
global: [],
selected: [{ key: "foo", value: "bar" }],
})
}) })
}) })

View File

@@ -0,0 +1,178 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import "@relmify/jest-fp-ts"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)
describe("pw.env.get", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
`
const data = pw.env.get("a")
pw.expect(data).toBe("b")
`,
{
global: [],
selected: [
{
key: "a",
value: "b",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'b' to be 'b'",
},
],
}),
])
})
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
`
const data = pw.env.get("a")
pw.expect(data).toBe("b")
`,
{
global: [
{
key: "a",
value: "b",
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'b' to be 'b'",
},
],
}),
])
})
test("returns undefined for a key that is not present in both selected or environment", () => {
return expect(
func(
`
const data = pw.env.get("a")
pw.expect(data).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("returns the value defined in selected environment if it is also present in global", () => {
return expect(
func(
`
const data = pw.env.get("a")
pw.expect(data).toBe("selected val")
`,
{
global: [
{
key: "a",
value: "global val",
},
],
selected: [
{
key: "a",
value: "selected val",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selected val' to be 'selected val'",
},
],
}),
])
})
test("does not resolve environment values", () => {
return expect(
func(
`
const data = pw.env.get("a")
pw.expect(data).toBe("<<hello>>")
`,
{
global: [],
selected: [
{
key: "a",
value: "<<hello>>",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '<<hello>>' to be '<<hello>>'",
},
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
const data = pw.env.get(5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
})

View File

@@ -0,0 +1,219 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import "@relmify/jest-fp-ts"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests),
TE.mapLeft((x) => {
console.log(x)
return x
})
)
describe("pw.env.getResolve", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("b")
`,
{
global: [],
selected: [
{
key: "a",
value: "b",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'b' to be 'b'",
},
],
}),
])
})
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("b")
`,
{
global: [
{
key: "a",
value: "b",
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'b' to be 'b'",
},
],
}),
])
})
test("returns undefined for a key that is not present in both selected or environment", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("returns the value defined in selected environment if it is also present in global", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("selected val")
`,
{
global: [
{
key: "a",
value: "global val",
},
],
selected: [
{
key: "a",
value: "selected val",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selected val' to be 'selected val'",
},
],
}),
])
})
test("resolve environment values", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("there")
`,
{
global: [],
selected: [
{
key: "a",
value: "<<hello>>",
},
{
key: "hello",
value: "there",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'there' to be 'there'",
},
],
}),
])
})
test("returns unresolved value on infinite loop in resolution", () => {
return expect(
func(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("<<hello>>")
`,
{
global: [],
selected: [
{
key: "a",
value: "<<hello>>",
},
{
key: "hello",
value: "<<a>>",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '<<hello>>' to be '<<hello>>'",
},
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
const data = pw.env.getResolve(5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
})

View File

@@ -0,0 +1,156 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)
describe("pw.env.resolve", () => {
test("value should be a string", () => {
return expect(
func(
`
pw.env.resolve(5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
test("resolves global variables correctly", () => {
return expect(
func(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
`,
{
global: [
{
key: "hello",
value: "there",
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'there' to be 'there'",
},
],
}),
])
})
test("resolves selected env variables correctly", () => {
return expect(
func(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
`,
{
global: [],
selected: [
{
key: "hello",
value: "there",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'there' to be 'there'",
},
],
}),
])
})
test("chooses selected env variable over global variables when both have same variable", () => {
return expect(
func(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
`,
{
global: [
{
key: "hello",
value: "yo",
},
],
selected: [
{
key: "hello",
value: "there",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'there' to be 'there'",
},
],
}),
])
})
test("if infinite loop in resolution, abandons resolutions altogether", () => {
return expect(
func(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("<<hello>>")
`,
{
global: [],
selected: [
{
key: "hello",
value: "<<there>>",
},
{
key: "there",
value: "<<hello>>",
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '<<hello>>' to be '<<hello>>'",
},
],
}),
])
})
})

View File

@@ -0,0 +1,208 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)
describe("pw.env.set", () => {
test("updates the selected environment variable correctly", () => {
return expect(
func(
`
pw.env.set("a", "c")
`,
{
global: [],
selected: [
{
key: "a",
value: "b",
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "a",
value: "c",
},
],
})
)
})
test("updates the global environment variable correctly", () => {
return expect(
func(
`
pw.env.set("a", "c")
`,
{
global: [
{
key: "a",
value: "b",
},
],
selected: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "a",
value: "c",
},
],
})
)
})
test("updates the selected environment if env present in both", () => {
return expect(
func(
`
pw.env.set("a", "c")
`,
{
global: [
{
key: "a",
value: "b",
},
],
selected: [
{
key: "a",
value: "d",
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "a",
value: "b",
},
],
selected: [
{
key: "a",
value: "c",
},
],
})
)
})
test("non existent keys are created in the selected environment", () => {
return expect(
func(
`
pw.env.set("a", "c")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
global: [],
selected: [
{
key: "a",
value: "c",
},
],
})
)
})
test("keys should be a string", () => {
return expect(
func(
`
pw.env.set(5, "c")
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
test("values should be a string", () => {
return expect(
func(
`
pw.env.set("a", 5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
test("both keys and values should be strings", () => {
return expect(
func(
`
pw.env.set(5, 5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
test("set environment values are reflected in the script execution", () => {
return expect(
funcTest(
`
pw.env.set("a", "b")
pw.expect(pw.env.get("a")).toBe("b")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'b' to be 'b'",
},
],
}),
])
})
})

View File

@@ -1,3 +1,5 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner" import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts" import "@relmify/jest-fp-ts"
@@ -7,11 +9,17 @@ const fakeResponse: TestResponse = {
headers: [], headers: [],
} }
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)
describe("toBe", () => { describe("toBe", () => {
describe("general assertion (no negation)", () => { describe("general assertion (no negation)", () => {
test("expect equals expected passes assertion", () => { test("expect equals expected passes assertion", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBe(2) pw.expect(2).toBe(2)
`, `,
@@ -28,7 +36,7 @@ describe("toBe", () => {
test("expect not equals expected fails assertion", () => { test("expect not equals expected fails assertion", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBe(4) pw.expect(2).toBe(4)
`, `,
@@ -47,7 +55,7 @@ describe("toBe", () => {
describe("general assertion (with negation)", () => { describe("general assertion (with negation)", () => {
test("expect equals expected fails assertion", () => { test("expect equals expected fails assertion", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).not.toBe(2) pw.expect(2).not.toBe(2)
`, `,
@@ -67,7 +75,7 @@ describe("toBe", () => {
test("expect not equals expected passes assertion", () => { test("expect not equals expected passes assertion", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).not.toBe(4) pw.expect(2).not.toBe(4)
`, `,
@@ -89,7 +97,7 @@ describe("toBe", () => {
test("strict checks types", () => { test("strict checks types", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBe("2") pw.expect(2).toBe("2")
`, `,

View File

@@ -1,3 +1,5 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner" import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts" import "@relmify/jest-fp-ts"
@@ -7,11 +9,17 @@ const fakeResponse: TestResponse = {
headers: [], headers: [],
} }
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)
describe("toBeLevel2xx", () => { describe("toBeLevel2xx", () => {
test("assertion passes for 200 series with no negation", async () => { test("assertion passes for 200 series with no negation", async () => {
for (let i = 200; i < 300; i++) { for (let i = 200; i < 300; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -28,7 +36,7 @@ describe("toBeLevel2xx", () => {
test("assertion fails for non 200 series with no negation", async () => { test("assertion fails for non 200 series with no negation", async () => {
for (let i = 300; i < 500; i++) { for (let i = 300; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -44,7 +52,7 @@ describe("toBeLevel2xx", () => {
test("give error if the expect value was not a number with no negation", async () => { test("give error if the expect value was not a number with no negation", async () => {
await expect( await expect(
execTestScript(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)() func(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -61,7 +69,7 @@ describe("toBeLevel2xx", () => {
test("assertion fails for 200 series with negation", async () => { test("assertion fails for 200 series with negation", async () => {
for (let i = 200; i < 300; i++) { for (let i = 200; i < 300; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -78,7 +86,7 @@ describe("toBeLevel2xx", () => {
test("assertion passes for non 200 series with negation", async () => { test("assertion passes for non 200 series with negation", async () => {
for (let i = 300; i < 500; i++) { for (let i = 300; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -94,7 +102,7 @@ describe("toBeLevel2xx", () => {
test("give error if the expect value was not a number with negation", async () => { test("give error if the expect value was not a number with negation", async () => {
await expect( await expect(
execTestScript(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)() func(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -113,7 +121,7 @@ describe("toBeLevel3xx", () => {
test("assertion passes for 300 series with no negation", async () => { test("assertion passes for 300 series with no negation", async () => {
for (let i = 300; i < 400; i++) { for (let i = 300; i < 400; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -130,7 +138,7 @@ describe("toBeLevel3xx", () => {
test("assertion fails for non 300 series with no negation", async () => { test("assertion fails for non 300 series with no negation", async () => {
for (let i = 400; i < 500; i++) { for (let i = 400; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -146,7 +154,7 @@ describe("toBeLevel3xx", () => {
test("give error if the expect value is not a number without negation", () => { test("give error if the expect value is not a number without negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)() func(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -163,7 +171,7 @@ describe("toBeLevel3xx", () => {
test("assertion fails for 400 series with negation", async () => { test("assertion fails for 400 series with negation", async () => {
for (let i = 300; i < 400; i++) { for (let i = 300; i < 400; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -180,7 +188,7 @@ describe("toBeLevel3xx", () => {
test("assertion passes for non 200 series with negation", async () => { test("assertion passes for non 200 series with negation", async () => {
for (let i = 400; i < 500; i++) { for (let i = 400; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -196,7 +204,7 @@ describe("toBeLevel3xx", () => {
test("give error if the expect value is not a number with negation", () => { test("give error if the expect value is not a number with negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)() func(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -215,7 +223,7 @@ describe("toBeLevel4xx", () => {
test("assertion passes for 400 series with no negation", async () => { test("assertion passes for 400 series with no negation", async () => {
for (let i = 400; i < 500; i++) { for (let i = 400; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -232,7 +240,7 @@ describe("toBeLevel4xx", () => {
test("assertion fails for non 400 series with no negation", async () => { test("assertion fails for non 400 series with no negation", async () => {
for (let i = 500; i < 600; i++) { for (let i = 500; i < 600; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -248,7 +256,7 @@ describe("toBeLevel4xx", () => {
test("give error if the expected value is not a number without negation", () => { test("give error if the expected value is not a number without negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)() func(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -265,7 +273,7 @@ describe("toBeLevel4xx", () => {
test("assertion fails for 400 series with negation", async () => { test("assertion fails for 400 series with negation", async () => {
for (let i = 400; i < 500; i++) { for (let i = 400; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -282,7 +290,7 @@ describe("toBeLevel4xx", () => {
test("assertion passes for non 400 series with negation", async () => { test("assertion passes for non 400 series with negation", async () => {
for (let i = 500; i < 600; i++) { for (let i = 500; i < 600; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -298,7 +306,7 @@ describe("toBeLevel4xx", () => {
test("give error if the expected value is not a number with negation", () => { test("give error if the expected value is not a number with negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)() func(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -317,7 +325,7 @@ describe("toBeLevel5xx", () => {
test("assertion passes for 500 series with no negation", async () => { test("assertion passes for 500 series with no negation", async () => {
for (let i = 500; i < 600; i++) { for (let i = 500; i < 600; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -334,7 +342,7 @@ describe("toBeLevel5xx", () => {
test("assertion fails for non 500 series with no negation", async () => { test("assertion fails for non 500 series with no negation", async () => {
for (let i = 200; i < 500; i++) { for (let i = 200; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)() func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -350,7 +358,7 @@ describe("toBeLevel5xx", () => {
test("give error if the expect value is not a number with no negation", () => { test("give error if the expect value is not a number with no negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)() func(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -367,7 +375,7 @@ describe("toBeLevel5xx", () => {
test("assertion fails for 500 series with negation", async () => { test("assertion fails for 500 series with negation", async () => {
for (let i = 500; i < 600; i++) { for (let i = 500; i < 600; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -384,7 +392,7 @@ describe("toBeLevel5xx", () => {
test("assertion passes for non 500 series with negation", async () => { test("assertion passes for non 500 series with negation", async () => {
for (let i = 200; i < 500; i++) { for (let i = 200; i < 500; i++) {
await expect( await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)() func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [
@@ -400,7 +408,7 @@ describe("toBeLevel5xx", () => {
test("give error if the expect value is not a number with negation", () => { test("give error if the expect value is not a number with negation", () => {
return expect( return expect(
execTestScript(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)() func(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([ ).resolves.toEqualRight([
expect.objectContaining({ expect.objectContaining({
expectResults: [ expectResults: [

View File

@@ -1,3 +1,5 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner" import { execTestScript, TestResponse } from "../../../test-runner"
const fakeResponse: TestResponse = { const fakeResponse: TestResponse = {
@@ -6,10 +8,16 @@ const fakeResponse: TestResponse = {
headers: [], headers: [],
} }
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)
describe("toBeType", () => { describe("toBeType", () => {
test("asserts true for valid type expectations with no negation", () => { test("asserts true for valid type expectations with no negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBeType("number") pw.expect(2).toBeType("number")
pw.expect("2").toBeType("string") pw.expect("2").toBeType("string")
@@ -40,7 +48,7 @@ describe("toBeType", () => {
test("asserts false for invalid type expectations with no negation", () => { test("asserts false for invalid type expectations with no negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBeType("string") pw.expect(2).toBeType("string")
pw.expect("2").toBeType("number") pw.expect("2").toBeType("number")
@@ -71,7 +79,7 @@ describe("toBeType", () => {
test("asserts false for valid type expectations with negation", () => { test("asserts false for valid type expectations with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).not.toBeType("number") pw.expect(2).not.toBeType("number")
pw.expect("2").not.toBeType("string") pw.expect("2").not.toBeType("string")
@@ -105,7 +113,7 @@ describe("toBeType", () => {
test("asserts true for invalid type expectations with negation", () => { test("asserts true for invalid type expectations with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).not.toBeType("string") pw.expect(2).not.toBeType("string")
pw.expect("2").not.toBeType("number") pw.expect("2").not.toBeType("number")
@@ -139,7 +147,7 @@ describe("toBeType", () => {
test("gives error for invalid type names without negation", () => { test("gives error for invalid type names without negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).toBeType("foo") pw.expect(2).toBeType("foo")
pw.expect("2").toBeType("bar") pw.expect("2").toBeType("bar")
@@ -179,7 +187,7 @@ describe("toBeType", () => {
test("gives error for invalid type names with negation", () => { test("gives error for invalid type names with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(2).not.toBeType("foo") pw.expect(2).not.toBeType("foo")
pw.expect("2").not.toBeType("bar") pw.expect("2").not.toBeType("bar")

View File

@@ -1,3 +1,5 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner" import { execTestScript, TestResponse } from "../../../test-runner"
const fakeResponse: TestResponse = { const fakeResponse: TestResponse = {
@@ -6,10 +8,16 @@ const fakeResponse: TestResponse = {
headers: [], headers: [],
} }
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)
describe("toHaveLength", () => { describe("toHaveLength", () => {
test("asserts true for valid lengths with no negation", () => { test("asserts true for valid lengths with no negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([1, 2, 3, 4]).toHaveLength(4) pw.expect([1, 2, 3, 4]).toHaveLength(4)
pw.expect([]).toHaveLength(0) pw.expect([]).toHaveLength(0)
@@ -28,7 +36,7 @@ describe("toHaveLength", () => {
test("asserts false for invalid lengths with no negation", () => { test("asserts false for invalid lengths with no negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([]).toHaveLength(4) pw.expect([]).toHaveLength(4)
pw.expect([1, 2, 3, 4]).toHaveLength(0) pw.expect([1, 2, 3, 4]).toHaveLength(0)
@@ -47,7 +55,7 @@ describe("toHaveLength", () => {
test("asserts false for valid lengths with negation", () => { test("asserts false for valid lengths with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([1, 2, 3, 4]).not.toHaveLength(4) pw.expect([1, 2, 3, 4]).not.toHaveLength(4)
pw.expect([]).not.toHaveLength(0) pw.expect([]).not.toHaveLength(0)
@@ -72,7 +80,7 @@ describe("toHaveLength", () => {
test("asserts true for invalid lengths with negation", () => { test("asserts true for invalid lengths with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([]).not.toHaveLength(4) pw.expect([]).not.toHaveLength(4)
pw.expect([1, 2, 3, 4]).not.toHaveLength(0) pw.expect([1, 2, 3, 4]).not.toHaveLength(0)
@@ -97,7 +105,7 @@ describe("toHaveLength", () => {
test("gives error if not called on an array or a string with no negation", () => { test("gives error if not called on an array or a string with no negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(5).toHaveLength(0) pw.expect(5).toHaveLength(0)
pw.expect(true).toHaveLength(0) pw.expect(true).toHaveLength(0)
@@ -124,7 +132,7 @@ describe("toHaveLength", () => {
test("gives error if not called on an array or a string with negation", () => { test("gives error if not called on an array or a string with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect(5).not.toHaveLength(0) pw.expect(5).not.toHaveLength(0)
pw.expect(true).not.toHaveLength(0) pw.expect(true).not.toHaveLength(0)
@@ -151,7 +159,7 @@ describe("toHaveLength", () => {
test("gives an error if toHaveLength parameter is not a number without negation", () => { test("gives an error if toHaveLength parameter is not a number without negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([1, 2, 3, 4]).toHaveLength("a") pw.expect([1, 2, 3, 4]).toHaveLength("a")
`, `,
@@ -171,7 +179,7 @@ describe("toHaveLength", () => {
test("gives an error if toHaveLength parameter is not a number with negation", () => { test("gives an error if toHaveLength parameter is not a number with negation", () => {
return expect( return expect(
execTestScript( func(
` `
pw.expect([1, 2, 3, 4]).not.toHaveLength("a") pw.expect([1, 2, 3, 4]).not.toHaveLength("a")
`, `,

View File

@@ -1,3 +1,5 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../test-runner" import { execTestScript, TestResponse } from "../../test-runner"
const fakeResponse: TestResponse = { const fakeResponse: TestResponse = {
@@ -6,10 +8,16 @@ const fakeResponse: TestResponse = {
headers: [], headers: [],
} }
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)
describe("execTestScript function behavior", () => { describe("execTestScript function behavior", () => {
test("returns a resolved promise for a valid test scripts with all green", () => { test("returns a resolved promise for a valid test scripts with all green", () => {
return expect( return expect(
execTestScript( func(
` `
pw.test("Arithmetic operations", () => { pw.test("Arithmetic operations", () => {
const size = 500 + 500; const size = 500 + 500;
@@ -26,7 +34,7 @@ describe("execTestScript function behavior", () => {
test("resolves for tests with failed expectations", () => { test("resolves for tests with failed expectations", () => {
return expect( return expect(
execTestScript( func(
` `
pw.test("Arithmetic operations", () => { pw.test("Arithmetic operations", () => {
const size = 500 + 500; const size = 500 + 500;
@@ -44,7 +52,7 @@ describe("execTestScript function behavior", () => {
// TODO: We need a more concrete behavior for this // TODO: We need a more concrete behavior for this
test("rejects for invalid syntax on tests", () => { test("rejects for invalid syntax on tests", () => {
return expect( return expect(
execTestScript( func(
` `
pw.test("Arithmetic operations", () => { pw.test("Arithmetic operations", () => {
const size = 500 + 500; const size = 500 + 500;

View File

@@ -1,10 +1,11 @@
import { pipe } from "fp-ts/lib/function" import { pipe } from "fp-ts/function"
import { chain, right } from "fp-ts/lib/TaskEither" import * as TE from "fp-ts/TaskEither"
import { execPreRequestScript } from "./preRequest" import { execPreRequestScript } from "./preRequest"
import { import {
execTestScript, execTestScript,
TestResponse, TestResponse,
TestDescriptor as _TestDescriptor, TestDescriptor as _TestDescriptor,
TestResult,
} from "./test-runner" } from "./test-runner"
export type TestDescriptor = _TestDescriptor export type TestDescriptor = _TestDescriptor
@@ -13,10 +14,19 @@ export type TestDescriptor = _TestDescriptor
* @param testScript The string of the script to run * @param testScript The string of the script to run
* @returns A TaskEither with an error message or a TestDescriptor with the final status * @returns A TaskEither with an error message or a TestDescriptor with the final status
*/ */
export const runTestScript = (testScript: string, response: TestResponse) => export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
) =>
pipe( pipe(
execTestScript(testScript, response), execTestScript(testScript, envs, response),
chain((results) => right(results[0])) // execTestScript returns an array of descriptors with a single element (extract that) TE.chain((results) =>
TE.right(<TestResult & { tests: TestDescriptor }>{
envs: results.envs,
tests: results.tests[0],
})
) // execTestScript returns an array of descriptors with a single element (extract that)
) )
/** /**

View File

@@ -1,63 +1,148 @@
import { pipe } from "fp-ts/lib/function" import { pipe } from "fp-ts/function"
import { chain, TaskEither, tryCatch, right, left } from "fp-ts/lib/TaskEither" import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/lib/TaskEither"
import * as qjs from "quickjs-emscripten" import * as qjs from "quickjs-emscripten"
import clone from "lodash/clone" import cloneDeep from "lodash/clone"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { getEnv, setEnv } from "./utils"
type EnvEntry = { type Envs = {
key: string global: Environment["variables"]
value: string selected: Environment["variables"]
} }
export const execPreRequestScript = ( export const execPreRequestScript = (
preRequestScript: string, preRequestScript: string,
env: EnvEntry[] envs: Envs
): TaskEither<string, EnvEntry[]> => ): TE.TaskEither<string, Envs> =>
pipe( pipe(
tryCatch( TE.tryCatch(
async () => await qjs.getQuickJS(), async () => await qjs.getQuickJS(),
(reason) => `QuickJS initialization failed: ${reason}` (reason) => `QuickJS initialization failed: ${reason}`
), ),
chain((QuickJS) => { TE.chain((QuickJS) => {
const finalEnv = clone(env) let currentEnvs = cloneDeep(envs)
const vm = QuickJS.createVm() const vm = QuickJS.createVm()
const pwHandle = vm.newObject() const pwHandle = vm.newObject()
// Environment management APIs
// TODO: Unified Implementation
const envHandle = vm.newObject() const envHandle = vm.newObject()
const envSetFuncHandle = vm.newFunction( const envGetHandle = vm.newFunction("get", (keyHandle) => {
"set", const key: unknown = vm.dump(keyHandle)
(keyHandle, valueHandle) => {
const key = vm.dump(keyHandle)
const value = vm.dump(valueHandle)
if (typeof key !== "string")
return {
error: vm.newString("Expected key to be a string"),
}
if (typeof value !== "string")
return {
error: vm.newString("Expected value to be a string"),
}
const keyIndex = finalEnv.findIndex((env) => env.key === key)
if (keyIndex === -1) {
finalEnv.push({ key, value })
} else {
finalEnv[keyIndex] = { key, value }
}
if (typeof key !== "string") {
return { return {
value: vm.undefined, error: vm.newString("Expected key to be a string"),
} }
} }
)
vm.setProp(envHandle, "set", envSetFuncHandle) const result = pipe(
envSetFuncHandle.dispose() getEnv(key, currentEnvs),
O.match(
() => vm.undefined,
({ value }) => vm.newString(value)
)
)
return {
value: result,
}
})
const envGetResolveHandle = vm.newFunction("getResolve", (keyHandle) => {
const key: unknown = vm.dump(keyHandle)
if (typeof key !== "string") {
return {
error: vm.newString("Expected key to be a string"),
}
}
const result = pipe(
getEnv(key, currentEnvs),
E.fromOption(() => "INVALID_KEY" as const),
E.map(({ value }) =>
pipe(
parseTemplateStringE(value, [...envs.selected, ...envs.global]),
// If the recursive resolution failed, return the unresolved value
E.getOrElse(() => value)
)
),
// Create a new VM String
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
E.map((x) => vm.newString(x)),
E.getOrElse(() => vm.undefined)
)
return {
value: result,
}
})
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
const key: unknown = vm.dump(keyHandle)
const value: unknown = vm.dump(valueHandle)
if (typeof key !== "string") {
return {
error: vm.newString("Expected key to be a string"),
}
}
if (typeof value !== "string") {
return {
error: vm.newString("Expected value to be a string"),
}
}
currentEnvs = setEnv(key, value, currentEnvs)
return {
value: vm.undefined,
}
})
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
const value: unknown = vm.dump(valueHandle)
if (typeof value !== "string") {
return {
error: vm.newString("Expected value to be a string"),
}
}
const result = pipe(
parseTemplateStringE(value, [
...currentEnvs.selected,
...currentEnvs.global,
]),
E.getOrElse(() => value)
)
return {
value: vm.newString(result),
}
})
vm.setProp(envHandle, "resolve", envResolveHandle)
envResolveHandle.dispose()
vm.setProp(envHandle, "set", envSetHandle)
envSetHandle.dispose()
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
envGetResolveHandle.dispose()
vm.setProp(envHandle, "get", envGetHandle)
envGetHandle.dispose()
vm.setProp(pwHandle, "env", envHandle) vm.setProp(pwHandle, "env", envHandle)
envHandle.dispose() envHandle.dispose()
@@ -71,11 +156,11 @@ export const execPreRequestScript = (
const errorData = vm.dump(evalRes.error) const errorData = vm.dump(evalRes.error)
evalRes.error.dispose() evalRes.error.dispose()
return left(errorData) return TE.left(errorData)
} }
vm.dispose() vm.dispose()
return right(finalEnv) return TE.right(currentEnvs)
}) })
) )

View File

@@ -1,8 +1,11 @@
import { isLeft } from "fp-ts/lib/Either" import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/lib/function" import * as E from "fp-ts/Either"
import { TaskEither, tryCatch, chain, right, left } from "fp-ts/lib/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import * as qjs from "quickjs-emscripten" import * as qjs from "quickjs-emscripten"
import { marshalObjectToVM } from "./utils" import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import cloneDeep from "lodash/cloneDeep"
import { getEnv, marshalObjectToVM, setEnv } from "./utils"
/** /**
* The response object structure exposed to the test script * The response object structure exposed to the test script
@@ -44,6 +47,17 @@ export type TestDescriptor = {
children: TestDescriptor[] children: TestDescriptor[]
} }
/**
* Defines the result of a test script execution
*/
export type TestResult = {
tests: TestDescriptor[]
envs: {
global: Environment["variables"]
selected: Environment["variables"]
}
}
/** /**
* Creates an Expectation object for use inside the sandbox * Creates an Expectation object for use inside the sandbox
* @param vm The QuickJS sandbox VM instance * @param vm The QuickJS sandbox VM instance
@@ -325,16 +339,19 @@ function createExpectation(
export const execTestScript = ( export const execTestScript = (
testScript: string, testScript: string,
envs: TestResult["envs"],
response: TestResponse response: TestResponse
): TaskEither<string, TestDescriptor[]> => ): TE.TaskEither<string, TestResult> =>
pipe( pipe(
tryCatch( TE.tryCatch(
async () => await qjs.getQuickJS(), async () => await qjs.getQuickJS(),
(reason) => `QuickJS initialization failed: ${reason}` (reason) => `QuickJS initialization failed: ${reason}`
), ),
chain( TE.chain(
// TODO: Make this more functional ? // TODO: Make this more functional ?
(QuickJS) => { (QuickJS) => {
let currentEnvs = cloneDeep(envs)
const vm = QuickJS.createVm() const vm = QuickJS.createVm()
const pwHandle = vm.newObject() const pwHandle = vm.newObject()
@@ -374,8 +391,10 @@ export const execTestScript = (
// Marshal response object // Marshal response object
const responseObjHandle = marshalObjectToVM(vm, response) const responseObjHandle = marshalObjectToVM(vm, response)
if (isLeft(responseObjHandle)) if (E.isLeft(responseObjHandle))
return left(`Response marshalling failed: ${responseObjHandle.left}`) return TE.left(
`Response marshalling failed: ${responseObjHandle.left}`
)
vm.setProp(pwHandle, "response", responseObjHandle.right) vm.setProp(pwHandle, "response", responseObjHandle.right)
responseObjHandle.right.dispose() responseObjHandle.right.dispose()
@@ -386,6 +405,134 @@ export const execTestScript = (
vm.setProp(pwHandle, "test", testFuncHandle) vm.setProp(pwHandle, "test", testFuncHandle)
testFuncHandle.dispose() testFuncHandle.dispose()
// Environment management APIs
// TODO: Unified Implementation
const envHandle = vm.newObject()
const envGetHandle = vm.newFunction("get", (keyHandle) => {
const key: unknown = vm.dump(keyHandle)
if (typeof key !== "string") {
return {
error: vm.newString("Expected key to be a string"),
}
}
const result = pipe(
getEnv(key, currentEnvs),
O.match(
() => vm.undefined,
({ value }) => vm.newString(value)
)
)
return {
value: result,
}
})
const envGetResolveHandle = vm.newFunction(
"getResolve",
(keyHandle) => {
const key: unknown = vm.dump(keyHandle)
if (typeof key !== "string") {
return {
error: vm.newString("Expected key to be a string"),
}
}
const result = pipe(
getEnv(key, currentEnvs),
E.fromOption(() => "INVALID_KEY" as const),
E.map(({ value }) =>
pipe(
parseTemplateStringE(value, [
...envs.selected,
...envs.global,
]),
// If the recursive resolution failed, return the unresolved value
E.getOrElse(() => value)
)
),
// Create a new VM String
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
E.map((x) => vm.newString(x)),
E.getOrElse(() => vm.undefined)
)
console.log("result")
console.log(result)
return {
value: result,
}
}
)
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
const key: unknown = vm.dump(keyHandle)
const value: unknown = vm.dump(valueHandle)
if (typeof key !== "string") {
return {
error: vm.newString("Expected key to be a string"),
}
}
if (typeof value !== "string") {
return {
error: vm.newString("Expected value to be a string"),
}
}
currentEnvs = setEnv(key, value, currentEnvs)
return {
value: vm.undefined,
}
})
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
const value: unknown = vm.dump(valueHandle)
if (typeof value !== "string") {
return {
error: vm.newString("Expected value to be a string"),
}
}
const result = pipe(
parseTemplateStringE(value, [
...currentEnvs.selected,
...currentEnvs.global,
]),
E.getOrElse(() => value)
)
return {
value: vm.newString(result),
}
})
vm.setProp(envHandle, "resolve", envResolveHandle)
envResolveHandle.dispose()
vm.setProp(envHandle, "set", envSetHandle)
envSetHandle.dispose()
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
envGetResolveHandle.dispose()
vm.setProp(envHandle, "get", envGetHandle)
envGetHandle.dispose()
vm.setProp(pwHandle, "env", envHandle)
envHandle.dispose()
vm.setProp(vm.global, "pw", pwHandle) vm.setProp(vm.global, "pw", pwHandle)
pwHandle.dispose() pwHandle.dispose()
@@ -395,12 +542,15 @@ export const execTestScript = (
const errorData = vm.dump(evalRes.error) const errorData = vm.dump(evalRes.error)
evalRes.error.dispose() evalRes.error.dispose()
return left(`Script evaluation failed: ${errorData}`) return TE.left(`Script evaluation failed: ${errorData}`)
} }
vm.dispose() vm.dispose()
return right(testRunStack) return TE.right({
tests: testRunStack,
envs: currentEnvs,
})
} }
) )
) )

View File

@@ -1,16 +1,18 @@
import { Either, left, right } from "fp-ts/lib/Either" import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
import * as QuickJS from "quickjs-emscripten" import * as QuickJS from "quickjs-emscripten"
import { TestResult } from "./test-runner"
export function marshalObjectToVM( export function marshalObjectToVM(
vm: QuickJS.QuickJSVm, vm: QuickJS.QuickJSVm,
obj: object obj: object
): Either<string, QuickJS.QuickJSHandle> { ): E.Either<string, QuickJS.QuickJSHandle> {
let jsonString let jsonString
try { try {
jsonString = JSON.stringify(obj) jsonString = JSON.stringify(obj)
} catch (e) { } catch (e) {
return left("Marshaling stringification failed") return E.left("Marshaling stringification failed")
} }
const vmStringHandle = vm.newString(jsonString) const vmStringHandle = vm.newString(jsonString)
@@ -26,7 +28,7 @@ export function marshalObjectToVM(
if (parseResultHandle.error) { if (parseResultHandle.error) {
parseResultHandle.error.dispose() parseResultHandle.error.dispose()
return left("Marshaling failed") return E.left("Marshaling failed")
} }
const resultHandle = vm.unwrapResult(parseResultHandle) const resultHandle = vm.unwrapResult(parseResultHandle)
@@ -35,5 +37,53 @@ export function marshalObjectToVM(
parseFuncHandle.dispose() parseFuncHandle.dispose()
jsonHandle.dispose() jsonHandle.dispose()
return right(resultHandle) return E.right(resultHandle)
}
export function getEnv(envName: string, envs: TestResult["envs"]) {
return O.fromNullable(
envs.selected.find((x) => x.key === envName) ??
envs.global.find((x) => x.key === envName)
)
}
export function setEnv(
envName: string,
envValue: string,
envs: TestResult["envs"]
): TestResult["envs"] {
const indexInSelected = envs.selected.findIndex((x) => x.key === envName)
// Found the match in selected
if (indexInSelected >= 0) {
envs.selected[indexInSelected].value = envValue
return {
global: envs.global,
selected: envs.selected,
}
}
const indexInGlobal = envs.global.findIndex((x) => x.key == envName)
// Found a match in globals
if (indexInGlobal >= 0) {
envs.global[indexInGlobal].value = envValue
return {
global: envs.global,
selected: envs.selected,
}
}
// Didn't find in both places, create a new variable in selected
envs.selected.push({
key: envName,
value: envValue,
})
return {
global: envs.global,
selected: envs.selected,
}
} }

27
pnpm-lock.yaml generated
View File

@@ -76,7 +76,7 @@ importers:
'@graphql-typed-document-node/core': ^3.1.1 '@graphql-typed-document-node/core': ^3.1.1
'@hoppscotch/codemirror-lang-graphql': workspace:^0.1.0 '@hoppscotch/codemirror-lang-graphql': workspace:^0.1.0
'@hoppscotch/data': workspace:^0.4.0 '@hoppscotch/data': workspace:^0.4.0
'@hoppscotch/js-sandbox': workspace:^1.0.0 '@hoppscotch/js-sandbox': workspace:^2.0.0
'@nuxt/types': ^2.15.8 '@nuxt/types': ^2.15.8
'@nuxt/typescript-build': ^2.1.0 '@nuxt/typescript-build': ^2.1.0
'@nuxtjs/axios': ^5.13.6 '@nuxtjs/axios': ^5.13.6
@@ -345,6 +345,7 @@ importers:
packages/hoppscotch-js-sandbox: packages/hoppscotch-js-sandbox:
specifiers: specifiers:
'@digitak/esrun': ^3.1.2 '@digitak/esrun': ^3.1.2
'@hoppscotch/data': workspace:^0.4.0
'@relmify/jest-fp-ts': ^1.1.1 '@relmify/jest-fp-ts': ^1.1.1
'@types/jest': ^27.4.0 '@types/jest': ^27.4.0
'@types/lodash': ^4.14.178 '@types/lodash': ^4.14.178
@@ -363,6 +364,7 @@ importers:
ts-jest: ^27.1.3 ts-jest: ^27.1.3
typescript: ^4.5.5 typescript: ^4.5.5
dependencies: dependencies:
'@hoppscotch/data': link:../hoppscotch-data
fp-ts: 2.11.8 fp-ts: 2.11.8
lodash: 4.17.21 lodash: 4.17.21
quickjs-emscripten: 0.15.0 quickjs-emscripten: 0.15.0
@@ -3866,17 +3868,17 @@ packages:
ufo: 0.7.9 ufo: 0.7.9
dev: false dev: false
/@nuxt/kit-edge/3.0.0-27418474.8adff2e: /@nuxt/kit-edge/3.0.0-27420153.70542a3:
resolution: {integrity: sha512-wKU7jxYg22P7mETM2y7xIRz+dUohsp1RaFVekoNWLfckuzA/uHEg8NFlL+zoT0JbxivaRkyEgVC9Z4GlF4lgoA==} resolution: {integrity: sha512-XPhoj5GW/FG1euPyK+T3T63dJs76brk81nVm0tTLBafx9ohtoMdDtZnTVvhN5i7m4thAzmhaPwZereM9dsIyEw==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0}
dependencies: dependencies:
'@nuxt/schema': /@nuxt/schema-edge/3.0.0-27418474.8adff2e '@nuxt/schema': /@nuxt/schema-edge/3.0.0-27420153.70542a3
c12: 0.1.3 c12: 0.1.3
consola: 2.15.3 consola: 2.15.3
defu: 5.0.1 defu: 5.0.1
globby: 13.1.1 globby: 13.1.1
hash-sum: 2.0.0 hash-sum: 2.0.0
jiti: 1.12.15 jiti: 1.13.0
knitwork: 0.1.0 knitwork: 0.1.0
lodash.template: 4.5.0 lodash.template: 4.5.0
mlly: 0.4.3 mlly: 0.4.3
@@ -3908,14 +3910,14 @@ packages:
node-fetch: 2.6.6 node-fetch: 2.6.6
dev: false dev: false
/@nuxt/schema-edge/3.0.0-27418474.8adff2e: /@nuxt/schema-edge/3.0.0-27420153.70542a3:
resolution: {integrity: sha512-yooh4a/NRssSxBfSJpLuC4MTIwxFtb8ZKIN2WtDOD5JOd2XwSqvk4Z9mIiP/VzKrGVRJcxFNY6GiXVBozlYsXg==} resolution: {integrity: sha512-m5b6Yp/X1z7lrmOtfBOgw4hKGI63WSLuiE7Z+4PwEiGeGQ7AKHmQVmyjXjJ7NJif+RJCTQ8Ps+SdjaE2A3M+Zw==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0}
dependencies: dependencies:
c12: 0.1.3 c12: 0.1.3
create-require: 1.1.1 create-require: 1.1.1
defu: 5.0.1 defu: 5.0.1
jiti: 1.12.15 jiti: 1.13.0
pathe: 0.2.0 pathe: 0.2.0
scule: 0.2.1 scule: 0.2.1
std-env: 3.0.1 std-env: 3.0.1
@@ -6916,7 +6918,7 @@ packages:
defu: 5.0.1 defu: 5.0.1
dotenv: 14.3.2 dotenv: 14.3.2
gittar: 0.1.1 gittar: 0.1.1
jiti: 1.12.15 jiti: 1.13.0
mlly: 0.4.3 mlly: 0.4.3
pathe: 0.2.0 pathe: 0.2.0
rc9: 1.2.0 rc9: 1.2.0
@@ -12318,6 +12320,11 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/jiti/1.13.0:
resolution: {integrity: sha512-/n9mNxZj/HDSrincJ6RP+L+yXbpnB8FybySBa+IjIaoH9FIxBbrbRT5XUbe8R7zuVM2AQqNMNDDqz0bzx3znOQ==}
hasBin: true
dev: true
/joycon/3.1.1: /joycon/3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -13865,7 +13872,7 @@ packages:
/nuxt-windicss/2.2.5: /nuxt-windicss/2.2.5:
resolution: {integrity: sha512-0T21d/SLCxla5gwtwSgisncnbDc2uuUXOu/qYNKEvh8URw8t9y7qZ1Z+gwyCDqORxBrqSzgXXZ7aLOntfPBHNQ==} resolution: {integrity: sha512-0T21d/SLCxla5gwtwSgisncnbDc2uuUXOu/qYNKEvh8URw8t9y7qZ1Z+gwyCDqORxBrqSzgXXZ7aLOntfPBHNQ==}
dependencies: dependencies:
'@nuxt/kit': /@nuxt/kit-edge/3.0.0-27418474.8adff2e '@nuxt/kit': /@nuxt/kit-edge/3.0.0-27420153.70542a3
'@windicss/plugin-utils': 1.7.1 '@windicss/plugin-utils': 1.7.1
consola: 2.15.3 consola: 2.15.3
defu: 5.0.1 defu: 5.0.1