postwomanTesting.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { HoppRESTResponse } from "./types/HoppRESTResponse"
  2. const styles = {
  3. PASS: { icon: "check", class: "success-response" },
  4. FAIL: { icon: "close", class: "cl-error-response" },
  5. ERROR: { icon: "close", class: "cl-error-response" },
  6. }
  7. type TestScriptResponse = {
  8. body: any
  9. headers: any[]
  10. status: number
  11. __newRes: HoppRESTResponse
  12. }
  13. type TestScriptVariables = {
  14. response: TestScriptResponse
  15. }
  16. type TestReportStartBlock = {
  17. startBlock: string
  18. }
  19. type TestReportEndBlock = {
  20. endBlock: true
  21. }
  22. type TestReportEntry = {
  23. result: "PASS" | "FAIL" | "ERROR"
  24. message: string
  25. styles: {
  26. icon: string
  27. }
  28. }
  29. type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
  30. export default function runTestScriptWithVariables(
  31. script: string,
  32. variables?: TestScriptVariables
  33. ) {
  34. const pw = {
  35. _errors: [],
  36. _testReports: [] as TestReport[],
  37. _report: "",
  38. expect(value: any) {
  39. try {
  40. return expect(value, this._testReports)
  41. } catch (e: any) {
  42. pw._testReports.push({
  43. result: "ERROR",
  44. message: `${e}`,
  45. styles: styles.ERROR,
  46. })
  47. }
  48. },
  49. test: (descriptor: string, func: () => void) =>
  50. test(descriptor, func, pw._testReports),
  51. // globals that the script is allowed to have access to.
  52. }
  53. Object.assign(pw, variables)
  54. // run pre-request script within this function so that it has access to the pw object.
  55. // eslint-disable-next-line no-new-func
  56. new Function("pw", script)(pw)
  57. return {
  58. report: pw._report,
  59. errors: pw._errors,
  60. testResults: pw._testReports,
  61. }
  62. }
  63. function test(
  64. descriptor: string,
  65. func: () => void,
  66. _testReports: TestReport[]
  67. ) {
  68. _testReports.push({ startBlock: descriptor })
  69. try {
  70. func()
  71. } catch (e: any) {
  72. _testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
  73. }
  74. _testReports.push({ endBlock: true })
  75. // TODO: Organize and generate text report of each {descriptor: true} section in testReports.
  76. // add checkmark or x depending on if each testReport is pass=true or pass=false
  77. }
  78. function expect(expectValue: any, _testReports: TestReport[]) {
  79. return new Expectation(expectValue, null, _testReports)
  80. }
  81. class Expectation {
  82. private expectValue: any
  83. private not: true | Expectation
  84. private _testReports: TestReport[]
  85. constructor(
  86. expectValue: any,
  87. _not: boolean | null,
  88. _testReports: TestReport[]
  89. ) {
  90. this.expectValue = expectValue
  91. this.not = _not || new Expectation(this.expectValue, true, _testReports)
  92. this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
  93. }
  94. private _satisfies(expectValue: any, targetValue?: any): boolean {
  95. // Used for testing if two values match the expectation, which could be === OR !==, depending on if not
  96. // was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
  97. // Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
  98. if (!targetValue) {
  99. targetValue = expectValue
  100. expectValue = this.expectValue
  101. }
  102. if (this.not === true) {
  103. // test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
  104. return expectValue !== targetValue
  105. } else {
  106. return expectValue === targetValue
  107. }
  108. }
  109. _fmtNot(message: string) {
  110. // given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
  111. if (this.not === true) {
  112. return message.replace("(not)", "not ")
  113. } else {
  114. return message.replace("(not)", "")
  115. }
  116. }
  117. _fail(message: string) {
  118. return this._testReports.push({
  119. result: "FAIL",
  120. message,
  121. styles: styles.FAIL,
  122. })
  123. }
  124. _pass(message: string) {
  125. return this._testReports.push({
  126. result: "PASS",
  127. message,
  128. styles: styles.PASS,
  129. })
  130. }
  131. // TEST METHODS DEFINED BELOW
  132. // these are the usual methods that would follow expect(...)
  133. toBe(value: any) {
  134. return this._satisfies(value)
  135. ? this._pass(
  136. this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
  137. )
  138. : this._fail(
  139. this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
  140. )
  141. }
  142. toHaveProperty(value: string) {
  143. return this._satisfies(
  144. Object.prototype.hasOwnProperty.call(this.expectValue, value),
  145. true
  146. )
  147. ? this._pass(
  148. this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
  149. )
  150. : this._fail(
  151. this._fmtNot(
  152. `Expected object ${this.expectValue} to (not)have property ${value}`
  153. )
  154. )
  155. }
  156. toBeLevel2xx() {
  157. const code = parseInt(this.expectValue, 10)
  158. if (Number.isNaN(code)) {
  159. return this._fail(
  160. `Expected 200-level status but could not parse value ${this.expectValue}`
  161. )
  162. }
  163. return this._satisfies(code >= 200 && code < 300, true)
  164. ? this._pass(
  165. this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
  166. )
  167. : this._fail(
  168. this._fmtNot(
  169. `Expected ${this.expectValue} to (not)be 200-level status`
  170. )
  171. )
  172. }
  173. toBeLevel3xx() {
  174. const code = parseInt(this.expectValue, 10)
  175. if (Number.isNaN(code)) {
  176. return this._fail(
  177. `Expected 300-level status but could not parse value ${this.expectValue}`
  178. )
  179. }
  180. return this._satisfies(code >= 300 && code < 400, true)
  181. ? this._pass(
  182. this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
  183. )
  184. : this._fail(
  185. this._fmtNot(
  186. `Expected ${this.expectValue} to (not)be 300-level status`
  187. )
  188. )
  189. }
  190. toBeLevel4xx() {
  191. const code = parseInt(this.expectValue, 10)
  192. if (Number.isNaN(code)) {
  193. return this._fail(
  194. `Expected 400-level status but could not parse value ${this.expectValue}`
  195. )
  196. }
  197. return this._satisfies(code >= 400 && code < 500, true)
  198. ? this._pass(
  199. this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
  200. )
  201. : this._fail(
  202. this._fmtNot(
  203. `Expected ${this.expectValue} to (not)be 400-level status`
  204. )
  205. )
  206. }
  207. toBeLevel5xx() {
  208. const code = parseInt(this.expectValue, 10)
  209. if (Number.isNaN(code)) {
  210. return this._fail(
  211. `Expected 500-level status but could not parse value ${this.expectValue}`
  212. )
  213. }
  214. return this._satisfies(code >= 500 && code < 600, true)
  215. ? this._pass(
  216. this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
  217. )
  218. : this._fail(
  219. this._fmtNot(
  220. `Expected ${this.expectValue} to (not)be 500-level status`
  221. )
  222. )
  223. }
  224. toHaveLength(expectedLength: number) {
  225. const actualLength = this.expectValue.length
  226. return this._satisfies(actualLength, expectedLength)
  227. ? this._pass(
  228. this._fmtNot(
  229. `Length expectation of (not)being ${expectedLength} is kept`
  230. )
  231. )
  232. : this._fail(
  233. this._fmtNot(
  234. `Expected length to be ${expectedLength} but actual length was ${actualLength}`
  235. )
  236. )
  237. }
  238. toBeType(expectedType: string) {
  239. const actualType = typeof this.expectValue
  240. if (
  241. ![
  242. "string",
  243. "boolean",
  244. "number",
  245. "object",
  246. "undefined",
  247. "bigint",
  248. "symbol",
  249. "function",
  250. ].includes(expectedType)
  251. ) {
  252. return this._fail(
  253. this._fmtNot(
  254. `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
  255. )
  256. )
  257. }
  258. return this._satisfies(actualType, expectedType)
  259. ? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
  260. : this._fail(
  261. this._fmtNot(
  262. `Expected type to be "${expectedType}" but actual type was "${actualType}"`
  263. )
  264. )
  265. }
  266. }
  267. export function transformResponseForTesting(
  268. response: HoppRESTResponse
  269. ): TestScriptResponse {
  270. if (response.type === "loading") {
  271. throw new Error("Cannot transform loading responses")
  272. }
  273. if (response.type === "network_fail") {
  274. throw new Error("Cannot transform failed responses")
  275. }
  276. let body: any = new TextDecoder("utf-8").decode(response.body)
  277. // Try parsing to JSON
  278. try {
  279. body = JSON.parse(body)
  280. } catch (_) {}
  281. return {
  282. body,
  283. headers: response.headers,
  284. status: response.statusCode,
  285. __newRes: response,
  286. }
  287. }