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