diff options
Diffstat (limited to '')
65 files changed, 12975 insertions, 0 deletions
diff --git a/accessible/tests/mochitest/events.js b/accessible/tests/mochitest/events.js new file mode 100644 index 0000000000..a6c216e01d --- /dev/null +++ b/accessible/tests/mochitest/events.js @@ -0,0 +1,2660 @@ +/* import-globals-from common.js */ +/* import-globals-from states.js */ +/* import-globals-from text.js */ + +// XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests. +/* eslint-disable no-redeclare */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; +const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; +const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; +const EVENT_DOCUMENT_LOAD_COMPLETE = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; +const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; +const EVENT_DOCUMENT_LOAD_STOPPED = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; +const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; +const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; +const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; +const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; +const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; +const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; +const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; +const EVENT_OBJECT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; +const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; +const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; +const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; +const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; +const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; +const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; +const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; +const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; +const EVENT_TEXT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; +const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; +const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; +const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; +const EVENT_TEXT_SELECTION_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; +const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; +const EVENT_VIRTUALCURSOR_CHANGED = + nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; + +const kNotFromUserInput = 0; +const kFromUserInput = 1; + +// ////////////////////////////////////////////////////////////////////////////// +// General + +/** + * Set up this variable to dump events into DOM. + */ +var gA11yEventDumpID = ""; + +/** + * Set up this variable to dump event processing into console. + */ +var gA11yEventDumpToConsole = false; + +/** + * Set up this variable to dump event processing into error console. + */ +var gA11yEventDumpToAppConsole = false; + +/** + * Semicolon separated set of logging features. + */ +var gA11yEventDumpFeature = ""; + +/** + * Function to detect HTML elements when given a node. + */ +function isHTMLElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == "http://www.w3.org/1999/xhtml" + ); +} + +function isXULElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ); +} + +/** + * Executes the function when requested event is handled. + * + * @param aEventType [in] event type + * @param aTarget [in] event target + * @param aFunc [in] function to call when event is handled + * @param aContext [in, optional] object in which context the function is + * called + * @param aArg1 [in, optional] argument passed into the function + * @param aArg2 [in, optional] argument passed into the function + */ +function waitForEvent( + aEventType, + aTargetOrFunc, + aFunc, + aContext, + aArg1, + aArg2 +) { + var handler = { + handleEvent: function handleEvent(aEvent) { + var target = aTargetOrFunc; + if (typeof aTargetOrFunc == "function") { + target = aTargetOrFunc.call(); + } + + if (target) { + if (target instanceof nsIAccessible && target != aEvent.accessible) { + return; + } + + if (Node.isInstance(target) && target != aEvent.DOMNode) { + return; + } + } + + unregisterA11yEventListener(aEventType, this); + + window.setTimeout(function () { + aFunc.call(aContext, aArg1, aArg2); + }, 0); + }, + }; + + registerA11yEventListener(aEventType, handler); +} + +/** + * Generate mouse move over image map what creates image map accessible (async). + * See waitForImageMap() function. + */ +function waveOverImageMap(aImageMapID) { + var imageMapNode = getNode(aImageMapID); + synthesizeMouse( + imageMapNode, + 10, + 10, + { type: "mousemove" }, + imageMapNode.ownerGlobal + ); +} + +/** + * Call the given function when the tree of the given image map is built. + */ +function waitForImageMap(aImageMapID, aTestFunc) { + waveOverImageMap(aImageMapID); + + var imageMapAcc = getAccessible(aImageMapID); + if (imageMapAcc.firstChild) { + aTestFunc(); + return; + } + + waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); +} + +/** + * Register accessibility event listener. + * + * @param aEventType the accessible event type (see nsIAccessibleEvent for + * available constants). + * @param aEventHandler event listener object, when accessible event of the + * given type is handled then 'handleEvent' method of + * this object is invoked with nsIAccessibleEvent object + * as the first argument. + */ +function registerA11yEventListener(aEventType, aEventHandler) { + listenA11yEvents(true); + addA11yEventListener(aEventType, aEventHandler); +} + +/** + * Unregister accessibility event listener. Must be called for every registered + * event listener (see registerA11yEventListener() function) when the listener + * is not needed. + */ +function unregisterA11yEventListener(aEventType, aEventHandler) { + removeA11yEventListener(aEventType, aEventHandler); + listenA11yEvents(false); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue + +/** + * Return value of invoke method of invoker object. Indicates invoker was unable + * to prepare action. + */ +const INVOKER_ACTION_FAILED = 1; + +/** + * Return value of eventQueue.onFinish. Indicates eventQueue should not finish + * tests. + */ +const DO_NOT_FINISH_TEST = 1; + +/** + * Creates event queue for the given event type. The queue consists of invoker + * objects, each of them generates the event of the event type. When queue is + * started then every invoker object is asked to generate event after timeout. + * When event is caught then current invoker object is asked to check whether + * event was handled correctly. + * + * Invoker interface is: + * + * var invoker = { + * // Generates accessible event or event sequence. If returns + * // INVOKER_ACTION_FAILED constant then stop tests. + * invoke: function(){}, + * + * // [optional] Invoker's check of handled event for correctness. + * check: function(aEvent){}, + * + * // [optional] Invoker's check before the next invoker is proceeded. + * finalCheck: function(aEvent){}, + * + * // [optional] Is called when event of any registered type is handled. + * debugCheck: function(aEvent){}, + * + * // [ignored if 'eventSeq' is defined] DOM node event is generated for + * // (used in the case when invoker expects single event). + * DOMNode getter: function() {}, + * + * // [optional] if true then event sequences are ignored (no failure if + * // sequences are empty). Use you need to invoke an action, do some check + * // after timeout and proceed a next invoker. + * noEventsOnAction getter: function() {}, + * + * // Array of checker objects defining expected events on invoker's action. + * // + * // Checker object interface: + * // + * // var checker = { + * // * DOM or a11y event type. * + * // type getter: function() {}, + * // + * // * DOM node or accessible. * + * // target getter: function() {}, + * // + * // * DOM event phase (false - bubbling). * + * // phase getter: function() {}, + * // + * // * Callback, called to match handled event. * + * // match : function(aEvent) {}, + * // + * // * Callback, called when event is handled + * // check: function(aEvent) {}, + * // + * // * Checker ID * + * // getID: function() {}, + * // + * // * Event that don't have predefined order relative other events. * + * // async getter: function() {}, + * // + * // * Event that is not expected. * + * // unexpected getter: function() {}, + * // + * // * No other event of the same type is not allowed. * + * // unique getter: function() {} + * // }; + * eventSeq getter() {}, + * + * // Array of checker objects defining unexpected events on invoker's + * // action. + * unexpectedEventSeq getter() {}, + * + * // The ID of invoker. + * getID: function(){} // returns invoker ID + * }; + * + * // Used to add a possible scenario of expected/unexpected events on + * // invoker's action. + * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) + * + * + * @param aEventType [in, optional] the default event type (isn't used if + * invoker defines eventSeq property). + */ +function eventQueue(aEventType) { + // public + + /** + * Add invoker object into queue. + */ + this.push = function eventQueue_push(aEventInvoker) { + this.mInvokers.push(aEventInvoker); + }; + + /** + * Start the queue processing. + */ + this.invoke = function eventQueue_invoke() { + listenA11yEvents(true); + + // XXX: Intermittent test_events_caretmove.html fails withouth timeout, + // see bug 474952. + this.processNextInvokerInTimeout(true); + }; + + /** + * This function is called when all events in the queue were handled. + * Override it if you need to be notified of this. + */ + this.onFinish = function eventQueue_finish() {}; + + // private + + /** + * Process next invoker. + */ + // eslint-disable-next-line complexity + this.processNextInvoker = function eventQueue_processNextInvoker() { + // Some scenario was matched, we wait on next invoker processing. + if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerNotScheduled, + "scenario was matched, wait for next invoker activation" + ); + return; + } + + this.setInvokerStatus( + kInvokerNotScheduled, + "the next invoker is processed now" + ); + + // Finish processing of the current invoker if any. + var testFailed = false; + + var invoker = this.getInvoker(); + if (invoker) { + if ("finalCheck" in invoker) { + invoker.finalCheck(); + } + + if (this.mScenarios && this.mScenarios.length) { + var matchIdx = -1; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (!this.areExpectedEventsLeft(eventSeq)) { + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + if ( + (checker.unexpected && checker.wasCaught) || + (!checker.unexpected && checker.wasCaught != 1) + ) { + break; + } + } + + // Ok, we have matched scenario. Report it was completed ok. In + // case of empty scenario guess it was matched but if later we + // find out that non empty scenario was matched then it will be + // a final match. + if (idx == eventSeq.length) { + if ( + matchIdx != -1 && + !!eventSeq.length && + this.mScenarios[matchIdx].length + ) { + ok( + false, + "We have a matched scenario at index " + + matchIdx + + " already." + ); + } + + if (matchIdx == -1 || eventSeq.length) { + matchIdx = scnIdx; + } + + // Report everything is ok. + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Test with ID = '" + this.getEventID(checker) + "' succeed. "; + + if (checker.unexpected) { + ok(true, msg + `There's no unexpected '${typeStr}' event.`); + } else if (checker.todo) { + todo(false, `Todo event '${typeStr}' was caught`); + } else { + ok(true, `${msg} Event '${typeStr}' was handled.`); + } + } + } + } + } + + // We don't have completely matched scenario. Report each failure/success + // for every scenario. + if (matchIdx == -1) { + testFailed = true; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Scenario #" + + scnIdx + + " of test with ID = '" + + this.getEventID(checker) + + "' failed. "; + + if (checker.wasCaught > 1) { + ok(false, msg + "Dupe " + typeStr + " event."); + } + + if (checker.unexpected) { + if (checker.wasCaught) { + ok(false, msg + "There's unexpected " + typeStr + " event."); + } + } else if (!checker.wasCaught) { + var rf = checker.todo ? todo : ok; + rf(false, `${msg} '${typeStr} event is missed.`); + } + } + } + } + } + } + + this.clearEventHandler(); + + // Check if need to stop the test. + if (testFailed || this.mIndex == this.mInvokers.length - 1) { + listenA11yEvents(false); + + var res = this.onFinish(); + if (res != DO_NOT_FINISH_TEST) { + SimpleTest.executeSoon(SimpleTest.finish); + } + + return; + } + + // Start processing of next invoker. + invoker = this.getNextInvoker(); + + // Set up event listeners. Process a next invoker if no events were added. + if (!this.setEventHandler(invoker)) { + this.processNextInvoker(); + return; + } + + if (gLogger.isEnabled()) { + gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); + gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); + } + + var infoText = "Invoke the '" + invoker.getID() + "' test { "; + var scnCount = this.mScenarios ? this.mScenarios.length : 0; + for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { + infoText += "scenario #" + scnIdx + ": "; + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + infoText += eventSeq[idx].unexpected + ? "un" + : "" + + "expected '" + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + "' event; "; + } + } + infoText += " }"; + info(infoText); + + if (invoker.invoke() == INVOKER_ACTION_FAILED) { + // Invoker failed to prepare action, fail and finish tests. + this.processNextInvoker(); + return; + } + + if (this.hasUnexpectedEventsScenario()) { + this.processNextInvokerInTimeout(true); + } + }; + + this.processNextInvokerInTimeout = + function eventQueue_processNextInvokerInTimeout(aUncondProcess) { + this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); + + // No need to wait extra timeout when a) we know we don't need to do that + // and b) there's no any single unexpected event. + if (!aUncondProcess && this.areAllEventsExpected()) { + // We need delay to avoid events coalesce from different invokers. + var queue = this; + SimpleTest.executeSoon(function () { + queue.processNextInvoker(); + }); + return; + } + + // Check in timeout invoker didn't fire registered events. + window.setTimeout( + function (aQueue) { + aQueue.processNextInvoker(); + }, + 300, + this + ); + }; + + /** + * Handle events for the current invoker. + */ + // eslint-disable-next-line complexity + this.handleEvent = function eventQueue_handleEvent(aEvent) { + var invoker = this.getInvoker(); + if (!invoker) { + // skip events before test was started + return; + } + + if (!this.mScenarios) { + // Bad invoker object, error will be reported before processing of next + // invoker in the queue. + this.processNextInvoker(); + return; + } + + if ("debugCheck" in invoker) { + invoker.debugCheck(aEvent); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + // Search through handled expected events to report error if one of them + // is handled for a second time. + if ( + !checker.unexpected && + checker.wasCaught > 0 && + eventQueue.isSameEvent(checker, aEvent) + ) { + checker.wasCaught++; + continue; + } + + // Search through unexpected events, any match results in error report + // after this invoker processing (in case of matched scenario only). + if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { + checker.wasCaught++; + continue; + } + + // Report an error if we handled not expected event of unique type + // (i.e. event types are matched, targets differs). + if ( + !checker.unexpected && + checker.unique && + eventQueue.compareEventTypes(checker, aEvent) + ) { + var isExpected = false; + for (var jdx = 0; jdx < eventSeq.length; jdx++) { + isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); + if (isExpected) { + break; + } + } + + if (!isExpected) { + ok( + false, + "Unique type " + + eventQueue.getEventTypeAsString(checker) + + " event was handled." + ); + } + } + } + } + + var hasMatchedCheckers = false; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + // Check if handled event matches expected sync event. + var nextChecker = this.getNextExpectedEvent(eventSeq); + if (nextChecker) { + if (eventQueue.compareEvents(nextChecker, aEvent)) { + this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); + hasMatchedCheckers = true; + continue; + } + } + + // Check if handled event matches any expected async events. + var haveUnmatchedAsync = false; + for (idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) { + break; + } + + if (!eventSeq[idx].wasCaught) { + haveUnmatchedAsync = true; + } + + if (!eventSeq[idx].unexpected && eventSeq[idx].async) { + if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { + this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); + hasMatchedCheckers = true; + break; + } + } + } + } + + if (hasMatchedCheckers) { + var invoker = this.getInvoker(); + if ("check" in invoker) { + invoker.check(aEvent); + } + } + + for (idx = 0; idx < eventSeq.length; idx++) { + if (!eventSeq[idx].wasCaught) { + if (eventSeq[idx] instanceof orderChecker) { + eventSeq[idx].wasCaught++; + } else { + break; + } + } + } + + // If we don't have more events to wait then schedule next invoker. + if (this.hasMatchedScenario()) { + if (this.mNextInvokerStatus == kInvokerNotScheduled) { + this.processNextInvokerInTimeout(); + } else if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerPending, + "Full match. Void the cancelation of next invoker processing" + ); + } + return; + } + + // If we have scheduled a next invoker then cancel in case of match. + if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) { + this.setInvokerStatus( + kInvokerCanceled, + "Cancel the scheduled invoker in case of match" + ); + } + }; + + // Helpers + this.processMatchedChecker = function eventQueue_function( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx + ) { + aMatchedChecker.wasCaught++; + + if ("check" in aMatchedChecker) { + aMatchedChecker.check(aEvent); + } + + eventQueue.logEvent( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + this.areExpectedEventsLeft(), + this.mNextInvokerStatus + ); + }; + + this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent( + aEventSeq + ) { + if (!("idx" in aEventSeq)) { + aEventSeq.idx = 0; + } + + while ( + aEventSeq.idx < aEventSeq.length && + (aEventSeq[aEventSeq.idx].unexpected || + aEventSeq[aEventSeq.idx].todo || + aEventSeq[aEventSeq.idx].async || + aEventSeq[aEventSeq.idx] instanceof orderChecker || + aEventSeq[aEventSeq.idx].wasCaught > 0) + ) { + aEventSeq.idx++; + } + + return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; + }; + + this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft( + aScenario + ) { + function scenarioHasUnhandledExpectedEvent(aEventSeq) { + // Check if we have unhandled async (can be anywhere in the sequance) or + // sync expcected events yet. + for (var idx = 0; idx < aEventSeq.length; idx++) { + if ( + !aEventSeq[idx].unexpected && + !aEventSeq[idx].todo && + !aEventSeq[idx].wasCaught && + !(aEventSeq[idx] instanceof orderChecker) + ) { + return true; + } + } + + return false; + } + + if (aScenario) { + return scenarioHasUnhandledExpectedEvent(aScenario); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (scenarioHasUnhandledExpectedEvent(eventSeq)) { + return true; + } + } + return false; + }; + + this.areAllEventsExpected = function eventQueue_areAllEventsExpected() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx].unexpected || eventSeq[idx].todo) { + return false; + } + } + } + + return true; + }; + + this.isUnexpectedEventScenario = + function eventQueue_isUnexpectedEventsScenario(aScenario) { + for (var idx = 0; idx < aScenario.length; idx++) { + if (!aScenario[idx].unexpected && !aScenario[idx].todo) { + break; + } + } + + return idx == aScenario.length; + }; + + this.hasUnexpectedEventsScenario = + function eventQueue_hasUnexpectedEventsScenario() { + if (this.getInvoker().noEventsOnAction) { + return true; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) { + return true; + } + } + + return false; + }; + + this.hasMatchedScenario = function eventQueue_hasMatchedScenario() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var scn = this.mScenarios[scnIdx]; + if ( + !this.isUnexpectedEventScenario(scn) && + !this.areExpectedEventsLeft(scn) + ) { + return true; + } + } + return false; + }; + + this.getInvoker = function eventQueue_getInvoker() { + return this.mInvokers[this.mIndex]; + }; + + this.getNextInvoker = function eventQueue_getNextInvoker() { + return this.mInvokers[++this.mIndex]; + }; + + this.setEventHandler = function eventQueue_setEventHandler(aInvoker) { + if (!("scenarios" in aInvoker) || !aInvoker.scenarios.length) { + var eventSeq = aInvoker.eventSeq; + var unexpectedEventSeq = aInvoker.unexpectedEventSeq; + if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) { + eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)]; + } + + if (eventSeq || unexpectedEventSeq) { + defineScenario(aInvoker, eventSeq, unexpectedEventSeq); + } + } + + if (aInvoker.noEventsOnAction) { + return true; + } + + this.mScenarios = aInvoker.scenarios; + if (!this.mScenarios || !this.mScenarios.length) { + ok(false, "Broken invoker '" + aInvoker.getID() + "'"); + return false; + } + + // Register event listeners. + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + if (gLogger.isEnabled()) { + var msg = + "scenario #" + + scnIdx + + ", registered events number: " + + eventSeq.length; + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + // Do not warn about empty event sequances when more than one scenario + // was registered. + if (this.mScenarios.length == 1 && !eventSeq.length) { + ok( + false, + "Broken scenario #" + + scnIdx + + " of invoker '" + + aInvoker.getID() + + "'. No registered events" + ); + return false; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + eventSeq[idx].wasCaught = 0; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + if (gLogger.isEnabled()) { + var msg = "registered"; + if (eventSeq[idx].unexpected) { + msg += " unexpected"; + } + if (eventSeq[idx].async) { + msg += " async"; + } + + msg += + ": event type: " + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + ", target: " + + eventQueue.getEventTargetDescr(eventSeq[idx], true); + + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + if (!target) { + ok(false, "no target for DOM event!"); + return false; + } + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.addEventListener(eventType, this, phase); + } else { + // A11y event + addA11yEventListener(eventType, this); + } + } + } + + return true; + }; + + this.clearEventHandler = function eventQueue_clearEventHandler() { + if (!this.mScenarios) { + return; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.removeEventListener(eventType, this, phase); + } else { + // A11y event + removeA11yEventListener(eventType, this); + } + } + } + this.mScenarios = null; + }; + + this.getEventID = function eventQueue_getEventID(aChecker) { + if ("getID" in aChecker) { + return aChecker.getID(); + } + + var invoker = this.getInvoker(); + return invoker.getID(); + }; + + this.setInvokerStatus = function eventQueue_setInvokerStatus( + aStatus, + aLogMsg + ) { + this.mNextInvokerStatus = aStatus; + + // Uncomment it to debug invoker processing logic. + // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); + }; + + this.mDefEventType = aEventType; + + this.mInvokers = []; + this.mIndex = -1; + this.mScenarios = null; + + this.mNextInvokerStatus = kInvokerNotScheduled; +} + +// ////////////////////////////////////////////////////////////////////////////// +// eventQueue static members and constants + +const kInvokerNotScheduled = 0; +const kInvokerPending = 1; +const kInvokerCanceled = 2; + +eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString( + aEventOrChecker +) { + if (Event.isInstance(aEventOrChecker)) { + return aEventOrChecker.type; + } + + if (aEventOrChecker instanceof nsIAccessibleEvent) { + return eventTypeToString(aEventOrChecker.eventType); + } + + return typeof aEventOrChecker.type == "string" + ? aEventOrChecker.type + : eventTypeToString(aEventOrChecker.type); +}; + +eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr( + aEventOrChecker, + aDontForceTarget +) { + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.originalTarget); + } + + // XXXbz this block doesn't seem to be reachable... + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.accessible); + } + + var descr = aEventOrChecker.targetDescr; + if (descr) { + return descr; + } + + if (aDontForceTarget) { + return "no target description"; + } + + var target = "target" in aEventOrChecker ? aEventOrChecker.target : null; + return prettyName(target); +}; + +eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) { + return "phase" in aChecker ? aChecker.phase : true; +}; + +eventQueue.getEventTarget = function eventQueue_getEventTarget(aChecker) { + if ("eventTarget" in aChecker) { + switch (aChecker.eventTarget) { + case "element": + return aChecker.target; + case "document": + default: + return aChecker.target.ownerDocument; + } + } + return aChecker.target.ownerDocument; +}; + +eventQueue.compareEventTypes = function eventQueue_compareEventTypes( + aChecker, + aEvent +) { + var eventType = Event.isInstance(aEvent) ? aEvent.type : aEvent.eventType; + return aChecker.type == eventType; +}; + +eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) { + if (!eventQueue.compareEventTypes(aChecker, aEvent)) { + return false; + } + + // If checker provides "match" function then allow the checker to decide + // whether event is matched. + if ("match" in aChecker) { + return aChecker.match(aEvent); + } + + var target1 = aChecker.target; + if (target1 instanceof nsIAccessible) { + var target2 = Event.isInstance(aEvent) + ? getAccessible(aEvent.target) + : aEvent.accessible; + + return target1 == target2; + } + + // If original target isn't suitable then extend interface to support target + // (original target is used in test_elm_media.html). + var target2 = Event.isInstance(aEvent) + ? aEvent.originalTarget + : aEvent.DOMNode; + return target1 == target2; +}; + +eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) { + // We don't have stored info about handled event other than its type and + // target, thus we should filter text change and state change events since + // they may occur on the same element because of complex changes. + return ( + this.compareEvents(aChecker, aEvent) && + !(aEvent instanceof nsIAccessibleTextChangeEvent) && + !(aEvent instanceof nsIAccessibleStateChangeEvent) + ); +}; + +eventQueue.invokerStatusToMsg = function eventQueue_invokerStatusToMsg( + aInvokerStatus, + aMsg +) { + var msg = "invoker status: "; + switch (aInvokerStatus) { + case kInvokerNotScheduled: + msg += "not scheduled"; + break; + case kInvokerPending: + msg += "pending"; + break; + case kInvokerCanceled: + msg += "canceled"; + break; + } + + if (aMsg) { + msg += " (" + aMsg + ")"; + } + + return msg; +}; + +eventQueue.logEvent = function eventQueue_logEvent( + aOrigEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + aAreExpectedEventsLeft, + aInvokerStatus +) { + // Dump DOM event information. Skip a11y event since it is dumped by + // gA11yEventObserver. + if (Event.isInstance(aOrigEvent)) { + var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); + info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); + gLogger.logToDOM(info); + } + + var infoMsg = + "unhandled expected events: " + + aAreExpectedEventsLeft + + ", " + + eventQueue.invokerStatusToMsg(aInvokerStatus); + + var currType = eventQueue.getEventTypeAsString(aMatchedChecker); + var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); + var consoleMsg = + "*****\nScenario " + + aScenarioIdx + + ", event " + + aEventIdx + + " matched: " + + currType + + "\n" + + infoMsg + + "\n*****"; + gLogger.logToConsole(consoleMsg); + + var emphText = "matched "; + var msg = + "EQ event, type: " + + currType + + ", target: " + + currTargetDescr + + ", " + + infoMsg; + gLogger.logToDOM(msg, true, emphText); +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Action sequence + +/** + * Deal with action sequence. Used when you need to execute couple of actions + * each after other one. + */ +function sequence() { + /** + * Append new sequence item. + * + * @param aProcessor [in] object implementing interface + * { + * // execute item action + * process: function() {}, + * // callback, is called when item was processed + * onProcessed: function() {} + * }; + * @param aEventType [in] event type of expected event on item action + * @param aTarget [in] event target of expected event on item action + * @param aItemID [in] identifier of item + */ + this.append = function sequence_append( + aProcessor, + aEventType, + aTarget, + aItemID + ) { + var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); + this.items.push(item); + }; + + /** + * Process next sequence item. + */ + this.processNext = function sequence_processNext() { + this.idx++; + if (this.idx >= this.items.length) { + ok(false, "End of sequence: nothing to process!"); + SimpleTest.finish(); + return; + } + + this.items[this.idx].startProcess(); + }; + + this.items = []; + this.idx = -1; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Defines a scenario of expected/unexpected events. Each invoker can have + * one or more scenarios of events. Only one scenario must be completed. + */ +function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) { + if (!("scenarios" in aInvoker)) { + aInvoker.scenarios = []; + } + + // Create unified event sequence concatenating expected and unexpected + // events. + if (!aEventSeq) { + aEventSeq = []; + } + + for (var idx = 0; idx < aEventSeq.length; idx++) { + aEventSeq[idx].unexpected |= false; + aEventSeq[idx].async |= false; + } + + if (aUnexpectedEventSeq) { + for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { + aUnexpectedEventSeq[idx].unexpected = true; + aUnexpectedEventSeq[idx].async = false; + } + + aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); + } + + aInvoker.scenarios.push(aEventSeq); +} + +/** + * Invokers defined below take a checker object (or array of checker objects). + * An invoker listens for default event type registered in event queue object + * until its checker is provided. + * + * Note, checker object or array of checker objects is optional. + */ + +/** + * Click invoker. + */ +function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthClick_invoke() { + var targetNode = this.DOMNode; + if (targetNode.nodeType == targetNode.DOCUMENT_NODE) { + targetNode = this.DOMNode.body + ? this.DOMNode.body + : this.DOMNode.documentElement; + } + + // Scroll the node into view, otherwise synth click may fail. + if (isHTMLElement(targetNode)) { + targetNode.scrollIntoView(true); + } else if (isXULElement(targetNode)) { + var targetAcc = getAccessible(targetNode); + targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); + } + + var x = 1, + y = 1; + if (aArgs && "where" in aArgs) { + if (aArgs.where == "right") { + if (isHTMLElement(targetNode)) { + x = targetNode.offsetWidth - 1; + } else if (isXULElement(targetNode)) { + x = targetNode.getBoundingClientRect().width - 1; + } + } else if (aArgs.where == "center") { + if (isHTMLElement(targetNode)) { + x = targetNode.offsetWidth / 2; + y = targetNode.offsetHeight / 2; + } else if (isXULElement(targetNode)) { + x = targetNode.getBoundingClientRect().width / 2; + y = targetNode.getBoundingClientRect().height / 2; + } + } + } + synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); + }; + + this.finalCheck = function synthClick_finalCheck() { + // Scroll top window back. + window.top.scrollTo(0, 0); + }; + + this.getID = function synthClick_getID() { + return prettyName(aNodeOrID) + " click"; + }; +} + +/** + * Scrolls the node into view. + */ +function scrollIntoView(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function scrollIntoView_invoke() { + var targetNode = this.DOMNode; + if (isHTMLElement(targetNode)) { + targetNode.scrollIntoView(true); + } else if (isXULElement(targetNode)) { + var targetAcc = getAccessible(targetNode); + targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); + } + }; + + this.getID = function scrollIntoView_getID() { + return prettyName(aNodeOrID) + " scrollIntoView"; + }; +} + +/** + * Mouse move invoker. + */ +function synthMouseMove(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function synthMouseMove_invoke() { + synthesizeMouse(this.DOMNode, 5, 5, { type: "mousemove" }); + synthesizeMouse(this.DOMNode, 6, 6, { type: "mousemove" }); + }; + + this.getID = function synthMouseMove_getID() { + return prettyName(aID) + " mouse move"; + }; +} + +/** + * General key press invoker. + */ +function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthKey_invoke() { + synthesizeKey(this.mKey, this.mArgs, this.mWindow); + }; + + this.getID = function synthKey_getID() { + var key = this.mKey; + switch (this.mKey) { + case "VK_TAB": + key = "tab"; + break; + case "VK_DOWN": + key = "down"; + break; + case "VK_UP": + key = "up"; + break; + case "VK_LEFT": + key = "left"; + break; + case "VK_RIGHT": + key = "right"; + break; + case "VK_HOME": + key = "home"; + break; + case "VK_END": + key = "end"; + break; + case "VK_ESCAPE": + key = "escape"; + break; + case "VK_RETURN": + key = "enter"; + break; + } + if (aArgs) { + if (aArgs.shiftKey) { + key += " shift"; + } + if (aArgs.ctrlKey) { + key += " ctrl"; + } + if (aArgs.altKey) { + key += " alt"; + } + } + return prettyName(aNodeOrID) + " '" + key + " ' key"; + }; + + this.mKey = aKey; + this.mArgs = aArgs ? aArgs : {}; + this.mWindow = aArgs ? aArgs.window : null; +} + +/** + * Tab key invoker. + */ +function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: false, window: aWindow }, + aCheckerOrEventSeq + ); +} + +/** + * Shift tab key invoker. + */ +function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: true }, + aCheckerOrEventSeq + ); +} + +/** + * Escape key invoker. + */ +function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_ESCAPE", + null, + aCheckerOrEventSeq + ); +} + +/** + * Down arrow key invoker. + */ +function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_DOWN", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Up arrow key invoker. + */ +function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq); +} + +/** + * Left arrow key invoker. + */ +function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_LEFT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Right arrow key invoker. + */ +function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_RIGHT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Home key invoker. + */ +function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); +} + +/** + * End key invoker. + */ +function synthEndKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); +} + +/** + * Enter key invoker + */ +function synthEnterKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); +} + +/** + * Synth alt + down arrow to open combobox. + */ +function synthOpenComboboxKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); + + this.getID = function synthOpenComboboxKey_getID() { + return "open combobox (alt + down arrow) " + prettyName(aID); + }; +} + +/** + * Focus invoker. + */ +function synthFocus(aNodeOrID, aCheckerOrEventSeq) { + var checkerOfEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(aNodeOrID); + this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); + + this.invoke = function synthFocus_invoke() { + if (this.DOMNode.editor) { + this.DOMNode.selectionStart = this.DOMNode.selectionEnd = + this.DOMNode.value.length; + } + this.DOMNode.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " focus"; + }; +} + +/** + * Focus invoker. Focus the HTML body of content document of iframe. + */ +function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) { + var frameDoc = getNode(aNodeOrID).contentDocument; + var checkerOrEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(frameDoc); + this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); + + this.invoke = function synthFocus_invoke() { + this.DOMNode.body.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " frame document focus"; + }; +} + +/** + * Change the current item when the widget doesn't have a focus. + */ +function changeCurrentItem(aID, aItemID) { + this.eventSeq = [new nofocusChecker()]; + + this.invoke = function changeCurrentItem_invoke() { + var controlNode = getNode(aID); + var itemNode = getNode(aItemID); + + // HTML + if (controlNode.localName == "input") { + if (controlNode.checked) { + this.reportError(); + } + + controlNode.checked = true; + return; + } + + if (controlNode.localName == "select") { + if (controlNode.selectedIndex == itemNode.index) { + this.reportError(); + } + + controlNode.selectedIndex = itemNode.index; + return; + } + + // XUL + if (controlNode.localName == "tree") { + if (controlNode.currentIndex == aItemID) { + this.reportError(); + } + + controlNode.currentIndex = aItemID; + return; + } + + if (controlNode.localName == "menulist") { + if (controlNode.selectedItem == itemNode) { + this.reportError(); + } + + controlNode.selectedItem = itemNode; + return; + } + + if (controlNode.currentItem == itemNode) { + ok( + false, + "Error in test: proposed current item is already current" + + prettyName(aID) + ); + } + + controlNode.currentItem = itemNode; + }; + + this.getID = function changeCurrentItem_getID() { + return "current item change for " + prettyName(aID); + }; + + this.reportError = function changeCurrentItem_reportError() { + ok( + false, + "Error in test: proposed current item '" + + aItemID + + "' is already current" + ); + }; +} + +/** + * Toggle top menu invoker. + */ +function toggleTopMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq); + + this.getID = function toggleTopMenu_getID() { + return "toggle top menu on " + prettyName(aID); + }; +} + +/** + * Context menu invoker. + */ +function synthContextMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, { + button: 0, + type: "contextmenu", + }); + + this.getID = function synthContextMenu_getID() { + return "context menu on " + prettyName(aID); + }; +} + +/** + * Open combobox, autocomplete and etc popup, check expandable states. + */ +function openCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID), + ]; + + this.invoke = function openCombobox_invoke() { + getNode(aComboboxID).focus(); + synthesizeKey("VK_DOWN", { altKey: true }); + }; + + this.getID = function openCombobox_getID() { + return "open combobox " + prettyName(aComboboxID); + }; +} + +/** + * Close combobox, autocomplete and etc popup, check expandable states. + */ +function closeCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID), + ]; + + this.invoke = function closeCombobox_invoke() { + synthesizeKey("KEY_Escape"); + }; + + this.getID = function closeCombobox_getID() { + return "close combobox " + prettyName(aComboboxID); + }; +} + +/** + * Select all invoker. + */ +function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthSelectAll_invoke() { + if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement") { + this.DOMNode.select(); + } else { + window.getSelection().selectAllChildren(this.DOMNode); + } + }; + + this.getID = function synthSelectAll_getID() { + return aNodeOrID + " selectall"; + }; +} + +/** + * Move the caret to the end of line. + */ +function moveToLineEnd(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_RIGHT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthEndKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to the end of previous line if any. + */ +function moveToPrevLineEnd(aID, aCaretOffset) { + this.__proto__ = new synthAction( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + + this.invoke = function moveToPrevLineEnd_invoke() { + synthesizeKey("KEY_ArrowUp"); + + if (MAC) { + synthesizeKey("Key_ArrowRight", { metaKey: true }); + } else { + synthesizeKey("KEY_End"); + } + }; + + this.getID = function moveToPrevLineEnd_getID() { + return "move to previous line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the line. + */ +function moveToLineStart(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_LEFT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthHomeKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line start in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the text. + */ +function moveToTextStart(aID) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_UP", + { metaKey: true }, + new caretMoveChecker(0, true, aID) + ); + } else { + this.__proto__ = new synthKey( + aID, + "VK_HOME", + { ctrlKey: true }, + new caretMoveChecker(0, true, aID) + ); + } + + this.getID = function moveToTextStart_getID() { + return "move to text start in " + prettyName(aID); + }; +} + +/** + * Move the caret in text accessible. + */ +function moveCaretToDOMPoint( + aID, + aDOMPointNodeID, + aDOMPointOffset, + aExpectedOffset, + aFocusTargetID, + aCheckFunc +) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.DOMPointNode = getNode(aDOMPointNodeID); + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + this.focusNode = this.focus ? this.focus.DOMNode : null; + + this.invoke = function moveCaretToDOMPoint_invoke() { + if (this.focusNode) { + this.focusNode.focus(); + } + + var selection = this.DOMPointNode.ownerGlobal.getSelection(); + var selRange = selection.getRangeAt(0); + selRange.setStart(this.DOMPointNode, aDOMPointOffset); + selRange.collapse(true); + + selection.removeRange(selRange); + selection.addRange(selRange); + }; + + this.getID = function moveCaretToDOMPoint_getID() { + return ( + "Set caret on " + + prettyName(aID) + + " at point: " + + prettyName(aDOMPointNodeID) + + " node with offset " + + aDOMPointOffset + ); + }; + + this.finalCheck = function moveCaretToDOMPoint_finalCheck() { + if (aCheckFunc) { + aCheckFunc.call(); + } + }; + + this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +/** + * Set caret offset in text accessible. + */ +function setCaretOffset(aID, aOffset, aFocusTargetID) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.offset = aOffset == -1 ? this.target.characterCount : aOffset; + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + + this.invoke = function setCaretOffset_invoke() { + this.target.caretOffset = this.offset; + }; + + this.getID = function setCaretOffset_getID() { + return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; + }; + + this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue checkers + +/** + * Common invoker checker (see eventSeq of eventQueue). + */ +function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) { + this.type = aEventType; + this.async = aIsAsync; + + this.__defineGetter__("target", invokerChecker_targetGetter); + this.__defineSetter__("target", invokerChecker_targetSetter); + + // implementation details + function invokerChecker_targetGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.call(null, this.mTargetFuncArg); + } + if (typeof this.mTarget == "string") { + return getNode(this.mTarget); + } + + return this.mTarget; + } + + function invokerChecker_targetSetter(aValue) { + this.mTarget = aValue; + return this.mTarget; + } + + this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); + + function invokerChecker_targetDescrGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.name + ", arg: " + this.mTargetFuncArg; + } + + return prettyName(this.mTarget); + } + + this.mTarget = aTargetOrFunc; + this.mTargetFuncArg = aTargetFuncArg; +} + +/** + * event checker that forces preceeding async events to happen before this + * checker. + */ +function orderChecker() { + // XXX it doesn't actually work to inherit from invokerChecker, but maybe we + // should fix that? + // this.__proto__ = new invokerChecker(null, null, null, false); +} + +/** + * Generic invoker checker for todo events. + */ +function todo_invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + this.todo = true; +} + +/** + * Generic invoker checker for unexpected events. + */ +function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + + this.unexpected = true; +} + +/** + * Common invoker checker for async events. + */ +function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +function focusChecker(aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_FOCUS, + aTargetOrFunc, + aTargetFuncArg, + false + ); + + this.unique = true; // focus event must be unique for invoker action + + this.check = function focusChecker_check(aEvent) { + testStates(aEvent.accessible, STATE_FOCUSED); + }; +} + +function nofocusChecker(aID) { + this.__proto__ = new focusChecker(aID); + this.unexpected = true; +} + +/** + * Text inserted/removed events checker. + * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput + */ +function textChangeChecker( + aID, + aStart, + aEnd, + aTextOrFunc, + aIsInserted, + aFromUser, + aAsync +) { + this.target = getNode(aID); + this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; + this.startOffset = aStart; + this.endOffset = aEnd; + this.textOrFunc = aTextOrFunc; + this.async = aAsync; + + this.match = function stextChangeChecker_match(aEvent) { + if ( + !(aEvent instanceof nsIAccessibleTextChangeEvent) || + aEvent.accessible !== getAccessible(this.target) + ) { + return false; + } + + let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + let modifiedText = + typeof this.textOrFunc === "function" + ? this.textOrFunc() + : this.textOrFunc; + return modifiedText === tcEvent.modifiedText; + }; + + this.check = function textChangeChecker_check(aEvent) { + aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + + var modifiedText = + typeof this.textOrFunc == "function" + ? this.textOrFunc() + : this.textOrFunc; + var modifiedTextLen = + this.endOffset == -1 ? modifiedText.length : aEnd - aStart; + + is( + aEvent.start, + this.startOffset, + "Wrong start offset for " + prettyName(aID) + ); + is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); + var changeInfo = aIsInserted ? "inserted" : "removed"; + is( + aEvent.isInserted, + aIsInserted, + "Text was " + changeInfo + " for " + prettyName(aID) + ); + is( + aEvent.modifiedText, + modifiedText, + "Wrong " + changeInfo + " text for " + prettyName(aID) + ); + if (typeof aFromUser != "undefined") { + is( + aEvent.isFromUserInput, + aFromUser, + "wrong value of isFromUserInput() for " + prettyName(aID) + ); + } + }; +} + +/** + * Caret move events checker. + */ +function caretMoveChecker( + aCaretOffset, + aIsSelectionCollapsed, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync +) { + this.__proto__ = new invokerChecker( + EVENT_TEXT_CARET_MOVED, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function caretMoveChecker_check(aEvent) { + let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent); + is( + evt.caretOffset, + aCaretOffset, + "Wrong caret offset for " + prettyName(aEvent.accessible) + ); + is( + evt.isSelectionCollapsed, + aIsSelectionCollapsed, + "wrong collapsed value for " + prettyName(aEvent.accessible) + ); + }; +} + +function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new caretMoveChecker( + aCaretOffset, + true, // Caret is collapsed + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Text selection change checker. + */ +function textSelectionChecker( + aID, + aStartOffset, + aEndOffset, + aRangeStartContainer, + aRangeStartOffset, + aRangeEndContainer, + aRangeEndOffset +) { + this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); + + this.check = function textSelectionChecker_check(aEvent) { + if (aStartOffset == aEndOffset) { + ok(true, "Collapsed selection triggered text selection change event."); + } else { + testTextGetSelection(aID, aStartOffset, aEndOffset, 0); + + // Test selection test range + let selectionRanges = aEvent.QueryInterface( + nsIAccessibleTextSelectionChangeEvent + ).selectionRanges; + let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange); + is( + range.startContainer, + getAccessible(aRangeStartContainer), + "correct range start container" + ); + is(range.startOffset, aRangeStartOffset, "correct range start offset"); + is(range.endOffset, aRangeEndOffset, "correct range end offset"); + is( + range.endContainer, + getAccessible(aRangeEndContainer), + "correct range end container" + ); + } + }; +} + +/** + * Object attribute changed checker + */ +function objAttrChangedChecker(aID, aAttr) { + this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID); + + this.check = function objAttrChangedChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + } catch (e) { + ok(false, "Object attribute changed event was expected"); + } + + if (!event) { + return; + } + + is( + event.changedAttribute, + aAttr, + "Wrong attribute name of the object attribute changed event." + ); + }; + + this.match = function objAttrChangedChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) { + var scEvent = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.changedAttribute == aAttr + ); + } + return false; + }; +} + +/** + * State change checker. + */ +function stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync, + aSkipCurrentStateCheck +) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function stateChangeChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is( + event.isExtraState, + aIsExtraState, + "Wrong extra state bit of the statechange event." + ); + isState( + event.state, + aState, + aIsExtraState, + "Wrong state of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + if (aSkipCurrentStateCheck) { + todo(false, "State checking was skipped!"); + return; + } + + var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; + var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; + var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState; + var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0; + testStates( + event.accessible, + state, + extraState, + unxpdState, + unxpdExtraState + ); + }; + + this.match = function stateChangeChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleStateChangeEvent) { + var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.state == aState + ); + } + return false; + }; +} + +function asyncStateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg +) { + this.__proto__ = new stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Expanded state change checker. + */ +function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg + ); + + this.check = function expandedStateChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); + is( + event.isExtraState, + false, + "Wrong extra state bit of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED); + }; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event sequances (array of predefined checkers) + +/** + * Event seq for single selection change. + */ +function selChangeSeq(aUnselectedID, aSelectedID) { + if (!aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ]; + } + + // Return two possible scenarios: depending on widget type when selection is + // moved the the order of items that get selected and unselected may vary. + return [ + [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + ]; +} + +/** + * Event seq for item removed form the selection. + */ +function selRemoveSeq(aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID), + ]; +} + +/** + * Event seq for item added to the selection. + */ +function selAddSeq(aSelectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION_ADD, aSelectedID), + ]; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details. +// ////////////////////////////////////////////////////////////////////////////// + +// ////////////////////////////////////////////////////////////////////////////// +// General + +var gA11yEventListeners = {}; +var gA11yEventApplicantsCount = 0; + +var gA11yEventObserver = { + // eslint-disable-next-line complexity + observe: function observe(aSubject, aTopic, aData) { + if (aTopic != "accessible-event") { + return; + } + + var event; + try { + event = aSubject.QueryInterface(nsIAccessibleEvent); + } catch (ex) { + // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. + // Remove the leftover observer, otherwise it "leaks" to all the following tests. + Services.obs.removeObserver(this, "accessible-event"); + // Forward the exception, with added explanation. + throw new Error( + "[accessible/events.js, gA11yEventObserver.observe] This is expected " + + `if a previous test has been aborted... Initial exception was: [ ${ex} ]` + ); + } + var listenersArray = gA11yEventListeners[event.eventType]; + + var eventFromDumpArea = false; + if (gLogger.isEnabled()) { + // debug stuff + eventFromDumpArea = true; + + var target = event.DOMNode; + var dumpElm = gA11yEventDumpID + ? document.getElementById(gA11yEventDumpID) + : null; + + if (dumpElm) { + var parent = target; + while (parent && parent != dumpElm) { + parent = parent.parentNode; + } + } + + if (!dumpElm || parent != dumpElm) { + var type = eventTypeToString(event.eventType); + var info = "Event type: " + type; + + if (event instanceof nsIAccessibleStateChangeEvent) { + var stateStr = statesToString( + event.isExtraState ? 0 : event.state, + event.isExtraState ? event.state : 0 + ); + info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; + } else if (event instanceof nsIAccessibleTextChangeEvent) { + info += + ", start: " + + event.start + + ", length: " + + event.length + + ", " + + (event.isInserted ? "inserted" : "removed") + + " text: " + + event.modifiedText; + } + + info += ". Target: " + prettyName(event.accessible); + + if (listenersArray) { + info += ". Listeners count: " + listenersArray.length; + } + + if (gLogger.hasFeature("parentchain:" + type)) { + info += "\nParent chain:\n"; + var acc = event.accessible; + while (acc) { + info += " " + prettyName(acc) + "\n"; + acc = acc.parent; + } + } + + eventFromDumpArea = false; + gLogger.log(info); + } + } + + // Do not notify listeners if event is result of event log changes. + if (!listenersArray || eventFromDumpArea) { + return; + } + + for (var index = 0; index < listenersArray.length; index++) { + listenersArray[index].handleEvent(event); + } + }, +}; + +function listenA11yEvents(aStartToListen) { + if (aStartToListen) { + // Add observer when adding the first applicant only. + if (!gA11yEventApplicantsCount++) { + Services.obs.addObserver(gA11yEventObserver, "accessible-event"); + } + } else { + // Remove observer when there are no more applicants only. + // '< 0' case should not happen, but just in case: removeObserver() will throw. + // eslint-disable-next-line no-lonely-if + if (--gA11yEventApplicantsCount <= 0) { + Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); + } + } +} + +function addA11yEventListener(aEventType, aEventHandler) { + if (!(aEventType in gA11yEventListeners)) { + gA11yEventListeners[aEventType] = []; + } + + var listenersArray = gA11yEventListeners[aEventType]; + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + listenersArray.push(aEventHandler); + } +} + +function removeA11yEventListener(aEventType, aEventHandler) { + var listenersArray = gA11yEventListeners[aEventType]; + if (!listenersArray) { + return false; + } + + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + return false; + } + + listenersArray.splice(index, 1); + + if (!listenersArray.length) { + gA11yEventListeners[aEventType] = null; + delete gA11yEventListeners[aEventType]; + } + + return true; +} + +/** + * Used to dump debug information. + */ +var gLogger = { + /** + * Return true if dump is enabled. + */ + isEnabled: function debugOutput_isEnabled() { + return ( + gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole + ); + }, + + /** + * Dump information into DOM and console if applicable. + */ + log: function logger_log(aMsg) { + this.logToConsole(aMsg); + this.logToAppConsole(aMsg); + this.logToDOM(aMsg); + }, + + /** + * Log message to DOM. + * + * @param aMsg [in] the primary message + * @param aHasIndent [in, optional] if specified the message has an indent + * @param aPreEmphText [in, optional] the text is colored and appended prior + * primary message + */ + logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) { + if (gA11yEventDumpID == "") { + return; + } + + var dumpElm = document.getElementById(gA11yEventDumpID); + if (!dumpElm) { + ok( + false, + "No dump element '" + gA11yEventDumpID + "' within the document!" + ); + return; + } + + var containerTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "div" + : "description"; + + var container = document.createElement(containerTagName); + if (aHasIndent) { + container.setAttribute("style", "padding-left: 10px;"); + } + + if (aPreEmphText) { + var inlineTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "span" + : "description"; + var emphElm = document.createElement(inlineTagName); + emphElm.setAttribute("style", "color: blue;"); + emphElm.textContent = aPreEmphText; + + container.appendChild(emphElm); + } + + var textNode = document.createTextNode(aMsg); + container.appendChild(textNode); + + dumpElm.appendChild(container); + }, + + /** + * Log message to console. + */ + logToConsole: function logger_logToConsole(aMsg) { + if (gA11yEventDumpToConsole) { + dump("\n" + aMsg + "\n"); + } + }, + + /** + * Log message to error console. + */ + logToAppConsole: function logger_logToAppConsole(aMsg) { + if (gA11yEventDumpToAppConsole) { + Services.console.logStringMessage("events: " + aMsg); + } + }, + + /** + * Return true if logging feature is enabled. + */ + hasFeature: function logger_hasFeature(aFeature) { + var startIdx = gA11yEventDumpFeature.indexOf(aFeature); + if (startIdx == -1) { + return false; + } + + var endIdx = startIdx + aFeature.length; + return ( + endIdx == gA11yEventDumpFeature.length || + gA11yEventDumpFeature[endIdx] == ";" + ); + }, +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Sequence + +/** + * Base class of sequence item. + */ +function sequenceItem(aProcessor, aEventType, aTarget, aItemID) { + // private + + this.startProcess = function sequenceItem_startProcess() { + this.queue.invoke(); + }; + + this.queue = new eventQueue(); + this.queue.onFinish = function () { + aProcessor.onProcessed(); + return DO_NOT_FINISH_TEST; + }; + + var invoker = { + invoke: function invoker_invoke() { + return aProcessor.process(); + }, + getID: function invoker_getID() { + return aItemID; + }, + eventSeq: [new invokerChecker(aEventType, aTarget)], + }; + + this.queue.push(invoker); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Invoker base class for prepare an action. + */ +function synthAction(aNodeOrID, aEventsObj) { + this.DOMNode = getNode(aNodeOrID); + + if (aEventsObj) { + var scenarios = null; + if (aEventsObj instanceof Array) { + if (aEventsObj[0] instanceof Array) { + scenarios = aEventsObj; + } + // scenarios + else { + scenarios = [aEventsObj]; + } // event sequance + } else { + scenarios = [[aEventsObj]]; // a single checker object + } + + for (var i = 0; i < scenarios.length; i++) { + defineScenario(this, scenarios[i]); + } + } + + this.getID = function synthAction_getID() { + return prettyName(aNodeOrID) + " action"; + }; +} diff --git a/accessible/tests/mochitest/events/a11y.toml b/accessible/tests/mochitest/events/a11y.toml new file mode 100644 index 0000000000..501b59f7b7 --- /dev/null +++ b/accessible/tests/mochitest/events/a11y.toml @@ -0,0 +1,128 @@ +[DEFAULT] +support-files = [ + "focus.html", + "scroll.html", + "slow_image.sjs", + "!/accessible/tests/mochitest/*.js", + "!/accessible/tests/mochitest/letters.gif", + "!/image/test/mochitest/animated-gif-finalframe.gif", + "!/image/test/mochitest/animated-gif.gif"] + +["test_announcement.html"] + +["test_aria_alert.html"] + +["test_aria_menu.html"] + +["test_aria_objattr.html"] + +["test_aria_owns.html"] + +["test_aria_statechange.html"] + +["test_attrchange.html"] + +["test_attrs.html"] + +["test_bug1322593-2.html"] + +["test_bug1322593.html"] + +["test_caretmove.html"] + +["test_coalescence.html"] + +["test_contextmenu.html"] + +["test_descrchange.html"] + +["test_dragndrop.html"] + +["test_flush.html"] + +["test_focus_aria_activedescendant.html"] + +["test_focus_autocomplete.html"] + +["test_focus_autocomplete.xhtml"] +# Disabled on Linux and Windows due to frequent failures - bug 695019, bug 890795 +skip-if = [ + "os == 'win'", + "os == 'linux'", +] + +["test_focus_canvas.html"] + +["test_focus_contextmenu.xhtml"] + +["test_focus_controls.html"] + +["test_focus_doc.html"] + +["test_focus_general.html"] + +["test_focus_general.xhtml"] + +["test_focus_listcontrols.xhtml"] + +["test_focus_menu.xhtml"] + +["test_focus_name.html"] + +["test_focus_removal.html"] + +["test_focus_selects.html"] + +["test_focus_tabbox.xhtml"] +skip-if = ["true"] + +["test_focus_tree.xhtml"] + +["test_focusable_statechange.html"] + +["test_fromUserInput.html"] + +["test_label.xhtml"] + +["test_menu.xhtml"] + +["test_mutation.html"] + +["test_namechange.html"] + +["test_namechange.xhtml"] + +["test_scroll.xhtml"] + +["test_scroll_caret.xhtml"] + +["test_selection.html"] +skip-if = [ + "os == 'mac'", +] + +["test_selection.xhtml"] +skip-if = [ + "os == 'mac'", +] + +["test_selection_aria.html"] + +["test_statechange.html"] + +["test_statechange.xhtml"] + +["test_text.html"] + +["test_text_alg.html"] + +["test_textattrchange.html"] + +["test_textselchange.html"] + +["test_tree.xhtml"] + +["test_valuechange.html"] +skip-if = [ + "os == 'mac'", +] diff --git a/accessible/tests/mochitest/events/docload/a11y.toml b/accessible/tests/mochitest/events/docload/a11y.toml new file mode 100644 index 0000000000..dc71817725 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/a11y.toml @@ -0,0 +1,21 @@ +[DEFAULT] +support-files = [ + "docload_wnd.html", + "!/accessible/tests/mochitest/*.js"] + +["test_docload_aria.html"] + +["test_docload_busy.html"] + +["test_docload_embedded.html"] + +["test_docload_iframe.html"] + +["test_docload_root.html"] +skip-if = ["os == 'mac'"] # bug 1456997 + +["test_docload_shutdown.html"] +skip-if = [ + "os == 'mac'", # bug 1456997 + "display == 'wayland'", # bug 1850412 +] diff --git a/accessible/tests/mochitest/events/docload/docload_wnd.html b/accessible/tests/mochitest/events/docload/docload_wnd.html new file mode 100644 index 0000000000..93df1e86d4 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/docload_wnd.html @@ -0,0 +1,37 @@ +<html> +<head> + <title>Accessible events testing for document</title> + <script> + const STATE_BUSY = Ci.nsIAccessibleStates.STATE_BUSY; + + var gService = null; + function waitForDocLoad() { + if (!gService) { + gService = Cc["@mozilla.org/accessibilityService;1"]. + getService(Ci.nsIAccessibilityService); + } + + var accDoc = gService.getAccessibleFor(document); + + var state = {}; + accDoc.getState(state, {}); + if (state.value & STATE_BUSY) { + window.setTimeout(waitForDocLoad, 0); + return; + } + + hideIFrame(); + } + + function hideIFrame() { + var iframe = document.getElementById("iframe"); + gService.getAccessibleFor(iframe.contentDocument); + iframe.style.display = "none"; + } + </script> +</head> + +<body onload="waitForDocLoad();"> + <iframe id="iframe"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_aria.html b/accessible/tests/mochitest/events/docload/test_docload_aria.html new file mode 100644 index 0000000000..c5fc099918 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_aria.html @@ -0,0 +1,75 @@ +<html> + +<head> + <title>Accessible events testing for ARIA document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function showARIADialog(aID) { + this.dialogNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, this.dialogNode), + ]; + + this.invoke = function showARIADialog_invoke() { + this.dialogNode.style.display = "block"; + }; + + this.getID = function showARIADialog_getID() { + return "show ARIA dialog"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new showARIADialog("dialog")); + gQueue.push(new showARIADialog("document")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=759833" + title="ARIA documents should fire document loading events"> + Mozilla Bug 759833 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="dialog" id="dialog" style="display: none;">It's a dialog</div> + <div role="document" id="document" style="display: none;">It's a document</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_busy.html b/accessible/tests/mochitest/events/docload/test_docload_busy.html new file mode 100644 index 0000000000..37caf306bb --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_busy.html @@ -0,0 +1,83 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function makeIFrameVisible(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.DOMNode.parentNode), + { + type: EVENT_STATE_CHANGE, + get target() { + return getAccessible("iframe").firstChild; + }, + match(aEvent) { + // The document shouldn't have busy state (the DOM document was + // loaded before its accessible was created). Do this test lately to + // make sure the content of document accessible was created + // initially, prior to this the document accessible keeps busy + // state. The initial creation happens asynchronously after document + // creation, there are no events we could use to catch it. + let { state, isEnabled } = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + return state & STATE_BUSY && !isEnabled; + }, + }, + ]; + + this.invoke = () => (this.DOMNode.style.visibility = "visible"); + + this.getID = () => + "The accessible for DOM document loaded before it's shown shouldn't have busy state."; + } + + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(); + gQueue.push(new makeIFrameVisible("iframe")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=658185" + title="The DOM document loaded before it's shown shouldn't have busy state"> + Mozilla Bug 658185 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe" src="about:mozilla" style="visibility: hidden;"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_embedded.html b/accessible/tests/mochitest/events/docload/test_docload_embedded.html new file mode 100644 index 0000000000..18873dc904 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_embedded.html @@ -0,0 +1,85 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function changeIframeSrc(aIdentifier, aURL, aTitle) { + this.DOMNode = getNode(aIdentifier); + + function getIframeDoc() { + return getAccessible(getNode(aIdentifier).contentDocument); + } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getAccessible(this.DOMNode)), + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, getIframeDoc), + ]; + + this.invoke = () => (this.DOMNode.src = aURL); + + this.finalCheck = () => + testAccessibleTree(this.DOMNode, { + role: ROLE_INTERNAL_FRAME, + children: [ + { + role: ROLE_DOCUMENT, + name: aTitle, + }, + ], + }); + + this.getID = () => `change iframe src on ${aURL}`; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(); + gQueue.push(new changeIframeSrc("iframe", "about:license", "Licenses")); + gQueue.push(new changeIframeSrc("iframe", "about:buildconfig", "Build Configuration")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=420845" + title="Fire event_reorder on any embedded frames/iframes whos document has just loaded"> + Mozilla Bug 420845 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=754165" + title="Fire document load events on iframes too"> + Mozilla Bug 754165 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_iframe.html b/accessible/tests/mochitest/events/docload/test_docload_iframe.html new file mode 100644 index 0000000000..d410ebb7e2 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_iframe.html @@ -0,0 +1,99 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + const kHide = 1; + const kShow = 2; + const kRemove = 3; + + function morphIFrame(aIdentifier, aAction) { + this.DOMNode = getNode(aIdentifier); + this.IFrameContainerDOMNode = this.DOMNode.parentNode; + + this.eventSeq = [ + new invokerChecker(aAction === kShow ? EVENT_SHOW : EVENT_HIDE, this.DOMNode), + new invokerChecker(EVENT_REORDER, this.IFrameContainerDOMNode), + ]; + + this.invoke = () => { + if (aAction === kRemove) { + this.IFrameContainerDOMNode.removeChild(this.DOMNode); + } else { + this.DOMNode.style.display = aAction === kHide ? "none" : "block"; + } + }; + + this.finalCheck = () => + testAccessibleTree(this.IFrameContainerDOMNode, { + role: ROLE_SECTION, + children: (aAction == kHide || aAction == kRemove) ? [ ] : + [ + { + role: ROLE_INTERNAL_FRAME, + children: [ + { role: ROLE_DOCUMENT }, + ], + }, + ], + }); + + this.getID = () => { + if (aAction === kRemove) { + return "remove iframe"; + } + + return `change display style of iframe to ${aAction === kHide ? "none" : "block"}`; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(EVENT_REORDER); + gQueue.push(new morphIFrame("iframe", kHide)); + gQueue.push(new morphIFrame("iframe", kShow)); + gQueue.push(new morphIFrame("iframe", kRemove)); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566103" + title="Reorganize accessible document handling"> + Mozilla Bug 566103 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_root.html b/accessible/tests/mochitest/events/docload/test_docload_root.html new file mode 100644 index 0000000000..91ce3a10ee --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_root.html @@ -0,0 +1,125 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + let gDialog; + let gDialogDoc; + let gRootAcc; + + function openDialogWnd(aURL) { + // Get application root accessible. + let docAcc = getAccessible(document); + while (docAcc) { + gRootAcc = docAcc; + try { + docAcc = docAcc.parent; + } catch (e) { + ok(false, `Can't get parent for ${prettyName(docAcc)}`); + throw e; + } + } + + this.eventSeq = [ + new asyncInvokerChecker(EVENT_REORDER, gRootAcc), + // We use a function here to get the target because gDialog isn't set + // yet, but it will be when the function is called. + new invokerChecker(EVENT_FOCUS, () => gDialog.document) + ]; + + this.invoke = () => (gDialog = window.browsingContext.topChromeWindow.openDialog(aURL)); + + this.finalCheck = () => { + const accTree = { + role: ROLE_APP_ROOT, + children: [ + { + role: ROLE_CHROME_WINDOW, + }, + { + role: ROLE_CHROME_WINDOW, + }, + ], + }; + + testAccessibleTree(gRootAcc, accTree); + + gDialogDoc = gDialog.document; + ok(isAccessibleInCache(gDialogDoc), + `The document accessible for '${aURL}' is not in cache!`); + }; + + this.getID = () => `open dialog '${aURL}'`; + } + + function closeDialogWnd() { + this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ]; + + this.invoke = () => { + gDialog.close(); + window.focus(); + }; + + this.finalCheck = () => { + ok(!isAccessibleInCache(gDialogDoc), + `The document accessible for dialog is in cache still!`); + + gDialog = gDialogDoc = gRootAcc = null; + }; + + this.getID = () => "close dialog"; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + // Front end stuff sometimes likes to stuff things in the hidden window(s) + // in which case we should repress all accessibles for those. + + // Try to create an accessible for the hidden window's document. + let doc = Services.appShell.hiddenDOMWindow.document; + let hiddenDocAcc = gAccService.getAccessibleFor(doc); + ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible."); + + const gQueue = new eventQueue(); + gQueue.push(new openDialogWnd("about:about")); + gQueue.push(new closeDialogWnd()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=506206" + title="Fire event_reorder application root accessible"> + Mozilla Bug 506206 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_shutdown.html b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html new file mode 100644 index 0000000000..a111d9e43b --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html @@ -0,0 +1,142 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + let gDialog; + let gDialogDoc; + let gRootAcc; + let gIframeDoc; + + function openWndShutdownDoc(aURL) { + // Get application root accessible. + let docAcc = getAccessible(document); + while (docAcc) { + gRootAcc = docAcc; + try { + docAcc = docAcc.parent; + } catch (e) { + ok(false, `Can't get parent for ${prettyName(docAcc)}`); + throw e; + } + } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, gRootAcc), + { + type: EVENT_HIDE, + get target() { + gDialogDoc = gDialog.document; + const iframe = gDialogDoc.getElementById("iframe"); + gIframeDoc = iframe.contentDocument; + return iframe; + }, + get targetDescr() { + return "inner iframe of docload_wnd.html document"; + }, + }, + ]; + + + this.invoke = () => gDialog = window.browsingContext.topChromeWindow.openDialog(aURL); + + this.finalCheck = () => { + const accTree = { + role: ROLE_APP_ROOT, + children: [ + { + role: ROLE_CHROME_WINDOW, + }, + { + role: ROLE_CHROME_WINDOW, + }, + ], + }; + + testAccessibleTree(gRootAcc, accTree); + // After timeout after event hide for iframe was handled the document + // accessible for iframe's document should no longer be in cache. + ok(!isAccessibleInCache(gIframeDoc), + "The document accessible for iframe is in cache still after iframe hide!"); + ok(isAccessibleInCache(gDialogDoc), + `The document accessible for '${aURL}' is not in cache!`); + }; + + this.getID = () => `open dialog '${aURL}'`; + } + + function closeWndShutdownDoc() { + this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ]; + + this.invoke = () => { + gDialog.close(); + window.focus(); + }; + + this.finalCheck = () => { + ok(!isAccessibleInCache(gDialogDoc), + "The document accessible for dialog is in cache still!"); + // After the window is closed all alive subdocument accessibles should + // be shut down. + ok(!isAccessibleInCache(gIframeDoc), + "The document accessible for iframe is in cache still!"); + + gDialog = gDialogDoc = gRootAcc = gIframeDoc = null; + }; + + this.getID = () => "close dialog"; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + // Front end stuff sometimes likes to stuff things in the hidden window(s) + // in which case we should repress all accessibles for those. + + // Try to create an accessible for the hidden window's document. + let doc = Services.appShell.hiddenDOMWindow.document; + let hiddenDocAcc = gAccService.getAccessibleFor(doc); + ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible."); + + const gQueue = new eventQueue(); + gQueue.push(new openWndShutdownDoc("../../events/docload/docload_wnd.html")); + gQueue.push(new closeWndShutdownDoc()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=571459" + title="Shutdown document accessible when presshell goes away"> + Mozilla Bug 571459 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/events/focus.html b/accessible/tests/mochitest/events/focus.html new file mode 100644 index 0000000000..ab055df82c --- /dev/null +++ b/accessible/tests/mochitest/events/focus.html @@ -0,0 +1,10 @@ +<html> + +<head> + <title>editable document</title> +</head> + +<body contentEditable="true"> + editable document +</body> +</html> diff --git a/accessible/tests/mochitest/events/scroll.html b/accessible/tests/mochitest/events/scroll.html new file mode 100644 index 0000000000..562e0a3825 --- /dev/null +++ b/accessible/tests/mochitest/events/scroll.html @@ -0,0 +1,181 @@ +<html> + +<head> + <title>nsIAccessible actions testing for anchors</title> +</head> + +<body> + <p> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> + <a name="link1">link1</a> + + <p style="color: blue"> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> + + <h1 id="heading_1">heading 1</h1> + <p style="color: blue"> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> +</body> +<html> diff --git a/accessible/tests/mochitest/events/slow_image.sjs b/accessible/tests/mochitest/events/slow_image.sjs new file mode 100644 index 0000000000..f322568be6 --- /dev/null +++ b/accessible/tests/mochitest/events/slow_image.sjs @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + let x = { + data, + QueryInterface: ChromeUtils.generateQI([]), + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function (x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +function handleRequest(request, response) { + if (request.queryString == "complete") { + // Unblock the previous request. + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/json", false); + response.write("true"); // the payload doesn't matter. + + let blockedResponse = getGlobalState("a11y-image"); + if (blockedResponse) { + blockedResponse.setStatusLine(request.httpVersion, 200, "OK"); + blockedResponse.setHeader("Cache-Control", "no-cache", false); + blockedResponse.setHeader("Content-Type", "image/png", false); + blockedResponse.write(IMG_BYTES); + blockedResponse.finish(); + + setGlobalState(undefined, "a11y-image"); + } + } else { + // Getting the image + response.processAsync(); + // Store the response in the global state + setGlobalState(response, "a11y-image"); + } +} diff --git a/accessible/tests/mochitest/events/test_announcement.html b/accessible/tests/mochitest/events/test_announcement.html new file mode 100644 index 0000000000..eb303e4aa9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_announcement.html @@ -0,0 +1,61 @@ +<html> + +<head> + <title>Announcement event and method testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTests() { + let acc = getAccessible("display"); + + let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE); + let evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "please", "announcement matches."); + is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches"); + + onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE); + evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "do it", "announcement matches."); + is(evt.priority, nsIAccessibleAnnouncementEvent.ASSERTIVE, + "priority matches"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1525980" + title="Introduce announcement event and method"> + Mozilla Bug 1525980 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_alert.html b/accessible/tests/mochitest/events/test_aria_alert.html new file mode 100644 index 0000000000..48f4197b50 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_alert.html @@ -0,0 +1,84 @@ +<html> + +<head> + <title>ARIA alert event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function showAlert(aID) { + this.DOMNode = document.createElement("div"); + + this.invoke = function showAlert_invoke() { + this.DOMNode.setAttribute("role", "alert"); + this.DOMNode.setAttribute("id", aID); + var text = document.createTextNode("alert"); + this.DOMNode.appendChild(text); + document.body.appendChild(this.DOMNode); + }; + + this.getID = function showAlert_getID() { + return "Show ARIA alert " + aID; + }; + } + + function changeAlert(aID) { + this.__defineGetter__("DOMNode", function() { return getNode(aID); }); + + this.invoke = function changeAlert_invoke() { + this.DOMNode.textContent = "new alert"; + }; + + this.getID = function showAlert_getID() { + return "Change ARIA alert " + aID; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuging + // enableLogging("tree,events,verbose"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_ALERT); + + gQueue.push(new showAlert("alert")); + gQueue.push(new changeAlert("alert")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=591199" + title="mochitest for bug 334386: fire alert event when ARIA alert is shown or new its children are inserted"> + Mozilla Bug 591199 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_menu.html b/accessible/tests/mochitest/events/test_aria_menu.html new file mode 100644 index 0000000000..b240090cb9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_menu.html @@ -0,0 +1,267 @@ +<html> + +<head> + <title>ARIA menu events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + const kViaDisplayStyle = 0; + const kViaVisibilityStyle = 1; + + function focusMenu(aMenuBarID, aMenuID, aActiveMenuBarID) { + this.eventSeq = []; + + if (aActiveMenuBarID) { + this.eventSeq.push(new invokerChecker(EVENT_MENU_END, + getNode(aActiveMenuBarID))); + } + + this.eventSeq.push(new invokerChecker(EVENT_MENU_START, getNode(aMenuBarID))); + this.eventSeq.push(new invokerChecker(EVENT_FOCUS, getNode(aMenuID))); + + this.invoke = function focusMenu_invoke() { + getNode(aMenuID).focus(); + }; + + this.getID = function focusMenu_getID() { + return "focus menu '" + aMenuID + "'"; + }; + } + + function showMenu(aMenuID, aParentMenuID, aHow) { + this.menuNode = getNode(aMenuID); + + // Because of aria-owns processing we may have menupopup start fired before + // related show event. + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.menuNode), + new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)), + new invokerChecker(EVENT_MENUPOPUP_START, this.menuNode), + ]; + + this.invoke = function showMenu_invoke() { + if (aHow == kViaDisplayStyle) + this.menuNode.style.display = "block"; + else + this.menuNode.style.visibility = "visible"; + }; + + this.getID = function showMenu_getID() { + return "Show ARIA menu '" + aMenuID + "' by " + + (aHow == kViaDisplayStyle ? "display" : "visibility") + + " style tricks"; + }; + } + + function closeMenu(aMenuID, aParentMenuID, aHow) { + this.menuNode = getNode(aMenuID); + this.menu = null; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getMenu, this), + new invokerChecker(EVENT_MENUPOPUP_END, getMenu, this), + new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)), + ]; + + this.invoke = function closeMenu_invoke() { + // Store menu accessible reference while menu is still open. + this.menu = getAccessible(this.menuNode); + + // Hide menu. + if (aHow == kViaDisplayStyle) + this.menuNode.style.display = "none"; + else + this.menuNode.style.visibility = "hidden"; + }; + + this.getID = function closeMenu_getID() { + return "Close ARIA menu " + aMenuID + " by " + + (aHow == kViaDisplayStyle ? "display" : "visibility") + + " style tricks"; + }; + + function getMenu(aThisObj) { + return aThisObj.menu; + } + } + + function focusInsideMenu(aMenuID, aMenuBarID) { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode(aMenuID)), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)), + ]; + + this.invoke = function focusInsideMenu_invoke() { + getNode(aMenuID).focus(); + }; + + this.getID = function focusInsideMenu_getID() { + return "focus menu '" + aMenuID + "'"; + }; + } + + function blurMenu(aMenuBarID) { + var eventSeq = [ + new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)), + new invokerChecker(EVENT_FOCUS, getNode("outsidemenu")), + ]; + + this.__proto__ = new synthClick("outsidemenu", eventSeq); + + this.getID = function blurMenu_getID() { + return "blur menu"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuging + // enableLogging("tree,events,verbose"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new focusMenu("menubar2", "menu-help")); + gQueue.push(new focusMenu("menubar", "menu-file", "menubar2")); + gQueue.push(new showMenu("menupopup-file", "menu-file", kViaDisplayStyle)); + gQueue.push(new closeMenu("menupopup-file", "menu-file", kViaDisplayStyle)); + gQueue.push(new showMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle)); + gQueue.push(new closeMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle)); + gQueue.push(new focusInsideMenu("menu-edit", "menubar")); + gQueue.push(new blurMenu("menubar")); + + gQueue.push(new focusMenu("menubar3", "mb3-mi-outside")); + gQueue.push(new showMenu("mb4-menu", document, kViaDisplayStyle)); + gQueue.push(new focusMenu("menubar4", "mb4-item1")); + gQueue.push(new focusMenu("menubar5", "mb5-mi")); + + gQueue.push(new synthFocus("mi6")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606207" + title="Dojo dropdown buttons are broken"> + Bug 606207 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=614829" + title="Menupopup end event isn't fired for ARIA menus"> + Bug 614829 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189" + title="Clean up FireAccessibleFocusEvent"> + Bug 615189 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=933322" + title="menustart/end events are missing when aria-owns makes a menu hierarchy"> + Bug 933322 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=934460" + title="menustart/end events may be missed when top level menuitem is focused"> + Bug 934460 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=970005" + title="infinite long loop in a11y:FocusManager::ProcessFocusEvent"> + Bug 970005 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="menubar" role="menubar"> + <div id="menu-file" role="menuitem" tabindex="0"> + File + <div id="menupopup-file" role="menu" style="display: none;"> + <div id="menuitem-newtab" role="menuitem" tabindex="0">New Tab</div> + <div id="menuitem-newwindow" role="menuitem" tabindex="0">New Window</div> + </div> + </div> + <div id="menu-edit" role="menuitem" tabindex="0"> + Edit + <div id="menupopup-edit" role="menu" style="visibility: hidden;"> + <div id="menuitem-undo" role="menuitem" tabindex="0">Undo</div> + <div id="menuitem-redo" role="menuitem" tabindex="0">Redo</div> + </div> + </div> + </div> + <div id="menubar2" role="menubar"> + <div id="menu-help" role="menuitem" tabindex="0"> + Help + <div id="menupopup-help" role="menu" style="display: none;"> + <div id="menuitem-about" role="menuitem" tabindex="0">About</div> + </div> + </div> + </div> + <div tabindex="0" id="outsidemenu">outsidemenu</div> + + <!-- aria-owns relations --> + <div id="menubar3" role="menubar" aria-owns="mb3-mi-outside"></div> + <div id="mb3-mi-outside" role="menuitem" tabindex="0">Outside</div> + + <div id="menubar4" role="menubar"> + <div id="mb4_topitem" role="menuitem" aria-haspopup="true" + aria-owns="mb4-menu">Item</div> + </div> + <div id="mb4-menu" role="menu" style="display:none;"> + <div role="menuitem" id="mb4-item1" tabindex="0">Item 1.1</div> + <div role="menuitem" tabindex="0">Item 1.2</div> + </div> + + <!-- focus top-level menu item having haspopup --> + <div id="menubar5" role="menubar"> + <div role="menuitem" aria-haspopup="true" id="mb5-mi" tabindex="0"> + Item + <div role="menu" style="display:none;"> + <div role="menuitem" tabindex="0">Item 1.1</div> + <div role="menuitem" tabindex="0">Item 1.2</div> + </div> + </div> + </div> + + <!-- other aria-owns relations --> + <div id="mi6" tabindex="0" role="menuitem">aria-owned item</div> + <div aria-owns="mi6">Obla</div> + + <div id="eventdump"></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_objattr.html b/accessible/tests/mochitest/events/test_aria_objattr.html new file mode 100644 index 0000000000..709089ca02 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_objattr.html @@ -0,0 +1,68 @@ +<html> + +<head> + <title>Accessible ARIA object attribute changes</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + /** + * Do tests. + */ + var gQueue = null; + function updateAttribute(aID, aAttr, aValue) { + this.node = getNode(aID); + this.accessible = getAccessible(this.node); + + this.eventSeq = [ + new objAttrChangedChecker(aID, aAttr), + ]; + + this.invoke = function updateAttribute_invoke() { + this.node.setAttribute(aAttr, aValue); + }; + + this.getID = function updateAttribute_getID() { + return aAttr + " for " + aID + " " + aValue; + }; + } + + // gA11yEventDumpToConsole = true; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new updateAttribute("sortable", "aria-sort", "ascending")); + + // For experimental ARIA extensions + gQueue.push(new updateAttribute("custom", "aria-blah", "true")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="sortable" role="columnheader" aria-sort="none">aria-sort</div> + + <div id="custom" role="custom" aria-blah="false">Fat free cheese</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_owns.html b/accessible/tests/mochitest/events/test_aria_owns.html new file mode 100644 index 0000000000..3c638ad838 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_owns.html @@ -0,0 +1,122 @@ +<html> + +<head> + <title>Aria-owns targets shouldn't be on invalidation list so shouldn't have + show/hide events</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Do tests. + + // gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("tree,eventTree,verbose"); + + /** + * Aria-owns target shouldn't have a show event. + * Markup: + * <div id="t1_fc" aria-owns="t1_owns"></div> + * <span id="t1_owns"></div> + */ + function testAriaOwns() { + this.parent = getNode("t1"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t1_fc"); + this.owns = document.createElement("span"); + this.owns.setAttribute("id", "t1_owns"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function testAriaOwns_invoke() { + getNode("t1").appendChild(this.fc); + getNode("t1").appendChild(this.owns); + getNode("t1_fc").setAttribute("aria-owns", "t1_owns"); + }; + + this.getID = function testAriaOwns_getID() { + return "Aria-owns target shouldn't have show event"; + }; + } + + /** + * Target of both aria-owns and other aria attribute like aria-labelledby + * shouldn't have a show event. + * Markup: + * <div id="t2_fc" aria-owns="t1_owns"></div> + * <div id="t2_sc" aria-labelledby="t2_owns"></div> + * <span id="t2_owns"></div> + */ + function testAriaOwnsAndLabelledBy() { + this.parent = getNode("t2"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t2_fc"); + this.sc = document.createElement("div"); + this.sc.setAttribute("id", "t2_sc"); + this.owns = document.createElement("span"); + this.owns.setAttribute("id", "t2_owns"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new invokerChecker(EVENT_SHOW, this.sc), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function testAriaOwns_invoke() { + getNode("t2").appendChild(this.fc); + getNode("t2").appendChild(this.sc); + getNode("t2").appendChild(this.owns); + getNode("t2_fc").setAttribute("aria-owns", "t2_owns"); + getNode("t2_sc").setAttribute("aria-labelledby", "t2_owns"); + }; + + this.getID = function testAriaOwns_getID() { + return "Aria-owns and aria-labelledby target shouldn't have show event"; + }; + } + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + gQueue.push(new testAriaOwns()); + gQueue.push(new testAriaOwnsAndLabelledBy()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1296420" + title="Aria-owns targets shouldn't be on invalidation list so shouldn't + have show/hide events"> + Mozilla Bug 1296420 + </a><br> + + <div id="testContainer"> + <div id="t1"></div> + + <div id="t2"></div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_statechange.html b/accessible/tests/mochitest/events/test_aria_statechange.html new file mode 100644 index 0000000000..7796d88ec4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_statechange.html @@ -0,0 +1,231 @@ +<html> + +<head> + <title>ARIA state change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debugging + // gA11yEventDumpToConsole = true; // debugging + + function expandNode(aID, aIsExpanded) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new expandedStateChecker(aIsExpanded, this.DOMNode), + ]; + + this.invoke = function expandNode_invoke() { + this.DOMNode.setAttribute("aria-expanded", + (aIsExpanded ? "true" : "false")); + }; + + this.getID = function expandNode_getID() { + return prettyName(aID) + " aria-expanded changed to '" + aIsExpanded + "'"; + }; + } + + function busyify(aID, aIsBusy) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(STATE_BUSY, kOrdinalState, aIsBusy, this.DOMNode), + ]; + + this.invoke = function busyify_invoke() { + this.DOMNode.setAttribute("aria-busy", (aIsBusy ? "true" : "false")); + }; + + this.getID = function busyify_getID() { + return prettyName(aID) + " aria-busy changed to '" + aIsBusy + "'"; + }; + } + + function makeCurrent(aID, aIsCurrent, aValue) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(EXT_STATE_CURRENT, true, aIsCurrent, this.DOMNode), + ]; + + this.invoke = function makeCurrent_invoke() { + this.DOMNode.setAttribute("aria-current", aValue); + }; + + this.getID = function makeCurrent_getID() { + return prettyName(aID) + " aria-current changed to " + aValue; + }; + } + + async function testToggleAttribute(aID, aAttribute, aIncludeMixed) { + let toggleState = aAttribute == "aria-pressed" ? STATE_PRESSED : STATE_CHECKED; + + // bug 472142. Role changes here if aria-pressed is added, + // accessible should be recreated? + let stateChange = PromEvents.waitForStateChange(aID, toggleState, true); + getNode(aID).setAttribute(aAttribute, "true"); + await stateChange; + + stateChange = PromEvents.waitForStateChange(aID, toggleState, false); + getNode(aID).setAttribute(aAttribute, "false"); + await stateChange; + + if (aIncludeMixed) { + stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, true); + getNode(aID).setAttribute(aAttribute, "mixed"); + await stateChange; + + stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, false); + getNode(aID).setAttribute(aAttribute, ""); + await stateChange; + } + + stateChange = PromEvents.waitForStateChange(aID, toggleState, true); + getNode(aID).setAttribute(aAttribute, "true"); + await stateChange; + + if (aIncludeMixed) { + stateChange = Promise.all([ + PromEvents.waitForStateChange(aID, STATE_MIXED, true), + PromEvents.waitForStateChange(aID, toggleState, false)]); + getNode(aID).setAttribute(aAttribute, "mixed"); + await stateChange; + + stateChange = Promise.all([ + PromEvents.waitForStateChange(aID, STATE_MIXED, false), + PromEvents.waitForStateChange(aID, toggleState, true)]); + getNode(aID).setAttribute(aAttribute, "true"); + await stateChange; + } + + // bug 472142. Role changes here too if aria-pressed is removed, + // accessible should be recreated? + stateChange = PromEvents.waitForStateChange(aID, toggleState, false); + getNode(aID).removeAttribute(aAttribute); + await stateChange; + } + + async function doTests() { + gQueue = new eventQueue(); + + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new expandNode("section", true)); + gQueue.push(new expandNode("section", false)); + gQueue.push(new expandNode("div", true)); + gQueue.push(new expandNode("div", false)); + + gQueue.push(new busyify("aria_doc", true)); + gQueue.push(new busyify("aria_doc", false)); + + gQueue.push(new makeCurrent("current_page_1", false, "false")); + gQueue.push(new makeCurrent("current_page_2", true, "page")); + gQueue.push(new makeCurrent("current_page_2", false, "false")); + gQueue.push(new makeCurrent("current_page_3", true, "true")); + gQueue.push(new makeCurrent("current_page_3", false, "")); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + await testToggleAttribute("pressable", "aria-pressed", true); + await testToggleAttribute("pressable_native", "aria-pressed", true); + await testToggleAttribute("checkable", "aria-checked", true); + await testToggleAttribute("checkableBool", "aria-checked", false); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=551684" + title="No statechange event for aria-expanded on native HTML elements, is fired on ARIA widgets"> + Mozilla Bug 551684 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=648133" + title="fire state change event for aria-busy"> + Mozilla Bug 648133 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467143" + title="mixed state change event is fired for focused accessible only"> + Mozilla Bug 467143 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958" + title="Pressed state is not exposed on a button element with aria-pressed attribute"> + Mozilla Bug 989958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563" + title="Support ARIA 1.1 switch role"> + Mozilla Bug 1136563 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921" + title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE"> + Mozilla Bug 1355921 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- aria-expanded --> + <div id="section" role="section" aria-expanded="false">expandable section</div> + <div id="div" aria-expanded="false">expandable native div</div> + + <!-- aria-busy --> + <div id="aria_doc" role="document" tabindex="0">A document</div> + + <!-- aria-pressed --> + <div id="pressable" role="button"></div> + <button id="pressable_native"></button> + + <!-- aria-checked --> + <div id="checkable" role="checkbox"></div> + <div id="checkableBool" role="switch"></div> + + <!-- aria-current --> + <div id="current_page_1" role="link" aria-current="page">1</div> + <div id="current_page_2" role="link" aria-current="false">2</div> + <div id="current_page_3" role="link">3</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_attrchange.html b/accessible/tests/mochitest/events/test_attrchange.html new file mode 100644 index 0000000000..edd9195ddd --- /dev/null +++ b/accessible/tests/mochitest/events/test_attrchange.html @@ -0,0 +1,107 @@ +<html> + +<head> + <title>Accessible attr change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + async function testGotAttrChange(elem, name, value) { + const waitFor = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, elem); + if (value) { + document.getElementById(elem).setAttribute(name, value); + } else { + document.getElementById(elem).removeAttribute(name); + } + await waitFor; + } + + async function doTests() { + info("Removing summary attr"); + // after summary is removed, we should have a layout table + await testGotAttrChange( + "sampleTable", + "summary", + null + ); + + info("Setting abbr attr"); + // after abbr is set we should have a data table again + await testGotAttrChange( + "cellOne", + "abbr", + "hello world" + ); + + info("Removing abbr attr"); + // after abbr is removed we should have a layout table again + await testGotAttrChange( + "cellOne", + "abbr", + null + ); + + info("Setting scope attr"); + // after scope is set we should have a data table again + await testGotAttrChange( + "cellOne", + "scope", + "col" + ); + + info("Removing scope attr"); + // remove scope should give layout + await testGotAttrChange( + "cellOne", + "scope", + null + ); + + info("Setting headers attr"); + // add headers attr should give data + await testGotAttrChange( + "cellThree", + "headers", + "cellOne" + ); + + info("Removing headers attr"); + // remove headers attr should give layout + await testGotAttrChange( + "cellThree", + "headers", + null + ); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<body> + <table id="sampleTable" summary="example summary"> + <tr> + <td id="cellOne">cell1</td> + <td>cell2</td> + </tr> + <tr> + <td id="cellThree">cell3</td> + <td>cell4</td> + </tr> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_attrs.html b/accessible/tests/mochitest/events/test_attrs.html new file mode 100644 index 0000000000..c09bd9cf1e --- /dev/null +++ b/accessible/tests/mochitest/events/test_attrs.html @@ -0,0 +1,85 @@ +<html> + +<head> + <title>Event object attributes tests</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + /** + * Test "event-from-input" object attribute. + */ + function eventFromInputChecker(aEventType, aID, aValue, aNoTargetID) { + this.type = aEventType; + this.target = getAccessible(aID); + + this.noTarget = getNode(aNoTargetID); + + this.check = function checker_check(aEvent) { + testAttrs(aEvent.accessible, { "event-from-input": aValue }, true); + + var accessible = getAccessible(this.noTarget); + testAbsentAttrs(accessible, { "event-from-input": "" }); + }; + } + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; // debug stuff + + function doTests() { + gQueue = new eventQueue(); + + var id = "textbox", noTargetId = "textarea"; + let checker = + new eventFromInputChecker(EVENT_FOCUS, id, "false", noTargetId); + gQueue.push(new synthFocus(id, checker)); + + if (!MAC) { // Mac failure is bug 541093 + checker = + new eventFromInputChecker(EVENT_TEXT_CARET_MOVED, id, "true", noTargetId); + gQueue.push(new synthHomeKey(id, checker)); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=540285" + title="Event object attributes testing"> + Mozilla Bug 540285 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox" value="hello"> + <textarea id="textarea"></textarea> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_bug1322593-2.html b/accessible/tests/mochitest/events/test_bug1322593-2.html new file mode 100644 index 0000000000..05bd31ffa6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593-2.html @@ -0,0 +1,77 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function changeMultipleElements() { + this.node1 = getNode("span1"); + this.node2 = getNode("span2"); + + this.eventSeq = [ + new textChangeChecker("container", 0, 5, "hello", false, undefined, true), + new textChangeChecker("container", 6, 11, "world", false, undefined, true), + new orderChecker(), + new textChangeChecker("container", 0, 1, "a", true, undefined, true), + new textChangeChecker("container", 7, 8, "b", true, undefined, true), + ]; + + this.invoke = function changeMultipleElements_invoke() { + this.node1.textContent = "a"; + this.node2.textContent = "b"; + }; + + this.getID = function changeMultipleElements_invoke_getID() { + return "Change the text content of multiple sibling divs"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests +// gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new changeMultipleElements()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593" + title="missing text change events when multiple elements updated at once"> + Mozilla Bug 1322593 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"> + <span id="span1">hello</span> + <span>your</span> + <span id="span2">world</span> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_bug1322593.html b/accessible/tests/mochitest/events/test_bug1322593.html new file mode 100644 index 0000000000..968e808106 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593.html @@ -0,0 +1,74 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function changeMultipleElements() { + this.node1 = getNode("div1"); + this.node2 = getNode("div2"); + + this.eventSeq = [ + new textChangeChecker("div1", 0, 5, "hello", false, undefined, true), + new textChangeChecker("div2", 0, 5, "world", false, undefined, true), + new orderChecker(), + new textChangeChecker("div1", 0, 1, "a", true, undefined, true), + new textChangeChecker("div2", 0, 1, "b", true, undefined, true), + ]; + + this.invoke = function changeMultipleElements_invoke() { + this.node1.textContent = "a"; + this.node2.textContent = "b"; + }; + + this.getID = function changeMultipleElements_invoke_getID() { + return "Change the text content of multiple sibling divs"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests +// gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new changeMultipleElements()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593" + title="missing text change events when multiple elements updated at once"> + Mozilla Bug 1322593 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="div1">hello</div> + <div id="div2">world</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_caretmove.html b/accessible/tests/mochitest/events/test_caretmove.html new file mode 100644 index 0000000000..d1091ac7f1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_caretmove.html @@ -0,0 +1,142 @@ +<html> + +<head> + <title>Accessible caret move events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Click checker. + */ + function clickChecker(aCaretOffset, aIsSelectionCollapsed, aID, aExtraNodeOrID, aExtraCaretOffset) { + this.__proto__ = new caretMoveChecker(aCaretOffset, aIsSelectionCollapsed, aID); + + this.extraNode = getNode(aExtraNodeOrID); + + this.check = function clickChecker_check(aEvent) { + this.__proto__.check(aEvent); + + if (this.extraNode) { + var acc = getAccessible(this.extraNode, [nsIAccessibleText]); + is(acc.caretOffset, aExtraCaretOffset, + "Wrong caret offset for " + aExtraNodeOrID); + } + }; + } + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpToConsole = true; + + function doTests() { + // test caret move events and caret offsets + gQueue = new eventQueue(); + + var id = "textbox"; + gQueue.push(new synthFocus(id, new caretMoveChecker(5, true, id))); + gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id))); + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + + if (!MAC) { + gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id))); + gQueue.push(new synthHomeKey(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + } + else { + todo(false, "Make these tests pass on OSX (bug 650294)"); + } + + id = "textarea"; + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + gQueue.push(new synthDownKey(id, new caretMoveChecker(12, true, id))); + + id = "textarea_wrapped"; + gQueue.push(new setCaretOffset(id, 4, id)); + gQueue.push(new synthLeftKey(id, new caretMoveChecker(4, true, id))); + + id = "p"; + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + gQueue.push(new synthDownKey(id, new caretMoveChecker(6, true, id))); + + id = "p1_in_div"; + gQueue.push(new synthClick(id, new clickChecker(0, true, id, "p2_in_div", -1))); + + id = "p"; + gQueue.push(new synthShiftTab(id, new caretMoveChecker(0, true, id))); + id = "textarea"; + gQueue.push(new synthShiftTab(id, new caretMoveChecker(12, true, id))); + id = "p"; + gQueue.push(new synthTab(id, new caretMoveChecker(0, true, id))); + + // Set caret after a child of span element, i.e. after 'text' text. + gQueue.push(new moveCaretToDOMPoint("test1", getNode("test1_span"), 1, + 4, "test1")); + gQueue.push(new moveCaretToDOMPoint("test2", getNode("test2_span"), 1, + 4, "test2")); + + // empty text element + gQueue.push(new moveCaretToDOMPoint("test3", getNode("test3"), 0, + 0, "test3")); + gQueue.push(new moveCaretToDOMPoint("test4", getNode("test4_span"), 0, + 0, "test4")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=454377" + title="Accessible caret move events testing"> + Bug 454377 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=567571" + title="caret-moved events missing at the end of a wrapped line of text"> + Bug 567571 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=824901" + title="HyperTextAccessible::DOMPointToHypertextOffset fails for node and offset equal to node child count"> + Bug 824901 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox" value="hello"/> + + <textarea id="textarea">text<br>text</textarea> + <p id="p" contentEditable="true"><span>text</span><br/>text</p> + <div id="div" contentEditable="true"><p id="p1_in_div">text</p><p id="p2_in_div">text</p></div> + + <p contentEditable="true" id="test1"><span id="test1_span">text</span>ohoho</p> + <p contentEditable="true" id="test2"><span><span id="test2_span">text</span></span>ohoho</p> + <p contentEditable="true" id="test3"></p> + <p contentEditable="true" id="test4"><span id="test4_span"></span></p> + + <textarea id="textarea_wrapped" cols="5">hey friend</textarea> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_coalescence.html b/accessible/tests/mochitest/events/test_coalescence.html new file mode 100644 index 0000000000..0f8ad52a8b --- /dev/null +++ b/accessible/tests/mochitest/events/test_coalescence.html @@ -0,0 +1,817 @@ +<html> + +<head> + <title>Accessible mutation events coalescence testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invoker base classes + + const kRemoveElm = 1; + const kHideElm = 2; + const kAddElm = 3; + const kShowElm = 4; + + /** + * Base class to test of mutation events coalescence. + */ + function coalescenceBase(aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace) { + // Invoker interface + + this.invoke = function coalescenceBase_invoke() { + if (aPerformActionOnChildInTheFirstPlace) { + this.invokeAction(this.childNode, aChildAction); + this.invokeAction(this.parentNode, aParentAction); + } else { + this.invokeAction(this.parentNode, aParentAction); + this.invokeAction(this.childNode, aChildAction); + } + }; + + this.getID = function coalescenceBase_getID() { + var childAction = this.getActionName(aChildAction) + " child"; + var parentAction = this.getActionName(aParentAction) + " parent"; + + if (aPerformActionOnChildInTheFirstPlace) + return childAction + " and then " + parentAction; + + return parentAction + " and then " + childAction; + }; + + this.finalCheck = function coalescenceBase_check() { + if (this.getEventType(aChildAction) == EVENT_HIDE) { + testIsDefunct(this.child); + } + if (this.getEventType(aParentAction) == EVENT_HIDE) { + testIsDefunct(this.parent); + } + }; + + // Implementation details + + this.invokeAction = function coalescenceBase_invokeAction(aNode, aAction) { + switch (aAction) { + case kRemoveElm: + aNode.remove(); + break; + + case kHideElm: + aNode.style.display = "none"; + break; + + case kAddElm: + if (aNode == this.parentNode) + this.hostNode.appendChild(this.parentNode); + else + this.parentNode.appendChild(this.childNode); + break; + + case kShowElm: + aNode.style.display = "block"; + break; + + default: + return INVOKER_ACTION_FAILED; + } + // 0 means the action succeeded. + return 0; + }; + + this.getEventType = function coalescenceBase_getEventType(aAction) { + switch (aAction) { + case kRemoveElm: case kHideElm: + return EVENT_HIDE; + case kAddElm: case kShowElm: + return EVENT_SHOW; + } + return 0; + }; + + this.getActionName = function coalescenceBase_getActionName(aAction) { + switch (aAction) { + case kRemoveElm: + return "remove"; + case kHideElm: + return "hide"; + case kAddElm: + return "add"; + case kShowElm: + return "show"; + default: + return "??"; + } + }; + + this.initSequence = function coalescenceBase_initSequence() { + // expected events + var eventType = this.getEventType(aParentAction); + this.eventSeq = [ + new invokerChecker(eventType, this.parentNode), + new invokerChecker(EVENT_REORDER, this.hostNode), + ]; + + // unexpected events + this.unexpectedEventSeq = [ + new invokerChecker(this.getEventType(aChildAction), this.childNode), + new invokerChecker(EVENT_REORDER, this.parentNode), + ]; + }; + } + + /** + * Remove or hide mutation events coalescence testing. + */ + function removeOrHideCoalescenceBase(aChildID, aParentID, + aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace) { + this.__proto__ = new coalescenceBase(aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace); + + this.init = function removeOrHideCoalescenceBase_init() { + this.childNode = getNode(aChildID); + this.parentNode = getNode(aParentID); + this.child = getAccessible(this.childNode); + this.parent = getAccessible(this.parentNode); + this.hostNode = this.parentNode.parentNode; + }; + + // Initalization + + this.init(); + this.initSequence(); + } + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Remove child node and then its parent node from DOM tree. + */ + function removeChildNParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kRemoveElm, + true); + } + + /** + * Remove parent node and then its child node from DOM tree. + */ + function removeParentNChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kRemoveElm, + false); + } + + /** + * Hide child node and then its parent node. + */ + function hideChildNParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kHideElm, + true); + } + + /** + * Hide parent node and then its child node. + */ + function hideParentNChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kHideElm, + false); + } + + /** + * Hide child node and then remove its parent node. + */ + function hideChildNRemoveParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kRemoveElm, + true); + } + + /** + * Hide parent node and then remove its child node. + */ + function hideParentNRemoveChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kHideElm, + false); + } + + /** + * Remove child node and then hide its parent node. + */ + function removeChildNHideParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kHideElm, + true); + } + + /** + * Remove parent node and then hide its child node. + */ + function removeParentNHideChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kRemoveElm, + false); + } + + /** + * Create and append parent node and create and append child node to it. + */ + function addParentNChild(aHostID, aPerformActionOnChildInTheFirstPlace) { + this.init = function addParentNChild_init() { + this.hostNode = getNode(aHostID); + this.parentNode = document.createElement("select"); + this.childNode = document.createElement("option"); + this.childNode.textContent = "testing"; + }; + + this.__proto__ = new coalescenceBase(kAddElm, kAddElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Show parent node and show child node to it. + */ + function showParentNChild(aParentID, aChildID, + aPerformActionOnChildInTheFirstPlace) { + this.init = function showParentNChild_init() { + this.parentNode = getNode(aParentID); + this.hostNode = this.parentNode.parentNode; + this.childNode = getNode(aChildID); + }; + + this.__proto__ = new coalescenceBase(kShowElm, kShowElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Create and append child node to the DOM and then show parent node. + */ + function showParentNAddChild(aParentID, + aPerformActionOnChildInTheFirstPlace) { + this.init = function showParentNAddChild_init() { + this.parentNode = getNode(aParentID); + this.hostNode = this.parentNode.parentNode; + this.childNode = document.createElement("option"); + this.childNode.textContent = "testing"; + }; + + this.__proto__ = new coalescenceBase(kAddElm, kShowElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Remove children and parent + */ + function removeGrandChildrenNHideParent(aChild1Id, aChild2Id, aParentId) { + this.child1 = getNode(aChild1Id); + this.child2 = getNode(aChild2Id); + this.parent = getNode(aParentId); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible(aParentId)), + new invokerChecker(EVENT_REORDER, getNode(aParentId).parentNode), + new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild1Id)), + new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild2Id)), + new unexpectedInvokerChecker(EVENT_REORDER, getAccessible(aParentId)), + ]; + + this.invoke = function removeGrandChildrenNHideParent_invoke() { + this.child1.remove(); + this.child2.remove(); + this.parent.hidden = true; + }; + + this.getID = function removeGrandChildrenNHideParent_getID() { + return "remove grand children of different parents and then hide their grand parent"; + }; + } + + /** + * Remove a child, and then its parent. + */ + function test3() { + this.o = getAccessible("t3_o"); + this.ofc = getAccessible("t3_o").firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.o), + new invokerChecker(EVENT_REORDER, "t3_lb"), + new unexpectedInvokerChecker(EVENT_HIDE, this.ofc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o), + ]; + + this.invoke = function test3_invoke() { + getNode("t3_o").textContent = ""; + getNode("t3_lb").removeChild(getNode("t3_o")); + }; + + this.finalCheck = function test3_finalCheck() { + testIsDefunct(this.o); + testIsDefunct(this.ofc); + }; + + this.getID = function test3_getID() { + return "remove a child, and then its parent"; + }; + } + + /** + * Remove children, and then a parent of 2nd child. + */ + function test4() { + this.o1 = getAccessible("t4_o1"); + this.o1fc = this.o1.firstChild; + this.o2 = getAccessible("t4_o2"); + this.o2fc = this.o2.firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.o1fc), + new invokerChecker(EVENT_HIDE, this.o2), + new invokerChecker(EVENT_REORDER, "t4_lb"), + new unexpectedInvokerChecker(EVENT_HIDE, this.o2fc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o1), + new unexpectedInvokerChecker(EVENT_REORDER, this.o2), + ]; + + this.invoke = function test4_invoke() { + getNode("t4_o1").textContent = ""; + getNode("t4_o2").textContent = ""; + getNode("t4_lb").removeChild(getNode("t4_o2")); + }; + + this.finalCheck = function test4_finalCheck() { + testIsDefunct(this.o1fc); + testIsDefunct(this.o2); + testIsDefunct(this.o2fc); + }; + + this.getID = function test4_getID() { + return "remove children, and then a parent of 2nd child"; + }; + } + + /** + * Remove a child, remove a parent sibling, remove the parent + */ + function test5() { + this.o = getAccessible("t5_o"); + this.ofc = this.o.firstChild; + this.b = getAccessible("t5_b"); + this.lb = getAccessible("t5_lb"); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.b), + new invokerChecker(EVENT_HIDE, this.o), + new invokerChecker(EVENT_REORDER, "t5"), + new unexpectedInvokerChecker(EVENT_HIDE, this.ofc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o), + new unexpectedInvokerChecker(EVENT_REORDER, this.lb), + ]; + + this.invoke = function test5_invoke() { + getNode("t5_o").textContent = ""; + getNode("t5").removeChild(getNode("t5_b")); + getNode("t5_lb").removeChild(getNode("t5_o")); + }; + + this.finalCheck = function test5_finalCheck() { + testIsDefunct(this.ofc); + testIsDefunct(this.o); + testIsDefunct(this.b); + }; + + this.getID = function test5_getID() { + return "remove a child, remove a parent sibling, remove the parent"; + }; + } + + /** + * Insert accessibles with a child node moved by aria-owns + * Markup: + * <div id="t6_fc"> + * <div id="t6_owns"></div> + * </div> + * <div id="t6_sc" aria-owns="t6_owns"></div> + */ + function test6() { + this.parent = getNode("t6"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t6_fc"); + this.owns = document.createElement("div"); + this.owns.setAttribute("id", "t6_owns"); + this.sc = document.createElement("div"); + this.sc.setAttribute("id", "t6_sc"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new invokerChecker(EVENT_SHOW, this.sc), + new invokerChecker(EVENT_REORDER, this.parent), + new unexpectedInvokerChecker(EVENT_REORDER, this.fc), + new unexpectedInvokerChecker(EVENT_REORDER, this.sc), + new unexpectedInvokerChecker(EVENT_HIDE, this.owns), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function test6_invoke() { + getNode("t6").appendChild(this.fc); + getNode("t6_fc").appendChild(this.owns); + getNode("t6").appendChild(this.sc); + getNode("t6_sc").setAttribute("aria-owns", "t6_owns"); + }; + + this.getID = function test6_getID() { + return "Insert accessibles with a child node moved by aria-owns"; + }; + } + + /** + * Insert text nodes under direct and grand children, and then hide + * their container by means of aria-owns. + * + * Markup: + * <div id="t7_moveplace" aria-owns="t7_c"></div> + * <div id="t7_c"> + * <div id="t7_c_directchild">ha</div> + * <div><div id="t7_c_grandchild">ha</div></div> + * </div> + */ + function test7() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t7_c")), + new invokerChecker(EVENT_SHOW, getNode("t7_c")), + new invokerChecker(EVENT_REORDER, getNode("t7")), + new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_directchild")), + new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_grandchild")), + new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_directchild").firstChild), + new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_grandchild").firstChild), + ]; + + this.invoke = function test7_invoke() { + getNode("t7_c_directchild").textContent = "ha"; + getNode("t7_c_grandchild").textContent = "ha"; + getNode("t7_moveplace").setAttribute("aria-owns", "t7_c"); + }; + + this.getID = function test7_getID() { + return "Show child accessibles and then hide their container"; + }; + } + + /** + * Move a node by aria-owns from right to left in the tree, so that + * the eventing looks this way: + * reorder for 't8_c1' + * hide for 't8_c1_child' + * show for 't8_c2_moved' + * reorder for 't8_c2' + * hide for 't8_c2_moved' + * + * The hide event should be delivered before the paired show event. + */ + function test8() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t8_c1_child")), + new invokerChecker(EVENT_HIDE, "t8_c2_moved"), + new invokerChecker(EVENT_SHOW, "t8_c2_moved"), + new invokerChecker(EVENT_REORDER, "t8_c2"), + new invokerChecker(EVENT_REORDER, "t8_c1"), + ]; + + this.invoke = function test8_invoke() { + // Remove a node from 't8_c1' container to give the event tree a + // desired structure (the 't8_c1' container node goes first in the event + // tree) + getNode("t8_c1_child").remove(); + // then move 't8_c2_moved' from 't8_c2' to 't8_c1'. + getNode("t8_c1").setAttribute("aria-owns", "t8_c2_moved"); + }; + + this.getID = function test8_getID() { + return "Move a node by aria-owns to left within the tree"; + }; + } + + /** + * Move 't9_c3_moved' node under 't9_c2_moved', and then move 't9_c2_moved' + * node by aria-owns (same as test10 but has different aria-owns + * ordering), the eventing looks same way as in test10: + * reorder for 't9_c1' + * hide for 't9_c1_child' + * show for 't9_c2_moved' + * reorder for 't9_c2' + * hide for 't9_c2_child' + * hide for 't9_c2_moved' + * reorder for 't9_c3' + * hide for 't9_c3_moved' + * + * The hide events for 't9_c2_moved' and 't9_c3_moved' should be delivered + * before the show event for 't9_c2_moved'. + */ + function test9() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t9_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t9_c2_child")), + new invokerChecker(EVENT_HIDE, "t9_c3_moved"), + new invokerChecker(EVENT_HIDE, "t9_c2_moved"), + new invokerChecker(EVENT_SHOW, "t9_c2_moved"), + new invokerChecker(EVENT_REORDER, "t9_c3"), + new invokerChecker(EVENT_REORDER, "t9_c2"), + new invokerChecker(EVENT_REORDER, "t9_c1"), + new unexpectedInvokerChecker(EVENT_SHOW, "t9_c3_moved"), + ]; + + this.invoke = function test9_invoke() { + // Remove child nodes from 't9_c1' and 't9_c2' containers to give + // the event tree a needed structure ('t9_c1' and 't9_c2' nodes go + // first in the event tree), + getNode("t9_c1_child").remove(); + getNode("t9_c2_child").remove(); + // then do aria-owns magic. + getNode("t9_c2_moved").setAttribute("aria-owns", "t9_c3_moved"); + getNode("t9_c1").setAttribute("aria-owns", "t9_c2_moved"); + }; + + this.getID = function test9_getID() { + return "Move node #1 by aria-owns and then move node #2 into node #1"; + }; + } + + /** + * Move a node 't10_c3_moved' by aria-owns under a node 't10_c2_moved', + * moved by under 't10_1', so that the eventing looks this way: + * reorder for 't10_c1' + * hide for 't10_c1_child' + * show for 't10_c2_moved' + * reorder for 't10_c2' + * hide for 't10_c2_child' + * hide for 't10_c2_moved' + * reorder for 't10_c3' + * hide for 't10_c3_moved' + * + * The hide events for 't10_c2_moved' and 't10_c3_moved' should be delivered + * before the show event for 't10_c2_moved'. + */ + function test10() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t10_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t10_c2_child")), + new invokerChecker(EVENT_HIDE, getNode("t10_c2_moved")), + new invokerChecker(EVENT_HIDE, getNode("t10_c3_moved")), + new invokerChecker(EVENT_SHOW, getNode("t10_c2_moved")), + new invokerChecker(EVENT_REORDER, "t10_c2"), + new invokerChecker(EVENT_REORDER, "t10_c1"), + new invokerChecker(EVENT_REORDER, "t10_c3"), + ]; + + this.invoke = function test10_invoke() { + // Remove child nodes from 't10_c1' and 't10_c2' containers to give + // the event tree a needed structure ('t10_c1' and 't10_c2' nodes go first + // in the event tree), + getNode("t10_c1_child").remove(); + getNode("t10_c2_child").remove(); + // then do aria-owns stuff. + getNode("t10_c1").setAttribute("aria-owns", "t10_c2_moved"); + getNode("t10_c2_moved").setAttribute("aria-owns", "t10_c3_moved"); + }; + + this.getID = function test10_getID() { + return "Move a node by aria-owns into a node moved by aria-owns to left within the tree"; + }; + } + + /** + * Move a node by aria-owns from right to left in the tree, and then + * move its parent too by aria-owns. No hide event should be fired for + * original node. + */ + function test11() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t11_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t11_c2")), + new orderChecker(), + new asyncInvokerChecker(EVENT_SHOW, "t11_c2_child"), + new asyncInvokerChecker(EVENT_SHOW, "t11_c2"), + new orderChecker(), + new invokerChecker(EVENT_REORDER, "t11"), + new unexpectedInvokerChecker(EVENT_HIDE, "t11_c2_child"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c1"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c2"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c3"), + ]; + + this.invoke = function test11_invoke() { + // Remove a node from 't11_c1' container to give the event tree a + // desired structure (the 't11_c1' container node goes first in + // the event tree), + getNode("t11_c1_child").remove(); + // then move 't11_c2_moved' from 't11_c2' to 't11_c1', and then move + // 't11_c2' to 't11_c3'. + getNode("t11_c1").setAttribute("aria-owns", "t11_c2_child"); + getNode("t11_c3").setAttribute("aria-owns", "t11_c2"); + }; + + this.getID = function test11_getID() { + return "Move a node by aria-owns to left within the tree"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests. + + gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("eventTree"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new removeChildNParent("option1", "select1")); + gQueue.push(new removeParentNChild("option2", "select2")); + gQueue.push(new hideChildNParent("option3", "select3")); + gQueue.push(new hideParentNChild("option4", "select4")); + gQueue.push(new hideChildNRemoveParent("option5", "select5")); + gQueue.push(new hideParentNRemoveChild("option6", "select6")); + gQueue.push(new removeChildNHideParent("option7", "select7")); + gQueue.push(new removeParentNHideChild("option8", "select8")); + + gQueue.push(new addParentNChild("testContainer", false)); + gQueue.push(new addParentNChild("testContainer", true)); + gQueue.push(new showParentNChild("select9", "option9", false)); + gQueue.push(new showParentNChild("select10", "option10", true)); + gQueue.push(new showParentNAddChild("select11", false)); + gQueue.push(new showParentNAddChild("select12", true)); + + gQueue.push(new removeGrandChildrenNHideParent("t1_child1", "t1_child2", "t1_parent")); + gQueue.push(new test3()); + gQueue.push(new test4()); + gQueue.push(new test5()); + gQueue.push(new test6()); + gQueue.push(new test7()); + gQueue.push(new test8()); + gQueue.push(new test9()); + gQueue.push(new test10()); + gQueue.push(new test11()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513213" + title="coalesce events when new event is appended to the queue"> + Mozilla Bug 513213 + </a><br> + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"> + <select id="select1"> + <option id="option1">option</option> + </select> + <select id="select2"> + <option id="option2">option</option> + </select> + <select id="select3"> + <option id="option3">option</option> + </select> + <select id="select4"> + <option id="option4">option</option> + </select> + <select id="select5"> + <option id="option5">option</option> + </select> + <select id="select6"> + <option id="option6">option</option> + </select> + <select id="select7"> + <option id="option7">option</option> + </select> + <select id="select8"> + <option id="option8">option</option> + </select> + + <select id="select9" style="display: none"> + <option id="option9" style="display: none">testing</option> + </select> + <select id="select10" style="display: none"> + <option id="option10" style="display: none">testing</option> + </select> + <select id="select11" style="display: none"></select> + <select id="select12" style="display: none"></select> + </div> + + <div id="testContainer2"> + <div id="t1_parent"> + <div id="t1_mid1"><div id="t1_child1"></div></div> + <div id="t1_mid2"><div id="t1_child2"></div></div> + </div> + </div> + + <div id="t3"> + <div role="listbox" id="t3_lb"> + <div role="option" id="t3_o">opt</div> + </div> + </div> + + <div id="t4"> + <div role="listbox" id="t4_lb"> + <div role="option" id="t4_o1">opt1</div> + <div role="option" id="t4_o2">opt2</div> + </div> + </div> + + <div id="t5"> + <div role="button" id="t5_b">btn</div> + <div role="listbox" id="t5_lb"> + <div role="option" id="t5_o">opt</div> + </div> + </div> + + <div id="t6"> + </div> + + <div id="t7"> + <div id="t7_moveplace"></div> + <div id="t7_c"> + <div><div id="t7_c_grandchild"></div></div> + <div id="t7_c_directchild"></div> + </div> + </div> + + <div id="t8"> + <div id="t8_c1"><div id="t8_c1_child"></div></div> + <div id="t8_c2"> + <div id="t8_c2_moved"></div> + </div> + </div> + + <div id="t9"> + <div id="t9_c1"><div id="t9_c1_child"></div></div> + <div id="t9_c2"> + <div id="t9_c2_child"></div> + <div id="t9_c2_moved"></div> + </div> + <div id="t9_c3"> + <div id="t9_c3_moved"></div> + </div> + </div> + + <div id="t10"> + <div id="t10_c1"><div id="t10_c1_child"></div></div> + <div id="t10_c2"> + <div id="t10_c2_child"></div> + <div id="t10_c2_moved"></div> + </div> + <div id="t10_c3"> + <div id="t10_c3_moved"></div> + </div> + </div> + + <div id="t11"> + <div id="t11_c1"><div id="t11_c1_child"></div></div> + <div id="t11_c2"><div id="t11_c2_child"></div></div> + <div id="t11_c3"></div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_contextmenu.html b/accessible/tests/mochitest/events/test_contextmenu.html new file mode 100644 index 0000000000..6200efc00d --- /dev/null +++ b/accessible/tests/mochitest/events/test_contextmenu.html @@ -0,0 +1,133 @@ +<html> + +<head> + <title>Context menu tests</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function showContextMenu(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_START, getContextMenuNode()), + ]; + + this.invoke = function showContextMenu_invoke() { + synthesizeMouse(this.DOMNode, 4, 4, { type: "contextmenu", button: 2 }); + }; + + this.getID = function showContextMenu_getID() { + return "show context menu"; + }; + } + + function selectMenuItem() { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getFocusedMenuItem), + ]; + + this.invoke = function selectMenuItem_invoke() { + synthesizeKey("KEY_ArrowDown"); + }; + + this.getID = function selectMenuItem_getID() { + return "select first menuitem"; + }; + } + + function closeContextMenu(aID) { + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_END, + getAccessible(getContextMenuNode())), + ]; + + this.invoke = function closeContextMenu_invoke() { + synthesizeKey("KEY_Escape"); + }; + + this.getID = function closeContextMenu_getID() { + return "close context menu"; + }; + } + + function getContextMenuNode() { + return getRootAccessible().DOMDocument. + getElementById("contentAreaContextMenu"); + } + + function getFocusedMenuItem() { + var menu = getAccessible(getAccessible(getContextMenuNode())); + for (var idx = 0; idx < menu.childCount; idx++) { + var item = menu.getChildAt(idx); + + if (hasState(item, STATE_FOCUSED)) + return getAccessible(item); + } + return null; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new showContextMenu("input")); + gQueue.push(new selectMenuItem()); + gQueue.push(new closeContextMenu()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + if (AppConstants.platform == "macosx" && + Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + ok(true, "Native context menus handle accessibility notifications natively and cannot be tested with synthesized key events."); + } else { + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + } + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=580535" + title="Broken accessibility in context menus"> + Mozilla Bug 580535 + </a><br> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_descrchange.html b/accessible/tests/mochitest/events/test_descrchange.html new file mode 100644 index 0000000000..1eaecd6b59 --- /dev/null +++ b/accessible/tests/mochitest/events/test_descrchange.html @@ -0,0 +1,142 @@ +<html> + +<head> + <title>Accessible description change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function setAttr(aID, aAttr, aValue, aChecker) { + this.eventSeq = [ aChecker ]; + this.invoke = function setAttr_invoke() { + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + async function doTests() { + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new setAttr("tst1", "aria-describedby", "display", + new invokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "title", "title", + new unexpectedInvokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1"))); + + // A title has lower priority over text content. There should be no name change event. + gQueue.push(new setAttr("tst2", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + const describedBy = getNode("describedBy"); + const description = getNode("description"); + let descChanged = PromEvents.waitForEvent( + EVENT_DESCRIPTION_CHANGE, + describedBy + ); + info("Changing text of aria-describedby target"); + description.textContent = "d2"; + await descChanged; + descChanged = PromEvents.waitForEvent( + EVENT_DESCRIPTION_CHANGE, + describedBy + ); + info("Adding node to aria-describedby target"); + description.innerHTML = '<p id="descriptionChild">d3</p>'; + await descChanged; + descChanged = PromEvents.waitForEvent( + EVENT_DESCRIPTION_CHANGE, + describedBy + ); + info("Changing text of aria-describedby target's child"); + getNode("descriptionChild").textContent = "d4"; + await descChanged; + + const lateDescribedBy = getNode("lateDescribedBy"); + descChanged = PromEvents.waitForEvent( + EVENT_DESCRIPTION_CHANGE, + lateDescribedBy + ); + info("Setting aria-describedby"); + lateDescribedBy.setAttribute("aria-describedby", "lateDescription"); + await descChanged; + descChanged = PromEvents.waitForEvent( + EVENT_DESCRIPTION_CHANGE, + lateDescribedBy + ); + info("Changing text of late aria-describedby target's child"); + getNode("lateDescriptionChild").textContent = "d2"; + await descChanged; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969" + title="Event not fired when description changes"> + Bug 991969 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="tst1">btn1</button> + <button id="tst2">btn2</button> + + <div id="describedBy" aria-describedby="description"></div> + <div id="description">d1</div> + + <div id="lateDescribedBy"></div> + <div id="lateDescription"><p id="lateDescriptionChild">d1</p></div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_dragndrop.html b/accessible/tests/mochitest/events/test_dragndrop.html new file mode 100644 index 0000000000..2613a310a2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_dragndrop.html @@ -0,0 +1,106 @@ +<html> + +<head> + <title>Accessible drag and drop event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + /** + * Do tests. + */ + var gQueue = null; + + // aria grabbed invoker + function changeGrabbed(aNodeOrID, aGrabValue) { + this.DOMNode = getNode(aNodeOrID); + + this.invoke = function changeGrabbed_invoke() { + if (aGrabValue != undefined) { + this.DOMNode.setAttribute("aria-grabbed", aGrabValue); + } + }; + + this.check = function changeGrabbed_check() { + testAttrs(aNodeOrID, {"grabbed": aGrabValue}, true); + }; + + this.getID = function changeGrabbed_getID() { + return prettyName(aNodeOrID) + " aria-grabbed changed"; + }; + } + + // aria dropeffect invoker + function changeDropeffect(aNodeOrID, aDropeffectValue) { + this.DOMNode = getNode(aNodeOrID); + + this.invoke = function changeDropeffect_invoke() { + if (aDropeffectValue != undefined) { + this.DOMNode.setAttribute("aria-dropeffect", aDropeffectValue); + } + }; + + this.check = function changeDropeffect_check() { + testAttrs(aNodeOrID, {"dropeffect": aDropeffectValue}, true); + }; + + this.getID = function changeDropeffect_getID() { + return prettyName(aNodeOrID) + " aria-dropeffect changed"; + }; + } + + function doTests() { + // Test aria attribute mutation events + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED); + + let id = "grabbable"; + gQueue.push(new changeGrabbed(id, "true")); + gQueue.push(new changeGrabbed(id, "false")); + todo(false, "uncomment this test when 472142 is fixed."); + // gQueue.push(new changeGrabbed(id, "undefined")); + + id = "dropregion"; + gQueue.push(new changeDropeffect(id, "copy")); + gQueue.push(new changeDropeffect(id, "execute")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=510441" + title="Add support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGED"> + Mozilla Bug 510441 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- ARIA grabbed --> + <div id="grabbable" role="button" aria-grabbed="foo">button</div> + + <!-- ARIA dropeffect --> + <div id="dropregion" role="region" aria-dropeffect="none">button</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_flush.html b/accessible/tests/mochitest/events/test_flush.html new file mode 100644 index 0000000000..7d7b60b81e --- /dev/null +++ b/accessible/tests/mochitest/events/test_flush.html @@ -0,0 +1,74 @@ +<html> + +<head> + <title>Flush delayed events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + SimpleTest.expectAssertions(0, 1); + + var gFocusHandler = { + handleEvent(aEvent) { + switch (this.count) { + case 0: + is(aEvent.DOMNode, getNode("input1"), + "Focus event for input1 was expected!"); + getAccessible("input2").takeFocus(); + break; + + case 1: + is(aEvent.DOMNode, getNode("input2"), + "Focus event for input2 was expected!"); + + unregisterA11yEventListener(EVENT_FOCUS, gFocusHandler); + SimpleTest.finish(); + break; + + default: + ok(false, "Wrong focus event!"); + } + + this.count++; + }, + + count: 0, + }; + + function doTests() { + registerA11yEventListener(EVENT_FOCUS, gFocusHandler); + + getAccessible("input1").takeFocus(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=477551" + title="DocAccessible::FlushPendingEvents isn't robust"> + Mozilla Bug 477551 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input1"> + <input id="input2"> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html new file mode 100644 index 0000000000..661284619a --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html @@ -0,0 +1,327 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429547 +--> +<head> + <title>aria-activedescendant focus tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + // gA11yEventDumpToConsole = true; // debugging + + function changeARIAActiveDescendant(aContainer, aItem, aPrevItemId) { + let itemID = Node.isInstance(aItem) ? aItem.id : aItem; + this.eventSeq = [new focusChecker(aItem)]; + + if (aPrevItemId) { + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId) + ); + } + + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aItem) + ); + + this.invoke = function changeARIAActiveDescendant_invoke() { + getNode(aContainer).setAttribute("aria-activedescendant", itemID); + }; + + this.getID = function changeARIAActiveDescendant_getID() { + return "change aria-activedescendant on " + itemID; + }; + } + + function clearARIAActiveDescendant(aID, aPrevItemId) { + this.eventSeq = [ + new focusChecker(aID), + ]; + + if (aPrevItemId) { + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId) + ); + } + + this.invoke = function clearARIAActiveDescendant_invoke() { + getNode(aID).removeAttribute("aria-activedescendant"); + }; + + this.getID = function clearARIAActiveDescendant_getID() { + return "clear aria-activedescendant on container " + aID; + }; + } + + /** + * Change aria-activedescendant to an invalid (non-existent) id. + * Ensure that focus is fired on the element itself. + */ + function changeARIAActiveDescendantInvalid(aID, aInvalidID, aPrevItemId) { + if (!aInvalidID) { + aInvalidID = "invalid"; + } + + this.eventSeq = [ + new focusChecker(aID), + ]; + + if (aPrevItemId) { + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId) + ); + } + + this.invoke = function changeARIAActiveDescendant_invoke() { + getNode(aID).setAttribute("aria-activedescendant", aInvalidID); + }; + + this.getID = function changeARIAActiveDescendant_getID() { + return "change aria-activedescendant to invalid id"; + }; + } + + function insertItemNFocus(aID, aNewItemID, aPrevItemId) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, aNewItemID), + new focusChecker(aNewItemID), + ]; + + if (aPrevItemId) { + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId) + ); + } + + this.eventSeq.push( + new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aNewItemID) + ); + + this.invoke = function insertItemNFocus_invoke() { + var container = getNode(aID); + + var itemNode = document.createElement("div"); + itemNode.setAttribute("id", aNewItemID); + itemNode.setAttribute("role", "listitem"); + itemNode.textContent = aNewItemID; + container.appendChild(itemNode); + + container.setAttribute("aria-activedescendant", aNewItemID); + }; + + this.getID = function insertItemNFocus_getID() { + return "insert new node and focus it with ID: " + aNewItemID; + }; + } + + /** + * Change the id of an element to another id which is the target of + * aria-activedescendant. + * If another element already has the desired id, remove it from that + * element first. + * Ensure that focus is fired on the target element which was given the + * desired id. + * @param aFromID The existing id of the target element. + * @param aToID The desired id to be given to the target element. + */ + function moveARIAActiveDescendantID(aFromID, aToID) { + this.eventSeq = [ + new focusChecker(aToID), + new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aToID), + ]; + + this.invoke = function moveARIAActiveDescendantID_invoke() { + let orig = document.getElementById(aToID); + if (orig) { + orig.id = ""; + } + document.getElementById(aFromID).id = aToID; + }; + + this.getID = function moveARIAActiveDescendantID_getID() { + return "move aria-activedescendant id " + aToID; + }; + } + + var gQueue = null; + async function doTest() { + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new synthFocus("listbox", new focusChecker("item1"))); + gQueue.push(new changeARIAActiveDescendant("listbox", "item2", "item1")); + gQueue.push(new changeARIAActiveDescendant("listbox", "item3", "item2")); + + gQueue.push(new synthFocus("combobox_entry", new focusChecker("combobox_entry"))); + gQueue.push(new changeARIAActiveDescendant("combobox", "combobox_option2")); + + gQueue.push(new synthFocus("listbox", new focusChecker("item3"))); + gQueue.push(new insertItemNFocus("listbox", "item4", "item3")); + + gQueue.push(new clearARIAActiveDescendant("listbox", "item4")); + gQueue.push(new changeARIAActiveDescendant("listbox", "item1")); + gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "invalid", "item1")); + + gQueue.push(new changeARIAActiveDescendant("listbox", "roaming")); + gQueue.push(new moveARIAActiveDescendantID("roaming2", "roaming")); + gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "roaming3", "roaming")); + gQueue.push(new moveARIAActiveDescendantID("roaming", "roaming3")); + + gQueue.push(new synthFocus("activedesc_nondesc_input", + new focusChecker("activedesc_nondesc_option"))); + + let shadowRoot = document.getElementById("shadow").shadowRoot; + let shadowListbox = shadowRoot.getElementById("shadowListbox"); + let shadowItem1 = shadowRoot.getElementById("shadowItem1"); + let shadowItem2 = shadowRoot.getElementById("shadowItem2"); + gQueue.push(new synthFocus(shadowListbox, new focusChecker(shadowItem1))); + gQueue.push(new changeARIAActiveDescendant(shadowListbox, shadowItem2)); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + info("Testing simultaneous insertion, relocation and aria-activedescendant"); + let comboboxWithHiddenList = getNode("comboboxWithHiddenList"); + let evtProm = PromEvents.waitForEvent(EVENT_FOCUS, comboboxWithHiddenList); + comboboxWithHiddenList.focus(); + await evtProm; + testStates(comboboxWithHiddenList, STATE_FOCUSED); + // hiddenList is owned, so unhiding causes insertion and relocation. + getNode("hiddenList").hidden = false; + evtProm = Promise.all([ + PromEvents.waitForEvent(EVENT_FOCUS, "hiddenListOption"), + PromEvents.waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true), + ]); + comboboxWithHiddenList.setAttribute("aria-activedescendant", "hiddenListOption"); + await evtProm; + testStates("hiddenListOption", STATE_FOCUSED); + + info("Testing active state changes when not focused"); + testStates("listbox", 0, 0, STATE_FOCUSED); + evtProm = Promise.all([ + PromEvents.waitForStateChange("roaming3", EXT_STATE_ACTIVE, false, true), + PromEvents.waitForStateChange("item1", EXT_STATE_ACTIVE, true, true), + ]); + getNode("listbox").setAttribute("aria-activedescendant", "item1"); + await evtProm; + + info("Testing that focus is always fired first"); + const listbox = getNode("listbox"); + evtProm = PromEvents.waitForEvent(EVENT_FOCUS, "item1"); + listbox.focus(); + await evtProm; + const item1 = getNode("item1"); + evtProm = PromEvents.waitForOrderedEvents([ + [EVENT_FOCUS, "item2"], + [EVENT_NAME_CHANGE, item1], + ], "Focus then name change"); + item1.setAttribute("aria-label", "changed"); + listbox.setAttribute("aria-activedescendant", "item2"); + await evtProm; + + info("Setting aria-activedescendant to invalid id on non-focused node"); + const combobox_entry = getNode("combobox_entry"); + evtProm = PromEvents.waitForEvents({ + expected: [[EVENT_FOCUS, combobox_entry]], + unexpected: [[EVENT_FOCUS, listbox]], + }); + combobox_entry.focus(); + listbox.setAttribute("aria-activedescendant", "invalid"); + await evtProm; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547" + title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()"> + Mozilla Bug 429547 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761102" + title="Focus may be missed when ARIA active-descendant is changed on active composite widget"> + Mozilla Bug 761102 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1" + aria-owns="item3"> + <div role="listitem" id="item1">item1</div> + <div role="listitem" id="item2">item2</div> + <div role="listitem" id="roaming">roaming</div> + <div role="listitem" id="roaming2">roaming2</div> + </div> + <div role="listitem" id="item3">item3</div> + + <div role="combobox" id="combobox"> + <input id="combobox_entry"> + <ul> + <li role="option" id="combobox_option1">option1</li> + <li role="option" id="combobox_option2">option2</li> + </ul> + </div> + + <!-- aria-activedescendant targeting a non-descendant --> + <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option"> + <div role="listbox"> + <div role="option" id="activedesc_nondesc_option">option</div> + </div> + + <div id="shadow"></div> + <script> + let host = document.getElementById("shadow"); + let shadow = host.attachShadow({mode: "open"}); + let listbox = document.createElement("div"); + listbox.id = "shadowListbox"; + listbox.setAttribute("role", "listbox"); + listbox.setAttribute("tabindex", "0"); + shadow.appendChild(listbox); + let item = document.createElement("div"); + item.id = "shadowItem1"; + item.setAttribute("role", "option"); + listbox.appendChild(item); + listbox.setAttribute("aria-activedescendant", "shadowItem1"); + item = document.createElement("div"); + item.id = "shadowItem2"; + item.setAttribute("role", "option"); + listbox.appendChild(item); + </script> + + <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList"> + </div> + <div id="hiddenList" hidden role="listbox"> + <div id="hiddenListOption" role="option"></div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.html b/accessible/tests/mochitest/events/test_focus_autocomplete.html new file mode 100644 index 0000000000..c7fdbbb6a5 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_autocomplete.html @@ -0,0 +1,83 @@ +<!doctype html> + +<head> + <title>Form Autocomplete Tests</title> + + <link rel="stylesheet" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script src="../common.js"></script> + <script src="../promisified-events.js"></script> + <script src="../role.js"></script> + + <script type="application/javascript"> + const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs"); + + async function waitForFocusOnOptionWithname(name) { + let event = await waitForEvent( + EVENT_FOCUS, + evt => evt.accessible.role == ROLE_COMBOBOX_OPTION + ); + while (!event.accessible.name) { + // Sometimes, the name is null for a very short time after the focus + // event. + await waitForEvent(EVENT_NAME_CHANGE, event.accessible); + } + is(event.accessible.name, name, "Got focus on option with name " + name); + } + + async function doTests() { + const input = getNode("input"); + info("Focusing the input"); + let focused = waitForEvent(EVENT_FOCUS, input); + input.focus(); + await focused; + + let shown = waitForEvent(EVENT_SHOW, event => + event.accessible.role == ROLE_GROUPING && + event.accessible.firstChild.role == ROLE_COMBOBOX_LIST); + info("Pressing ArrowDown to open the popup"); + synthesizeKey("KEY_ArrowDown"); + await shown; + // The popup still doesn't seem to be ready even once it's fired an a11y + // show event! + const controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + info("Waiting for popup to be fully open and ready"); + await TestUtils.waitForCondition(() => controller.input.popupOpen); + + focused = waitForFocusOnOptionWithname("a"); + info("Pressing ArrowDown to focus first item"); + synthesizeKey("KEY_ArrowDown"); + await focused; + + focused = waitForFocusOnOptionWithname("b"); + info("Pressing ArrowDown to focus the second item"); + synthesizeKey("KEY_ArrowDown"); + await focused; + + focused = waitForEvent(EVENT_FOCUS, input); + info("Pressing enter to select the second item"); + synthesizeKey("KEY_Enter"); + await focused; + is(input.value, "b", "input value filled with second item"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<body> + <input id="input" list="list"> + <datalist id="list"> + <option id="a" value="a"> + <option id="b" value="b"> + </datalist> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml new file mode 100644 index 0000000000..69cdac14c5 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml @@ -0,0 +1,507 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<!-- Firefox searchbar --> +<?xml-stylesheet href="chrome://browser/content/browser.css" + type="text/css"?> +<!-- SeaMonkey searchbar --> +<?xml-stylesheet href="chrome://navigator/content/navigator.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript" + src="../autocomplete.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Hacky stuffs + + // This is the hacks needed to use a searchbar without browser.js. + var BrowserSearch = { + updateOpenSearchBadge() {} + }; + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function loadFormAutoComplete(aIFrameID) + { + this.iframeNode = getNode(aIFrameID); + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function loadFormAutoComplete_invoke() + { + var url = "data:text/html,<html><body><form id='form'>" + + "<input id='input' name='a11ytest-formautocomplete'>" + + "</form></body></html>"; + this.iframeNode.setAttribute("src", url); + } + + this.getID = function loadFormAutoComplete_getID() + { + return "load form autocomplete page"; + } + } + + function initFormAutoCompleteBy(aIFrameID, aAutoCompleteValue) + { + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function initFormAutoCompleteBy_invoke() + { + var iframeDOMDoc = getIFrameDOMDoc(aIFrameID); + + var inputNode = iframeDOMDoc.getElementById("input"); + inputNode.value = aAutoCompleteValue; + var formNode = iframeDOMDoc.getElementById("form"); + formNode.submit(); + } + + this.getID = function initFormAutoCompleteBy_getID() + { + return "init form autocomplete by '" + aAutoCompleteValue + "'"; + } + } + + function loadHTML5ListAutoComplete(aIFrameID) + { + this.iframeNode = getNode(aIFrameID); + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function loadHTML5ListAutoComplete_invoke() + { + var url = "data:text/html,<html><body>" + + "<datalist id='cities'><option>hello</option><option>hi</option></datalist>" + + "<input id='input' list='cities'>" + + "</body></html>"; + this.iframeNode.setAttribute("src", url); + } + + this.getID = function loadHTML5ListAutoComplete_getID() + { + return "load HTML5 list autocomplete page"; + } + } + + function removeChar(aID, aCheckerOrEventSeq) + { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function removeChar_invoke() + { + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_Delete"); + } + + this.getID = function removeChar_getID() + { + return "remove char on " + prettyName(aID); + } + } + + function replaceOnChar(aID, aChar, aCheckerOrEventSeq) + { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function replaceOnChar_invoke() + { + this.DOMNode.select(); + sendString(aChar); + } + + this.getID = function replaceOnChar_getID() + { + return "replace on char '" + aChar + "' for" + prettyName(aID); + } + } + + function focusOnMouseOver(aIDFunc, aIDFuncArg) + { + this.eventSeq = [ new focusChecker(aIDFunc, aIDFuncArg) ]; + + this.invoke = function focusOnMouseOver_invoke() + { + this.id = aIDFunc(aIDFuncArg); + this.node = getNode(this.id); + this.window = this.node.ownerGlobal; + + if (this.node.localName == "tree") { + var tree = getAccessible(this.node); + var accessible = getAccessible(this.id); + if (tree != accessible) { + var itemX = {}, itemY = {}, treeX = {}, treeY = {}; + accessible.getBounds(itemX, itemY, {}, {}); + tree.getBounds(treeX, treeY, {}, {}); + this.x = itemX.value - treeX.value; + this.y = itemY.value - treeY.value; + } + } + + // Generate mouse move events in timeouts until autocomplete popup list + // doesn't have it, the reason is do that because autocomplete popup + // ignores mousemove events firing in too short range. + synthesizeMouse(this.node, this.x, this.y, { type: "mousemove" }); + this.doMouseMoveFlood(this); + } + + this.finalCheck = function focusOnMouseOver_getID() + { + this.isFlooding = false; + } + + this.getID = function focusOnMouseOver_getID() + { + return "mouse over on " + prettyName(aIDFunc(aIDFuncArg)); + } + + this.doMouseMoveFlood = function focusOnMouseOver_doMouseMoveFlood(aThis) + { + synthesizeMouse(aThis.node, aThis.x + 1, aThis.y + 1, + { type: "mousemove" }, aThis.window); + + if (aThis.isFlooding) + aThis.window.setTimeout(aThis.doMouseMoveFlood, 0, aThis); + } + + this.id = null; + this.node = null; + this.window = null; + + this.isFlooding = true; + this.x = 1; + this.y = 1; + } + + function selectByClick(aIDFunc, aIDFuncArg, + aFocusTargetFunc, aFocusTargetFuncArg) + { + this.eventSeq = [ new focusChecker(aFocusTargetFunc, aFocusTargetFuncArg) ]; + + this.invoke = function selectByClick_invoke() + { + var id = aIDFunc(aIDFuncArg); + var node = getNode(id); + var targetWindow = node.ownerGlobal; + + if (node.localName == "tree") { + var tree = getAccessible(node); + var accessible = getAccessible(id); + if (tree != accessible) { + var itemX = {}, itemY = {}, treeX = {}, treeY = {}; + accessible.getBounds(itemX, itemY, {}, {}); + tree.getBounds(treeX, treeY, {}, {}); + this.x = itemX.value - treeX.value; + this.y = itemY.value - treeY.value; + } + } + + synthesizeMouseAtCenter(node, {}, targetWindow); + } + + this.getID = function selectByClick_getID() + { + return "select by click " + prettyName(aIDFunc(aIDFuncArg)); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Target getters + + function getItem(aItemObj) + { + var autocompleteNode = aItemObj.autocompleteNode; + + // XUL searchbar + if (autocompleteNode.localName == "searchbar") { + let popupNode = autocompleteNode._popup; + if (popupNode) { + let list = getAccessible(popupNode); + return list.getChildAt(aItemObj.index); + } + } + + // XUL autocomplete + let popupNode = autocompleteNode.popup; + if (!popupNode) { + // HTML form autocomplete + var controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + popupNode = controller.input.popup; + } + + if (popupNode) { + if ("richlistbox" in popupNode) { + let list = getAccessible(popupNode.richlistbox); + return list.getChildAt(aItemObj.index); + } + + let list = getAccessible(popupNode.tree); + return list.getChildAt(aItemObj.index + 1); + } + return null; + } + + function getTextEntry(aID) + { + // For form autocompletes the autocomplete widget and text entry widget + // is the same widget, for XUL autocompletes the text entry is a first + // child. + var localName = getNode(aID).localName; + + // HTML form autocomplete + if (localName == "input") + return getAccessible(aID); + + // XUL searchbar + if (localName == "searchbar") + return getAccessible(getNode(aID).textbox); + + return null; + } + + function itemObj(aID, aIdx) + { + this.autocompleteNode = getNode(aID); + + this.autocomplete = this.autocompleteNode.localName == "searchbar" ? + getAccessible(this.autocompleteNode.textbox) : + getAccessible(this.autocompleteNode); + + this.index = aIdx; + } + + function getIFrameDOMDoc(aIFrameID) + { + return getNode(aIFrameID).contentDocument; + } + + //////////////////////////////////////////////////////////////////////////// + // Test helpers + + function queueAutoCompleteTests(aID) + { + // focus autocomplete text entry + gQueue.push(new synthFocus(aID, new focusChecker(getTextEntry, aID))); + + // open autocomplete popup + gQueue.push(new synthDownKey(aID, new nofocusChecker())); + + // select second option ('hi' option), focus on it + gQueue.push(new synthUpKey(aID, + new focusChecker(getItem, new itemObj(aID, 1)))); + + // choose selected option, focus on text entry + gQueue.push(new synthEnterKey(aID, new focusChecker(getTextEntry, aID))); + + // remove char, autocomplete popup appears + gQueue.push(new removeChar(aID, new nofocusChecker())); + + // select first option ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // mouse move on second option ('hi' option), focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 1))); + + // autocomplete popup updated (no selected item), focus on textentry + gQueue.push(new synthKey(aID, "e", null, new focusChecker(getTextEntry, aID))); + + // select first option ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // popup gets hidden, focus on textentry + gQueue.push(new synthRightKey(aID, new focusChecker(getTextEntry, aID))); + + // popup gets open, no focus + gQueue.push(new synthOpenComboboxKey(aID, new nofocusChecker())); + + // select first option again ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // no option is selected, focus on text entry + gQueue.push(new synthUpKey(aID, new focusChecker(getTextEntry, aID))); + + // close popup, no focus + gQueue.push(new synthEscapeKey(aID, new nofocusChecker())); + + // autocomplete popup appears (no selected item), focus stays on textentry + gQueue.push(new replaceOnChar(aID, "h", new nofocusChecker())); + + // mouse move on first option ('hello' option), focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 0))); + + // click first option ('hello' option), popup closes, focus on text entry + gQueue.push(new selectByClick(getItem, new itemObj(aID, 0), getTextEntry, aID)); + } + + //////////////////////////////////////////////////////////////////////////// + // Tests + + //gA11yEventDumpToConsole = true; // debug stuff + + var gInitQueue = null; + function initTests() + { + if (SEAMONKEY || MAC) { + todo(false, "Skipping this test on SeaMonkey ftb. (Bug 718237), and on Mac (bug 746177)"); + shutdownAutoComplete(); + SimpleTest.finish(); + return; + } + + gInitQueue = new eventQueue(); + gInitQueue.push(new loadFormAutoComplete("iframe")); + gInitQueue.push(new initFormAutoCompleteBy("iframe", "hello")); + gInitQueue.push(new initFormAutoCompleteBy("iframe", "hi")); + gInitQueue.push(new loadHTML5ListAutoComplete("iframe2")); + gInitQueue.onFinish = function initQueue_onFinish() + { + SimpleTest.executeSoon(doTests); + return DO_NOT_FINISH_TEST; + } + gInitQueue.invoke(); + } + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + //////////////////////////////////////////////////////////////////////////// + // tree popup autocomplete tests + queueAutoCompleteTests("autocomplete"); + + //////////////////////////////////////////////////////////////////////////// + // richlistbox popup autocomplete tests + queueAutoCompleteTests("richautocomplete"); + + //////////////////////////////////////////////////////////////////////////// + // HTML form autocomplete tests + queueAutoCompleteTests(getIFrameDOMDoc("iframe").getElementById("input")); + + //////////////////////////////////////////////////////////////////////////// + // HTML5 list autocomplete tests + queueAutoCompleteTests(getIFrameDOMDoc("iframe2").getElementById("input")); + + //////////////////////////////////////////////////////////////////////////// + // searchbar tests + + // focus searchbar, focus on text entry + gQueue.push(new synthFocus("searchbar", + new focusChecker(getTextEntry, "searchbar"))); + // open search engine popup, no focus + gQueue.push(new synthOpenComboboxKey("searchbar", new nofocusChecker())); + // select first item, focus on it + gQueue.push(new synthDownKey("searchbar", + new focusChecker(getItem, new itemObj("searchbar", 0)))); + // mouse over on second item, focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj("searchbar", 1))); + // press enter key, focus on text entry + gQueue.push(new synthEnterKey("searchbar", + new focusChecker(getTextEntry, "searchbar"))); + // click on search button, open popup, focus goes to document + var searchBtn = getAccessible(getNode("searchbar").searchButton); + gQueue.push(new synthClick(searchBtn, new focusChecker(document))); + // select first item, focus on it + gQueue.push(new synthDownKey("searchbar", + new focusChecker(getItem, new itemObj("searchbar", 0)))); + // close popup, focus goes on document + gQueue.push(new synthEscapeKey("searchbar", new focusChecker(document))); + + gQueue.onFinish = function() + { + // unregister 'test-a11y-search' autocomplete search + shutdownAutoComplete(); + } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + // Register 'test-a11y-search' autocomplete search. + // XPFE AutoComplete needs to register early. + initAutoComplete([ "hello", "hi" ], + [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]); + + addA11yLoadEvent(initTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=383759" + title="Focus event inconsistent for search box autocomplete"> + Mozilla Bug 383759 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766" + title="Add accessibility support for @list on HTML input and for HTML datalist"> + Mozilla Bug 559766 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <html:input is="autocomplete-input" + id="autocomplete" + autocompletesearch="test-a11y-search"/> + + <html:input is="autocomplete-input" + id="richautocomplete" + autocompletesearch="test-a11y-search" + autocompletepopup="richpopup"/> + <panel is="autocomplete-richlistbox-popup" + id="richpopup" + type="autocomplete-richlistbox" + noautofocus="true"/> + + <iframe id="iframe"/> + + <iframe id="iframe2"/> + + <searchbar id="searchbar"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_canvas.html b/accessible/tests/mochitest/events/test_focus_canvas.html new file mode 100644 index 0000000000..e2464e41a6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_canvas.html @@ -0,0 +1,58 @@ +<html> + +<head> + <title>Accessible focus testing in canvas subdom</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("button")); + gQueue.push(new synthTab("button", new focusChecker("textbox"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <a target="_blank" + title="Expose content in Canvas element" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912"> + Mozilla Bug 495912 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <canvas> + <input id="button" type="button"> + <input id="textbox"> + </canvas> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml new file mode 100644 index 0000000000..a0c92212dc --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Context menu focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var winLowerThanVista = navigator.platform.indexOf("Win") == 0; + if (winLowerThanVista) { + var version = Services.sysinfo.getProperty("version"); + version = parseFloat(version); + winLowerThanVista = !(version >= 6.0); + } + + var gQueue = null; + function doTests() + { + // bug 746183 - Whole file times out on OS X + if (MAC || winLowerThanVista) { + todo(false, "Reenable on mac after fixing bug 746183!"); + SimpleTest.finish(); + return; + } + + // Test focus events. + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("button")); + gQueue.push(new synthContextMenu("button", [ + new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"), + new invokerChecker("popupshown", "contextmenu"), + ])); + gQueue.push(new synthDownKey("button", new focusChecker("item1"))); + gQueue.push(new synthEscapeKey("contextmenu", new focusChecker("button"))); + + gQueue.push(new synthContextMenu("button", + new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"))); + gQueue.push(new synthDownKey("contextmenu", new focusChecker("item1"))); + gQueue.push(new synthDownKey("item1", new focusChecker("item2"))); + gQueue.push(new synthRightKey("item2", new focusChecker("item2.1"))); + if (WIN) { + todo(false, "synthEscapeKey for item2.1 and item2 disabled due to bug 691580"); + } else { + gQueue.push(new synthEscapeKey("item2.1", new focusChecker("item2"))); + gQueue.push(new synthEscapeKey("item2", new focusChecker("button"))); + } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <button id="button" context="contextmenu" label="button"/> + <menupopup id="contextmenu"> + <menuitem id="item1" label="item1"/> + <menu id="item2" label="item2"> + <menupopup> + <menuitem id="item2.1" label="item2.1"/> + </menupopup> + </menu> + </menupopup> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_controls.html b/accessible/tests/mochitest/events/test_focus_controls.html new file mode 100644 index 0000000000..268ec5d0e4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_controls.html @@ -0,0 +1,76 @@ +<html> + +<head> + <title>Accessible focus testing on HTML controls</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(EVENT_FOCUS); + + gQueue.push(new synthFocus("textbox")); + gQueue.push(new synthFocus("textarea")); + gQueue.push(new synthFocus("button1")); + gQueue.push(new synthFocus("button2")); + gQueue.push(new synthFocus("checkbox")); + gQueue.push(new synthFocus("radio1")); + gQueue.push(new synthDownKey("radio1", new focusChecker("radio2"))); + + // no focus events for checkbox or radio inputs when they are checked + // programmatically + gQueue.push(new changeCurrentItem("checkbox")); + gQueue.push(new changeCurrentItem("radio1")); + + let fileBrowseButton = getAccessible("file"); + gQueue.push(new synthFocus("file", new focusChecker(fileBrowseButton))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox"> + <textarea id="textarea"></textarea> + + <input id="button1" type="button" value="button"> + <button id="button2">button</button> + <input id="checkbox" type="checkbox"> + <input id="radio1" type="radio" name="radiogroup"> + <input id="radio2" type="radio" name="radiogroup"> + <input id="file" type="file"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_doc.html b/accessible/tests/mochitest/events/test_focus_doc.html new file mode 100644 index 0000000000..a35fc06ed0 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_doc.html @@ -0,0 +1,92 @@ +<html> + +<head> + <title>Accessible document focus event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + var gQueue = null; + + // var gA11yEventDumpID = "eventdump"; + // gA11yEventDumpToConsole = true; + + function doTests() { + // setup + var frameDoc = document.getElementById("iframe").contentDocument; + frameDoc.designMode = "on"; + var frameDocAcc = getAccessible(frameDoc, [nsIAccessibleDocument]); + var buttonAcc = getAccessible("b1"); + + var frame2Doc = document.getElementById("iframe2").contentDocument; + var frame2Input = frame2Doc.getElementById("input"); + var frame2DocAcc = getAccessible(frame2Doc); + var frame2InputAcc = getAccessible(frame2Input); + + // Test focus events. + gQueue = new eventQueue(); + + // try to give focus to contentEditable frame twice to cover bug 512059 + gQueue.push(new synthFocus(buttonAcc)); + gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc))); + gQueue.push(new synthFocus(buttonAcc)); + gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc))); + + // focus on not editable document + gQueue.push(new synthFocus(frame2InputAcc)); + gQueue.push(new synthShiftTab(frame2DocAcc, new focusChecker(frame2DocAcc))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512058" + title="Can't set focus to designMode document via accessibility APIs"> + Mozilla Bug 512058 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512059" + title="Accessibility focus event never fired for designMode document after the first focus"> + Mozilla Bug 512059 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=618046" + title="No focus change event when Shift+Tab at top of screen"> + Mozilla Bug 618046 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="eventdump"></div> + + <div id="testContainer"> + <button id="b1">a button</button> + <iframe id="iframe" src="about:blank"></iframe> + <button id="b2">a button</button> + <iframe id="iframe2" src="data:text/html,<html><input id='input'></html>"></iframe> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_general.html b/accessible/tests/mochitest/events/test_focus_general.html new file mode 100644 index 0000000000..6919ed8860 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.html @@ -0,0 +1,176 @@ +<html> + +<head> + <title>Accessible focus testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function focusElmWhileSubdocIsFocused(aID) { + this.DOMNode = getNode(aID); + + this.invoke = function focusElmWhileSubdocIsFocused_invoke() { + this.DOMNode.focus(); + }; + + this.eventSeq = [ + new focusChecker(this.DOMNode), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_FOCUS, this.DOMNode.ownerDocument), + ]; + + this.getID = function focusElmWhileSubdocIsFocused_getID() { + return "Focus element while subdocument is focused " + prettyName(aID); + }; + } + + function imageMapChecker(aID) { + var node = getNode(aID); + this.type = EVENT_FOCUS; + this.match = function imageMapChecker_match(aEvent) { + return aEvent.DOMNode == node; + }; + } + + function topMenuChecker() { + this.type = EVENT_FOCUS; + this.match = function topMenuChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_PARENT_MENUITEM; + }; + } + + function contextMenuChecker() { + this.type = EVENT_MENUPOPUP_START; + this.match = function contextMenuChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_MENUPOPUP; + }; + } + + function focusContextMenuItemChecker() { + this.__proto__ = new focusChecker(); + + this.match = function focusContextMenuItemChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_MENUITEM; + }; + } + + /** + * Do tests. + */ + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTests() { + var frameDoc = document.getElementById("iframe").contentDocument; + + var editableDoc = document.getElementById("editabledoc").contentDocument; + editableDoc.designMode = "on"; + + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("editablearea")); + gQueue.push(new synthFocus("navarea")); + gQueue.push(new synthTab("navarea", new focusChecker(frameDoc))); + gQueue.push(new focusElmWhileSubdocIsFocused("link")); + + gQueue.push(new synthTab(editableDoc, new focusChecker(editableDoc))); + if (WIN || LINUX) { + // Alt key is used to active menubar and focus menu item on Windows, + // other platforms requires setting a ui.key.menuAccessKeyFocuses + // preference. + gQueue.push(new toggleTopMenu(editableDoc, new topMenuChecker())); + gQueue.push(new toggleTopMenu(editableDoc, new focusChecker(editableDoc))); + } + if (!(MAC && Services.prefs.getBoolPref("widget.macos.native-context-menus", false))) { + // Context menu accessibility is handled natively and not testable when + // native context menus are used on macOS. + gQueue.push(new synthContextMenu(editableDoc, new contextMenuChecker())); + gQueue.push(new synthDownKey(editableDoc, new focusContextMenuItemChecker())); + gQueue.push(new synthEscapeKey(editableDoc, new focusChecker(editableDoc))); + } else { + // If this test is run as part of multiple tests, it is displayed in the test harness iframe. + // In the non-native context menu case, right-clicking the editableDoc causes the editableDoc + // to scroll fully into view, and as a side-effect, the img below it ends up on the screen. + // When we're skipping the context menu check, scroll img onto the screen manually, because + // otherwise it may remain out-of-view and clipped by the test harness iframe. + var img = document.querySelector("img"); + gQueue.push(new scrollIntoView(img, new nofocusChecker(img))); + } + if (SEAMONKEY) { + todo(false, "shift tab from editable document fails on (Windows) SeaMonkey! (Bug 718235)"); + } else if (LINUX || MAC) { + todo(false, "shift tab from editable document fails on linux and Mac, bug 746519!"); + } else { + gQueue.push(new synthShiftTab("link", new focusChecker("link"))); + } // ! SEAMONKEY + + gQueue.push(new synthFocus("a", new imageMapChecker("a"))); + gQueue.push(new synthFocus("b", new imageMapChecker("b"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=352220" + title="Inconsistent focus events when returning to a document frame"> + Mozilla Bug 352220 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=550338" + title="Broken focus when returning to editable documents from menus"> + Mozilla Bug 550338 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=961696" + title="Accessible object:state-changed:focused events for imagemap links are broken"> + Mozilla Bug 961696 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="editablearea" contentEditable="true">editable area</div> + <div id="navarea" tabindex="0">navigable area</div> + <iframe id="iframe" src="data:text/html,<html></html>"></iframe> + <a id="link" href="">link</a> + <iframe id="editabledoc" src="about:blank"></iframe> + + <map name="atoz_map"> + <area id="a" coords="0,0,13,14" shape="rect"> + <area id="b" coords="17,0,30,14" shape="rect"> + </map> + <img width="447" height="15" usemap="#atoz_map" src="../letters.gif"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_general.xhtml b/accessible/tests/mochitest/events/test_focus_general.xhtml new file mode 100644 index 0000000000..c446359b32 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.xhtml @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("textbox", + new focusChecker(getNode("textbox")))); + gQueue.push(new synthFocusOnFrame("editabledoc")); + gQueue.push(new synthFocus("radioclothes", + new focusChecker("radiosweater"))); + gQueue.push(new synthDownKey("radiosweater", + new focusChecker("radiojacket"))); + gQueue.push(new synthFocus("checkbox")); + gQueue.push(new synthFocus("button")); + gQueue.push(new synthFocus("checkbutton")); + gQueue.push(new synthFocus("radiobutton")); + + // focus menubutton + gQueue.push(new synthFocus("menubutton")); + // click menubutton, open popup, focus stays on menu button + gQueue.push(new synthClick("menubutton", new nofocusChecker())); + // select first menu item ("item 1"), focus on menu item + gQueue.push(new synthDownKey("menubutton", new focusChecker("mb_item1"))); + // choose select menu item, focus gets back to menubutton + gQueue.push(new synthEnterKey("mb_item1", new focusChecker("menubutton"))); + // press enter to open popup, focus stays on menubutton + gQueue.push(new synthEnterKey("menubutton", new nofocusChecker())); + // select second menu item ("item 2"), focus on menu item + gQueue.push(new synthUpKey("menubutton", new focusChecker("mb_item2"))); + // close the popup + gQueue.push(new synthEscapeKey("menubutton", new focusChecker("menubutton"))); + + // clicking on button having associated popup doesn't change focus + gQueue.push(new synthClick("popupbutton", [ + new nofocusChecker(), + new invokerChecker("popupshown", "backpopup") + ])); + + // select first menu item ("item 1"), focus on menu item + gQueue.push(new synthDownKey("popupbutton", new focusChecker("bp_item1"))); + // choose select menu item, focus gets back to menubutton + gQueue.push(new synthEnterKey("bp_item1", new focusChecker("menubutton"))); + // show popup again for the next test + gQueue.push(new synthClick("popupbutton", new nofocusChecker())); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368" + title=" fire focus event on document accessible whenever the root or body element is focused"> + Mozilla Bug 552368 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <html:input id="textbox" value="hello"/> + <iframe id="editabledoc" src="focus.html"/> + <radiogroup id="radioclothes"> + <radio id="radiosweater" label="radiosweater"/> + <radio id="radiocap" label="radiocap" disabled="true"/> + <radio id="radiojacket" label="radiojacket"/> + </radiogroup> + <checkbox id="checkbox" label="checkbox"/> + <button id="button" label="button"/> + + <button id="menubutton" type="menu" label="menubutton"> + <menupopup> + <menuitem id="mb_item1" label="item1"/> + <menuitem id="mb_item2" label="item2"/> + </menupopup> + </button> + + <button id="checkbutton" type="checkbox" label="checkbutton"/> + <button id="radiobutton" type="radio" group="rbgroup" label="radio1"/> + + <popupset> + <menupopup id="backpopup" position="after_start"> + <menuitem id="bp_item1" label="Page 1"/> + <menuitem id="bp_item2" label="Page 2"/> + </menupopup> + </popupset> + <button id="popupbutton" label="Pop Me Up" popup="backpopup"/> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml new file mode 100644 index 0000000000..848657d3b3 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml @@ -0,0 +1,153 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + //gA11yEventDumpID = "eventdump"; // debug stuff + gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + async function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new synthFocus("richlistbox", new focusChecker("rlb_item1"))); + gQueue.push(new synthDownKey("rlb_item1", new focusChecker("rlb_item2"))); + gQueue.push(new synthFocus("multiselrichlistbox", new focusChecker("msrlb_item1"))); + gQueue.push(new synthDownKey("msrlb_item1", new focusChecker("msrlb_item2"), { shiftKey: true })); + gQueue.push(new synthFocus("emptyrichlistbox", new focusChecker("emptyrichlistbox"))); + + gQueue.push(new synthFocus("menulist")); + gQueue.push(new synthClick("menulist", new focusChecker("ml_tangerine"), + { where: "center" })); + gQueue.push(new synthDownKey("ml_tangerine", new focusChecker("ml_marmalade"))); + gQueue.push(new synthEscapeKey("ml_marmalade", new focusChecker("menulist"))); + + // On Windows, items get selected during navigation. + let expectedItem = WIN ? "ml_strawberry" : "ml_marmalade"; + gQueue.push(new synthDownKey("menulist", new nofocusChecker(expectedItem))); + gQueue.push(new synthOpenComboboxKey("menulist", new focusChecker(expectedItem))); + gQueue.push(new synthEnterKey(expectedItem, new focusChecker("menulist"))); + + // no focus events for unfocused list controls when current item is + // changed. + gQueue.push(new synthFocus("emptyrichlistbox")); + + gQueue.push(new changeCurrentItem("richlistbox", "rlb_item1")); + gQueue.push(new changeCurrentItem("menulist", WIN ? "ml_marmalade" : "ml_tangerine")); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + // When a menulist contains something other than XUL menuitems, we need + // to manage focus with aria-activedescendant. + info("Testing opening a menupopup with aria-activedescendant"); + let popupDiv1 = getNode("menupopup_ad_div1"); + let focused = PromEvents.waitForEvent(EVENT_FOCUS, popupDiv1); + let popup = getNode("menupopup_ad"); + popup.openPopup(); + await focused; + info("Testing removal of previous active descendant + setting new active descendant"); + focused = PromEvents.waitForEvent(EVENT_FOCUS, "menupopup_ad_div2"); + popupDiv1.remove(); + popup.setAttribute("aria-activedescendant", "menupopup_ad_div2"); + await focused; + popup.hidePopup(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418" + title="Accessibles for focused HTML Select elements are not getting focused state"> + Mozilla Bug 433418 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893" + title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused"> + Mozilla Bug 474893 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368" + title=" fire focus event on document accessible whenever the root or body element is focused"> + Mozilla Bug 552368 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <richlistbox id="richlistbox"> + <richlistitem id="rlb_item1"> + <description>A XUL Description!</description> + </richlistitem> + <richlistitem id="rlb_item2"> + <button label="A XUL Button"/> + </richlistitem> + </richlistbox> + <richlistbox id="multiselrichlistbox" seltype="multiple"> + <richlistitem id="msrlb_item1"> + <description>A XUL Description!</description> + </richlistitem> + <richlistitem id="msrlb_item2"> + <button label="A XUL Button"/> + </richlistitem> + </richlistbox> + <richlistbox id="emptyrichlistbox" seltype="multiple"/> + + <menulist id="menulist"> + <menupopup> + <menuitem id="ml_tangerine" label="tangerine trees"/> + <menuitem id="ml_marmalade" label="marmalade skies"/> + <menuitem id="ml_strawberry" label="strawberry fields"/> + </menupopup> + </menulist> + + <menulist> + <menupopup id="menupopup_ad" aria-activedescendant="menupopup_ad_div1"> + <div id="menupopup_ad_div1" role="option"></div> + <div id="menupopup_ad_div2" role="option"></div> + </menupopup> + </menulist> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_menu.xhtml b/accessible/tests/mochitest/events/test_focus_menu.xhtml new file mode 100644 index 0000000000..dda10517eb --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_menu.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Menu focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + if (WIN) { + gQueue.push(new toggleTopMenu("fruit", new focusChecker("fruit"))); + gQueue.push(new synthRightKey("fruit", new focusChecker("vehicle"))); + gQueue.push(new synthEscapeKey("vehicle", new focusChecker(document))); + } + + // mouse move activate items but no focus event until menubar is active + gQueue.push(new synthMouseMove("fruit", new nofocusChecker("apple"))); + + // mouseover and click on menuitem makes it active before menubar is + // active + gQueue.push(new synthClick("fruit", new focusChecker("fruit"), { where: "center" })); + + // mouseover on menuitem when menubar is active + gQueue.push(new synthMouseMove("apple", new focusChecker("apple"))); + + // keydown on disabled menuitem (disabled items are skipped on linux) + if (WIN) + gQueue.push(new synthDownKey("apple", new focusChecker("orange"))); + + // menu and menuitem are both active + // XXX: intermitent failure because two focus events may be coalesced, + // think to workaround or fix this issue, when done enable queue invoker + // below and remove next two. + //gQueue.push(new synthRightKey("apple", + // [ new focusChecker("vehicle"), + // new focusChecker("cycle")])); + gQueue.push(new synthMouseMove("vehicle", new focusChecker("vehicle"))); + gQueue.push(new synthDownKey("vehicle", new focusChecker("cycle"))); + + // open submenu + gQueue.push(new synthRightKey("cycle", new focusChecker("tricycle"))); + + // move to first menu in cycle, DOMMenuItemActive is fired for fruit, + // cycle and apple menuitems (bug 685191) + todo(false, "focus is fired for 'cycle' menuitem"); + //gQueue.push(new synthRightKey("vehicle", new focusChecker("apple"))); + + // click menuitem to close menu, focus gets back to document + gQueue.push(new synthClick("tricycle", new focusChecker(document), { where: "center" })); + + //enableLogging("focus,DOMEvents,tree"); // logging for bug708927 + //gQueue.onFinish = function() { disableLogging(); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar> + <menu id="fruit" label="Fruit"> + <menupopup> + <menuitem id="apple" label="Apple"/> + <menuitem id="orange" label="Orange" disabled="true"/> + </menupopup> + </menu> + <menu id="vehicle" label="Vehicle"> + <menupopup id="vehiclePopup"> + <menu id="cycle" label="cycle"> + <menupopup> + <menuitem id="tricycle" label="tricycle"/> + </menupopup> + </menu> + <menuitem id="car" label="Car" disabled="true"/> + </menupopup> + </menu> + </menubar> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_name.html b/accessible/tests/mochitest/events/test_focus_name.html new file mode 100644 index 0000000000..aa77923909 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_name.html @@ -0,0 +1,116 @@ +<html> + +<head> + <title>Accessible name testing on focus</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Checker for invokers. + */ + function actionChecker(aID, aDescription) { + this.__proto__ = new invokerChecker(EVENT_FOCUS, aID); + + this.check = function actionChecker_check(aEvent) { + var target = aEvent.accessible; + is(target.description, aDescription, + "Wrong description for " + prettyName(target)); + }; + } + + var gFocusHandler = { + handleEvent: function gFocusHandler_handleEvent(aEvent) { + var elm = aEvent.target; + if (elm.nodeType != Node.ELEMENT_NODE) + return; + + gTooltipElm.style.display = "block"; + + elm.setAttribute("aria-describedby", "tooltip"); + }, + }; + + var gBlurHandler = { + handleEvent: function gBlurHandler_handleEvent(aEvent) { + gTooltipElm.style.display = "none"; + + var elm = aEvent.target; + if (elm.nodeType == Node.ELEMENT_NODE) + elm.removeAttribute("aria-describedby"); + }, + }; + + /** + * Do tests. + */ + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + var gButtonElm = null; + var gTextboxElm = null; + var gTooltipElm = null; + + function doTests() { + gButtonElm = getNode("button"); + gTextboxElm = getNode("textbox"); + gTooltipElm = getNode("tooltip"); + + gButtonElm.addEventListener("focus", gFocusHandler); + gButtonElm.addEventListener("blur", gBlurHandler); + gTextboxElm.addEventListener("focus", gFocusHandler); + gTextboxElm.addEventListener("blur", gBlurHandler); + + // The aria-describedby is changed on DOM focus. Accessible description + // should be updated when a11y focus is fired. + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_FOCUS); + gQueue.onFinish = function() { + gButtonElm.removeEventListener("focus", gFocusHandler); + gButtonElm.removeEventListener("blur", gBlurHandler); + gTextboxElm.removeEventListener("focus", gFocusHandler); + gTextboxElm.removeEventListener("blur", gBlurHandler); + }; + + var descr = "It's a tooltip"; + gQueue.push(new synthFocus("button", new actionChecker("button", descr))); + gQueue.push(new synthTab("textbox", new actionChecker("textbox", descr))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=520709" + title="mochitest to ensure name/description are updated on a11y focus if they were changed on DOM focus"> + Mozilla Bug 520709 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="tooltip" style="display: none" aria-hidden="true">It's a tooltip</div> + <button id="button">button</button> + <input id="textbox"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_removal.html b/accessible/tests/mochitest/events/test_focus_removal.html new file mode 100644 index 0000000000..eb47b07075 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_removal.html @@ -0,0 +1,95 @@ +<html> + +<head> + <title>Test removal of focused accessible</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function setFocus(aNodeToFocus, aExpectedFocus) { + let expected = aExpectedFocus || aNodeToFocus; + let focused = waitForEvent(EVENT_FOCUS, expected); + info("Focusing " + aNodeToFocus.id); + aNodeToFocus.focus(); + await focused; + ok(true, expected.id + " focused after " + + aNodeToFocus.id + ".focus()"); + } + + async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus, aDisplayNone = false) { + let focused = waitForEvent(EVENT_FOCUS, aExpectedFocus); + info("Removing " + aNodeToRemove.id); + if (aDisplayNone) { + aNodeToRemove.style.display = "none"; + } else { + aNodeToRemove.remove(); + } + await focused; + let friendlyExpected = aExpectedFocus == document ? + "document" : aExpectedFocus.id; + ok(true, friendlyExpected + " focused after " + + aNodeToRemove.id + " removed"); + } + + async function doTests() { + info("Testing removal of focused node itself"); + let button = getNode("button"); + await setFocus(button); + await expectFocusAfterRemove(button, document); + + info("Testing removal of focused node's parent"); + let dialog = getNode("dialog"); + let dialogButton = getNode("dialogButton"); + await setFocus(dialogButton); + await expectFocusAfterRemove(dialog, document); + + info("Testing removal of aria-activedescendant target"); + let listbox = getNode("listbox"); + let option = getNode("option"); + await setFocus(listbox, option); + await expectFocusAfterRemove(option, listbox); + + info("Test hiding focused element with display: none"); + let groupingButton = getNode("groupingButton"); + await setFocus(groupingButton); + await expectFocusAfterRemove(groupingButton, document, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="button"></button> + + <div role="dialog" id="dialog"> + <button id="dialogButton"></button> + </div> + + <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option"> + <div role="option" id="option"></div> + </div> + + <div role="grouping" id="grouping"> + <button id="groupingButton"> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_selects.html b/accessible/tests/mochitest/events/test_focus_selects.html new file mode 100644 index 0000000000..2346691dfd --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_selects.html @@ -0,0 +1,190 @@ +<html> + +<head> + <title>Accessible focus testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + var gQueue = null; + + async function doTests() { + // Bug 746534 - File causes crash or hang on OS X + if (MAC) { + todo(false, "Bug 746534 - test file causes crash or hang on OS X"); + SimpleTest.finish(); + return; + } + + let p = waitForEvent(EVENT_FOCUS, "orange"); + // first item is focused until there's selection + getNode("list").focus(); + await p; + + p = waitForEvents({ + expected: [[EVENT_SELECTION, "orange"]], + unexpected: [ + [EVENT_FOCUS], + stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true), + ], + }); + // item is selected and stays focused and active + synthesizeKey("VK_DOWN"); + await p; + + p = waitForEvents([ + stateChangeEventArgs("orange", EXT_STATE_ACTIVE, false, true), + stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true), + [EVENT_FOCUS, "apple"], + ]); + // last selected item is focused + synthesizeKey("VK_DOWN", { shiftKey: true }); + await p; + + p = waitForEvents({ + expected: [ + [EVENT_FOCUS, "orange"], + stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true), + ], + unexpected: [ + [EVENT_FOCUS, "apple"], + stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true), + ], + }); + // no focus event if nothing is changed + synthesizeKey("VK_DOWN"); + // current item is focused + synthesizeKey("VK_UP", { ctrlKey: true }); + await p; + + p = waitForEvent(EVENT_FOCUS, "emptylist"); + // focus on empty list (no items to be focused) + synthesizeKey("VK_TAB"); + await p; + + p = waitForEvents({ + expected: [[EVENT_FOCUS, "orange"]], + unexpected: [stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true)], + }); + // current item is focused + synthesizeKey("VK_TAB", { shiftKey: true }); + await p; + + p = waitForEvent(EVENT_FOCUS, "combobox"); + getNode("combobox").focus(); + await p; + + p = waitForEvents({ + expected: [[EVENT_SELECTION, "cb_apple"]], + unexpected: [ + [EVENT_FOCUS], + stateChangeEventArgs("cb_apple", EXT_STATE_ACTIVE, true, true), + ], + }); + // collapsed combobox keeps a focus + synthesizeKey("VK_DOWN"); + await p; + + // no focus events for unfocused list controls when current item is + // changed + + p = waitForEvent(EVENT_FOCUS, "emptylist"); + getNode("emptylist").focus(); + await p; + + p = waitForEvents({ + expected: [[EVENT_SELECTION, "orange"]], + unexpected: [ + [EVENT_FOCUS], + stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true), + ], + }); + // An unfocused selectable list gets selection change events, + // but not active or focus change events. + getNode("list").selectedIndex = getNode("orange").index; + await p; + + p = waitForEvents({ + expected: [[EVENT_SELECTION, "cb_orange"]], + unexpected: [ + [EVENT_FOCUS], + stateChangeEventArgs("cb_orange", EXT_STATE_ACTIVE, true, true), + ], + }); + // An unfocused selectable combobox gets selection change events, + // but not focus events nor active state change events. + getNode("cb_orange").selected = true; + await p; + + // Bug 1838983: Make sure we don't fail a C++ assertion when the focused + // active item is destroyed. + info("Focusing recreate"); + p = waitForEvent(EVENT_FOCUS, "recreateA"); + const recreate = getNode("recreate"); + recreate.focus(); + await p; + info("Changing recreate size"); + p = waitForEvent(EVENT_FOCUS, recreate); + // This will recreate the select and its children. + recreate.size = 1; + await p; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418" + title="Accessibles for focused HTML Select elements are not getting focused state"> + Mozilla Bug 433418 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893" + title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused"> + Mozilla Bug 474893 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="list" size="5" multiple=""> + <option id="orange">Orange</option> + <option id="apple">Apple</option> + </select> + + <select id="emptylist" size="5"></select> + + <select id="combobox"> + <option id="cb_orange">Orange</option> + <option id="cb_apple">Apple</option> + </select> + + <select id="recreate" size="5"> + <option id="recreateA">a</option> + </select> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_tabbox.xhtml b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml new file mode 100644 index 0000000000..1b808831dc --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Tabbox focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + if (MAC) { + todo(false, "Tests disabled because of imminent failure."); + SimpleTest.finish(); + return; + } + + // Test focus events. + gQueue = new eventQueue(); + + var input = getNode("input"); + gQueue.push(new synthClick("tab1", new focusChecker("tab1"))); + gQueue.push(new synthTab("tab1", new focusChecker("checkbox1"))); + gQueue.push(new synthKey("tab1", "VK_TAB", { ctrlKey: true }, + new focusChecker(input))); + gQueue.push(new synthKey("tab2", "VK_TAB", { ctrlKey: true }, + new focusChecker("tab3"))); + gQueue.push(new synthKey("tab3", "VK_TAB", { ctrlKey: true }, + new focusChecker("tab1"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=370396" + title="Control+Tab to an empty tab panel in a tabbox causes focus to leave the tabbox"> + Mozilla Bug 370396 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tabbox> + <tabs> + <tab id="tab1" label="Tab1" selected="true"/> + <tab id="tab2" label="Tab2" /> + <tab id="tab3" label="Tab3" /> + </tabs> + <tabpanels> + <tabpanel orient="vertical"> + <groupbox orient="vertical"> + <checkbox id="checkbox1" label="Monday" width="75"/> + <checkbox label="Tuesday" width="75"/> + <checkbox label="Wednesday" width="75"/> + <checkbox label="Thursday" width="75"/> + <checkbox label="Friday" width="75"/> + <checkbox label="Saturday" width="75"/> + <checkbox label="Sunday" width="75"/> + </groupbox> + + <spacer style="height: 10px" /> + <label value="Label After checkboxes" /> + </tabpanel> + <tabpanel orient="vertical"> + <html:input id="input" /> + </tabpanel> + <tabpanel orient="vertical"> + <description>Tab 3 content</description> + </tabpanel> + </tabpanels> + </tabbox> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_tree.xhtml b/accessible/tests/mochitest/events/test_focus_tree.xhtml new file mode 100644 index 0000000000..f36816c788 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_tree.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function focusTree(aTreeID) + { + var checker = new focusChecker(getFirstTreeItem, aTreeID); + this.__proto__ = new synthFocus(aTreeID, [ checker ]); + } + + function moveToNextItem(aTreeID) + { + var checker = new focusChecker(getSecondTreeItem, aTreeID); + this.__proto__ = new synthDownKey(aTreeID, [ checker ]); + } + + //////////////////////////////////////////////////////////////////////////// + // Helpers + + function getTreeItemAt(aTreeID, aIdx) + { return getAccessible(aTreeID).getChildAt(aIdx + 1); } + + function getFirstTreeItem(aTreeID) + { return getTreeItemAt(aTreeID, 0); } + + function getSecondTreeItem(aTreeID) + { return getTreeItemAt(aTreeID, 1); } + + //////////////////////////////////////////////////////////////////////////// + // Test + + var gQueue = null; + + //gA11yEventDumpID = "debug"; // debugging + //gA11yEventDumpToConsole = true; // debugging + + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new focusTree("tree")); + gQueue.push(new moveToNextItem("tree")); + gQueue.push(new synthFocus("emptytree")); + + // no focus event for changed current item for unfocused tree + gQueue.push(new changeCurrentItem("tree", 0)); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(5)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=386821" + title="Need better solution for firing delayed event against xul tree"> + Mozilla Bug 386821 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=406308" + title="Don't fire accessible focus events if widget is not actually in focus, confuses screen readers"> + Mozilla Bug 406308 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + <tree id="emptytree" flex="1"> + <treecols> + <treecol id="emptytree_col1" flex="1" primary="true" label="column"/> + <treecol id="emptytree_col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="emptytree_treechildren"/> + </tree> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/events/test_focusable_statechange.html b/accessible/tests/mochitest/events/test_focusable_statechange.html new file mode 100644 index 0000000000..03f4f12587 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focusable_statechange.html @@ -0,0 +1,122 @@ +<html> + +<head> + <title>Test removal of focused accessible</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + function focusableStateChange(id, enabled) { + return [EVENT_STATE_CHANGE, e => { + e.QueryInterface(nsIAccessibleStateChangeEvent); + return getAccessible(id) == e.accessible && + e.state == STATE_FOCUSABLE && (enabled == undefined || e.isEnabled == enabled); + }]; + } + + function editableStateChange(id, enabled) { + return [EVENT_STATE_CHANGE, e => { + e.QueryInterface(nsIAccessibleStateChangeEvent); + return getAccessible(id) == e.accessible && + e.state == EXT_STATE_EDITABLE && e.isExtraState && + (enabled == undefined || e.isEnabled == enabled); + }]; + } + + async function doTests() { + info("disable buttons."); + // Expect focusable change with 'disabled', + // and don't expect it with 'aria-disabled'. + let p = waitForEvents({ + expected: [focusableStateChange("button2", false)], + unexpected: [focusableStateChange("button1")] + }); + getNode("button1").setAttribute("aria-disabled", "true"); + getNode("button2").disabled = true; + await p; + + info("re-enable button"); + // Expect focusable change with 'disabled', + // and don't expect it with 'aria-disabled'. + p = waitForEvents({ + expected: [focusableStateChange("button2", true)], + unexpected: [focusableStateChange("button1")] + }); + getNode("button1").setAttribute("aria-disabled", "false"); + getNode("button2").disabled = false; + await p; + + info("add tabindex"); + // Expect focusable change on non-input, + // and don't expect event on an already focusable input. + p = waitForEvents({ + expected: [focusableStateChange("div", true)], + unexpected: [focusableStateChange("button2")] + }); + getNode("button2").tabIndex = "0"; + getNode("div").tabIndex = "0"; + await p; + + info("remove tabindex"); + // Expect focusable change when removing tabindex. + p = waitForEvent(...focusableStateChange("div", false)); + getNode("div").removeAttribute("tabindex"); + await p; + + info("add contenteditable"); + // Expect editable change on non-input, + // and don't expect event on a native input. + p = waitForEvents({ + expected: [focusableStateChange("div", true), editableStateChange("div", true)], + unexpected: [focusableStateChange("input"), editableStateChange("input")] + }); + getNode("input").contentEditable = true; + getNode("div").contentEditable = true; + await p; + + info("remove contenteditable"); + // Expect editable change on non-input, + // and don't expect event on a native input. + p = waitForEvents({ + expected: [focusableStateChange("div", false), editableStateChange("div", false)], + unexpected: [focusableStateChange("input"), editableStateChange("input")] + }); + getNode("input").contentEditable = false; + getNode("div").contentEditable = false; + await p; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="button1"></button> + <button id="button2"></button> + + <div id="div">Hello</div> + + <input id="input" value="Hello"> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_fromUserInput.html b/accessible/tests/mochitest/events/test_fromUserInput.html new file mode 100644 index 0000000000..b3617358cf --- /dev/null +++ b/accessible/tests/mochitest/events/test_fromUserInput.html @@ -0,0 +1,112 @@ +<html> + +<head> + <title>Testing of isFromUserInput in text events</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + /** + * Remove text data from HTML input. + */ + function removeTextFromInput(aID, aStart, aEnd, aText, aFromUser) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser), + ]; + + this.invoke = function removeTextFromInput_invoke() { + this.DOMNode.focus(); + this.DOMNode.setSelectionRange(aStart, aEnd); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromInput_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + /** + * Remove text data from text node. + */ + function removeTextFromContentEditable(aID, aStart, aEnd, aText, aFromUser) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser), + ]; + + this.invoke = function removeTextFromContentEditable_invoke() { + this.DOMNode.focus(); + this.textNode = getNode(aID).firstChild; + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(this.textNode, aStart); + range.setEnd(this.textNode, aEnd); + selection.addRange(range); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromContentEditable_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + // gA11yEventDumpID = "eventdump"; // debug stuff + + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + // Focused editable text node + gQueue.push(new removeTextFromContentEditable("div", 0, 3, "hel", true)); + + // Focused editable HTML input + gQueue.push(new removeTextFromInput("input", 1, 2, "n", true)); + + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + + </script> +</head> + + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=686909" + title="isFromUserInput flag on accessible text change events not correct"> + Mozilla Bug 686909 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="eventdump"></div> + + <div id="div" contentEditable="true">hello</div> + <input id="input" value="input"> + +</body> + +</html> diff --git a/accessible/tests/mochitest/events/test_label.xhtml b/accessible/tests/mochitest/events/test_label.xhtml new file mode 100644 index 0000000000..5780629dc6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_label.xhtml @@ -0,0 +1,178 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Tests: accessible XUL label/description events"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invokers + + const kRecreated = 0; + const kTextRemoved = 1; + const kTextChanged = 2; + + const kNoValue = 0; + + /** + * Set/remove @value attribute. + */ + function setValue(aID, aValue, aResult, aOldValue) + { + this.labelNode = getNode(aID); + + this.eventSeq = []; + + switch (aResult) { + case kRecreated: + this.eventSeq.push(new invokerChecker(EVENT_HIDE, this.labelNode)); + this.eventSeq.push(new invokerChecker(EVENT_SHOW, this.labelNode)); + break; + case kTextRemoved: + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aOldValue.length, + aOldValue, false)); + break; + case kTextChanged: + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aOldValue.length, + aOldValue, false)); + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aValue.length, + aValue, true)); + break; + } + + this.invoke = function setValue_invoke() + { + if (aValue === kNoValue) + this.labelNode.removeAttribute("value"); + else + this.labelNode.setAttribute("value", aValue); + } + + this.finalCheck = function setValue_finalCheck() + { + let tree = + { LABEL: [] }; + + const expectChild = (() => { + if (aValue === kNoValue) { + return false; + } + if (aValue === "") { + return this.labelNode.nodeName == "label"; + } + return true; + })(); + + if (expectChild) { + tree.LABEL.push({ STATICTEXT: [ ] }); + } + testAccessibleTree(aID, tree); + } + + this.getID = function setValue_getID() + { + return "set @value='" + aValue + "' for label " + prettyName(aID); + } + } + + /** + * Change @crop attribute. + */ + function setCrop(aID, aCropValue, aRemovedText, aInsertedText) + { + this.labelNode = getNode(aID); + this.width = this.labelNode.getBoundingClientRect().width; + this.charWidth = this.width / this.labelNode.value.length; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.labelNode), + new invokerChecker(EVENT_SHOW, this.labelNode), + ]; + + this.invoke = function setCrop_invoke() + { + if (!this.labelNode.hasAttribute("crop")) + this.labelNode.style.width = Math.floor(this.width - 2 * this.charWidth) + "px"; + + this.labelNode.setAttribute("crop", aCropValue); + } + + this.getID = function setCrop_finalCheck() + { + return "set crop " + aCropValue; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new setValue("label", "shiroka strana", kRecreated)); + gQueue.push(new setValue("label", "?<>!+_", kTextChanged, "shiroka strana")); + gQueue.push(new setValue("label", "", kRecreated)); + gQueue.push(new setValue("label", kNoValue, kRecreated)); + + gQueue.push(new setValue("descr", "hello world", kRecreated)); + gQueue.push(new setValue("descr", "si_ya", kTextChanged, "hello world")); + gQueue.push(new setValue("descr", "", kTextRemoved, "si_ya")); + gQueue.push(new setValue("descr", kNoValue, kRecreated)); + + gQueue.push(new setCrop("croplabel", "center")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166" + title="xul:label@value accessible should implement nsIAccessibleText"> + Bug 396166 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label id="label"/> + <description id="descr"/> + + <hbox> + <label id="croplabel" value="valuetocro" + style="font-family: monospace;"/> + </hbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/events/test_menu.xhtml b/accessible/tests/mochitest/events/test_menu.xhtml new file mode 100644 index 0000000000..271831ba83 --- /dev/null +++ b/accessible/tests/mochitest/events/test_menu.xhtml @@ -0,0 +1,200 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible menu events testing for XUL menu"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script><![CDATA[ + function openFileMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_MENU_START, "menubar"), + new invokerChecker("popupshown", "menupopup-file") + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-newtab")) intermitent failure + ]; + + this.invoke = function openFileMenu_invoke() + { + synthesizeKey("F", {altKey: true, shiftKey: true}); + } + + this.getID = function openFileMenu_getID() + { + return "open file menu by alt+F press"; + } + } + + function openEditMenu() + { + this.eventSeq = [ + new invokerChecker("popuphidden", "menupopup-file"), + new invokerChecker("popupshown", "menupopup-edit"), + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-undo")) intermitent failure + ]; + + this.invoke = function openEditMenu_invoke() + { + synthesizeKey("KEY_ArrowRight"); + } + + this.getID = function openEditMenu_getID() + { + return "open edit menu by lef arrow press"; + } + } + + function closeEditMenu() + { + this.eventSeq = [ + //new invokerChecker(EVENT_FOCUS, document), intermitent failure + new invokerChecker("popuphidden", "menupopup-edit"), + ]; + + this.invoke = function closeEditMenu_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function closeEditMenu_getID() + { + return "close edit menu"; + } + } + + function focusFileMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_MENU_START, getNode("menubar")) + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-file")) //intermitent failure + ]; + + this.invoke = function focusFileMenu_invoke() + { + synthesizeKey("KEY_Alt"); + } + + this.getID = function focusFileMenu_getID() + { + return "activate menubar, focus file menu (atl press)"; + } + } + + function focusEditMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode("menuitem-edit")) + ]; + + this.invoke = function focusEditMenu_invoke() + { + synthesizeKey("KEY_ArrowRight"); + } + + this.getID = function focusEditMenu_getID() + { + return "focus edit menu"; + } + } + + function leaveMenubar() + { + this.eventSeq = [ + //new invokerChecker(EVENT_FOCUS, document), intermitent failure + new invokerChecker(EVENT_MENU_END, "menubar") + ]; + + this.invoke = function leaveMenubar_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function leaveMenubar_getID() + { + return "leave menubar"; + } + } + + /** + * Do tests. + */ + + //gA11yEventDumpID = "eventdump"; + //gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTests() + { + if (!WIN && !LINUX) { + todo(false, "Enable this test on other platforms."); + SimpleTest.finish(); + return; + } + + todo(false, + "Fix intermitent failures. Focus may randomly occur before or after menupopup events!"); + + gQueue = new eventQueue(); + + gQueue.push(new openFileMenu()); + gQueue.push(new openEditMenu()); + gQueue.push(new closeEditMenu()); + gQueue.push(new leaveMenubar()); + + // Alt key is used to active menubar and focus menu item on Windows, + // other platforms requires setting a ui.key.menuAccessKeyFocuses + // preference. + if (WIN || LINUX) { + gQueue.push(new focusFileMenu()); + gQueue.push(new focusEditMenu()); + gQueue.push(new leaveMenubar()); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]></script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189" + title="Clean up FireAccessibleFocusEvent"> + Mozilla Bug 615189 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar id="menubar"> + <menu id="menuitem-file" label="File" accesskey="F"> + <menupopup id="menupopup-file"> + <menuitem id="menuitem-newtab" label="New Tab"/> + </menupopup> + </menu> + <menu id="menuitem-edit" label="Edit" accesskey="E"> + <menupopup id="menupopup-edit"> + <menuitem id="menuitem-undo" label="Undo"/> + </menupopup> + </menu> + </menubar> + + <vbox id="eventdump" role="log"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_mutation.html b/accessible/tests/mochitest/events/test_mutation.html new file mode 100644 index 0000000000..7ee876570b --- /dev/null +++ b/accessible/tests/mochitest/events/test_mutation.html @@ -0,0 +1,580 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + div.displayNone a { display:none; } + div.visibilityHidden a { visibility:hidden; } +</style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Invokers. + */ + var kNoEvents = 0; + + var kShowEvent = 1; + var kHideEvent = 2; + var kReorderEvent = 4; + var kShowEvents = kShowEvent | kReorderEvent; + var kHideEvents = kHideEvent | kReorderEvent; + var kHideAndShowEvents = kHideEvents | kShowEvent; + + /** + * Base class to test mutation a11y events. + * + * @param aNodeOrID [in] node invoker's action is executed for + * @param aEventTypes [in] events to register (see constants above) + * @param aDoNotExpectEvents [in] boolean indicates if events are expected + */ + function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents) { + // Interface + this.DOMNode = getNode(aNodeOrID); + this.doNotExpectEvents = aDoNotExpectEvents; + this.eventSeq = []; + this.unexpectedEventSeq = []; + + /** + * Change default target (aNodeOrID) registered for the given event type. + */ + this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget) { + var type = this.getA11yEventType(aEventType); + for (var idx = 0; idx < this.getEventSeq().length; idx++) { + if (this.getEventSeq()[idx].type == type) { + this.getEventSeq()[idx].target = aTarget; + return idx; + } + } + return -1; + }; + + /** + * Replace the default target currently registered for a given event type + * with the nodes in the targets array. + */ + this.setTargets = function mutateA11yTree_setTargets(aEventType, aTargets) { + var targetIdx = this.setTarget(aEventType, aTargets[0]); + + var type = this.getA11yEventType(aEventType); + for (var i = 1; i < aTargets.length; i++) { + let checker = new invokerChecker(type, aTargets[i]); + this.getEventSeq().splice(++targetIdx, 0, checker); + } + }; + + // Implementation + this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType) { + if (aEventType == kReorderEvent) + return nsIAccessibleEvent.EVENT_REORDER; + + if (aEventType == kHideEvent) + return nsIAccessibleEvent.EVENT_HIDE; + + if (aEventType == kShowEvent) + return nsIAccessibleEvent.EVENT_SHOW; + + return 0; + }; + + this.getEventSeq = function mutateA11yTree_getEventSeq() { + return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq; + }; + + if (aEventTypes & kHideEvent) { + let checker = new invokerChecker(this.getA11yEventType(kHideEvent), + this.DOMNode); + this.getEventSeq().push(checker); + } + + if (aEventTypes & kShowEvent) { + let checker = new invokerChecker(this.getA11yEventType(kShowEvent), + this.DOMNode); + this.getEventSeq().push(checker); + } + + if (aEventTypes & kReorderEvent) { + let checker = new invokerChecker(this.getA11yEventType(kReorderEvent), + this.DOMNode.parentNode); + this.getEventSeq().push(checker); + } + } + + /** + * Change CSS style for the given node. + */ + function changeStyle(aNodeOrID, aProp, aValue, aEventTypes) { + this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false); + + this.invoke = function changeStyle_invoke() { + this.DOMNode.style[aProp] = aValue; + }; + + this.getID = function changeStyle_getID() { + return aNodeOrID + " change style " + aProp + " on value " + aValue; + }; + } + + /** + * Change class name for the given node. + */ + function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes) { + this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false); + + this.invoke = function changeClass_invoke() { + this.parentDOMNode.className = aClassName; + }; + + this.getID = function changeClass_getID() { + return aNodeOrID + " change class " + aClassName; + }; + + this.parentDOMNode = getNode(aParentNodeOrID); + } + + /** + * Clone the node and append it to its parent. + */ + function cloneAndAppendToDOM(aNodeOrID, aEventTypes, + aTargetsFunc, aReorderTargetFunc) { + var eventTypes = aEventTypes || kShowEvents; + var doNotExpectEvents = (aEventTypes == kNoEvents); + + this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes, + doNotExpectEvents); + + this.invoke = function cloneAndAppendToDOM_invoke() { + var newElm = this.DOMNode.cloneNode(true); + newElm.removeAttribute("id"); + + var targets = aTargetsFunc ? + aTargetsFunc(newElm) : [newElm]; + this.setTargets(kShowEvent, targets); + + if (aReorderTargetFunc) { + var reorderTarget = aReorderTargetFunc(this.DOMNode); + this.setTarget(kReorderEvent, reorderTarget); + } + + this.DOMNode.parentNode.appendChild(newElm); + }; + + this.getID = function cloneAndAppendToDOM_getID() { + return aNodeOrID + " clone and append to DOM."; + }; + } + + /** + * Removes the node from DOM. + */ + function removeFromDOM(aNodeOrID, aEventTypes, + aTargetsFunc, aReorderTargetFunc) { + var eventTypes = aEventTypes || kHideEvents; + var doNotExpectEvents = (aEventTypes == kNoEvents); + + this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes, + doNotExpectEvents); + + this.invoke = function removeFromDOM_invoke() { + this.DOMNode.remove(); + }; + + this.getID = function removeFromDOM_getID() { + return prettyName(aNodeOrID) + " remove from DOM."; + }; + + if (aTargetsFunc && (eventTypes & kHideEvent)) + this.setTargets(kHideEvent, aTargetsFunc(this.DOMNode)); + + if (aReorderTargetFunc && (eventTypes & kReorderEvent)) + this.setTarget(kReorderEvent, aReorderTargetFunc(this.DOMNode)); + } + + /** + * Clone the node and replace the original node by cloned one. + */ + function cloneAndReplaceInDOM(aNodeOrID) { + this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents, + false); + + this.invoke = function cloneAndReplaceInDOM_invoke() { + this.DOMNode.parentNode.replaceChild(this.newElm, this.DOMNode); + }; + + this.getID = function cloneAndReplaceInDOM_getID() { + return aNodeOrID + " clone and replace in DOM."; + }; + + this.newElm = this.DOMNode.cloneNode(true); + this.newElm.removeAttribute("id"); + this.setTarget(kShowEvent, this.newElm); + } + + /** + * Trigger content insertion (flush layout), removal and insertion of + * the same element for the same parent. + */ + function test1(aContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test1"); + this.containerNode = getNode(aContainerID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function test1_invoke() { + this.containerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.containerNode.removeChild(this.divNode); + this.containerNode.appendChild(this.divNode); + }; + + this.getID = function test1_getID() { + return "fuzzy test #1: content insertion (flush layout), removal and" + + "reinsertion"; + }; + } + + /** + * Trigger content insertion (flush layout), removal and insertion of + * the same element for the different parents. + */ + function test2(aContainerID, aTmpContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test2"); + this.containerNode = getNode(aContainerID); + this.tmpContainerNode = getNode(aTmpContainerID); + this.container = getAccessible(this.containerNode); + this.tmpContainer = getAccessible(this.tmpContainerNode); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_REORDER, this.tmpContainerNode), + ]; + + this.invoke = function test2_invoke() { + this.tmpContainerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.tmpContainerNode.removeChild(this.divNode); + this.containerNode.appendChild(this.divNode); + }; + + this.getID = function test2_getID() { + return "fuzzy test #2: content insertion (flush layout), removal and" + + "reinsertion under another container"; + }; + } + + /** + * Content insertion (flush layout) and then removal (nothing was changed). + */ + function test3(aContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test3"); + this.containerNode = getNode(aContainerID); + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_HIDE, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function test3_invoke() { + this.containerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.containerNode.removeChild(this.divNode); + }; + + this.getID = function test3_getID() { + return "fuzzy test #3: content insertion (flush layout) and removal"; + }; + } + + function insertReferredElm(aContainerID) { + this.containerNode = getNode(aContainerID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.firstChild; }, this.containerNode), + new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.lastChild; }, this.containerNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function insertReferredElm_invoke() { + let span = document.createElement("span"); + span.setAttribute("id", "insertReferredElms_span"); + let input = document.createElement("input"); + input.setAttribute("aria-labelledby", "insertReferredElms_span"); + this.containerNode.appendChild(span); + this.containerNode.appendChild(input); + }; + + this.getID = function insertReferredElm_getID() { + return "insert inaccessible element and then insert referring element to make it accessible"; + }; + } + + function showHiddenParentOfVisibleChild() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("c4_child")), + new invokerChecker(EVENT_SHOW, getNode("c4_middle")), + new invokerChecker(EVENT_REORDER, getNode("c4")), + ]; + + this.invoke = function showHiddenParentOfVisibleChild_invoke() { + getNode("c4_middle").style.visibility = "visible"; + }; + + this.getID = function showHiddenParentOfVisibleChild_getID() { + return "show hidden parent of visible child"; + }; + } + + function hideNDestroyDoc() { + this.txt = null; + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, () => { return this.txt; }), + ]; + + this.invoke = function hideNDestroyDoc_invoke() { + this.txt = getAccessible("c5").firstChild.firstChild; + this.txt.DOMNode.remove(); + }; + + this.check = function hideNDestroyDoc_check() { + getNode("c5").remove(); + }; + + this.getID = function hideNDestroyDoc_getID() { + return "remove text node and destroy a document on hide event"; + }; + } + + function hideHideNDestroyDoc() { + this.target = null; + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, () => { return this.target; }), + ]; + + this.invoke = function hideHideNDestroyDoc_invoke() { + var doc = getAccessible("c6").firstChild; + var l1 = doc.firstChild; + this.target = l1.firstChild; + var l2 = doc.lastChild; + l1.DOMNode.firstChild.remove(); + l2.DOMNode.firstChild.remove(); + }; + + this.check = function hideHideNDestroyDoc_check() { + getNode("c6").remove(); + }; + + this.getID = function hideHideNDestroyDoc_getID() { + return "remove text nodes (2 events in the queue) and destroy a document on first hide event"; + }; + } + + /** + * Target getters. + */ + function getFirstChild(aNode) { + return [aNode.firstChild]; + } + function getLastChild(aNode) { + return [aNode.lastChild]; + } + + function getNEnsureFirstChild(aNode) { + var node = aNode.firstChild; + getAccessible(node); + return [node]; + } + + function getNEnsureChildren(aNode) { + var children = []; + var node = aNode.firstChild; + do { + children.push(node); + getAccessible(node); + node = node.nextSibling; + } while (node); + + return children; + } + + function getParent(aNode) { + return aNode.parentNode; + } + + // gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("events,verbose"); + + /** + * Do tests. + */ + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + // Show/hide events by changing of display style of accessible DOM node + // from 'inline' to 'none', 'none' to 'inline'. + let id = "link1"; + getAccessible(id); // ensure accessible is created + gQueue.push(new changeStyle(id, "display", "none", kHideEvents)); + gQueue.push(new changeStyle(id, "display", "inline", kShowEvents)); + + // Show/hide events by changing of visibility style of accessible DOM node + // from 'visible' to 'hidden', 'hidden' to 'visible'. + id = "link2"; + getAccessible(id); + gQueue.push(new changeStyle(id, "visibility", "hidden", kHideEvents)); + gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents)); + + // Show/hide events by changing of visibility style of accessible DOM node + // from 'collapse' to 'visible', 'visible' to 'collapse'. + id = "link4"; + gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents)); + gQueue.push(new changeStyle(id, "visibility", "collapse", kHideEvents)); + + // Show/hide events by adding new accessible DOM node and removing old one. + id = "link5"; + gQueue.push(new cloneAndAppendToDOM(id)); + gQueue.push(new removeFromDOM(id)); + + // No show/hide events by adding new not accessible DOM node and removing + // old one, no reorder event for their parent. + id = "child1"; + gQueue.push(new cloneAndAppendToDOM(id, kNoEvents)); + gQueue.push(new removeFromDOM(id, kNoEvents)); + + // Show/hide events by adding new accessible DOM node and removing + // old one, there is reorder event for their parent. + id = "child2"; + gQueue.push(new cloneAndAppendToDOM(id)); + gQueue.push(new removeFromDOM(id)); + + // Show/hide events by adding new DOM node containing accessible DOM and + // removing old one, there is reorder event for their parent. + id = "child3"; + gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild, + getParent)); + + // Hide event for accessible child of unaccessible removed DOM node and + // reorder event for its parent. + gQueue.push(new removeFromDOM(id, kHideEvents, + getNEnsureFirstChild, getParent)); + + // Hide events for accessible children of unaccessible removed DOM node + // and reorder event for its parent. + gQueue.push(new removeFromDOM("child4", kHideEvents, + getNEnsureChildren, getParent)); + + // Show/hide events by creating new accessible DOM node and replacing + // old one. + getAccessible("link6"); // ensure accessible is created + gQueue.push(new cloneAndReplaceInDOM("link6")); + + // Show/hide events by changing class name on the parent node. + gQueue.push(new changeClass("container2", "link7", "", kShowEvents)); + gQueue.push(new changeClass("container2", "link7", "displayNone", + kHideEvents)); + + gQueue.push(new changeClass("container3", "link8", "", kShowEvents)); + gQueue.push(new changeClass("container3", "link8", "visibilityHidden", + kHideEvents)); + + gQueue.push(new test1("testContainer")); + gQueue.push(new test2("testContainer", "testContainer2")); + gQueue.push(new test2("testContainer", "testNestedContainer")); + gQueue.push(new test3("testContainer")); + gQueue.push(new insertReferredElm("testContainer3")); + gQueue.push(new showHiddenParentOfVisibleChild()); + + gQueue.push(new hideNDestroyDoc()); + gQueue.push(new hideHideNDestroyDoc()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=469985" + title=" turn the test from bug 354745 into mochitest"> + Mozilla Bug 469985</a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=472662" + title="no reorder event when html:link display property is changed from 'none' to 'inline'"> + Mozilla Bug 472662</a> + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275</a> + <a target="_blank" + title="Develop a way to handle visibility style" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125"> + Mozilla Bug 606125</a> + <a target="_blank" + title="Update accessible tree on content insertion after layout" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015"> + Mozilla Bug 498015</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <div id="testContainer"> + <a id="link1" href="http://www.google.com">Link #1</a> + <a id="link2" href="http://www.google.com">Link #2</a> + <a id="link3" href="http://www.google.com">Link #3</a> + <a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a> + <a id="link5" href="http://www.google.com">Link #5</a> + + <div id="container" role="list"> + <span id="child1"></span> + <span id="child2" role="listitem"></span> + <span id="child3"><span role="listitem"></span></span> + <span id="child4"><span id="child4_1" role="listitem"></span><span id="child4_2" role="listitem"></span></span> + </div> + + <a id="link6" href="http://www.google.com">Link #6</a> + + <div id="container2" class="displayNone"><a id="link7">Link #7</a></div> + <div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div> + <div id="testNestedContainer"></div> + </div> + <div id="testContainer2"></div> + <div id="testContainer3"></div> + + <div id="c4"> + <div style="visibility:hidden" id="c4_middle"> + <div style="visibility:visible" id="c4_child"></div> + </div> + + <iframe id="c5" src="data:text/html,hey"></iframe> + <iframe id="c6" src="data:text/html,<label>l</label><label>l</label>"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_namechange.html b/accessible/tests/mochitest/events/test_namechange.html new file mode 100644 index 0000000000..840e2dfb4f --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.html @@ -0,0 +1,185 @@ +<html> + +<head> + <title>Accessible name change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function setAttr(aID, aAttr, aValue, aChecker) { + this.eventSeq = [ aChecker ]; + this.invoke = function setAttr_invoke() { + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + /** + * No name change on an accessible, because the accessible is recreated. + */ + function setAttr_recreate(aID, aAttr, aValue) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible(aID)), + new invokerChecker(EVENT_SHOW, aID), + ]; + this.invoke = function setAttr_recreate_invoke() { + todo(false, "No accessible recreation should happen, just name change event"); + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_recreate_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + async function doTests() { + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new setAttr("tst1", "aria-label", "hi", + new invokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "alt", "alt", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "aria-labelledby", "display", + new invokerChecker(EVENT_NAME_CHANGE, "tst1"))); + + gQueue.push(new setAttr("tst2", "aria-labelledby", "display", + new invokerChecker(EVENT_NAME_CHANGE, "tst2"))); + gQueue.push(new setAttr("tst2", "alt", "alt", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + gQueue.push(new setAttr("tst2", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + gQueue.push(new setAttr("tst2", "aria-label", "hi", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + + // When `alt` attribute is added or removed from a broken img, + // the accessible is recreated. + gQueue.push(new setAttr_recreate("tst3", "alt", "one")); + // When an `alt` attribute is changed, there is a name change event. + gQueue.push(new setAttr("tst3", "alt", "two", + new invokerChecker(EVENT_NAME_CHANGE, "tst3"))); + gQueue.push(new setAttr("tst3", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst3"))); + + gQueue.push(new setAttr("tst4", "title", "title", + new invokerChecker(EVENT_NAME_CHANGE, "tst4"))); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + const labelledBy = getNode("labelledBy"); + const label = getNode("label"); + let nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy); + info("Changing text of aria-labelledby target"); + label.textContent = "l2"; + await nameChanged; + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy); + info("Adding node to aria-labelledby target"); + label.innerHTML = '<p id="labelChild">l3</p>'; + await nameChanged; + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy); + info("Changing text of aria-labelledby target's child"); + getNode("labelChild").textContent = "l4"; + await nameChanged; + + const lateLabelledBy = getNode("lateLabelledBy"); + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy); + info("Setting aria-labelledby"); + lateLabelledBy.setAttribute("aria-labelledby", "lateLabel"); + await nameChanged; + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy); + info("Changing text of late aria-labelledby target's child"); + getNode("lateLabelChild").textContent = "l2"; + await nameChanged; + + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "listitem"); + info("Changing textContent of listitem child"); + // Changing textContent replaces the text leaf with a new one. + getNode("listitem").textContent = "world"; + await nameChanged; + + nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "button"); + info("Changing text of button's text leaf"); + // Changing the text node's data changes the text without replacing the + // leaf. + getNode("button").firstChild.data = "after"; + await nameChanged; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969" + title="Event not fired when description changes"> + Bug 991969 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <img id="tst1" alt="initial" src="../moz.png"> + <img id="tst2" src="../moz.png"> + <img id="tst3"> + <img id="tst4" src="../moz.png"> + + <div id="labelledBy" aria-labelledby="label"></div> + <div id="label">l1</div> + + <div id="lateLabelledBy"></div> + <div id="lateLabel"><p id="lateLabelChild">l1</p></div> + + <ul><li id="listitem">hello</li></ul> + + <button id="button">before</button> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_namechange.xhtml b/accessible/tests/mochitest/events/test_namechange.xhtml new file mode 100644 index 0000000000..a6dd8cb218 --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.xhtml @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + /** + * Check name changed a11y event. + */ + function nameChangeChecker(aMsg, aID) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + return getAccessible(aID); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + " name changed"; + } + } + + function changeRichListItemChild() + { + this.invoke = function changeRichListItemChild_invoke() + { + getNode('childcontent').setAttribute('value', 'Changed.'); + } + + this.eventSeq = + [ + new nameChangeChecker("changeRichListItemChild: ", "listitem") + ]; + + this.getID = function changeRichListItemChild_getID() + { + return "changeRichListItemChild"; + } + } + + function doTest() + { + var queue = new eventQueue(); + queue.push(new changeRichListItemChild()); + queue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=986054" + title="Propagate name change events"> + Mozilla Bug 986054 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <richlistbox> + <richlistitem id="listitem"> + <description id="childcontent" value="This will be changed."/> + </richlistitem> + </richlistbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_scroll.xhtml b/accessible/tests/mochitest/events/test_scroll.xhtml new file mode 100644 index 0000000000..d3cc2a7bda --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll.xhtml @@ -0,0 +1,107 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../promisified-events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Tests + + function matchesAnchorJumpInTabDocument(aTabIdx) { + return evt => { + let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument(); + let anchorAcc = getAccessible(tabDoc.querySelector("a[name='link1']")); + return anchorAcc == evt.accessible; + } + } + + function matchesTabDocumentAt(aTabIdx) { + return evt => { + let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument(); + return getAccessible(tabDoc) == evt.accessible; + } + } + + async function doTest() { + const kURL = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#link1"; + let evtProm = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument], + [EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument()], + ], "Load foreground tab then scroll to anchor"); + + tabBrowser().loadURI(Services.io.newURI(kURL), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + await evtProm; + + + evtProm = waitForEvents({ + expected: [[EVENT_DOCUMENT_LOAD_COMPLETE, matchesTabDocumentAt(1)]], + unexpected: [[EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument(1)]], + }, "Load background tab, don't dispatch scroll to anchor event"); + + tabBrowser().addTab(kURL, { + referrerURI: null, + charset: "", + postData: null, + inBackground: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + await evtProm; + + evtProm = waitForEvent(EVENT_SCROLLING_START, + matchesAnchorJumpInTabDocument(), + "Scroll to anchor event dispatched when switching to loaded doc."); + tabBrowser().selectTabAtIndex(1); + await evtProm; + + closeBrowserWindow(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=691734" + title="Make sure scrolling start event is fired when document receive focus"> + Mozilla Bug 691734 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_scroll_caret.xhtml b/accessible/tests/mochitest/events/test_scroll_caret.xhtml new file mode 100644 index 0000000000..f0f0fccfb2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll_caret.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Tests + + function getAnchorJumpInTabDocument(aTabIdx) + { + var tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument(); + return tabDoc.querySelector("h1[id='heading_1']"); + } + + function loadTab(aURL) + { + this.eventSeq = [ + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument), + new asyncCaretMoveChecker(0, getAnchorJumpInTabDocument) + ]; + + this.invoke = function loadTab_invoke() + { + tabBrowser().loadURI(Services.io.newURI(aURL), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } + + this.getID = function loadTab_getID() + { + return "load tab: " + aURL; + } + } + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + var url = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#heading_1"; + gQueue.push(new loadTab(url)); + gQueue.onFinish = function() { closeBrowserWindow(); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1056459" + title="Make sure caret move event is fired when document receive focus"> + Mozilla Bug 1056459 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_selection.html b/accessible/tests/mochitest/events/test_selection.html new file mode 100644 index 0000000000..a749dd9c4c --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.html @@ -0,0 +1,109 @@ +<html> + +<head> + <title>Accessible selection event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + + // closed combobox + gQueue.push(new synthFocus("combobox")); + gQueue.push(new synthDownKey("cb1_item1", + selChangeSeq("cb1_item1", "cb1_item2"))); + + // listbox + gQueue.push(new synthClick("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + selChangeSeq("lb1_item1", "lb1_item2"))); + + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + selChangeSeq(null, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + selAddSeq("lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + selRemoveSeq("lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + selRemoveSeq("lb2_item1"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Bug 414302 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268" + title="There's no way to know unselected item when selection in single selection was changed"> + Bug 810268 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="combobox"> + <option id="cb1_item1" value="mushrooms">mushrooms + <option id="cb1_item2" value="greenpeppers">green peppers + <option id="cb1_item3" value="onions" id="onions">onions + <option id="cb1_item4" value="tomatoes">tomatoes + <option id="cb1_item5" value="olives">olives + </select> + + <select id="listbox" size=5> + <option id="lb1_item1" value="mushrooms">mushrooms + <option id="lb1_item2" value="greenpeppers">green peppers + <option id="lb1_item3" value="onions" id="onions">onions + <option id="lb1_item4" value="tomatoes">tomatoes + <option id="lb1_item5" value="olives">olives + </select> + + <p>Pizza</p> + <select id="listbox2" multiple size=5> + <option id="lb2_item1" value="mushrooms">mushrooms + <option id="lb2_item2" value="greenpeppers">green peppers + <option id="lb2_item3" value="onions" id="onions">onions + <option id="lb2_item4" value="tomatoes">tomatoes + <option id="lb2_item5" value="olives">olives + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_selection.xhtml b/accessible/tests/mochitest/events/test_selection.xhtml new file mode 100644 index 0000000000..9c34ddf286 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.xhtml @@ -0,0 +1,254 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Selection event tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + function advanceTab(aTabsID, aDirection, aNextTabID) + { + var eventSeq1 = [ + new invokerChecker(EVENT_SELECTION, aNextTabID) + ] + defineScenario(this, eventSeq1); + + var eventSeq2 = [ + new invokerChecker(EVENT_HIDE, getAccessible(aNextTabID)), + new invokerChecker(EVENT_SHOW, aNextTabID) + ]; + defineScenario(this, eventSeq2); + + this.invoke = function advanceTab_invoke() + { + todo(false, "No accessible recreation should happen, just selection event"); + getNode(aTabsID).advanceSelectedTab(aDirection, true); + } + + this.getID = function synthFocus_getID() + { + return "advanceTab on " + prettyName(aTabsID) + " to " + prettyName(aNextTabID); + } + } + + function select4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(3)) + ]; + + this.invoke = function select4FirstItems_invoke() + { + synthesizeKey("VK_DOWN", { shiftKey: true }); // selects two items + synthesizeKey("VK_DOWN", { shiftKey: true }); + synthesizeKey("VK_DOWN", { shiftKey: true }); + } + + this.getID = function select4FirstItems_getID() + { + return "select 4 first items for " + prettyName(aID); + } + } + + function unselect4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(3)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselect4FirstItems_invoke() + { + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey(" ", { ctrlKey: true }); // unselect first item + } + + this.getID = function unselect4FirstItems_getID() + { + return "unselect 4 first items for " + prettyName(aID); + } + } + + function selectAllItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function selectAllItems_invoke() + { + synthesizeKey("VK_END", { shiftKey: true }); + } + + this.getID = function selectAllItems_getID() + { + return "select all items for " + prettyName(aID); + } + } + + function unselectAllItemsButFirst(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function unselectAllItemsButFirst_invoke() + { + synthesizeKey("VK_HOME", { shiftKey: true }); + } + + this.getID = function unselectAllItemsButFirst_getID() + { + return "unselect all items for " + prettyName(aID); + } + } + + function unselectSelectItem(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselectSelectItem_invoke() + { + synthesizeKey(" ", { ctrlKey: true }); // select item + synthesizeKey(" ", { ctrlKey: true }); // unselect item + } + + this.getID = function unselectSelectItem_getID() + { + return "unselect and then select first item for " + prettyName(aID); + } + } + + /** + * Do tests. + */ + var gQueue = null; + + //enableLogging("events"); + //gA11yEventDumpToConsole = true; // debuggin + + function doTests() + { + gQueue = new eventQueue(); + + ////////////////////////////////////////////////////////////////////////// + // tabbox + gQueue.push(new advanceTab("tabs", 1, "tab3")); + + ////////////////////////////////////////////////////////////////////////// + // single selection listbox, the first item is selected by default + + gQueue.push(new synthClick("lb1_item2", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + gQueue.push(new synthUpKey("lb1_item2", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + + ////////////////////////////////////////////////////////////////////////// + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); + + ////////////////////////////////////////////////////////////////////////// + // selection event coalescence + + // fire 4 selection_add events + gQueue.push(new select4FirstItems("listbox2")); + // fire 4 selection_remove events + gQueue.push(new unselect4FirstItems("listbox2")); + // fire selection_within event + gQueue.push(new selectAllItems("listbox2")); + // fire selection_within event + gQueue.push(new unselectAllItemsButFirst("listbox2")); + // fire selection_remove/add events + gQueue.push(new unselectSelectItem("listbox2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Mozilla Bug 414302 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <tabbox id="tabbox" selectedIndex="1"> + <tabs id="tabs"> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab2"/> + <tab id="tab3" label="tab3"/> + <tab id="tab4" label="tab4"/> + </tabs> + <tabpanels> + <tabpanel><!-- tabpanel First elements go here --></tabpanel> + <tabpanel><button id="b1" label="b1"/></tabpanel> + <tabpanel><button id="b2" label="b2"/></tabpanel> + <tabpanel></tabpanel> + </tabpanels> + </tabbox> + + <richlistbox id="listbox"> + <richlistitem id="lb1_item1"><label value="item1"/></richlistitem> + <richlistitem id="lb1_item2"><label value="item2"/></richlistitem> + </richlistbox> + + <richlistbox id="listbox2" seltype="multiple"> + <richlistitem id="lb2_item1"><label value="item1"/></richlistitem> + <richlistitem id="lb2_item2"><label value="item2"/></richlistitem> + <richlistitem id="lb2_item3"><label value="item3"/></richlistitem> + <richlistitem id="lb2_item4"><label value="item4"/></richlistitem> + <richlistitem id="lb2_item5"><label value="item5"/></richlistitem> + <richlistitem id="lb2_item6"><label value="item6"/></richlistitem> + <richlistitem id="lb2_item7"><label value="item7"/></richlistitem> + </richlistbox> + + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_selection_aria.html b/accessible/tests/mochitest/events/test_selection_aria.html new file mode 100644 index 0000000000..c479868e03 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection_aria.html @@ -0,0 +1,122 @@ +<html> + +<head> + <title>ARIA selection event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function selectItem(aSelectID, aItemID) { + this.selectNode = getNode(aSelectID); + this.itemNode = getNode(aItemID); + + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION, aItemID), + ]; + + this.invoke = function selectItem_invoke() { + var itemNode = this.selectNode.querySelector("*[aria-selected='true']"); + if (itemNode) + itemNode.removeAttribute("aria-selected"); + + this.itemNode.setAttribute("aria-selected", "true"); + }; + + this.getID = function selectItem_getID() { + return "select item " + prettyName(aItemID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + + // gA11yEventDumpToConsole = true; // debug stuff + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new selectItem("tablist", "tab1")); + gQueue.push(new selectItem("tablist", "tab2")); + + gQueue.push(new selectItem("tree", "treeitem1")); + gQueue.push(new selectItem("tree", "treeitem1a")); + gQueue.push(new selectItem("tree", "treeitem1a1")); + + gQueue.push(new selectItem("tree2", "tree2item1")); + gQueue.push(new selectItem("tree2", "tree2item1a")); + gQueue.push(new selectItem("tree2", "tree2item1a1")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653" + title="Make selection events async"> + Mozilla Bug 569653 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=804040" + title="Selection event not fired when selection of ARIA tab changes"> + Mozilla Bug 804040 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="tablist" id="tablist"> + <div role="tab" id="tab1">tab1</div> + <div role="tab" id="tab2">tab2</div> + </div> + + <div id="tree" role="tree"> + <div id="treeitem1" role="treeitem">Canada + <div id="treeitem1a" role="treeitem">- Ontario + <div id="treeitem1a1" role="treeitem">-- Toronto</div> + </div> + <div id="treeitem1b" role="treeitem">- Manitoba</div> + </div> + <div id="treeitem2" role="treeitem">Germany</div> + <div id="treeitem3" role="treeitem">Russia</div> + </div> + + <div id="tree2" role="tree" aria-multiselectable="true"> + <div id="tree2item1" role="treeitem">Canada + <div id="tree2item1a" role="treeitem">- Ontario + <div id="tree2item1a1" role="treeitem">-- Toronto</div> + </div> + <div id="tree2item1b" role="treeitem">- Manitoba</div> + </div> + <div id="tree2item2" role="treeitem">Germany</div> + <div id="tree2item3" role="treeitem">Russia</div> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_statechange.html b/accessible/tests/mochitest/events/test_statechange.html new file mode 100644 index 0000000000..0642851408 --- /dev/null +++ b/accessible/tests/mochitest/events/test_statechange.html @@ -0,0 +1,565 @@ +<html> + +<head> + <title>Accessible state change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + async function openNode(aIDDetails, aIDSummary, aIsOpen) { + let p = waitForStateChange(aIDSummary, STATE_EXPANDED, aIsOpen, false); + if (aIsOpen) { + getNode(aIDDetails).setAttribute("open", ""); + } else { + getNode(aIDDetails).removeAttribute("open"); + } + await p; + } + + async function makeEditableDoc(aDocNode, aIsEnabled) { + let p = waitForStateChange(aDocNode, EXT_STATE_EDITABLE, true, true); + aDocNode.designMode = "on"; + await p; + } + + async function invalidInput(aNodeOrID) { + let p = waitForStateChange(aNodeOrID, STATE_INVALID, true, false); + getNode(aNodeOrID).value = "I am not an email"; + await p; + } + + async function changeCheckInput(aID, aIsChecked) { + let p = waitForStateChange(aID, STATE_CHECKED, aIsChecked, false); + getNode(aID).checked = aIsChecked; + await p; + } + + async function changeRequiredState(aID, aIsRequired) { + let p = waitForStateChange(aID, STATE_REQUIRED, aIsRequired, false); + getNode(aID).required = aIsRequired; + await p; + } + + async function stateChangeOnFileInput(aID, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let fileControlNode = getNode(aID); + let browseButton = getAccessible(fileControlNode); + let p = waitForStateChange( + browseButton, aState, aIsEnabled, aIsExtraState + ); + fileControlNode.setAttribute(aAttr, aValue); + await p; + } + + function toggleSentinel() { + let sentinel = getNode("sentinel"); + if (sentinel.hasAttribute("aria-busy")) { + sentinel.removeAttribute("aria-busy"); + } else { + sentinel.setAttribute("aria-busy", "true"); + } + } + + async function toggleStateChange(aID, aAttr, aState, aIsExtraState) { + let p = waitForEvents([ + stateChangeEventArgs(aID, aState, true, aIsExtraState), + [EVENT_STATE_CHANGE, "sentinel"] + ]); + getNode(aID).setAttribute(aAttr, "true"); + toggleSentinel(); + await p; + p = waitForEvents([ + stateChangeEventArgs(aID, aState, false, aIsExtraState), + [EVENT_STATE_CHANGE, "sentinel"] + ]); + getNode(aID).setAttribute(aAttr, "false"); + toggleSentinel(); + await p; + } + + async function dupeStateChange(aID, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let p = waitForEvents([ + stateChangeEventArgs(aID, aState, aIsEnabled, aIsExtraState), + [EVENT_STATE_CHANGE, "sentinel"] + ]); + getNode(aID).setAttribute(aAttr, aValue); + getNode(aID).setAttribute(aAttr, aValue); + toggleSentinel(); + await p; + } + + async function oppositeStateChange(aID, aAttr, aState, aIsExtraState) { + let p = waitForEvents({ + expected: [[EVENT_STATE_CHANGE, "sentinel"]], + unexpected: [ + stateChangeEventArgs(aID, aState, false, aIsExtraState), + stateChangeEventArgs(aID, aState, true, aIsExtraState) + ] + }); + getNode(aID).setAttribute(aAttr, "false"); + getNode(aID).setAttribute(aAttr, "true"); + toggleSentinel(); + await p; + } + + /** + * Change concomitant ARIA and native attribute at once. + */ + async function echoingStateChange(aID, aARIAAttr, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let p = waitForStateChange(aID, aState, aIsEnabled, aIsExtraState); + if (aValue == null) { + getNode(aID).removeAttribute(aARIAAttr); + getNode(aID).removeAttribute(aAttr); + } else { + getNode(aID).setAttribute(aARIAAttr, aValue); + getNode(aID).setAttribute(aAttr, aValue); + } + await p; + } + + async function testHasPopup() { + let p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false); + getNode("popupButton").setAttribute("aria-haspopup", "true"); + await p; + + p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false); + getNode("popupButton").setAttribute("aria-haspopup", "false"); + await p; + + p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false); + getNode("popupButton").setAttribute("aria-haspopup", "true"); + await p; + + p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false); + getNode("popupButton").removeAttribute("aria-haspopup"); + await p; + } + + async function testDefaultSubmitChange() { + testStates("default-button", + STATE_DEFAULT, 0, + 0, 0, + "button should have DEFAULT state"); + let button = document.createElement("button"); + button.textContent = "new default"; + let p = waitForStateChange("default-button", STATE_DEFAULT, false, false); + getNode("default-button").before(button); + await p; + testStates("default-button", + 0, 0, + STATE_DEFAULT, 0, + "button should not have DEFAULT state"); + p = waitForStateChange("default-button", STATE_DEFAULT, true, false); + button.remove(); + await p; + testStates("default-button", + STATE_DEFAULT, 0, + 0, 0, + "button should have DEFAULT state"); + } + + async function testReadOnly() { + let p = waitForStateChange("email", STATE_READONLY, true, false); + getNode("email").setAttribute("readonly", "true"); + await p; + p = waitForStateChange("email", STATE_READONLY, false, false); + getNode("email").removeAttribute("readonly"); + await p; + } + + async function testReadonlyUntilEditable() { + testStates("article", + STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE, + "article is READONLY and not EDITABLE"); + let p = waitForEvents([ + stateChangeEventArgs("article", STATE_READONLY, false, false), + stateChangeEventArgs("article", EXT_STATE_EDITABLE, true, true)]); + getNode("article").contentEditable = "true"; + await p; + testStates("article", + 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0, + "article is EDITABLE and not READONLY"); + p = waitForEvents([ + stateChangeEventArgs("article", STATE_READONLY, true, false), + stateChangeEventArgs("article", EXT_STATE_EDITABLE, false, true)]); + getNode("article").contentEditable = "false"; + await p; + testStates("article", + STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE, + "article is READONLY and not EDITABLE"); + } + + async function testAnimatedImage() { + testStates("animated-image", + STATE_ANIMATED, 0, + 0, 0, + "image should be animated 1"); + let p = waitForStateChange("animated-image", STATE_ANIMATED, false, false); + getNode("animated-image").src = "../animated-gif-finalframe.gif"; + await p; + testStates("animated-image", + 0, 0, + STATE_ANIMATED, 0, + "image should not be animated 2"); + p = waitForStateChange("animated-image", STATE_ANIMATED, true, false); + getNode("animated-image").src = "../animated-gif.gif"; + await p; + testStates("animated-image", + STATE_ANIMATED, 0, + 0, 0, + "image should be animated 3"); + } + + async function testImageLoad() { + let img = document.createElement("img"); + img.id = "image"; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + img.src = "http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs"; + let p = waitForEvent(EVENT_SHOW, "image"); + getNode("eventdump").before(img); + await p; + testStates("image", + STATE_INVISIBLE, 0, + 0, 0, + "image should be invisible"); + p = waitForStateChange("image", STATE_INVISIBLE, false, false); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await fetch("http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete"); + await p; + testStates("image", + 0, 0, + STATE_INVISIBLE, 0, + "image should be invisible"); + } + + async function testMultiSelectable(aID, aAttribute) { + testStates(aID, + 0, 0, + STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, + `${aID} should not be multiselectable`); + let p = waitForEvents([ + stateChangeEventArgs(aID, STATE_MULTISELECTABLE, true, false), + stateChangeEventArgs(aID, STATE_EXTSELECTABLE, true, false), + ]); + getNode(aID).setAttribute(aAttribute, true); + await p; + testStates(aID, + STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, + 0, 0, + `${aID} should not be multiselectable`); + p = waitForEvents([ + stateChangeEventArgs(aID, STATE_MULTISELECTABLE, false, false), + stateChangeEventArgs(aID, STATE_EXTSELECTABLE, false, false), + ]); + getNode(aID).removeAttribute(aAttribute); + await p; + testStates(aID, + 0, 0, + STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, + `${aID} should not be multiselectable`); + } + + async function testAutocomplete() { + // A text input will have autocomplete via browser's form autofill... + testStates("input", + 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, + 0, 0, + "input supports autocompletion"); + // unless it is explicitly turned off. + testStates("input-autocomplete-off", + 0, 0, + 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, + "input-autocomplete-off does not support autocompletion"); + // An input with a datalist will always have autocomplete. + testStates("input-list", + 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, + 0, 0, + "input-list supports autocompletion"); + // password fields don't get autocomplete. + testStates("input-password", + 0, 0, + 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, + "input-autocomplete-off does not support autocompletion"); + + let p = waitForEvents({ + expected: [ + // Setting the form's autocomplete attribute to "off" will cause + // "input" to lost its autocomplete state. + stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true) + ], + unexpected: [ + // "input-list" should preserve its autocomplete state regardless of + // forms "autocomplete" attribute + [EVENT_STATE_CHANGE, "input-list"], + // "input-autocomplete-off" already has its autocomplte off, so no state + // change here. + [EVENT_STATE_CHANGE, "input-autocomplete-off"], + // passwords never get autocomplete + [EVENT_STATE_CHANGE, "input-password"], + ] + }); + + getNode("form").setAttribute("autocomplete", "off"); + + await p; + + // Same when we remove the form's autocomplete attribute. + p = waitForEvents({ + expected: [stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true)], + unexpected: [ + [EVENT_STATE_CHANGE, "input-list"], + [EVENT_STATE_CHANGE, "input-autocomplete-off"], + [EVENT_STATE_CHANGE, "input-password"], + ] + }); + + getNode("form").removeAttribute("autocomplete"); + + await p; + + p = waitForEvents({ + expected: [ + // Forcing autocomplete off on an input will cause a state change + stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true), + // Associating a datalist with an autocomplete=off input + // will give it an autocomplete state, regardless. + stateChangeEventArgs("input-autocomplete-off", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true), + // XXX: datalist inputs also get a HASPOPUP state, the inconsistent + // use of that state is inexplicable, but lets make sure we fire state + // change events for it anyway. + stateChangeEventArgs("input-autocomplete-off", STATE_HASPOPUP, true, false), + ], + unexpected: [ + // Forcing autocomplete off with a dataset input does nothing. + [EVENT_STATE_CHANGE, "input-list"], + // passwords never get autocomplete + [EVENT_STATE_CHANGE, "input-password"], + ] + }); + + getNode("input").setAttribute("autocomplete", "off"); + getNode("input-list").setAttribute("autocomplete", "off"); + getNode("input-autocomplete-off").setAttribute("list", "browsers"); + getNode("input-password").setAttribute("autocomplete", "off"); + + await p; + } + + async function doTests() { + // Disable mixed-content upgrading as this test is expecting an HTTP load + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.upgrade_display_content", false]] + }); + + // Test opening details objects + await openNode("detailsOpen", "summaryOpen", true); + await openNode("detailsOpen", "summaryOpen", false); + await openNode("detailsOpen1", "summaryOpen1", true); + await openNode("detailsOpen2", "summaryOpen2", true); + await openNode("detailsOpen3", "summaryOpen3", true); + await openNode("detailsOpen4", "summaryOpen4", true); + await openNode("detailsOpen5", "summaryOpen5", true); + await openNode("detailsOpen6", "summaryOpen6", true); + + // Test delayed editable state change + var doc = document.getElementById("iframe").contentDocument; + await makeEditableDoc(doc); + + // invalid state change + await invalidInput("email"); + + // checked state change + await changeCheckInput("checkbox", true); + await changeCheckInput("checkbox", false); + await changeCheckInput("radio", true); + await changeCheckInput("radio", false); + + // required state change + await changeRequiredState("checkbox", true); + + // file input inherited state changes + await stateChangeOnFileInput("file", "aria-busy", "true", + STATE_BUSY, false, true); + await stateChangeOnFileInput("file", "aria-required", "true", + STATE_REQUIRED, false, true); + await stateChangeOnFileInput("file", "aria-invalid", "true", + STATE_INVALID, false, true); + + await dupeStateChange("div", "aria-busy", "true", + STATE_BUSY, false, true); + await oppositeStateChange("div", "aria-busy", + STATE_BUSY, false); + + await echoingStateChange("text1", "aria-disabled", "disabled", "true", + EXT_STATE_ENABLED, true, false); + await echoingStateChange("text1", "aria-disabled", "disabled", null, + EXT_STATE_ENABLED, true, true); + + await testReadOnly(); + + await testReadonlyUntilEditable(); + + await testHasPopup(); + + await toggleStateChange("textbox", "aria-multiline", EXT_STATE_MULTI_LINE, true); + + await testDefaultSubmitChange(); + + await testAnimatedImage(); + + await testImageLoad(); + + await testMultiSelectable("listbox", "aria-multiselectable"); + + await testMultiSelectable("select", "multiple"); + + await testAutocomplete(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<style> + details.openBefore::before{ + content: "before detail content: "; + background: blue; + } + summary.openBefore::before{ + content: "before summary content: "; + background: green; + } + details.openAfter::after{ + content: " :after detail content"; + background: blue; + } + summary.openAfter::after{ + content: " :after summary content"; + background: green; + } +</style> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471" + title="Make state change events async"> + Bug 564471 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728" + title="Fire a11y event based on HTML5 constraint validation"> + Bug 555728 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" + title="File input control should be propogate states to descendants"> + Bug 699017 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389" + title="Fire statechange event whenever checked state is changed not depending on focused state"> + Bug 788389 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=926812" + title="State change event not fired when both disabled and aria-disabled are toggled"> + Bug 926812 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- open --> + <details id="detailsOpen"><summary id="summaryOpen">open</summary>details can be opened</details> + <details id="detailsOpen1">order doesn't matter<summary id="summaryOpen1">open</summary></details> + <details id="detailsOpen2"><div>additional elements don't matter</div><summary id="summaryOpen2">open</summary></details> + <details id="detailsOpen3" class="openBefore"><summary id="summaryOpen3">summary</summary>content</details> + <details id="detailsOpen4" class="openAfter"><summary id="summaryOpen4">summary</summary>content</details> + <details id="detailsOpen5"><summary id="summaryOpen5" class="openBefore">summary</summary>content</details> + <details id="detailsOpen6"><summary id="summaryOpen6" class="openAfter">summary</summary>content</details> + + + <div id="testContainer"> + <iframe id="iframe"></iframe> + </div> + + <input id="email" type='email'> + + <input id="checkbox" type="checkbox"> + <input id="radio" type="radio"> + + <input id="file" type="file"> + + <div id="div"></div> + + <!-- A sentinal guards from events of interest being fired after it emits a state change --> + <div id="sentinel"></div> + + <input id="text1"> + + <div id="textbox" role="textbox" aria-multiline="false">hello</div> + + <form id="form"> + <button id="default-button">hello</button> + <button>world</button> + <input id="input"> + <input id="input-autocomplete-off" autocomplete="off"> + <input id="input-list" list="browsers"> + <input id="input-password" type="password"> + <datalist id="browsers"> + <option value="Internet Explorer"> + <option value="Firefox"> + <option value="Google Chrome"> + <option value="Opera"> + <option value="Safari"> + </datalist> + </form> + + <div id="article" role="article">hello</div> + + <img id="animated-image" src="../animated-gif.gif"> + + <ul id="listbox" role="listbox"> + <li role="option">one</li> + <li role="option">two</li> + <li role="option">three</li> + <li role="option">four</li> + <li role="option">five</li> + </ul> + + <select id="select" size="2"> + <option>one</option> + <option>two</option> + <option>three</option> + <option>four</option> + <option>five</option> + <option>size</option> + </select> + + <div id="eventdump"></div> + + <div id="eventdump"></div> + <button id="popupButton">action</button> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_statechange.xhtml b/accessible/tests/mochitest/events/test_statechange.xhtml new file mode 100644 index 0000000000..4d63c664f1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_statechange.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL state change event tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../promisified-events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function offscreenChangeEvent(acc, enabled) { + return [ + EVENT_STATE_CHANGE, + event => { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + return event.accessible == acc && + scEvent.state == STATE_OFFSCREEN && + scEvent.isEnabled == enabled; + } + ]; + } + + async function testTabpanels() { + const tabs = getNode("tabs"); + is(tabs.selectedIndex, 0, "tab1 initially selected"); + const panel1 = getAccessible("panel1"); + testStates(panel1, 0, 0, STATE_OFFSCREEN); + const panel2 = getAccessible("panel2"); + testStates(panel2, STATE_OFFSCREEN); + const panel3 = getAccessible("panel3"); + testStates(panel3, STATE_OFFSCREEN); + + let events = waitForEvents([ + offscreenChangeEvent(panel1, true), + offscreenChangeEvent(panel2, false) + ]); + info("Selecting tab2"); + tabs.selectedIndex = 1; + await events; + + events = waitForEvents([ + offscreenChangeEvent(panel2, true), + offscreenChangeEvent(panel3, false) + ]); + info("Selecting tab3"); + tabs.selectedIndex = 2; + await events; + + events = waitForEvents([ + offscreenChangeEvent(panel3, true), + offscreenChangeEvent(panel1, false) + ]); + info("Selecting tab1"); + tabs.selectedIndex = 0; + await events; + } + + async function testPressed() { + const toolbarbuttonCheckbox = getNode("toolbarbuttonCheckbox"); + testStates(toolbarbuttonCheckbox, 0, 0, STATE_PRESSED); + info("Checking toolbarbuttonCheckbox"); + let changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, true); + toolbarbuttonCheckbox.setAttribute("checked", true); + await changed; + info("Unchecking toolbarbuttonCheckbox"); + changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, false); + toolbarbuttonCheckbox.removeAttribute("checked"); + await changed; + } + + async function doTests() { + await testTabpanels(); + await testPressed(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <tabbox id="tabbox" selectedIndex="0"> + <tabs id="tabs"> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab2"/> + <tab id="tab3" label="tab3"/> + </tabs> + <tabpanels> + <hbox id="panel1"><button label="b1"/></hbox> + <hbox id="panel2"><button label="b2"/></hbox> + <hbox id="panel3"><button label="b3"/></hbox> + </tabpanels> + </tabbox> + + <toolbarbutton id="toolbarbuttonCheckbox" type="checkbox">toolbarbuttonCheckbox</toolbarbutton> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_text.html b/accessible/tests/mochitest/events/test_text.html new file mode 100644 index 0000000000..8a0bd7f9a4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_text.html @@ -0,0 +1,310 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Base text remove invoker and checker. + */ + function textRemoveInvoker(aID, aStart, aEnd, aText) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false), + ]; + } + + function textInsertInvoker(aID, aStart, aEnd, aText) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, true), + ]; + } + + /** + * Remove inaccessible child node containing accessibles. + */ + function removeChildSpan(aID) { + this.__proto__ = new textRemoveInvoker(aID, 0, 5, "33322"); + + this.invoke = function removeChildSpan_invoke() { + // remove HTML span, a first child of the node + this.DOMNode.firstChild.remove(); + }; + + this.getID = function removeChildSpan_getID() { + return "Remove inaccessible span containing accessible nodes" + prettyName(aID); + }; + } + + /** + * Insert inaccessible child node containing accessibles. + */ + function insertChildSpan(aID, aInsertAllTogether) { + this.__proto__ = new textInsertInvoker(aID, 0, 5, "33322"); + + this.invoke = function insertChildSpan_invoke() { + // <span><span>333</span><span>22</span></span> + if (aInsertAllTogether) { + let topSpan = document.createElement("span"); + let fSpan = document.createElement("span"); + fSpan.textContent = "333"; + topSpan.appendChild(fSpan); + let sSpan = document.createElement("span"); + sSpan.textContent = "22"; + topSpan.appendChild(sSpan); + + this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]); + } else { + let topSpan = document.createElement("span"); + this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]); + + let fSpan = document.createElement("span"); + fSpan.textContent = "333"; + topSpan.appendChild(fSpan); + + let sSpan = document.createElement("span"); + sSpan.textContent = "22"; + topSpan.appendChild(sSpan); + } + }; + + this.getID = function insertChildSpan_getID() { + return "Insert inaccessible span containing accessibles" + + prettyName(aID); + }; + } + + /** + * Remove child embedded accessible. + */ + function removeChildDiv(aID) { + this.__proto__ = new textRemoveInvoker(aID, 5, 6, kEmbedChar); + + this.invoke = function removeChildDiv_invoke() { + var childDiv = this.DOMNode.childNodes[1]; + + // Ensure accessible is created to get text remove event when it's + // removed. + getAccessible(childDiv); + + this.DOMNode.removeChild(childDiv); + }; + + this.getID = function removeChildDiv_getID() { + return "Remove accessible div from the middle of text accessible " + + prettyName(aID); + }; + } + + /** + * Insert child embedded accessible. + */ + function insertChildDiv(aID) { + this.__proto__ = new textInsertInvoker(aID, 5, 6, kEmbedChar); + + this.invoke = function insertChildDiv_invoke() { + var childDiv = document.createElement("div"); + // Note after bug 646216, a sole div without text won't be accessible + // and would not result in an embedded character. + // Therefore, add some text. + childDiv.textContent = "hello"; + this.DOMNode.insertBefore(childDiv, this.DOMNode.childNodes[1]); + }; + + this.getID = function insertChildDiv_getID() { + return "Insert accessible div into the middle of text accessible " + + prettyName(aID); + }; + } + + /** + * Remove children from text container from first to last child or vice + * versa. + */ + function removeChildren(aID, aLastToFirst, aStart, aEnd, aText) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.invoke = function removeChildren_invoke() { + if (aLastToFirst) { + while (this.DOMNode.firstChild) + this.DOMNode.removeChild(this.DOMNode.lastChild); + } else { + while (this.DOMNode.firstChild) + this.DOMNode.firstChild.remove(); + } + }; + + this.getID = function removeChildren_getID() { + return "remove children of " + prettyName(aID) + + (aLastToFirst ? " from last to first" : " from first to last"); + }; + } + + /** + * Remove text from HTML input. + */ + function removeTextFromInput(aID, aStart, aEnd, aText) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode)); + + this.invoke = function removeTextFromInput_invoke() { + this.DOMNode.focus(); + this.DOMNode.setSelectionRange(aStart, aEnd); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromInput_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + /** + * Add text into HTML input. + */ + function insertTextIntoInput(aID, aStart, aEnd, aText) { + this.__proto__ = new textInsertInvoker(aID, aStart, aEnd, aText); + + this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode)); + + this.invoke = function insertTextIntoInput_invoke() { + this.DOMNode.focus(); + sendString("a"); + }; + + this.getID = function insertTextIntoInput_getID() { + return "Insert text to " + aStart + " for " + prettyName(aID); + }; + } + + /** + * Remove text data from text node of editable area. + */ + function removeTextFromEditable(aID, aStart, aEnd, aText, aTextNode) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.invoke = function removeTextFromEditable_invoke() { + this.DOMNode.focus(); + + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(this.textNode, aStart); + range.setEnd(this.textNode, aEnd); + selection.addRange(range); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromEditable_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + + this.textNode = getNode(aTextNode); + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + // Text remove event on inaccessible child HTML span removal containing + // accessible text nodes. + gQueue.push(new removeChildSpan("p")); + gQueue.push(new insertChildSpan("p"), true); + gQueue.push(new insertChildSpan("p"), false); + + // Remove embedded character. + gQueue.push(new removeChildDiv("div")); + gQueue.push(new insertChildDiv("div")); + + // Remove all children. + var text = kEmbedChar + "txt" + kEmbedChar; + gQueue.push(new removeChildren("div2", true, 0, 5, text)); + gQueue.push(new removeChildren("div3", false, 0, 5, text)); + + // Text remove from text node within hypertext accessible. + gQueue.push(new removeTextFromInput("input", 1, 3, "al")); + gQueue.push(new insertTextIntoInput("input", 1, 2, "a")); + + // bug 570691 + todo(false, "Fix text change events from editable area, see bug 570691"); + // var textNode = getNode("editable").firstChild; + // gQueue.push(new removeTextFromEditable("editable", 1, 3, "al", textNode)); + // textNode = getNode("editable2").firstChild.firstChild; + // gQueue.push(new removeTextFromEditable("editable2", 1, 3, "al", textNode)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566293" + title=" wrong length of text remove event when inaccessible node containing accessible nodes is removed"> + Mozilla Bug 566293 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570710" + title="Avoid extra array traversal during text event creation"> + Mozilla Bug 570710 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=574003" + title="Coalesce text events on nodes removal"> + Mozilla Bug 574003 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=575052" + title="Cache text offsets within hypertext accessible"> + Mozilla Bug 575052 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275" + title="Rework accessible tree update code"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p"><span><span>333</span><span>22</span></span>1111</p> + <div id="div">hello<div>hello</div>hello</div> + <div id="div2"><div>txt</div>txt<div>txt</div></div> + <div id="div3"><div>txt</div>txt<div>txt</div></div> + <input id="input" value="value"> + <div contentEditable="true" id="editable">value</div> + <div contentEditable="true" id="editable2"><span>value</span></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_text_alg.html b/accessible/tests/mochitest/events/test_text_alg.html new file mode 100644 index 0000000000..f9b55c8b23 --- /dev/null +++ b/accessible/tests/mochitest/events/test_text_alg.html @@ -0,0 +1,249 @@ +<html> + +<head> + <title>Accessible text update algorithm testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + const kRemoval = false; + const kInsertion = true; + const kUnexpected = true; + + function changeText(aContainerID, aValue, aEventList) { + this.containerNode = getNode(aContainerID); + this.textNode = this.containerNode.firstChild; + this.textData = this.textNode.data; + + this.eventSeq = [ ]; + this.unexpectedEventSeq = [ ]; + + for (var i = 0; i < aEventList.length; i++) { + var event = aEventList[i]; + + var isInserted = event[0]; + var str = event[1]; + var offset = event[2]; + var checker = new textChangeChecker(this.containerNode, offset, + offset + str.length, str, + isInserted); + + if (event[3] == kUnexpected) + this.unexpectedEventSeq.push(checker); + else + this.eventSeq.push(checker); + } + + this.invoke = function changeText_invoke() { + this.textNode.data = aValue; + }; + + this.getID = function changeText_getID() { + return "change text '" + shortenString(this.textData) + "' -> '" + + shortenString(this.textNode.data) + "' for " + + prettyName(this.containerNode); + }; + } + + function expStr(x, doublings) { + for (var i = 0; i < doublings; ++i) + x = x + x; + return x; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + // //////////////////////////////////////////////////////////////////////// + // wqrema -> tqb: substitution coalesced with removal + + var events = [ + [ kRemoval, "w", 0 ], // wqrema -> qrema + [ kInsertion, "t", 0], // qrema -> tqrema + [ kRemoval, "rema", 2 ], // tqrema -> tq + [ kInsertion, "b", 2], // tq -> tqb + ]; + gQueue.push(new changeText("p1", "tqb", events)); + + // //////////////////////////////////////////////////////////////////////// + // b -> insa: substitution coalesced with insertion (complex substitution) + + events = [ + [ kRemoval, "b", 0 ], // b -> + [ kInsertion, "insa", 0], // -> insa + ]; + gQueue.push(new changeText("p2", "insa", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> def: coalesced substitutions + + events = [ + [ kRemoval, "abc", 0 ], // abc -> + [ kInsertion, "def", 0], // -> def + ]; + gQueue.push(new changeText("p3", "def", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcabc -> abcDEFabc: coalesced insertions + + events = [ + [ kInsertion, "DEF", 3], // abcabc -> abcDEFabc + ]; + gQueue.push(new changeText("p4", "abcDEFabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> defabc: insertion into begin + + events = [ + [ kInsertion, "def", 0], // abc -> defabc + ]; + gQueue.push(new changeText("p5", "defabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> abcdef: insertion into end + + events = [ + [ kInsertion, "def", 3], // abc -> abcdef + ]; + gQueue.push(new changeText("p6", "abcdef", events)); + + // //////////////////////////////////////////////////////////////////////// + // defabc -> abc: removal from begin + + events = [ + [ kRemoval, "def", 0], // defabc -> abc + ]; + gQueue.push(new changeText("p7", "abc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcdef -> abc: removal from the end + + events = [ + [ kRemoval, "def", 3], // abcdef -> abc + ]; + gQueue.push(new changeText("p8", "abc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcDEFabc -> abcabc: coalesced removals + + events = [ + [ kRemoval, "DEF", 3], // abcDEFabc -> abcabc + ]; + gQueue.push(new changeText("p9", "abcabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // !abcdef@ -> @axbcef!: insertion, deletion and substitutions + + events = [ + [ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@ + [ kInsertion, "@", 0], // abcdef@ -> @abcdef@ + [ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@ + [ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@ + [ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef + [ kInsertion, "!", 7 ], // @axbcef -> @axbcef! + ]; + gQueue.push(new changeText("p10", "@axbcef!", events)); + + // //////////////////////////////////////////////////////////////////////// + // meilenstein -> levenshtein: insertion, complex and simple substitutions + + events = [ + [ kRemoval, "m", 0 ], // meilenstein -> eilenstein + [ kInsertion, "l", 0], // eilenstein -> leilenstein + [ kRemoval, "il", 2 ], // leilenstein -> leenstein + [ kInsertion, "v", 2], // leenstein -> levenstein + [ kInsertion, "h", 6 ], // levenstein -> levenshtein + ]; + gQueue.push(new changeText("p11", "levenshtein", events)); + + // //////////////////////////////////////////////////////////////////////// + // long strings, remove/insert pair as the old string was replaced on + // new one + + var longStr1 = expStr("x", 16); + var longStr2 = expStr("X", 16); + + var newStr = "a" + longStr1 + "b", insStr = longStr1, rmStr = ""; + events = [ + [ kRemoval, rmStr, 1, kUnexpected ], + [ kInsertion, insStr, 1 ], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + newStr = "a" + longStr2 + "b"; + insStr = longStr2; + rmStr = longStr1; + events = [ + [ kRemoval, rmStr, 1 ], + [ kInsertion, insStr, 1], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + newStr = "ab"; + insStr = ""; + rmStr = longStr2; + events = [ + [ kRemoval, rmStr, 1 ], + [ kInsertion, insStr, 1, kUnexpected ], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660" + title="Cache rendered text on a11y side"> + Mozilla Bug 626660 + </a> + <br> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- Note: only editable text gets diffed this way. --> + <div contenteditable="true"> + <p id="p1">wqrema</p> + <p id="p2">b</p> + <p id="p3">abc</p> + <p id="p4">abcabc</p> + <p id="p5">abc</p> + <p id="p6">abc</p> + <p id="p7">defabc</p> + <p id="p8">abcdef</p> + <p id="p9">abcDEFabc</p> + <p id="p10">!abcdef@</p> + <p id="p11">meilenstein</p> + <p id="p12">ab</p> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_textattrchange.html b/accessible/tests/mochitest/events/test_textattrchange.html new file mode 100644 index 0000000000..b46e1ef399 --- /dev/null +++ b/accessible/tests/mochitest/events/test_textattrchange.html @@ -0,0 +1,107 @@ +<html> + +<head> + <title>Text attribute changed event for misspelled text</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + const {InlineSpellChecker} = ChromeUtils.importESModule( + "resource://gre/modules/InlineSpellChecker.sys.mjs" + ); + + function spelledTextInvoker(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_TEXT_ATTRIBUTE_CHANGED, this.DOMNode), + ]; + + this.invoke = function spelledTextInvoker_invoke() { + var editor = this.DOMNode.editor; + var spellChecker = new InlineSpellChecker(editor); + spellChecker.enabled = true; + + // var spellchecker = editor.getInlineSpellChecker(true); + // spellchecker.enableRealTimeSpell = true; + + this.DOMNode.value = "valid text inalid tixt"; + }; + + this.finalCheck = function spelledTextInvoker_finalCheck() { + var defAttrs = buildDefaultTextAttrs(this.DOMNode, kInputFontSize, + kNormalFontWeight, + kInputFontFamily); + testDefaultTextAttrs(aID, defAttrs); + + var attrs = { }; + var misspelledAttrs = { + "invalid": "spelling", + }; + + testTextAttrs(aID, 0, attrs, defAttrs, 0, 11); + testTextAttrs(aID, 11, misspelledAttrs, defAttrs, 11, 17); + testTextAttrs(aID, 17, attrs, defAttrs, 17, 18); + testTextAttrs(aID, 18, misspelledAttrs, defAttrs, 18, 22); + }; + + this.getID = function spelledTextInvoker_getID() { + return "text attribute change for misspelled text"; + }; + } + + /** + * Do tests. + */ + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + // Synth focus before spellchecking turning on to make sure editor + // gets a time for initialization. + + gQueue = new eventQueue(); + gQueue.push(new synthFocus("input")); + gQueue.push(new spelledTextInvoker("input")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759" + title="Implement text attributes"> + Mozilla Bug 345759 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input"/> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_textselchange.html b/accessible/tests/mochitest/events/test_textselchange.html new file mode 100644 index 0000000000..3dce0760eb --- /dev/null +++ b/accessible/tests/mochitest/events/test_textselchange.html @@ -0,0 +1,82 @@ +<html> + +<head> + <title>Accessible text selection change events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function getOnclickSeq(aID) { + return [ + new caretMoveChecker(0, true, aID), + new unexpectedInvokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID), + ]; + } + + function doTests() { + // test caret move events and caret offsets + gQueue = new eventQueue(); + + gQueue.push(new synthClick("c1_p1", getOnclickSeq("c1_p1"))); + gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 1, "c1_p1", 0, "c1_p2", 0), { shiftKey: true })); + gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 2, "c1_p1", 0, "c1_p2", 9), { shiftKey: true })); + + gQueue.push(new synthClick("ta1", getOnclickSeq("ta1"))); + gQueue.push(new synthRightKey("ta1", + new textSelectionChecker("ta1", 0, 1, "ta1", 0, "ta1", 1), + { shiftKey: true })); + gQueue.push(new synthLeftKey("ta1", + [new textSelectionChecker("ta1", 0, 0, "ta1", 0, "ta1", 0), + new caretMoveChecker(0, true, "ta1")])); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=762934" + title="Text selection change event has a wrong target when selection is spanned through several objects"> + Bug 762934 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=956032" + title="Text selection change event missed when selected text becomes unselected"> + Bug 956032 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1" contentEditable="true"> + <p id="c1_p1">paragraph</p> + <p id="c1_p2">paragraph</p> + </div> + + <textarea id="ta1">Hello world</textarea> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_tree.xhtml b/accessible/tests/mochitest/events/test_tree.xhtml new file mode 100644 index 0000000000..af7feafde8 --- /dev/null +++ b/accessible/tests/mochitest/events/test_tree.xhtml @@ -0,0 +1,358 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="DOM TreeRowCountChanged and a11y name change events."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + var gView; + + //////////////////////////////////////////////////////////////////////////// + // Invoker's checkers + + /** + * Check TreeRowCountChanged event. + */ + function rowCountChangedChecker(aMsg, aIdx, aCount) + { + this.type = "TreeRowCountChanged"; + this.target = gTree; + this.check = function check(aEvent) + { + var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2); + var index = propBag.getPropertyAsInt32("index"); + is(index, aIdx, "Wrong 'index' data of 'treeRowCountChanged' event."); + + var count = propBag.getPropertyAsInt32("count"); + is(count, aCount, "Wrong 'count' data of 'treeRowCountChanged' event."); + } + this.getID = function getID() + { + return aMsg + "TreeRowCountChanged"; + } + } + + /** + * Check TreeInvalidated event. + */ + function treeInvalidatedChecker(aMsg, aStartRow, aEndRow, aStartCol, aEndCol) + { + this.type = "TreeInvalidated"; + this.target = gTree; + this.check = function check(aEvent) + { + var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2); + try { + var startRow = propBag.getPropertyAsInt32("startrow"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + startRow = null; + } + is(startRow, aStartRow, + "Wrong 'startrow' of 'treeInvalidated' event on " + aMsg); + + try { + var endRow = propBag.getPropertyAsInt32("endrow"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + endRow = null; + } + is(endRow, aEndRow, + "Wrong 'endrow' of 'treeInvalidated' event on " + aMsg); + + try { + var startCol = propBag.getPropertyAsInt32("startcolumn"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + startCol = null; + } + is(startCol, aStartCol, + "Wrong 'startcolumn' of 'treeInvalidated' event on " + aMsg); + + try { + var endCol = propBag.getPropertyAsInt32("endcolumn"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + endCol = null; + } + is(endCol, aEndCol, + "Wrong 'endcolumn' of 'treeInvalidated' event on " + aMsg); + } + this.getID = function getID() + { + return "TreeInvalidated on " + aMsg; + } + } + + /** + * Check name changed a11y event. + */ + function nameChangeChecker(aMsg, aRow, aCol) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + var acc = getAccessible(gTree); + + var tableAcc = getAccessible(acc, [nsIAccessibleTable]); + return tableAcc.getCellAt(aRow, aCol); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + "name changed"; + } + } + + /** + * Check name changed a11y event for a row. + */ + function rowNameChangeChecker(aMsg, aRow) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + var acc = getAccessible(gTree); + return acc.getChildAt(aRow + 1); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + "name changed"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Set tree view. + */ + function setTreeView() + { + this.invoke = function setTreeView_invoke() + { + gTree.view = gView; + } + + this.getID = function setTreeView_getID() { return "set tree view"; } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, gTree) + ]; + }; + + /** + * Insert row at 0 index and checks TreeRowCountChanged and TreeInvalidated + * event. + */ + function insertRow() + { + this.invoke = function insertRow_invoke() + { + gView.appendItem("last"); + gTree.rowCountChanged(0, 1); + } + + this.eventSeq = + [ + new rowCountChangedChecker("insertRow: ", 0, 1), + new treeInvalidatedChecker("insertRow", 0, 5, null, null) + ]; + + this.getID = function insertRow_getID() + { + return "insert row"; + } + } + + /** + * Invalidates first column and checks six name changed events for each + * treeitem plus TreeInvalidated event. + */ + function invalidateColumn() + { + this.invoke = function invalidateColumn_invoke() + { + // Make sure accessible subtree of XUL tree is created otherwise no + // name change events for cell accessibles are emitted. + var tree = getAccessible(gTree); + var child = tree.firstChild; + var walkDown = true; + while (child != tree) { + if (walkDown) { + var grandChild = child.firstChild; + if (grandChild) { + child = grandChild; + continue; + } + } + + var sibling = child.nextSibling; + if (sibling) { + child = sibling; + walkDown = true; + continue; + } + + child = child.parent; + walkDown = false; + } + + // Fire 'TreeInvalidated' event by InvalidateColumn() + var firstCol = gTree.columns.getFirstColumn(); + for (var i = 0; i < gView.rowCount; i++) + gView.setCellText(i, firstCol, "hey " + String(i) + "x0"); + + gTree.invalidateColumn(firstCol); + } + + this.eventSeq = + [ + new nameChangeChecker("invalidateColumn: ", 0, 0), + new nameChangeChecker("invalidateColumn: ", 1, 0), + new nameChangeChecker("invalidateColumn: ", 2, 0), + new nameChangeChecker("invalidateColumn: ", 3, 0), + new nameChangeChecker("invalidateColumn: ", 4, 0), + new nameChangeChecker("invalidateColumn: ", 5, 0), + new treeInvalidatedChecker("invalidateColumn", null, null, 0, 0) + ]; + + this.getID = function invalidateColumn_getID() + { + return "invalidate column"; + } + } + + /** + * Invalidates second row and checks name changed event for first treeitem + * (note, there are two name changed events on linux due to different + * accessible tree for xul:tree element) plus TreeInvalidated event. + */ + function invalidateRow() + { + this.invoke = function invalidateRow_invoke() + { + // Fire 'TreeInvalidated' event by InvalidateRow() + // eslint-disable-next-line no-unused-vars + var colCount = gTree.columns.count; + var column = gTree.columns.getFirstColumn(); + while (column) { + gView.setCellText(1, column, "aloha 1x" + String(column.index)); + column = column.getNext(); + } + + gTree.invalidateRow(1); + } + + this.eventSeq = + [ + new nameChangeChecker("invalidateRow: ", 1, 0), + new nameChangeChecker("invalidateRow: ", 1, 1), + new rowNameChangeChecker("invalidateRow: ", 1), + new treeInvalidatedChecker("invalidateRow", 1, 1, null, null) + ]; + + this.getID = function invalidateRow_getID() + { + return "invalidate row"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + var gTree = null; + var gTreeView = null; + var gQueue = null; + + // gA11yEventDumpID = "debug"; + gA11yEventDumpToConsole = true; // debuggin + + function doTest() + { + // Initialize the tree + gTree = document.getElementById("tree"); + gView = new nsTableTreeView(5); + + // Perform actions + gQueue = new eventQueue(); + + gQueue.push(new setTreeView()); + gQueue.push(new insertRow()); + gQueue.push(new invalidateColumn()); + gQueue.push(new invalidateRow()); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=368835" + title="Fire TreeViewChanged/TreeRowCountChanged events."> + Mozilla Bug 368835 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=308564" + title="No accessibility events when data in a tree row changes."> + Mozilla Bug 308564 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=739524" + title="replace TreeViewChanged DOM event on direct call from XUL tree."> + Mozilla Bug 739524 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=743568" + title="Thunderbird message list tree emitting incorrect focus signals after message deleted."> + Mozilla Bug 743568 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/events/test_valuechange.html b/accessible/tests/mochitest/events/test_valuechange.html new file mode 100644 index 0000000000..1ad3c0359d --- /dev/null +++ b/accessible/tests/mochitest/events/test_valuechange.html @@ -0,0 +1,315 @@ +<html> + +<head> + <title>Accessible value change events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript" + src="../value.js"></script> + + <script type="application/javascript"> + /** + * Do tests. + */ + var gQueue = null; + + // Value change invoker + function changeARIAValue(aNodeOrID, aValuenow, aValuetext) { + this.DOMNode = getNode(aNodeOrID); + this.eventSeq = [ new invokerChecker(aValuetext ? + EVENT_TEXT_VALUE_CHANGE : + EVENT_VALUE_CHANGE, this.DOMNode), + ]; + + this.invoke = function changeARIAValue_invoke() { + // Note: this should not fire an EVENT_VALUE_CHANGE when aria-valuetext + // is not empty + if (aValuenow != undefined) + this.DOMNode.setAttribute("aria-valuenow", aValuenow); + + // Note: this should always fire an EVENT_VALUE_CHANGE + if (aValuetext != undefined) + this.DOMNode.setAttribute("aria-valuetext", aValuetext); + }; + + this.check = function changeARIAValue_check() { + var acc = getAccessible(aNodeOrID, [nsIAccessibleValue]); + if (!acc) + return; + + // Note: always test against valuetext first because the existence of + // aria-valuetext takes precedence over aria-valuenow in gecko. + is(acc.value, (aValuetext != undefined) ? aValuetext : aValuenow, + "Wrong value of " + prettyName(aNodeOrID)); + }; + + this.getID = function changeARIAValue_getID() { + return prettyName(aNodeOrID) + " value changed"; + }; + } + + function changeValue(aID, aValue) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode), + ]; + + this.invoke = function changeValue_invoke() { + this.DOMNode.value = aValue; + }; + + this.check = function changeValue_check() { + var acc = getAccessible(this.DOMNode); + is(acc.value, aValue, "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeValue_getID() { + return prettyName(aID) + " value changed"; + }; + } + + function changeProgressValue(aID, aValue) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)]; + + this.invoke = function changeProgressValue_invoke() { + this.DOMNode.value = aValue; + }; + + this.check = function changeProgressValue_check() { + var acc = getAccessible(this.DOMNode); + is(acc.value, aValue + "%", "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeProgressValue_getID() { + return prettyName(aID) + " value changed"; + }; + } + + function changeRangeValueWithMouse(aID) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)]; + + this.invoke = function changeRangeValue_invoke() { + synthesizeMouse(getNode(aID), 5, 5, { }); + }; + + this.finalCheck = function changeRangeValue_finalCheck() { + var acc = getAccessible(this.DOMNode); + is(acc.value, "0", "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeRangeValue_getID() { + return prettyName(aID) + " range value changed"; + }; + } + + function changeRangeValueWithA11yAPI(aID) { + this.DOMNode = getNode(aID); + let inputChecker = new invokerChecker("input", this.DOMNode); + inputChecker.eventTarget = "element"; + + let changeChecker = new invokerChecker("change", this.DOMNode); + changeChecker.eventTarget = "element"; + + this.eventSeq = [ + inputChecker, + changeChecker, + new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode), + ]; + + this.invoke = function changeRangeValue_invoke() { + this.DOMNode.focus(); + let acc = getAccessible(this.DOMNode, [nsIAccessibleValue]); + acc.currentValue = 0; + this.DOMNode.blur(); + }; + + this.finalCheck = function changeRangeValue_finalCheck() { + var acc = getAccessible(this.DOMNode); + is(acc.value, "0", "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeRangeValue_getID() { + return prettyName(aID) + " range value changed"; + }; + } + + function changeSelectValue(aID, aKey, aValue) { + this.eventSeq = + [ new invokerChecker(EVENT_TEXT_VALUE_CHANGE, getAccessible(aID)) ]; + + this.invoke = function changeSelectValue_invoke() { + getAccessible(aID).takeFocus(); + synthesizeKey(aKey, {}, window); + }; + + this.finalCheck = function changeSelectValue_finalCheck() { + is(getAccessible(aID).value, aValue, "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeSelectValue_getID() { + return `${prettyName(aID)} closed select value change on '${aKey}'' key press`; + }; + } + + // enableLogging("DOMEvents"); + // gA11yEventDumpToConsole = true; + function doTests() { + + // Test initial values + testValue("slider_vn", "5", 5, 0, 1000, 0); + testValue("slider_vnvt", "plain", 0, 0, 5, 0); + testValue("slider_vt", "hi", 1.5, 0, 3, 0); + testValue("scrollbar", "5", 5, 0, 1000, 0); + testValue("splitter", "5", 5, 0, 1000, 0); + testValue("progress", "22%", 22, 0, 100, 0); + testValue("range", "6", 6, 0, 10, 1); + testValue("range2", "6", 6, 0, 10, 1); + + // Test that elements which should not expose values do not + let separatorVal = getAccessible("separator", [nsIAccessibleValue], null, DONOTFAIL_IF_NO_INTERFACE); + ok(!separatorVal, "value interface is not exposed for separator"); + let separatorAcc = getAccessible("separator"); + ok(!separatorAcc.value, "Value text is not exposed for separator"); + + // Test value change events + gQueue = new eventQueue(); + + gQueue.push(new changeARIAValue("slider_vn", "6", undefined)); + gQueue.push(new changeARIAValue("slider_vt", undefined, "hey!")); + gQueue.push(new changeARIAValue("slider_vnvt", "3", "sweet")); + gQueue.push(new changeARIAValue("scrollbar", "6", undefined)); + gQueue.push(new changeARIAValue("splitter", "6", undefined)); + + gQueue.push(new changeValue("combobox", "hello")); + + gQueue.push(new changeProgressValue("progress", "50")); + gQueue.push(new changeRangeValueWithMouse("range")); + gQueue.push(new changeRangeValueWithA11yAPI("range2")); + + gQueue.push(new changeSelectValue("select", "VK_DOWN", "2nd")); + gQueue.push(new changeSelectValue("select", "3", "3rd")); + + let iframeSelect = getAccessible("selectIframe").firstChild.firstChild; + gQueue.push(new changeSelectValue(iframeSelect, "VK_DOWN", "2")); + + let shadowSelect = getAccessible("selectShadow").firstChild; + gQueue.push(new changeSelectValue(shadowSelect, "VK_DOWN", "2")); + gQueue.push(new changeValue("number", "2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=478032" + title=" Fire delayed value changed event for aria-valuetext changes"> + Mozilla Bug 478032 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289" + title="We dont expose new aria role 'scrollbar' and property aria-orientation"> + Mozilla Bug 529289 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764" + title="Make HTML5 input@type=range element accessible"> + Mozilla Bug 559764 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=703202" + title="ARIA comboboxes don't fire value change events"> + Mozilla Bug 703202 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761901" + title=" HTML5 progress accessible should fire value change event"> + Mozilla Bug 761901 + </a> + + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- ARIA sliders --> + <div id="slider_vn" role="slider" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">slider</div> + + <div id="slider_vt" role="slider" aria-valuetext="hi" + aria-valuemin="0" aria-valuemax="3">greeting slider</div> + + <div id="slider_vnvt" role="slider" aria-valuenow="0" aria-valuetext="plain" + aria-valuemin="0" aria-valuemax="5">sweetness slider</div> + + <!-- ARIA scrollbar --> + <div id="scrollbar" role="scrollbar" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">slider</div> + + <!-- ARIA separator which is focusable (i.e. a splitter) --> + <div id="splitter" role="separator" tabindex="0" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">splitter</div> + + <!-- ARIA separator which is not focusable and should not expose values --> + <div id="separator" role="separator" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">splitter</div> + + <!-- ARIA combobox --> + <input id="combobox" role="combobox" aria-autocomplete="inline"> + + <!-- progress bar --> + <progress id="progress" value="22" max="100"></progress> + + <!-- input@type="range" --> + <input type="range" id="range" min="0" max="10" value="6"> + + <!-- input@type="range" --> + <input type="range" id="range2" min="0" max="10" value="6"> + + <select id="select"> + <option>1st</option> + <option>2nd</option> + <option>3rd</option> + </select> + + <iframe id="selectIframe" + src="data:text/html,<select id='iframeSelect'><option>1</option><option>2</option></select>"> + </iframe> + + <div id="selectShadow"></div> + <script> + let host = document.getElementById("selectShadow"); + let shadow = host.attachShadow({mode: "open"}); + let select = document.createElement("select"); + select.id = "shadowSelect"; + let option = document.createElement("option"); + option.textContent = "1"; + select.appendChild(option); + option = document.createElement("option"); + option.textContent = "2"; + select.appendChild(option); + shadow.appendChild(select); + </script> + + <input type="number" id="number" value="1"> +</body> +</html> |