]> git.donarmstrong.com Git - dactyl.git/blob - common/tests/functional/dactyl.jsm
finalize changelog for 7904
[dactyl.git] / common / tests / functional / dactyl.jsm
1 var utils = {}; Components.utils.import(/([^ ]+\/)[^\/]+$/.exec(Components.stack.filename)[1] + "utils.jsm", utils);
2
3 var EXPORTED_SYMBOLS = ["Controller"];
4
5 const { module, NS } = utils;
6
7 var elementslib = module("resource://mozmill/modules/elementslib.js");
8 var frame = module("resource://mozmill/modules/frame.js");
9 var jumlib = module("resource://mozmill/modules/jum.js");
10
11 function wrapAssertNoErrors(func, message) {
12     return function wrapped(arg) this.assertNoErrors(func, this, arguments, message || arg);
13 }
14
15 function assertMessage(funcName, want, got, message) {
16     if (typeof want === "string")
17         return utils.assertEqual(funcName, want, got, message);
18     else if (typeof want === "function") {
19         var res = want(got);
20         if (res === undefined)
21             return true;
22         return utils.test(res, {
23             function: funcName,
24             want: want, got: got,
25             comment: message
26         });
27     }
28     else
29         return utils.test(want.test(got), {
30             function: funcName,
31             want: want, got: got,
32             comment: message
33         });
34 }
35
36 /**
37  * A controller for simulating Dactyl related user actions and for making
38  * assertions about the expected outcomes of such actions.
39  *
40  * @param {MozMillController} controller The browser's MozMill controller.
41  */
42 function Controller(controller) {
43     this.controller = controller;
44
45     /**
46      * @property {object} The dactyl modules namespace, to be used
47      * sparingly in tests.
48      */
49     this.modules = controller.window.dactyl.modules;
50
51     this.errorCount = 0;
52
53     this._countBeep = () => {
54         this.beepCount++;
55     }
56     this.errors = [];
57     this._countError = (message, highlight) => {
58         if (/\b(Error|Warning)Msg\b/.test(highlight))
59             this.errors.push(String(message));
60     }
61     this.modules.dactyl.registerObserver("beep", this._countBeep);
62     this.modules.dactyl.registerObserver("echoLine", this._countError);
63     this.modules.dactyl.registerObserver("echoMultiline", this._countError);
64
65     this.resetErrorCount();
66 }
67
68 Controller.prototype = {
69
70     teardown: function () {
71         this.modules.dactyl.unregisterObserver("beep", this._countBeep);
72         this.modules.dactyl.unregisterObserver("echoLine", this._countError);
73         this.modules.dactyl.unregisterObserver("echoMultiline", this._countError);
74     },
75
76     beepCount: 0,
77     errorCount: 0,
78
79     /**
80      * Asserts that an error message is displayed during the execution
81      * of *func*.
82      *
83      * @param {function} func A function to call during before the
84      *      assertion takes place.
85      * @param {object} self The 'this' object to be used during the call
86      *      of *func*. @optional
87      * @param {Array} args Arguments to be passed to *func*. @optional
88      * @param {string} message The message to display upon assertion failure. @optional
89      */
90     assertMessageError: function (func, self, args, message) {
91         let errorCount = this.errors.length;
92         this.assertNoErrors(func, self, args, message);
93         // dump("assertMessageError " + errorCount + " " + this.errorMessageCount + "\n");
94         return utils.assert('dactyl.assertMessageError', this.errors.length > errorCount,
95                             "Expected error but got none" + (message ? ": " + message : ""));
96     },
97
98     /**
99      * Asserts that any output message text content matches *text*.
100      *
101      * @param {string|RegExp|function} want The expected text of the expected message line.
102      * @param {string} message The message to display upon assertion failure.
103      */
104     assertMessage: function (want, message) {
105         return assertMessage('dactyl.assertMessage', want,
106                              this.readMessageWindow() || this.readMessageLine(),
107                              message);
108     },
109
110     /**
111      * Asserts that the output message line text content matches *text*.
112      *
113      * @param {string|RegExp|function} want The expected text of the expected message line.
114      * @param {string} message The message to display upon assertion failure.
115      */
116     assertMessageLine: function (want, message) {
117         return assertMessage('dactyl.assertMessageLine', want,
118                              this.readMessageLine(),
119                              message);
120     },
121
122     /**
123      * Asserts that the output message window text content matches *text*.
124      *
125      * @param {string|RegExp|function} want The expected text of the message window.
126      * @param {string} message The message to display upon assertion failure.
127      */
128     assertMessageWindow: function (want, message) {
129         return assertMessage('dactyl.assertMessageWindow', want,
130                              this.readMessageWindow(),
131                              message);
132     },
133
134     /**
135      * Asserts that the output message line text is an error and content matches *text*.
136      *
137      * @param {string|RegExp|function} want The expected text of the expected message line.
138      * @param {string} message The message to display upon assertion failure.
139      */
140     assertErrorMessage: function (want, message) {
141         return assertMessage('dactyl.assertMessageError', want,
142                              this.readMessageLine(),
143                              message) &&
144                assertMessage('dactyl.assertMessageError', /\bErrorMsg\b/,
145                              this.elements.message.getAttributeNS(NS, "highlight"),
146                              message);
147     },
148
149     /**
150      * Asserts that the multi-line output window is in the given state.
151      *
152      * @param {boolean} open True if the window is expected to be open.
153      * @param {string} message The message to display upon assertion failure. @optional
154      */
155     assertMessageWindowOpen: function (open, message) {
156         return utils.assertEqual('dactyl.assertMessageWindowOpen', open,
157                                  !this.elements.multilineContainer.collapsed,
158                                  message || "Multi-line output not in the expected state");
159     },
160
161     /**
162      * Asserts that the no errors have been reported since the last call
163      * to resetErrorCount.
164      *
165      * @param {function} func A function to call during before the
166      *      assertion takes place. When present, the current error count
167      *      is reset before execution.
168      *      @optional
169      * @param {object} self The 'this' object to be used during the call
170      *      of *func*. @optional
171      * @param {Array} args Arguments to be passed to *func*. @optional
172      * @param {string} message The message to display upon assertion failure. @optional
173      */
174     assertNoErrors: function (func, self, args, message) {
175         let msg = message ? ": " + message : "";
176
177         let beepCount = this.beepCount;
178         let errorCount = this.errorCount;
179         if (func) {
180             errorCount = this.modules.util.errorCount;
181
182             try {
183                 var returnVal = func.apply(self || this, args || []);
184             }
185             catch (e) {
186                 this.modules.util.reportError(e);
187             }
188         }
189
190         if (this.beepCount > beepCount)
191             frame.log({
192                 function: "dactyl.beepMonitor",
193                 want: beepCount, got: this.beepCount,
194                 comment: "Got " + (this.beepCount - beepCount) + " beeps during execution" + msg
195             });
196
197         if (errorCount != this.modules.util.errorCount)
198             var errors = this.modules.util.errors.slice(errorCount - this.modules.util.errorCount)
199                              .join("\n");
200
201         var res = utils.assertEqual('dactyl.assertNoErrors',
202                                     errorCount, this.modules.util.errorCount,
203                                     "Errors were reported during the execution of this test" + msg + "\n" + errors);
204
205         return returnVal === undefined ? res : returnVal;
206     },
207
208     /**
209      * Asserts that the no error messages are reported during the call
210      * of *func*.
211      *
212      * @param {function} func A function to call during before the
213      *      assertion takes place. When present, the current error count
214      *      is reset before execution.
215      *      @optional
216      * @param {object} self The 'this' object to be used during the call
217      *      of *func*. @optional
218      * @param {Array} args Arguments to be passed to *func*. @optional
219      * @param {string} message The message to display upon assertion failure. @optional
220      */
221     assertNoErrorMessages: function (func, self, args, message) {
222         let msg = message ? ": " + message : "";
223         let count = this.errors.length;
224
225         try {
226             func.apply(self || this, args || []);
227         }
228         catch (e) {
229             this.modules.util.reportError(e);
230         }
231
232         return utils.assertEqual('dactyl.assertNoErrorMessages', count, this.errors.length,
233                                  "Error messsages were reported" + msg + ":\n\t" +
234                                  this.errors.slice(count).join("\n\t"));
235     },
236
237     /**
238      * Resets the error count used to determine whether new errors were
239      * reported during the execution of a test.
240      */
241     resetErrorCount: function () {
242         this.errorCount = this.modules.util.errorCount;
243     },
244
245     /**
246      * Wraps the given function such that any errors triggered during
247      * its execution will trigger a failed assertion.
248      *
249      * @param {function} func The function to wrap.
250      * @param {string} message The message to display upon assertion failure. @optional
251      */
252     wrapAssertNoErrors: function (func, message) {
253         return () => this.assertNoErrors(func, this, arguments, message);
254     },
255
256     /**
257      * Asserts that the current window selection matches *text*.
258      *
259      * @param {string|RegExp|function} text The expected text of the current selection.
260      * @param {string} message The message to display upon assertion failure.
261      */
262     assertSelection: function (want, message) {
263         return assertMessage('dactyl.assertSelection', want,
264                              String(this.controller.window.content.getSelection()),
265                              message);
266     },
267
268     /**
269      * @property {string} The name of dactyl's current key handling
270      * mode.
271      */
272     get currentMode() this.modules.modes.main.name,
273
274     /**
275      * @property {object} A map of dactyl widgets to be used sparingly
276      * for focus assertions.
277      */
278     get elements() let (self = this) ({
279         /**
280          * @property {HTMLInputElement} The command line's command input box
281          */
282         get commandInput() self.modules.commandline.widgets.active.command.inputField,
283         /**
284          * @property {Node|null} The currently focused node.
285          */
286         get focused() self.controller.window.document.commandDispatcher.focusedElement,
287         /**
288          * @property {HTMLInputElement} The message bar's command input box
289          */
290         get message() self.modules.commandline.widgets.active.message,
291         /**
292          * @property {Node} The multi-line output window.
293          */
294         get multiline() self.modules.commandline.widgets.multilineOutput,
295         /**
296          * @property {Node} The multi-line output container.
297          */
298         get multilineContainer() self.modules.commandline.widgets.mowContainer,
299     }),
300
301     /**
302      * Returns dactyl to Normal mode.
303      */
304     setNormalMode: wrapAssertNoErrors(function () {
305         // XXX: Normal mode test
306         for (let i = 0; i < 15 && this.modules.modes.stack.length > 1; i++)
307             this.controller.keypress(null, "VK_ESCAPE", {});
308
309         this.controller.keypress(null, "l", { ctrlKey: true });
310
311         utils.assert("dactyl.setNormalMode", this.modules.modes.stack.length == 1,
312                      "Failed to return to Normal mode");
313
314         this.assertMessageWindowOpen(false, "Returning to Normal mode: Multi-line output not closed");
315         this.assertMessageLine(function (msg) !msg, "Returning to Normal mode: Message not cleared");
316     }, "Returning to Normal mode"),
317
318     /**
319      * Returns dactyl to Ex mode.
320      */
321     setExMode: wrapAssertNoErrors(function () {
322         if (this.currentMode !== "EX") {
323             this.setNormalMode();
324             this.controller.keypress(null, ":", {});
325         }
326         else {
327             this.elements.commandInput.value = "";
328         }
329     }),
330
331     /**
332      * Runs a Vi command.
333      *
334      * @param {string|Array} keys Either a string of simple keys suitable for
335      *     {@link MozMillController#type} or an array of keysym - modifier
336      *     pairs suitable for {@link MozMillController#keypress}.
337      */
338     runViCommand: wrapAssertNoErrors(function (keys) {
339         if (typeof keys == "string")
340             keys = [[k] for each (k in keys)];
341         keys.forEach(function ([key, modifiers]) { this.controller.keypress(null, key, modifiers || {}); }, this);
342     }),
343
344     /**
345      * Runs an Ex command.
346      *
347      * @param {string} cmd The Ex command string as entered on the command
348      *     line.
349      * @param {object} args An args object by means of which to execute
350      *     the command. If absent *cmd* is parsed as a complete
351      *     arguments string. @optional
352      */
353     // TODO: Use execution code from commandline.js to catch more
354     // possible errors without being insanely inefficient after the
355     // merge.
356     runExCommand: wrapAssertNoErrors(function (cmd, args) {
357         this.setNormalMode();
358         try {
359             // Force async commands to wait for their output to be ready
360             // before returning.
361             this.modules.commandline.savingOutput = true;
362             if (args)
363                 this.modules.ex[cmd](args);
364             else if (true)
365                 this.modules.commands.execute(cmd, null, false, null,
366                                              { file: "[Command Line]", line: 1 });
367             else {
368                 var doc = this.controller.window.document;
369                 var event = doc.createEvent("Events");
370                 event.initEvent("dactyl.execute", false, false);
371                 doc.documentElement.setAttribute("dactyl-execute", cmd);
372                 doc.documentElement.dispatchEvent(event);
373             }
374         }
375         finally {
376             this.modules.commandline.savingOutput = false;
377         }
378     }),
379
380     /**
381      * Triggers a completion function with the given arguments an
382      * ensures that no errors have occurred during the process.
383      *
384      * @param {object} self The 'this' object for which to trigger the
385      *     completer.
386      * @param {function|string} func The method or method name to call.
387      * @param {string} string The method or method name to call. @optional
388      * @param {string} message The message to display upon assertion failure. @optional
389      * @param {...} Extra arguments are passed to the completion
390      *     function directly.
391      */
392     testCompleter: wrapAssertNoErrors(function testCompleter(self, func, string, message, ...args) {
393         var context = this.modules.CompletionContext(string || "");
394         context.tabPressed = true;
395         context.forkapply("completions", 0, self, func, args);
396
397         utils.assert("dactyl.runCompletions", context.wait(5000),
398                      message || "Completion failed: " + self + "." + func);
399
400         for (var [, ctxt] in Iterator(context.contextList))
401             for (var [, item] in Iterator(ctxt.items))
402                 ctxt.createRow(item);
403
404         return context;
405     }),
406
407     /**
408      * Triggers Ex completion for the given command string and ensures
409      * that no errors have occurred during the process.
410      *
411      * @param {string} cmd The Ex command string as entered on the command
412      *     line.
413      * @param {boolean} longWay Whether to test the completion by
414      *     entering it into the command line and dispatching a <Tab> key
415      *     press.
416      */
417     runExCompletion: wrapAssertNoErrors(function (cmd, longWay) {
418         // dump("runExCompletion " + cmd + "\n");
419         if (!longWay) {
420             var context = this.modules.CompletionContext(cmd);
421             context.tabPressed = true;
422             context.fork("ex", 0, this.modules.completion, "ex");
423
424             utils.assert("dactyl.runCompletions", context.wait(5000),
425                          "Completion failed: " + cmd.quote());
426
427             for (var [, ctxt] in Iterator(context.contextList))
428                 for (var [, item] in Iterator(ctxt.items))
429                     ctxt.createRow(item);
430
431             return context;
432         }
433         else {
434             this.setExMode();
435
436             utils.assertEqual("dactyl.assertCommandLineFocused",
437                               this.elements.commandInput,
438                               this.elements.focused,
439                               "Running Ex Completion: The command line is not focused");
440
441             if (true) {
442                 let input = this.elements.commandInput;
443                 input.value = cmd;
444
445                 var event = input.ownerDocument.createEvent("Events");
446                 event.initEvent("change", true, false);
447                 input.dispatchEvent(event);
448             }
449             else {
450                 this.controller.type(null, cmd);
451
452                 utils.assertEqual("dactyl.runExCompletion", cmd,
453                                   this.elements.commandInput.editor.rootElement.firstChild.textContent,
454                                   "Command line does not have the expected value: " + cmd);
455             }
456
457             this.controller.keypress(null, "VK_TAB", {});
458
459             // XXX
460             if (this.modules.commandline._tabTimer)
461                 this.modules.commandline._tabTimer.flush();
462             else if (this.modules.commandline.commandSession && this.modules.commandline.commandSession.completions)
463                 this.modules.commandline.commandSession.completions.tabTimer.flush();
464         }
465     }),
466
467     /**
468      * Returns the text content of the output message line.
469      *
470      * @returns {string} The message line text content.
471      */
472     readMessageLine: function () {
473         return this.elements.message.value;
474     },
475
476     /**
477      * Returns the text content of the output message window.
478      *
479      * @returns {string} The message window text content.
480      */
481     readMessageWindow: function () {
482         if (!this.elements.multilineContainer.collapsed)
483             return this.elements.multiline.contentDocument.body.textContent;
484         return "";
485     },
486
487     /**
488      * Opens the output message window by echoing a single newline character.
489      */
490     openMessageWindow: wrapAssertNoErrors(function () {
491         this.modules.dactyl.echo("\n");
492     }, "Opening message window"),
493
494     /**
495      * Clears the current message.
496      */
497     clearMessage: function () {
498         this.elements.message.value = ""; // XXX
499     },
500
501     /**
502      * Closes the output message window if open.
503      */
504     closeMessageWindow: wrapAssertNoErrors(function () {
505         for (let i = 0; i < 15 && !this.elements.multilineContainer.collapsed; i++)
506             this.controller.keypress(null, "VK_ESCAPE", {});
507         this.assertMessageWindowOpen(false, "Clearing message window failed");
508     }, "Clearing message window"),
509
510     /**
511      * @property {string} The specific Dactyl application. Eg. Pentadactyl
512      */
513     get applicationName() this.modules.config.appName // XXX
514 };
515
516 // vim: sw=4 ts=8 et ft=javascript: