// Copyright 2023 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/test/base/ash/interactive/interactive_ash_test.h"
#include <optional>
#include "ash/ash_element_identifiers.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/json/string_escape.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_switches.h"
#include "base/values.h"
#include "chrome/browser/ash/app_restore/full_restore_app_launch_handler.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/test/base/ash/interactive/settings/interactive_uitest_elements.h"
#include "chrome/test/base/chromeos/crosier/aura_window_title_observer.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/events/event_constants.h"
#include "url/gurl.h"
namespace {
using InteractiveMixinBasedBrowserTest =
InteractiveBrowserTestT<MixinBasedInProcessBrowserTest>;
// This JavaScript is used to select an option from a dropdown menu. This
// JavaScript can be formatted with a single string to identify the desired
// option. The string can be a substring of the complete option name.
constexpr char kSelectDropdownElementOptionJs[] = R"(
(el) => {
const elements = el.querySelectorAll('option');
for (let i = 0; i < elements.length; i++) {
if (elements[i].label.indexOf('%s') == -1) {
continue;
}
el.selectedIndex = elements[i].index;
el.dispatchEvent(new Event('change'));
return true;
}
return false;
})";
// This JavaScript defines a function "action" that returns `true` if `el`
// contains the expected inner text.
constexpr char kFindElementWithTextActionJs[] = R"(
function action(el) {
return el && el.innerText.indexOf(%s) >= 0;
})";
// This JavaScript defines a function "action" that returns `true` if `el`
// contains the expected inner text. Before returning `el` is clicked.
constexpr char kClickElementWithTextActionJs[] = R"(
function action(el) {
if (el && el.innerText.indexOf(%s) >= 0) {
el.click();
return true;
}
return false;
})";
// This JavaScript defines a function "action" that returns `true` if `el`
// has a child element that contains the expected text and matches the
// provided selectors, and if the child element has a sibling that matches
// the provided selectors. The sibling element is clicked before returning.
constexpr char kClickChildOfElementWithTextActionJs[] = R"(
function action(el) {
if (!el) {
return false;
}
var text = el.shadowRoot.querySelector(%s);
if (!text || text.innerText.indexOf(%s) == -1) {
return false;
}
var child = el.shadowRoot.querySelector(%s);
if (child) {
child.click();
return true;
}
return false;
})";
// This JavaScript is used to search for an element in the DOM. The element is
// described in terms of a root element and the relative path provided via an
// array of selectors, and the element will be considered "found" if it matches
// the entire array of selectors AND meets an arbitrary criteria.
//
// This JavaScript should be formatted with two strings: a JavaScript function
// and a list of selectors. The JavaScript function should be named "action",
// accept a single element parameter, and return `true` if the element parameter
// meets the desired criteria. The list of selectors should define the path from
// `el` to the desired element. The function will only be called on each element
// that matches the list of selectors.
constexpr char kFindElementAndDoActionJs[] = R"(
(el) => {
%s;
function findElement(root, index, selectors) {
const elements = root.shadowRoot.querySelectorAll(selectors[index]);
for (let el of elements) {
if (index < selectors.length - 1) {
if (findElement(el, index + 1, selectors)) {
return true;
}
continue;
}
if (action(el)) {
return true;
}
}
return false;
}
return findElement(el, 0, %s);
})";
std::string DeepQueryToSelectors(
const WebContentsInteractionTestUtil::DeepQuery& query) {
// Safely convert the selector list in `where` to a JSON/JS list.
base::Value::List selector_list;
for (const auto& selector : query) {
selector_list.Append(selector);
}
std::string selectors;
CHECK(base::JSONWriter::Write(selector_list, &selectors));
return selectors;
}
} // namespace
InteractiveAshTest::InteractiveAshTest() {
// See header file class comment.
set_launch_browser_for_testing(nullptr);
// Give all widgets the same Kombucha context.This is useful for ash system
// UI because the UI uses a variety of small widgets. Note that if this test
// used multiple displays we would need to provide a different context per
// display (i.e. the widget's native window's root window). Elements like
// the home button, shelf, etc. appear once per display.
views::ElementTrackerViews::SetContextOverrideCallback(
base::BindRepeating([](views::Widget* widget) {
return ui::ElementContext(ash::Shell::GetPrimaryRootWindow());
}));
}
InteractiveAshTest::~InteractiveAshTest() {
views::ElementTrackerViews::SetContextOverrideCallback({});
}
void InteractiveAshTest::SetupContextWidget() {
views::Widget* status_area_widget =
ash::Shell::GetPrimaryRootWindowController()
->shelf()
->GetStatusAreaWidget();
SetContextWidget(status_area_widget);
}
void InteractiveAshTest::InstallSystemApps() {
Profile* profile = ProfileManager::GetActiveUserProfile();
CHECK(profile);
ash::SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
}
ui::ElementContext InteractiveAshTest::LaunchSystemWebApp(
ash::SystemWebAppType type,
const ui::ElementIdentifier& element_id) {
Profile* profile = GetActiveUserProfile();
CHECK(profile);
RunTestSequence(InstrumentNextTab(element_id, AnyBrowser()),
Do([&]() { LaunchSystemWebAppAsync(profile, type); }),
InAnyContext(WaitForShow(element_id)));
return FindSystemWebApp(type);
}
ui::ElementContext InteractiveAshTest::FindSystemWebApp(
ash::SystemWebAppType type) {
Profile* profile = GetActiveUserProfile();
CHECK(profile);
Browser* browser = FindSystemWebAppBrowser(profile, type);
CHECK(browser);
return browser->window()->GetElementContext();
}
void InteractiveAshTest::CloseSystemWebApp(ash::SystemWebAppType type) {
if (Profile* const profile = GetActiveUserProfile()) {
if (Browser* const browser = FindSystemWebAppBrowser(profile, type)) {
chrome::CloseWindow(browser);
}
}
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateSettingsToInternetPage(
const ui::ElementIdentifier& element_id) {
return NavigateSettingsToPage(element_id, /*path=*/"/internet");
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateSettingsToBluetoothPage(
const ui::ElementIdentifier& element_id) {
return NavigateSettingsToPage(element_id, /*path=*/"/bluetooth");
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::OpenQuickSettings() {
return Steps(PressButton(ash::kUnifiedSystemTrayElementId),
WaitForShow(ash::kQuickSettingsViewElementId));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateQuickSettingsToNetworkPage() {
return NavigateQuickSettingsToPage(
ash::kNetworkFeatureTileDrillInArrowElementId);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateQuickSettingsToBluetoothPage() {
return NavigateQuickSettingsToPage(
ash::kBluetoothFeatureTileDrillInArrowElementId);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateToApnRevampDetailsPage(
const ui::ElementIdentifier& element_id) {
return Steps(
WaitForElementExists(element_id,
ash::settings::cellular::CellularSubpageApnRow()),
ScrollIntoView(element_id,
ash::settings::cellular::CellularSubpageApnRow()),
MoveMouseTo(element_id, ash::settings::cellular::CellularSubpageApnRow()),
ClickMouse(),
WaitForElementExists(
element_id, ash::settings::cellular::ApnSubpageActionMenuButton()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::OpenAddCustomApnDetailsDialog(
const ui::ElementIdentifier& element_id) {
return Steps(
WaitForElementEnabled(
element_id, ash::settings::cellular::ApnSubpageActionMenuButton()),
ClickElement(element_id,
ash::settings::cellular::ApnSubpageActionMenuButton()),
WaitForElementEnabled(
element_id, ash::settings::cellular::ApnSubpageCreateApnButton()),
ClickElement(element_id,
ash::settings::cellular::ApnSubpageCreateApnButton()),
WaitForElementExists(element_id, ash::settings::cellular::ApnDialog()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::OpenApnSelectionDialog(
const ui::ElementIdentifier& element_id) {
return Steps(
WaitForElementEnabled(
element_id, ash::settings::cellular::ApnSubpageActionMenuButton()),
ClickElement(element_id,
ash::settings::cellular::ApnSubpageActionMenuButton()),
WaitForElementEnabled(
element_id, ash::settings::cellular::ApnSubpageShowKnownApnsButton()),
ClickElement(element_id,
ash::settings::cellular::ApnSubpageShowKnownApnsButton()),
WaitForElementExists(element_id,
ash::settings::cellular::ApnSelectionDialog()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::OpenAddBuiltInVpnDialog(
const ui::ElementIdentifier& element_id) {
return Steps(
WaitForElementEnabled(element_id,
ash::settings::AddConnectionsExpandButton()),
ClickElement(element_id, ash::settings::AddConnectionsExpandButton()),
WaitForElementExpanded(element_id,
ash::settings::AddConnectionsExpandButton()),
WaitForElementEnabled(element_id, ash::settings::AddBuiltInVpnRow()),
ClickElement(element_id, ash::settings::AddBuiltInVpnRow()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::OpenAddWifiDialog(const ui::ElementIdentifier& element_id) {
return Steps(
NavigateSettingsToNetworkSubpage(element_id,
ash::NetworkTypePattern::WiFi()),
WaitForElementExists(element_id, ash::settings::wifi::AddWifiButton()),
ClickElement(element_id, ash::settings::wifi::AddWifiButton()),
WaitForElementExists(element_id,
ash::settings::wifi::ConfigureWifiDialog()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::CompleteAddWifiDialog(
const ui::ElementIdentifier& element_id,
const WifiDialogConfig& config) {
CHECK(!config.ssid.empty());
ui::test::internal::InteractiveTestPrivate::MultiStep steps = Steps(
Log(base::StringPrintf("Entering SSID \"%s\" for the network",
config.ssid.c_str())),
WaitForElementExists(element_id,
ash::settings::wifi::ConfigureWifiDialogSsidInput()),
ClearInputAndEnterText(
element_id, ash::settings::wifi::ConfigureWifiDialogSsidInput(),
config.ssid.c_str()),
Log("Ensuring the network has the correct security"));
if (config.security_type !=
::chromeos::network_config::mojom::SecurityType::kNone) {
// TODO([email protected]): Add logic for selecting security type
// and filling out the relevant fields.
NOTREACHED();
}
AddStep(steps,
Log(base::StringPrintf("Configuring the network as %s",
config.is_shared ? "shared" : "not shared")));
if (config.security_type ==
::chromeos::network_config::mojom::SecurityType::kNone) {
AddStep(steps, WaitForElementChecked(
element_id,
ash::settings::wifi::ConfigureWifiDialogShareToggle()));
if (!config.is_shared) {
AddStep(
steps,
Steps(ClickElement(
element_id,
ash::settings::wifi::ConfigureWifiDialogShareToggle()),
WaitForElementUnchecked(
element_id,
ash::settings::wifi::ConfigureWifiDialogShareToggle())));
}
} else {
// TODO([email protected]): Add logic for marking the network as
// that it is shared.
NOTREACHED();
}
AddStep(steps,
Steps(Log("Clicking the connect button and waiting for the dialog "
"to disappear"),
WaitForElementEnabled(
element_id,
ash::settings::wifi::ConfigureWifiDialogConnectButton()),
ClickElement(
element_id,
ash::settings::wifi::ConfigureWifiDialogConnectButton()),
WaitForElementDoesNotExist(
element_id, ash::settings::wifi::ConfigureWifiDialog())));
return steps;
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateQuickSettingsToHotspotPage() {
return NavigateQuickSettingsToPage(
ash::kHotspotFeatureTileDrillInArrowElementId);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateSettingsToNetworkSubpage(
const ui::ElementIdentifier& element_id,
const ash::NetworkTypePattern network_pattern) {
WebContentsInteractionTestUtil::DeepQuery internet_summary_row;
if (network_pattern.MatchesPattern(ash::NetworkTypePattern::Mobile())) {
internet_summary_row = ash::settings::cellular::CellularSummaryItem();
} else if (network_pattern.MatchesPattern(ash::NetworkTypePattern::VPN())) {
internet_summary_row = ash::settings::vpn::VpnSummaryItem();
} else if (network_pattern.MatchesPattern(ash::NetworkTypePattern::WiFi())) {
internet_summary_row = ash::settings::wifi::WifiSummaryItem();
} else {
// Unsupported Network pattern.
NOTREACHED();
}
return Steps(NavigateSettingsToInternetPage(element_id),
WaitForElementExists(element_id, internet_summary_row),
ScrollIntoView(element_id, internet_summary_row),
MoveMouseTo(element_id, internet_summary_row), ClickMouse());
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateToInternetDetailsPage(
const ui::ElementIdentifier& element_id,
const ash::NetworkTypePattern network_pattern,
const std::string& network_name) {
WebContentsInteractionTestUtil::DeepQuery network_list;
WebContentsInteractionTestUtil::DeepQuery network_list_item(
{"network-list-item"});
WebContentsInteractionTestUtil::DeepQuery network_list_item_title(
{"div#divText"});
WebContentsInteractionTestUtil::DeepQuery network_list_item_subpage_arrow(
{"cr-icon-button#subpageButton"});
std::string element_selector;
// TODO: Add other network types.
if (network_pattern.MatchesPattern(ash::NetworkTypePattern::Mobile())) {
network_list = ash::settings::cellular::CellularNetworksList();
network_list_item = WebContentsInteractionTestUtil::DeepQuery(
{"network-list", "network-list-item"});
} else if (network_pattern.MatchesPattern(ash::NetworkTypePattern::VPN())) {
network_list = ash::settings::vpn::VpnNetworksList();
} else {
// Unsupported Network pattern.
NOTREACHED();
}
return Steps(NavigateSettingsToNetworkSubpage(element_id, network_pattern),
FindElementAndDoActionOnChildren(
element_id, network_list, network_list_item,
ClickElementWithSiblingContainsText(
network_list_item_title, network_name,
network_list_item_subpage_arrow)),
WaitForElementTextContains(
element_id, ash::settings::InternetSettingsSubpageTitle(),
/*text=*/network_name.c_str()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateToBluetoothDeviceDetailsPage(
const ui::ElementIdentifier& element_id,
const std::string& device_name) {
const WebContentsInteractionTestUtil::DeepQuery bluetooth_device_item_title(
{"os-settings-paired-bluetooth-list-item", "div#deviceName"});
return Steps(NavigateSettingsToBluetoothPage(element_id),
WaitForAnyElementTextContains(
element_id, ash::settings::bluetooth::BluetoothDeviceList(),
bluetooth_device_item_title, device_name),
ClickAnyElementTextContains(
element_id, ash::settings::bluetooth::BluetoothDeviceList(),
bluetooth_device_item_title, device_name),
WaitForElementTextContains(
element_id, ash::settings::bluetooth::BluetoothDeviceName(),
device_name));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateToKnownNetworksPage(
const ui::ElementIdentifier& element_id) {
return Steps(
NavigateSettingsToInternetPage(element_id),
WaitForElementEnabled(element_id, ash::settings::wifi::WifiSummaryItem()),
ClickElement(element_id, ash::settings::wifi::WifiSummaryItem()),
WaitForElementEnabled(
element_id, ash::settings::wifi::WifiKnownNetworksSubpageButton()),
ClickElement(element_id,
ash::settings::wifi::WifiKnownNetworksSubpageButton()),
WaitForElementExists(element_id,
ash::settings::wifi::KnownNetworksSubpage()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateToPasspointSubscriptionSubpage(
const ui::ElementIdentifier& element_id,
const std::string& passpoint_name) {
const WebContentsInteractionTestUtil::DeepQuery
passpoint_subscription_item_name(
{"cr-link-row#subscriptionItem", "div#label"});
return Steps(
NavigateToKnownNetworksPage(element_id),
WaitForElementExists(
element_id,
ash::settings::wifi::KnownNetworksSubpagePasspointSubsciptions()),
ClickAnyElementTextContains(
element_id, ash::settings::wifi::KnownNetworksSubpage(),
passpoint_subscription_item_name, passpoint_name),
WaitForElementTextContains(element_id,
ash::settings::InternetSettingsSubpageTitle(),
/*text=*/passpoint_name.c_str()));
}
Profile* InteractiveAshTest::GetActiveUserProfile() {
return ProfileManager::GetActiveUserProfile();
}
base::WeakPtr<content::NavigationHandle>
InteractiveAshTest::CreateBrowserWindow(const GURL& url) {
Profile* profile = GetActiveUserProfile();
CHECK(profile);
NavigateParams params(profile, url, ui::PAGE_TRANSITION_TYPED);
params.disposition = WindowOpenDisposition::NEW_WINDOW;
params.window_action = NavigateParams::SHOW_WINDOW;
return Navigate(¶ms);
}
void InteractiveAshTest::TearDownOnMainThread() {
// Passing --test-launcher-interactive leaves the browser running after the
// end of the test.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kTestLauncherInteractive)) {
base::RunLoop loop;
loop.Run();
}
InteractiveBrowserTestT<
MixinBasedInProcessBrowserTest>::TearDownOnMainThread();
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForWindowWithTitle(aura::Env* env,
std::u16string title) {
DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(AuraWindowTitleObserver, kTitleObserver);
return Steps(
ObserveState(kTitleObserver,
std::make_unique<AuraWindowTitleObserver>(env, title)),
WaitForState(kTitleObserver, true), StopObservingState(kTitleObserver));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementExists(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementExists);
StateChange element_exists;
element_exists.event = kElementExists;
element_exists.where = query;
return WaitForStateChange(element_id, element_exists);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementDoesNotExist(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementDoesNotExist);
StateChange does_not_exist;
does_not_exist.type = StateChange::Type::kDoesNotExist;
does_not_exist.event = kElementDoesNotExist;
does_not_exist.where = query;
return WaitForStateChange(element_id, does_not_exist);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementEnabled(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementEnabled);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementEnabled;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return !el.disabled; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementWithManagedPropertyBoolean(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query,
const std::string& property,
bool expected_value) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kManagedBooleanChange);
StateChange managed_boolean_change;
managed_boolean_change.event = kManagedBooleanChange;
managed_boolean_change.where = query;
managed_boolean_change.type = StateChange::Type::kExistsAndConditionTrue;
managed_boolean_change.test_function =
base::StringPrintf(R"(
(el) => {
return el &&
el.managedProperties_ &&
el.managedProperties_.%s.activeValue === %s;
}
)",
property.c_str(), expected_value ? "true" : "false");
return WaitForStateChange(element_id, managed_boolean_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementDisabled(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementDisabled);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementDisabled;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return el.disabled; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementChecked(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementChecked);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementChecked;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return el.checked; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementUnchecked(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementUnchecked);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementUnchecked;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return !el.checked; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementExpanded(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementExpanded);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementExpanded;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return el.expanded; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementOpened(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementOpened);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementOpened;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return el.opened || el.open; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementUnopened(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementUnopened);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementUnopened;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = "(el) => { return !el.opened && !el.open; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementFocused(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementFocused);
StateChange element_focused;
element_focused.event = kElementFocused;
element_focused.where = query;
element_focused.test_function =
"(el) => { return el === document.activeElement; }";
return WaitForStateChange(element_id, element_focused);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementTextContains(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query,
const std::string& expected) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kTextFound);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.type = WebContentsInteractionTestUtil::StateChange::Type::
kExistsAndConditionTrue;
state_change.where = query;
state_change.test_function = "function(el) { return el.innerText.indexOf(" +
base::GetQuotedJSONString(expected) +
") >= 0; }";
state_change.event = kTextFound;
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForAnyElementTextContains(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& root,
const WebContentsInteractionTestUtil::DeepQuery& selectors,
const std::string& expected) {
return FindElementAndDoActionOnChildren(
element_id, root, selectors,
base::StringPrintf(kFindElementWithTextActionJs,
base::GetQuotedJSONString(expected).c_str()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementHasAttribute(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element,
const std::string& attribute) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementHasAttribute);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementHasAttribute;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function = base::StringPrintf(
"(el) => { return el.hasAttribute('%s'); }", attribute.c_str());
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementDisplayNone(
const ui::ElementIdentifier& element_id,
WebContentsInteractionTestUtil::DeepQuery element) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementHasDisplayNone);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.event = kElementHasDisplayNone;
state_change.where = element;
state_change.type = StateChange::Type::kExistsAndConditionTrue;
state_change.test_function =
"(el) => { return el.style.display === 'none'; }";
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForToggleState(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query,
bool is_checked) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kToggleState);
StateChange toggle_state_change;
toggle_state_change.event = kToggleState;
toggle_state_change.where = query;
toggle_state_change.type = StateChange::Type::kExistsAndConditionTrue;
toggle_state_change.test_function = is_checked
? "(el) => { return el.checked; }"
: "(el) => { return !el.checked; }";
return WaitForStateChange(element_id, toggle_state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::ClearInputFieldValue(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kInputElementText);
StateChange input_element_change;
input_element_change.event = kInputElementText;
input_element_change.where = query;
input_element_change.type = StateChange::Type::kExistsAndConditionTrue;
input_element_change.test_function = "(el) => { return el.value === ''; }";
return Steps(MoveMouseTo(element_id, query), ClickMouse(),
CheckJsResultAt(element_id, query,
"(el) => {el.value = ''; return true;}"),
WaitForStateChange(element_id, input_element_change));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::WaitForElementToRender(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementRenders);
StateChange element_renders;
element_renders.event = kElementRenders;
element_renders.where = query;
element_renders.test_function =
"(el) => { if (el !== null) { let rect = el.getBoundingClientRect(); "
"return rect.width > 0 && rect.height > 0; } return false; }";
return WaitForStateChange(element_id, element_renders);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::ClickElement(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query) {
return Steps(MoveMouseTo(element_id, query), ClickMouse());
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::ClickAnyElementTextContains(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& root,
const WebContentsInteractionTestUtil::DeepQuery& selectors,
const std::string& expected) {
return FindElementAndDoActionOnChildren(
element_id, root, selectors,
base::StringPrintf(kClickElementWithTextActionJs,
base::GetQuotedJSONString(expected).c_str()));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::SelectDropdownElementOption(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query,
const std::string& option) {
return Steps(
WaitForElementExists(element_id, query),
CheckJsResultAt(
element_id, query,
base::StringPrintf(kSelectDropdownElementOptionJs, option.c_str())));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::SendTextAsKeyEvents(const ui::ElementIdentifier& element_id,
const std::string& text) {
MultiStep steps;
// The text that should be entered as key events is provided as a std::string,
// but key events require a key code. The loop below converts each character
// of the string into a single key event; this loop avoids code duplication be
// identifying when a character exists within a known contiguous range, e.g. A
// through Z, and computing the desired key code by calculating the offset of
// the character into the known contiguous range.
for (char c : text) {
std::optional<ui::KeyboardCode> key_code;
unsigned short offset = 0;
int modifiers = 0;
if (c >= 'a' && c <= 'z') {
key_code = ui::VKEY_A;
offset = c - 'a';
} else if (c >= 'A' && c <= 'Z') {
key_code = ui::VKEY_A;
offset = c - 'A';
modifiers = ui::EF_SHIFT_DOWN;
} else if (c >= '0' && c <= '9') {
key_code = ui::VKEY_0;
offset = c - '0';
} else if (c == '$') {
key_code = ui::VKEY_4;
modifiers = ui::EF_SHIFT_DOWN;
} else if (c == '-') {
key_code = ui::VKEY_OEM_MINUS;
} else if (c == '.') {
key_code = ui::VKEY_OEM_PERIOD;
} else if (c == ';') {
key_code = ui::VKEY_OEM_1;
} else if (c == ':') {
key_code = ui::VKEY_OEM_1;
modifiers = ui::EF_SHIFT_DOWN;
} else if (c == '_') {
key_code = ui::VKEY_OEM_MINUS;
modifiers = ui::EF_SHIFT_DOWN;
} else if (c == '\n') {
key_code = ui::VKEY_RETURN;
} else if (c == ' ') {
key_code = ui::VKEY_SPACE;
}
if (!key_code.has_value()) {
// Unsupported input.
NOTREACHED_IN_MIGRATION();
}
AddStep(steps, SendAccelerator(
element_id,
ui::Accelerator(
static_cast<ui::KeyboardCode>(*key_code + offset),
modifiers, ui::Accelerator::KeyState::PRESSED)));
}
return steps;
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::ClearInputAndEnterText(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& query,
const std::string& text) {
return Steps(WaitForElementExists(element_id, query),
ClearInputFieldValue(element_id, query),
ClickElement(element_id, query),
SendTextAsKeyEvents(element_id, text));
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::FindElementAndDoActionOnChildren(
const ui::ElementIdentifier& element_id,
const WebContentsInteractionTestUtil::DeepQuery& root,
const WebContentsInteractionTestUtil::DeepQuery& selectors,
const std::string& action) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementWithActionReturnTrue);
WebContentsInteractionTestUtil::StateChange state_change;
state_change.type = WebContentsInteractionTestUtil::StateChange::Type::
kExistsAndConditionTrue;
state_change.where = root;
state_change.test_function =
base::StringPrintf(kFindElementAndDoActionJs, action.c_str(),
DeepQueryToSelectors(selectors).c_str());
state_change.event = kElementWithActionReturnTrue;
return WaitForStateChange(element_id, state_change);
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateSettingsToPage(
const ui::ElementIdentifier& element_id,
const char* path) {
CHECK(path);
const WebContentsInteractionTestUtil::DeepQuery menu_item(
{{"os-settings-ui", "os-settings-menu",
base::StringPrintf("os-settings-menu-item[path=\"%s\"]", path)}});
return Steps(ScrollIntoView(element_id, menu_item),
MoveMouseTo(element_id, menu_item), ClickMouse());
}
ui::test::internal::InteractiveTestPrivate::MultiStep
InteractiveAshTest::NavigateQuickSettingsToPage(
const ui::ElementIdentifier& element_id) {
// This function assumes that the drill-in arrow is or will become visible
// without any action.
return Steps(WaitForShow(ash::kQuickSettingsViewElementId),
WaitForShow(element_id), MoveMouseTo(element_id), ClickMouse());
}
const std::string InteractiveAshTest::ClickElementWithSiblingContainsText(
const WebContentsInteractionTestUtil::DeepQuery& element_with_text,
const std::string& expected,
const WebContentsInteractionTestUtil::DeepQuery& element_to_click) {
return base::StringPrintf(kClickChildOfElementWithTextActionJs,
DeepQueryToSelectors(element_with_text).c_str(),
base::GetQuotedJSONString(expected).c_str(),
DeepQueryToSelectors(element_to_click).c_str());
}