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