<lambda>null1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * Any copyright is dedicated to the Public Domain.
3 http://creativecommons.org/publicdomain/zero/1.0/ */
4
5 package org.mozilla.geckoview.test
6
7 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
8 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
9
10 import android.graphics.Rect
11
12 import android.os.Build
13 import android.os.Bundle
14 import android.os.SystemClock
15
16 import androidx.test.filters.MediumTest
17 import androidx.test.platform.app.InstrumentationRegistry
18 import androidx.test.ext.junit.runners.AndroidJUnit4
19
20 import android.text.InputType
21 import android.util.SparseLongArray
22
23 import android.view.accessibility.AccessibilityNodeInfo
24 import android.view.accessibility.AccessibilityNodeProvider
25 import android.view.accessibility.AccessibilityEvent
26 import android.view.accessibility.AccessibilityRecord
27 import android.view.View
28 import android.view.ViewGroup
29 import android.widget.EditText
30
31 import android.widget.FrameLayout
32
33 import org.hamcrest.Matchers.*
34 import org.junit.Assume.assumeThat
35 import org.junit.Test
36 import org.junit.Before
37 import org.junit.After
38 import org.junit.Ignore
39 import org.junit.runner.RunWith
40 import org.mozilla.geckoview.AllowOrDeny
41 import org.mozilla.geckoview.GeckoResult
42 import org.mozilla.geckoview.GeckoSession
43 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
44
45 const val DISPLAY_WIDTH = 480
46 const val DISPLAY_HEIGHT = 640
47
48 @RunWith(AndroidJUnit4::class)
49 @MediumTest
50 @WithDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
51 class AccessibilityTest : BaseSessionTest() {
52 lateinit var view: View
53 val screenRect = Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT)
54 val provider: AccessibilityNodeProvider get() = view.accessibilityNodeProvider
55 private val nodeInfos = mutableListOf<AccessibilityNodeInfo>()
56
57 // Given a child ID, return the virtual descendent ID.
58 private fun getVirtualDescendantId(childId: Long): Int {
59 try {
60 val getVirtualDescendantIdMethod =
61 AccessibilityNodeInfo::class.java.getMethod("getVirtualDescendantId", Long::class.java)
62 val virtualDescendantId = getVirtualDescendantIdMethod.invoke(null, childId) as Int
63 return if (virtualDescendantId == Int.MAX_VALUE) -1 else virtualDescendantId
64 } catch (ex: Exception) {
65 return 0
66 }
67 }
68
69 // Retrieve the virtual descendent ID of the event's source.
70 private fun getSourceId(event: AccessibilityEvent): Int {
71 try {
72 val getSourceIdMethod =
73 AccessibilityRecord::class.java.getMethod("getSourceNodeId")
74 return getVirtualDescendantId(getSourceIdMethod.invoke(event) as Long)
75 } catch (ex: Exception) {
76 return 0
77 }
78 }
79
80 private fun createNodeInfo(id: Int): AccessibilityNodeInfo {
81 val node = provider.createAccessibilityNodeInfo(id);
82 nodeInfos.add(node)
83 return node;
84 }
85
86 // Get a child ID by index.
87 private fun AccessibilityNodeInfo.getChildId(index: Int): Int =
88 getVirtualDescendantId(
89 if (Build.VERSION.SDK_INT >= 21)
90 AccessibilityNodeInfo::class.java.getMethod(
91 "getChildId", Int::class.java).invoke(this, index) as Long
92 else
93 (AccessibilityNodeInfo::class.java.getMethod("getChildNodeIds")
94 .invoke(this) as SparseLongArray).get(index))
95
96 private interface EventDelegate {
97 fun onAccessibilityFocused(event: AccessibilityEvent) { }
98 fun onAccessibilityFocusCleared(event: AccessibilityEvent) { }
99 fun onClicked(event: AccessibilityEvent) { }
100 fun onFocused(event: AccessibilityEvent) { }
101 fun onSelected(event: AccessibilityEvent) { }
102 fun onScrolled(event: AccessibilityEvent) { }
103 fun onTextSelectionChanged(event: AccessibilityEvent) { }
104 fun onTextChanged(event: AccessibilityEvent) { }
105 fun onTextTraversal(event: AccessibilityEvent) { }
106 fun onWinContentChanged(event: AccessibilityEvent) { }
107 fun onWinStateChanged(event: AccessibilityEvent) { }
108 fun onAnnouncement(event: AccessibilityEvent) { }
109 }
110
111 @Before fun setup() {
112 // We initialize a view with a parent and grandparent so that the
113 // accessibility events propagate up at least to the parent.
114 val context = InstrumentationRegistry.getInstrumentation().targetContext
115 view = FrameLayout(context)
116 FrameLayout(context).addView(view)
117 FrameLayout(context).addView(view.parent as View)
118
119 // Force on accessibility and assign the session's accessibility
120 // object a view.
121 sessionRule.runtime.settings.forceEnableAccessibility = true;
122 mainSession.accessibility.view = view
123
124 // Set up an external delegate that will intercept accessibility events.
125 sessionRule.addExternalDelegateUntilTestEnd(
126 EventDelegate::class,
127 { newDelegate -> (view.parent as View).setAccessibilityDelegate(object : View.AccessibilityDelegate() {
128 override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean {
129 when (event.eventType) {
130 AccessibilityEvent.TYPE_VIEW_FOCUSED -> newDelegate.onFocused(event)
131 AccessibilityEvent.TYPE_VIEW_CLICKED -> newDelegate.onClicked(event)
132 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
133 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED -> newDelegate.onAccessibilityFocusCleared(event)
134 AccessibilityEvent.TYPE_VIEW_SELECTED -> newDelegate.onSelected(event)
135 AccessibilityEvent.TYPE_VIEW_SCROLLED -> newDelegate.onScrolled(event)
136 AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> newDelegate.onTextSelectionChanged(event)
137 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> newDelegate.onTextChanged(event)
138 AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> newDelegate.onTextTraversal(event)
139 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> newDelegate.onWinContentChanged(event)
140 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> newDelegate.onWinStateChanged(event)
141 AccessibilityEvent.TYPE_ANNOUNCEMENT -> newDelegate.onAnnouncement(event)
142 else -> {}
143 }
144 return false
145 }
146 }) },
147 { (view.parent as View).setAccessibilityDelegate(null) },
148 object : EventDelegate { })
149 }
150
151 @After fun teardown() {
152 sessionRule.runtime.settings.forceEnableAccessibility = false
153 sessionRule.session.accessibility.view = null
154 nodeInfos.forEach { node -> node.recycle() }
155 }
156
157 private fun waitForInitialFocus(moveToFirstChild: Boolean = false) {
158 sessionRule.waitUntilCalled(object: GeckoSession.NavigationDelegate {
159 override fun onLoadRequest(session: GeckoSession,
160 request: GeckoSession.NavigationDelegate.LoadRequest)
161 : GeckoResult<AllowOrDeny>? {
162 return GeckoResult.allow()
163 }
164 })
165 // XXX: Sometimes we get the window state change of the initial
166 // about:blank page loading. Need to figure out how to ignore that.
167 sessionRule.waitUntilCalled(object : EventDelegate {
168 @AssertCalled(count = 1)
169 override fun onFocused(event: AccessibilityEvent) { }
170
171 @AssertCalled
172 override fun onWinStateChanged(event: AccessibilityEvent) { }
173
174 @AssertCalled
175 override fun onWinContentChanged(event: AccessibilityEvent) { }
176 })
177
178 if (moveToFirstChild) {
179 provider.performAction(View.NO_ID,
180 AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
181 }
182 }
183
184 @Test fun testRootNode() {
185 assertThat("provider is not null", provider, notNullValue())
186 val node = createNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID)
187 assertThat("Root node should have WebView class name",
188 node.className.toString(), equalTo("android.webkit.WebView"))
189 }
190
191 @Test fun testPageLoad() {
192 sessionRule.session.loadTestPath(INPUTS_PATH)
193
194 sessionRule.waitUntilCalled(object : EventDelegate {
195 @AssertCalled(count = 1)
196 override fun onFocused(event: AccessibilityEvent) { }
197 })
198 }
199
200 @Test fun testAccessibilityFocus() {
201 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
202 sessionRule.session.loadTestPath(INPUTS_PATH)
203 waitForInitialFocus(true)
204
205 sessionRule.waitUntilCalled(object : EventDelegate {
206 @AssertCalled(count = 1)
207 override fun onAccessibilityFocused(event: AccessibilityEvent) {
208 nodeId = getSourceId(event)
209 val node = createNodeInfo(nodeId)
210 assertThat("Label accessibility focused", node.className.toString(),
211 equalTo("android.view.View"))
212 assertThat("Text node should not be focusable", node.isFocusable, equalTo(false))
213 assertThat("Text node should be a11y focused", node.isAccessibilityFocused, equalTo(true))
214 assertThat("Text node should not be clickable", node.isClickable, equalTo(false))
215 }
216 })
217
218 provider.performAction(nodeId,
219 AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
220
221 sessionRule.waitUntilCalled(object : EventDelegate {
222 @AssertCalled(count = 1)
223 override fun onAccessibilityFocused(event: AccessibilityEvent) {
224 nodeId = getSourceId(event)
225 val node = createNodeInfo(nodeId)
226 assertThat("Editbox accessibility focused", node.className.toString(),
227 equalTo("android.widget.EditText"))
228 assertThat("Entry node should be focusable", node.isFocusable, equalTo(true))
229 assertThat("Entry node should be a11y focused", node.isAccessibilityFocused, equalTo(true))
230 assertThat("Entry node should be clickable", node.isClickable, equalTo(true))
231 }
232 })
233
234 provider.performAction(nodeId,
235 AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null)
236
237 sessionRule.waitUntilCalled(object : EventDelegate {
238 @AssertCalled(count = 1)
239 override fun onAccessibilityFocusCleared(event: AccessibilityEvent) {
240 assertThat("Accessibility focused node is now cleared", getSourceId(event), equalTo(nodeId))
241 val node = createNodeInfo(nodeId)
242 assertThat("Entry node should node be a11y focused", node.isAccessibilityFocused, equalTo(false))
243 }
244 })
245 }
246
247 fun loadTestPage(page: String) {
248 sessionRule.session.loadTestPath("/assets/www/accessibility/$page.html")
249 }
250
251 @Test fun testTextEntryNode() {
252 loadTestPage("test-text-entry-node")
253 waitForInitialFocus()
254
255 mainSession.evaluateJS("document.querySelector('input[aria-label=Name]').focus()")
256
257 sessionRule.waitUntilCalled(object : EventDelegate {
258 @AssertCalled(count = 1)
259 override fun onFocused(event: AccessibilityEvent) {
260 val nodeId = getSourceId(event)
261 val node = createNodeInfo(nodeId)
262 assertThat("Focused EditBox", node.className.toString(),
263 equalTo("android.widget.EditText"))
264 if (Build.VERSION.SDK_INT >= 19) {
265 assertThat("Hint has field name",
266 node.extras.getString("AccessibilityNodeInfo.hint"),
267 equalTo("Name description"))
268 }
269 }
270 })
271
272 mainSession.evaluateJS("document.querySelector('input[aria-label=Last]').focus()")
273
274 sessionRule.waitUntilCalled(object : EventDelegate {
275 @AssertCalled(count = 1)
276 override fun onFocused(event: AccessibilityEvent) {
277 val nodeId = getSourceId(event)
278 val node = createNodeInfo(nodeId)
279 assertThat("Focused EditBox", node.className.toString(),
280 equalTo("android.widget.EditText"))
281 if (Build.VERSION.SDK_INT >= 19) {
282 assertThat("Hint has field name",
283 node.extras.getString("AccessibilityNodeInfo.hint"),
284 equalTo("Last, required"))
285 }
286 }
287 })
288 }
289
290 @Test fun testMoveCaretAccessibilityFocus() {
291 loadTestPage("test-move-caret-accessibility-focus")
292 waitForInitialFocus(false)
293
294 mainSession.evaluateJS("""
295 this.select = function select(node, start, end) {
296 let r = new Range();
297 r.setStart(node, start);
298 r.setEnd(node, end);
299 let s = getSelection();
300 s.removeAllRanges();
301 s.addRange(r);
302 };
303 this.select(document.querySelector('p').childNodes[2], 2, 6);
304 """.trimIndent())
305
306 sessionRule.waitUntilCalled(object : EventDelegate {
307 @AssertCalled(count = 1)
308 override fun onAccessibilityFocused(event: AccessibilityEvent) {
309 val node = createNodeInfo(getSourceId(event))
310 assertThat("Text node should match text", node.text as String, equalTo(", sweet "))
311 }
312 })
313
314 mainSession.evaluateJS("""
315 this.select(document.querySelector('p').lastElementChild.firstChild, 1, 2);
316 """.trimIndent())
317
318 sessionRule.waitUntilCalled(object : EventDelegate {
319 @AssertCalled(count = 1)
320 override fun onAccessibilityFocused(event: AccessibilityEvent) {
321 val node = createNodeInfo(getSourceId(event))
322 assertThat("Text node should match text", node.text as String, equalTo("world"))
323 }
324 })
325
326 mainSession.finder.find("sweet", 0)
327 sessionRule.waitUntilCalled(object : EventDelegate {
328 @AssertCalled(count = 1)
329 override fun onAccessibilityFocused(event: AccessibilityEvent) {
330 val node = createNodeInfo(getSourceId(event))
331 assertThat("Text node should match text", node.contentDescription as String, equalTo("sweet"))
332 }
333 })
334
335 // reset caret position
336 mainSession.evaluateJS("""
337 this.select(document.body, 0, 0);
338 """.trimIndent())
339 sessionRule.waitUntilCalled(object : EventDelegate {
340 @AssertCalled(count = 1)
341 override fun onFocused(event: AccessibilityEvent) {}
342 })
343
344 mainSession.finder.find("Hell", 0)
345 sessionRule.waitUntilCalled(object : EventDelegate {
346 @AssertCalled(count = 1)
347 override fun onAccessibilityFocused(event: AccessibilityEvent) {
348 val node = createNodeInfo(getSourceId(event))
349 assertThat("Text node should match text", node.text as String, equalTo("Hello "))
350 }
351 })
352 }
353
354 private fun waitUntilTextSelectionChanged(fromIndex: Int, toIndex: Int) {
355 var eventFromIndex = 0;
356 var eventToIndex = 0;
357 do {
358 sessionRule.waitUntilCalled(object : EventDelegate {
359 override fun onTextSelectionChanged(event: AccessibilityEvent) {
360 eventFromIndex = event.fromIndex;
361 eventToIndex = event.toIndex;
362 }
363 })
364 } while (fromIndex != eventFromIndex || toIndex != eventToIndex)
365 }
366
367 private fun waitUntilTextTraversed(fromIndex: Int, toIndex: Int,
368 expectedNode: Int? = null): Int {
369 var nodeId: Int = AccessibilityNodeProvider.HOST_VIEW_ID
370 sessionRule.waitUntilCalled(object : EventDelegate {
371 @AssertCalled(count = 1)
372 override fun onTextTraversal(event: AccessibilityEvent) {
373 nodeId = getSourceId(event)
374 if (expectedNode != null) {
375 assertThat("Node matches", nodeId, equalTo(expectedNode))
376 }
377 assertThat("fromIndex matches", event.fromIndex, equalTo(fromIndex))
378 assertThat("toIndex matches", event.toIndex, equalTo(toIndex))
379 }
380 })
381 return nodeId
382 }
383
384 private fun waitUntilClick(checked: Boolean) {
385 sessionRule.waitUntilCalled(object : EventDelegate {
386 @AssertCalled(count = 1)
387 override fun onClicked(event: AccessibilityEvent) {
388 var nodeId = getSourceId(event)
389 var node = createNodeInfo(nodeId)
390 assertThat("Event's checked state matches", event.isChecked, equalTo(checked))
391 assertThat("Checkbox node has correct checked state", node.isChecked, equalTo(checked))
392 }
393 })
394 }
395
396 private fun waitUntilSelect(selected: Boolean) {
397 sessionRule.waitUntilCalled(object : EventDelegate {
398 @AssertCalled(count = 1)
399 override fun onSelected(event: AccessibilityEvent) {
400 var nodeId = getSourceId(event)
401 var node = createNodeInfo(nodeId)
402 assertThat("Selectable node has correct selected state", node.isSelected, equalTo(selected))
403 }
404 })
405 }
406
407 private fun setSelectionArguments(start: Int, end: Int): Bundle {
408 val arguments = Bundle(2)
409 arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, start)
410 arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, end)
411 return arguments
412 }
413
414 private fun moveByGranularityArguments(granularity: Int, extendSelection: Boolean = false): Bundle {
415 val arguments = Bundle(2)
416 arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, granularity)
417 arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, extendSelection)
418 return arguments
419 }
420
421 @Test fun testClipboard() {
422 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
423 loadTestPage("test-clipboard")
424 waitForInitialFocus()
425
426 mainSession.evaluateJS("document.querySelector('input').focus()")
427
428 sessionRule.waitUntilCalled(object : EventDelegate {
429 @AssertCalled(count = 1)
430 override fun onFocused(event: AccessibilityEvent) {
431 nodeId = getSourceId(event)
432 val node = createNodeInfo(nodeId)
433 assertThat("Focused EditBox", node.className.toString(),
434 equalTo("android.widget.EditText"))
435 }
436
437 @AssertCalled(count = 1)
438 override fun onTextSelectionChanged(event: AccessibilityEvent) {
439 assertThat("fromIndex should be at start", event.fromIndex, equalTo(0))
440 assertThat("toIndex should be at start", event.toIndex, equalTo(0))
441 }
442 })
443
444 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(5, 11))
445 waitUntilTextSelectionChanged(5, 11)
446
447 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_COPY, null)
448
449 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(11, 11))
450 waitUntilTextSelectionChanged(11, 11)
451
452 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PASTE, null)
453 sessionRule.waitUntilCalled(object : EventDelegate {
454 @AssertCalled(count = 1)
455 override fun onTextChanged(event: AccessibilityEvent) {
456 assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel world"))
457 }
458 })
459
460 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(17, 23))
461 waitUntilTextSelectionChanged(17, 23)
462
463 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PASTE, null)
464 sessionRule.waitUntilCalled(object : EventDelegate {
465 @AssertCalled
466 override fun onTextChanged(event: AccessibilityEvent) {
467 assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel cruel"))
468 }
469 })
470
471 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(0, 0))
472 waitUntilTextSelectionChanged(0, 0)
473
474 provider.performAction(nodeId,
475 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
476 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD, true))
477 waitUntilTextSelectionChanged(0, 5)
478
479 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CUT, null)
480 sessionRule.waitUntilCalled(object : EventDelegate {
481 @AssertCalled
482 override fun onTextChanged(event: AccessibilityEvent) {
483 assertThat("text should be cut", event.text[0].toString(), equalTo(" cruel cruel cruel"))
484 }
485 })
486 }
487
488 @Test fun testMoveByCharacter() {
489 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
490 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
491 waitForInitialFocus(true)
492
493 sessionRule.waitUntilCalled(object : EventDelegate {
494 @AssertCalled(count = 1)
495 override fun onAccessibilityFocused(event: AccessibilityEvent) {
496 nodeId = getSourceId(event)
497 val node = createNodeInfo(nodeId)
498 assertThat("Accessibility focus on first text leaf", node.text as String, startsWith("Lorem ipsum"))
499 }
500 })
501
502 provider.performAction(nodeId,
503 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
504 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
505 waitUntilTextTraversed(0, 1, nodeId) // "L"
506
507 provider.performAction(nodeId,
508 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
509 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
510 waitUntilTextTraversed(1, 2, nodeId) // "o"
511
512 provider.performAction(nodeId,
513 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
514 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
515 waitUntilTextTraversed(0, 1, nodeId) // "L"
516 }
517
518 @Test fun testMoveByWord() {
519 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
520 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
521 waitForInitialFocus(true)
522
523 sessionRule.waitUntilCalled(object : EventDelegate {
524 @AssertCalled(count = 1)
525 override fun onAccessibilityFocused(event: AccessibilityEvent) {
526 nodeId = getSourceId(event)
527 val node = createNodeInfo(nodeId)
528 assertThat("Accessibility focus on first text leaf", node.text as String, startsWith("Lorem ipsum"))
529 }
530 })
531
532 provider.performAction(nodeId,
533 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
534 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
535 waitUntilTextTraversed(0, 5, nodeId) // "Lorem"
536
537 provider.performAction(nodeId,
538 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
539 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
540 waitUntilTextTraversed(6, 11, nodeId) // "ipsum"
541
542 provider.performAction(nodeId,
543 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
544 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
545 waitUntilTextTraversed(0, 5, nodeId) // "Lorem"
546 }
547
548 @Test fun testMoveByLine() {
549 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
550 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
551 waitForInitialFocus(true)
552
553 sessionRule.waitUntilCalled(object : EventDelegate {
554 @AssertCalled(count = 1)
555 override fun onAccessibilityFocused(event: AccessibilityEvent) {
556 nodeId = getSourceId(event)
557 val node = createNodeInfo(nodeId)
558 assertThat("Accessibility focus on first text leaf", node.text as String, startsWith("Lorem ipsum"))
559 }
560 })
561
562 provider.performAction(nodeId,
563 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
564 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
565 waitUntilTextTraversed(0, 18, nodeId) // "Lorem ipsum dolor "
566
567 provider.performAction(nodeId,
568 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
569 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
570 waitUntilTextTraversed(18, 28, nodeId) // "sit amet, "
571
572 provider.performAction(nodeId,
573 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
574 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
575 waitUntilTextTraversed(0, 18, nodeId) // "Lorem ipsum dolor "
576 }
577
578 @Test fun testMoveByCharacterAtEdges() {
579 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
580 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
581 waitForInitialFocus()
582
583 // Move to the first link containing "anim id".
584 val bundle = Bundle()
585 bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "LINK")
586 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
587 sessionRule.waitUntilCalled(object : EventDelegate {
588 @AssertCalled(count = 1)
589 override fun onAccessibilityFocused(event: AccessibilityEvent) {
590 nodeId = getSourceId(event)
591 val node = createNodeInfo(nodeId)
592 assertThat("Accessibility focus on link", node.contentDescription as String, startsWith("anim id"))
593 }
594 })
595
596 var success: Boolean
597 // Navigate forward through "anim id" character by character.
598 for (start in 0..6) {
599 success = provider.performAction(nodeId,
600 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
601 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
602 assertThat("Next char should succeed", success, equalTo(true))
603 waitUntilTextTraversed(start, start + 1, nodeId)
604 }
605
606 // Try to navigate forward past end.
607 success = provider.performAction(nodeId,
608 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
609 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
610 assertThat("Next char should fail at end", success, equalTo(false))
611
612 // We're already on "d". Navigate backward through "anim i".
613 for (start in 5 downTo 0) {
614 success = provider.performAction(nodeId,
615 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
616 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
617 assertThat("Prev char should succeed", success, equalTo(true))
618 waitUntilTextTraversed(start, start + 1, nodeId)
619 }
620
621 // Try to navigate backward past start.
622 success = provider.performAction(nodeId,
623 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
624 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
625 assertThat("Prev char should fail at start", success, equalTo(false))
626 }
627
628 @Test fun testMoveByWordAtEdges() {
629 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
630 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
631 waitForInitialFocus()
632
633 // Move to the first link containing "anim id".
634 val bundle = Bundle()
635 bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "LINK")
636 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
637 sessionRule.waitUntilCalled(object : EventDelegate {
638 @AssertCalled(count = 1)
639 override fun onAccessibilityFocused(event: AccessibilityEvent) {
640 nodeId = getSourceId(event)
641 val node = createNodeInfo(nodeId)
642 assertThat("Accessibility focus on link", node.contentDescription as String, startsWith("anim id"))
643 }
644 })
645
646 var success: Boolean
647 success = provider.performAction(nodeId,
648 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
649 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
650 assertThat("Next word should succeed", success, equalTo(true))
651 waitUntilTextTraversed(0, 4, nodeId) // "anim"
652
653 success = provider.performAction(nodeId,
654 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
655 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
656 assertThat("Next word should succeed", success, equalTo(true))
657 waitUntilTextTraversed(5, 7, nodeId) // "id"
658
659 // Try to navigate forward past end.
660 success = provider.performAction(nodeId,
661 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
662 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
663 assertThat("Next word should fail at end", success, equalTo(false))
664
665 success = provider.performAction(nodeId,
666 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
667 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
668 assertThat("Prev word should succeed", success, equalTo(true))
669 waitUntilTextTraversed(0, 4, nodeId) // "anim"
670
671 // Try to navigate backward past start.
672 success = provider.performAction(nodeId,
673 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
674 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
675 assertThat("Prev word should fail at start", success, equalTo(false))
676 }
677
678 @Test fun testMoveAtEndOfTextTrailingWhitespace() {
679 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
680 sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
681 waitForInitialFocus(true)
682
683 sessionRule.waitUntilCalled(object : EventDelegate {
684 @AssertCalled(count = 1)
685 override fun onAccessibilityFocused(event: AccessibilityEvent) {
686 nodeId = getSourceId(event)
687 val node = createNodeInfo(nodeId)
688 assertThat("Accessibility focus on first text leaf", node.text as String, startsWith("Lorem ipsum"))
689 }
690 })
691
692 // Initial move backward to move to last word.
693 var success = provider.performAction(nodeId,
694 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
695 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
696 assertThat("Prev word should succeed", success, equalTo(true))
697 waitUntilTextTraversed(418, 424, nodeId) // "mollit"
698
699 // Try to move forward past last word.
700 success = provider.performAction(nodeId,
701 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
702 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
703 assertThat("Next word should fail at last word", success, equalTo(false))
704
705 // Move forward by character (onto trailing space).
706 success = provider.performAction(nodeId,
707 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
708 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
709 assertThat("Next char should succeed", success, equalTo(true))
710 waitUntilTextTraversed(424, 425, nodeId) // " "
711
712 // Try to move forward past last character.
713 success = provider.performAction(nodeId,
714 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
715 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
716 assertThat("Next char should fail at last char", success, equalTo(false))
717 }
718
719 @Test fun testHeadings() {
720 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
721 loadTestPage("test-headings")
722 waitForInitialFocus()
723
724 val bundle = Bundle()
725 bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "HEADING")
726
727 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
728 sessionRule.waitUntilCalled(object : EventDelegate {
729 @AssertCalled(count = 1)
730 override fun onAccessibilityFocused(event: AccessibilityEvent) {
731 nodeId = getSourceId(event)
732 val node = createNodeInfo(nodeId)
733 assertThat("Accessibility focus on first heading", node.contentDescription as String, startsWith("Fried cheese"))
734 if (Build.VERSION.SDK_INT >= 19) {
735 assertThat("First heading is level 1",
736 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
737 equalTo("heading level 1"))
738 }
739 }
740 })
741
742 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
743 sessionRule.waitUntilCalled(object : EventDelegate {
744 @AssertCalled(count = 1)
745 override fun onAccessibilityFocused(event: AccessibilityEvent) {
746 nodeId = getSourceId(event)
747 val node = createNodeInfo(nodeId)
748 assertThat("Accessibility focus on second heading", node.contentDescription as String, startsWith("Popcorn shrimp"))
749 if (Build.VERSION.SDK_INT >= 19) {
750 assertThat("Second heading is level 2",
751 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
752 equalTo("heading level 2"))
753 }
754 }
755 })
756
757 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
758 sessionRule.waitUntilCalled(object : EventDelegate {
759 @AssertCalled(count = 1)
760 override fun onAccessibilityFocused(event: AccessibilityEvent) {
761 nodeId = getSourceId(event)
762 val node = createNodeInfo(nodeId)
763 assertThat("Accessibility focus on second heading", node.contentDescription as String, startsWith("Chicken fingers"))
764 if (Build.VERSION.SDK_INT >= 19) {
765 assertThat("Third heading is level 3",
766 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
767 equalTo("heading level 3"))
768 }
769 }
770 })
771 }
772
773 @Test fun testCheckbox() {
774 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
775 loadTestPage("test-checkbox")
776 waitForInitialFocus(true)
777
778 sessionRule.waitUntilCalled(object : EventDelegate {
779 @AssertCalled(count = 1)
780 override fun onAccessibilityFocused(event: AccessibilityEvent) {
781 nodeId = getSourceId(event)
782 var node = createNodeInfo(nodeId)
783 assertThat("Checkbox node is checkable", node.isCheckable, equalTo(true))
784 assertThat("Checkbox node is clickable", node.isClickable, equalTo(true))
785 assertThat("Checkbox node is focusable", node.isFocusable, equalTo(true))
786 assertThat("Checkbox node is not checked", node.isChecked, equalTo(false))
787 assertThat("Checkbox node has correct role", node.text.toString(), equalTo("many option"))
788 if (Build.VERSION.SDK_INT >= 19) {
789 assertThat("Hint has description", node.extras.getString("AccessibilityNodeInfo.hint"),
790 equalTo("description"))
791 }
792
793 }
794 })
795
796 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
797 waitUntilClick(true)
798
799 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
800 waitUntilClick(false)
801 }
802
803 @Test fun testExpandable() {
804 var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
805 loadTestPage("test-expandable")
806 waitForInitialFocus(true)
807
808 sessionRule.waitUntilCalled(object : EventDelegate {
809 @AssertCalled(count = 1)
810 override fun onAccessibilityFocused(event: AccessibilityEvent) {
811 nodeId = getSourceId(event)
812 if (Build.VERSION.SDK_INT >= 21) {
813 val node = createNodeInfo(nodeId)
814 assertThat("button is expandable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND))
815 assertThat("button is not collapsable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE)))
816 }
817 }
818 })
819
820 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_EXPAND, null)
821 sessionRule.waitUntilCalled(object : EventDelegate {
822 @AssertCalled(count = 1)
823 override fun onClicked(event: AccessibilityEvent) {
824 assertThat("Clicked event is from same node", getSourceId(event), equalTo(nodeId))
825 if (Build.VERSION.SDK_INT >= 21) {
826 val node = createNodeInfo(nodeId)
827 assertThat("button is collapsable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE))
828 assertThat("button is not expandable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND)))
829 }
830 }
831 })
832
833 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_COLLAPSE, null)
834 sessionRule.waitUntilCalled(object : EventDelegate {
835 @AssertCalled(count = 1)
836 override fun onClicked(event: AccessibilityEvent) {
837 assertThat("Clicked event is from same node", getSourceId(event), equalTo(nodeId))
838 if (Build.VERSION.SDK_INT >= 21) {
839 val node = createNodeInfo(nodeId)
840 assertThat("button is expandable", node.actionList, hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND))
841 assertThat("button is not collapsable", node.actionList, not(hasItem(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE)))
842 }
843 }
844 })
845 }
846
847 @Test fun testSelectable() {
848 var nodeId = View.NO_ID
849 loadTestPage("test-selectable")
850 waitForInitialFocus(true)
851
852 sessionRule.waitUntilCalled(object : EventDelegate {
853 @AssertCalled(count = 1)
854 override fun onAccessibilityFocused(event: AccessibilityEvent) {
855 nodeId = getSourceId(event)
856 var node = createNodeInfo(nodeId)
857 assertThat("Selectable node is clickable", node.isClickable, equalTo(true))
858 assertThat("Selectable node is not selected", node.isSelected, equalTo(false))
859 assertThat("Selectable node has correct text", node.text.toString(), equalTo("1"))
860 }
861 })
862
863 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
864 waitUntilSelect(true)
865
866 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
867 waitUntilSelect(false)
868
869 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SELECT, null)
870 waitUntilSelect(true)
871
872 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SELECT, null)
873 waitUntilSelect(false)
874 }
875
876 @Test fun testMutation() {
877 loadTestPage("test-mutation")
878 waitForInitialFocus()
879
880 val rootNode = createNodeInfo(View.NO_ID)
881 assertThat("Document has 1 child", rootNode.childCount, equalTo(1))
882
883 assertThat("Section has 1 child",
884 createNodeInfo(rootNode.getChildId(0)).childCount, equalTo(1))
885 mainSession.evaluateJS("document.querySelector('#to_show').style.display = 'none';")
886 sessionRule.waitUntilCalled(object : EventDelegate {
887 @AssertCalled(count = 0)
888 override fun onAnnouncement(event: AccessibilityEvent) { }
889
890 @AssertCalled(count = 1)
891 override fun onWinContentChanged(event: AccessibilityEvent) { }
892 })
893
894 assertThat("Section has no children",
895 createNodeInfo(rootNode.getChildId(0)).childCount, equalTo(0))
896 }
897
898 @Test fun testLiveRegion() {
899 loadTestPage("test-live-region")
900 waitForInitialFocus()
901
902 mainSession.evaluateJS("document.querySelector('#to_change').textContent = 'Hello';")
903 sessionRule.waitUntilCalled(object : EventDelegate {
904 @AssertCalled(count = 1)
905 override fun onAnnouncement(event: AccessibilityEvent) {
906 assertThat("Announcement is correct", event.text[0].toString(), equalTo("Hello"))
907 }
908
909 @AssertCalled(count = 1)
910 override fun onWinContentChanged(event: AccessibilityEvent) { }
911 })
912 }
913
914 @Test fun testLiveRegionDescendant() {
915 loadTestPage("test-live-region-descendant")
916 waitForInitialFocus()
917
918 mainSession.evaluateJS("document.querySelector('#to_show').style.display = 'none';")
919 sessionRule.waitUntilCalled(object : EventDelegate {
920 @AssertCalled(count = 0)
921 override fun onAnnouncement(event: AccessibilityEvent) { }
922
923 @AssertCalled(count = 1)
924 override fun onWinContentChanged(event: AccessibilityEvent) { }
925 })
926
927 mainSession.evaluateJS("document.querySelector('#to_show').style.display = 'block';")
928 sessionRule.waitUntilCalled(object : EventDelegate {
929 @AssertCalled(count = 1)
930 override fun onAnnouncement(event: AccessibilityEvent) {
931 assertThat("Announcement is correct", event.text[0].toString(), equalTo("I will be shown"))
932 }
933
934 @AssertCalled(count = 1)
935 override fun onWinContentChanged(event: AccessibilityEvent) { }
936 })
937 }
938
939 @Test fun testLiveRegionAtomic() {
940 loadTestPage("test-live-region-atomic")
941 waitForInitialFocus()
942
943 mainSession.evaluateJS("document.querySelector('p').textContent = '4pm';")
944 sessionRule.waitUntilCalled(object : EventDelegate {
945 @AssertCalled(count = 1)
946 override fun onAnnouncement(event: AccessibilityEvent) {
947 assertThat("Announcement is correct", event.text[0].toString(), equalTo("The time is 4pm"))
948 }
949
950 @AssertCalled(count = 1)
951 override fun onWinContentChanged(event: AccessibilityEvent) { }
952 })
953
954 mainSession.evaluateJS("document.querySelector('#container').removeAttribute('aria-atomic');" +
955 "document.querySelector('p').textContent = '5pm';")
956 sessionRule.waitUntilCalled(object : EventDelegate {
957 @AssertCalled(count = 1)
958 override fun onAnnouncement(event: AccessibilityEvent) {
959 assertThat("Announcement is correct", event.text[0].toString(), equalTo("5pm"))
960 }
961
962 @AssertCalled(count = 1)
963 override fun onWinContentChanged(event: AccessibilityEvent) { }
964 })
965 }
966
967 @Test fun testLiveRegionImage() {
968 loadTestPage("test-live-region-image")
969 waitForInitialFocus()
970
971 mainSession.evaluateJS("document.querySelector('img').alt = 'sad';")
972 sessionRule.waitUntilCalled(object : EventDelegate {
973 @AssertCalled(count = 1)
974 override fun onAnnouncement(event: AccessibilityEvent) {
975 assertThat("Announcement is correct", event.text[0].toString(), equalTo("This picture is sad"))
976 }
977 })
978 }
979
980 @Test fun testLiveRegionImageLabeledBy() {
981 loadTestPage("test-live-region-image-labeled-by")
982 waitForInitialFocus()
983
984 mainSession.evaluateJS("document.querySelector('img').setAttribute('aria-labelledby', 'l2');")
985 sessionRule.waitUntilCalled(object : EventDelegate {
986 @AssertCalled(count = 1)
987 override fun onAnnouncement(event: AccessibilityEvent) {
988 assertThat("Announcement is correct", event.text[0].toString(), equalTo("Goodbye"))
989 }
990 })
991 }
992
993 private fun screenContainsNode(nodeId: Int): Boolean {
994 var node = createNodeInfo(nodeId)
995 var nodeBounds = Rect()
996 node.getBoundsInScreen(nodeBounds)
997 return screenRect.contains(nodeBounds)
998 }
999
1000 @Ignore // Bug 1506276 - We need to reliably wait for APZC here, and it's not trivial.
1001 @Test fun testScroll() {
1002 var nodeId = View.NO_ID
1003 loadTestPage("test-scroll.html")
1004
1005 sessionRule.waitUntilCalled(object : EventDelegate {
1006 @AssertCalled
1007 override fun onWinStateChanged(event: AccessibilityEvent) { }
1008
1009 @AssertCalled(count = 1)
1010 @Suppress("deprecation")
1011 override fun onFocused(event: AccessibilityEvent) {
1012 nodeId = getSourceId(event)
1013 var node = createNodeInfo(nodeId)
1014 var nodeBounds = Rect()
1015 node.getBoundsInParent(nodeBounds)
1016 assertThat("Default root node bounds are correct", nodeBounds, equalTo(screenRect))
1017 }
1018 })
1019
1020 provider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1021 sessionRule.waitUntilCalled(object : EventDelegate {
1022 @AssertCalled(count = 1, order = [1])
1023 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1024 nodeId = getSourceId(event)
1025 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
1026 }
1027
1028 @AssertCalled(count = 1, order = [2])
1029 override fun onScrolled(event: AccessibilityEvent) {
1030 assertThat("View is scrolled for focused node to be onscreen", event.scrollY, greaterThan(0))
1031 assertThat("View is not scrolled to the end", event.scrollY, lessThan(event.maxScrollY))
1032 }
1033
1034 @AssertCalled(count = 1, order = [3])
1035 override fun onWinContentChanged(event: AccessibilityEvent) {
1036 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
1037 }
1038 })
1039
1040 SystemClock.sleep(100);
1041 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null)
1042 sessionRule.waitUntilCalled(object : EventDelegate {
1043 @AssertCalled(count = 1, order = [1])
1044 override fun onScrolled(event: AccessibilityEvent) {
1045 assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
1046 }
1047
1048 @AssertCalled(count = 1, order = [2])
1049 override fun onWinContentChanged(event: AccessibilityEvent) {
1050 assertThat("Focused node is still onscreen", screenContainsNode(nodeId), equalTo(true))
1051 }
1052 })
1053
1054 SystemClock.sleep(100)
1055 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null)
1056 sessionRule.waitUntilCalled(object : EventDelegate {
1057 @AssertCalled(count = 1, order = [1])
1058 override fun onScrolled(event: AccessibilityEvent) {
1059 assertThat("View is scrolled to the beginning", event.scrollY, equalTo(0))
1060 }
1061
1062 @AssertCalled(count = 1, order = [2])
1063 override fun onWinContentChanged(event: AccessibilityEvent) {
1064 assertThat("Focused node is offscreen", screenContainsNode(nodeId), equalTo(false))
1065 }
1066 })
1067
1068 SystemClock.sleep(100)
1069 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1070 sessionRule.waitUntilCalled(object : EventDelegate {
1071 @AssertCalled(count = 1, order = [1])
1072 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1073 nodeId = getSourceId(event)
1074 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
1075 }
1076
1077 @AssertCalled(count = 1, order = [2])
1078 override fun onScrolled(event: AccessibilityEvent) {
1079 assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
1080 }
1081
1082 @AssertCalled(count = 1, order = [3])
1083 override fun onWinContentChanged(event: AccessibilityEvent) {
1084 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
1085 }
1086 })
1087 }
1088
1089 @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
1090 @Test fun autoFill() {
1091 // Wait for the accessibility nodes to populate.
1092 mainSession.loadTestPath(FORMS_HTML_PATH)
1093 waitForInitialFocus()
1094
1095 val autoFills = mapOf(
1096 "#user1" to "bar", "#pass1" to "baz", "#user2" to "bar", "#pass2" to "baz") +
1097 if (Build.VERSION.SDK_INT >= 19) mapOf(
1098 "#email1" to "a@b.c", "#number1" to "24", "#tel1" to "42")
1099 else mapOf(
1100 "#email1" to "bar", "#number1" to "", "#tel1" to "bar")
1101
1102 // Set up promises to monitor the values changing.
1103 val promises = autoFills.flatMap { entry ->
1104 // Repeat each test with both the top document and the iframe document.
1105 arrayOf("document", "document.querySelector('#iframe').contentDocument").map { doc ->
1106 mainSession.evaluatePromiseJS("""new Promise(resolve =>
1107 $doc.querySelector('${entry.key}').addEventListener(
1108 'input', event => {
1109 let eventInterface =
1110 event instanceof $doc.defaultView.InputEvent ? "InputEvent" :
1111 event instanceof $doc.defaultView.UIEvent ? "UIEvent" :
1112 event instanceof $doc.defaultView.Event ? "Event" : "Unknown";
1113 resolve([event.target.value, '${entry.value}', eventInterface]);
1114 }, { once: true }))""")
1115 }
1116 }
1117
1118 // Perform auto-fill and return number of auto-fills performed.
1119 fun autoFillChild(id: Int, child: AccessibilityNodeInfo) {
1120 // Seal the node info instance so we can perform actions on it.
1121 if (child.childCount > 0) {
1122 for (i in 0 until child.childCount) {
1123 val childId = child.getChildId(i)
1124 autoFillChild(childId, createNodeInfo(childId))
1125 }
1126 }
1127
1128 if (EditText::class.java.name == child.className) {
1129 assertThat("Input should be enabled", child.isEnabled, equalTo(true))
1130 assertThat("Input should be focusable", child.isFocusable, equalTo(true))
1131 if (Build.VERSION.SDK_INT >= 19) {
1132 assertThat("Password type should match", child.isPassword, equalTo(
1133 child.inputType == InputType.TYPE_CLASS_TEXT or
1134 InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD))
1135 }
1136
1137 val args = Bundle(1)
1138 val value = if (child.isPassword) "baz" else
1139 if (Build.VERSION.SDK_INT < 19) "bar" else
1140 when (child.inputType) {
1141 InputType.TYPE_CLASS_TEXT or
1142 InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "a@b.c"
1143 InputType.TYPE_CLASS_NUMBER -> "24"
1144 InputType.TYPE_CLASS_PHONE -> "42"
1145 else -> "bar"
1146 }
1147
1148 val ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = if (Build.VERSION.SDK_INT >= 21)
1149 AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE else
1150 "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"
1151 val ACTION_SET_TEXT = if (Build.VERSION.SDK_INT >= 21)
1152 AccessibilityNodeInfo.ACTION_SET_TEXT else 0x200000
1153
1154 args.putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
1155 assertThat("Can perform auto-fill",
1156 provider.performAction(id, ACTION_SET_TEXT, args), equalTo(true))
1157 }
1158 }
1159
1160 autoFillChild(View.NO_ID, createNodeInfo(View.NO_ID))
1161
1162 // Wait on the promises and check for correct values.
1163 for ((actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
1164 assertThat("Auto-filled value must match", actual, equalTo(expected))
1165 assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
1166 }
1167 }
1168
1169 @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
1170 @Test fun autoFill_navigation() {
1171 // Fails with BFCache in the parent.
1172 // https://bugzilla.mozilla.org/show_bug.cgi?id=1715480
1173 sessionRule.setPrefsUntilTestEnd(mapOf(
1174 "fission.bfcacheInParent" to false))
1175 // disable test on debug for frequently failing #Bug 1505353
1176 assumeThat(sessionRule.env.isDebugBuild, equalTo(false))
1177 fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
1178 { it.className == "android.widget.EditText" },
1179 id: Int = View.NO_ID): Int {
1180 val info = createNodeInfo(id)
1181 return (if (cond(info) && info.className != "android.webkit.WebView" ) 1 else 0) + (if (info.childCount > 0)
1182 (0 until info.childCount).sumBy {
1183 countAutoFillNodes(cond, info.getChildId(it))
1184 } else 0)
1185 }
1186
1187 // Wait for the accessibility nodes to populate.
1188 mainSession.loadTestPath(FORMS_HTML_PATH)
1189 waitForInitialFocus()
1190
1191 assertThat("Initial auto-fill count should match",
1192 countAutoFillNodes(), equalTo(18))
1193 assertThat("Password auto-fill count should match",
1194 countAutoFillNodes({ it.isPassword }), equalTo(4))
1195
1196 // Now wait for the nodes to clear.
1197 mainSession.loadTestPath(HELLO_HTML_PATH)
1198 waitForInitialFocus()
1199 assertThat("Should not have auto-fill fields",
1200 countAutoFillNodes(), equalTo(0))
1201
1202 // Now wait for the nodes to reappear.
1203 mainSession.goBack()
1204 waitForInitialFocus()
1205 assertThat("Should have auto-fill fields again",
1206 countAutoFillNodes(), equalTo(18))
1207 assertThat("Should not have focused field",
1208 countAutoFillNodes({ it.isFocused }), equalTo(0))
1209
1210 mainSession.evaluateJS("document.querySelector('#pass1').focus()")
1211 sessionRule.waitUntilCalled(object : EventDelegate {
1212 @AssertCalled
1213 override fun onFocused(event: AccessibilityEvent) {
1214 }
1215 })
1216 assertThat("Should have one focused field",
1217 countAutoFillNodes({ it.isFocused }), equalTo(1))
1218
1219 mainSession.evaluateJS("document.querySelector('#pass1').blur()")
1220 sessionRule.waitUntilCalled(object : EventDelegate {
1221 @AssertCalled
1222 override fun onFocused(event: AccessibilityEvent) {
1223 }
1224 })
1225 assertThat("Should not have focused field",
1226 countAutoFillNodes({ it.isFocused }), equalTo(0))
1227 }
1228
1229 @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
1230 @Test fun testTree() {
1231 loadTestPage("test-tree")
1232 waitForInitialFocus()
1233
1234 val rootNode = createNodeInfo(View.NO_ID)
1235 assertThat("Document has 3 children", rootNode.childCount, equalTo(3))
1236
1237 val labelNode = createNodeInfo(rootNode.getChildId(0))
1238 assertThat("First node is a label", labelNode.className.toString(), equalTo("android.view.View"))
1239 assertThat("Label has text", labelNode.text.toString(), equalTo("Name:"))
1240
1241 val entryNode = createNodeInfo(rootNode.getChildId(1))
1242 assertThat("Second node is an entry", entryNode.className.toString(), equalTo("android.widget.EditText"))
1243 assertThat("Entry has vieIdwResourceName of 'name'", entryNode.viewIdResourceName, equalTo("name"))
1244 assertThat("Entry value is text", entryNode.text.toString(), equalTo("Julie"))
1245 if (Build.VERSION.SDK_INT >= 19) {
1246 assertThat("Entry hint is label",
1247 entryNode.extras.getString("AccessibilityNodeInfo.hint"),
1248 equalTo("Name:"))
1249 assertThat("Entry input type is correct", entryNode.inputType,
1250 equalTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT))
1251 }
1252
1253 val buttonNode = createNodeInfo(rootNode.getChildId(2))
1254 assertThat("Last node is a button", buttonNode.className.toString(), equalTo("android.widget.Button"))
1255 // The child text leaf is pruned, so this button is childless.
1256 assertThat("Button has a single text leaf", buttonNode.childCount, equalTo(0))
1257 assertThat("Button has correct text", buttonNode.text.toString(), equalTo("Submit"))
1258 }
1259
1260 @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
1261 @Test fun testCollection() {
1262 loadTestPage("test-collection")
1263 waitForInitialFocus()
1264
1265 val rootNode = createNodeInfo(View.NO_ID)
1266 assertThat("Document has 2 children", rootNode.childCount, equalTo(2))
1267
1268 val firstList = createNodeInfo(rootNode.getChildId(0))
1269 assertThat("First list has 2 children", firstList.childCount, equalTo(2))
1270 assertThat("List is a ListView", firstList.className.toString(), equalTo("android.widget.ListView"))
1271 if (Build.VERSION.SDK_INT >= 19) {
1272 assertThat("First list should have collectionInfo", firstList.collectionInfo, notNullValue())
1273 assertThat("First list has 2 rowCount", firstList.collectionInfo.rowCount, equalTo(2))
1274 assertThat("First list should not be hierarchical", firstList.collectionInfo.isHierarchical, equalTo(false))
1275 }
1276
1277 val firstListFirstItem = createNodeInfo(firstList.getChildId(0))
1278 if (Build.VERSION.SDK_INT >= 19) {
1279 assertThat("Item has collectionItemInfo", firstListFirstItem.collectionItemInfo, notNullValue())
1280 assertThat("Item has collectionItemInfo", firstListFirstItem.collectionItemInfo.rowIndex, equalTo(1))
1281 }
1282
1283 val secondList = createNodeInfo(rootNode.getChildId(1))
1284 assertThat("Second list has 1 child", secondList.childCount, equalTo(1))
1285 if (Build.VERSION.SDK_INT >= 19) {
1286 assertThat("Second list should have collectionInfo", secondList.collectionInfo, notNullValue())
1287 assertThat("Second list has 2 rowCount", secondList.collectionInfo.rowCount, equalTo(1))
1288 assertThat("Second list should be hierarchical", secondList.collectionInfo.isHierarchical, equalTo(true))
1289 }
1290 }
1291
1292 @Setting(key = Setting.Key.FULL_ACCESSIBILITY_TREE, value = "true")
1293 @Test fun testRange() {
1294 loadTestPage("test-range")
1295 waitForInitialFocus()
1296
1297 val rootNode = createNodeInfo(View.NO_ID)
1298 assertThat("Document has 3 children", rootNode.childCount, equalTo(3))
1299
1300 val firstRange = createNodeInfo(rootNode.getChildId(0))
1301 assertThat("Range has right label", firstRange.text.toString(), equalTo("Rating"))
1302 assertThat("Range is SeekBar", firstRange.className.toString(), equalTo("android.widget.SeekBar"))
1303 if (Build.VERSION.SDK_INT >= 19) {
1304 assertThat("'Rating' has rangeInfo", firstRange.rangeInfo, notNullValue())
1305 assertThat("'Rating' has correct value", firstRange.rangeInfo.current, equalTo(4f))
1306 assertThat("'Rating' has correct max", firstRange.rangeInfo.max, equalTo(10f))
1307 assertThat("'Rating' has correct min", firstRange.rangeInfo.min, equalTo(1f))
1308 assertThat("'Rating' has correct range type", firstRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT))
1309 }
1310
1311 val secondRange = createNodeInfo(rootNode.getChildId(1))
1312 assertThat("Range has right label", secondRange.text.toString(), equalTo("Stars"))
1313 if (Build.VERSION.SDK_INT >= 19) {
1314 assertThat("'Rating' has rangeInfo", secondRange.rangeInfo, notNullValue())
1315 assertThat("'Rating' has correct value", secondRange.rangeInfo.current, equalTo(4.5f))
1316 assertThat("'Rating' has correct max", secondRange.rangeInfo.max, equalTo(5f))
1317 assertThat("'Rating' has correct min", secondRange.rangeInfo.min, equalTo(1f))
1318 assertThat("'Rating' has correct range type", secondRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT))
1319 }
1320
1321 val thirdRange = createNodeInfo(rootNode.getChildId(2))
1322 assertThat("Range has right label", thirdRange.text.toString(), equalTo("Percent"))
1323 if (Build.VERSION.SDK_INT >= 19) {
1324 assertThat("'Rating' has rangeInfo", thirdRange.rangeInfo, notNullValue())
1325 assertThat("'Rating' has correct value", thirdRange.rangeInfo.current, equalTo(0.83f))
1326 assertThat("'Rating' has correct max", thirdRange.rangeInfo.max, equalTo(1f))
1327 assertThat("'Rating' has correct min", thirdRange.rangeInfo.min, equalTo(0f))
1328 assertThat("'Rating' has correct range type", thirdRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_PERCENT))
1329 }
1330 }
1331
1332 @Test fun testLinksMovingByDefault() {
1333 loadTestPage("test-links")
1334 waitForInitialFocus()
1335 var nodeId = View.NO_ID;
1336
1337 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1338 sessionRule.waitUntilCalled(object : EventDelegate {
1339 @AssertCalled(count = 1)
1340 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1341 nodeId = getSourceId(event)
1342 val node = createNodeInfo(nodeId)
1343 assertThat("Accessibility focus on a with href",
1344 node.contentDescription as String, startsWith("a with href"))
1345 if (Build.VERSION.SDK_INT >= 19) {
1346 assertThat("a with href is a link",
1347 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1348 equalTo("link"))
1349 }
1350 }
1351 })
1352
1353 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1354 sessionRule.waitUntilCalled(object : EventDelegate {
1355 @AssertCalled(count = 1)
1356 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1357 nodeId = getSourceId(event)
1358 val node = createNodeInfo(nodeId)
1359 assertThat("Accessibility focus on a with no attributes",
1360 node.text as String, startsWith("a with no attributes"))
1361 if (Build.VERSION.SDK_INT >= 19) {
1362 assertThat("a with no attributes is not a link",
1363 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1364 equalTo(""))
1365 }
1366 }
1367 })
1368
1369 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1370 sessionRule.waitUntilCalled(object : EventDelegate {
1371 @AssertCalled(count = 1)
1372 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1373 nodeId = getSourceId(event)
1374 val node = createNodeInfo(nodeId)
1375 assertThat("Accessibility focus on a with name",
1376 node.text as String, startsWith("a with name"))
1377 if (Build.VERSION.SDK_INT >= 19) {
1378 assertThat("a with name is not a link",
1379 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1380 equalTo(""))
1381 }
1382 }
1383 })
1384
1385 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1386 sessionRule.waitUntilCalled(object : EventDelegate {
1387 @AssertCalled(count = 1)
1388 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1389 nodeId = getSourceId(event)
1390 val node = createNodeInfo(nodeId)
1391 assertThat("Accessibility focus on a with onclick",
1392 node.contentDescription as String, startsWith("a with onclick"))
1393 if (Build.VERSION.SDK_INT >= 19) {
1394 assertThat("a with onclick is a link",
1395 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1396 equalTo("link"))
1397 }
1398 }
1399 })
1400
1401 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1402 sessionRule.waitUntilCalled(object : EventDelegate {
1403 @AssertCalled(count = 1)
1404 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1405 nodeId = getSourceId(event)
1406 val node = createNodeInfo(nodeId)
1407 assertThat("Accessibility focus on span with role link",
1408 node.contentDescription as String, startsWith("span with role link"))
1409 if (Build.VERSION.SDK_INT >= 19) {
1410 assertThat("span with role link is a link",
1411 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1412 equalTo("link"))
1413 }
1414 }
1415 })
1416 }
1417
1418 @Test fun testLinksMovingByLink() {
1419 loadTestPage("test-links")
1420 waitForInitialFocus()
1421 var nodeId = View.NO_ID;
1422
1423 val bundle = Bundle()
1424 bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "LINK")
1425
1426 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
1427 sessionRule.waitUntilCalled(object : EventDelegate {
1428 @AssertCalled(count = 1)
1429 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1430 nodeId = getSourceId(event)
1431 val node = createNodeInfo(nodeId)
1432 assertThat("Accessibility focus on a with href",
1433 node.contentDescription as String, startsWith("a with href"))
1434 }
1435 })
1436
1437 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
1438 sessionRule.waitUntilCalled(object : EventDelegate {
1439 @AssertCalled(count = 1)
1440 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1441 nodeId = getSourceId(event)
1442 val node = createNodeInfo(nodeId)
1443 assertThat("Accessibility focus on a with onclick",
1444 node.contentDescription as String, startsWith("a with onclick"))
1445 }
1446 })
1447
1448 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
1449 sessionRule.waitUntilCalled(object : EventDelegate {
1450 @AssertCalled(count = 1)
1451 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1452 nodeId = getSourceId(event)
1453 val node = createNodeInfo(nodeId)
1454 assertThat("Accessibility focus on span with role link",
1455 node.contentDescription as String, startsWith("span with role link"))
1456 }
1457 })
1458 }
1459
1460 @Test fun testAriaComboBoxesMovingByDefault() {
1461 loadTestPage("test-aria-comboboxes")
1462 waitForInitialFocus()
1463 var nodeId = View.NO_ID;
1464
1465 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1466 sessionRule.waitUntilCalled(object : EventDelegate {
1467 @AssertCalled(count = 1)
1468 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1469 nodeId = getSourceId(event)
1470 val node = createNodeInfo(nodeId)
1471 assertThat("Accessibility focus is EditBox",
1472 node.className.toString(),
1473 equalTo("android.widget.EditText"))
1474 if (Build.VERSION.SDK_INT >= 19) {
1475 assertThat("Accessibility focus on ARIA 1.0 combobox",
1476 node.extras.getString("AccessibilityNodeInfo.hint"),
1477 equalTo("ARIA 1.0 combobox"))
1478 }
1479 }
1480 })
1481
1482 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1483 sessionRule.waitUntilCalled(object : EventDelegate {
1484 @AssertCalled(count = 1)
1485 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1486 nodeId = getSourceId(event)
1487 val node = createNodeInfo(nodeId)
1488 assertThat("Accessibility focus is EditBox",
1489 node.className.toString(),
1490 equalTo("android.widget.EditText"))
1491 if (Build.VERSION.SDK_INT >= 19) {
1492 assertThat("Accessibility focus on ARIA 1.1 combobox",
1493 node.extras.getString("AccessibilityNodeInfo.hint"),
1494 equalTo("ARIA 1.1 combobox"))
1495 }
1496 }
1497 })
1498 }
1499
1500 @Test fun testAriaComboBoxesMovingByControl() {
1501 loadTestPage("test-aria-comboboxes")
1502 waitForInitialFocus()
1503 var nodeId = View.NO_ID;
1504
1505 val bundle = Bundle()
1506 bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "CONTROL")
1507
1508 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
1509 sessionRule.waitUntilCalled(object : EventDelegate {
1510 @AssertCalled(count = 1)
1511 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1512 nodeId = getSourceId(event)
1513 val node = createNodeInfo(nodeId)
1514 assertThat("Accessibility focus is EditBox",
1515 node.className.toString(),
1516 equalTo("android.widget.EditText"))
1517 if (Build.VERSION.SDK_INT >= 19) {
1518 assertThat("Accessibility focus on ARIA 1.0 combobox",
1519 node.extras.getString("AccessibilityNodeInfo.hint"),
1520 equalTo("ARIA 1.0 combobox"))
1521 }
1522 }
1523 })
1524
1525 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
1526 sessionRule.waitUntilCalled(object : EventDelegate {
1527 @AssertCalled(count = 1)
1528 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1529 nodeId = getSourceId(event)
1530 val node = createNodeInfo(nodeId)
1531 assertThat("Accessibility focus is EditBox",
1532 node.className.toString(),
1533 equalTo("android.widget.EditText"))
1534 if (Build.VERSION.SDK_INT >= 19) {
1535 assertThat("Accessibility focus on ARIA 1.1 combobox",
1536 node.extras.getString("AccessibilityNodeInfo.hint"),
1537 equalTo("ARIA 1.1 combobox"))
1538 }
1539 }
1540 })
1541 }
1542
1543 @Test fun testAccessibilityFocusBoundaries() {
1544 loadTestPage("test-links")
1545 waitForInitialFocus()
1546 var nodeId = View.NO_ID
1547 var performedAction: Boolean
1548
1549 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1550 assertThat("Successfully moved a11y focus to first node", performedAction, equalTo(true))
1551 sessionRule.waitUntilCalled(object : EventDelegate {
1552 @AssertCalled(count = 1)
1553 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1554 nodeId = getSourceId(event)
1555 val node = createNodeInfo(nodeId)
1556 assertThat("Accessibility focus on a with href",
1557 node.contentDescription as String, startsWith("a with href"))
1558 }
1559 })
1560
1561 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, null)
1562 assertThat("Successfully moved a11y focus past first node", performedAction, equalTo(true))
1563 sessionRule.waitUntilCalled(object : EventDelegate {
1564 @AssertCalled(count = 1)
1565 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1566 assertThat("Accessibility focus on web view", getSourceId(event), equalTo(View.NO_ID))
1567 }
1568 })
1569
1570 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1571 assertThat("Successfully moved a11y focus to second node", performedAction, equalTo(true))
1572 sessionRule.waitUntilCalled(object : EventDelegate {
1573 @AssertCalled(count = 1)
1574 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1575 nodeId = getSourceId(event)
1576 val node = createNodeInfo(nodeId)
1577 assertThat("Accessibility focus on a with no attributes",
1578 node.text as String, startsWith("a with no attributes"))
1579 }
1580 })
1581
1582 // hide first and last link
1583 mainSession.evaluateJS("document.querySelectorAll('body > :first-child, body > :last-child').forEach(e => e.style.display = 'none');")
1584 sessionRule.waitUntilCalled(object : EventDelegate {
1585 @AssertCalled(count = 1)
1586 override fun onWinContentChanged(event: AccessibilityEvent) { }
1587 })
1588
1589 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, null)
1590 assertThat("Successfully moved a11y focus past first visible node", performedAction, equalTo(true))
1591 sessionRule.waitUntilCalled(object : EventDelegate {
1592 @AssertCalled(count = 1)
1593 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1594 assertThat("Accessibility focus on web view", getSourceId(event), equalTo(View.NO_ID))
1595 }
1596 })
1597
1598
1599 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1600 sessionRule.waitUntilCalled(object : EventDelegate {
1601 @AssertCalled(count = 1)
1602 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1603 nodeId = getSourceId(event)
1604 val node = createNodeInfo(nodeId)
1605 assertThat("Accessibility focus on a with name",
1606 node.text as String, startsWith("a with name"))
1607 if (Build.VERSION.SDK_INT >= 19) {
1608 assertThat("a with name is not a link",
1609 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1610 equalTo(""))
1611 }
1612 }
1613 })
1614
1615 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1616 sessionRule.waitUntilCalled(object : EventDelegate {
1617 @AssertCalled(count = 1)
1618 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1619 nodeId = getSourceId(event)
1620 val node = createNodeInfo(nodeId)
1621 assertThat("Accessibility focus on a with onclick",
1622 node.contentDescription as String, startsWith("a with onclick"))
1623 if (Build.VERSION.SDK_INT >= 19) {
1624 assertThat("a with onclick is a link",
1625 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1626 equalTo("link"))
1627 }
1628 }
1629 })
1630
1631 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1632 assertThat("Should fail to move a11y focus to last hidden node", performedAction, equalTo(false))
1633
1634 // show last link
1635 mainSession.evaluateJS("document.querySelector('body > :last-child').style.display = 'initial';")
1636 sessionRule.waitUntilCalled(object : EventDelegate {
1637 @AssertCalled(count = 1)
1638 override fun onWinContentChanged(event: AccessibilityEvent) { }
1639 })
1640
1641 provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1642 sessionRule.waitUntilCalled(object : EventDelegate {
1643 @AssertCalled(count = 1)
1644 override fun onAccessibilityFocused(event: AccessibilityEvent) {
1645 nodeId = getSourceId(event)
1646 val node = createNodeInfo(nodeId)
1647 assertThat("Accessibility focus on span with role link",
1648 node.contentDescription as String, startsWith("span with role link"))
1649 if (Build.VERSION.SDK_INT >= 19) {
1650 assertThat("span with role link is a link",
1651 node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription")!!.toString(),
1652 equalTo("link"))
1653 }
1654 }
1655 })
1656
1657 performedAction = provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
1658 assertThat("Should fail to move a11y focus beyond last node", performedAction, equalTo(false))
1659
1660 performedAction = provider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, null)
1661 assertThat("Should fail to move a11y focus before web content", performedAction, equalTo(false))
1662 }
1663
1664 @Test fun testTextEntry() {
1665 loadTestPage("test-text-entry-node")
1666 waitForInitialFocus()
1667
1668 mainSession.evaluateJS("document.querySelector('input[aria-label=Name]').focus()")
1669 sessionRule.waitUntilCalled(object : EventDelegate {
1670 @AssertCalled(count = 1)
1671 override fun onFocused(event: AccessibilityEvent) {}
1672 })
1673
1674 mainSession.evaluateJS("document.querySelector('input[aria-label=Name]').value = 'Tobiasas'")
1675
1676 sessionRule.waitUntilCalled(object : EventDelegate {
1677 @AssertCalled(count = 1)
1678 override fun onTextChanged(event: AccessibilityEvent) {}
1679
1680 @AssertCalled(count = 1)
1681 override fun onTextSelectionChanged(event: AccessibilityEvent) {}
1682
1683 // Don't fire a11y focus for collapsed caret changes.
1684 // This will interfere with on screen keyboards and throw a11y focus
1685 // back and fourth.
1686 @AssertCalled(count = 0)
1687 override fun onAccessibilityFocused(event: AccessibilityEvent) {}
1688 })
1689 }
1690
1691 }
1692