From 0901edec4173395e9644e7ffa42b19870034fb3f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 2 Nov 2020 10:57:09 -0800 Subject: [PATCH] test: more accessibility tests (#65) --- .../playwright/impl/Serialization.java | 10 + .../playwright/TestAccessibility.java | 420 +++++++++++++++++- 2 files changed, 429 insertions(+), 1 deletion(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java index 76efb83c..d5c7e9cc 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -40,6 +40,7 @@ class Serialization { .registerTypeAdapter(ColorScheme.class, new ColorSchemeAdapter().nullSafe()) .registerTypeAdapter(Page.EmulateMediaOptions.Media.class, new MediaSerializer()) .registerTypeAdapter(Optional.class, new OptionalSerializer()) + .registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer()) .registerTypeAdapter(Path.class, new PathSerializer()).create(); } return gson; @@ -265,6 +266,15 @@ class Serialization { } } + private static class HandleSerializer implements JsonSerializer { + @Override + public JsonElement serialize(JSHandleImpl src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + json.addProperty("guid", src.guid); + return json; + } + } + private static class MediaSerializer implements JsonSerializer { @Override public JsonElement serialize(Page.EmulateMediaOptions.Media src, Type typeOfSrc, JsonSerializationContext context) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestAccessibility.java b/playwright/src/test/java/com/microsoft/playwright/TestAccessibility.java index 1a2873a2..e16a3c7c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestAccessibility.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestAccessibility.java @@ -16,11 +16,177 @@ package com.microsoft.playwright; +import com.google.gson.*; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.lang.reflect.Type; + +import static org.junit.jupiter.api.Assertions.*; public class TestAccessibility extends TestBase { + private static class AccessibilityNodeSerializer implements JsonSerializer { + @Override + public JsonElement serialize(AccessibilityNode src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + if (src.role() != null) { + json.addProperty("role", src.role()); + } + if (src.name() != null) { + json.addProperty("name", src.name()); + } + if (src.valueString() != null) { + json.addProperty("valueString", src.valueString()); + } + if (src.valueNumber() != null) { + json.addProperty("valueNumber", src.valueNumber()); + } + if (src.description() != null) { + json.addProperty("description", src.description()); + } + if (src.keyshortcuts() != null) { + json.addProperty("keyshortcuts", src.keyshortcuts()); + } + if (src.roledescription() != null) { + json.addProperty("roledescription", src.roledescription()); + } + if (src.valuetext() != null) { + json.addProperty("valuetext", src.valuetext()); + } + if (src.disabled() != null) { + json.addProperty("disabled", src.disabled()); + } + if (src.expanded() != null) { + json.addProperty("expanded", src.expanded()); + } + if (src.focused() != null) { + json.addProperty("focused", src.focused()); + } + if (src.modal() != null) { + json.addProperty("modal", src.modal()); + } + if (src.multiline() != null) { + json.addProperty("multiline", src.multiline()); + } + if (src.multiselectable() != null) { + json.addProperty("multiselectable", src.multiselectable()); + } + if (src.readonly() != null) { + json.addProperty("readonly", src.readonly()); + } + if (src.required() != null) { + json.addProperty("required", src.required()); + } + if (src.selected() != null) { + json.addProperty("selected", src.selected()); + } + if (src.level() != null) { + json.addProperty("level", src.level()); + } + if (src.valuemin() != null) { + json.addProperty("valuemin", src.valuemin()); + } + if (src.valuemax() != null) { + json.addProperty("valuemax", src.valuemax()); + } + if (src.autocomplete() != null) { + json.addProperty("autocomplete", src.autocomplete()); + } + if (src.haspopup() != null) { + json.addProperty("haspopup", src.haspopup()); + } + if (src.invalid() != null) { + json.addProperty("invalid", src.invalid()); + } + if (src.orientation() != null) { + json.addProperty("orientation", src.orientation()); + } + if (src.checked() != null) { + json.addProperty("checked", src.checked().toString().toLowerCase()); + } + if (src.pressed() != null) { + json.addProperty("pressed", src.pressed().toString().toLowerCase()); + } + if (src.children() != null) { + JsonArray children = new JsonArray(); + for (AccessibilityNode child : src.children()) { + children.add(context.serialize(child)); + } + json.add("children", children); + } + return json; + } + } + + private static Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(AccessibilityNode.class, new AccessibilityNodeSerializer()).create(); + + + static void assertNodeEquals(String expected, AccessibilityNode actual) { + JsonElement actualJson = gson.toJsonTree(actual); + assertEquals(JsonParser.parseString(expected), actualJson); + } + + @Test + void shouldWork() { + page.setContent("\n" + + " Accessibility Test\n" + + "\n" + + "\n" + + "

Inputs

\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""); + // autofocus happens after a delay in chrome these days + page.waitForFunction("() => document.activeElement.hasAttribute('autofocus')"); + + String golden = isFirefox ? "{\n" + + " role: 'document',\n" + + " name: 'Accessibility Test',\n" + + " children: [\n" + + " {role: 'heading', name: 'Inputs', level: 1},\n" + + " {role: 'textbox', name: 'Empty input', focused: true},\n" + + " {role: 'textbox', name: 'readonly input', readonly: true},\n" + + " {role: 'textbox', name: 'disabled input', disabled: true},\n" + + " {role: 'textbox', name: 'Input with whitespace', valueString: ' '},\n" + + " {role: 'textbox', name: '', valueString: 'value only'},\n" + + " {role: 'textbox', name: '', valueString: 'and a value'}, // firefox doesn't use aria-placeholder for the name\n" + + " {role: 'textbox', name: '', valueString: 'and a value', description: 'This is a description!'} // and here\n" + + " ]\n" + + "}" : isChromium ? "{\n" + + " role: 'WebArea',\n" + + " name: 'Accessibility Test',\n" + + " children: [\n" + + " {role: 'heading', name: 'Inputs', level: 1},\n" + + " {role: 'textbox', name: 'Empty input', focused: true},\n" + + " {role: 'textbox', name: 'readonly input', readonly: true},\n" + + " {role: 'textbox', name: 'disabled input', disabled: true},\n" + + " {role: 'textbox', name: 'Input with whitespace', valueString: ' '},\n" + + " {role: 'textbox', name: '', valueString: 'value only'},\n" + + " {role: 'textbox', name: 'placeholder', valueString: 'and a value'},\n" + + " {role: 'textbox', name: 'placeholder', valueString: 'and a value', description: 'This is a description!'}\n" + + " ]\n" + + "}" : "{\n" + + " role: 'WebArea',\n" + + " name: 'Accessibility Test',\n" + + " children: [\n" + + " {role: 'heading', name: 'Inputs', level: 1},\n" + + " {role: 'textbox', name: 'Empty input', focused: true},\n" + + " {role: 'textbox', name: 'readonly input', readonly: true},\n" + + " {role: 'textbox', name: 'disabled input', disabled: true},\n" + + " {role: 'textbox', name: 'Input with whitespace', valueString: ' ' },\n" + + " {role: 'textbox', name: '', valueString: 'value only' },\n" + + " {role: 'textbox', name: 'placeholder', valueString: 'and a value'},\n" + + " {role: 'textbox', name: 'This is a description!',valueString: 'and a value'} // webkit uses the description over placeholder for the name\n" + + " ]\n" + + "}"; + assertNodeEquals(golden, page.accessibility().snapshot()); + } @Test void shouldWorkWithRegularText() { @@ -65,4 +231,256 @@ public class TestAccessibility extends TestBase { AccessibilityNode snapshot = page.accessibility().snapshot(); assertEquals("foo", snapshot.children().get(0).keyshortcuts()); } + + @Test + void shouldNotReportTextNodesInsideControls() { + page.setContent("
\n" + + "
Tab1
\n" + + "
Tab2
\n" + + "
"); + String golden = "{\n" + + " role: '" + (isFirefox ? "document" : "WebArea") + "',\n" + + " name: '',\n" + + " children: [{\n" + + " role: 'tab',\n" + + " name: 'Tab1',\n" + + " selected: true\n" + + " }, {\n" + + " role: 'tab',\n" + + " name: 'Tab2'\n" + + " }]\n" + + "}"; + assertNodeEquals(golden, page.accessibility().snapshot()); + } + + @Test + void richTextEditableFieldsShouldHaveChildren() { +// TODO: test.skip(browserName === "webkit", "WebKit rich text accessibility is iffy"); + page.setContent("
\n" + + " Edit this image: my fake image\n" + + "
"); + String golden = isFirefox ? "{\n" + + " role: 'section',\n" + + " name: '',\n" + + " children: [{\n" + + " role: 'text leaf',\n" + + " name: 'Edit this image: '\n" + + " }, {\n" + + " role: 'text',\n" + + " name: 'my fake image'\n" + + " }]\n" + + "}" : "{\n" + + " role: 'generic',\n" + + " name: '',\n" + + " valueString: 'Edit this image: ',\n" + + " children: [{\n" + + " role: 'text',\n" + + " name: 'Edit this image:'\n" + + " }, {\n" + + " role: 'img',\n" + + " name: 'my fake image'\n" + + " }]\n" + + "}"; + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals(golden, snapshot.children().get(0)); + } + + @Test + void richTextEditableFieldsWithRoleShouldHaveChildren() { +// TODO: test.skip(browserName === "webkit", "WebKit rich text accessibility is iffy"); + page.setContent("
\n" + + " Edit this image: my fake image\n" + + "
"); + String golden = isFirefox ? "{\n" + + " role: 'textbox',\n" + + " name: '',\n" + + " valueString: 'Edit this image: my fake image',\n" + + " children: [{\n" + + " role: 'text',\n" + + " name: 'my fake image'\n" + + " }]\n" + + "}" : "{\n" + + " role: 'textbox',\n" + + " name: '',\n" + + " valueString: 'Edit this image: ',\n" + + " children: [{\n" + + " role: 'text',\n" + + " name: 'Edit this image:'\n" + + " }, {\n" + + " role: 'img',\n" + + " name: 'my fake image'\n" + + " }]\n" + + "}"; + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals(golden, snapshot.children().get(0)); + } + + // TODO: suite.skip(browserName === "firefox", "Firefox does not support contenteditable='plaintext-only'"); +// TODO: suite.skip(browserName === "webkit", "WebKit rich text accessibility is iffy"); + @Test + void plainTextFieldWithRoleShouldNotHaveChildren() { + page.setContent("
Edit this image:my fake image
"); + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals("{\n" + + " role: 'textbox',\n" + + " name: '',\n" + + " valueString: 'Edit this image:'\n" + + "}", snapshot.children().get(0)); + } + + @Test + void plainTextFieldWithoutRoleShouldNotHaveContent() { + page.setContent("
Edit this image:my fake image
"); + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals("{\n" + + " role: 'generic',\n" + + " name: ''\n" + + "}", snapshot.children().get(0)); + } + + @Test + void plainTextFieldWithTabindexAndWithoutRoleShouldNotHaveContent() { + page.setContent("
Edit this image:my fake image
"); + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals("{\n" + + " role: 'generic',\n" + + " name: ''\n" + + "}", snapshot.children().get(0)); + } + + @Test + void nonEditableTextboxWithRoleAndTabIndexAndLabelShouldNotHaveChildren() { + page.setContent("
\n" + + "this is the inner content\n" + + "yo\n" + + "
"); + String golden = isFirefox ? "{\n" + + " role: 'textbox',\n" + + " name: 'my favorite textbox',\n" + + " valueString: 'this is the inner content yo'\n" + + "}" : isChromium ? "{\n" + + " role: 'textbox',\n" + + " name: 'my favorite textbox',\n" + + " valueString: 'this is the inner content '\n" + + "}" : "{\n" + + " role: 'textbox',\n" + + " name: 'my favorite textbox',\n" + + " valueString: 'this is the inner content ',\n" + + "}"; + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals(golden, snapshot.children().get(0)); + } + + @Test + void checkboxWithAndTabIndexAndLabelShouldNotHaveChildren() { + page.setContent("
\n" + + "this is the inner content\n" + + "yo\n" + + "
"); + String golden = "{\n" + + " role: 'checkbox',\n" + + " name: 'my favorite checkbox',\n" + + " checked: 'checked'\n" + + "}"; + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals(golden, snapshot.children().get(0)); + } + + @Test + void checkboxWithoutLabelShouldNotHaveChildren() { + page.setContent("
\n" + + "this is the inner content\n" + + "yo\n" + + "
"); + String golden = isFirefox ? "{\n" + + " role: 'checkbox',\n" + + " name: 'this is the inner content yo',\n" + + " checked: 'checked'\n" + + "}" : "{\n" + + " role: 'checkbox',\n" + + " name: 'this is the inner content yo',\n" + + " checked: 'checked'\n" + + "}"; + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertNodeEquals(golden, snapshot.children().get(0)); + } + + @Test + void shouldWorkAButton() { + page.setContent(""); + + ElementHandle button = page.querySelector("button"); + assertNodeEquals("{\n" + + " role: 'button',\n" + + " name: 'My Button'\n" + + "}", page.accessibility().snapshot(new Accessibility.SnapshotOptions().withRoot(button))); + } + + @Test + void shouldWorkAnInput() { + page.setContent(""); + + ElementHandle input = page.querySelector("input"); + assertNodeEquals("{\n" + + " role: 'textbox',\n" + + " name: 'My Input',\n" + + " valueString: 'My Value'\n" + + "}", page.accessibility().snapshot(new Accessibility.SnapshotOptions().withRoot(input))); + } + + @Test + void shouldWorkOnAMenu() { + page.setContent("
\n" + + "
First Item
\n" + + "
Second Item
\n" + + "
Third Item
\n" + + "
"); + + ElementHandle menu = page.querySelector("div[role='menu']"); + assertNodeEquals("{\n" + + " role: 'menu',\n" + + " name: 'My Menu',\n" + + " children:\n" + + " [ { role: 'menuitem', name: 'First Item' },\n" + + " { role: 'menuitem', name: 'Second Item' },\n" + + " { role: 'menuitem', name: 'Third Item' } ]\n" + + (isWebKit ? ", orientation: 'vertical'" : "") + + " }", page.accessibility().snapshot(new Accessibility.SnapshotOptions().withRoot(menu))); + } + + @Test + void shouldReturnNullWhenTheElementIsNoLongerInDOM() { + page.setContent(""); + ElementHandle button = page.querySelector("button"); + page.evalOnSelector("button", "button => button.remove()"); + assertEquals(null, page.accessibility().snapshot(new Accessibility.SnapshotOptions().withRoot(button))); + } + + @Test + void shouldShowUninterestingNodes() { + page.setContent("
\n" + + "
\n" + + " hello\n" + + "
\n" + + " world\n" + + "
\n" + + "
\n" + + "
"); + ElementHandle root = page.querySelector("#root"); + AccessibilityNode snapshot = page.accessibility().snapshot( + new Accessibility.SnapshotOptions().withRoot(root).withInterestingOnly(false)); + assertEquals("textbox", snapshot.role()); + assertTrue(snapshot.valueString().contains("hello")); + assertTrue(snapshot.valueString().contains("world")); + assertNotNull(snapshot.children()); + } + + @Test + void shouldWorkWhenThereIsATitle() { + page.setContent("This is the title\n" + + "
This is the content
"); + AccessibilityNode snapshot = page.accessibility().snapshot(); + assertEquals("This is the title", snapshot.name()); + assertEquals("This is the content", snapshot.children().get(0).name()); + } }