| |
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 }; |