/* import-globals-from common.js */ /* import-globals-from events.js */ /* import-globals-from role.js */ /* import-globals-from states.js */ /* import-globals-from text.js */ // ////////////////////////////////////////////////////////////////////////////// // Constants const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; 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 NO_BOUNDARY = nsIAccessiblePivot.NO_BOUNDARY; 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() { return [ROLE_HEADING]; }, preFilter: PREFILTER_INVISIBLE, match(aAccessible) { return FILTER_MATCH; }, QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), }; /** * Traversal rule for all focusable nodes or leafs. */ var ObjectTraversalRule = { getMatchRoles() { return []; }, preFilter: PREFILTER_INVISIBLE | PREFILTER_TRANSPARENT, match(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_LISTITEM_MARKER && aAccessible.name.trim() ) { rv = FILTER_MATCH; } return rv; }, QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), }; // ////////////////////////////////////////////////////////////////////////////// // Virtual state invokers and checkers /** * A checker for virtual cursor changed events. */ function VCChangedChecker( aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod, aIsFromUserInput, aBoundaryType = NO_BOUNDARY ) { 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 && event.boundaryType == aBoundaryType ); }; 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 " + prettyName(aIdOrNameOrAcc) + ", got '" + prettyName(position) ); SimpleTest.is( aEvent.isFromUserInput, aIsFromUserInput, "Expected user input is " + aIsFromUserInput + "\n" ); SimpleTest.is( event.newAccessible, position, "new position in event is incorrect" ); if (aTextOffsets) { SimpleTest.is( aDocAcc.virtualCursor.startOffset, aTextOffsets[0], "wrong start offset" ); SimpleTest.is( aDocAcc.virtualCursor.endOffset, aTextOffsets[1], "wrong end offset" ); SimpleTest.is( event.newStartOffset, aTextOffsets[0], "wrong start offset in event" ); SimpleTest.is( event.newEndOffset, aTextOffsets[1], "wrong end offset in event" ); } 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_NONE, moveNextByText: nsIAccessiblePivot.REASON_NEXT, movePreviousByText: nsIAccessiblePivot.REASON_PREV, 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 ) { // eslint-disable-next-line mozilla/no-compare-against-boolean-literals 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 ) { // eslint-disable-next-line mozilla/no-compare-against-boolean-literals 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 ); 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, aBoundary ), ]; } 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 ) { // eslint-disable-next-line mozilla/no-compare-against-boolean-literals 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 (let i = 1; i < aSequence.length; i++) { let 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 (let i = aSequence.length - 2; i >= 0; i--) { let 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.remove(); }; 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.remove(); }; 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"); }