blob: 99e5dec1a389339c68cfb64c5efb3bc4f12e2735 [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 "chrome/browser/ash/accessibility/facegaze_test_utils.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_writer.h"
#include "base/path_service.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_registry_test_helper.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/gfx/geometry/point.h"
namespace ash {
using FaceGazeGesture = FaceGazeTestUtils::FaceGazeGesture;
using MediapipeGesture = FaceGazeTestUtils::MediapipeGesture;
namespace {
const char* kDefaultDisplaySize = "1200x800";
constexpr char kMediapipeMV2TestFilePath[] =
"resources/chromeos/accessibility/accessibility_common/mv2/third_party/"
"mediapipe_task_vision";
constexpr char kMediapipeMV3TestFilePath[] =
"resources/chromeos/accessibility/accessibility_common/mv3/third_party/"
"mediapipe_task_vision";
const int kMouseDeviceId = 1;
constexpr char kTestSupportMV2Path[] =
"chrome/browser/resources/chromeos/accessibility/accessibility_common/mv2/"
"facegaze/facegaze_test_support.js";
constexpr char kTestSupportMV3Path[] =
"chrome/browser/resources/chromeos/accessibility/accessibility_common/mv3/"
"facegaze/facegaze_test_support.js";
PrefService* GetPrefs() {
return AccessibilityManager::Get()->profile()->GetPrefs();
}
} // namespace
FaceGazeTestUtils::Config::Config() = default;
FaceGazeTestUtils::Config::~Config() = default;
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::Default() {
forehead_location_ = gfx::PointF(0.1, 0.2);
cursor_location_ = gfx::Point(600, 400);
cursor_speeds_ = {/*up=*/20, /*down=*/20, /*left=*/20, /*right=*/20};
buffer_size_ = 1;
use_cursor_acceleration_ = false;
use_landmark_weights_ = false;
use_velocity_threshold_ = false;
dialog_accepted_ = true;
// For simplicity, allow tests to recognize gestures instantly rather than
// requiring a valid duration.
use_gesture_duration_ = false;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithForeheadLocation(
const gfx::PointF& location) {
forehead_location_ = location;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithCursorLocation(
const gfx::Point& location) {
cursor_location_ = location;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithBufferSize(int size) {
buffer_size_ = size;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithCursorAcceleration(
bool acceleration) {
use_cursor_acceleration_ = acceleration;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithDialogAccepted(
bool accepted) {
dialog_accepted_ = accepted;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithBindings(
const base::flat_map<FaceGazeGesture, MacroName>& gestures_to_macros,
const base::flat_map<FaceGazeGesture, int>& gesture_confidences) {
gestures_to_macros_ = std::move(gestures_to_macros);
gesture_confidences_ = std::move(gesture_confidences);
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithCursorSpeeds(
const CursorSpeeds& speeds) {
cursor_speeds_ = speeds;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithGestureRepeatDelayMs(
int delay) {
gesture_repeat_delay_ms_ = delay;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithLandmarkWeights(
bool use_weights) {
use_landmark_weights_ = use_weights;
return *this;
}
FaceGazeTestUtils::Config& FaceGazeTestUtils::Config::WithVelocityThreshold(
bool use_threshold) {
use_velocity_threshold_ = use_threshold;
return *this;
}
FaceGazeTestUtils::MockFaceLandmarkerResult::MockFaceLandmarkerResult() =
default;
FaceGazeTestUtils::MockFaceLandmarkerResult::~MockFaceLandmarkerResult() =
default;
FaceGazeTestUtils::MockFaceLandmarkerResult&
FaceGazeTestUtils::MockFaceLandmarkerResult::WithNormalizedForeheadLocation(
const std::pair<double, double>& location) {
forehead_location_.Set("x", location.first);
forehead_location_.Set("y", location.second);
return *this;
}
FaceGazeTestUtils::MockFaceLandmarkerResult&
FaceGazeTestUtils::MockFaceLandmarkerResult::WithGesture(
const MediapipeGesture& gesture,
int confidence) {
// For readability and consistency with the gesture confidence pref, this
// method accepts confidence values [0, 100]. However, the FaceLandmarker
// receives confidence scores as values [0, 1], so we need to convert the
// confidence to a decimal before processing it.
recognized_gestures_.Append(
base::Value::Dict()
.Set("categoryName", ToString(gesture))
.Set("score", static_cast<double>(confidence) / 100.0));
return *this;
}
FaceGazeTestUtils::MockFaceLandmarkerResult&
FaceGazeTestUtils::MockFaceLandmarkerResult::WithLatency(int latency) {
latency_ = latency;
return *this;
}
FaceGazeTestUtils::FaceGazeTestUtils() = default;
FaceGazeTestUtils::~FaceGazeTestUtils() = default;
// static.
std::string FaceGazeTestUtils::ToString(const FaceGazeGesture& gesture) {
switch (gesture) {
case FaceGazeGesture::BROW_INNER_UP:
return "browInnerUp";
case FaceGazeGesture::BROWS_DOWN:
return "browsDown";
case FaceGazeGesture::EYE_SQUINT_LEFT:
return "eyeSquintLeft";
case FaceGazeGesture::EYE_SQUINT_RIGHT:
return "eyeSquintRight";
case FaceGazeGesture::EYES_BLINK:
return "eyesBlink";
case FaceGazeGesture::EYES_LOOK_DOWN:
return "eyesLookDown";
case FaceGazeGesture::EYES_LOOK_LEFT:
return "eyesLookLeft";
case FaceGazeGesture::EYES_LOOK_RIGHT:
return "eyesLookRight";
case FaceGazeGesture::EYES_LOOK_UP:
return "eyesLookUp";
case FaceGazeGesture::JAW_LEFT:
return "jawLeft";
case FaceGazeGesture::JAW_OPEN:
return "jawOpen";
case FaceGazeGesture::JAW_RIGHT:
return "jawRight";
case FaceGazeGesture::MOUTH_FUNNEL:
return "mouthFunnel";
case FaceGazeGesture::MOUTH_LEFT:
return "mouthLeft";
case FaceGazeGesture::MOUTH_PUCKER:
return "mouthPucker";
case FaceGazeGesture::MOUTH_RIGHT:
return "mouthRight";
case FaceGazeGesture::MOUTH_SMILE:
return "mouthSmile";
case FaceGazeGesture::MOUTH_UPPER_UP:
return "mouthUpperUp";
}
}
// static.
std::string FaceGazeTestUtils::ToString(const MediapipeGesture& gesture) {
switch (gesture) {
case MediapipeGesture::BROW_DOWN_LEFT:
return "browDownLeft";
case MediapipeGesture::BROW_DOWN_RIGHT:
return "browDownRight";
case MediapipeGesture::BROW_INNER_UP:
return "browInnerUp";
case MediapipeGesture::EYE_BLINK_LEFT:
return "eyeBlinkLeft";
case MediapipeGesture::EYE_BLINK_RIGHT:
return "eyeBlinkRight";
case MediapipeGesture::EYE_LOOK_DOWN_LEFT:
return "eyeLookDownLeft";
case MediapipeGesture::EYE_LOOK_DOWN_RIGHT:
return "eyeLookDownRight";
case MediapipeGesture::EYE_LOOK_IN_LEFT:
return "eyeLookInLeft";
case MediapipeGesture::EYE_LOOK_IN_RIGHT:
return "eyeLookInRight";
case MediapipeGesture::EYE_LOOK_OUT_LEFT:
return "eyeLookOutLeft";
case MediapipeGesture::EYE_LOOK_OUT_RIGHT:
return "eyeLookOutRight";
case MediapipeGesture::EYE_LOOK_UP_LEFT:
return "eyeLookUpLeft";
case MediapipeGesture::EYE_LOOK_UP_RIGHT:
return "eyeLookUpRight";
case MediapipeGesture::EYE_SQUINT_LEFT:
return "eyeSquintLeft";
case MediapipeGesture::EYE_SQUINT_RIGHT:
return "eyeSquintRight";
case MediapipeGesture::JAW_LEFT:
return "jawLeft";
case MediapipeGesture::JAW_OPEN:
return "jawOpen";
case MediapipeGesture::JAW_RIGHT:
return "jawRight";
case MediapipeGesture::MOUTH_FUNNEL:
return "mouthFunnel";
case MediapipeGesture::MOUTH_LEFT:
return "mouthLeft";
case MediapipeGesture::MOUTH_PUCKER:
return "mouthPucker";
case MediapipeGesture::MOUTH_RIGHT:
return "mouthRight";
case MediapipeGesture::MOUTH_SMILE_LEFT:
return "mouthSmileLeft";
case MediapipeGesture::MOUTH_SMILE_RIGHT:
return "mouthSmileRight";
case MediapipeGesture::MOUTH_UPPER_UP_LEFT:
return "mouthUpperUpLeft";
case MediapipeGesture::MOUTH_UPPER_UP_RIGHT:
return "mouthUpperUpRight";
}
}
void FaceGazeTestUtils::EnableFaceGaze(const Config& config) {
// TODO(b/309121742): Add display size to Config so that tests can configure
// it.
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.UpdateDisplay(kDefaultDisplaySize);
event_generator_ = std::make_unique<ui::test::EventGenerator>(
Shell::Get()->GetPrimaryRootWindow());
// Before enabling FaceGaze, ensure that the dialog accepted pref matches
// what is specified in the config.
GetPrefs()->SetBoolean(
prefs::kAccessibilityFaceGazeAcceleratorDialogHasBeenAccepted,
config.dialog_accepted());
const bool v3_manifest =
::features::IsAccessibilityManifestV3EnabledForAccessibilityCommon();
FaceGazeTestUtils::SetUpMediapipeDir(v3_manifest ? kMediapipeMV3TestFilePath
: kMediapipeMV2TestFilePath);
ASSERT_FALSE(AccessibilityManager::Get()->IsFaceGazeEnabled());
// Use ExtensionHostTestHelper to detect when the accessibility common
// extension loads.
extensions::ExtensionHostTestHelper host_helper(
AccessibilityManager::Get()->profile(),
extension_misc::kAccessibilityCommonExtensionId);
// Watch events from an MV3 extension which runs in a service worker.
extensions::ExtensionRegistryTestHelper observer(
extension_misc::kAccessibilityCommonExtensionId,
AccessibilityManager::Get()->profile());
AccessibilityManager::Get()->EnableFaceGaze(true);
if (observer.WaitForManifestVersion() == 3) {
observer.WaitForServiceWorkerStart();
} else {
host_helper.WaitForHostCompletedFirstLoad();
}
WaitForJSReady();
SetUpJSTestSupport(v3_manifest ? kTestSupportMV3Path : kTestSupportMV2Path);
if (config.dialog_accepted()) {
// The FaceLandmarker will be automatically initialized after the dialog has
// been accepted.
WaitForFaceLandmarker();
}
CancelMouseControllerInterval();
ConfigureFaceGaze(config);
}
void FaceGazeTestUtils::WaitForCursorPosition(const gfx::Point& location) {
std::string script =
base::StringPrintf("faceGazeTestSupport.waitForCursorLocation(%d, %d);",
location.x(), location.y());
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::ProcessFaceLandmarkerResult(
const MockFaceLandmarkerResult& result) {
std::string forehead_location_json =
base::WriteJson(result.forehead_location()).value();
std::string recognized_gestures_json =
base::WriteJson(result.recognized_gestures()).value();
std::string script;
if (result.latency().has_value()) {
script = base::StringPrintf(
"faceGazeTestSupport.processFaceLandmarkerResult(%s, %s, %d)",
forehead_location_json.c_str(), recognized_gestures_json.c_str(),
result.latency().value());
} else {
script = base::StringPrintf(
"faceGazeTestSupport.processFaceLandmarkerResult(%s, %s)",
forehead_location_json.c_str(), recognized_gestures_json.c_str());
}
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::TriggerMouseControllerInterval() {
std::string script = "faceGazeTestSupport.triggerMouseControllerInterval();";
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::MoveMouseTo(const gfx::Point& location) {
event_generator_->MoveMouseTo(location.x(), location.y());
}
void FaceGazeTestUtils::AssertCursorAt(const gfx::Point& location) {
WaitForCursorPosition(location);
ASSERT_EQ(location, display::Screen::Get()->GetCursorScreenPoint());
}
void FaceGazeTestUtils::AssertScrollMode(bool active) {
std::string true_script = "faceGazeTestSupport.assertScrollMode(true);";
std::string false_script = "faceGazeTestSupport.assertScrollMode(false);";
ExecuteAccessibilityCommonScript(active ? true_script : false_script);
}
void FaceGazeTestUtils::ExecuteAccessibilityCommonScript(
const std::string& script) {
extensions::browsertest_util::ExecuteScriptInBackgroundPage(
/*context=*/AccessibilityManager::Get()->profile(),
/*extension_id=*/extension_misc::kAccessibilityCommonExtensionId,
/*script=*/script);
}
void FaceGazeTestUtils::SetUpMediapipeDir(const char* mediapipe_dir) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath gen_root_dir;
ASSERT_TRUE(
base::PathService::Get(base::DIR_OUT_TEST_DATA_ROOT, &gen_root_dir));
base::FilePath test_file_path = gen_root_dir.AppendASCII(mediapipe_dir);
ASSERT_TRUE(base::PathExists(test_file_path));
AccessibilityManager::Get()->SetDlcPathForTest(test_file_path);
}
void FaceGazeTestUtils::WaitForJSReady() {
std::string script = base::StringPrintf(R"JS(
(async function() {
globalThis.accessibilityCommon.setFeatureLoadCallbackForTest('facegaze',
() => {
chrome.test.sendScriptResult('ready');
});
})();
)JS");
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::SetUpJSTestSupport(const char* test_support_dir) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath source_dir;
CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_dir));
auto test_support_path = source_dir.AppendASCII(test_support_dir);
std::string script;
ASSERT_TRUE(base::ReadFileToString(test_support_path, &script))
<< test_support_path;
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::CancelMouseControllerInterval() {
std::string script = "faceGazeTestSupport.cancelMouseControllerInterval();";
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::WaitForFaceLandmarker() {
std::string script = "faceGazeTestSupport.waitForFaceLandmarker();";
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::ConfigureFaceGaze(const Config& config) {
// Set optional configuration properties.
if (config.cursor_speeds().has_value()) {
SetCursorSpeeds(config.cursor_speeds().value());
}
if (config.gestures_to_macros().has_value()) {
SetGesturesToMacros(config.gestures_to_macros().value());
}
if (config.gesture_confidences().has_value()) {
SetGestureConfidences(config.gesture_confidences().value());
}
if (config.gesture_repeat_delay_ms().has_value()) {
SetGestureRepeatDelayMs(config.gesture_repeat_delay_ms().value());
}
// Set required configuration properties.
SetBufferSize(config.buffer_size());
SetCursorAcceleration(config.use_cursor_acceleration());
SetLandmarkWeights(config.use_landmark_weights());
SetVelocityThreshold(config.use_velocity_threshold());
SetGestureDuration(config.use_gesture_duration());
// By default the cursor is placed at the center of the screen. To
// initialize FaceGaze, move the cursor somewhere, then move it to the
// location specified by the config.
event_generator_->set_mouse_source_device_id(kMouseDeviceId);
MoveMouseTo(gfx::Point(0, 0));
AssertCursorAt(gfx::Point(0, 0));
MoveMouseTo(config.cursor_location());
AssertCursorAt(config.cursor_location());
// TODO(b/309121742): only call ProcessFaceLandmarkerResult if the forehead
// location is specified by the config.
// No matter the starting location, the cursor position won't change
// initially, and upcoming forehead locations will be computed relative to
// this.
ProcessFaceLandmarkerResult(
MockFaceLandmarkerResult().WithNormalizedForeheadLocation(std::make_pair(
config.forehead_location().x(), config.forehead_location().y())));
TriggerMouseControllerInterval();
AssertCursorAt(config.cursor_location());
}
void FaceGazeTestUtils::SetCursorSpeeds(const CursorSpeeds& speeds) {
GetPrefs()->SetInteger(prefs::kAccessibilityFaceGazeCursorSpeedUp, speeds.up);
GetPrefs()->SetInteger(prefs::kAccessibilityFaceGazeCursorSpeedDown,
speeds.down);
GetPrefs()->SetInteger(prefs::kAccessibilityFaceGazeCursorSpeedLeft,
speeds.left);
GetPrefs()->SetInteger(prefs::kAccessibilityFaceGazeCursorSpeedRight,
speeds.right);
GetPrefs()->CommitPendingWrite();
}
void FaceGazeTestUtils::SetBufferSize(int size) {
std::string script =
base::StringPrintf("faceGazeTestSupport.setBufferSize(%d);", size);
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::SetCursorAcceleration(bool use_acceleration) {
GetPrefs()->SetBoolean(prefs::kAccessibilityFaceGazeCursorUseAcceleration,
use_acceleration);
GetPrefs()->CommitPendingWrite();
}
void FaceGazeTestUtils::SetLandmarkWeights(bool use_weights) {
std::string true_script = "faceGazeTestSupport.setLandmarkWeights(true);";
std::string false_script = "faceGazeTestSupport.setLandmarkWeights(false);";
ExecuteAccessibilityCommonScript(use_weights ? true_script : false_script);
}
void FaceGazeTestUtils::SetVelocityThreshold(bool use_threshold) {
// TODO(b/309121742): Update this to set the pref value after a pref for
// velocity threshold has been added.
std::string true_script = "faceGazeTestSupport.setVelocityThreshold(true);";
std::string false_script = "faceGazeTestSupport.setVelocityThreshold(false);";
ExecuteAccessibilityCommonScript(use_threshold ? true_script : false_script);
}
void FaceGazeTestUtils::SetGesturesToMacros(
const base::flat_map<FaceGazeGesture, MacroName>& gestures_to_macros) {
// Copy the stricly-typed mapping of gestures to macros into a dictionary
// value that can be used as the preference value.
base::Value::Dict dict;
for (const auto& mapping : gestures_to_macros) {
dict.Set(ToString(mapping.first), mapping.second);
}
GetPrefs()->SetDict(prefs::kAccessibilityFaceGazeGesturesToMacros,
std::move(dict));
GetPrefs()->CommitPendingWrite();
}
void FaceGazeTestUtils::SetGestureConfidences(
const base::flat_map<FaceGazeGesture, int>& gesture_confidences) {
// Copy the stricly-typed mapping of gestures to confidences into a dictionary
// value that can be used as the preference value.
base::Value::Dict dict;
for (const auto& mapping : gesture_confidences) {
dict.Set(ToString(mapping.first), mapping.second);
}
GetPrefs()->SetDict(prefs::kAccessibilityFaceGazeGesturesToConfidence,
std::move(dict));
GetPrefs()->CommitPendingWrite();
}
void FaceGazeTestUtils::SetGestureRepeatDelayMs(int delay) {
std::string script = base::StringPrintf(
"faceGazeTestSupport.setGestureRepeatDelayMs(%d);", delay);
ExecuteAccessibilityCommonScript(script);
}
void FaceGazeTestUtils::SetGestureDuration(bool use_duration) {
std::string true_script = "faceGazeTestSupport.setGestureDuration(true);";
std::string false_script = "faceGazeTestSupport.setGestureDuration(false);";
ExecuteAccessibilityCommonScript(use_duration ? true_script : false_script);
}
} // namespace ash