| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h" |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" |
| #include "third_party/blink/renderer/core/editing/position.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_range.h" |
| #include "third_party/blink/renderer/modules/accessibility/testing/accessibility_test.h" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| |
| using AXIntListAttribute = ax::mojom::blink::IntListAttribute; |
| using AXMarkerType = ax::mojom::blink::MarkerType; |
| |
| namespace blink { |
| namespace test { |
| |
| TEST_F(AccessibilityTest, GetWordBoundaries) { |
| // ☀ is the sun emoji symbol. |
| // জ is circled digit one. |
| // Full string: "This, ☀ জ is ... a---+++test. <p>word</p>" |
| SetBodyInnerHTML(R"HTML( |
| <p id="paragraph"> |
| "This, ☀ জ is ... a---+++test. <p>word</p>" |
| </p>)HTML"); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| const AXObject* ax_inline_text_box = |
| ax_paragraph->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| |
| VectorOf<int> expected_word_starts{0, 1, 5, 7, 9, 11, 14, 18, 19, 22, 23, |
| 24, 25, 29, 31, 32, 33, 34, 38, 40, 41}; |
| VectorOf<int> expected_word_ends{1, 5, 6, 8, 10, 13, 17, 19, 22, 23, 24, |
| 25, 29, 30, 32, 33, 34, 38, 40, 41, 43}; |
| VectorOf<int> word_starts, word_ends; |
| ax_inline_text_box->GetWordBoundaries(word_starts, word_ends); |
| EXPECT_EQ(expected_word_starts, word_starts); |
| EXPECT_EQ(expected_word_ends, word_ends); |
| } |
| |
| TEST_F(AccessibilityTest, GetDocumentMarkers) { |
| // There should be four inline text boxes in the following paragraph. |
| SetBodyInnerHTML(R"HTML( |
| <style>* { font-size: 10px; }</style> |
| <p id="paragraph" style="width: 10ch;"> |
| Misspelled text with a grammar error. |
| </p>)HTML"); |
| |
| const Node* text = GetElementById("paragraph")->firstChild(); |
| ASSERT_NE(nullptr, text); |
| ASSERT_TRUE(text->IsTextNode()); |
| |
| // Mark the part of the paragraph that says "Misspelled text" with a spelling |
| // marker, and the part that says "a grammar error" with a grammar marker. |
| // |
| // Note that the inline text boxes and the markers do not occupy the same text |
| // range. The ranges simply overlap. Also note that the marker ranges include |
| // non-collapsed white space found in the DOM. |
| DocumentMarkerController& marker_controller = GetDocument().Markers(); |
| const EphemeralRange misspelled_range(Position(text, 9), Position(text, 24)); |
| marker_controller.AddSpellingMarker(misspelled_range); |
| const EphemeralRange grammar_range(Position(text, 30), Position(text, 45)); |
| marker_controller.AddGrammarMarker(grammar_range); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| // kStaticText: "Misspelled text with a grammar error.". |
| const AXObject* ax_text = ax_paragraph->FirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_text); |
| ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text->RoleValue()); |
| ASSERT_EQ(4, ax_text->ChildCountIncludingIgnored()); |
| |
| // For each inline text box, angle brackets indicate where the marker starts |
| // and ends respectively. |
| |
| // kInlineTextBox: "<Misspelled >". |
| AXObject* ax_inline_text_box = ax_text->ChildAtIncludingIgnored(0); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| { |
| ScopedFreezeAXCache freeze(ax_inline_text_box->AXObjectCache()); |
| ui::AXNodeData node_data; |
| ax_inline_text_box->Serialize(&node_data, ui::kAXModeComplete); |
| |
| EXPECT_EQ(std::vector<int32_t>{int32_t(AXMarkerType::kSpelling)}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerTypes)); |
| EXPECT_EQ(std::vector<int32_t>{0}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerStarts)); |
| EXPECT_EQ(std::vector<int32_t>{11}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerEnds)); |
| } |
| |
| // kInlineTextBox: "<text> with <a >". |
| ax_inline_text_box = ax_text->ChildAtIncludingIgnored(1); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| { |
| ScopedFreezeAXCache freeze(ax_inline_text_box->AXObjectCache()); |
| ui::AXNodeData node_data; |
| ax_inline_text_box->Serialize(&node_data, ui::kAXModeComplete); |
| EXPECT_EQ((std::vector<int32_t>{int32_t(AXMarkerType::kSpelling), |
| int32_t(AXMarkerType::kGrammar)}), |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerTypes)); |
| EXPECT_EQ((std::vector<int32_t>{0, 10}), |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerStarts)); |
| EXPECT_EQ((std::vector<int32_t>{4, 12}), |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerEnds)); |
| } |
| |
| // kInlineTextBox: "<grammar >". |
| ax_inline_text_box = ax_text->ChildAtIncludingIgnored(2); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| { |
| ScopedFreezeAXCache freeze(ax_inline_text_box->AXObjectCache()); |
| ui::AXNodeData node_data; |
| ax_inline_text_box->Serialize(&node_data, ui::kAXModeComplete); |
| EXPECT_EQ(std::vector<int32_t>{int32_t(AXMarkerType::kGrammar)}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerTypes)); |
| EXPECT_EQ(std::vector<int32_t>{0}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerStarts)); |
| EXPECT_EQ(std::vector<int32_t>{8}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerEnds)); |
| } |
| |
| // kInlineTextBox: "<error>.". |
| ax_inline_text_box = ax_text->ChildAtIncludingIgnored(3); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| { |
| ScopedFreezeAXCache freeze(ax_inline_text_box->AXObjectCache()); |
| ui::AXNodeData node_data; |
| ax_inline_text_box->Serialize(&node_data, ui::kAXModeComplete); |
| EXPECT_EQ(std::vector<int32_t>{int32_t(AXMarkerType::kGrammar)}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerTypes)); |
| EXPECT_EQ(std::vector<int32_t>{0}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerStarts)); |
| EXPECT_EQ(std::vector<int32_t>{5}, |
| node_data.GetIntListAttribute(AXIntListAttribute::kMarkerEnds)); |
| } |
| } |
| |
| TEST_F(AccessibilityTest, TextOffsetInContainerWithASpan) { |
| // There should be three inline text boxes in the following paragraph. The |
| // span should reset the text start offset of all of them to 0. |
| SetBodyInnerHTML(R"HTML( |
| <style>* { font-size: 10px; }</style> |
| <p id="paragraph"> |
| Hello <span>world </span>there. |
| </p>)HTML"); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| const AXObject* ax_inline_text_box = |
| ax_paragraph->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(1, ax_inline_text_box->TextOffsetInContainer(1)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextInPreOrderIncludingIgnored() |
| ->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(2, ax_inline_text_box->TextOffsetInContainer(2)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextInPreOrderIncludingIgnored() |
| ->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(3, ax_inline_text_box->TextOffsetInContainer(3)); |
| |
| ASSERT_EQ(nullptr, ax_inline_text_box->NextInPreOrderIncludingIgnored()); |
| } |
| |
| TEST_F(AccessibilityTest, TextOffsetInContainerWithMultipleInlineTextBoxes) { |
| // There should be four inline text boxes in the following paragraph. The span |
| // should not affect the text start offset of the text outside the span. |
| SetBodyInnerHTML(R"HTML( |
| <style>* { font-size: 10px; }</style> |
| <p id="paragraph" style="width: 5ch;"> |
| <span>Offset</span>Hello world there. |
| </p>)HTML"); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| const AXObject* ax_inline_text_box = |
| ax_paragraph->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(1, ax_inline_text_box->TextOffsetInContainer(1)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextInPreOrderIncludingIgnored() |
| ->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(1, ax_inline_text_box->TextOffsetInContainer(1)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(6, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(8, ax_inline_text_box->TextOffsetInContainer(2)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(12, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(15, ax_inline_text_box->TextOffsetInContainer(3)); |
| |
| ASSERT_EQ(nullptr, ax_inline_text_box->NextInPreOrderIncludingIgnored()); |
| } |
| |
| TEST_F(AccessibilityTest, TextOffsetInContainerWithLineBreak) { |
| // There should be three inline text boxes in the following paragraph. The |
| // line break should reset the text start offset to 0 of both the inline text |
| // box inside the line break, as well as the text start ofset of the second |
| // line. |
| SetBodyInnerHTML(R"HTML( |
| <style>* { font-size: 10px; }</style> |
| <p id="paragraph"> |
| Line one.<br> |
| Line two. |
| </p>)HTML"); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| const AXObject* ax_inline_text_box = |
| ax_paragraph->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(1, ax_inline_text_box->TextOffsetInContainer(1)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextInPreOrderIncludingIgnored() |
| ->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| |
| ax_inline_text_box = ax_inline_text_box->NextInPreOrderIncludingIgnored() |
| ->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| EXPECT_EQ(0, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(2, ax_inline_text_box->TextOffsetInContainer(2)); |
| |
| ASSERT_EQ(nullptr, ax_inline_text_box->NextInPreOrderIncludingIgnored()); |
| } |
| |
| TEST_F(AccessibilityTest, TextOffsetInContainerWithBreakWord) { |
| // There should be three inline text boxes in the following paragraph because |
| // of the narrow width and the long word, coupled with the CSS "break-word" |
| // property. Each inline text box should have a different offset in container. |
| LoadAhem(); |
| SetBodyInnerHTML(R"HTML( |
| <style>* { font: 10px/10px Ahem; }</style> |
| <p id="paragraph" style="width: 5ch; word-wrap: break-word;"> |
| VeryLongWord |
| </p>)HTML"); |
| |
| AXObject* ax_paragraph = GetAXObjectByElementId("paragraph"); |
| ASSERT_NE(nullptr, ax_paragraph); |
| ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph->RoleValue()); |
| ax_paragraph->LoadInlineTextBoxes(); |
| |
| const AXObject* ax_inline_text_box = |
| ax_paragraph->DeepestFirstChildIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| |
| int text_start_offset = 0; |
| int text_end_offset = ax_inline_text_box->TextLength(); |
| EXPECT_EQ(text_start_offset, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(text_end_offset, ax_inline_text_box->TextOffsetInContainer( |
| ax_inline_text_box->TextLength())); |
| |
| ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| |
| text_start_offset = text_end_offset; |
| text_end_offset = text_start_offset + ax_inline_text_box->TextLength(); |
| EXPECT_EQ(text_start_offset, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(text_end_offset, ax_inline_text_box->TextOffsetInContainer( |
| ax_inline_text_box->TextLength())); |
| |
| ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored(); |
| ASSERT_NE(nullptr, ax_inline_text_box); |
| ASSERT_EQ(ax::mojom::Role::kInlineTextBox, ax_inline_text_box->RoleValue()); |
| |
| text_start_offset = text_end_offset; |
| text_end_offset = text_start_offset + ax_inline_text_box->TextLength(); |
| EXPECT_EQ(text_start_offset, ax_inline_text_box->TextOffsetInContainer(0)); |
| EXPECT_EQ(text_end_offset, ax_inline_text_box->TextOffsetInContainer( |
| ax_inline_text_box->TextLength())); |
| |
| ASSERT_EQ(nullptr, ax_inline_text_box->NextSiblingIncludingIgnored()); |
| } |
| |
| } // namespace test |
| } // namespace blink |