blob: e3ce5e3806303002108a70252775fe0bdeade217 [file] [log] [blame]
// Copyright (c) 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_impl.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "services/image_annotation/public/cpp/image_processor.h"
#include "services/image_annotation/public/mojom/image_annotation.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_tree.h"
#include "url/gurl.h"
constexpr base::FilePath::CharType kDocRoot[] =
FILE_PATH_LITERAL("chrome/test/data/accessibility");
namespace {
void DescribeNodesWithAnnotations(const ui::AXNode& node,
std::vector<std::string>* descriptions) {
std::string annotation =
node.GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation);
if (!annotation.empty()) {
descriptions->push_back(ui::ToString(node.data().role) + std::string(" ") +
annotation);
}
for (const auto* child : node.children())
DescribeNodesWithAnnotations(*child, descriptions);
}
std::vector<std::string> DescribeNodesWithAnnotations(
const ui::AXTreeUpdate& tree_update) {
ui::AXTree tree(tree_update);
std::vector<std::string> descriptions;
DCHECK(tree.root());
DescribeNodesWithAnnotations(*tree.root(), &descriptions);
return descriptions;
}
bool HasNodeWithAnnotationStatus(const ui::AXTreeUpdate& tree_update,
ax::mojom::ImageAnnotationStatus status) {
for (const auto& node_data : tree_update.nodes) {
if (node_data.GetImageAnnotationStatus() == status)
return true;
}
return false;
}
// A fake implementation of the Annotator mojo interface that
// returns predictable results based on the filename of the image
// it's asked to annotate. Enables us to test the rest of the
// system without using the real annotator that queries a back-end
// API.
class FakeAnnotator : public image_annotation::mojom::Annotator {
public:
static void SetReturnOcrResults(bool ocr) { return_ocr_results_ = ocr; }
static void SetReturnLabelResults(bool label) {
return_label_results_ = label;
}
static void SetReturnErrorCode(
image_annotation::mojom::AnnotateImageError error_code) {
return_error_code_ = error_code;
}
FakeAnnotator() = default;
~FakeAnnotator() override = default;
void BindReceiver(
mojo::PendingReceiver<image_annotation::mojom::Annotator> receiver) {
receivers_.Add(this, std::move(receiver));
}
void AnnotateImage(
const std::string& image_id,
const std::string& description_language_tag,
mojo::PendingRemote<image_annotation::mojom::ImageProcessor>
image_processor,
AnnotateImageCallback callback) override {
if (return_error_code_) {
image_annotation::mojom::AnnotateImageResultPtr result =
image_annotation::mojom::AnnotateImageResult::NewErrorCode(
*return_error_code_);
std::move(callback).Run(std::move(result));
return;
}
// Use the filename to create an annotation string.
// Adds some trailing whitespace and punctuation to check that clean-up
// happens correctly when combining annotation strings.
std::string image_filename = GURL(image_id).ExtractFileName();
image_annotation::mojom::AnnotationPtr ocr_annotation =
image_annotation::mojom::Annotation::New(
image_annotation::mojom::AnnotationType::kOcr, 1.0,
image_filename + " Annotation . ");
image_annotation::mojom::AnnotationPtr label_annotation =
image_annotation::mojom::Annotation::New(
image_annotation::mojom::AnnotationType::kLabel, 1.0,
image_filename + " '" + description_language_tag + "' Label");
// Return enabled results as an annotation.
std::vector<image_annotation::mojom::AnnotationPtr> annotations;
if (return_ocr_results_)
annotations.push_back(std::move(ocr_annotation));
if (return_label_results_)
annotations.push_back(std::move(label_annotation));
image_annotation::mojom::AnnotateImageResultPtr result =
image_annotation::mojom::AnnotateImageResult::NewAnnotations(
std::move(annotations));
std::move(callback).Run(std::move(result));
}
private:
mojo::ReceiverSet<image_annotation::mojom::Annotator> receivers_;
static bool return_ocr_results_;
static bool return_label_results_;
static base::Optional<image_annotation::mojom::AnnotateImageError>
return_error_code_;
DISALLOW_COPY_AND_ASSIGN(FakeAnnotator);
};
// static
bool FakeAnnotator::return_ocr_results_ = false;
// static
bool FakeAnnotator::return_label_results_ = false;
// static
base::Optional<image_annotation::mojom::AnnotateImageError>
FakeAnnotator::return_error_code_;
// The fake ImageAnnotationService, which handles mojo calls from the renderer
// process and passes them to FakeAnnotator.
class FakeImageAnnotationService
: public image_annotation::mojom::ImageAnnotationService {
public:
FakeImageAnnotationService() = default;
~FakeImageAnnotationService() override = default;
private:
// image_annotation::mojom::ImageAnnotationService:
void BindAnnotator(mojo::PendingReceiver<image_annotation::mojom::Annotator>
receiver) override {
annotator_.BindReceiver(std::move(receiver));
}
FakeAnnotator annotator_;
DISALLOW_COPY_AND_ASSIGN(FakeImageAnnotationService);
};
void BindImageAnnotatorService(
mojo::PendingReceiver<image_annotation::mojom::ImageAnnotationService>
receiver) {
mojo::MakeSelfOwnedReceiver(std::make_unique<FakeImageAnnotationService>(),
std::move(receiver));
}
} // namespace
class ImageAnnotationBrowserTest : public InProcessBrowserTest {
public:
ImageAnnotationBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
https_server_.AddDefaultHandlers(base::FilePath(kDocRoot));
}
protected:
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(
features::kExperimentalAccessibilityLabels);
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(https_server_.Start());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ProfileImpl::OverrideImageAnnotationServiceBinderForTesting(
base::BindRepeating(&BindImageAnnotatorService));
ui::AXMode mode = ui::kAXModeComplete;
mode.set_mode(ui::AXMode::kLabelImages, true);
web_contents->SetAccessibilityMode(mode);
SetAcceptLanguages("en,fr");
}
void TearDownOnMainThread() override {
ProfileImpl::OverrideImageAnnotationServiceBinderForTesting(
base::NullCallback());
InProcessBrowserTest::TearDownOnMainThread();
}
void SetAcceptLanguages(const std::string& accept_languages) {
content::BrowserContext* context =
static_cast<content::BrowserContext*>(browser()->profile());
DCHECK(context);
PrefService* prefs = user_prefs::UserPrefs::Get(context);
DCHECK(prefs);
prefs->Set(language::prefs::kAcceptLanguages,
base::Value(accept_languages));
}
protected:
net::EmbeddedTestServer https_server_;
private:
base::test::ScopedFeatureList scoped_feature_list_;
DISALLOW_COPY_AND_ASSIGN(ImageAnnotationBrowserTest);
};
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest,
AnnotateImageInAccessibilityTree) {
FakeAnnotator::SetReturnOcrResults(true);
FakeAnnotator::SetReturnLabelResults(true);
ui_test_utils::NavigateToURL(browser(),
https_server_.GetURL("/image_annotation.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::WaitForAccessibilityTreeToContainNodeWithName(
web_contents,
"Appears to say: red.png Annotation. Appears to be: red.png 'en' Label");
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, ImagesInLinks) {
FakeAnnotator::SetReturnOcrResults(true);
ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("/image_annotation_link.html"));
// Block until the accessibility tree has at least 8 annotations. If
// that never happens, the test will time out.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
while (10 > DescribeNodesWithAnnotations(
content::GetAccessibilityTreeSnapshot(web_contents))
.size()) {
content::WaitForAccessibilityTreeToChange(web_contents);
}
// All images should be annotated. Only links that contain exactly one image
// should be annotated.
ui::AXTreeUpdate ax_tree_update =
content::GetAccessibilityTreeSnapshot(web_contents);
EXPECT_THAT(
DescribeNodesWithAnnotations(ax_tree_update),
testing::ElementsAre("image Appears to say: red.png Annotation",
"link Appears to say: green.png Annotation",
"image Appears to say: green.png Annotation",
"image Appears to say: red.png Annotation",
"image Appears to say: printer.png Annotation",
"image Appears to say: red.png Annotation",
"link Appears to say: printer.png Annotation",
"image Appears to say: printer.png Annotation",
"link Appears to say: green.png Annotation",
"image Appears to say: green.png Annotation"));
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, ImageDoc) {
FakeAnnotator::SetReturnOcrResults(true);
ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("/image_annotation_doc.html"));
// Block until the accessibility tree has at least 2 annotations. If
// that never happens, the test will time out.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
while (2 > DescribeNodesWithAnnotations(
content::GetAccessibilityTreeSnapshot(web_contents))
.size()) {
content::WaitForAccessibilityTreeToChange(web_contents);
}
// When a document contains exactly one image, the document should be
// annotated with the image's annotation, too.
ui::AXTreeUpdate ax_tree_update =
content::GetAccessibilityTreeSnapshot(web_contents);
EXPECT_THAT(
DescribeNodesWithAnnotations(ax_tree_update),
testing::ElementsAre("rootWebArea Appears to say: red.png Annotation",
"image Appears to say: red.png Annotation"));
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, ImageUrl) {
FakeAnnotator::SetReturnOcrResults(true);
ui_test_utils::NavigateToURL(browser(), https_server_.GetURL("/red.png"));
// Block until the accessibility tree has at least 2 annotations. If
// that never happens, the test will time out.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
while (2 > DescribeNodesWithAnnotations(
content::GetAccessibilityTreeSnapshot(web_contents))
.size()) {
content::WaitForAccessibilityTreeToChange(web_contents);
}
// When a document contains exactly one image, the document should be
// annotated with the image's annotation, too.
ui::AXTreeUpdate ax_tree_update =
content::GetAccessibilityTreeSnapshot(web_contents);
EXPECT_THAT(
DescribeNodesWithAnnotations(ax_tree_update),
testing::ElementsAre("rootWebArea Appears to say: red.png Annotation",
"image Appears to say: red.png Annotation"));
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, NoAnnotationsAvailable) {
// Don't return any results.
FakeAnnotator::SetReturnOcrResults(false);
FakeAnnotator::SetReturnLabelResults(false);
ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("/image_annotation_doc.html"));
// Block until the annotation status for the root is empty. If that
// never occurs then the test will time out.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui::AXTreeUpdate snapshot =
content::GetAccessibilityTreeSnapshot(web_contents);
while (snapshot.nodes[0].GetImageAnnotationStatus() !=
ax::mojom::ImageAnnotationStatus::kAnnotationEmpty) {
content::WaitForAccessibilityTreeToChange(web_contents);
snapshot = content::GetAccessibilityTreeSnapshot(web_contents);
}
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, AnnotationError) {
// Return an error code.
FakeAnnotator::SetReturnErrorCode(
image_annotation::mojom::AnnotateImageError::kFailure);
ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("/image_annotation_doc.html"));
// Block until the annotation status for the root contains an error code. If
// that never occurs then the test will time out.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui::AXTreeUpdate snapshot =
content::GetAccessibilityTreeSnapshot(web_contents);
while (snapshot.nodes[0].GetImageAnnotationStatus() !=
ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed) {
content::WaitForAccessibilityTreeToChange(web_contents);
snapshot = content::GetAccessibilityTreeSnapshot(web_contents);
}
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest, ImageWithSrcSet) {
FakeAnnotator::SetReturnOcrResults(true);
FakeAnnotator::SetReturnLabelResults(true);
ui_test_utils::NavigateToURL(browser(),
https_server_.GetURL("/image_srcset.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::WaitForAccessibilityTreeToContainNodeWithName(
web_contents,
"Appears to say: red.png Annotation. Appears to be: red.png 'en' Label");
}
// Disabled due to flakiness. http://crbug.com/983404
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest,
DISABLED_AnnotationLanguages) {
FakeAnnotator::SetReturnOcrResults(true);
FakeAnnotator::SetReturnLabelResults(true);
ui_test_utils::NavigateToURL(browser(),
https_server_.GetURL("/image_annotation.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::WaitForAccessibilityTreeToContainNodeWithName(
web_contents,
"Appears to say: red.png Annotation. Appears to be: red.png 'en' Label");
SetAcceptLanguages("fr,en");
ui_test_utils::NavigateToURL(browser(),
https_server_.GetURL("/image_annotation.html"));
web_contents = browser()->tab_strip_model()->GetActiveWebContents();
content::WaitForAccessibilityTreeToContainNodeWithName(
web_contents,
"Appears to say: red.png Annotation. Appears to be: red.png 'fr' Label");
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest,
DoesntAnnotateInternalPages) {
FakeAnnotator::SetReturnLabelResults(true);
ui_test_utils::NavigateToURL(browser(), GURL("chrome://version"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui::AXMode mode = ui::kAXModeComplete;
mode.set_mode(ui::AXMode::kLabelImages, true);
web_contents->SetAccessibilityMode(mode);
std::string svg_image =
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><circle "
"cx='50' cy='50' r='40' fill='yellow' /></svg>";
const std::string javascript =
"var image = document.createElement('img');"
"image.src = \"" +
svg_image +
"\";"
"var outer = document.getElementById('outer');"
"outer.insertBefore(image, outer.childNodes[0]);";
EXPECT_TRUE(content::ExecuteScript(web_contents, javascript));
ui::AXTreeUpdate snapshot =
content::GetAccessibilityTreeSnapshot(web_contents);
// Wait for the accessibility tree to contain an error that the image cannot
// be annotated due to the page url's scheme.
while (!HasNodeWithAnnotationStatus(
snapshot,
ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme)) {
content::WaitForAccessibilityTreeToChange(web_contents);
snapshot = content::GetAccessibilityTreeSnapshot(web_contents);
}
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest,
TutorMessageOnlyOnFirstImage) {
// We should not promote the image annotation service on more than one image
// in the same renderer.
FakeAnnotator::SetReturnOcrResults(false);
FakeAnnotator::SetReturnLabelResults(false);
// The following test page should have at least two images on it.
ui_test_utils::NavigateToURL(browser(),
https_server_.GetURL("/image_annotation.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui::AXMode mode = ui::kAXModeComplete;
mode.set_mode(ui::AXMode::kLabelImages, false);
web_contents->SetAccessibilityMode(mode);
// Block until there are at least two images that have been processed. One of
// them should get the tutor message and the other shouldn't. The annotation
// status for the image that didn't get the tutor message should be
// kSilentlyEligibleForAnnotation whilst the status for the image that did
// should be kEligibleForAnnotation. If that never occurs then the test will
// time out.
ui::AXTreeUpdate snapshot =
content::GetAccessibilityTreeSnapshot(web_contents);
while (
!HasNodeWithAnnotationStatus(
snapshot,
ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation) ||
!HasNodeWithAnnotationStatus(
snapshot, ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation)) {
content::WaitForAccessibilityTreeToChange(web_contents);
snapshot = content::GetAccessibilityTreeSnapshot(web_contents);
}
}
IN_PROC_BROWSER_TEST_F(ImageAnnotationBrowserTest,
TutorMessageOnlyOnFirstImageInLinks) {
// We should not promote the image annotation service on more than one image
// in the same renderer.
FakeAnnotator::SetReturnOcrResults(false);
FakeAnnotator::SetReturnLabelResults(false);
// The following test page should have at least two images on it.
ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("/image_annotation_link.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui::AXMode mode = ui::kAXModeComplete;
mode.set_mode(ui::AXMode::kLabelImages, false);
web_contents->SetAccessibilityMode(mode);
// Block until there are at least two images that have been processed. One of
// them should get the tutor message and the other shouldn't. The annotation
// status for the image that didn't get the tutor message should be
// kSilentlyEligibleForAnnotation whilst the status for the image that did
// should be kEligibleForAnnotation. If that never occurs then the test will
// time out.
ui::AXTreeUpdate snapshot =
content::GetAccessibilityTreeSnapshot(web_contents);
while (
!HasNodeWithAnnotationStatus(
snapshot,
ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation) ||
!HasNodeWithAnnotationStatus(
snapshot, ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation)) {
content::WaitForAccessibilityTreeToChange(web_contents);
snapshot = content::GetAccessibilityTreeSnapshot(web_contents);
}
}