322 lines
8.5 KiB
TypeScript
322 lines
8.5 KiB
TypeScript
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
|
|
|
const styles = {
|
|
PASS: { icon: "check", class: "success-response" },
|
|
FAIL: { icon: "close", class: "cl-error-response" },
|
|
ERROR: { icon: "close", class: "cl-error-response" },
|
|
}
|
|
|
|
type TestScriptResponse = {
|
|
body: any
|
|
headers: any[]
|
|
status: number
|
|
|
|
__newRes: HoppRESTResponse
|
|
}
|
|
|
|
type TestScriptVariables = {
|
|
response: TestScriptResponse
|
|
}
|
|
|
|
type TestReportStartBlock = {
|
|
startBlock: string
|
|
}
|
|
|
|
type TestReportEndBlock = {
|
|
endBlock: true
|
|
}
|
|
|
|
type TestReportEntry = {
|
|
result: "PASS" | "FAIL" | "ERROR"
|
|
message: string
|
|
styles: {
|
|
icon: string
|
|
}
|
|
}
|
|
|
|
type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
|
|
|
|
export default function runTestScriptWithVariables(
|
|
script: string,
|
|
variables?: TestScriptVariables
|
|
) {
|
|
const pw = {
|
|
_errors: [],
|
|
_testReports: [] as TestReport[],
|
|
_report: "",
|
|
expect(value: any) {
|
|
try {
|
|
return expect(value, this._testReports)
|
|
} catch (e) {
|
|
pw._testReports.push({
|
|
result: "ERROR",
|
|
message: e.toString(),
|
|
styles: styles.ERROR,
|
|
})
|
|
}
|
|
},
|
|
test: (descriptor: string, func: () => void) =>
|
|
test(descriptor, func, pw._testReports),
|
|
// globals that the script is allowed to have access to.
|
|
}
|
|
Object.assign(pw, variables)
|
|
|
|
// run pre-request script within this function so that it has access to the pw object.
|
|
// eslint-disable-next-line no-new-func
|
|
new Function("pw", script)(pw)
|
|
|
|
return {
|
|
report: pw._report,
|
|
errors: pw._errors,
|
|
testResults: pw._testReports,
|
|
}
|
|
}
|
|
|
|
function test(
|
|
descriptor: string,
|
|
func: () => void,
|
|
_testReports: TestReport[]
|
|
) {
|
|
_testReports.push({ startBlock: descriptor })
|
|
try {
|
|
func()
|
|
} catch (e) {
|
|
_testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
|
|
}
|
|
_testReports.push({ endBlock: true })
|
|
|
|
// TODO: Organize and generate text report of each {descriptor: true} section in testReports.
|
|
// add checkmark or x depending on if each testReport is pass=true or pass=false
|
|
}
|
|
|
|
function expect(expectValue: any, _testReports: TestReport[]) {
|
|
return new Expectation(expectValue, null, _testReports)
|
|
}
|
|
|
|
class Expectation {
|
|
private expectValue: any
|
|
private not: true | Expectation
|
|
private _testReports: TestReport[]
|
|
|
|
constructor(
|
|
expectValue: any,
|
|
_not: boolean | null,
|
|
_testReports: TestReport[]
|
|
) {
|
|
this.expectValue = expectValue
|
|
this.not = _not || new Expectation(this.expectValue, true, _testReports)
|
|
this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
|
|
}
|
|
|
|
private _satisfies(expectValue: any, targetValue?: any): boolean {
|
|
// Used for testing if two values match the expectation, which could be === OR !==, depending on if not
|
|
// was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
|
|
// Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
|
|
if (!targetValue) {
|
|
targetValue = expectValue
|
|
expectValue = this.expectValue
|
|
}
|
|
if (this.not === true) {
|
|
// test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
|
|
return expectValue !== targetValue
|
|
} else {
|
|
return expectValue === targetValue
|
|
}
|
|
}
|
|
|
|
_fmtNot(message: string) {
|
|
// given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
|
|
if (this.not === true) {
|
|
return message.replace("(not)", "not ")
|
|
} else {
|
|
return message.replace("(not)", "")
|
|
}
|
|
}
|
|
|
|
_fail(message: string) {
|
|
return this._testReports.push({
|
|
result: "FAIL",
|
|
message,
|
|
styles: styles.FAIL,
|
|
})
|
|
}
|
|
|
|
_pass(message: string) {
|
|
return this._testReports.push({
|
|
result: "PASS",
|
|
message,
|
|
styles: styles.PASS,
|
|
})
|
|
}
|
|
|
|
// TEST METHODS DEFINED BELOW
|
|
// these are the usual methods that would follow expect(...)
|
|
toBe(value: any) {
|
|
return this._satisfies(value)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
|
|
)
|
|
}
|
|
|
|
toHaveProperty(value: string) {
|
|
return this._satisfies(
|
|
Object.prototype.hasOwnProperty.call(this.expectValue, value),
|
|
true
|
|
)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected object ${this.expectValue} to (not)have property ${value}`
|
|
)
|
|
)
|
|
}
|
|
|
|
toBeLevel2xx() {
|
|
const code = parseInt(this.expectValue, 10)
|
|
if (Number.isNaN(code)) {
|
|
return this._fail(
|
|
`Expected 200-level status but could not parse value ${this.expectValue}`
|
|
)
|
|
}
|
|
return this._satisfies(code >= 200 && code < 300, true)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected ${this.expectValue} to (not)be 200-level status`
|
|
)
|
|
)
|
|
}
|
|
|
|
toBeLevel3xx() {
|
|
const code = parseInt(this.expectValue, 10)
|
|
if (Number.isNaN(code)) {
|
|
return this._fail(
|
|
`Expected 300-level status but could not parse value ${this.expectValue}`
|
|
)
|
|
}
|
|
return this._satisfies(code >= 300 && code < 400, true)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected ${this.expectValue} to (not)be 300-level status`
|
|
)
|
|
)
|
|
}
|
|
|
|
toBeLevel4xx() {
|
|
const code = parseInt(this.expectValue, 10)
|
|
if (Number.isNaN(code)) {
|
|
return this._fail(
|
|
`Expected 400-level status but could not parse value ${this.expectValue}`
|
|
)
|
|
}
|
|
return this._satisfies(code >= 400 && code < 500, true)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected ${this.expectValue} to (not)be 400-level status`
|
|
)
|
|
)
|
|
}
|
|
|
|
toBeLevel5xx() {
|
|
const code = parseInt(this.expectValue, 10)
|
|
if (Number.isNaN(code)) {
|
|
return this._fail(
|
|
`Expected 500-level status but could not parse value ${this.expectValue}`
|
|
)
|
|
}
|
|
return this._satisfies(code >= 500 && code < 600, true)
|
|
? this._pass(
|
|
this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected ${this.expectValue} to (not)be 500-level status`
|
|
)
|
|
)
|
|
}
|
|
|
|
toHaveLength(expectedLength: number) {
|
|
const actualLength = this.expectValue.length
|
|
return this._satisfies(actualLength, expectedLength)
|
|
? this._pass(
|
|
this._fmtNot(
|
|
`Length expectation of (not)being ${expectedLength} is kept`
|
|
)
|
|
)
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected length to be ${expectedLength} but actual length was ${actualLength}`
|
|
)
|
|
)
|
|
}
|
|
|
|
toBeType(expectedType: string) {
|
|
const actualType = typeof this.expectValue
|
|
if (
|
|
![
|
|
"string",
|
|
"boolean",
|
|
"number",
|
|
"object",
|
|
"undefined",
|
|
"bigint",
|
|
"symbol",
|
|
"function",
|
|
].includes(expectedType)
|
|
) {
|
|
return this._fail(
|
|
this._fmtNot(
|
|
`Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
|
|
)
|
|
)
|
|
}
|
|
return this._satisfies(actualType, expectedType)
|
|
? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
|
|
: this._fail(
|
|
this._fmtNot(
|
|
`Expected type to be "${expectedType}" but actual type was "${actualType}"`
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
export function transformResponseForTesting(
|
|
response: HoppRESTResponse
|
|
): TestScriptResponse {
|
|
if (response.type === "loading") {
|
|
throw new Error("Cannot transform loading responses")
|
|
}
|
|
|
|
if (response.type === "network_fail") {
|
|
throw new Error("Cannot transform failed responses")
|
|
}
|
|
|
|
let body: any = new TextDecoder("utf-8").decode(response.body)
|
|
|
|
// Try parsing to JSON
|
|
try {
|
|
body = JSON.parse(body)
|
|
} catch (_) {}
|
|
|
|
return {
|
|
body,
|
|
headers: response.headers,
|
|
status: response.statusCode,
|
|
|
|
__newRes: response,
|
|
}
|
|
}
|