AX: should dispatch accessibilityPerformPressAction async on MacOS
authorn_wang@apple.com <n_wang@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jul 2017 07:39:01 +0000 (07:39 +0000)
committern_wang@apple.com <n_wang@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jul 2017 07:39:01 +0000 (07:39 +0000)
https://bugs.webkit.org/show_bug.cgi?id=174849

Reviewed by Chris Fleizach.

Source/WebCore:

If performing the accessibility press action results in a modal alert being displayed,
it can cause VoiceOver to hang. To fix it, we should dispatch the action asynchronously.

Updated tests to adapt to this change.

* accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(-[WebAccessibilityObjectWrapper accessibilityPerformPressAction]):
(-[WebAccessibilityObjectWrapper _accessibilityPerformPressAction]):

LayoutTests:

* accessibility/file-upload-button-with-axpress.html:
* accessibility/mac/html5-input-number.html:
* accessibility/mac/search-field-cancel-button.html:
* accessibility/press-target-uses-text-descendant-node.html:
* accessibility/press-targets-center-point.html:
* accessibility/press-works-on-control-types.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@219906 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/accessibility/file-upload-button-with-axpress.html
LayoutTests/accessibility/mac/html5-input-number.html
LayoutTests/accessibility/mac/search-field-cancel-button.html
LayoutTests/accessibility/mac/search-predicate.html
LayoutTests/accessibility/press-target-uses-text-descendant-node.html
LayoutTests/accessibility/press-targets-center-point.html
LayoutTests/accessibility/press-works-on-control-types.html
LayoutTests/media/media-controls-accessibility.html
Source/WebCore/ChangeLog
Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm

index 936c940..1bd4c18 100644 (file)
@@ -1,3 +1,17 @@
+2017-07-26  Nan Wang  <n_wang@apple.com>
+
+        AX: should dispatch accessibilityPerformPressAction async on MacOS
+        https://bugs.webkit.org/show_bug.cgi?id=174849
+
+        Reviewed by Chris Fleizach.
+
+        * accessibility/file-upload-button-with-axpress.html:
+        * accessibility/mac/html5-input-number.html:
+        * accessibility/mac/search-field-cancel-button.html:
+        * accessibility/press-target-uses-text-descendant-node.html:
+        * accessibility/press-targets-center-point.html:
+        * accessibility/press-works-on-control-types.html:
+
 2017-07-25  Andy Estes  <aestes@apple.com>
 
         [Apple Pay] Add "carteBancaire" as a supported payment network
index b055c5f..30daa1c 100644 (file)
@@ -19,7 +19,9 @@ if (window.testRunner && window.accessibilityController) {
 
     inputFile.addEventListener("DOMActivate", function() { 
         debug("DOMActivate was called"); 
-        finishJSTest();
+        setTimeout(function() {
+            finishJSTest();
+        }, 10);
     });
 
     accessibilityController.accessibleElementById("filetype").press();
index da13c94..fc123f6 100644 (file)
     description("This tests that input type='number' exposes the accessibility of it's stepper correctly");
 
     if (window.accessibilityController) {
-
+        window.jsTestIsAsync = true;
+        
         document.getElementById("number").focus();
-        var textfield = accessibilityController.focusedElement;
+        var textfield = accessibilityController.accessibleElementById("number");
 
         // Verify that the click point is the same as the child.
         shouldBe("textfield.childrenCount", "1");
 
         // Increment.
         incrementor.childAtIndex(0).press();
-        shouldBe("textfield.stringValue", "'AXValue: 1'");
-
-        shouldBe("incrementor.childAtIndex(1).role", "'AXRole: AXButton'");
-        shouldBe("incrementor.childAtIndex(1).subrole", "'AXSubrole: AXDecrementArrow'");
-        shouldBeTrue("incrementor.childAtIndex(1).width > 0");
-        shouldBeTrue("incrementor.childAtIndex(1).height > 0");
-        shouldBeTrue("incrementor.childAtIndex(1).isEnabled");
-
-        // Decrement.
-        incrementor.childAtIndex(1).press();
-        shouldBe("textfield.stringValue", "'AXValue: 0'");
+        setTimeout(function() {
+            shouldBe("textfield.stringValue", "'AXValue: 1'");
+            shouldBe("incrementor.childAtIndex(1).role", "'AXRole: AXButton'");
+            shouldBe("incrementor.childAtIndex(1).subrole", "'AXSubrole: AXDecrementArrow'");
+            shouldBeTrue("incrementor.childAtIndex(1).width > 0");
+            shouldBeTrue("incrementor.childAtIndex(1).height > 0");
+            shouldBeTrue("incrementor.childAtIndex(1).isEnabled");
+            
+            // Decrement.
+            incrementor.childAtIndex(1).press();
+            setTimeout(function() {
+                shouldBe("textfield.stringValue", "'AXValue: 0'");
+                finishJSTest();
+            }, 10);
+        }, 10);
     }
 
 </script>
index 7b0766b..ea91bfd 100644 (file)
@@ -15,6 +15,8 @@
     description("This tests that the search field cancel button is exposed correctly.");
     
     if (window.accessibilityController) {
+        window.jsTestIsAsync = true;
+    
         var button = accessibilityController.accessibleElementById("search").childAtIndex(1);
         
         shouldBe("button.description", "'AXDescription: cancel'");
         shouldBe("document.getElementById('search').value", "'X'");
         
         button.press();
-        
-        // Search field has no value after press.
-        shouldBe("document.getElementById('search').value", "''");
+        setTimeout(function() {
+            // Search field has no value after press.
+            shouldBe("document.getElementById('search').value", "''");
+            finishJSTest();
+        }, 10);
     }
 </script>
 
index c7e9802..250ff55 100644 (file)
@@ -61,6 +61,7 @@
     description("This tests the ability to search for accessible elements by key or text.");
     
     if (window.accessibilityController) {
+        jsTestIsAsync = true;
         window.testRunner.keepWebHistory();
         
         document.getElementById("body").focus();
         
         // Visited link.
         accessibilityController.focusedElement.childAtIndex(14).childAtIndex(0).press();
-        startElement = accessibilityController.focusedElement.childAtIndex(0);
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXVisitedLinkSearchKey", "", false);
-        shouldBe("resultElement.boolAttributeValue('AXVisited')", "true");
-        shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: link'");
-        
-        // Previous text search.
-        startElement = accessibilityController.focusedElement.childAtIndex(10);
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, false, "", "sans-serif black bold text with underline", false);
-        shouldBe("resultElement.role", "'AXRole: AXStaticText'");
-        shouldBe("resultElement.stringValue", "'AXValue: sans-serif black bold text with underline'");
-        
-        // Execute a search for the next heading level 2 or the next link.
-        startElement = accessibilityController.focusedElement.childAtIndex(0);
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
-        shouldBe("resultElement.role", "'AXRole: AXHeading'");
-        shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: heading level 2'");
-        
-        // After finding the heading, execute the search again and we should find the link.
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
-        shouldBe("resultElement.role", "'AXRole: AXLink'");
-        shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: link'");
+        setTimeout(function() {
+            
+            startElement = accessibilityController.focusedElement.childAtIndex(0);
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXVisitedLinkSearchKey", "", false);
+            shouldBe("resultElement.boolAttributeValue('AXVisited')", "true");
+            shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: link'");
+        
+            // Previous text search.
+            startElement = accessibilityController.focusedElement.childAtIndex(10);
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, false, "", "sans-serif black bold text with underline", false);
+            shouldBe("resultElement.role", "'AXRole: AXStaticText'");
+            shouldBe("resultElement.stringValue", "'AXValue: sans-serif black bold text with underline'");
+        
+            // Execute a search for the next heading level 2 or the next link.
+            startElement = accessibilityController.focusedElement.childAtIndex(0);
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
+            shouldBe("resultElement.role", "'AXRole: AXHeading'");
+            shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: heading level 2'");
+        
+            // After finding the heading, execute the search again and we should find the link.
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
+            shouldBe("resultElement.role", "'AXRole: AXLink'");
+            shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: link'");
 
-        // From the link, execute the search in reverse and we should land back on the heading.
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, false, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
-        shouldBe("resultElement.role", "'AXRole: AXHeading'");
-        shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: heading level 2'");
+            // From the link, execute the search in reverse and we should land back on the heading.
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, false, ["AXHeadingLevel2SearchKey", "AXLinkSearchKey"], "", false);
+            shouldBe("resultElement.role", "'AXRole: AXHeading'");
+            shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: heading level 2'");
                
-        // Now, we need to test isVisible. Save off the first object
-        startElement = accessibilityController.focusedElement.childAtIndex(0);
-        
-        // Scroll all the way to the bottom of the content
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "", "test button 3", false);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 3'");
-        resultElement.scrollToMakeVisible();
-        
-        // find the start of the isVisible test section
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "", "isVisible test start", false);
-        shouldBe("resultElement.role", "'AXRole: AXHeading'");
-        shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: isVisible test start'");       
-        
-        // save away the "isVisible test start" heading as the start element
-        startElement = resultElement;
-        
-        // If we don't care about visible only, then we should easily find 3 buttons
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", false);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 1'");
-        
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", false);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 2'");
-        
-        // save away testButton2 so we can make it visible later
-        testButton2 = resultElement;
+            // Now, we need to test isVisible. Save off the first object
+            startElement = accessibilityController.focusedElement.childAtIndex(0);
+        
+            // Scroll all the way to the bottom of the content
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "", "test button 3", false);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 3'");
+            resultElement.scrollToMakeVisible();
+        
+            // find the start of the isVisible test section
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "", "isVisible test start", false);
+            shouldBe("resultElement.role", "'AXRole: AXHeading'");
+            shouldBe("resultElement.childAtIndex(0).stringValue", "'AXValue: isVisible test start'");       
+        
+            // save away the "isVisible test start" heading as the start element
+            startElement = resultElement;
+        
+            // If we don't care about visible only, then we should easily find 3 buttons
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", false);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 1'");
+        
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", false);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 2'");
+        
+            // save away testButton2 so we can make it visible later
+            testButton2 = resultElement;
 
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", false);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 3'");
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", false);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 3'");
 
-        // if we care about visible only, then we should not find "test button 2"
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 1'");
+            // if we care about visible only, then we should not find "test button 2"
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 1'");
         
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 3'");
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 3'");
         
-        // now, scroll to the second button, and confirm that we don't see the first button
-        testButton2.scrollToMakeVisible();
+            // now, scroll to the second button, and confirm that we don't see the first button
+            testButton2.scrollToMakeVisible();
 
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 2'");
-        
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 3'");
-        
-        // Now since the page is scrolled to the bottom, the first visible button should be #2
-        startElement = accessibilityController.focusedElement.childAtIndex(0);
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: test button 2'");
-        
-        // lets scroll to the top of the page and ensure that the submit button is visible
-        startElement.scrollToMakeVisible();
-        resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
-        shouldBe("resultElement.role", "'AXRole: AXButton'");
-        shouldBe("resultElement.title", "'AXTitle: Submit'");
-        
-        // there should be no more visible buttons
-        resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
-        shouldBeUndefined("resultElement");
-        
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 2'");
+        
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 3'");
+        
+            // Now since the page is scrolled to the bottom, the first visible button should be #2
+            startElement = accessibilityController.focusedElement.childAtIndex(0);
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: test button 2'");
+        
+            // lets scroll to the top of the page and ensure that the submit button is visible
+            startElement.scrollToMakeVisible();
+            resultElement = containerElement.uiElementForSearchPredicate(startElement, true, "AXButtonSearchKey", "", true);
+            shouldBe("resultElement.role", "'AXRole: AXButton'");
+            shouldBe("resultElement.title", "'AXTitle: Submit'");
+        
+            // there should be no more visible buttons
+            resultElement = containerElement.uiElementForSearchPredicate(resultElement, true, "AXButtonSearchKey", "", true);
+            shouldBeUndefined("resultElement");
+        
+            finishJSTest();
+        }, 50);
     }
     
 </script>
index a39abfa..760dd3c 100644 (file)
 
     function startTest() {
        accessibilityController.accessibleElementById("link").press();
-       debug("\nNow pressing on button\n");
-       accessibilityController.accessibleElementById("button").press();
-       finishJSTest();
+       setTimeout(function() {
+           debug("\nNow pressing on button\n");
+           accessibilityController.accessibleElementById("button").press();
+           setTimeout(function() {
+               finishJSTest();
+           }, 10);
+       }, 10);
     }
 
     if (window.accessibilityController) {
index 4646e5b..9a5c645 100644 (file)
     }
     
     if (window.accessibilityController) {
+        window.jsTestIsAsync = true;
+        
         // Press all targets.
-        for (var i = 0; i < targetCount; ++i)
-            accessibilityController.accessibleElementById("t" + i).press();
+        accessibilityPress(0);
+    }
+    
+    function accessibilityPress(i) {
+        if (i == targetCount)
+            finishJSTest();
+        
+        accessibilityController.accessibleElementById("t" + i).press();
+        setTimeout(function() {
+            accessibilityPress(i+1);
+        }, 10);
     }
 </script>
 
index 27e82ba..828ecd1 100644 (file)
 
     description("This tests that when certain control type elements are pressed, a valid event is sent that references the right element.");
 
+    var items = new Array("group", "button", "tab", "radio", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio", "listitem", "button2");
+    var length = items.length;
+    
     if (window.accessibilityController) {
-
-        var items = new Array("group", "button", "tab", "radio", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio", "listitem", "button2");
-        for (var k = 0; k < items.length; k++) {
-           document.getElementById(items[k]).focus();
-           accessibilityController.focusedElement.press();
-        }
+        jsTestIsAsync = true;
+        // AXPress on Mac is async, let's give it some delay and do it recursively.
+        accessibilityPress(0);
+    }
+    
+    function accessibilityPress(k) {
+        if (k == length)
+            finishJSTest();
+        
+        accessibilityController.accessibleElementById(items[k]).press();
+        setTimeout(function() {
+            accessibilityPress(k+1);
+        }, 10);
     }
 
 </script>
index 702247c..e8822a3 100644 (file)
@@ -37,10 +37,12 @@ if (window.accessibilityController) {
         debug("muteButton.stringValue: " + muteButton.stringValue);
         debug("press muteButton");
         muteButton.press();
-        debug("muteButton.stringValue: " + muteButton.stringValue + "\n");
+        setTimeout(function() {
+            debug("muteButton.stringValue: " + muteButton.stringValue + "\n");
               
-        // Left/Right arrow key should have 0.5 second step on timeline. 
-        checkTimeLineValue(rightArrow);
+            // Left/Right arrow key should have 0.5 second step on timeline. 
+            checkTimeLineValue(rightArrow);
+        }, 10);
     }); 
     
     waitForEvent("seeked", function () {
index bd354d2..af058fe 100644 (file)
@@ -1,3 +1,19 @@
+2017-07-26  Nan Wang  <n_wang@apple.com>
+
+        AX: should dispatch accessibilityPerformPressAction async on MacOS
+        https://bugs.webkit.org/show_bug.cgi?id=174849
+
+        Reviewed by Chris Fleizach.
+
+        If performing the accessibility press action results in a modal alert being displayed,
+        it can cause VoiceOver to hang. To fix it, we should dispatch the action asynchronously.
+
+        Updated tests to adapt to this change.
+
+        * accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
+        (-[WebAccessibilityObjectWrapper accessibilityPerformPressAction]):
+        (-[WebAccessibilityObjectWrapper _accessibilityPerformPressAction]):
+
 2017-07-25  Carlos Garcia Campos  <cgarcia@igalia.com>
 
         Icon loader error on startup
index e4e365a..b1edf27 100644 (file)
@@ -3415,6 +3415,15 @@ static NSString* roleValueToNSString(AccessibilityRole value)
 
 - (void)accessibilityPerformPressAction
 {
+    // In case anything we do by performing the press action causes an alert or other modal
+    // behaviors, we need to return now, so that VoiceOver doesn't hang indefinitely.
+    dispatch_async(dispatch_get_main_queue(), ^ {
+        [self _accessibilityPerformPressAction];
+    });
+}
+
+- (void)_accessibilityPerformPressAction
+{
     if (![self updateObjectBackingStore])
         return;