Bug 1465539 - Introduce accessibility test for editable node. r=jchen

--HG--
rename : mobile/android/geckoview/src/androidTest/assets/www/selectionAction.html => mobile/android/geckoview/src/androidTest/assets/www/inputs.html
This commit is contained in:
Eitan Isaacson 2018-06-21 13:20:00 +03:00
parent 6e9c4e588f
commit 04cfa9d84b
7 changed files with 181 additions and 38 deletions

View File

@ -1,5 +1,5 @@
<html>
<head><title>SelectionActionDelegate Test</title></head>
<head><title>Inputs</title></head>
<body>
<div id="text">lorem</div>
<input id="input" value="ipsum">

View File

@ -0,0 +1,143 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.geckoview.test
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
import android.support.test.filters.MediumTest
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityRecord
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import org.hamcrest.Matchers.*
import org.junit.Test
import org.junit.Before
import org.junit.After
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@WithDisplay(width = 480, height = 640)
@WithDevToolsAPI
class AccessibilityTest : BaseSessionTest() {
lateinit var view: View
val provider: AccessibilityNodeProvider get() = view.getAccessibilityNodeProvider()
// Given a child ID, return the virtual descendent ID.
private fun getVirtualDescendantId(childId: Long): Int {
try {
var getVirtualDescendantIdMethod =
AccessibilityNodeInfo::class.java.getMethod("getVirtualDescendantId", Long::class.java)
return getVirtualDescendantIdMethod.invoke(null, childId) as Int
} catch (ex: Exception) {
return 0
}
}
// Retrieve the virtual descendent ID of the event's source.
private fun getSourceId(event: AccessibilityEvent): Int {
try {
var getSourceIdMethod =
AccessibilityRecord::class.java.getMethod("getSourceNodeId")
return getVirtualDescendantId(getSourceIdMethod.invoke(event) as Long)
} catch (ex: Exception) {
return 0
}
}
private interface EventDelegate {
fun onAccessibilityFocused(event: AccessibilityEvent) { }
fun onFocused(event: AccessibilityEvent) { }
}
@Before fun setup() {
// We initialize a view with a parent and grandparent so that the
// accessibility events propogate up at least to the parent.
view = FrameLayout(InstrumentationRegistry.getTargetContext())
FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view)
FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view.parent as View)
// Force on accessibility and assign the session's accessibility
// object a view.
sessionRule.setPrefsUntilTestEnd(mapOf("accessibility.force_disabled" to -1))
mainSession.accessibility.view = view
// Set up an external delegate that will intercept accessibility events.
sessionRule.addExternalDelegateUntilTestEnd(
EventDelegate::class,
{ newDelegate -> (view.parent as View).setAccessibilityDelegate(object : View.AccessibilityDelegate() {
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean {
when (event.getEventType()) {
AccessibilityEvent.TYPE_VIEW_FOCUSED -> newDelegate.onFocused(event)
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
else -> {}
}
return false
}
}) },
{ (view.parent as View).setAccessibilityDelegate(null) },
object : EventDelegate { })
}
@After fun teardown() {
sessionRule.session.accessibility.view = null
}
@Test fun testRootNode() {
assertThat("provider is not null", provider, notNullValue())
var node = provider.createAccessibilityNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID)
assertThat("Root node should have WebView class name",
node.getClassName().toString(), equalTo("android.webkit.WebView"))
}
@Test fun testPageLoad() {
sessionRule.session.loadTestPath(INPUTS_PATH)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onFocused(event: AccessibilityEvent) { }
})
}
@Test fun testAccessibilityFocus() {
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
sessionRule.session.loadTestPath(INPUTS_PATH)
sessionRule.waitForPageStop()
provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAccessibilityFocused(event: AccessibilityEvent) {
nodeId = getSourceId(event)
var node = provider.createAccessibilityNodeInfo(nodeId)
assertThat("Text node should not be focusable", node.isFocusable(), equalTo(false))
}
})
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAccessibilityFocused(event: AccessibilityEvent) {
nodeId = getSourceId(event)
var node = provider.createAccessibilityNodeInfo(nodeId)
assertThat("Entry node should be focusable", node.isFocusable(), equalTo(true))
}
})
}
}

View File

@ -30,10 +30,10 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
const val HELLO_HTML_PATH = "/assets/www/hello.html"
const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
const val INPUTS_PATH = "/assets/www/inputs.html"
const val INVALID_URI = "http://www.test.invalid/"
const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
const val SELECTION_ACTION_PATH = "/assets/www/selectionAction.html"
const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
const val TRACKERS_PATH = "/assets/www/trackers.html"
}

View File

@ -173,7 +173,7 @@ class SelectionActionDelegateTest : BaseSessionTest() {
sessionRule.setPrefsUntilTestEnd(mapOf(
"layout.accessiblecaret.enabled_on_touch" to true))
mainSession.loadTestPath(SELECTION_ACTION_PATH)
mainSession.loadTestPath(INPUTS_PATH)
mainSession.waitForPageStop()
content.focus()

View File

@ -50,7 +50,7 @@ class TextInputDelegateTest : BaseSessionTest() {
@Test fun restartInput() {
// Check that restartInput is called on focus and blur.
mainSession.loadTestPath(SELECTION_ACTION_PATH)
mainSession.loadTestPath(INPUTS_PATH)
mainSession.waitForPageStop()
mainSession.evaluateJS("$('$id').focus()")
@ -85,7 +85,7 @@ class TextInputDelegateTest : BaseSessionTest() {
// Our user action trick doesn't work for design-mode, so we can't test that here.
assumeThat("Not in designmode", id, not(equalTo("#designmode")))
mainSession.loadTestPath(SELECTION_ACTION_PATH)
mainSession.loadTestPath(INPUTS_PATH)
mainSession.waitForPageStop()
// Focus the input once here and once below, but we should only get a
@ -119,7 +119,7 @@ class TextInputDelegateTest : BaseSessionTest() {
// Our user action trick doesn't work for design-mode, so we can't test that here.
assumeThat("Not in designmode", id, not(equalTo("#designmode")))
mainSession.loadTestPath(SELECTION_ACTION_PATH)
mainSession.loadTestPath(INPUTS_PATH)
mainSession.waitForPageStop()
// Simulate a user action so we're allowed to show/hide the keyboard.
@ -154,7 +154,7 @@ class TextInputDelegateTest : BaseSessionTest() {
// Our user action trick doesn't work for design-mode, so we can't test that here.
assumeThat("Not in designmode", id, not(equalTo("#designmode")))
mainSession.loadTestPath(SELECTION_ACTION_PATH)
mainSession.loadTestPath(INPUTS_PATH)
mainSession.waitForPageStop()
// Simulate a user action so we're allowed to show/hide the keyboard.

View File

@ -7,6 +7,7 @@ package org.mozilla.geckoview;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
@ -109,7 +110,10 @@ public class SessionAccessibility {
// as a child. It is a source for events,
// but not a member of the tree you
// can get to by traversing down.
onInitializeAccessibilityNodeInfo(mView, info);
if (mView.getDisplay() != null) {
// When running junit tests we don't have a display
onInitializeAccessibilityNodeInfo(mView, info);
}
info.setClassName("android.webkit.WebView"); // TODO: WTF
if (Build.VERSION.SDK_INT >= 19) {
Bundle bundle = info.getExtras();
@ -244,19 +248,14 @@ public class SessionAccessibility {
});
}
public static class Settings {
private static class Settings {
private static final Settings INSTANCE = new Settings();
private boolean mEnabled;
private static final String FORCE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
private volatile boolean mEnabled;
/* package */ volatile boolean mForceEnabled;
public Settings() {
EventDispatcher.getInstance().registerUiThreadListener(new BundleEventListener() {
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
updateAccessibilitySettings();
}
}, "GeckoView:AccessibilityReady", null);
final Context context = GeckoAppShell.getApplicationContext();
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
@ -283,6 +282,17 @@ public class SessionAccessibility {
}
);
}
PrefsHelper.PrefHandler prefHandler = new PrefsHelper.PrefHandlerBase() {
@Override
public void prefValue(String pref, int value) {
if (pref.equals(FORCE_ACCESSIBILITY_PREF)) {
mForceEnabled = value < 0;
dispatch();
}
}
};
PrefsHelper.addObserver(new String[]{ FORCE_ACCESSIBILITY_PREF }, prefHandler);
}
public static Settings getInstance() {
@ -290,26 +300,20 @@ public class SessionAccessibility {
}
public static boolean isEnabled() {
return INSTANCE.mEnabled;
return INSTANCE.mEnabled || INSTANCE.mForceEnabled;
}
private void updateAccessibilitySettings() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final AccessibilityManager accessibilityManager = (AccessibilityManager)
GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() &&
accessibilityManager.isTouchExplorationEnabled();
final AccessibilityManager accessibilityManager = (AccessibilityManager)
GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
dispatch();
}
});
dispatch();
}
private void dispatch() {
final GeckoBundle ret = new GeckoBundle(1);
ret.putBoolean("enabled", mEnabled);
ret.putBoolean("enabled", mEnabled || mForceEnabled);
// "GeckoView:AccessibilitySettings" is dispatched to the Gecko thread.
EventDispatcher.getInstance().dispatch("GeckoView:AccessibilitySettings", ret);
// "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.
@ -358,7 +362,9 @@ public class SessionAccessibility {
node.setPassword(message.getBoolean("password"));
node.setFocusable(message.getBoolean("focusable"));
node.setFocused(message.getBoolean("focused"));
node.setEditable(message.getBoolean("editable"));
if (Build.VERSION.SDK_INT >= 18) {
node.setEditable(message.getBoolean("editable"));
}
final String[] textArray = message.getStringArray("text");
StringBuilder sb = new StringBuilder();
@ -374,9 +380,6 @@ public class SessionAccessibility {
if (message.getBoolean("clickable")) {
node.setClickable(true);
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
} else {
node.setClickable(false);
node.removeAction(AccessibilityNodeInfo.ACTION_CLICK);
}
final GeckoBundle bounds = message.getBundle("bounds");
@ -427,9 +430,7 @@ public class SessionAccessibility {
eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)) {
// In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
// it work with TalkBack.
if (mVirtualContentNode == null) {
mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
}
mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
populateNodeInfoFromJSON(mVirtualContentNode, message);
}

View File

@ -16,7 +16,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
class GeckoViewAccessibility extends GeckoViewModule {
onInit() {
EventDispatcher.instance.dispatch("GeckoView:AccessibilityReady");
EventDispatcher.instance.registerListener((aEvent, aData, aCallback) => {
if (aData.enabled) {
AccessFu.enable();