Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); //////////////////////////////////////////////////////////////////////////////// // Constants const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; const PREFILTER_ARIA_HIDDEN = nsIAccessibleTraversalRule.PREFILTER_ARIA_HIDDEN; const PREFILTER_TRANSPARENT = nsIAccessibleTraversalRule.PREFILTER_TRANSPARENT; const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY; const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY; const NS_ERROR_NOT_IN_TREE = 0x80780026; const NS_ERROR_INVALID_ARG = 0x80070057; //////////////////////////////////////////////////////////////////////////////// // Traversal rules /** * Rule object to traverse all focusable nodes and text nodes. */ var HeadersTraversalRule = { getMatchRoles: function(aRules) { aRules.value = [ROLE_HEADING]; return aRules.value.length; }, preFilter: PREFILTER_INVISIBLE, match: function(aAccessible) { return FILTER_MATCH; }, QueryInterface: XPCOMUtils.generateQI([nsIAccessibleTraversalRule]) } /** * Traversal rule for all focusable nodes or leafs. */ var ObjectTraversalRule = { getMatchRoles: function(aRules) { aRules.value = []; return 0; }, preFilter: PREFILTER_INVISIBLE | PREFILTER_ARIA_HIDDEN | PREFILTER_TRANSPARENT, match: function(aAccessible) { var rv = FILTER_IGNORE; var role = aAccessible.role; if (hasState(aAccessible, STATE_FOCUSABLE) && (role != ROLE_DOCUMENT && role != ROLE_INTERNAL_FRAME)) rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH; else if (aAccessible.childCount == 0 && role != ROLE_STATICTEXT && aAccessible.name.trim()) rv = FILTER_MATCH; return rv; }, QueryInterface: XPCOMUtils.generateQI([nsIAccessibleTraversalRule]) }; //////////////////////////////////////////////////////////////////////////////// // Virtual state invokers and checkers /** * A checker for virtual cursor changed events. */ function VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod, aIsFromUserInput) { this.__proto__ = new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc); this.match = function VCChangedChecker_match(aEvent) { var event = null; try { event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); } catch (e) { return false; } var expectedReason = VCChangedChecker.methodReasonMap[aPivotMoveMethod] || nsIAccessiblePivot.REASON_NONE; return event.reason == expectedReason; }; this.check = function VCChangedChecker_check(aEvent) { SimpleTest.info("VCChangedChecker_check"); var event = null; try { event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); } catch (e) { SimpleTest.ok(false, "Does not support correct interface: " + e); } var position = aDocAcc.virtualCursor.position; var idMatches = position && position.DOMNode.id == aIdOrNameOrAcc; var nameMatches = position && position.name == aIdOrNameOrAcc; var accMatches = position == aIdOrNameOrAcc; SimpleTest.ok(idMatches || nameMatches || accMatches, "id or name matches", "expecting " + aIdOrNameOrAcc + ", got '" + prettyName(position)); SimpleTest.is(aEvent.isFromUserInput, aIsFromUserInput, "Expected user input is " + aIsFromUserInput + '\n'); if (aTextOffsets) { SimpleTest.is(aDocAcc.virtualCursor.startOffset, aTextOffsets[0], "wrong start offset"); SimpleTest.is(aDocAcc.virtualCursor.endOffset, aTextOffsets[1], "wrong end offset"); } var prevPosAndOffset = VCChangedChecker. getPreviousPosAndOffset(aDocAcc.virtualCursor); if (prevPosAndOffset) { SimpleTest.is(event.oldAccessible, prevPosAndOffset.position, "previous position does not match"); SimpleTest.is(event.oldStartOffset, prevPosAndOffset.startOffset, "previous start offset does not match"); SimpleTest.is(event.oldEndOffset, prevPosAndOffset.endOffset, "previous end offset does not match"); } }; } VCChangedChecker.prevPosAndOffset = {}; VCChangedChecker.storePreviousPosAndOffset = function storePreviousPosAndOffset(aPivot) { VCChangedChecker.prevPosAndOffset[aPivot] = {position: aPivot.position, startOffset: aPivot.startOffset, endOffset: aPivot.endOffset}; }; VCChangedChecker.getPreviousPosAndOffset = function getPreviousPosAndOffset(aPivot) { return VCChangedChecker.prevPosAndOffset[aPivot]; }; VCChangedChecker.methodReasonMap = { 'moveNext': nsIAccessiblePivot.REASON_NEXT, 'movePrevious': nsIAccessiblePivot.REASON_PREV, 'moveFirst': nsIAccessiblePivot.REASON_FIRST, 'moveLast': nsIAccessiblePivot.REASON_LAST, 'setTextRange': nsIAccessiblePivot.REASON_TEXT, 'moveNextByText': nsIAccessiblePivot.REASON_TEXT, 'movePreviousByText': nsIAccessiblePivot.REASON_TEXT, 'moveToPoint': nsIAccessiblePivot.REASON_POINT }; /** * Set a text range in the pivot and wait for virtual cursor change event. * * @param aDocAcc [in] document that manages the virtual cursor * @param aTextAccessible [in] accessible to set to virtual cursor's position * @param aTextOffsets [in] start and end offsets of text range to set in * virtual cursor. */ function setVCRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) { this.invoke = function virtualCursorChangedInvoker_invoke() { VCChangedChecker. storePreviousPosAndOffset(aDocAcc.virtualCursor); SimpleTest.info(prettyName(aTextAccessible) + " " + aTextOffsets); aDocAcc.virtualCursor.setTextRange(aTextAccessible, aTextOffsets[0], aTextOffsets[1]); }; this.getID = function setVCRangeInvoker_getID() { return "Set offset in " + prettyName(aTextAccessible) + " to (" + aTextOffsets[0] + ", " + aTextOffsets[1] + ")"; }; this.eventSeq = [ new VCChangedChecker(aDocAcc, aTextAccessible, aTextOffsets, "setTextRange", true) ]; } /** * Move the pivot and wait for virtual cursor change event. * * @param aDocAcc [in] document that manages the virtual cursor * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) * @param aRule [in] traversal rule object * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect * virtual cursor to land on after performing move method. * false if no move is expected. * @param aIsFromUserInput [in] set user input flag when invoking method, and * expect it in the event. */ function setVCPosInvoker(aDocAcc, aPivotMoveMethod, aRule, aIdOrNameOrAcc, aIsFromUserInput) { var expectMove = (aIdOrNameOrAcc != false); this.invoke = function virtualCursorChangedInvoker_invoke() { VCChangedChecker. storePreviousPosAndOffset(aDocAcc.virtualCursor); if (aPivotMoveMethod && aRule) { var moved = false; switch (aPivotMoveMethod) { case 'moveFirst': case 'moveLast': moved = aDocAcc.virtualCursor[aPivotMoveMethod](aRule, aIsFromUserInput === undefined ? true : aIsFromUserInput); break; case 'moveNext': case 'movePrevious': moved = aDocAcc.virtualCursor[aPivotMoveMethod](aRule, aDocAcc.virtualCursor.position, false, aIsFromUserInput === undefined ? true : aIsFromUserInput); break; } SimpleTest.is(!!moved, !!expectMove, "moved pivot with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc); } else { aDocAcc.virtualCursor.position = getAccessible(aIdOrNameOrAcc); } }; this.getID = function setVCPosInvoker_getID() { return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod; }; if (expectMove) { this.eventSeq = [ new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, aPivotMoveMethod, aIsFromUserInput === undefined ? !!aPivotMoveMethod : aIsFromUserInput) ]; } else { this.eventSeq = []; this.unexpectedEventSeq = [ new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) ]; } } /** * Move the pivot by text and wait for virtual cursor change event. * * @param aDocAcc [in] document that manages the virtual cursor * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) * @param aBoundary [in] boundary constant * @param aTextOffsets [in] start and end offsets of text range to set in * virtual cursor. * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect * virtual cursor to land on after performing move method. * false if no move is expected. * @param aIsFromUserInput [in] set user input flag when invoking method, and * expect it in the event. */ function setVCTextInvoker(aDocAcc, aPivotMoveMethod, aBoundary, aTextOffsets, aIdOrNameOrAcc, aIsFromUserInput) { var expectMove = (aIdOrNameOrAcc != false); this.invoke = function virtualCursorChangedInvoker_invoke() { VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); SimpleTest.info(aDocAcc.virtualCursor.position); var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aBoundary, aIsFromUserInput === undefined ? true : false); SimpleTest.is(!!moved, !!expectMove, "moved pivot by text with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc); }; this.getID = function setVCPosInvoker_getID() { return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod + " in " + prettyName(aIdOrNameOrAcc) + ", " + boundaryToString(aBoundary) + ", [" + aTextOffsets + "]"; }; if (expectMove) { this.eventSeq = [ new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod, aIsFromUserInput === undefined ? true : aIsFromUserInput) ]; } else { this.eventSeq = []; this.unexpectedEventSeq = [ new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) ]; } } /** * Move the pivot to the position under the point. * * @param aDocAcc [in] document that manages the virtual cursor * @param aX [in] screen x coordinate * @param aY [in] screen y coordinate * @param aIgnoreNoMatch [in] don't unset position if no object was found at * point. * @param aRule [in] traversal rule object * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect * virtual cursor to land on after performing move method. * false if no move is expected. */ function moveVCCoordInvoker(aDocAcc, aX, aY, aIgnoreNoMatch, aRule, aIdOrNameOrAcc) { var expectMove = (aIdOrNameOrAcc != false); this.invoke = function virtualCursorChangedInvoker_invoke() { VCChangedChecker. storePreviousPosAndOffset(aDocAcc.virtualCursor); var moved = aDocAcc.virtualCursor.moveToPoint(aRule, aX, aY, aIgnoreNoMatch); SimpleTest.ok((expectMove && moved) || (!expectMove && !moved), "moved pivot"); }; this.getID = function setVCPosInvoker_getID() { return "Do " + (expectMove ? "" : "no-op ") + "moveToPoint " + aIdOrNameOrAcc; }; if (expectMove) { this.eventSeq = [ new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, 'moveToPoint', true) ]; } else { this.eventSeq = []; this.unexpectedEventSeq = [ new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) ]; } } /** * Change the pivot modalRoot * * @param aDocAcc [in] document that manages the virtual cursor * @param aModalRootAcc [in] accessible of the modal root, or null * @param aExpectedResult [in] error result expected. 0 if expecting success */ function setModalRootInvoker(aDocAcc, aModalRootAcc, aExpectedResult) { this.invoke = function setModalRootInvoker_invoke() { var errorResult = 0; try { aDocAcc.virtualCursor.modalRoot = aModalRootAcc; } catch (x) { SimpleTest.ok( x.result, "Unexpected exception when changing modal root: " + x); errorResult = x.result; } SimpleTest.is(errorResult, aExpectedResult, "Did not get expected result when changing modalRoot"); }; this.getID = function setModalRootInvoker_getID() { return "Set modalRoot to " + prettyName(aModalRootAcc); }; this.eventSeq = []; this.unexpectedEventSeq = [ new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) ]; } /** * Add invokers to a queue to test a rule and an expected sequence of element ids * or accessible names for that rule in the given document. * * @param aQueue [in] event queue in which to push invoker sequence. * @param aDocAcc [in] the managing document of the virtual cursor we are * testing * @param aRule [in] the traversal rule to use in the invokers * @param aModalRoot [in] a modal root to use in this traversal sequence * @param aSequence [in] a sequence of accessible names or element ids to expect * with the given rule in the given document */ function queueTraversalSequence(aQueue, aDocAcc, aRule, aModalRoot, aSequence) { aDocAcc.virtualCursor.position = null; // Add modal root (if any) aQueue.push(new setModalRootInvoker(aDocAcc, aModalRoot, 0)); aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); for (var i = 1; i < aSequence.length; i++) { var invoker = new setVCPosInvoker(aDocAcc, "moveNext", aRule, aSequence[i]); aQueue.push(invoker); } // No further more matches for given rule, expect no virtual cursor changes. aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); for (var i = aSequence.length-2; i >= 0; i--) { var invoker = new setVCPosInvoker(aDocAcc, "movePrevious", aRule, aSequence[i]); aQueue.push(invoker); } // No previous more matches for given rule, expect no virtual cursor changes. aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); aQueue.push(new setVCPosInvoker(aDocAcc, "moveLast", aRule, aSequence[aSequence.length - 1])); // No further more matches for given rule, expect no virtual cursor changes. aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); // set isFromUserInput to false, just to test.. aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0], false)); // No previous more matches for given rule, expect no virtual cursor changes. aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); // Remove modal root (if any). aQueue.push(new setModalRootInvoker(aDocAcc, null, 0)); } /** * A checker for removing an accessible while the virtual cursor is on it. */ function removeVCPositionChecker(aDocAcc, aHiddenParentAcc) { this.__proto__ = new invokerChecker(EVENT_REORDER, aHiddenParentAcc); this.check = function removeVCPositionChecker_check(aEvent) { var errorResult = 0; try { aDocAcc.virtualCursor.moveNext(ObjectTraversalRule); } catch (x) { errorResult = x.result; } SimpleTest.is( errorResult, NS_ERROR_NOT_IN_TREE, "Expecting NOT_IN_TREE error when moving pivot from invalid position."); }; } /** * Put the virtual cursor's position on an object, and then remove it. * * @param aDocAcc [in] document that manages the virtual cursor * @param aPosNode [in] DOM node to hide after virtual cursor's position is * set to it. */ function removeVCPositionInvoker(aDocAcc, aPosNode) { this.accessible = getAccessible(aPosNode); this.invoke = function removeVCPositionInvoker_invoke() { aDocAcc.virtualCursor.position = this.accessible; aPosNode.parentNode.removeChild(aPosNode); }; this.getID = function removeVCPositionInvoker_getID() { return "Bring virtual cursor to accessible, and remove its DOM node."; }; this.eventSeq = [ new removeVCPositionChecker(aDocAcc, this.accessible.parent) ]; } /** * A checker for removing the pivot root and then calling moveFirst, and * checking that an exception is thrown. */ function removeVCRootChecker(aPivot) { this.__proto__ = new invokerChecker(EVENT_REORDER, aPivot.root.parent); this.check = function removeVCRootChecker_check(aEvent) { var errorResult = 0; try { aPivot.moveLast(ObjectTraversalRule); } catch (x) { errorResult = x.result; } SimpleTest.is( errorResult, NS_ERROR_NOT_IN_TREE, "Expecting NOT_IN_TREE error when moving pivot from invalid position."); }; } /** * Create a pivot, remove its root, and perform an operation where the root is * needed. * * @param aRootNode [in] DOM node of which accessible will be the root of the * pivot. Should have more than one child. */ function removeVCRootInvoker(aRootNode) { this.pivot = gAccService.createAccessiblePivot(getAccessible(aRootNode)); this.invoke = function removeVCRootInvoker_invoke() { this.pivot.position = this.pivot.root.firstChild; aRootNode.parentNode.removeChild(aRootNode); }; this.getID = function removeVCRootInvoker_getID() { return "Remove root of pivot from tree."; }; this.eventSeq = [ new removeVCRootChecker(this.pivot) ]; } /** * A debug utility for writing proper sequences for queueTraversalSequence. */ function dumpTraversalSequence(aPivot, aRule) { var sequence = []; if (aPivot.moveFirst(aRule)) { do { sequence.push("'" + prettyName(aPivot.position) + "'"); } while (aPivot.moveNext(aRule)) } SimpleTest.info("\n[" + sequence.join(", ") + "]\n"); }