blob: 9a5926ea468ad4134c51fa3836c22835b09fed92 [file] [log] [blame]
// Copyright 2018 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 "ash/accessibility/accessibility_highlight_controller.h"
#include <cmath>
#include <memory>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/accessibility_cursor_ring_layer.h"
#include "ash/accessibility/accessibility_focus_ring_controller.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/macros.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/aura/window.h"
#include "ui/base/ime/dummy_text_input_client.h"
#include "ui/compositor/compositor_switches.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/gfx/image/image.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
class MockTextInputClient : public ui::DummyTextInputClient {
public:
MockTextInputClient() : ui::DummyTextInputClient(ui::TEXT_INPUT_TYPE_TEXT) {}
~MockTextInputClient() override = default;
void SetCaretBounds(const gfx::Rect& bounds) { caret_bounds_ = bounds; }
private:
gfx::Rect GetCaretBounds() const override { return caret_bounds_; }
gfx::Rect caret_bounds_;
DISALLOW_COPY_AND_ASSIGN(MockTextInputClient);
};
} // namespace
class AccessibilityHighlightControllerTest : public AshTestBase {
protected:
AccessibilityHighlightControllerTest() = default;
~AccessibilityHighlightControllerTest() override = default;
void SetUp() override {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kEnablePixelOutputInTests);
AshTestBase::SetUp();
Shell::Get()->accessibility_focus_ring_controller()->SetNoFadeForTesting();
}
void CaptureBeforeImage(const gfx::Rect& bounds) {
Capture(bounds);
if (before_bmp_.tryAllocPixels(image_.AsBitmap().info())) {
image_.AsBitmap().readPixels(before_bmp_.info(), before_bmp_.getPixels(),
before_bmp_.rowBytes(), 0, 0);
}
}
void CaptureAfterImage(const gfx::Rect& bounds) {
Capture(bounds);
if (after_bmp_.tryAllocPixels(image_.AsBitmap().info())) {
image_.AsBitmap().readPixels(after_bmp_.info(), after_bmp_.getPixels(),
after_bmp_.rowBytes(), 0, 0);
}
}
void ComputeImageStats() {
diff_count_ = 0;
double accum[4] = {0, 0, 0, 0};
for (int x = 0; x < before_bmp_.width(); ++x) {
for (int y = 0; y < before_bmp_.height(); ++y) {
SkColor before_color = before_bmp_.getColor(x, y);
SkColor after_color = after_bmp_.getColor(x, y);
if (before_color != after_color) {
diff_count_++;
accum[0] += SkColorGetB(after_color);
accum[1] += SkColorGetG(after_color);
accum[2] += SkColorGetR(after_color);
accum[3] += SkColorGetA(after_color);
}
}
}
average_diff_color_ =
SkColorSetARGB(static_cast<unsigned char>(accum[3] / diff_count_),
static_cast<unsigned char>(accum[2] / diff_count_),
static_cast<unsigned char>(accum[1] / diff_count_),
static_cast<unsigned char>(accum[0] / diff_count_));
}
int diff_count() const { return diff_count_; }
SkColor average_diff_color() const { return average_diff_color_; }
void Capture(const gfx::Rect& bounds) {
// Occasionally we don't get any pixels the first try.
// Keep trying until we get the correct size butmap and
// the first pixel is not transparent.
while (true) {
aura::Window* window = ash::Shell::GetPrimaryRootWindow();
base::RunLoop run_loop;
ui::GrabWindowSnapshotAndScaleAsync(
window, bounds, bounds.size(),
base::Bind(
[](base::RunLoop* run_loop, gfx::Image* image,
gfx::Image got_image) {
run_loop->Quit();
*image = got_image;
},
&run_loop, &image_));
run_loop.Run();
SkBitmap bitmap = image_.AsBitmap();
if (bitmap.width() != bounds.width() ||
bitmap.height() != bounds.height()) {
LOG(INFO) << "Bitmap not correct size, trying to capture again";
continue;
} else if (255 == SkColorGetA(bitmap.getColor(0, 0))) {
LOG(INFO) << "Bitmap is transparent, trying to capture again";
break;
}
}
}
private:
gfx::Image image_;
SkBitmap before_bmp_;
SkBitmap after_bmp_;
int diff_count_ = 0;
SkColor average_diff_color_ = 0;
DISALLOW_COPY_AND_ASSIGN(AccessibilityHighlightControllerTest);
};
TEST_F(AccessibilityHighlightControllerTest, TestCaretRingDrawsBluePixels) {
// Create a white background window for captured image color smoke test.
CreateTestWindowInShell(SK_ColorWHITE, -1,
Shell::GetPrimaryRootWindow()->bounds());
gfx::Rect capture_bounds(200, 300, 100, 100);
gfx::Rect caret_bounds(230, 330, 1, 25);
CaptureBeforeImage(capture_bounds);
AccessibilityHighlightController controller;
controller.HighlightCaret(true);
MockTextInputClient text_input_client;
text_input_client.SetCaretBounds(caret_bounds);
controller.OnCaretBoundsChanged(&text_input_client);
CaptureAfterImage(capture_bounds);
ComputeImageStats();
// This is a smoke test to assert that something is drawn in the right part of
// the screen of approximately the right size and color.
// There's deliberately some tolerance for tiny errors.
EXPECT_NEAR(1487, diff_count(), 50);
EXPECT_NEAR(175, SkColorGetR(average_diff_color()), 5);
EXPECT_NEAR(175, SkColorGetG(average_diff_color()), 5);
EXPECT_NEAR(255, SkColorGetB(average_diff_color()), 5);
}
TEST_F(AccessibilityHighlightControllerTest, TestFocusRingDrawsPixels) {
// Create a white background window for captured image color smoke test.
CreateTestWindowInShell(SK_ColorWHITE, -1,
Shell::GetPrimaryRootWindow()->bounds());
gfx::Rect capture_bounds(200, 300, 100, 100);
gfx::Rect focus_bounds(230, 330, 40, 40);
CaptureBeforeImage(capture_bounds);
AccessibilityHighlightController controller;
controller.HighlightFocus(true);
controller.SetFocusHighlightRect(focus_bounds);
CaptureAfterImage(capture_bounds);
ComputeImageStats();
// This is a smoke test to assert that something is drawn in the right part of
// the screen of approximately the right size and color.
// There's deliberately some tolerance for tiny errors.
EXPECT_NEAR(1608, diff_count(), 50);
EXPECT_NEAR(255, SkColorGetR(average_diff_color()), 5);
EXPECT_NEAR(201, SkColorGetG(average_diff_color()), 5);
EXPECT_NEAR(152, SkColorGetB(average_diff_color()), 5);
}
// Integration test of cursor handling between AccessibilityHighlightController
// and AccessibilityFocusRingController.
TEST_F(AccessibilityHighlightControllerTest, CursorWorksOnMultipleDisplays) {
UpdateDisplay("400x400,500x500");
aura::Window::Windows root_windows = ash::Shell::Get()->GetAllRootWindows();
ASSERT_EQ(2u, root_windows.size());
AccessibilityHighlightController highlight_controller;
highlight_controller.HighlightCursor(true);
gfx::Point location(90, 90);
ui::MouseEvent event0(ui::ET_MOUSE_MOVED, location, location,
ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi event_mod(&event0);
event_mod.set_target(root_windows[0]);
highlight_controller.OnMouseEvent(&event0);
AccessibilityFocusRingController* focus_ring_controller =
Shell::Get()->accessibility_focus_ring_controller();
auto* cursor_layer = focus_ring_controller->cursor_layer_for_testing();
EXPECT_EQ(root_windows[0], cursor_layer->root_window());
EXPECT_LT(
std::abs(cursor_layer->layer()->GetTargetBounds().x() - location.x()),
50);
EXPECT_LT(
std::abs(cursor_layer->layer()->GetTargetBounds().y() - location.y()),
50);
ui::MouseEvent event1(ui::ET_MOUSE_MOVED, location, location,
ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi event_mod1(&event1);
event_mod1.set_target(root_windows[1]);
highlight_controller.OnMouseEvent(&event1);
cursor_layer = focus_ring_controller->cursor_layer_for_testing();
EXPECT_EQ(root_windows[1], cursor_layer->root_window());
EXPECT_LT(
std::abs(cursor_layer->layer()->GetTargetBounds().x() - location.x()),
50);
EXPECT_LT(
std::abs(cursor_layer->layer()->GetTargetBounds().y() - location.y()),
50);
}
// Integration test of caret handling between AccessibilityHighlightController
// and AccessibilityFocusRingController.
TEST_F(AccessibilityHighlightControllerTest, CaretRingDrawnOnlyWithinBounds) {
// Given caret bounds that are not within the active window, expect that the
// caret ring highlight is not drawn.
std::unique_ptr<views::Widget> window = CreateTestWidget();
window->SetBounds(gfx::Rect(5, 5, 300, 300));
AccessibilityHighlightController highlight_controller;
MockTextInputClient text_input_client;
highlight_controller.HighlightCaret(true);
gfx::Rect caret_bounds(10, 10, 40, 40);
text_input_client.SetCaretBounds(caret_bounds);
highlight_controller.OnCaretBoundsChanged(&text_input_client);
AccessibilityFocusRingController* focus_ring_controller =
Shell::Get()->accessibility_focus_ring_controller();
auto* caret_layer = focus_ring_controller->caret_layer_for_testing();
EXPECT_EQ(
std::abs(caret_layer->layer()->GetTargetBounds().x() - caret_bounds.x()),
20);
EXPECT_EQ(
std::abs(caret_layer->layer()->GetTargetBounds().y() - caret_bounds.y()),
20);
gfx::Rect not_visible_bounds(301, 301, 10, 10);
text_input_client.SetCaretBounds(not_visible_bounds);
highlight_controller.OnCaretBoundsChanged(&text_input_client);
EXPECT_FALSE(focus_ring_controller->caret_layer_for_testing());
}
// Tests that a zero-width text caret still results in a visible highlight.
// https://crbug.com/882762
TEST_F(AccessibilityHighlightControllerTest, ZeroWidthCaretRingVisible) {
AccessibilityHighlightController highlight_controller;
MockTextInputClient text_input_client;
highlight_controller.HighlightCaret(true);
// Simulate a zero-width text caret.
gfx::Rect zero_width(0, 16);
text_input_client.SetCaretBounds(zero_width);
highlight_controller.OnCaretBoundsChanged(&text_input_client);
// Caret ring is created.
EXPECT_TRUE(Shell::Get()
->accessibility_focus_ring_controller()
->caret_layer_for_testing());
// Simulate an empty text caret.
gfx::Rect empty;
text_input_client.SetCaretBounds(empty);
highlight_controller.OnCaretBoundsChanged(&text_input_client);
// Caret ring is gone.
EXPECT_FALSE(Shell::Get()
->accessibility_focus_ring_controller()
->caret_layer_for_testing());
}
// Tests setting the caret bounds explicitly via AccessibilityController, rather
// than via the input method observer. This path is used in production in mash.
TEST_F(AccessibilityHighlightControllerTest, SetCaretBounds) {
std::unique_ptr<views::Widget> window = CreateTestWidget();
window->SetBounds(gfx::Rect(5, 5, 300, 300));
AccessibilityController* accessibility_controller =
Shell::Get()->accessibility_controller();
accessibility_controller->SetCaretHighlightEnabled(true);
// Bounds inside the active window create a highlight.
accessibility_controller->SetCaretBounds(gfx::Rect(10, 10, 1, 16));
AccessibilityFocusRingController* focus_ring_controller =
Shell::Get()->accessibility_focus_ring_controller();
EXPECT_TRUE(focus_ring_controller->caret_layer_for_testing());
// Empty bounds remove the highlight.
accessibility_controller->SetCaretBounds(gfx::Rect());
EXPECT_FALSE(focus_ring_controller->caret_layer_for_testing());
}
} // namespace ash