|
1 // A common module to run tests on the AccessFu module |
|
2 |
|
3 'use strict'; |
|
4 |
|
5 /*global isDeeply, getMainChromeWindow, SimpleTest, SpecialPowers, Logger, |
|
6 AccessFu, Utils, addMessageListener, currentTabDocument, currentBrowser*/ |
|
7 |
|
8 /** |
|
9 * A global variable holding an array of test functions. |
|
10 */ |
|
11 var gTestFuncs = []; |
|
12 /** |
|
13 * A global Iterator for the array of test functions. |
|
14 */ |
|
15 var gIterator; |
|
16 |
|
17 Components.utils.import('resource://gre/modules/Services.jsm'); |
|
18 Components.utils.import("resource://gre/modules/accessibility/Utils.jsm"); |
|
19 Components.utils.import("resource://gre/modules/accessibility/EventManager.jsm"); |
|
20 Components.utils.import("resource://gre/modules/accessibility/Gestures.jsm"); |
|
21 |
|
22 const dwellThreshold = GestureSettings.dwellThreshold; |
|
23 const swipeMaxDuration = GestureSettings.swipeMaxDuration; |
|
24 const maxConsecutiveGestureDelay = GestureSettings.maxConsecutiveGestureDelay; |
|
25 |
|
26 // https://bugzilla.mozilla.org/show_bug.cgi?id=1001945 - sometimes |
|
27 // SimpleTest.executeSoon timeout is bigger than the timer settings in |
|
28 // GestureSettings that causes intermittents. |
|
29 GestureSettings.dwellThreshold = dwellThreshold * 10; |
|
30 GestureSettings.swipeMaxDuration = swipeMaxDuration * 10; |
|
31 GestureSettings.maxConsecutiveGestureDelay = maxConsecutiveGestureDelay * 10; |
|
32 |
|
33 var AccessFuTest = { |
|
34 |
|
35 addFunc: function AccessFuTest_addFunc(aFunc) { |
|
36 if (aFunc) { |
|
37 gTestFuncs.push(aFunc); |
|
38 } |
|
39 }, |
|
40 |
|
41 _registerListener: function AccessFuTest__registerListener(aWaitForMessage, aListenerFunc) { |
|
42 var listener = { |
|
43 observe: function observe(aMessage) { |
|
44 // Ignore unexpected messages. |
|
45 if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) { |
|
46 return; |
|
47 } |
|
48 if (aMessage.message.indexOf(aWaitForMessage) < 0) { |
|
49 return; |
|
50 } |
|
51 aListenerFunc.apply(listener); |
|
52 } |
|
53 }; |
|
54 Services.console.registerListener(listener); |
|
55 return listener; |
|
56 }, |
|
57 |
|
58 on_log: function AccessFuTest_on_log(aWaitForMessage, aListenerFunc) { |
|
59 return this._registerListener(aWaitForMessage, aListenerFunc); |
|
60 }, |
|
61 |
|
62 off_log: function AccessFuTest_off_log(aListener) { |
|
63 Services.console.unregisterListener(aListener); |
|
64 }, |
|
65 |
|
66 once_log: function AccessFuTest_once_log(aWaitForMessage, aListenerFunc) { |
|
67 return this._registerListener(aWaitForMessage, |
|
68 function listenAndUnregister() { |
|
69 Services.console.unregisterListener(this); |
|
70 aListenerFunc(); |
|
71 }); |
|
72 }, |
|
73 |
|
74 _addObserver: function AccessFuTest__addObserver(aWaitForData, aListener) { |
|
75 var listener = function listener(aSubject, aTopic, aData) { |
|
76 var data = JSON.parse(aData)[1]; |
|
77 // Ignore non-relevant outputs. |
|
78 if (!data) { |
|
79 return; |
|
80 } |
|
81 isDeeply(data.details.actions, aWaitForData, "Data is correct"); |
|
82 aListener.apply(listener); |
|
83 }; |
|
84 Services.obs.addObserver(listener, 'accessfu-output', false); |
|
85 return listener; |
|
86 }, |
|
87 |
|
88 on: function AccessFuTest_on(aWaitForData, aListener) { |
|
89 return this._addObserver(aWaitForData, aListener); |
|
90 }, |
|
91 |
|
92 off: function AccessFuTest_off(aListener) { |
|
93 Services.obs.removeObserver(aListener, 'accessfu-output'); |
|
94 }, |
|
95 |
|
96 once: function AccessFuTest_once(aWaitForData, aListener) { |
|
97 return this._addObserver(aWaitForData, function observerAndRemove() { |
|
98 Services.obs.removeObserver(this, 'accessfu-output'); |
|
99 aListener(); |
|
100 }); |
|
101 }, |
|
102 |
|
103 _waitForExplicitFinish: false, |
|
104 |
|
105 waitForExplicitFinish: function AccessFuTest_waitForExplicitFinish() { |
|
106 this._waitForExplicitFinish = true; |
|
107 }, |
|
108 |
|
109 finish: function AccessFuTest_finish() { |
|
110 // Disable the console service logging. |
|
111 Logger.test = false; |
|
112 Logger.logLevel = Logger.INFO; |
|
113 // Reset Gesture Settings. |
|
114 GestureSettings.dwellThreshold = dwellThreshold; |
|
115 GestureSettings.swipeMaxDuration = swipeMaxDuration; |
|
116 GestureSettings.maxConsecutiveGestureDelay = maxConsecutiveGestureDelay; |
|
117 // Finish through idle callback to let AccessFu._disable complete. |
|
118 SimpleTest.executeSoon(function () { |
|
119 AccessFu.detach(); |
|
120 SimpleTest.finish(); |
|
121 }); |
|
122 }, |
|
123 |
|
124 nextTest: function AccessFuTest_nextTest() { |
|
125 var testFunc; |
|
126 try { |
|
127 // Get the next test function from the iterator. If none left, |
|
128 // StopIteration exception is thrown. |
|
129 testFunc = gIterator.next()[1]; |
|
130 } catch (ex) { |
|
131 // StopIteration exception. |
|
132 this.finish(); |
|
133 return; |
|
134 } |
|
135 testFunc(); |
|
136 }, |
|
137 |
|
138 runTests: function AccessFuTest_runTests() { |
|
139 if (gTestFuncs.length === 0) { |
|
140 ok(false, "No tests specified!"); |
|
141 SimpleTest.finish(); |
|
142 return; |
|
143 } |
|
144 |
|
145 // Create an Iterator for gTestFuncs array. |
|
146 gIterator = Iterator(gTestFuncs); // jshint ignore:line |
|
147 |
|
148 // Start AccessFu and put it in stand-by. |
|
149 Components.utils.import("resource://gre/modules/accessibility/AccessFu.jsm"); |
|
150 |
|
151 AccessFu.attach(getMainChromeWindow(window)); |
|
152 |
|
153 AccessFu.readyCallback = function readyCallback() { |
|
154 // Enable logging to the console service. |
|
155 Logger.test = true; |
|
156 Logger.logLevel = Logger.DEBUG; |
|
157 }; |
|
158 |
|
159 SpecialPowers.pushPrefEnv({ |
|
160 'set': [['accessibility.accessfu.notify_output', 1], |
|
161 ['dom.mozSettings.enabled', true]] |
|
162 }, function () { |
|
163 if (AccessFuTest._waitForExplicitFinish) { |
|
164 // Run all test functions asynchronously. |
|
165 AccessFuTest.nextTest(); |
|
166 } else { |
|
167 // Run all test functions synchronously. |
|
168 [testFunc() for (testFunc of gTestFuncs)]; // jshint ignore:line |
|
169 AccessFuTest.finish(); |
|
170 } |
|
171 }); |
|
172 } |
|
173 }; |
|
174 |
|
175 function AccessFuContentTest(aFuncResultPairs) { |
|
176 this.queue = aFuncResultPairs; |
|
177 } |
|
178 |
|
179 AccessFuContentTest.prototype = { |
|
180 currentPair: null, |
|
181 |
|
182 start: function(aFinishedCallback) { |
|
183 Logger.logLevel = Logger.DEBUG; |
|
184 this.finishedCallback = aFinishedCallback; |
|
185 var self = this; |
|
186 |
|
187 // Get top content message manager, and set it up. |
|
188 this.mms = [Utils.getMessageManager(currentBrowser())]; |
|
189 this.setupMessageManager(this.mms[0], function () { |
|
190 // Get child message managers and set them up |
|
191 var frames = currentTabDocument().querySelectorAll('iframe'); |
|
192 if (frames.length === 0) { |
|
193 self.pump(); |
|
194 return; |
|
195 } |
|
196 |
|
197 var toSetup = 0; |
|
198 for (var i = 0; i < frames.length; i++ ) { |
|
199 var mm = Utils.getMessageManager(frames[i]); |
|
200 if (mm) { |
|
201 toSetup++; |
|
202 self.mms.push(mm); |
|
203 self.setupMessageManager(mm, function () { |
|
204 if (--toSetup === 0) { |
|
205 // All message managers are loaded and ready to go. |
|
206 self.pump(); |
|
207 } |
|
208 }); |
|
209 } |
|
210 } |
|
211 }); |
|
212 }, |
|
213 |
|
214 finish: function() { |
|
215 Logger.logLevel = Logger.INFO; |
|
216 for (var mm of this.mms) { |
|
217 mm.sendAsyncMessage('AccessFu:Stop'); |
|
218 } |
|
219 if (this.finishedCallback) { |
|
220 this.finishedCallback(); |
|
221 } |
|
222 }, |
|
223 |
|
224 setupMessageManager: function (aMessageManager, aCallback) { |
|
225 function contentScript() { |
|
226 addMessageListener('AccessFuTest:Focus', function (aMessage) { |
|
227 var elem = content.document.querySelector(aMessage.json.selector); |
|
228 if (elem) { |
|
229 if (aMessage.json.blur) { |
|
230 elem.blur(); |
|
231 } else { |
|
232 elem.focus(); |
|
233 } |
|
234 } |
|
235 }); |
|
236 } |
|
237 |
|
238 aMessageManager.addMessageListener('AccessFu:Present', this); |
|
239 aMessageManager.addMessageListener('AccessFu:CursorCleared', this); |
|
240 aMessageManager.addMessageListener('AccessFu:Ready', function () { |
|
241 aMessageManager.addMessageListener('AccessFu:ContentStarted', aCallback); |
|
242 aMessageManager.sendAsyncMessage('AccessFu:Start', |
|
243 { buildApp: 'browser', |
|
244 androidSdkVersion: Utils.AndroidSdkVersion, |
|
245 logLevel: 'DEBUG' }); |
|
246 }); |
|
247 |
|
248 aMessageManager.loadFrameScript( |
|
249 'chrome://global/content/accessibility/content-script.js', false); |
|
250 aMessageManager.loadFrameScript( |
|
251 'data:,(' + contentScript.toString() + ')();', false); |
|
252 }, |
|
253 |
|
254 pump: function() { |
|
255 this.currentPair = this.queue.shift(); |
|
256 |
|
257 if (this.currentPair) { |
|
258 if (this.currentPair[0] instanceof Function) { |
|
259 this.currentPair[0](this.mms[0]); |
|
260 } else if (this.currentPair[0]) { |
|
261 this.mms[0].sendAsyncMessage(this.currentPair[0].name, |
|
262 this.currentPair[0].json); |
|
263 } |
|
264 |
|
265 if (!this.currentPair[1]) { |
|
266 this.pump(); |
|
267 } |
|
268 } else { |
|
269 this.finish(); |
|
270 } |
|
271 }, |
|
272 |
|
273 receiveMessage: function(aMessage) { |
|
274 if (!this.currentPair) { |
|
275 return; |
|
276 } |
|
277 |
|
278 var expected = this.currentPair[1] || {}; |
|
279 |
|
280 // |expected| can simply be a name of a message, no more further testing. |
|
281 if (aMessage.name === expected) { |
|
282 ok(true, 'Received ' + expected); |
|
283 this.pump(); |
|
284 return; |
|
285 } |
|
286 |
|
287 var speech = this.extractUtterance(aMessage.json); |
|
288 var android = this.extractAndroid(aMessage.json, expected.android); |
|
289 if ((speech && expected.speak) || (android && expected.android)) { |
|
290 if (expected.speak) { |
|
291 (SimpleTest[expected.speak_checkFunc] || is)(speech, expected.speak, |
|
292 '"' + speech + '" spoken'); |
|
293 } |
|
294 |
|
295 if (expected.android) { |
|
296 var checkFunc = SimpleTest[expected.android_checkFunc] || ok; |
|
297 checkFunc.apply(SimpleTest, |
|
298 this.lazyCompare(android, expected.android)); |
|
299 } |
|
300 |
|
301 this.pump(); |
|
302 } |
|
303 |
|
304 }, |
|
305 |
|
306 lazyCompare: function lazyCompare(aReceived, aExpected) { |
|
307 var matches = true; |
|
308 var delta = []; |
|
309 for (var attr in aExpected) { |
|
310 var expected = aExpected[attr]; |
|
311 var received = aReceived !== undefined ? aReceived[attr] : null; |
|
312 if (typeof expected === 'object') { |
|
313 var [childMatches, childDelta] = this.lazyCompare(received, expected); |
|
314 if (!childMatches) { |
|
315 delta.push(attr + ' [ ' + childDelta + ' ]'); |
|
316 matches = false; |
|
317 } |
|
318 } else { |
|
319 if (received !== expected) { |
|
320 delta.push( |
|
321 attr + ' [ expected ' + expected + ' got ' + received + ' ]'); |
|
322 matches = false; |
|
323 } |
|
324 } |
|
325 } |
|
326 return [matches, delta.join(' ')]; |
|
327 }, |
|
328 |
|
329 extractUtterance: function(aData) { |
|
330 if (!aData) { |
|
331 return null; |
|
332 } |
|
333 |
|
334 for (var output of aData) { |
|
335 if (output && output.type === 'Speech') { |
|
336 for (var action of output.details.actions) { |
|
337 if (action && action.method == 'speak') { |
|
338 return action.data; |
|
339 } |
|
340 } |
|
341 } |
|
342 } |
|
343 |
|
344 return null; |
|
345 }, |
|
346 |
|
347 extractAndroid: function(aData, aExpectedEvents) { |
|
348 for (var output of aData) { |
|
349 if (output && output.type === 'Android') { |
|
350 for (var i in output.details) { |
|
351 // Only extract if event types match expected event types. |
|
352 var exp = aExpectedEvents ? aExpectedEvents[i] : null; |
|
353 if (!exp || (output.details[i].eventType !== exp.eventType)) { |
|
354 return null; |
|
355 } |
|
356 } |
|
357 return output.details; |
|
358 } |
|
359 } |
|
360 |
|
361 return null; |
|
362 } |
|
363 }; |
|
364 |
|
365 // Common content messages |
|
366 |
|
367 var ContentMessages = { |
|
368 simpleMoveFirst: { |
|
369 name: 'AccessFu:MoveCursor', |
|
370 json: { |
|
371 action: 'moveFirst', |
|
372 rule: 'Simple', |
|
373 inputType: 'gesture', |
|
374 origin: 'top' |
|
375 } |
|
376 }, |
|
377 |
|
378 simpleMoveLast: { |
|
379 name: 'AccessFu:MoveCursor', |
|
380 json: { |
|
381 action: 'moveLast', |
|
382 rule: 'Simple', |
|
383 inputType: 'gesture', |
|
384 origin: 'top' |
|
385 } |
|
386 }, |
|
387 |
|
388 simpleMoveNext: { |
|
389 name: 'AccessFu:MoveCursor', |
|
390 json: { |
|
391 action: 'moveNext', |
|
392 rule: 'Simple', |
|
393 inputType: 'gesture', |
|
394 origin: 'top' |
|
395 } |
|
396 }, |
|
397 |
|
398 simpleMovePrevious: { |
|
399 name: 'AccessFu:MoveCursor', |
|
400 json: { |
|
401 action: 'movePrevious', |
|
402 rule: 'Simple', |
|
403 inputType: 'gesture', |
|
404 origin: 'top' |
|
405 } |
|
406 }, |
|
407 |
|
408 clearCursor: { |
|
409 name: 'AccessFu:ClearCursor', |
|
410 json: { |
|
411 origin: 'top' |
|
412 } |
|
413 }, |
|
414 |
|
415 adjustRangeUp: { |
|
416 name: 'AccessFu:AdjustRange', |
|
417 json: { |
|
418 origin: 'top', |
|
419 direction: 'backward' |
|
420 } |
|
421 }, |
|
422 |
|
423 adjustRangeDown: { |
|
424 name: 'AccessFu:AdjustRange', |
|
425 json: { |
|
426 origin: 'top', |
|
427 direction: 'forward' |
|
428 } |
|
429 }, |
|
430 |
|
431 focusSelector: function focusSelector(aSelector, aBlur) { |
|
432 return { |
|
433 name: 'AccessFuTest:Focus', |
|
434 json: { |
|
435 selector: aSelector, |
|
436 blur: aBlur |
|
437 } |
|
438 }; |
|
439 }, |
|
440 |
|
441 activateCurrent: function activateCurrent(aOffset) { |
|
442 return { |
|
443 name: 'AccessFu:Activate', |
|
444 json: { |
|
445 origin: 'top', |
|
446 offset: aOffset |
|
447 } |
|
448 }; |
|
449 }, |
|
450 |
|
451 moveNextBy: function moveNextBy(aGranularity) { |
|
452 return { |
|
453 name: 'AccessFu:MoveByGranularity', |
|
454 json: { |
|
455 direction: 'Next', |
|
456 granularity: this._granularityMap[aGranularity] |
|
457 } |
|
458 }; |
|
459 }, |
|
460 |
|
461 movePreviousBy: function movePreviousBy(aGranularity) { |
|
462 return { |
|
463 name: 'AccessFu:MoveByGranularity', |
|
464 json: { |
|
465 direction: 'Previous', |
|
466 granularity: this._granularityMap[aGranularity] |
|
467 } |
|
468 }; |
|
469 }, |
|
470 |
|
471 moveCaretNextBy: function moveCaretNextBy(aGranularity) { |
|
472 return { |
|
473 name: 'AccessFu:MoveCaret', |
|
474 json: { |
|
475 direction: 'Next', |
|
476 granularity: this._granularityMap[aGranularity] |
|
477 } |
|
478 }; |
|
479 }, |
|
480 |
|
481 moveCaretPreviousBy: function moveCaretPreviousBy(aGranularity) { |
|
482 return { |
|
483 name: 'AccessFu:MoveCaret', |
|
484 json: { |
|
485 direction: 'Previous', |
|
486 granularity: this._granularityMap[aGranularity] |
|
487 } |
|
488 }; |
|
489 }, |
|
490 |
|
491 _granularityMap: { |
|
492 'character': 1, // MOVEMENT_GRANULARITY_CHARACTER |
|
493 'word': 2, // MOVEMENT_GRANULARITY_WORD |
|
494 'paragraph': 8 // MOVEMENT_GRANULARITY_PARAGRAPH |
|
495 } |
|
496 }; |
|
497 |
|
498 var AndroidEvent = { |
|
499 VIEW_CLICKED: 0x01, |
|
500 VIEW_LONG_CLICKED: 0x02, |
|
501 VIEW_SELECTED: 0x04, |
|
502 VIEW_FOCUSED: 0x08, |
|
503 VIEW_TEXT_CHANGED: 0x10, |
|
504 WINDOW_STATE_CHANGED: 0x20, |
|
505 VIEW_HOVER_ENTER: 0x80, |
|
506 VIEW_HOVER_EXIT: 0x100, |
|
507 VIEW_SCROLLED: 0x1000, |
|
508 VIEW_TEXT_SELECTION_CHANGED: 0x2000, |
|
509 ANNOUNCEMENT: 0x4000, |
|
510 VIEW_ACCESSIBILITY_FOCUSED: 0x8000, |
|
511 VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000 |
|
512 }; |