blob: 00a530e6e730f848f226f147490fbd234d6c5fa9 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/accessibility/annotations/ax_image_annotator.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_discardable_memory_allocator.h"
#include "content/renderer/accessibility/annotations/ax_annotators_manager.h"
#include "content/renderer/accessibility/render_accessibility_impl_test.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
namespace content {
using blink::WebAXObject;
using blink::WebDocument;
using testing::ElementsAre;
namespace {
constexpr char kImage1[] =
"data:imagepng;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/"
"w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
constexpr char kImage2[] =
"data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAACXBIWXMAAAsTAAALEwEAmpwYAA"
"AAB3RJTUUH4gcVABQvx8CBmAAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBk"
"LmUHAAAAFUlEQVQY02P8//8/A27AxIAXjFRpAKXjAxH/0Dm5AAAAAElFTkSuQmCC";
class TestAXImageAnnotator : public AXImageAnnotator {
public:
TestAXImageAnnotator(RenderAccessibilityImpl* const render_accessibility)
: AXImageAnnotator(render_accessibility) {}
TestAXImageAnnotator(const TestAXImageAnnotator&) = delete;
TestAXImageAnnotator& operator=(const TestAXImageAnnotator&) = delete;
~TestAXImageAnnotator() override = default;
private:
std::string GenerateImageSourceId(
const blink::WebAXObject& image) const override {
std::string image_id;
if (image.IsDetached() || image.IsNull() || image.GetNode().IsNull() ||
image.GetNode().To<blink::WebElement>().IsNull()) {
ADD_FAILURE() << "Unable to retrieve the image src.";
return image_id;
}
image_id =
image.GetNode().To<blink::WebElement>().GetAttribute("SRC").Utf8();
return image_id;
}
};
class MockImageAnnotationService : public image_annotation::mojom::Annotator {
public:
MockImageAnnotationService() = default;
MockImageAnnotationService(const MockImageAnnotationService&) = delete;
MockImageAnnotationService& operator=(const MockImageAnnotationService&) =
delete;
~MockImageAnnotationService() override = default;
mojo::PendingRemote<image_annotation::mojom::Annotator> GetRemote() {
mojo::PendingRemote<image_annotation::mojom::Annotator> remote;
receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
return remote;
}
void AnnotateImage(
const std::string& image_id,
const std::string& /* description_language_tag */,
mojo::PendingRemote<image_annotation::mojom::ImageProcessor>
image_processor,
AnnotateImageCallback callback) override {
image_ids_.push_back(image_id);
image_processors_.push_back(
mojo::Remote<image_annotation::mojom::ImageProcessor>(
std::move(image_processor)));
image_processors_.back().set_disconnect_handler(
base::BindOnce(&MockImageAnnotationService::ResetImageProcessor,
base::Unretained(this), image_processors_.size() - 1));
callbacks_.push_back(std::move(callback));
}
// Tests should not delete entries in these lists.
std::vector<std::string> image_ids_;
std::vector<mojo::Remote<image_annotation::mojom::ImageProcessor>>
image_processors_;
std::vector<AnnotateImageCallback> callbacks_;
private:
void ResetImageProcessor(const size_t index) {
image_processors_[index].reset();
}
mojo::ReceiverSet<image_annotation::mojom::Annotator> receivers_;
};
} // namespace
class AXImageAnnotatorTest : public RenderAccessibilityImplTest {
public:
AXImageAnnotatorTest() = default;
AXImageAnnotatorTest(const AXImageAnnotatorTest&) = delete;
AXImageAnnotatorTest& operator=(const AXImageAnnotatorTest&) = delete;
~AXImageAnnotatorTest() override = default;
protected:
void SetUp() override {
RenderAccessibilityImplTest::SetUp();
// TODO(nektar): Add the ability to test the AX action that labels images
// only once.
ui::AXMode mode = ui::kAXModeComplete;
mode.set_mode(ui::AXMode::kLabelImages, true);
SetMode(mode);
auto annotator =
std::make_unique<TestAXImageAnnotator>(GetRenderAccessibilityImpl());
annotator->BindAnnotatorForTesting(mock_annotator_service().GetRemote());
GetRenderAccessibilityImpl()
->ax_annotators_manager_for_testing()
->AddAnnotatorForTesting(std::move(annotator));
AXImageAnnotator::IgnoreProtocolChecksForTesting();
base::DiscardableMemoryAllocator::SetInstance(
&discardable_memory_allocator);
task_environment_.RunUntilIdle();
}
void TearDown() override {
base::DiscardableMemoryAllocator::SetInstance(nullptr);
GetRenderAccessibilityImpl()
->ax_annotators_manager_for_testing()
->ClearAnnotatorsForTesting();
task_environment_.RunUntilIdle();
RenderAccessibilityImplTest::TearDown();
}
MockImageAnnotationService& mock_annotator_service() {
return mock_annotator_service_;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
MockImageAnnotationService mock_annotator_service_;
base::TestDiscardableMemoryAllocator discardable_memory_allocator;
};
// TODO(crbug.com/1477047, fuchsia:132924): Reenable test on Fuchsia once
// post-lifecycle serialization is turned on.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_OnImageAdded DISABLED_OnImageAdded
#else
#define MAYBE_OnImageAdded OnImageAdded
#endif
TEST_F(AXImageAnnotatorTest, MAYBE_OnImageAdded) {
LoadHTMLAndRefreshAccessibilityTree(base::StringPrintf(R"HTML(
<body>
<p>Test document</p>
<img id="A" src="%s"
style="width: 200px; height: 150px;">
<img id="B" src="%s"
style="visibility: hidden; width: 200px; height: 150px;">
</body>
)HTML",
kImage1, kImage2)
.c_str());
// Every time we call a method on a Mojo interface, a message is posted to the
// current task queue. We need to ask the queue to drain itself before we
// check test expectations.
task_environment_.RunUntilIdle();
EXPECT_THAT(mock_annotator_service().image_ids_, ElementsAre(kImage1));
ASSERT_EQ(1u, mock_annotator_service().image_processors_.size());
EXPECT_TRUE(mock_annotator_service().image_processors_[0].is_bound());
EXPECT_EQ(1u, mock_annotator_service().callbacks_.size());
WebDocument document = GetMainFrame()->GetDocument();
WebAXObject root_obj = WebAXObject::FromWebDocument(document);
ASSERT_FALSE(root_obj.IsNull());
// Show node "B".
ExecuteJavaScriptForTests(
"document.getElementById('B').style.visibility = 'visible';");
SendPendingAccessibilityEvents();
ClearHandledUpdates();
// This should update the annotations of all images on the page, including the
// already visible one.
MarkSubtreeDirty(root_obj);
SendPendingAccessibilityEvents();
EXPECT_THAT(mock_annotator_service().image_ids_,
ElementsAre(kImage1, kImage2, kImage1, kImage2));
ASSERT_EQ(4u, mock_annotator_service().image_processors_.size());
EXPECT_TRUE(mock_annotator_service().image_processors_[0].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[1].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[2].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[3].is_bound());
EXPECT_EQ(4u, mock_annotator_service().callbacks_.size());
}
TEST_F(AXImageAnnotatorTest, OnImageUpdated) {
LoadHTMLAndRefreshAccessibilityTree(base::StringPrintf(R"HTML(
<body>
<p>Test document</p>
<img src="%s" style="width: 200px; height: 150px;">
</body>
)HTML",
kImage1)
.c_str());
// Every time we call a method on a Mojo interface, a message is posted to the
// current task queue. We need to ask the queue to drain itself before we
// check test expectations.
task_environment_.RunUntilIdle();
EXPECT_THAT(mock_annotator_service().image_ids_, ElementsAre(kImage1));
ASSERT_EQ(1u, mock_annotator_service().image_processors_.size());
EXPECT_TRUE(mock_annotator_service().image_processors_[0].is_bound());
EXPECT_EQ(1u, mock_annotator_service().callbacks_.size());
ClearHandledUpdates();
WebDocument document = GetMainFrame()->GetDocument();
WebAXObject root_obj = WebAXObject::FromWebDocument(document);
ASSERT_FALSE(root_obj.IsNull());
// This should update the annotations of all images on the page.
MarkSubtreeDirty(root_obj);
SendPendingAccessibilityEvents();
EXPECT_THAT(mock_annotator_service().image_ids_,
ElementsAre(kImage1, kImage1));
ASSERT_EQ(2u, mock_annotator_service().image_processors_.size());
EXPECT_TRUE(mock_annotator_service().image_processors_[0].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[1].is_bound());
EXPECT_EQ(2u, mock_annotator_service().callbacks_.size());
// Update node "A".
ExecuteJavaScriptForTests(
base::StringPrintf("document.querySelector('img').src = '%s';", kImage2));
SendPendingAccessibilityEvents();
ClearHandledUpdates();
// This should update the annotations of all images on the page, including the
// now updated image src.
SendPendingAccessibilityEvents();
EXPECT_THAT(mock_annotator_service().image_ids_,
ElementsAre(kImage1, kImage1, kImage2));
ASSERT_EQ(3u, mock_annotator_service().image_processors_.size());
EXPECT_TRUE(mock_annotator_service().image_processors_[0].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[1].is_bound());
EXPECT_TRUE(mock_annotator_service().image_processors_[2].is_bound());
EXPECT_EQ(3u, mock_annotator_service().callbacks_.size());
}
} // namespace content