Mochia.js (6880B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * Define Mochia's helpers on the given scope. 7 */ 8 (() => { 9 /** 10 * The context of each test suite. 11 */ 12 class Context { 13 static #stack = []; 14 15 static current() { 16 return Context.#stack.at(-1); 17 } 18 19 static push(ctx) { 20 Context.#stack.push(ctx); 21 } 22 23 static pop() { 24 Context.#stack.pop(); 25 } 26 27 constructor() { 28 this.description = []; 29 this.beforeEach = []; 30 this.afterEach = []; 31 } 32 33 clone() { 34 const newCtx = new Context(); 35 newCtx.description.push(...this.description); 36 newCtx.beforeEach.push(...this.beforeEach); 37 newCtx.afterEach.push(...this.afterEach); 38 return newCtx; 39 } 40 } 41 42 Context.push(new Context()); 43 44 let _testScope = null; 45 46 /** 47 * @typedef {void|Promise<void>} MaybePromise 48 * 49 * Either undefined or a Promise that resolves to undefined. 50 */ 51 52 const MochiaImpl = { 53 /** 54 * Describe a new test suite, which is a scoped environment for running setup 55 * and teardown. 56 * 57 * @param {string} desc 58 * A description of the test suite. 59 * 60 * @param {function(): MaybePromise} suite 61 * The test suite 62 */ 63 async describe(desc, suite) { 64 const ctx = Context.current().clone(); 65 ctx.description.push(desc); 66 67 Context.push(ctx); 68 69 const p = suite(); 70 if (p?.then) { 71 await p; 72 } 73 74 Context.pop(); 75 }, 76 77 /** 78 * Register a setup funciton to run before each test. 79 * 80 * Multiple functions can be registered with `beforeEach` and they will be run 81 * in order before each test in the suite and the suites nested inside of it. 82 * 83 * @param {function(): MaybePromise} setupFn 84 * The setup function. If this function returns a `Promise` it will be 85 * awaited. 86 */ 87 beforeEach(setupFn) { 88 Context.current().beforeEach.push(setupFn); 89 }, 90 91 /** 92 * Register a tear down function to run at the end of each test. 93 * 94 * Multiple functions can be registered with `afterEach` and they will run in 95 * reverse order after each test in the suite and the suites nested inside of it. 96 * 97 * @param {function(): MaybePromise} tearDownFn 98 * The tear down function. If this function returns a `Promise` it will 99 * be awaited. 100 */ 101 afterEach(tearDownFn) { 102 Context.current().afterEach.push(tearDownFn); 103 }, 104 105 /** 106 * Register a test function. 107 * 108 * The test will be registered via `add_task`. Each setup function registered 109 * before this function call will be called in order before the actual test 110 * and each teardown function before this function will be called in reverse 111 * order after the actual test. 112 * 113 * @param {string} desc 114 * A description of the test. This is logged at the start of the test. 115 * 116 * @param {function(): MaybePromise} testFn 117 * The test function. If this function returns a `Promise` it will be 118 * awaited. 119 * 120 * @returns {any} 121 * The result of calling `add_task` with the wrapped function. 122 */ 123 it(desc, testFn) { 124 return _testScope.add_task(MochiaImpl.wrap(desc, testFn)); 125 }, 126 127 /** 128 * Register a test that will be the only test run. 129 * 130 * @param {string} desc 131 * A description of the test. This is logged at the start of the test. 132 * 133 * @param {function(): MaybePromise} testFn 134 * The test function. If this function returns a `Promise` it will be 135 * awaited. 136 */ 137 only(desc, testFn) { 138 MochiaImpl.it(desc, testFn).only(); 139 }, 140 141 /** 142 * Register a test that will be skipped. 143 * 144 * @param {string} desc 145 * A description of the test. This is logged at the start of the test. 146 * 147 * @param {function(): MaybePromise} testFn 148 * The test function. If this function returns a `Promise` it will be 149 * awaited. 150 */ 151 skip(desc, testFn) { 152 MochiaImpl.it(desc, testFn).skip(); 153 }, 154 155 /** 156 * Register a test that will be skipped if the provided predicate evaluates to 157 * a truthy value. 158 * 159 * @param {string} desc 160 * A description of the test. This is logged at the start of the test. 161 * 162 * @param {function(): boolean} skipFn 163 * A predicate that will be called by the test harness to determine 164 * whether or not the test should be skipped. 165 * 166 * @param {function(): MaybePromise} testFn 167 * The test function. If this function returns a `Promise` it will be 168 * awaited. 169 * 170 * @returns {any} 171 * The result of calling `add_task` with the wrapped function. 172 */ 173 skipIf(desc, skipFn, testFn) { 174 return _testScope.add_task( 175 { skip_if: skipFn }, 176 MochiaImpl.wrap(desc, testFn) 177 ); 178 }, 179 180 /** 181 * Wrap `fn` so that all the setup functions declared with `beforeEach` are 182 * called before it and all the teardown functions declared with `afterEach` 183 * are called after. 184 * 185 * @param {string} desc 186 * A description of the test. 187 * 188 * @param {function(): MaybePromise} fn 189 * The function to wrap. 190 * 191 * @returns {function(): Promise<void>} 192 * The wrapped function. 193 */ 194 wrap(desc, fn) { 195 const ctx = Context.current().clone(); 196 const name = [...ctx.description, desc].join(" / "); 197 198 // This is a hack to give the function an implicit name. 199 const wrapper = { 200 [name]: async () => { 201 _testScope.info(name); 202 203 for (const before of ctx.beforeEach) { 204 const p = before(); 205 if (p?.then) { 206 await p; 207 } 208 } 209 210 { 211 const p = fn(); 212 if (p?.then) { 213 await p; 214 } 215 } 216 217 for (let i = ctx.afterEach.length - 1; i >= 0; i--) { 218 const after = ctx.afterEach[i]; 219 const p = after(); 220 if (p?.then) { 221 await p; 222 } 223 } 224 }, 225 }; 226 227 return wrapper[name]; 228 }, 229 }; 230 231 Object.defineProperties(MochiaImpl.it, { 232 only: { 233 configurable: false, 234 value: MochiaImpl.only, 235 }, 236 skip: { 237 configurable: false, 238 value: MochiaImpl.skip, 239 }, 240 skipIf: { 241 configurable: false, 242 value: MochiaImpl.skipIf, 243 }, 244 }); 245 246 _testScope = this; 247 248 Object.assign(_testScope, { 249 describe: MochiaImpl.describe, 250 beforeEach: MochiaImpl.beforeEach, 251 afterEach: MochiaImpl.afterEach, 252 it: MochiaImpl.it, 253 }); 254 })();