michael@0: // A common module to run tests on the AccessFu module michael@0: michael@0: 'use strict'; michael@0: michael@0: /*global isDeeply, getMainChromeWindow, SimpleTest, SpecialPowers, Logger, michael@0: AccessFu, Utils, addMessageListener, currentTabDocument, currentBrowser*/ michael@0: michael@0: /** michael@0: * A global variable holding an array of test functions. michael@0: */ michael@0: var gTestFuncs = []; michael@0: /** michael@0: * A global Iterator for the array of test functions. michael@0: */ michael@0: var gIterator; michael@0: michael@0: Components.utils.import('resource://gre/modules/Services.jsm'); michael@0: Components.utils.import("resource://gre/modules/accessibility/Utils.jsm"); michael@0: Components.utils.import("resource://gre/modules/accessibility/EventManager.jsm"); michael@0: Components.utils.import("resource://gre/modules/accessibility/Gestures.jsm"); michael@0: michael@0: const dwellThreshold = GestureSettings.dwellThreshold; michael@0: const swipeMaxDuration = GestureSettings.swipeMaxDuration; michael@0: const maxConsecutiveGestureDelay = GestureSettings.maxConsecutiveGestureDelay; michael@0: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=1001945 - sometimes michael@0: // SimpleTest.executeSoon timeout is bigger than the timer settings in michael@0: // GestureSettings that causes intermittents. michael@0: GestureSettings.dwellThreshold = dwellThreshold * 10; michael@0: GestureSettings.swipeMaxDuration = swipeMaxDuration * 10; michael@0: GestureSettings.maxConsecutiveGestureDelay = maxConsecutiveGestureDelay * 10; michael@0: michael@0: var AccessFuTest = { michael@0: michael@0: addFunc: function AccessFuTest_addFunc(aFunc) { michael@0: if (aFunc) { michael@0: gTestFuncs.push(aFunc); michael@0: } michael@0: }, michael@0: michael@0: _registerListener: function AccessFuTest__registerListener(aWaitForMessage, aListenerFunc) { michael@0: var listener = { michael@0: observe: function observe(aMessage) { michael@0: // Ignore unexpected messages. michael@0: if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) { michael@0: return; michael@0: } michael@0: if (aMessage.message.indexOf(aWaitForMessage) < 0) { michael@0: return; michael@0: } michael@0: aListenerFunc.apply(listener); michael@0: } michael@0: }; michael@0: Services.console.registerListener(listener); michael@0: return listener; michael@0: }, michael@0: michael@0: on_log: function AccessFuTest_on_log(aWaitForMessage, aListenerFunc) { michael@0: return this._registerListener(aWaitForMessage, aListenerFunc); michael@0: }, michael@0: michael@0: off_log: function AccessFuTest_off_log(aListener) { michael@0: Services.console.unregisterListener(aListener); michael@0: }, michael@0: michael@0: once_log: function AccessFuTest_once_log(aWaitForMessage, aListenerFunc) { michael@0: return this._registerListener(aWaitForMessage, michael@0: function listenAndUnregister() { michael@0: Services.console.unregisterListener(this); michael@0: aListenerFunc(); michael@0: }); michael@0: }, michael@0: michael@0: _addObserver: function AccessFuTest__addObserver(aWaitForData, aListener) { michael@0: var listener = function listener(aSubject, aTopic, aData) { michael@0: var data = JSON.parse(aData)[1]; michael@0: // Ignore non-relevant outputs. michael@0: if (!data) { michael@0: return; michael@0: } michael@0: isDeeply(data.details.actions, aWaitForData, "Data is correct"); michael@0: aListener.apply(listener); michael@0: }; michael@0: Services.obs.addObserver(listener, 'accessfu-output', false); michael@0: return listener; michael@0: }, michael@0: michael@0: on: function AccessFuTest_on(aWaitForData, aListener) { michael@0: return this._addObserver(aWaitForData, aListener); michael@0: }, michael@0: michael@0: off: function AccessFuTest_off(aListener) { michael@0: Services.obs.removeObserver(aListener, 'accessfu-output'); michael@0: }, michael@0: michael@0: once: function AccessFuTest_once(aWaitForData, aListener) { michael@0: return this._addObserver(aWaitForData, function observerAndRemove() { michael@0: Services.obs.removeObserver(this, 'accessfu-output'); michael@0: aListener(); michael@0: }); michael@0: }, michael@0: michael@0: _waitForExplicitFinish: false, michael@0: michael@0: waitForExplicitFinish: function AccessFuTest_waitForExplicitFinish() { michael@0: this._waitForExplicitFinish = true; michael@0: }, michael@0: michael@0: finish: function AccessFuTest_finish() { michael@0: // Disable the console service logging. michael@0: Logger.test = false; michael@0: Logger.logLevel = Logger.INFO; michael@0: // Reset Gesture Settings. michael@0: GestureSettings.dwellThreshold = dwellThreshold; michael@0: GestureSettings.swipeMaxDuration = swipeMaxDuration; michael@0: GestureSettings.maxConsecutiveGestureDelay = maxConsecutiveGestureDelay; michael@0: // Finish through idle callback to let AccessFu._disable complete. michael@0: SimpleTest.executeSoon(function () { michael@0: AccessFu.detach(); michael@0: SimpleTest.finish(); michael@0: }); michael@0: }, michael@0: michael@0: nextTest: function AccessFuTest_nextTest() { michael@0: var testFunc; michael@0: try { michael@0: // Get the next test function from the iterator. If none left, michael@0: // StopIteration exception is thrown. michael@0: testFunc = gIterator.next()[1]; michael@0: } catch (ex) { michael@0: // StopIteration exception. michael@0: this.finish(); michael@0: return; michael@0: } michael@0: testFunc(); michael@0: }, michael@0: michael@0: runTests: function AccessFuTest_runTests() { michael@0: if (gTestFuncs.length === 0) { michael@0: ok(false, "No tests specified!"); michael@0: SimpleTest.finish(); michael@0: return; michael@0: } michael@0: michael@0: // Create an Iterator for gTestFuncs array. michael@0: gIterator = Iterator(gTestFuncs); // jshint ignore:line michael@0: michael@0: // Start AccessFu and put it in stand-by. michael@0: Components.utils.import("resource://gre/modules/accessibility/AccessFu.jsm"); michael@0: michael@0: AccessFu.attach(getMainChromeWindow(window)); michael@0: michael@0: AccessFu.readyCallback = function readyCallback() { michael@0: // Enable logging to the console service. michael@0: Logger.test = true; michael@0: Logger.logLevel = Logger.DEBUG; michael@0: }; michael@0: michael@0: SpecialPowers.pushPrefEnv({ michael@0: 'set': [['accessibility.accessfu.notify_output', 1], michael@0: ['dom.mozSettings.enabled', true]] michael@0: }, function () { michael@0: if (AccessFuTest._waitForExplicitFinish) { michael@0: // Run all test functions asynchronously. michael@0: AccessFuTest.nextTest(); michael@0: } else { michael@0: // Run all test functions synchronously. michael@0: [testFunc() for (testFunc of gTestFuncs)]; // jshint ignore:line michael@0: AccessFuTest.finish(); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: function AccessFuContentTest(aFuncResultPairs) { michael@0: this.queue = aFuncResultPairs; michael@0: } michael@0: michael@0: AccessFuContentTest.prototype = { michael@0: currentPair: null, michael@0: michael@0: start: function(aFinishedCallback) { michael@0: Logger.logLevel = Logger.DEBUG; michael@0: this.finishedCallback = aFinishedCallback; michael@0: var self = this; michael@0: michael@0: // Get top content message manager, and set it up. michael@0: this.mms = [Utils.getMessageManager(currentBrowser())]; michael@0: this.setupMessageManager(this.mms[0], function () { michael@0: // Get child message managers and set them up michael@0: var frames = currentTabDocument().querySelectorAll('iframe'); michael@0: if (frames.length === 0) { michael@0: self.pump(); michael@0: return; michael@0: } michael@0: michael@0: var toSetup = 0; michael@0: for (var i = 0; i < frames.length; i++ ) { michael@0: var mm = Utils.getMessageManager(frames[i]); michael@0: if (mm) { michael@0: toSetup++; michael@0: self.mms.push(mm); michael@0: self.setupMessageManager(mm, function () { michael@0: if (--toSetup === 0) { michael@0: // All message managers are loaded and ready to go. michael@0: self.pump(); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: finish: function() { michael@0: Logger.logLevel = Logger.INFO; michael@0: for (var mm of this.mms) { michael@0: mm.sendAsyncMessage('AccessFu:Stop'); michael@0: } michael@0: if (this.finishedCallback) { michael@0: this.finishedCallback(); michael@0: } michael@0: }, michael@0: michael@0: setupMessageManager: function (aMessageManager, aCallback) { michael@0: function contentScript() { michael@0: addMessageListener('AccessFuTest:Focus', function (aMessage) { michael@0: var elem = content.document.querySelector(aMessage.json.selector); michael@0: if (elem) { michael@0: if (aMessage.json.blur) { michael@0: elem.blur(); michael@0: } else { michael@0: elem.focus(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: aMessageManager.addMessageListener('AccessFu:Present', this); michael@0: aMessageManager.addMessageListener('AccessFu:CursorCleared', this); michael@0: aMessageManager.addMessageListener('AccessFu:Ready', function () { michael@0: aMessageManager.addMessageListener('AccessFu:ContentStarted', aCallback); michael@0: aMessageManager.sendAsyncMessage('AccessFu:Start', michael@0: { buildApp: 'browser', michael@0: androidSdkVersion: Utils.AndroidSdkVersion, michael@0: logLevel: 'DEBUG' }); michael@0: }); michael@0: michael@0: aMessageManager.loadFrameScript( michael@0: 'chrome://global/content/accessibility/content-script.js', false); michael@0: aMessageManager.loadFrameScript( michael@0: 'data:,(' + contentScript.toString() + ')();', false); michael@0: }, michael@0: michael@0: pump: function() { michael@0: this.currentPair = this.queue.shift(); michael@0: michael@0: if (this.currentPair) { michael@0: if (this.currentPair[0] instanceof Function) { michael@0: this.currentPair[0](this.mms[0]); michael@0: } else if (this.currentPair[0]) { michael@0: this.mms[0].sendAsyncMessage(this.currentPair[0].name, michael@0: this.currentPair[0].json); michael@0: } michael@0: michael@0: if (!this.currentPair[1]) { michael@0: this.pump(); michael@0: } michael@0: } else { michael@0: this.finish(); michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: if (!this.currentPair) { michael@0: return; michael@0: } michael@0: michael@0: var expected = this.currentPair[1] || {}; michael@0: michael@0: // |expected| can simply be a name of a message, no more further testing. michael@0: if (aMessage.name === expected) { michael@0: ok(true, 'Received ' + expected); michael@0: this.pump(); michael@0: return; michael@0: } michael@0: michael@0: var speech = this.extractUtterance(aMessage.json); michael@0: var android = this.extractAndroid(aMessage.json, expected.android); michael@0: if ((speech && expected.speak) || (android && expected.android)) { michael@0: if (expected.speak) { michael@0: (SimpleTest[expected.speak_checkFunc] || is)(speech, expected.speak, michael@0: '"' + speech + '" spoken'); michael@0: } michael@0: michael@0: if (expected.android) { michael@0: var checkFunc = SimpleTest[expected.android_checkFunc] || ok; michael@0: checkFunc.apply(SimpleTest, michael@0: this.lazyCompare(android, expected.android)); michael@0: } michael@0: michael@0: this.pump(); michael@0: } michael@0: michael@0: }, michael@0: michael@0: lazyCompare: function lazyCompare(aReceived, aExpected) { michael@0: var matches = true; michael@0: var delta = []; michael@0: for (var attr in aExpected) { michael@0: var expected = aExpected[attr]; michael@0: var received = aReceived !== undefined ? aReceived[attr] : null; michael@0: if (typeof expected === 'object') { michael@0: var [childMatches, childDelta] = this.lazyCompare(received, expected); michael@0: if (!childMatches) { michael@0: delta.push(attr + ' [ ' + childDelta + ' ]'); michael@0: matches = false; michael@0: } michael@0: } else { michael@0: if (received !== expected) { michael@0: delta.push( michael@0: attr + ' [ expected ' + expected + ' got ' + received + ' ]'); michael@0: matches = false; michael@0: } michael@0: } michael@0: } michael@0: return [matches, delta.join(' ')]; michael@0: }, michael@0: michael@0: extractUtterance: function(aData) { michael@0: if (!aData) { michael@0: return null; michael@0: } michael@0: michael@0: for (var output of aData) { michael@0: if (output && output.type === 'Speech') { michael@0: for (var action of output.details.actions) { michael@0: if (action && action.method == 'speak') { michael@0: return action.data; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: extractAndroid: function(aData, aExpectedEvents) { michael@0: for (var output of aData) { michael@0: if (output && output.type === 'Android') { michael@0: for (var i in output.details) { michael@0: // Only extract if event types match expected event types. michael@0: var exp = aExpectedEvents ? aExpectedEvents[i] : null; michael@0: if (!exp || (output.details[i].eventType !== exp.eventType)) { michael@0: return null; michael@0: } michael@0: } michael@0: return output.details; michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: }; michael@0: michael@0: // Common content messages michael@0: michael@0: var ContentMessages = { michael@0: simpleMoveFirst: { michael@0: name: 'AccessFu:MoveCursor', michael@0: json: { michael@0: action: 'moveFirst', michael@0: rule: 'Simple', michael@0: inputType: 'gesture', michael@0: origin: 'top' michael@0: } michael@0: }, michael@0: michael@0: simpleMoveLast: { michael@0: name: 'AccessFu:MoveCursor', michael@0: json: { michael@0: action: 'moveLast', michael@0: rule: 'Simple', michael@0: inputType: 'gesture', michael@0: origin: 'top' michael@0: } michael@0: }, michael@0: michael@0: simpleMoveNext: { michael@0: name: 'AccessFu:MoveCursor', michael@0: json: { michael@0: action: 'moveNext', michael@0: rule: 'Simple', michael@0: inputType: 'gesture', michael@0: origin: 'top' michael@0: } michael@0: }, michael@0: michael@0: simpleMovePrevious: { michael@0: name: 'AccessFu:MoveCursor', michael@0: json: { michael@0: action: 'movePrevious', michael@0: rule: 'Simple', michael@0: inputType: 'gesture', michael@0: origin: 'top' michael@0: } michael@0: }, michael@0: michael@0: clearCursor: { michael@0: name: 'AccessFu:ClearCursor', michael@0: json: { michael@0: origin: 'top' michael@0: } michael@0: }, michael@0: michael@0: adjustRangeUp: { michael@0: name: 'AccessFu:AdjustRange', michael@0: json: { michael@0: origin: 'top', michael@0: direction: 'backward' michael@0: } michael@0: }, michael@0: michael@0: adjustRangeDown: { michael@0: name: 'AccessFu:AdjustRange', michael@0: json: { michael@0: origin: 'top', michael@0: direction: 'forward' michael@0: } michael@0: }, michael@0: michael@0: focusSelector: function focusSelector(aSelector, aBlur) { michael@0: return { michael@0: name: 'AccessFuTest:Focus', michael@0: json: { michael@0: selector: aSelector, michael@0: blur: aBlur michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: activateCurrent: function activateCurrent(aOffset) { michael@0: return { michael@0: name: 'AccessFu:Activate', michael@0: json: { michael@0: origin: 'top', michael@0: offset: aOffset michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: moveNextBy: function moveNextBy(aGranularity) { michael@0: return { michael@0: name: 'AccessFu:MoveByGranularity', michael@0: json: { michael@0: direction: 'Next', michael@0: granularity: this._granularityMap[aGranularity] michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: movePreviousBy: function movePreviousBy(aGranularity) { michael@0: return { michael@0: name: 'AccessFu:MoveByGranularity', michael@0: json: { michael@0: direction: 'Previous', michael@0: granularity: this._granularityMap[aGranularity] michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: moveCaretNextBy: function moveCaretNextBy(aGranularity) { michael@0: return { michael@0: name: 'AccessFu:MoveCaret', michael@0: json: { michael@0: direction: 'Next', michael@0: granularity: this._granularityMap[aGranularity] michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: moveCaretPreviousBy: function moveCaretPreviousBy(aGranularity) { michael@0: return { michael@0: name: 'AccessFu:MoveCaret', michael@0: json: { michael@0: direction: 'Previous', michael@0: granularity: this._granularityMap[aGranularity] michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: _granularityMap: { michael@0: 'character': 1, // MOVEMENT_GRANULARITY_CHARACTER michael@0: 'word': 2, // MOVEMENT_GRANULARITY_WORD michael@0: 'paragraph': 8 // MOVEMENT_GRANULARITY_PARAGRAPH michael@0: } michael@0: }; michael@0: michael@0: var AndroidEvent = { michael@0: VIEW_CLICKED: 0x01, michael@0: VIEW_LONG_CLICKED: 0x02, michael@0: VIEW_SELECTED: 0x04, michael@0: VIEW_FOCUSED: 0x08, michael@0: VIEW_TEXT_CHANGED: 0x10, michael@0: WINDOW_STATE_CHANGED: 0x20, michael@0: VIEW_HOVER_ENTER: 0x80, michael@0: VIEW_HOVER_EXIT: 0x100, michael@0: VIEW_SCROLLED: 0x1000, michael@0: VIEW_TEXT_SELECTION_CHANGED: 0x2000, michael@0: ANNOUNCEMENT: 0x4000, michael@0: VIEW_ACCESSIBILITY_FOCUSED: 0x8000, michael@0: VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000 michael@0: };