// A common module to run tests on the AccessFu module 'use strict'; /*global isDeeply, getMainChromeWindow, SimpleTest, SpecialPowers, Logger, AccessFu, Utils, addMessageListener, currentTabDocument, currentBrowser*/ /** * A global variable holding an array of test functions. */ var gTestFuncs = []; /** * A global Iterator for the array of test functions. */ var gIterator; Components.utils.import('resource://gre/modules/Services.jsm'); Components.utils.import("resource://gre/modules/accessibility/Utils.jsm"); Components.utils.import("resource://gre/modules/accessibility/EventManager.jsm"); Components.utils.import("resource://gre/modules/accessibility/Gestures.jsm"); var AccessFuTest = { addFunc: function AccessFuTest_addFunc(aFunc) { if (aFunc) { gTestFuncs.push(aFunc); } }, _registerListener: function AccessFuTest__registerListener(aWaitForMessage, aListenerFunc) { var listener = { observe: function observe(aMessage) { // Ignore unexpected messages. if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) { return; } if (aMessage.message.indexOf(aWaitForMessage) < 0) { return; } aListenerFunc.apply(listener); } }; Services.console.registerListener(listener); return listener; }, on_log: function AccessFuTest_on_log(aWaitForMessage, aListenerFunc) { return this._registerListener(aWaitForMessage, aListenerFunc); }, off_log: function AccessFuTest_off_log(aListener) { Services.console.unregisterListener(aListener); }, once_log: function AccessFuTest_once_log(aWaitForMessage, aListenerFunc) { return this._registerListener(aWaitForMessage, function listenAndUnregister() { Services.console.unregisterListener(this); aListenerFunc(); }); }, _addObserver: function AccessFuTest__addObserver(aWaitForData, aListener) { var listener = function listener(aSubject, aTopic, aData) { var data = JSON.parse(aData)[1]; // Ignore non-relevant outputs. if (!data) { return; } isDeeply(data.details, aWaitForData, "Data is correct"); aListener.apply(listener); }; Services.obs.addObserver(listener, 'accessibility-output', false); return listener; }, on: function AccessFuTest_on(aWaitForData, aListener) { return this._addObserver(aWaitForData, aListener); }, off: function AccessFuTest_off(aListener) { Services.obs.removeObserver(aListener, 'accessibility-output'); }, once: function AccessFuTest_once(aWaitForData, aListener) { return this._addObserver(aWaitForData, function observerAndRemove() { Services.obs.removeObserver(this, 'accessibility-output'); aListener(); }); }, _waitForExplicitFinish: false, waitForExplicitFinish: function AccessFuTest_waitForExplicitFinish() { this._waitForExplicitFinish = true; }, finish: function AccessFuTest_finish() { // Disable the console service logging. Logger.test = false; Logger.logLevel = Logger.INFO; // Reset Gesture Settings. GestureSettings.dwellThreshold = this.dwellThreshold = this.originalDwellThreshold; GestureSettings.swipeMaxDuration = this.swipeMaxDuration = this.originalSwipeMaxDuration; GestureSettings.maxGestureResolveTimeout = this.maxGestureResolveTimeout = this.originalMaxGestureResolveTimeout; // Finish through idle callback to let AccessFu._disable complete. SimpleTest.executeSoon(function () { AccessFu.detach(); SimpleTest.finish(); }); }, nextTest: function AccessFuTest_nextTest() { var result = gIterator.next(); if (result.done) { this.finish(); return; } var testFunc = result.value; testFunc(); }, runTests: function AccessFuTest_runTests(aAdditionalPrefs) { if (gTestFuncs.length === 0) { ok(false, "No tests specified!"); SimpleTest.finish(); return; } // Create an Iterator for gTestFuncs array. gIterator = (function*() { for (var testFunc of gTestFuncs) { yield testFunc; } })(); // Start AccessFu and put it in stand-by. Components.utils.import("resource://gre/modules/accessibility/AccessFu.jsm"); AccessFu.attach(getMainChromeWindow(window)); AccessFu.readyCallback = function readyCallback() { // Enable logging to the console service. Logger.test = true; Logger.logLevel = Logger.DEBUG; }; var prefs = [['accessibility.accessfu.notify_output', 1], ['dom.mozSettings.enabled', true]]; prefs.push.apply(prefs, aAdditionalPrefs); this.originalDwellThreshold = GestureSettings.dwellThreshold; this.originalSwipeMaxDuration = GestureSettings.swipeMaxDuration; this.originalMaxGestureResolveTimeout = GestureSettings.maxGestureResolveTimeout; // https://bugzilla.mozilla.org/show_bug.cgi?id=1001945 - sometimes // SimpleTest.executeSoon timeout is bigger than the timer settings in // GestureSettings that causes intermittents. this.dwellThreshold = GestureSettings.dwellThreshold = GestureSettings.dwellThreshold * 10; this.swipeMaxDuration = GestureSettings.swipeMaxDuration = GestureSettings.swipeMaxDuration * 10; this.maxGestureResolveTimeout = GestureSettings.maxGestureResolveTimeout = GestureSettings.maxGestureResolveTimeout * 10; SpecialPowers.pushPrefEnv({ 'set': prefs }, function () { if (AccessFuTest._waitForExplicitFinish) { // Run all test functions asynchronously. AccessFuTest.nextTest(); } else { // Run all test functions synchronously. gTestFuncs.forEach(testFunc => testFunc()); AccessFuTest.finish(); } }); } }; function AccessFuContentTest(aFuncResultPairs) { this.queue = aFuncResultPairs; } AccessFuContentTest.prototype = { expected: [], currentAction: null, actionNum: -1, start: function(aFinishedCallback) { Logger.logLevel = Logger.DEBUG; this.finishedCallback = aFinishedCallback; var self = this; // Get top content message manager, and set it up. this.mms = [Utils.getMessageManager(currentBrowser())]; this.setupMessageManager(this.mms[0], function () { // Get child message managers and set them up var frames = currentTabDocument().querySelectorAll('iframe'); if (frames.length === 0) { self.pump(); return; } var toSetup = 0; for (var i = 0; i < frames.length; i++ ) { var mm = Utils.getMessageManager(frames[i]); if (mm) { toSetup++; self.mms.push(mm); self.setupMessageManager(mm, function () { if (--toSetup === 0) { // All message managers are loaded and ready to go. self.pump(); } }); } } }); }, finish: function() { Logger.logLevel = Logger.INFO; for (var mm of this.mms) { mm.sendAsyncMessage('AccessFu:Stop'); mm.removeMessageListener('AccessFu:Present', this); mm.removeMessageListener('AccessFu:Input', this); mm.removeMessageListener('AccessFu:CursorCleared', this); mm.removeMessageListener('AccessFu:Focused', this); mm.removeMessageListener('AccessFu:AriaHidden', this); mm.removeMessageListener('AccessFu:Ready', this); mm.removeMessageListener('AccessFu:ContentStarted', this); } if (this.finishedCallback) { this.finishedCallback(); } }, setupMessageManager: function (aMessageManager, aCallback) { function contentScript() { addMessageListener('AccessFuTest:Focus', function (aMessage) { var elem = content.document.querySelector(aMessage.json.selector); if (elem) { if (aMessage.json.blur) { elem.blur(); } else { elem.focus(); } } }); } aMessageManager.addMessageListener('AccessFu:Present', this); aMessageManager.addMessageListener('AccessFu:Input', this); aMessageManager.addMessageListener('AccessFu:CursorCleared', this); aMessageManager.addMessageListener('AccessFu:Focused', this); aMessageManager.addMessageListener('AccessFu:AriaHidden', this); aMessageManager.addMessageListener('AccessFu:Ready', function () { aMessageManager.addMessageListener('AccessFu:ContentStarted', aCallback); aMessageManager.sendAsyncMessage('AccessFu:Start', { buildApp: 'browser', androidSdkVersion: Utils.AndroidSdkVersion, logLevel: 'DEBUG', inTest: true }); }); aMessageManager.loadFrameScript( 'chrome://global/content/accessibility/content-script.js', false); aMessageManager.loadFrameScript( 'data:,(' + contentScript.toString() + ')();', false); }, pump: function() { this.expected.shift(); if (this.expected.length) { return; } var currentPair = this.queue.shift(); if (currentPair) { this.actionNum++; this.currentAction = currentPair[0]; if (typeof this.currentAction === 'function') { this.currentAction(this.mms[0]); } else if (this.currentAction) { this.mms[0].sendAsyncMessage(this.currentAction.name, this.currentAction.json); } this.expected = currentPair.slice(1, currentPair.length); if (!this.expected[0]) { this.pump(); } } else { this.finish(); } }, receiveMessage: function(aMessage) { var expected = this.expected[0]; if (!expected) { return; } var actionsString = typeof this.currentAction === 'function' ? this.currentAction.name + '()' : JSON.stringify(this.currentAction); if (typeof expected === 'string') { ok(true, 'Got ' + expected + ' after ' + actionsString); this.pump(); } else if (expected.ignore && !expected.ignore(aMessage)) { expected.is(aMessage.json, 'after ' + actionsString + ' (' + this.actionNum + ')'); expected.is_correct_focus(); this.pump(); } } }; // Common content messages var ContentMessages = { simpleMoveFirst: { name: 'AccessFu:MoveCursor', json: { action: 'moveFirst', rule: 'Simple', inputType: 'gesture', origin: 'top' } }, simpleMoveLast: { name: 'AccessFu:MoveCursor', json: { action: 'moveLast', rule: 'Simple', inputType: 'gesture', origin: 'top' } }, simpleMoveNext: { name: 'AccessFu:MoveCursor', json: { action: 'moveNext', rule: 'Simple', inputType: 'gesture', origin: 'top' } }, simpleMovePrevious: { name: 'AccessFu:MoveCursor', json: { action: 'movePrevious', rule: 'Simple', inputType: 'gesture', origin: 'top' } }, clearCursor: { name: 'AccessFu:ClearCursor', json: { origin: 'top' } }, moveOrAdjustUp: function moveOrAdjustUp(aRule) { return { name: 'AccessFu:MoveCursor', json: { origin: 'top', action: 'movePrevious', inputType: 'gesture', rule: (aRule || 'Simple'), adjustRange: true } } }, moveOrAdjustDown: function moveOrAdjustUp(aRule) { return { name: 'AccessFu:MoveCursor', json: { origin: 'top', action: 'moveNext', inputType: 'gesture', rule: (aRule || 'Simple'), adjustRange: true } } }, androidScrollForward: function adjustUp() { return { name: 'AccessFu:AndroidScroll', json: { origin: 'top', direction: 'forward' } }; }, androidScrollBackward: function adjustDown() { return { name: 'AccessFu:AndroidScroll', json: { origin: 'top', direction: 'backward' } }; }, focusSelector: function focusSelector(aSelector, aBlur) { return { name: 'AccessFuTest:Focus', json: { selector: aSelector, blur: aBlur } }; }, activateCurrent: function activateCurrent(aOffset) { return { name: 'AccessFu:Activate', json: { origin: 'top', offset: aOffset } }; }, moveNextBy: function moveNextBy(aGranularity) { return { name: 'AccessFu:MoveByGranularity', json: { direction: 'Next', granularity: this._granularityMap[aGranularity] } }; }, movePreviousBy: function movePreviousBy(aGranularity) { return { name: 'AccessFu:MoveByGranularity', json: { direction: 'Previous', granularity: this._granularityMap[aGranularity] } }; }, moveCaretNextBy: function moveCaretNextBy(aGranularity) { return { name: 'AccessFu:MoveCaret', json: { direction: 'Next', granularity: this._granularityMap[aGranularity] } }; }, moveCaretPreviousBy: function moveCaretPreviousBy(aGranularity) { return { name: 'AccessFu:MoveCaret', json: { direction: 'Previous', granularity: this._granularityMap[aGranularity] } }; }, _granularityMap: { 'character': 1, // MOVEMENT_GRANULARITY_CHARACTER 'word': 2, // MOVEMENT_GRANULARITY_WORD 'paragraph': 8 // MOVEMENT_GRANULARITY_PARAGRAPH } }; function ExpectedMessage (aName, aOptions) { this.name = aName; this.options = aOptions || {}; this.json = {}; } ExpectedMessage.prototype.lazyCompare = function(aReceived, aExpected, aInfo) { if (aExpected && !aReceived) { return [false, 'Expected something but got nothing -- ' + aInfo]; } var matches = true; var delta = []; for (var attr in aExpected) { var expected = aExpected[attr]; var received = aReceived[attr]; if (typeof expected === 'object') { var [childMatches, childDelta] = this.lazyCompare(received, expected); if (!childMatches) { delta.push(attr + ' [ ' + childDelta + ' ]'); matches = false; } } else { if (received !== expected) { delta.push( attr + ' [ expected ' + JSON.stringify(expected) + ' got ' + JSON.stringify(received) + ' ]'); matches = false; } } } var msg = delta.length ? delta.join(' ') : 'Structures lazily match'; return [matches, msg + ' -- ' + aInfo]; }; ExpectedMessage.prototype.is = function(aReceived, aInfo) { var checkFunc = this.options.todo ? 'todo' : 'ok'; SimpleTest[checkFunc].apply( SimpleTest, this.lazyCompare(aReceived, this.json, aInfo)); }; ExpectedMessage.prototype.is_correct_focus = function(aInfo) { if (!this.options.focused) { return; } var checkFunc = this.options.focused_todo ? 'todo_is' : 'is'; var doc = currentTabDocument(); SimpleTest[checkFunc].apply(SimpleTest, [ doc.activeElement, doc.querySelector(this.options.focused), 'Correct element is focused: ' + this.options.focused + ' -- ' + aInfo ]); }; ExpectedMessage.prototype.ignore = function(aMessage) { return aMessage.name !== this.name; }; function ExpectedPresent(aB2g, aAndroid, aOptions) { ExpectedMessage.call(this, 'AccessFu:Present', aOptions); if (aB2g) { this.json.b2g = aB2g; } if (aAndroid) { this.json.android = aAndroid; } } ExpectedPresent.prototype = Object.create(ExpectedMessage.prototype); ExpectedPresent.prototype.is = function(aReceived, aInfo) { var received = this.extract_presenters(aReceived); for (var presenter of ['b2g', 'android']) { if (!this.options['no_' + presenter]) { var todo = this.options.todo || this.options[presenter + '_todo'] SimpleTest[todo ? 'todo' : 'ok'].apply( SimpleTest, this.lazyCompare(received[presenter], this.json[presenter], aInfo + ' (' + presenter + ')')); } } }; ExpectedPresent.prototype.extract_presenters = function(aReceived) { var received = { count: 0 }; for (var presenter of aReceived) { if (presenter) { received[presenter.type.toLowerCase()] = presenter.details; received.count++; } } return received }; ExpectedPresent.prototype.ignore = function(aMessage) { if (ExpectedMessage.prototype.ignore.call(this, aMessage)) { return true; } var received = this.extract_presenters(aMessage.json); return received.count === 0 || (received.visual && received.visual.eventType === 'viewport-change') || (received.android && received.android[0].eventType === AndroidEvent.VIEW_SCROLLED); }; function ExpectedCursorChange(aSpeech, aOptions) { ExpectedPresent.call(this, { eventType: 'vc-change', data: aSpeech }, [{ eventType: 0x8000, // VIEW_ACCESSIBILITY_FOCUSED }], aOptions); } ExpectedCursorChange.prototype = Object.create(ExpectedPresent.prototype); function ExpectedCursorTextChange(aSpeech, aStartOffset, aEndOffset, aOptions) { ExpectedPresent.call(this, { eventType: 'vc-change', data: aSpeech }, [{ eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, fromIndex: aStartOffset, toIndex: aEndOffset }], aOptions); // bug 980509 this.options.b2g_todo = true; } ExpectedCursorTextChange.prototype = Object.create(ExpectedCursorChange.prototype); function ExpectedClickAction(aOptions) { ExpectedPresent.call(this, { eventType: 'action', data: [{ string: 'clickAction' }] }, [{ eventType: AndroidEvent.VIEW_CLICKED }], aOptions); } ExpectedClickAction.prototype = Object.create(ExpectedPresent.prototype); function ExpectedCheckAction(aChecked, aOptions) { ExpectedPresent.call(this, { eventType: 'action', data: [{ string: aChecked ? 'checkAction' : 'uncheckAction' }] }, [{ eventType: AndroidEvent.VIEW_CLICKED, checked: aChecked }], aOptions); } ExpectedCheckAction.prototype = Object.create(ExpectedPresent.prototype); function ExpectedSwitchAction(aSwitched, aOptions) { ExpectedPresent.call(this, { eventType: 'action', data: [{ string: aSwitched ? 'onAction' : 'offAction' }] }, [{ eventType: AndroidEvent.VIEW_CLICKED, checked: aSwitched }], aOptions); } ExpectedSwitchAction.prototype = Object.create(ExpectedPresent.prototype); function ExpectedNameChange(aName, aOptions) { ExpectedPresent.call(this, { eventType: 'name-change', data: aName }, null, aOptions); } ExpectedNameChange.prototype = Object.create(ExpectedPresent.prototype); function ExpectedValueChange(aValue, aOptions) { ExpectedPresent.call(this, { eventType: 'value-change', data: aValue }, null, aOptions); } ExpectedValueChange.prototype = Object.create(ExpectedPresent.prototype); function ExpectedTextChanged(aValue, aOptions) { ExpectedPresent.call(this, { eventType: 'text-change', data: aValue }, null, aOptions); } ExpectedTextChanged.prototype = Object.create(ExpectedPresent.prototype); function ExpectedEditState(aEditState, aOptions) { ExpectedMessage.call(this, 'AccessFu:Input', aOptions); this.json = aEditState; } ExpectedEditState.prototype = Object.create(ExpectedMessage.prototype); function ExpectedTextSelectionChanged(aStart, aEnd, aOptions) { ExpectedPresent.call(this, null, [{ eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED, brailleOutput: { selectionStart: aStart, selectionEnd: aEnd }}], aOptions); } ExpectedTextSelectionChanged.prototype = Object.create(ExpectedPresent.prototype); function ExpectedTextCaretChanged(aFrom, aTo, aOptions) { ExpectedPresent.call(this, null, [{ eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, fromIndex: aFrom, toIndex: aTo }], aOptions); } ExpectedTextCaretChanged.prototype = Object.create(ExpectedPresent.prototype); function ExpectedAnnouncement(aAnnouncement, aOptions) { ExpectedPresent.call(this, null, [{ eventType: AndroidEvent.ANNOUNCEMENT, text: [ aAnnouncement], addedCount: aAnnouncement.length }], aOptions); } ExpectedAnnouncement.prototype = Object.create(ExpectedPresent.prototype); function ExpectedNoMove(aOptions) { ExpectedPresent.call(this, {eventType: 'no-move' }, null, aOptions); } ExpectedNoMove.prototype = Object.create(ExpectedPresent.prototype); var AndroidEvent = { VIEW_CLICKED: 0x01, VIEW_LONG_CLICKED: 0x02, VIEW_SELECTED: 0x04, VIEW_FOCUSED: 0x08, VIEW_TEXT_CHANGED: 0x10, WINDOW_STATE_CHANGED: 0x20, VIEW_HOVER_ENTER: 0x80, VIEW_HOVER_EXIT: 0x100, VIEW_SCROLLED: 0x1000, VIEW_TEXT_SELECTION_CHANGED: 0x2000, ANNOUNCEMENT: 0x4000, VIEW_ACCESSIBILITY_FOCUSED: 0x8000, VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000 };