test_eventemitter_basic.js (9883B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { 7 ConsoleAPIListener, 8 } = require("resource://devtools/server/actors/webconsole/listeners/console-api.js"); 9 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 10 const hasMethod = (target, method) => 11 method in target && typeof target[method] === "function"; 12 13 /** 14 * Each method of this object is a test; tests can be synchronous or asynchronous: 15 * 16 * 1. Plain functions are synchronous tests. 17 * 2. methods with `async` keyword are asynchronous tests. 18 * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to 19 * finish the test). 20 */ 21 const TESTS = { 22 testEventEmitterCreation() { 23 const emitter = getEventEmitter(); 24 const isAnEmitter = emitter instanceof EventEmitter; 25 26 ok(emitter, "We have an event emitter"); 27 ok( 28 hasMethod(emitter, "on") && 29 hasMethod(emitter, "off") && 30 hasMethod(emitter, "once") && 31 hasMethod(emitter, "count") && 32 !hasMethod(emitter, "decorate"), 33 `Event Emitter ${ 34 isAnEmitter ? "instance" : "mixin" 35 } has the expected methods.` 36 ); 37 }, 38 39 testEmittingEvents(done) { 40 const emitter = getEventEmitter(); 41 42 let beenHere1 = false; 43 let beenHere2 = false; 44 45 function next(str1, str2) { 46 equal(str1, "abc", "Argument 1 is correct"); 47 equal(str2, "def", "Argument 2 is correct"); 48 49 ok(!beenHere1, "first time in next callback"); 50 beenHere1 = true; 51 52 emitter.off("next", next); 53 54 emitter.emit("next"); 55 56 emitter.once("onlyonce", onlyOnce); 57 58 emitter.emit("onlyonce"); 59 emitter.emit("onlyonce"); 60 } 61 62 function onlyOnce() { 63 ok(!beenHere2, '"once" listener has been called once'); 64 beenHere2 = true; 65 emitter.emit("onlyonce"); 66 67 done(); 68 } 69 70 emitter.on("next", next); 71 emitter.emit("next", "abc", "def"); 72 }, 73 74 testThrowingExceptionInListener(done) { 75 const emitter = getEventEmitter(); 76 const listener = new ConsoleAPIListener(null, message => { 77 equal(message.level, "error"); 78 const [arg] = message.arguments; 79 equal(arg.message, "foo"); 80 equal(arg.stack, "bar"); 81 listener.destroy(); 82 done(); 83 }); 84 85 listener.init(); 86 87 function throwListener() { 88 emitter.off("throw-exception"); 89 const err = new Error("foo"); 90 err.stack = "bar"; 91 throw err; 92 } 93 94 emitter.on("throw-exception", throwListener); 95 emitter.emit("throw-exception"); 96 }, 97 98 testKillItWhileEmitting(done) { 99 const emitter = getEventEmitter(); 100 101 const c1 = () => ok(true, "c1 called"); 102 const c2 = () => { 103 ok(true, "c2 called"); 104 emitter.off("tick", c3); 105 }; 106 const c3 = () => ok(false, "c3 should not be called"); 107 const c4 = () => { 108 ok(true, "c4 called"); 109 done(); 110 }; 111 112 emitter.on("tick", c1); 113 emitter.on("tick", c2); 114 emitter.on("tick", c3); 115 emitter.on("tick", c4); 116 117 emitter.emit("tick"); 118 }, 119 120 testOffAfterOnce() { 121 const emitter = getEventEmitter(); 122 123 let enteredC1 = false; 124 const c1 = () => (enteredC1 = true); 125 126 emitter.once("oao", c1); 127 emitter.off("oao", c1); 128 129 emitter.emit("oao"); 130 131 ok(!enteredC1, "c1 should not be called"); 132 }, 133 134 testPromise() { 135 const emitter = getEventEmitter(); 136 const p = emitter.once("thing"); 137 138 // Check that the promise is only resolved once event though we 139 // emit("thing") more than once 140 let firstCallbackCalled = false; 141 const check1 = p.then(arg => { 142 equal(firstCallbackCalled, false, "first callback called only once"); 143 firstCallbackCalled = true; 144 equal(arg, "happened", "correct arg in promise"); 145 return "rval from c1"; 146 }); 147 148 emitter.emit("thing", "happened", "ignored"); 149 150 // Check that the promise is resolved asynchronously 151 let secondCallbackCalled = false; 152 const check2 = p.then(arg => { 153 ok(true, "second callback called"); 154 equal(arg, "happened", "correct arg in promise"); 155 secondCallbackCalled = true; 156 equal(arg, "happened", "correct arg in promise (a second time)"); 157 return "rval from c2"; 158 }); 159 160 // Shouldn't call any of the above listeners 161 emitter.emit("thing", "trashinate"); 162 163 // Check that we can still separate events with different names 164 // and that it works with no parameters 165 const pfoo = emitter.once("foo"); 166 const pbar = emitter.once("bar"); 167 168 const check3 = pfoo.then(arg => { 169 Assert.strictEqual(arg, undefined, "no arg for foo event"); 170 return "rval from c3"; 171 }); 172 173 pbar.then(() => { 174 ok(false, "pbar should not be called"); 175 }); 176 177 emitter.emit("foo"); 178 179 equal(secondCallbackCalled, false, "second callback not called yet"); 180 181 return Promise.all([check1, check2, check3]).then(args => { 182 equal(args[0], "rval from c1", "callback 1 done good"); 183 equal(args[1], "rval from c2", "callback 2 done good"); 184 equal(args[2], "rval from c3", "callback 3 done good"); 185 }); 186 }, 187 188 testClearEvents() { 189 const emitter = getEventEmitter(); 190 191 const received = []; 192 const listener = (...args) => received.push(args); 193 194 emitter.on("a", listener); 195 emitter.on("b", listener); 196 emitter.on("c", listener); 197 198 emitter.emit("a", 1); 199 emitter.emit("b", 1); 200 emitter.emit("c", 1); 201 202 equal(received.length, 3, "the listener was triggered three times"); 203 204 emitter.clearEvents(); 205 emitter.emit("a", 1); 206 emitter.emit("b", 1); 207 emitter.emit("c", 1); 208 equal(received.length, 3, "the listener was not called after clearEvents"); 209 }, 210 211 testOnReturn() { 212 const emitter = getEventEmitter(); 213 214 let called = false; 215 const removeOnTest = emitter.on("test", () => { 216 called = true; 217 }); 218 219 equal(typeof removeOnTest, "function", "`on` returns a function"); 220 removeOnTest(); 221 222 emitter.emit("test"); 223 equal(called, false, "event listener wasn't called"); 224 }, 225 226 async testEmitAsync() { 227 const emitter = getEventEmitter(); 228 229 let resolve1, resolve2; 230 emitter.once("test", async () => { 231 return new Promise(r => { 232 resolve1 = r; 233 }); 234 }); 235 236 // Adding a listener which doesn't return a promise should trigger a console warning. 237 emitter.once("test", () => {}); 238 239 emitter.once("test", async () => { 240 return new Promise(r => { 241 resolve2 = r; 242 }); 243 }); 244 245 info("Emit an event and wait for all listener resolutions"); 246 const onConsoleWarning = onConsoleWarningLogged( 247 "Listener for event 'test' did not return a promise." 248 ); 249 const onEmitted = emitter.emitAsync("test"); 250 let resolved = false; 251 onEmitted.then(() => { 252 info("emitAsync just resolved"); 253 resolved = true; 254 }); 255 256 info("Waiting for warning message about the second listener"); 257 await onConsoleWarning; 258 259 // Spin the event loop, to ensure that emitAsync did not resolved too early 260 await new Promise(r => Services.tm.dispatchToMainThread(r)); 261 262 ok(resolve1, "event listener has been called"); 263 ok(!resolved, "but emitAsync hasn't resolved yet"); 264 265 info("Resolve the first listener function"); 266 resolve1(); 267 ok(!resolved, "emitAsync isn't resolved until all listener resolve"); 268 269 info("Resolve the second listener function"); 270 resolve2(); 271 272 // emitAsync is only resolved in the next event loop 273 await new Promise(r => Services.tm.dispatchToMainThread(r)); 274 ok(resolved, "once we resolve all the listeners, emitAsync is resolved"); 275 }, 276 277 testCount() { 278 const emitter = getEventEmitter(); 279 280 equal(emitter.count("foo"), 0, "no listeners for 'foo' events"); 281 emitter.on("foo", () => {}); 282 equal(emitter.count("foo"), 1, "listener registered"); 283 emitter.on("foo", () => {}); 284 equal(emitter.count("foo"), 2, "another listener registered"); 285 emitter.off("foo"); 286 equal(emitter.count("foo"), 0, "listeners unregistered"); 287 }, 288 }; 289 290 // Wait for the next call to console.warn which includes 291 // the text passed as argument 292 function onConsoleWarningLogged(warningMessage) { 293 return new Promise(resolve => { 294 const ConsoleAPIStorage = Cc[ 295 "@mozilla.org/consoleAPI-storage;1" 296 ].getService(Ci.nsIConsoleAPIStorage); 297 298 const observer = subject => { 299 // This is the first argument passed to console.warn() 300 const message = subject.wrappedJSObject.arguments[0]; 301 if (message.includes(warningMessage)) { 302 ConsoleAPIStorage.removeLogEventListener(observer); 303 resolve(); 304 } 305 }; 306 307 ConsoleAPIStorage.addLogEventListener( 308 observer, 309 Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) 310 ); 311 }); 312 } 313 314 /** 315 * Create a runnable tests based on the tests descriptor given. 316 * 317 * @param {object} tests 318 * The tests descriptor object, contains the tests to run. 319 */ 320 const runnable = tests => 321 async function () { 322 for (const name of Object.keys(tests)) { 323 info(name); 324 if (tests[name].length === 1) { 325 await new Promise(resolve => tests[name](resolve)); 326 } else { 327 await tests[name](); 328 } 329 } 330 }; 331 332 // We want to run the same tests for both an instance of `EventEmitter` and an object 333 // decorate with EventEmitter; therefore we create two strategies (`createNewEmitter` and 334 // `decorateObject`) and a factory (`getEventEmitter`), where the factory is the actual 335 // function used in the tests. 336 337 const createNewEmitter = () => new EventEmitter(); 338 const decorateObject = () => EventEmitter.decorate({}); 339 340 // First iteration of the tests with a new instance of `EventEmitter`. 341 let getEventEmitter = createNewEmitter; 342 add_task(runnable(TESTS)); 343 // Second iteration of the tests with an object decorate using `EventEmitter` 344 add_task(() => (getEventEmitter = decorateObject)); 345 add_task(runnable(TESTS));