blob: a1cb25ff7f52c45beb4d6034203315ad37a10020 [file] [log] [blame]
// 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 "chrome/browser/ui/views/accessibility/accessibility_focus_highlight.h"
#include <math.h>
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/focused_node_details.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/focus_changed_observer.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/image/image.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
// To rebaseline this test on all platforms:
// 1. Run a CQ+1 dry run.
// 2. Click the failing bots for android, windows, mac, and linux.
// 3. Find the failing browser_tests step.
// 4. Click the "Deterministic failure" link for the failing test case.
// 5. Copy the "Actual pixels" data url and paste into browser.
// 6. Save the image into your chromium checkout in
// chrome/test/data/accessibility.
class AccessibilityFocusHighlightBrowserTest : public InProcessBrowserTest {
public:
AccessibilityFocusHighlightBrowserTest() = default;
~AccessibilityFocusHighlightBrowserTest() override = default;
AccessibilityFocusHighlightBrowserTest(
const AccessibilityFocusHighlightBrowserTest&) = delete;
AccessibilityFocusHighlightBrowserTest& operator=(
const AccessibilityFocusHighlightBrowserTest&) = delete;
// InProcessBrowserTest overrides:
void SetUp() override {
EnablePixelOutput();
InProcessBrowserTest::SetUp();
}
bool ColorsApproximatelyEqual(SkColor color1, SkColor color2) {
return abs(static_cast<int>(SkColorGetR(color1)) -
static_cast<int>(SkColorGetR(color2))) < 50 &&
abs(static_cast<int>(SkColorGetG(color1)) -
static_cast<int>(SkColorGetG(color2))) < 50 &&
abs(static_cast<int>(SkColorGetB(color1)) -
static_cast<int>(SkColorGetB(color2))) < 50;
}
float CountPercentPixelsWithColor(const gfx::Image& image, SkColor color) {
SkBitmap bitmap = image.AsBitmap();
int count = 0;
for (int x = 0; x < bitmap.width(); ++x) {
for (int y = 0; y < bitmap.height(); ++y) {
if (ColorsApproximatelyEqual(color, bitmap.getColor(x, y))) {
count++;
}
}
}
return count * 100.0f / (bitmap.width() * bitmap.height());
}
gfx::Image CaptureWindowContents() {
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(browser());
views::Widget* widget = browser_view->GetWidget();
gfx::Rect bounds = widget->GetWindowBoundsInScreen();
bounds.Offset(-bounds.OffsetFromOrigin());
gfx::NativeView native_view = widget->GetNativeView();
// Keep trying until we get a successful capture.
while (true) {
base::test::TestFuture<gfx::Image> future;
ui::GrabViewSnapshot(native_view, bounds, future.GetCallback());
gfx::Image result_image = future.Take();
if (result_image.Size().IsEmpty()) {
LOG(INFO) << "Bitmap not correct size, trying to capture again";
continue;
}
// Skip through every 16th pixel (just for speed, no need to check
// every single one). If we find at least one opaque pixel then we
// assume we got a valid image. If the capture fails we sometimes get
// an all transparent image, but when it succeeds there can be a
// transparent edge.
bool found_opaque_pixel = false;
for (int x = 0; x < bounds.width() && !found_opaque_pixel; x += 16) {
for (int y = 0; y < bounds.height() && !found_opaque_pixel; y += 16) {
if (SkColorGetA(result_image.AsBitmap().getColor(x, y)) ==
SK_AlphaOPAQUE) {
found_opaque_pixel = true;
}
}
}
if (!found_opaque_pixel) {
LOG(INFO) << "Bitmap not opaque, trying to capture again";
continue;
}
return result_image;
}
}
};
// Smoke test that ensures that when a node gets focus, the layer with the
// focus highlight actually gets drawn.
//
// Flaky on all platforms. TODO(crbug.com/40692704): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
DISABLED_DrawsHighlight) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL("data:text/html,"
"<body style='background-color: rgb(204, 255, 255);'>"
"<div tabindex=0 id='div'>Focusable div</div>")));
AccessibilityFocusHighlight::SetNoFadeForTesting();
AccessibilityFocusHighlight::SkipActivationCheckForTesting();
AccessibilityFocusHighlight::UseDefaultColorForTesting();
browser()->profile()->GetPrefs()->SetBoolean(
prefs::kAccessibilityFocusHighlightEnabled, true);
// The web page has a background with a specific color. Keep looping until we
// capture an image of the page that's more than 90% that color.
gfx::Image image;
do {
base::RunLoop().RunUntilIdle();
image = CaptureWindowContents();
} while (CountPercentPixelsWithColor(image, SkColorSetRGB(204, 255, 255)) <
90.0f);
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
SkColor highlight_color =
browser_view->GetColorProvider()->GetColor(kColorFocusHighlightDefault);
// Initially less than 0.05% of the image should be the focus ring's highlight
// color.
ASSERT_LT(CountPercentPixelsWithColor(image, highlight_color), 0.05f);
// Focus something.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
std::string script("document.getElementById('div').focus();");
EXPECT_TRUE(content::ExecJs(web_contents, script));
// Now wait until at least 0.1% of the image has the focus ring's highlight
// color. If it never does, the test will time out.
do {
base::RunLoop().RunUntilIdle();
image = CaptureWindowContents();
} while (CountPercentPixelsWithColor(image, highlight_color) < 0.1f);
}
IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
FocusBoundsIncludeImages) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL("data:text/html,"
"<a id='link' href=''>"
"<img id='image' width='220' height='147' "
"style='vertical-align: middle;'>"
"</a>")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::FocusChangedObserver observer(web_contents);
std::string script("document.getElementById('link').focus();");
ASSERT_TRUE(content::ExecJs(web_contents, script));
auto details = observer.Wait();
gfx::Rect bounds = details.node_bounds_in_screen;
EXPECT_EQ(220, bounds.width());
// Not sure where the extra px of height are coming from...
EXPECT_EQ(147, bounds.height());
}
class ReadbackHolder : public base::RefCountedThreadSafe<ReadbackHolder> {
public:
ReadbackHolder() : run_loop_(std::make_unique<base::RunLoop>()) {}
void OutputRequestCallback(std::unique_ptr<viz::CopyOutputResult> result) {
if (result->IsEmpty()) {
result_.reset();
} else {
auto scoped_sk_bitmap = result->ScopedAccessSkBitmap();
result_ =
std::make_unique<SkBitmap>(scoped_sk_bitmap.GetOutScopedBitmap());
EXPECT_TRUE(result_->readyToDraw());
}
run_loop_->Quit();
}
void WaitForReadback() { run_loop_->Run(); }
const SkBitmap& result() const { return *result_; }
private:
friend class base::RefCountedThreadSafe<ReadbackHolder>;
virtual ~ReadbackHolder() = default;
std::unique_ptr<SkBitmap> result_;
std::unique_ptr<base::RunLoop> run_loop_;
};
const cc::ExactPixelComparator pixel_comparator;
// TODO(crbug.com/40924319): Fix flaky test on Mac.
// TODO(crbug.com/373535999): Fix flaky test on Windows.
// TODO(crbug.com/446071321): Fix flaky test on Linux.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
#define MAYBE_FocusAppearance DISABLED_FocusAppearance
#else
#define MAYBE_FocusAppearance FocusAppearance
#endif
IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
MAYBE_FocusAppearance) {
base::ScopedAllowBlockingForTesting allow_blocking;
AccessibilityFocusHighlight::SetNoFadeForTesting();
AccessibilityFocusHighlight::SkipActivationCheckForTesting();
browser()->profile()->GetPrefs()->SetBoolean(
prefs::kAccessibilityFocusHighlightEnabled, true);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GURL("data:text/html,"
"<a id='link' href=''>"
"<img width='10' height='10'>"
"</a>")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::FocusChangedObserver observer(web_contents);
std::string script("document.getElementById('link').focus();");
ASSERT_TRUE(content::ExecJs(web_contents, script));
observer.Wait();
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
AccessibilityFocusHighlight* highlight =
browser_view->GetAccessibilityFocusHighlightForTesting();
ui::Layer* layer = highlight->GetLayerForTesting();
gfx::Rect source_rect = gfx::Rect(layer->size());
scoped_refptr<ReadbackHolder> holder(new ReadbackHolder);
std::unique_ptr<viz::CopyOutputRequest> request =
std::make_unique<viz::CopyOutputRequest>(
viz::CopyOutputRequest::ResultFormat::RGBA,
viz::CopyOutputRequest::ResultDestination::kSystemMemory,
base::BindOnce(&ReadbackHolder::OutputRequestCallback, holder));
request->set_area(source_rect);
layer->RequestCopyOfOutput(std::move(request));
holder->WaitForReadback();
SkBitmap bitmap(holder->result());
ASSERT_FALSE(bitmap.drawsNothing());
base::FilePath dir_test_data;
ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &dir_test_data));
std::string screenshot_filename = "focus_highlight_appearance";
std::string platform_suffix;
#if BUILDFLAG(IS_MAC)
platform_suffix = "_mac";
#elif BUILDFLAG(IS_WIN)
platform_suffix = "_win";
#endif
base::FilePath golden_filepath =
dir_test_data.AppendASCII("accessibility")
.AppendASCII(screenshot_filename + ".png");
base::FilePath golden_filepath_platform =
golden_filepath.InsertBeforeExtensionASCII(platform_suffix);
if (base::PathExists(golden_filepath_platform)) {
golden_filepath = golden_filepath_platform;
}
bool snapshot_matches =
cc::MatchesPNGFile(bitmap, golden_filepath, pixel_comparator);
EXPECT_TRUE(snapshot_matches);
}