refactor: bring js-sandbox project to the monorepo
This commit is contained in:
367
packages/hoppscotch-js-sandbox/src/test-runner.ts
Normal file
367
packages/hoppscotch-js-sandbox/src/test-runner.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { isLeft } from "fp-ts/lib/Either"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import { TaskEither, tryCatch, chain, right, left } from "fp-ts/lib/TaskEither"
|
||||
import * as qjs from "quickjs-emscripten"
|
||||
import { marshalObjectToVM } from "./utils"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
export type TestResponse = {
|
||||
/** Status Code of the response */
|
||||
status: number,
|
||||
/** List of headers returned */
|
||||
headers: { key: string, value: string }[],
|
||||
/**
|
||||
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
|
||||
*/
|
||||
body: string | object
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an expectation statement
|
||||
*/
|
||||
type ExpectResult =
|
||||
| { status: "pass" | "fail" | "error", message: string } // The expectation failed (fail) or errored (error)
|
||||
|
||||
/**
|
||||
* An object defining the result of the execution of a
|
||||
* test block
|
||||
*/
|
||||
export type TestDescriptor = {
|
||||
/**
|
||||
* The name of the test block
|
||||
*/
|
||||
descriptor: string
|
||||
|
||||
/**
|
||||
* Expectation results of the test block
|
||||
*/
|
||||
expectResults: ExpectResult[]
|
||||
|
||||
/**
|
||||
* Children test blocks (test blocks inside the test block)
|
||||
*/
|
||||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Expectation object for use inside the sandbox
|
||||
* @param vm The QuickJS sandbox VM instance
|
||||
* @param expectVal The expecting value of the expectation
|
||||
* @param negated Whether the expectation is negated (negative)
|
||||
* @param currTestStack The current state of the test execution stack
|
||||
* @returns Handle to the expectation object in VM
|
||||
*/
|
||||
function createExpectation(
|
||||
vm: qjs.QuickJSVm,
|
||||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
): qjs.QuickJSHandle {
|
||||
const resultHandle = vm.newObject()
|
||||
|
||||
const toBeFnHandle = vm.newFunction("toBe", (expectedValHandle) => {
|
||||
const expectedVal = vm.dump(expectedValHandle)
|
||||
|
||||
let assertion = expectVal === expectedVal
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be '${expectedVal}'`
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be '${expectedVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel2xxHandle = vm.newFunction("toBeLevel2xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 200 && expectVal <= 299
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be 200-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message:
|
||||
`Expected '${expectVal}' to${negated ? " not" : ""} be 200-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 200-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel3xxHandle = vm.newFunction("toBeLevel3xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 300 && expectVal <= 399
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be 300-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message:
|
||||
`Expected '${expectVal}' to${negated ? " not" : ""} be 300-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 300-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel4xxHandle = vm.newFunction("toBeLevel4xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 400 && expectVal <= 499
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be 400-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message:
|
||||
`Expected '${expectVal}' to${negated ? " not" : ""} be 400-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 400-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel5xxHandle = vm.newFunction("toBeLevel5xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 500 && expectVal <= 599
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be 500-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be 500-level status`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 500-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeTypeHandle = vm.newFunction("toBeType", (expectedValHandle) => {
|
||||
const expectedType = vm.dump(expectedValHandle)
|
||||
|
||||
// Check if the expectation param is a valid type name string, else error
|
||||
if (["string", "boolean", "number", "object", "undefined", "bigint", "symbol", "function"].includes(expectedType)) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be type '${expectedType}'`
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${negated ? " not" : ""} be type '${expectedType}'`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toHaveLengthHandle = vm.newFunction(
|
||||
"toHaveLength",
|
||||
(expectedValHandle) => {
|
||||
const expectedLength = vm.dump(expectedValHandle)
|
||||
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected toHaveLength to be called for an array or string`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
// Check if the parameter is a number, else error
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = (expectVal as any[]).length === expectedLength
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected the array to${negated ? " not" : ""} be of length '${expectedLength}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected the array to${negated ? " not" : ""} be of length '${expectedLength}'`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toHaveLength should be a number`
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
)
|
||||
|
||||
vm.setProp(resultHandle, "toBe", toBeFnHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel2xx", toBeLevel2xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel3xx", toBeLevel3xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel4xx", toBeLevel4xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel5xx", toBeLevel5xxHandle)
|
||||
vm.setProp(resultHandle, "toBeType", toBeTypeHandle)
|
||||
vm.setProp(resultHandle, "toHaveLength", toHaveLengthHandle)
|
||||
|
||||
vm.defineProp(resultHandle, "not", {
|
||||
get: () => {
|
||||
return createExpectation(vm, expectVal, !negated, currTestStack)
|
||||
},
|
||||
})
|
||||
|
||||
toBeFnHandle.dispose()
|
||||
toBeLevel2xxHandle.dispose()
|
||||
toBeLevel3xxHandle.dispose()
|
||||
toBeLevel4xxHandle.dispose()
|
||||
toBeLevel5xxHandle.dispose()
|
||||
toBeTypeHandle.dispose()
|
||||
toHaveLengthHandle.dispose()
|
||||
|
||||
return resultHandle
|
||||
}
|
||||
|
||||
export const execTestScript = (
|
||||
testScript: string,
|
||||
response: TestResponse
|
||||
): TaskEither<string, TestDescriptor[]> => pipe(
|
||||
tryCatch(
|
||||
async () => await qjs.getQuickJS(),
|
||||
(reason) => `QuickJS initialization failed: ${reason}`
|
||||
),
|
||||
chain(
|
||||
// TODO: Make this more functional ?
|
||||
(QuickJS) => {
|
||||
const vm = QuickJS.createVm()
|
||||
|
||||
const pwHandle = vm.newObject()
|
||||
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
const testFuncHandle = vm.newFunction(
|
||||
"test",
|
||||
(descriptorHandle, testFuncHandle) => {
|
||||
const descriptor = vm.getString(descriptorHandle)
|
||||
|
||||
testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
|
||||
const result = vm.unwrapResult(vm.callFunction(testFuncHandle, vm.null))
|
||||
result.dispose()
|
||||
|
||||
const child = testRunStack.pop() as TestDescriptor
|
||||
testRunStack[testRunStack.length - 1].children.push(child)
|
||||
}
|
||||
)
|
||||
|
||||
const expectFnHandle = vm.newFunction("expect", (expectValueHandle) => {
|
||||
const expectVal = vm.dump(expectValueHandle)
|
||||
|
||||
return {
|
||||
value: createExpectation(vm, expectVal, false, testRunStack),
|
||||
}
|
||||
})
|
||||
|
||||
// Marshal response object
|
||||
const responseObjHandle = marshalObjectToVM(vm, response)
|
||||
if (isLeft(responseObjHandle)) return left(`Response marshalling failed: ${responseObjHandle.left}`)
|
||||
|
||||
vm.setProp(pwHandle, "response", responseObjHandle.right)
|
||||
responseObjHandle.right.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "expect", expectFnHandle)
|
||||
expectFnHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "test", testFuncHandle)
|
||||
testFuncHandle.dispose()
|
||||
|
||||
vm.setProp(vm.global, "pw", pwHandle)
|
||||
pwHandle.dispose()
|
||||
|
||||
const evalRes = vm.evalCode(testScript)
|
||||
|
||||
if (evalRes.error) {
|
||||
const errorData = vm.dump(evalRes.error)
|
||||
evalRes.error.dispose()
|
||||
|
||||
return left(`Script evaluation failed: ${errorData}`)
|
||||
}
|
||||
|
||||
vm.dispose()
|
||||
|
||||
return right(testRunStack)
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user