|
1 /* |
|
2 * Copyright 2012, Mozilla Foundation and contributors |
|
3 * |
|
4 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
5 * you may not use this file except in compliance with the License. |
|
6 * You may obtain a copy of the License at |
|
7 * |
|
8 * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 * |
|
10 * Unless required by applicable law or agreed to in writing, software |
|
11 * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13 * See the License for the specific language governing permissions and |
|
14 * limitations under the License. |
|
15 */ |
|
16 |
|
17 'use strict'; |
|
18 |
|
19 // A copy of this code exists in firefox mochitests. They should be kept |
|
20 // in sync. Hence the exports synonym for non AMD contexts. |
|
21 this.EXPORTED_SYMBOLS = [ 'helpers' ]; |
|
22 var helpers = {}; |
|
23 this.helpers = helpers; |
|
24 |
|
25 var TargetFactory = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.TargetFactory; |
|
26 var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
27 |
|
28 var assert = { ok: ok, is: is, log: info }; |
|
29 var util = require('gcli/util/util'); |
|
30 var promise = require('gcli/util/promise'); |
|
31 var cli = require('gcli/cli'); |
|
32 var KeyEvent = require('gcli/util/util').KeyEvent; |
|
33 var gcli = require('gcli/index'); |
|
34 |
|
35 /** |
|
36 * See notes in helpers.checkOptions() |
|
37 */ |
|
38 var createFFDisplayAutomator = function(display) { |
|
39 var automator = { |
|
40 setInput: function(typed) { |
|
41 return display.inputter.setInput(typed); |
|
42 }, |
|
43 |
|
44 setCursor: function(cursor) { |
|
45 return display.inputter.setCursor(cursor); |
|
46 }, |
|
47 |
|
48 focus: function() { |
|
49 return display.inputter.focus(); |
|
50 }, |
|
51 |
|
52 fakeKey: function(keyCode) { |
|
53 var fakeEvent = { |
|
54 keyCode: keyCode, |
|
55 preventDefault: function() { }, |
|
56 timeStamp: new Date().getTime() |
|
57 }; |
|
58 |
|
59 display.inputter.onKeyDown(fakeEvent); |
|
60 |
|
61 if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) { |
|
62 var input = display.inputter.element; |
|
63 input.value = input.value.slice(0, -1); |
|
64 } |
|
65 |
|
66 return display.inputter.handleKeyUp(fakeEvent); |
|
67 }, |
|
68 |
|
69 getInputState: function() { |
|
70 return display.inputter.getInputState(); |
|
71 }, |
|
72 |
|
73 getCompleterTemplateData: function() { |
|
74 return display.completer._getCompleterTemplateData(); |
|
75 }, |
|
76 |
|
77 getErrorMessage: function() { |
|
78 return display.tooltip.errorEle.textContent; |
|
79 } |
|
80 }; |
|
81 |
|
82 Object.defineProperty(automator, 'focusManager', { |
|
83 get: function() { return display.focusManager; }, |
|
84 enumerable: true |
|
85 }); |
|
86 |
|
87 Object.defineProperty(automator, 'field', { |
|
88 get: function() { return display.tooltip.field; }, |
|
89 enumerable: true |
|
90 }); |
|
91 |
|
92 return automator; |
|
93 }; |
|
94 |
|
95 /** |
|
96 * Warning: For use with Firefox Mochitests only. |
|
97 * |
|
98 * Open a new tab at a URL and call a callback on load, and then tidy up when |
|
99 * the callback finishes. |
|
100 * The function will be passed a set of test options, and will usually return a |
|
101 * promise to indicate that the tab can be cleared up. (To be formal, we call |
|
102 * Promise.resolve() on the return value of the callback function) |
|
103 * |
|
104 * The options used by addTab include: |
|
105 * - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest |
|
106 * - tab: The new XUL tab element, as returned by gBrowser.addTab() |
|
107 * - target: The debug target as defined by the devtools framework |
|
108 * - browser: The XUL browser element for the given tab |
|
109 * - window: Content window for the created tab. a.k.a 'content' in mochitest |
|
110 * - isFirefox: Always true. Allows test sharing with GCLI |
|
111 * |
|
112 * Normally addTab will create an options object containing the values as |
|
113 * described above. However these options can be customized by the third |
|
114 * 'options' parameter. This has the ability to customize the value of |
|
115 * chromeWindow or isFirefox, and to add new properties. |
|
116 * |
|
117 * @param url The URL for the new tab |
|
118 * @param callback The function to call on page load |
|
119 * @param options An optional set of options to customize the way the tests run |
|
120 */ |
|
121 helpers.addTab = function(url, callback, options) { |
|
122 waitForExplicitFinish(); |
|
123 |
|
124 options = options || {}; |
|
125 options.chromeWindow = options.chromeWindow || window; |
|
126 options.isFirefox = true; |
|
127 |
|
128 var tabbrowser = options.chromeWindow.gBrowser; |
|
129 options.tab = tabbrowser.addTab(); |
|
130 tabbrowser.selectedTab = options.tab; |
|
131 options.browser = tabbrowser.getBrowserForTab(options.tab); |
|
132 options.target = TargetFactory.forTab(options.tab); |
|
133 |
|
134 var loaded = helpers.listenOnce(options.browser, "load", true).then(function(ev) { |
|
135 options.document = options.browser.contentDocument; |
|
136 options.window = options.document.defaultView; |
|
137 |
|
138 var reply = callback.call(null, options); |
|
139 |
|
140 return promise.resolve(reply).then(null, function(error) { |
|
141 ok(false, error); |
|
142 }).then(function() { |
|
143 tabbrowser.removeTab(options.tab); |
|
144 |
|
145 delete options.window; |
|
146 delete options.document; |
|
147 |
|
148 delete options.target; |
|
149 delete options.browser; |
|
150 delete options.tab; |
|
151 |
|
152 delete options.chromeWindow; |
|
153 delete options.isFirefox; |
|
154 }); |
|
155 }); |
|
156 |
|
157 options.browser.contentWindow.location = url; |
|
158 return loaded; |
|
159 }; |
|
160 |
|
161 /** |
|
162 * Open a new tab |
|
163 * @param url Address of the page to open |
|
164 * @param options Object to which we add properties describing the new tab. The |
|
165 * following properties are added: |
|
166 * - chromeWindow |
|
167 * - tab |
|
168 * - browser |
|
169 * - target |
|
170 * - document |
|
171 * - window |
|
172 * @return A promise which resolves to the options object when the 'load' event |
|
173 * happens on the new tab |
|
174 */ |
|
175 helpers.openTab = function(url, options) { |
|
176 waitForExplicitFinish(); |
|
177 |
|
178 options = options || {}; |
|
179 options.chromeWindow = options.chromeWindow || window; |
|
180 options.isFirefox = true; |
|
181 |
|
182 var tabbrowser = options.chromeWindow.gBrowser; |
|
183 options.tab = tabbrowser.addTab(); |
|
184 tabbrowser.selectedTab = options.tab; |
|
185 options.browser = tabbrowser.getBrowserForTab(options.tab); |
|
186 options.target = TargetFactory.forTab(options.tab); |
|
187 |
|
188 options.browser.contentWindow.location = url; |
|
189 |
|
190 return helpers.listenOnce(options.browser, "load", true).then(function() { |
|
191 options.document = options.browser.contentDocument; |
|
192 options.window = options.document.defaultView; |
|
193 return options; |
|
194 }); |
|
195 }; |
|
196 |
|
197 /** |
|
198 * Undo the effects of |helpers.openTab| |
|
199 * @param options The options object passed to |helpers.openTab| |
|
200 * @return A promise resolved (with undefined) when the tab is closed |
|
201 */ |
|
202 helpers.closeTab = function(options) { |
|
203 options.chromeWindow.gBrowser.removeTab(options.tab); |
|
204 |
|
205 delete options.window; |
|
206 delete options.document; |
|
207 |
|
208 delete options.target; |
|
209 delete options.browser; |
|
210 delete options.tab; |
|
211 |
|
212 delete options.chromeWindow; |
|
213 delete options.isFirefox; |
|
214 |
|
215 return promise.resolve(undefined); |
|
216 }; |
|
217 |
|
218 /** |
|
219 * Open the developer toolbar in a tab |
|
220 * @param options Object to which we add properties describing the developer |
|
221 * toolbar. The following properties are added: |
|
222 * - automator |
|
223 * - requisition |
|
224 * @return A promise which resolves to the options object when the 'load' event |
|
225 * happens on the new tab |
|
226 */ |
|
227 helpers.openToolbar = function(options) { |
|
228 return options.chromeWindow.DeveloperToolbar.show(true).then(function() { |
|
229 var display = options.chromeWindow.DeveloperToolbar.display; |
|
230 options.automator = createFFDisplayAutomator(display); |
|
231 options.requisition = display.requisition; |
|
232 }); |
|
233 }; |
|
234 |
|
235 /** |
|
236 * Undo the effects of |helpers.openToolbar| |
|
237 * @param options The options object passed to |helpers.openToolbar| |
|
238 * @return A promise resolved (with undefined) when the toolbar is closed |
|
239 */ |
|
240 helpers.closeToolbar = function(options) { |
|
241 return options.chromeWindow.DeveloperToolbar.hide().then(function() { |
|
242 delete options.automator; |
|
243 delete options.requisition; |
|
244 }); |
|
245 }; |
|
246 |
|
247 /** |
|
248 * A helper to work with Task.spawn so you can do: |
|
249 * return Task.spawn(realTestFunc).then(finish, helpers.handleError); |
|
250 */ |
|
251 helpers.handleError = function(ex) { |
|
252 console.error(ex); |
|
253 ok(false, ex); |
|
254 finish(); |
|
255 }; |
|
256 |
|
257 /** |
|
258 * A helper for calling addEventListener and then removeEventListener as soon |
|
259 * as the event is called, passing the results on as a promise |
|
260 * @param element The DOM element to listen on |
|
261 * @param event The name of the event to listen for |
|
262 * @param useCapture Should we use the capturing phase? |
|
263 * @return A promise resolved with the event object when the event first happens |
|
264 */ |
|
265 helpers.listenOnce = function(element, event, useCapture) { |
|
266 var deferred = promise.defer(); |
|
267 var onEvent = function(ev) { |
|
268 element.removeEventListener(event, onEvent, useCapture); |
|
269 deferred.resolve(ev); |
|
270 }; |
|
271 element.addEventListener(event, onEvent, useCapture); |
|
272 return deferred.promise; |
|
273 }; |
|
274 |
|
275 /** |
|
276 * A wrapper for calling Services.obs.[add|remove]Observer using promises. |
|
277 * @param topic The topic parameter to Services.obs.addObserver |
|
278 * @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a |
|
279 * default value of false |
|
280 * @return a promise that resolves when the ObserverService first notifies us |
|
281 * of the topic. The value of the promise is the first parameter to the observer |
|
282 * function other parameters are dropped. |
|
283 */ |
|
284 helpers.observeOnce = function(topic, ownsWeak=false) { |
|
285 let deferred = promise.defer(); |
|
286 let resolver = function(subject) { |
|
287 Services.obs.removeObserver(resolver, topic); |
|
288 deferred.resolve(subject); |
|
289 }; |
|
290 Services.obs.addObserver(resolver, topic, ownsWeak); |
|
291 return deferred.promise; |
|
292 }; |
|
293 |
|
294 /** |
|
295 * Takes a function that uses a callback as its last parameter, and returns a |
|
296 * new function that returns a promise instead |
|
297 */ |
|
298 helpers.promiseify = function(functionWithLastParamCallback, scope) { |
|
299 return function() { |
|
300 let deferred = promise.defer(); |
|
301 |
|
302 let args = [].slice.call(arguments); |
|
303 args.push(function(callbackParam) { |
|
304 deferred.resolve(callbackParam); |
|
305 }); |
|
306 |
|
307 try { |
|
308 functionWithLastParamCallback.apply(scope, args); |
|
309 } |
|
310 catch (ex) { |
|
311 deferred.resolve(ex); |
|
312 } |
|
313 |
|
314 return deferred.promise; |
|
315 } |
|
316 }; |
|
317 |
|
318 /** |
|
319 * Warning: For use with Firefox Mochitests only. |
|
320 * |
|
321 * As addTab, but that also opens the developer toolbar. In addition a new |
|
322 * 'automator' property is added to the options object with the display from GCLI |
|
323 * in the developer toolbar |
|
324 */ |
|
325 helpers.addTabWithToolbar = function(url, callback, options) { |
|
326 return helpers.addTab(url, function(innerOptions) { |
|
327 var win = innerOptions.chromeWindow; |
|
328 |
|
329 return win.DeveloperToolbar.show(true).then(function() { |
|
330 var display = win.DeveloperToolbar.display; |
|
331 innerOptions.automator = createFFDisplayAutomator(display); |
|
332 innerOptions.requisition = display.requisition; |
|
333 |
|
334 var reply = callback.call(null, innerOptions); |
|
335 |
|
336 return promise.resolve(reply).then(null, function(error) { |
|
337 ok(false, error); |
|
338 console.error(error); |
|
339 }).then(function() { |
|
340 win.DeveloperToolbar.hide().then(function() { |
|
341 delete innerOptions.automator; |
|
342 }); |
|
343 }); |
|
344 }); |
|
345 }, options); |
|
346 }; |
|
347 |
|
348 /** |
|
349 * Warning: For use with Firefox Mochitests only. |
|
350 * |
|
351 * Run a set of test functions stored in the values of the 'exports' object |
|
352 * functions stored under setup/shutdown will be run at the start/end of the |
|
353 * sequence of tests. |
|
354 * A test will be considered finished when its return value is resolved. |
|
355 * @param options An object to be passed to the test functions |
|
356 * @param tests An object containing named test functions |
|
357 * @return a promise which will be resolved when all tests have been run and |
|
358 * their return values resolved |
|
359 */ |
|
360 helpers.runTests = function(options, tests) { |
|
361 var testNames = Object.keys(tests).filter(function(test) { |
|
362 return test != "setup" && test != "shutdown"; |
|
363 }); |
|
364 |
|
365 var recover = function(error) { |
|
366 ok(false, error); |
|
367 console.error(error); |
|
368 }; |
|
369 |
|
370 info("SETUP"); |
|
371 var setupDone = (tests.setup != null) ? |
|
372 promise.resolve(tests.setup(options)) : |
|
373 promise.resolve(); |
|
374 |
|
375 var testDone = setupDone.then(function() { |
|
376 return util.promiseEach(testNames, function(testName) { |
|
377 info(testName); |
|
378 var action = tests[testName]; |
|
379 |
|
380 if (typeof action === "function") { |
|
381 var reply = action.call(tests, options); |
|
382 return promise.resolve(reply); |
|
383 } |
|
384 else if (Array.isArray(action)) { |
|
385 return helpers.audit(options, action); |
|
386 } |
|
387 |
|
388 return promise.reject("test action '" + testName + |
|
389 "' is not a function or helpers.audit() object"); |
|
390 }); |
|
391 }, recover); |
|
392 |
|
393 return testDone.then(function() { |
|
394 info("SHUTDOWN"); |
|
395 return (tests.shutdown != null) ? |
|
396 promise.resolve(tests.shutdown(options)) : |
|
397 promise.resolve(); |
|
398 }, recover); |
|
399 }; |
|
400 |
|
401 /////////////////////////////////////////////////////////////////////////////// |
|
402 |
|
403 /** |
|
404 * Ensure that the options object is setup correctly |
|
405 * options should contain an automator object that looks like this: |
|
406 * { |
|
407 * getInputState: function() { ... }, |
|
408 * setCursor: function(cursor) { ... }, |
|
409 * getCompleterTemplateData: function() { ... }, |
|
410 * focus: function() { ... }, |
|
411 * getErrorMessage: function() { ... }, |
|
412 * fakeKey: function(keyCode) { ... }, |
|
413 * setInput: function(typed) { ... }, |
|
414 * focusManager: ..., |
|
415 * field: ..., |
|
416 * } |
|
417 */ |
|
418 function checkOptions(options) { |
|
419 if (options == null) { |
|
420 console.trace(); |
|
421 throw new Error('Missing options object'); |
|
422 } |
|
423 if (options.requisition == null) { |
|
424 console.trace(); |
|
425 throw new Error('options.requisition == null'); |
|
426 } |
|
427 } |
|
428 |
|
429 /** |
|
430 * Various functions to return the actual state of the command line |
|
431 */ |
|
432 helpers._actual = { |
|
433 input: function(options) { |
|
434 return options.automator.getInputState().typed; |
|
435 }, |
|
436 |
|
437 hints: function(options) { |
|
438 return options.automator.getCompleterTemplateData().then(function(data) { |
|
439 var emptyParams = data.emptyParameters.join(''); |
|
440 return (data.directTabText + emptyParams + data.arrowTabText) |
|
441 .replace(/\u00a0/g, ' ') |
|
442 .replace(/\u21E5/, '->') |
|
443 .replace(/ $/, ''); |
|
444 }); |
|
445 }, |
|
446 |
|
447 markup: function(options) { |
|
448 var cursor = helpers._actual.cursor(options); |
|
449 var statusMarkup = options.requisition.getInputStatusMarkup(cursor); |
|
450 return statusMarkup.map(function(s) { |
|
451 return new Array(s.string.length + 1).join(s.status.toString()[0]); |
|
452 }).join(''); |
|
453 }, |
|
454 |
|
455 cursor: function(options) { |
|
456 return options.automator.getInputState().cursor.start; |
|
457 }, |
|
458 |
|
459 current: function(options) { |
|
460 var cursor = helpers._actual.cursor(options); |
|
461 return options.requisition.getAssignmentAt(cursor).param.name; |
|
462 }, |
|
463 |
|
464 status: function(options) { |
|
465 return options.requisition.status.toString(); |
|
466 }, |
|
467 |
|
468 predictions: function(options) { |
|
469 var cursor = helpers._actual.cursor(options); |
|
470 var assignment = options.requisition.getAssignmentAt(cursor); |
|
471 var context = options.requisition.executionContext; |
|
472 return assignment.getPredictions(context).then(function(predictions) { |
|
473 return predictions.map(function(prediction) { |
|
474 return prediction.name; |
|
475 }); |
|
476 }); |
|
477 }, |
|
478 |
|
479 unassigned: function(options) { |
|
480 return options.requisition._unassigned.map(function(assignment) { |
|
481 return assignment.arg.toString(); |
|
482 }.bind(this)); |
|
483 }, |
|
484 |
|
485 outputState: function(options) { |
|
486 var outputData = options.automator.focusManager._shouldShowOutput(); |
|
487 return outputData.visible + ':' + outputData.reason; |
|
488 }, |
|
489 |
|
490 tooltipState: function(options) { |
|
491 var tooltipData = options.automator.focusManager._shouldShowTooltip(); |
|
492 return tooltipData.visible + ':' + tooltipData.reason; |
|
493 }, |
|
494 |
|
495 options: function(options) { |
|
496 if (options.automator.field.menu == null) { |
|
497 return []; |
|
498 } |
|
499 return options.automator.field.menu.items.map(function(item) { |
|
500 return item.name.textContent ? item.name.textContent : item.name; |
|
501 }); |
|
502 }, |
|
503 |
|
504 message: function(options) { |
|
505 return options.automator.getErrorMessage(); |
|
506 } |
|
507 }; |
|
508 |
|
509 function shouldOutputUnquoted(value) { |
|
510 var type = typeof value; |
|
511 return value == null || type === 'boolean' || type === 'number'; |
|
512 } |
|
513 |
|
514 function outputArray(array) { |
|
515 return (array.length === 0) ? |
|
516 '[ ]' : |
|
517 '[ \'' + array.join('\', \'') + '\' ]'; |
|
518 } |
|
519 |
|
520 helpers._createDebugCheck = function(options) { |
|
521 checkOptions(options); |
|
522 var requisition = options.requisition; |
|
523 var command = requisition.commandAssignment.value; |
|
524 var cursor = helpers._actual.cursor(options); |
|
525 var input = helpers._actual.input(options); |
|
526 var padding = new Array(input.length + 1).join(' '); |
|
527 |
|
528 var hintsPromise = helpers._actual.hints(options); |
|
529 var predictionsPromise = helpers._actual.predictions(options); |
|
530 |
|
531 return promise.all(hintsPromise, predictionsPromise).then(function(values) { |
|
532 var hints = values[0]; |
|
533 var predictions = values[1]; |
|
534 var output = ''; |
|
535 |
|
536 output += 'return helpers.audit(options, [\n'; |
|
537 output += ' {\n'; |
|
538 |
|
539 if (cursor === input.length) { |
|
540 output += ' setup: \'' + input + '\',\n'; |
|
541 } |
|
542 else { |
|
543 output += ' name: \'' + input + ' (cursor=' + cursor + ')\',\n'; |
|
544 output += ' setup: function() {\n'; |
|
545 output += ' return helpers.setInput(options, \'' + input + '\', ' + cursor + ');\n'; |
|
546 output += ' },\n'; |
|
547 } |
|
548 |
|
549 output += ' check: {\n'; |
|
550 |
|
551 output += ' input: \'' + input + '\',\n'; |
|
552 output += ' hints: ' + padding + '\'' + hints + '\',\n'; |
|
553 output += ' markup: \'' + helpers._actual.markup(options) + '\',\n'; |
|
554 output += ' cursor: ' + cursor + ',\n'; |
|
555 output += ' current: \'' + helpers._actual.current(options) + '\',\n'; |
|
556 output += ' status: \'' + helpers._actual.status(options) + '\',\n'; |
|
557 output += ' options: ' + outputArray(helpers._actual.options(options)) + ',\n'; |
|
558 output += ' message: \'' + helpers._actual.message(options) + '\',\n'; |
|
559 output += ' predictions: ' + outputArray(predictions) + ',\n'; |
|
560 output += ' unassigned: ' + outputArray(requisition._unassigned) + ',\n'; |
|
561 output += ' outputState: \'' + helpers._actual.outputState(options) + '\',\n'; |
|
562 output += ' tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' + |
|
563 (command ? ',' : '') +'\n'; |
|
564 |
|
565 if (command) { |
|
566 output += ' args: {\n'; |
|
567 output += ' command: { name: \'' + command.name + '\' },\n'; |
|
568 |
|
569 requisition.getAssignments().forEach(function(assignment) { |
|
570 output += ' ' + assignment.param.name + ': { '; |
|
571 |
|
572 if (typeof assignment.value === 'string') { |
|
573 output += 'value: \'' + assignment.value + '\', '; |
|
574 } |
|
575 else if (shouldOutputUnquoted(assignment.value)) { |
|
576 output += 'value: ' + assignment.value + ', '; |
|
577 } |
|
578 else { |
|
579 output += '/*value:' + assignment.value + ',*/ '; |
|
580 } |
|
581 |
|
582 output += 'arg: \'' + assignment.arg + '\', '; |
|
583 output += 'status: \'' + assignment.getStatus().toString() + '\', '; |
|
584 output += 'message: \'' + assignment.message + '\''; |
|
585 output += ' },\n'; |
|
586 }); |
|
587 |
|
588 output += ' }\n'; |
|
589 } |
|
590 |
|
591 output += ' },\n'; |
|
592 output += ' exec: {\n'; |
|
593 output += ' output: \'\',\n'; |
|
594 output += ' type: \'string\',\n'; |
|
595 output += ' error: false\n'; |
|
596 output += ' }\n'; |
|
597 output += ' }\n'; |
|
598 output += ']);'; |
|
599 |
|
600 return output; |
|
601 }.bind(this), util.errorHandler); |
|
602 }; |
|
603 |
|
604 /** |
|
605 * Simulate focusing the input field |
|
606 */ |
|
607 helpers.focusInput = function(options) { |
|
608 checkOptions(options); |
|
609 options.automator.focus(); |
|
610 }; |
|
611 |
|
612 /** |
|
613 * Simulate pressing TAB in the input field |
|
614 */ |
|
615 helpers.pressTab = function(options) { |
|
616 checkOptions(options); |
|
617 return helpers.pressKey(options, KeyEvent.DOM_VK_TAB); |
|
618 }; |
|
619 |
|
620 /** |
|
621 * Simulate pressing RETURN in the input field |
|
622 */ |
|
623 helpers.pressReturn = function(options) { |
|
624 checkOptions(options); |
|
625 return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN); |
|
626 }; |
|
627 |
|
628 /** |
|
629 * Simulate pressing a key by keyCode in the input field |
|
630 */ |
|
631 helpers.pressKey = function(options, keyCode) { |
|
632 checkOptions(options); |
|
633 return options.automator.fakeKey(keyCode); |
|
634 }; |
|
635 |
|
636 /** |
|
637 * A list of special key presses and how to to them, for the benefit of |
|
638 * helpers.setInput |
|
639 */ |
|
640 var ACTIONS = { |
|
641 '<TAB>': function(options) { |
|
642 return helpers.pressTab(options); |
|
643 }, |
|
644 '<RETURN>': function(options) { |
|
645 return helpers.pressReturn(options); |
|
646 }, |
|
647 '<UP>': function(options) { |
|
648 return helpers.pressKey(options, KeyEvent.DOM_VK_UP); |
|
649 }, |
|
650 '<DOWN>': function(options) { |
|
651 return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN); |
|
652 }, |
|
653 '<BACKSPACE>': function(options) { |
|
654 return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE); |
|
655 } |
|
656 }; |
|
657 |
|
658 /** |
|
659 * Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into |
|
660 * an array like [ 'blah', '<TAB>', 'foo', '<UP>' ]. |
|
661 * When using this RegExp, you also need to filter out the blank strings. |
|
662 */ |
|
663 var CHUNKER = /([^<]*)(<[A-Z]+>)/; |
|
664 |
|
665 /** |
|
666 * Alter the input to <code>typed</code> optionally leaving the cursor at |
|
667 * <code>cursor</code>. |
|
668 * @return A promise of the number of key-presses to respond |
|
669 */ |
|
670 helpers.setInput = function(options, typed, cursor) { |
|
671 checkOptions(options); |
|
672 var inputPromise; |
|
673 var automator = options.automator; |
|
674 // We try to measure average keypress time, but setInput can simulate |
|
675 // several, so we try to keep track of how many |
|
676 var chunkLen = 1; |
|
677 |
|
678 // The easy case is a simple string without things like <TAB> |
|
679 if (typed.indexOf('<') === -1) { |
|
680 inputPromise = automator.setInput(typed); |
|
681 } |
|
682 else { |
|
683 // Cut the input up into input strings separated by '<KEY>' tokens. The |
|
684 // CHUNKS RegExp leaves blanks so we filter them out. |
|
685 var chunks = typed.split(CHUNKER).filter(function(s) { |
|
686 return s !== ''; |
|
687 }); |
|
688 chunkLen = chunks.length + 1; |
|
689 |
|
690 // We're working on this in chunks so first clear the input |
|
691 inputPromise = automator.setInput('').then(function() { |
|
692 return util.promiseEach(chunks, function(chunk) { |
|
693 if (chunk.charAt(0) === '<') { |
|
694 var action = ACTIONS[chunk]; |
|
695 if (typeof action !== 'function') { |
|
696 console.error('Known actions: ' + Object.keys(ACTIONS).join()); |
|
697 throw new Error('Key action not found "' + chunk + '"'); |
|
698 } |
|
699 return action(options); |
|
700 } |
|
701 else { |
|
702 return automator.setInput(automator.getInputState().typed + chunk); |
|
703 } |
|
704 }); |
|
705 }); |
|
706 } |
|
707 |
|
708 return inputPromise.then(function() { |
|
709 if (cursor != null) { |
|
710 automator.setCursor({ start: cursor, end: cursor }); |
|
711 } |
|
712 |
|
713 if (automator.focusManager) { |
|
714 automator.focusManager.onInputChange(); |
|
715 } |
|
716 |
|
717 // Firefox testing is noisy and distant, so logging helps |
|
718 if (options.isFirefox) { |
|
719 var cursorStr = (cursor == null ? '' : ', ' + cursor); |
|
720 log('setInput("' + typed + '"' + cursorStr + ')'); |
|
721 } |
|
722 |
|
723 return chunkLen; |
|
724 }); |
|
725 }; |
|
726 |
|
727 /** |
|
728 * Helper for helpers.audit() to ensure that all the 'check' properties match. |
|
729 * See helpers.audit for more information. |
|
730 * @param name The name to use in error messages |
|
731 * @param checks See helpers.audit for a list of available checks |
|
732 * @return A promise which resolves to undefined when the checks are complete |
|
733 */ |
|
734 helpers._check = function(options, name, checks) { |
|
735 // A test method to check that all args are assigned in some way |
|
736 var requisition = options.requisition; |
|
737 requisition._args.forEach(function(arg) { |
|
738 if (arg.assignment == null) { |
|
739 assert.ok(false, 'No assignment for ' + arg); |
|
740 } |
|
741 }); |
|
742 |
|
743 if (checks == null) { |
|
744 return promise.resolve(); |
|
745 } |
|
746 |
|
747 var outstanding = []; |
|
748 var suffix = name ? ' (for \'' + name + '\')' : ''; |
|
749 |
|
750 if (!options.isNoDom && 'input' in checks) { |
|
751 assert.is(helpers._actual.input(options), checks.input, 'input' + suffix); |
|
752 } |
|
753 |
|
754 if (!options.isNoDom && 'cursor' in checks) { |
|
755 assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix); |
|
756 } |
|
757 |
|
758 if (!options.isNoDom && 'current' in checks) { |
|
759 assert.is(helpers._actual.current(options), checks.current, 'current' + suffix); |
|
760 } |
|
761 |
|
762 if ('status' in checks) { |
|
763 assert.is(helpers._actual.status(options), checks.status, 'status' + suffix); |
|
764 } |
|
765 |
|
766 if (!options.isNoDom && 'markup' in checks) { |
|
767 assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix); |
|
768 } |
|
769 |
|
770 if (!options.isNoDom && 'hints' in checks) { |
|
771 var hintCheck = function(actualHints) { |
|
772 assert.is(actualHints, checks.hints, 'hints' + suffix); |
|
773 }; |
|
774 outstanding.push(helpers._actual.hints(options).then(hintCheck)); |
|
775 } |
|
776 |
|
777 if (!options.isNoDom && 'predictions' in checks) { |
|
778 var predictionsCheck = function(actualPredictions) { |
|
779 helpers.arrayIs(actualPredictions, |
|
780 checks.predictions, |
|
781 'predictions' + suffix); |
|
782 }; |
|
783 outstanding.push(helpers._actual.predictions(options).then(predictionsCheck)); |
|
784 } |
|
785 |
|
786 if (!options.isNoDom && 'predictionsContains' in checks) { |
|
787 var containsCheck = function(actualPredictions) { |
|
788 checks.predictionsContains.forEach(function(prediction) { |
|
789 var index = actualPredictions.indexOf(prediction); |
|
790 assert.ok(index !== -1, |
|
791 'predictionsContains:' + prediction + suffix); |
|
792 }); |
|
793 }; |
|
794 outstanding.push(helpers._actual.predictions(options).then(containsCheck)); |
|
795 } |
|
796 |
|
797 if ('unassigned' in checks) { |
|
798 helpers.arrayIs(helpers._actual.unassigned(options), |
|
799 checks.unassigned, |
|
800 'unassigned' + suffix); |
|
801 } |
|
802 |
|
803 /* TODO: Fix this |
|
804 if (!options.isNoDom && 'tooltipState' in checks) { |
|
805 assert.is(helpers._actual.tooltipState(options), |
|
806 checks.tooltipState, |
|
807 'tooltipState' + suffix); |
|
808 } |
|
809 */ |
|
810 |
|
811 if (!options.isNoDom && 'outputState' in checks) { |
|
812 assert.is(helpers._actual.outputState(options), |
|
813 checks.outputState, |
|
814 'outputState' + suffix); |
|
815 } |
|
816 |
|
817 if (!options.isNoDom && 'options' in checks) { |
|
818 helpers.arrayIs(helpers._actual.options(options), |
|
819 checks.options, |
|
820 'options' + suffix); |
|
821 } |
|
822 |
|
823 if (!options.isNoDom && 'error' in checks) { |
|
824 assert.is(helpers._actual.message(options), checks.error, 'error' + suffix); |
|
825 } |
|
826 |
|
827 if (checks.args != null) { |
|
828 Object.keys(checks.args).forEach(function(paramName) { |
|
829 var check = checks.args[paramName]; |
|
830 |
|
831 // We allow an 'argument' called 'command' to be the command itself, but |
|
832 // what if the command has a parameter called 'command' (for example, an |
|
833 // 'exec' command)? We default to using the parameter because checking |
|
834 // the command value is less useful |
|
835 var assignment = requisition.getAssignment(paramName); |
|
836 if (assignment == null && paramName === 'command') { |
|
837 assignment = requisition.commandAssignment; |
|
838 } |
|
839 |
|
840 if (assignment == null) { |
|
841 assert.ok(false, 'Unknown arg: ' + paramName + suffix); |
|
842 return; |
|
843 } |
|
844 |
|
845 if ('value' in check) { |
|
846 if (typeof check.value === 'function') { |
|
847 try { |
|
848 check.value(assignment.value); |
|
849 } |
|
850 catch (ex) { |
|
851 assert.ok(false, '' + ex); |
|
852 } |
|
853 } |
|
854 else { |
|
855 assert.is(assignment.value, |
|
856 check.value, |
|
857 'arg.' + paramName + '.value' + suffix); |
|
858 } |
|
859 } |
|
860 |
|
861 if ('name' in check) { |
|
862 assert.is(assignment.value.name, |
|
863 check.name, |
|
864 'arg.' + paramName + '.name' + suffix); |
|
865 } |
|
866 |
|
867 if ('type' in check) { |
|
868 assert.is(assignment.arg.type, |
|
869 check.type, |
|
870 'arg.' + paramName + '.type' + suffix); |
|
871 } |
|
872 |
|
873 if ('arg' in check) { |
|
874 assert.is(assignment.arg.toString(), |
|
875 check.arg, |
|
876 'arg.' + paramName + '.arg' + suffix); |
|
877 } |
|
878 |
|
879 if ('status' in check) { |
|
880 assert.is(assignment.getStatus().toString(), |
|
881 check.status, |
|
882 'arg.' + paramName + '.status' + suffix); |
|
883 } |
|
884 |
|
885 if (!options.isNoDom && 'message' in check) { |
|
886 if (typeof check.message.test === 'function') { |
|
887 assert.ok(check.message.test(assignment.message), |
|
888 'arg.' + paramName + '.message' + suffix); |
|
889 } |
|
890 else { |
|
891 assert.is(assignment.message, |
|
892 check.message, |
|
893 'arg.' + paramName + '.message' + suffix); |
|
894 } |
|
895 } |
|
896 }); |
|
897 } |
|
898 |
|
899 return promise.all(outstanding).then(function() { |
|
900 // Ensure the promise resolves to nothing |
|
901 return undefined; |
|
902 }); |
|
903 }; |
|
904 |
|
905 /** |
|
906 * Helper for helpers.audit() to ensure that all the 'exec' properties work. |
|
907 * See helpers.audit for more information. |
|
908 * @param name The name to use in error messages |
|
909 * @param expected See helpers.audit for a list of available exec checks |
|
910 * @return A promise which resolves to undefined when the checks are complete |
|
911 */ |
|
912 helpers._exec = function(options, name, expected) { |
|
913 var requisition = options.requisition; |
|
914 if (expected == null) { |
|
915 return promise.resolve({}); |
|
916 } |
|
917 |
|
918 var origLogErrors = cli.logErrors; |
|
919 if (expected.error) { |
|
920 cli.logErrors = false; |
|
921 } |
|
922 |
|
923 try { |
|
924 return requisition.exec({ hidden: true }).then(function(output) { |
|
925 if ('type' in expected) { |
|
926 assert.is(output.type, |
|
927 expected.type, |
|
928 'output.type for: ' + name); |
|
929 } |
|
930 |
|
931 if ('error' in expected) { |
|
932 assert.is(output.error, |
|
933 expected.error, |
|
934 'output.error for: ' + name); |
|
935 } |
|
936 |
|
937 if (!('output' in expected)) { |
|
938 return { output: output }; |
|
939 } |
|
940 |
|
941 var context = requisition.conversionContext; |
|
942 var convertPromise; |
|
943 if (options.isNoDom) { |
|
944 convertPromise = output.convert('string', context); |
|
945 } |
|
946 else { |
|
947 convertPromise = output.convert('dom', context).then(function(node) { |
|
948 return node.textContent.trim(); |
|
949 }); |
|
950 } |
|
951 |
|
952 return convertPromise.then(function(textOutput) { |
|
953 var doTest = function(match, against) { |
|
954 // Only log the real textContent if the test fails |
|
955 if (against.match(match) != null) { |
|
956 assert.ok(true, 'html output for \'' + name + '\' ' + |
|
957 'should match /' + (match.source || match) + '/'); |
|
958 } else { |
|
959 assert.ok(false, 'html output for \'' + name + '\' ' + |
|
960 'should match /' + (match.source || match) + '/. ' + |
|
961 'Actual textContent: "' + against + '"'); |
|
962 } |
|
963 }; |
|
964 |
|
965 if (typeof expected.output === 'string') { |
|
966 assert.is(textOutput, |
|
967 expected.output, |
|
968 'html output for ' + name); |
|
969 } |
|
970 else if (Array.isArray(expected.output)) { |
|
971 expected.output.forEach(function(match) { |
|
972 doTest(match, textOutput); |
|
973 }); |
|
974 } |
|
975 else { |
|
976 doTest(expected.output, textOutput); |
|
977 } |
|
978 |
|
979 if (expected.error) { |
|
980 cli.logErrors = origLogErrors; |
|
981 } |
|
982 return { output: output, text: textOutput }; |
|
983 }); |
|
984 }.bind(this)).then(function(data) { |
|
985 if (expected.error) { |
|
986 cli.logErrors = origLogErrors; |
|
987 } |
|
988 |
|
989 return data; |
|
990 }); |
|
991 } |
|
992 catch (ex) { |
|
993 assert.ok(false, 'Failure executing \'' + name + '\': ' + ex); |
|
994 util.errorHandler(ex); |
|
995 |
|
996 if (expected.error) { |
|
997 cli.logErrors = origLogErrors; |
|
998 } |
|
999 return promise.resolve({}); |
|
1000 } |
|
1001 }; |
|
1002 |
|
1003 /** |
|
1004 * Helper to setup the test |
|
1005 */ |
|
1006 helpers._setup = function(options, name, audit) { |
|
1007 if (typeof audit.setup === 'string') { |
|
1008 return helpers.setInput(options, audit.setup); |
|
1009 } |
|
1010 |
|
1011 if (typeof audit.setup === 'function') { |
|
1012 return promise.resolve(audit.setup.call(audit)); |
|
1013 } |
|
1014 |
|
1015 return promise.reject('\'setup\' property must be a string or a function. Is ' + audit.setup); |
|
1016 }; |
|
1017 |
|
1018 /** |
|
1019 * Helper to shutdown the test |
|
1020 */ |
|
1021 helpers._post = function(name, audit, data) { |
|
1022 if (typeof audit.post === 'function') { |
|
1023 return promise.resolve(audit.post.call(audit, data.output, data.text)); |
|
1024 } |
|
1025 return promise.resolve(audit.post); |
|
1026 }; |
|
1027 |
|
1028 /* |
|
1029 * We do some basic response time stats so we can see if we're getting slow |
|
1030 */ |
|
1031 var totalResponseTime = 0; |
|
1032 var averageOver = 0; |
|
1033 var maxResponseTime = 0; |
|
1034 var maxResponseCulprit; |
|
1035 var start; |
|
1036 |
|
1037 /** |
|
1038 * Restart the stats collection process |
|
1039 */ |
|
1040 helpers.resetResponseTimes = function() { |
|
1041 start = new Date().getTime(); |
|
1042 totalResponseTime = 0; |
|
1043 averageOver = 0; |
|
1044 maxResponseTime = 0; |
|
1045 maxResponseCulprit = undefined; |
|
1046 }; |
|
1047 |
|
1048 /** |
|
1049 * Expose an average response time in milliseconds |
|
1050 */ |
|
1051 Object.defineProperty(helpers, 'averageResponseTime', { |
|
1052 get: function() { |
|
1053 return averageOver === 0 ? |
|
1054 undefined : |
|
1055 Math.round(100 * totalResponseTime / averageOver) / 100; |
|
1056 }, |
|
1057 enumerable: true |
|
1058 }); |
|
1059 |
|
1060 /** |
|
1061 * Expose a maximum response time in milliseconds |
|
1062 */ |
|
1063 Object.defineProperty(helpers, 'maxResponseTime', { |
|
1064 get: function() { return Math.round(maxResponseTime * 100) / 100; }, |
|
1065 enumerable: true |
|
1066 }); |
|
1067 |
|
1068 /** |
|
1069 * Expose the name of the test that provided the maximum response time |
|
1070 */ |
|
1071 Object.defineProperty(helpers, 'maxResponseCulprit', { |
|
1072 get: function() { return maxResponseCulprit; }, |
|
1073 enumerable: true |
|
1074 }); |
|
1075 |
|
1076 /** |
|
1077 * Quick summary of the times |
|
1078 */ |
|
1079 Object.defineProperty(helpers, 'timingSummary', { |
|
1080 get: function() { |
|
1081 var elapsed = (new Date().getTime() - start) / 1000; |
|
1082 return 'Total ' + elapsed + 's, ' + |
|
1083 'ave response ' + helpers.averageResponseTime + 'ms, ' + |
|
1084 'max response ' + helpers.maxResponseTime + 'ms ' + |
|
1085 'from \'' + helpers.maxResponseCulprit + '\''; |
|
1086 }, |
|
1087 enumerable: true |
|
1088 }); |
|
1089 |
|
1090 /** |
|
1091 * A way of turning a set of tests into something more declarative, this helps |
|
1092 * to allow tests to be asynchronous. |
|
1093 * @param audits An array of objects each of which contains: |
|
1094 * - setup: string/function to be called to set the test up. |
|
1095 * If audit is a string then it is passed to helpers.setInput(). |
|
1096 * If audit is a function then it is executed. The tests will wait while |
|
1097 * tests that return promises complete. |
|
1098 * - name: For debugging purposes. If name is undefined, and 'setup' |
|
1099 * is a string then the setup value will be used automatically |
|
1100 * - skipIf: A function to define if the test should be skipped. Useful for |
|
1101 * excluding tests from certain environments (e.g. nodom, firefox, etc). |
|
1102 * The name of the test will be used in log messages noting the skip |
|
1103 * See helpers.reason for pre-defined skip functions. The skip function must |
|
1104 * be synchronous, and will be passed the test options object. |
|
1105 * - skipRemainingIf: A function to skip all the remaining audits in this set. |
|
1106 * See skipIf for details of how skip functions work. |
|
1107 * - check: Check data. Available checks: |
|
1108 * - input: The text displayed in the input field |
|
1109 * - cursor: The position of the start of the cursor |
|
1110 * - status: One of 'VALID', 'ERROR', 'INCOMPLETE' |
|
1111 * - hints: The hint text, i.e. a concatenation of the directTabText, the |
|
1112 * emptyParameters and the arrowTabText. The text as inserted into the UI |
|
1113 * will include NBSP and Unicode RARR characters, these should be |
|
1114 * represented using normal space and '->' for the arrow |
|
1115 * - markup: What state should the error markup be in. e.g. 'VVVIIIEEE' |
|
1116 * - args: Maps of checks to make against the arguments: |
|
1117 * - value: i.e. assignment.value (which ignores defaultValue) |
|
1118 * - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned |
|
1119 * Care should be taken with this since it's something of an |
|
1120 * implementation detail |
|
1121 * - arg: The toString value of the argument |
|
1122 * - status: i.e. assignment.getStatus |
|
1123 * - message: i.e. assignment.message |
|
1124 * - name: For commands - checks assignment.value.name |
|
1125 * - exec: Object to indicate we should execute the command and check the |
|
1126 * results. Available checks: |
|
1127 * - output: A string, RegExp or array of RegExps to compare with the output |
|
1128 * If typeof output is a string then the output should be exactly equal |
|
1129 * to the given string. If the type of output is a RegExp or array of |
|
1130 * RegExps then the output should match all RegExps |
|
1131 * - post: Function to be called after the checks have been run |
|
1132 */ |
|
1133 helpers.audit = function(options, audits) { |
|
1134 checkOptions(options); |
|
1135 var skipReason = null; |
|
1136 return util.promiseEach(audits, function(audit) { |
|
1137 var name = audit.name; |
|
1138 if (name == null && typeof audit.setup === 'string') { |
|
1139 name = audit.setup; |
|
1140 } |
|
1141 |
|
1142 if (assert.testLogging) { |
|
1143 log('- START \'' + name + '\' in ' + assert.currentTest); |
|
1144 } |
|
1145 |
|
1146 if (audit.skipRemainingIf) { |
|
1147 var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ? |
|
1148 audit.skipRemainingIf(options) : |
|
1149 !!audit.skipRemainingIf; |
|
1150 if (skipRemainingIf) { |
|
1151 skipReason = audit.skipRemainingIf.name ? |
|
1152 'due to ' + audit.skipRemainingIf.name : |
|
1153 ''; |
|
1154 assert.log('Skipped ' + name + ' ' + skipReason); |
|
1155 return promise.resolve(undefined); |
|
1156 } |
|
1157 } |
|
1158 |
|
1159 if (audit.skipIf) { |
|
1160 var skip = (typeof audit.skipIf === 'function') ? |
|
1161 audit.skipIf(options) : |
|
1162 !!audit.skipIf; |
|
1163 if (skip) { |
|
1164 var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : ''; |
|
1165 assert.log('Skipped ' + name + ' ' + reason); |
|
1166 return promise.resolve(undefined); |
|
1167 } |
|
1168 } |
|
1169 |
|
1170 if (skipReason != null) { |
|
1171 assert.log('Skipped ' + name + ' ' + skipReason); |
|
1172 return promise.resolve(undefined); |
|
1173 } |
|
1174 |
|
1175 var start = new Date().getTime(); |
|
1176 |
|
1177 var setupDone = helpers._setup(options, name, audit); |
|
1178 return setupDone.then(function(chunkLen) { |
|
1179 if (typeof chunkLen !== 'number') { |
|
1180 chunkLen = 1; |
|
1181 } |
|
1182 |
|
1183 // Nasty hack to allow us to auto-skip tests where we're actually testing |
|
1184 // a key-sequence (i.e. targeting terminal.js) when there is no terminal |
|
1185 if (chunkLen === -1) { |
|
1186 assert.log('Skipped ' + name + ' ' + skipReason); |
|
1187 return promise.resolve(undefined); |
|
1188 } |
|
1189 |
|
1190 if (assert.currentTest) { |
|
1191 var responseTime = (new Date().getTime() - start) / chunkLen; |
|
1192 totalResponseTime += responseTime; |
|
1193 if (responseTime > maxResponseTime) { |
|
1194 maxResponseTime = responseTime; |
|
1195 maxResponseCulprit = assert.currentTest + '/' + name; |
|
1196 } |
|
1197 averageOver++; |
|
1198 } |
|
1199 |
|
1200 var checkDone = helpers._check(options, name, audit.check); |
|
1201 return checkDone.then(function() { |
|
1202 var execDone = helpers._exec(options, name, audit.exec); |
|
1203 return execDone.then(function(data) { |
|
1204 return helpers._post(name, audit, data).then(function() { |
|
1205 if (assert.testLogging) { |
|
1206 log('- END \'' + name + '\' in ' + assert.currentTest); |
|
1207 } |
|
1208 }); |
|
1209 }); |
|
1210 }); |
|
1211 }); |
|
1212 }).then(function() { |
|
1213 return options.automator.setInput(''); |
|
1214 }); |
|
1215 }; |
|
1216 |
|
1217 /** |
|
1218 * Compare 2 arrays. |
|
1219 */ |
|
1220 helpers.arrayIs = function(actual, expected, message) { |
|
1221 assert.ok(Array.isArray(actual), 'actual is not an array: ' + message); |
|
1222 assert.ok(Array.isArray(expected), 'expected is not an array: ' + message); |
|
1223 |
|
1224 if (!Array.isArray(actual) || !Array.isArray(expected)) { |
|
1225 return; |
|
1226 } |
|
1227 |
|
1228 assert.is(actual.length, expected.length, 'array length: ' + message); |
|
1229 |
|
1230 for (var i = 0; i < actual.length && i < expected.length; i++) { |
|
1231 assert.is(actual[i], expected[i], 'member[' + i + ']: ' + message); |
|
1232 } |
|
1233 }; |
|
1234 |
|
1235 /** |
|
1236 * A quick helper to log to the correct place |
|
1237 */ |
|
1238 function log(message) { |
|
1239 if (typeof info === 'function') { |
|
1240 info(message); |
|
1241 } |
|
1242 else { |
|
1243 console.log(message); |
|
1244 } |
|
1245 } |