| // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package quicksettings is for controlling the Quick Settings directly from the UI. |
| package quicksettings |
| |
| import ( |
| "context" |
| "strconv" |
| "strings" |
| "time" |
| |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/apps" |
| "chromiumos/tast/local/bluetooth" |
| "chromiumos/tast/local/chrome" |
| "chromiumos/tast/local/chrome/ash" |
| "chromiumos/tast/local/chrome/uiauto" |
| "chromiumos/tast/local/chrome/uiauto/checked" |
| "chromiumos/tast/local/chrome/uiauto/lockscreen" |
| "chromiumos/tast/local/chrome/uiauto/nodewith" |
| "chromiumos/tast/local/chrome/uiauto/restriction" |
| "chromiumos/tast/local/chrome/uiauto/role" |
| "chromiumos/tast/local/chrome/uiauto/state" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/testing" |
| ) |
| |
| const uiTimeout = 10 * time.Second |
| |
| // findStatusArea finds the status area UI node. |
| func findStatusArea(ctx context.Context, tconn *chrome.TestConn) (*nodewith.Finder, error) { |
| ui := uiauto.New(tconn) |
| statusArea := nodewith.ClassName("UnifiedSystemTray").First() |
| return statusArea, ui.WithTimeout(uiTimeout).WaitUntilExists(statusArea)(ctx) |
| } |
| |
| // clickAndWaitForAnimation clicks the node found with the provided finder and |
| // waits until the Quick Settings is no longer animating. The node provided is |
| // expected to be, but not enforced to be, either the expand or collapse |
| // button. |
| func clickAndWaitForAnimation(ctx context.Context, tconn *chrome.TestConn, node *nodewith.Finder) error { |
| initialBounds, err := Rect(ctx, tconn) |
| |
| if err != nil { |
| return err |
| } |
| |
| previousBounds := initialBounds |
| checkIfAnimating := func(ctx context.Context) error { |
| if currentBounds, err := Rect(ctx, tconn); err != nil { |
| return testing.PollBreak(err) |
| } else if currentBounds != previousBounds { |
| previousBounds = currentBounds |
| return errors.New("the Quick Settings is still animating") |
| } |
| return nil |
| } |
| |
| if err := uiauto.New(tconn).LeftClick(node)(ctx); err != nil { |
| errors.Wrap(err, "failed to click the node") |
| } |
| |
| if err := testing.Poll(ctx, checkIfAnimating, &testing.PollOptions{Interval: 500 * time.Millisecond, Timeout: uiTimeout}); err != nil { |
| return errors.Wrap(err, "the Quick Settings did not stop animating") |
| } |
| return nil |
| } |
| |
| // Rect returns a coords.Rect struct for the Quick Settings area, which contains |
| // coordinate information about the rectangular region it occupies on the screen. |
| // As clients of this function generally expect the bounds of the window, not the |
| // "UnifiedSystemTrayView" view itself, this finds a not that has |
| // UnifiedSystemTrayView as a child. |
| func Rect(ctx context.Context, tconn *chrome.TestConn) (coords.Rect, error) { |
| ui := uiauto.New(tconn) |
| bubbleFrameView := nodewith.ClassName("BubbleFrameView") |
| results, err := ui.NodesInfo(ctx, bubbleFrameView) |
| if err != nil { |
| return coords.Rect{}, errors.Wrap(err, "failed to find quick settings") |
| } |
| |
| for i := range results { |
| if err := ui.Exists(quickSettingsFinder.Ancestor(bubbleFrameView.Nth(i)))(ctx); err == nil { |
| return results[i].Location, nil |
| } |
| } |
| return coords.Rect{}, errors.Wrap(err, "failed to find quick settings") |
| } |
| |
| // ClickStatusArea clicks the status area, which is the area on the shelf where info |
| // such as time and battery level are shown. |
| func ClickStatusArea(ctx context.Context, tconn *chrome.TestConn) error { |
| statusArea, err := findStatusArea(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to find status area widget") |
| } |
| ui := uiauto.New(tconn) |
| if err := ui.LeftClick(statusArea)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click the status area") |
| } |
| return nil |
| } |
| |
| // Shown checks if Quick Settings exists in the UI. |
| func Shown(ctx context.Context, tconn *chrome.TestConn) (bool, error) { |
| return uiauto.New(tconn).IsNodeFound(ctx, quickSettingsFinder) |
| } |
| |
| // Show will click the status area to show Quick Settings and wait for it to appear. |
| // If Quick Settings is already open, it does nothing. Quick Settings will remain |
| // open between tests if it's not closed explicitly, so this should be accompanied |
| // by a deferred call to Hide to clean up the UI before starting other tests. |
| func Show(ctx context.Context, tconn *chrome.TestConn) error { |
| if shown, err := Shown(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed initial quick settings visibility check") |
| } else if shown { |
| return nil |
| } |
| |
| if err := ClickStatusArea(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to click the status area") |
| } |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).WaitUntilExists(quickSettingsFinder)(ctx); err != nil { |
| return errors.Wrap(err, "failed waiting for quick settings to appear") |
| } |
| return nil |
| } |
| |
| // Hide will click the status area to hide Quick Settings if it's currently shown. |
| // It will then wait for it to be hidden for the duration specified by timeout. |
| func Hide(ctx context.Context, tconn *chrome.TestConn) error { |
| if shown, err := Shown(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed initial quick settings visibility check") |
| } else if !shown { |
| return nil |
| } |
| |
| if err := ClickStatusArea(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to click the status area") |
| } |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).WaitUntilGone(quickSettingsFinder)(ctx); err != nil { |
| return errors.Wrap(err, "failed waiting for quick settings to be hidden") |
| } |
| return nil |
| } |
| |
| // Collapse will result in the Quick Settings being opened and in a collapsed |
| // state. This is safe to call even when Quick Settings is already open. |
| func Collapse(ctx context.Context, tconn *chrome.TestConn) error { |
| if err := Hide(ctx, tconn); err != nil { |
| return err |
| } |
| |
| if err := ShowWithRetry(ctx, tconn, 5*time.Second); err != nil { |
| return err |
| } |
| |
| exist, err := uiauto.New(tconn).IsNodeFound(ctx, ExpandButton) |
| if err != nil { |
| return errors.Wrap(err, "failed to check if the expand button already exists") |
| } |
| if exist { |
| return nil |
| } |
| |
| if err := clickAndWaitForAnimation(ctx, tconn, CollapseButton); err != nil { |
| return errors.Wrap(err, "the Quick Settings did not collapse") |
| } |
| return nil |
| } |
| |
| // Expand will result in the Quick Settings being opened and in an expanded |
| // state. This is safe to call even when Quick Settings is already open. |
| func Expand(ctx context.Context, tconn *chrome.TestConn) error { |
| if err := Hide(ctx, tconn); err != nil { |
| return err |
| } |
| |
| if err := ShowWithRetry(ctx, tconn, 5*time.Second); err != nil { |
| return err |
| } |
| |
| exist, err := uiauto.New(tconn).IsNodeFound(ctx, CollapseButton) |
| if err != nil { |
| return errors.Wrap(err, "failed to check if the collapse button already exists") |
| } |
| if exist { |
| return nil |
| } |
| |
| if err := clickAndWaitForAnimation(ctx, tconn, ExpandButton); err != nil { |
| return errors.Wrap(err, "the Quick Settings did not expand") |
| } |
| return nil |
| } |
| |
| // ShowWithRetry will continuously click the status area until Quick Settings is shown, |
| // for the duration specified by timeout. Quick Settings sometimes does not open if the status area |
| // is clicked very early in the test, so this function can be used to ensure it will be opened. |
| // Callers should also defer a call to Hide to ensure Quick Settings is closed between tests. |
| // TODO(crbug/1099502): remove this once there's a better indicator for when the status area |
| // is ready to receive clicks. |
| func ShowWithRetry(ctx context.Context, tconn *chrome.TestConn, timeout time.Duration) error { |
| statusArea, err := findStatusArea(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to find the status area widget") |
| } |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithPollOpts(testing.PollOptions{Timeout: timeout, Interval: time.Second}).LeftClickUntil(statusArea, ui.Exists(quickSettingsFinder))(ctx); err != nil { |
| return errors.Wrap(err, "quick settings not shown") |
| } |
| return nil |
| } |
| |
| // PodIconButton generates nodewith.Finder for the specified quick setting feature pod icon button. |
| func PodIconButton(setting SettingPod) *nodewith.Finder { |
| // The network pod cannot be easily found by its Name attribute in both logged-in and lock screen states. |
| // Instead, find it by its unique ClassName. |
| if setting == SettingPodNetwork { |
| return nodewith.HasClass("NetworkFeaturePodButton") |
| } |
| |
| // The pod icon names change based on their state, but a substring containing the setting name stays |
| // the same regardless of state, so we can match that in the name attribute. |
| return nodewith.HasClass("FeaturePodIconButton").NameContaining(string(setting)) |
| } |
| |
| // PodLabelButton generates nodewith.Finder to enter the panel of the specified quick setting pod. |
| func PodLabelButton(setting SettingPod) *nodewith.Finder { |
| if setting == SettingPodDoNotDisturb { |
| return nodewith.HasClass("FeaturePodLabelButton").NameContaining("notification") |
| } |
| |
| return nodewith.HasClass("FeaturePodLabelButton").NameContaining(string(setting)) |
| } |
| |
| // ensureVisible ensures that Quick Settings is shown. If it's not visible, this function will |
| // show Quick Settings and return a cleanup function to hide it. If it is already visible, |
| // this function will do nothing and the returned function will do nothing, since no cleanup is required. |
| func ensureVisible(ctx context.Context, tconn *chrome.TestConn) (func(ctx context.Context) error, error) { |
| shown, err := Shown(ctx, tconn) |
| if err != nil { |
| return nil, err |
| } |
| |
| if !shown { |
| if err := Show(ctx, tconn); err != nil { |
| return nil, err |
| } |
| return func(ctx context.Context) error { |
| return Hide(ctx, tconn) |
| }, nil |
| } |
| |
| return func(ctx context.Context) error { |
| return nil |
| }, nil |
| } |
| |
| // SettingEnabled checks if the specified quick setting is on or off. |
| // In order to check the setting, Quick Settings will be shown if it's not already, |
| // but the original state will be restored once the check is complete. |
| func SettingEnabled(ctx context.Context, tconn *chrome.TestConn, setting SettingPod) (bool, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return false, err |
| } |
| defer cleanup(ctx) |
| |
| pod := PodIconButton(setting) |
| ui := uiauto.New(tconn) |
| info, err := ui.Info(ctx, pod) |
| if err != nil { |
| return false, errors.Wrap(err, "failed to get the pod icon button info") |
| } |
| switch status := info.Checked; status { |
| case checked.True: |
| return true, nil |
| case checked.False: |
| return false, nil |
| default: |
| return false, errors.New("invalid checked state for pod icon button; quick setting may not be toggleable") |
| } |
| } |
| |
| // ToggleSetting toggles a quick setting by clicking the corresponding pod icon. |
| // If Quick Settings is not already shown, it will be opened and then closed once the setting is toggled. |
| func ToggleSetting(ctx context.Context, tconn *chrome.TestConn, setting SettingPod, enable bool) error { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return err |
| } |
| defer cleanup(ctx) |
| |
| if currentState, err := SettingEnabled(ctx, tconn, setting); err != nil { |
| return errors.Wrap(err, "failed to get initial setting state") |
| } else if currentState == enable { |
| return nil |
| } |
| |
| pod := PodIconButton(setting) |
| ui := uiauto.New(tconn) |
| if err := ui.LeftClick(pod)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click the pod icon button") |
| } |
| return nil |
| } |
| |
| // PodRestricted checks if a pod icon is restricted and unable to be used on the lock screen. |
| func PodRestricted(ctx context.Context, tconn *chrome.TestConn, setting SettingPod) (bool, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return false, err |
| } |
| defer cleanup(ctx) |
| |
| pod := PodIconButton(setting) |
| ui := uiauto.New(tconn) |
| info, err := ui.Info(ctx, pod) |
| if err != nil { |
| return false, errors.Wrap(err, "failed to get the pod icon button info") |
| } |
| return info.Restriction == restriction.Disabled, nil |
| } |
| |
| // OpenSettingsApp will launch the Settings app by clicking on the Settings icon and wait |
| // for its icon to appear in the shelf. Quick Settings will be opened if not already shown. |
| func OpenSettingsApp(ctx context.Context, tconn *chrome.TestConn) error { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return err |
| } |
| defer cleanup(ctx) |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).WaitUntilExists(SettingsButton)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find settings top shortcut button") |
| } |
| |
| // Try clicking the Settings button until it goes away, indicating the click was received. |
| // todo(crbug/1099502): determine when this is clickable, and just click it once. |
| opts := testing.PollOptions{Timeout: 10 * time.Second, Interval: 500 * time.Millisecond} |
| if err := ui.WithPollOpts(opts).LeftClickUntil(SettingsButton, ui.Gone(SettingsButton))(ctx); err != nil { |
| return errors.Wrap(err, "settings button still present after clicking") |
| } |
| |
| if err := ash.WaitForApp(ctx, tconn, apps.Settings.ID, time.Minute); err != nil { |
| return errors.Wrapf(err, "settings app did not open within %v seconds", uiTimeout) |
| } |
| |
| return nil |
| } |
| |
| // LockScreen locks the screen. |
| func LockScreen(ctx context.Context, tconn *chrome.TestConn) error { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return err |
| } |
| defer cleanup(ctx) |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).LeftClick(LockButton)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find and click lock button") |
| } |
| |
| if st, err := lockscreen.WaitState(ctx, tconn, func(st lockscreen.State) bool { return st.Locked && st.ReadyForPassword }, uiTimeout); err != nil { |
| return errors.Wrapf(err, "waiting for screen to be locked failed (last status %+v)", st) |
| } |
| |
| return nil |
| |
| } |
| |
| // NotificationsHidden checks that the 'Notifications are hidden' label appears and that no notifications are visible. |
| func NotificationsHidden(ctx context.Context, tconn *chrome.TestConn) (bool, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return false, err |
| } |
| defer cleanup(ctx) |
| |
| // Wait for the 'Notifications are hidden' label at the top of Quick Settings. |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).WaitUntilExists(nodewith.ClassName("NotificationHiddenView"))(ctx); err != nil { |
| return false, errors.Wrap(err, "failed to find notifications hidden view") |
| } |
| |
| // Also check that no notifications are shown in the UI. |
| exists, err := ui.IsNodeFound(ctx, nodewith.ClassName("AshNotificationView")) |
| if err != nil { |
| return false, errors.Wrap(err, "failed checking if notification node exists") |
| } |
| return !exists, nil |
| } |
| |
| // findSlider finds the UI node for the specified slider. Callers should defer releasing the returned node. |
| func findSlider(ctx context.Context, tconn *chrome.TestConn, slider SliderType) (*nodewith.Finder, error) { |
| // The mic gain slider is on the audio settings page of Quick Settings, so we need to navigate there first. |
| if slider == SliderTypeMicGain { |
| if err := OpenAudioSettings(ctx, tconn); err != nil { |
| return nil, err |
| } |
| } |
| |
| ui := uiauto.New(tconn) |
| if err := ui.WithTimeout(uiTimeout).WaitUntilExists(SliderParamMap[slider])(ctx); err != nil { |
| return nil, errors.Wrapf(err, "failed finding the %v slider", slider) |
| } |
| return SliderParamMap[slider], nil |
| } |
| |
| // OpenAudioSettings opens Quick Settings' audio settings page. It does nothing if the page is already open. |
| func OpenAudioSettings(ctx context.Context, tconn *chrome.TestConn) error { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return err |
| } |
| defer cleanup(ctx) |
| |
| audioSettingsBtn := nodewith.Role(role.Button).Name("Audio settings") |
| audioDetailedView := nodewith.ClassName("AudioDetailedView") |
| |
| // If audio settings view is open, just return. |
| ui := uiauto.New(tconn) |
| exist, err := ui.IsNodeFound(ctx, audioDetailedView) |
| if err != nil { |
| return errors.Wrap(err, "failed to check audio detailed view") |
| } |
| if exist { |
| return nil |
| } |
| |
| // Expand the Quick Settings if it is collapsed. |
| if err := Expand(ctx, tconn); err != nil { |
| return err |
| } |
| |
| // It worth noting that LeftClickUntil will check the condition before doing the first |
| // left click. This actually gives time for the UI to be stable before clicking. |
| if err := ui.WithTimeout(uiTimeout).LeftClickUntil(audioSettingsBtn, ui.Exists(audioDetailedView))(ctx); err != nil { |
| return errors.Wrap(err, "failed to click audio settings button to show audio detailed view") |
| } |
| |
| return nil |
| } |
| |
| // SliderValue returns the slider value as an integer. |
| // The slider node's value taken directly is a string expressing a percentage, like "50%". |
| func SliderValue(ctx context.Context, tconn *chrome.TestConn, slider SliderType) (int, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return 0, err |
| } |
| defer cleanup(ctx) |
| |
| s, err := findSlider(ctx, tconn, slider) |
| if err != nil { |
| return 0, err |
| } |
| |
| ui := uiauto.New(tconn) |
| info, err := ui.Info(ctx, s) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to get the slider info") |
| } |
| percent := strings.Replace(info.Value, "%", "", 1) |
| level, err := strconv.Atoi(percent) |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to convert %v to int", percent) |
| } |
| return level, nil |
| } |
| |
| // focusSlider puts the keyboard focus on the slider. The keyboard can then be used to change the slider level. |
| // TODO(crbug/1123231): use better slider automation controls if possible, instead of keyboard controls. |
| func focusSlider(ctx context.Context, tconn *chrome.TestConn, kb *input.KeyboardEventWriter, slider SliderType) error { |
| s, err := findSlider(ctx, tconn, slider) |
| if err != nil { |
| return err |
| } |
| |
| ui := uiauto.New(tconn) |
| info, err := ui.Info(ctx, s) |
| if err != nil { |
| return errors.Wrap(err, "failed to get the slider info") |
| } |
| // Return if already focused. |
| if info.State[state.Focused] { |
| return nil |
| } |
| |
| // Press tab to ensure keyboard focus is already in Quick Settings, otherwise it may not receive the focus. |
| if err := kb.Accel(ctx, "Tab"); err != nil { |
| return errors.Wrap(err, "failed to press tab key") |
| } |
| |
| if err := ui.WithTimeout(uiTimeout).FocusAndWait(s)(ctx); err != nil { |
| return errors.Wrapf(err, "failed to focus the %v slider", slider) |
| } |
| return nil |
| } |
| |
| // changeSlider increments or decrements the slider using the keyboard. |
| // TODO(crbug/1123231): use better slider automation controls if possible, instead of keyboard controls. |
| func changeSlider(ctx context.Context, tconn *chrome.TestConn, kb *input.KeyboardEventWriter, slider SliderType, increase bool) error { |
| key := "up" |
| if !increase { |
| key = "down" |
| } |
| |
| if err := focusSlider(ctx, tconn, kb, slider); err != nil { |
| return err |
| } |
| |
| initial, err := SliderValue(ctx, tconn, slider) |
| if err != nil { |
| return err |
| } |
| |
| if err := kb.Accel(ctx, key); err != nil { |
| return errors.Wrapf(err, "failed to press %v arrow key", key) |
| } |
| |
| // The slider shouldn't move at all if we try to increase/decrease past the min/max value. |
| // The polling below will time out in these cases, so just return immediately after pressing the key. |
| if (initial == 0 && !increase) || (initial == 100 && increase) { |
| return nil |
| } |
| |
| // The value changes smoothly as the slider animates, so wait for it to finish before returning the final value. |
| previous := initial |
| slidingDone := func(ctx context.Context) error { |
| if current, err := SliderValue(ctx, tconn, slider); err != nil { |
| return testing.PollBreak(err) |
| } else if current == initial { |
| return errors.New("slider hasn't started moving yet") |
| } else if current == previous { |
| return nil |
| } else if (increase && current < previous) || (!increase && current > previous) { |
| return testing.PollBreak(errors.Errorf("slider moved opposite of the expected direction from %v to %v", previous, current)) |
| } else { |
| previous = current |
| return errors.New("slider still sliding") |
| } |
| } |
| |
| if err := testing.Poll(ctx, slidingDone, &testing.PollOptions{Interval: 500 * time.Millisecond, Timeout: uiTimeout}); err != nil { |
| return errors.Wrap(err, "failed waiting for slider animation to complete") |
| } |
| return nil |
| } |
| |
| // IncreaseSlider increments the slider positively using the keyboard and returns the new level. |
| func IncreaseSlider(ctx context.Context, tconn *chrome.TestConn, kb *input.KeyboardEventWriter, slider SliderType) (int, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return 0, err |
| } |
| defer cleanup(ctx) |
| |
| if err := changeSlider(ctx, tconn, kb, slider, true); err != nil { |
| return 0, err |
| } |
| |
| return SliderValue(ctx, tconn, slider) |
| } |
| |
| // DecreaseSlider increments the slider positively using the keyboard and returns the new level. |
| func DecreaseSlider(ctx context.Context, tconn *chrome.TestConn, kb *input.KeyboardEventWriter, slider SliderType) (int, error) { |
| cleanup, err := ensureVisible(ctx, tconn) |
| if err != nil { |
| return 0, err |
| } |
| defer cleanup(ctx) |
| |
| if err := changeSlider(ctx, tconn, kb, slider, false); err != nil { |
| return 0, err |
| } |
| |
| return SliderValue(ctx, tconn, slider) |
| } |
| |
| // MicEnabled checks if the microphone is enabled (unmuted). |
| func MicEnabled(ctx context.Context, tconn *chrome.TestConn) (bool, error) { |
| if err := OpenAudioSettings(ctx, tconn); err != nil { |
| return false, err |
| } |
| ui := uiauto.New(tconn) |
| // Scroll the mic toggle into view. |
| kb, err := input.Keyboard(ctx) |
| if err != nil { |
| return false, errors.Wrap(err, "failed to setup keyboard") |
| } |
| if err := kb.Accel(ctx, "Tab"); err != nil { |
| return false, errors.Wrap(err, "failed to press Tab to bring focus into Quick Settings") |
| } |
| if err := ui.FocusAndWait(MicToggle)(ctx); err != nil { |
| return false, errors.Wrap(err, "failed to scroll mic toggle into view") |
| } |
| info, err := ui.Info(ctx, MicToggle) |
| if err != nil { |
| return false, errors.Wrap(err, "failed to get the pod icon button info") |
| } |
| return info.Checked == checked.True, nil |
| } |
| |
| // ToggleMic toggles the microphone's enabled state by clicking the microphone icon adjacent to the slider. |
| // If the microphone is already in the desired state, this will do nothing. |
| func ToggleMic(ctx context.Context, tconn *chrome.TestConn, enable bool) error { |
| if err := OpenAudioSettings(ctx, tconn); err != nil { |
| return err |
| } |
| if current, err := MicEnabled(ctx, tconn); err != nil { |
| return err |
| } else if current != enable { |
| ui := uiauto.New(tconn) |
| if err := ui.DoDefault(MicToggle)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click mic toggle button") |
| } |
| } |
| return nil |
| } |
| |
| // SelectAudioOption selects the audio input or output device with the given name from the audio settings page. |
| func SelectAudioOption(ctx context.Context, tconn *chrome.TestConn, device string) error { |
| if err := OpenAudioSettings(ctx, tconn); err != nil { |
| return err |
| } |
| ui := uiauto.New(tconn) |
| option := nodewith.Role(role.CheckBox).Name(device) |
| |
| // If there are several audio options available, the target option may be out of view. |
| // Furthermore, chrome.automation occasionally reports the wrong location of the audio option after focusing it into view. |
| // Using DoDefault here is more reliable since we cannot rely on a stable location from the a11y tree. |
| if err := ui.DoDefault(option)(ctx); err != nil { |
| return errors.Wrapf(err, "failed to click %v audio option", device) |
| } |
| |
| return nil |
| } |
| |
| // RestrictedSettingsPods returns the setting pods that are restricted when Quick Settings is opened while a user is not signed in. |
| func RestrictedSettingsPods(ctx context.Context) ([]SettingPod, error) { |
| restrictedPods := []SettingPod{SettingPodNetwork} |
| |
| // First check for the bluetooth pod on devices with at least 1 bluetooth adapter. |
| // If bluetooth adapters exists, add the bluetooth settingPod in the restrictedPods list. |
| adapters, err := bluetooth.Adapters(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "unable to get Bluetooth adapters") |
| } |
| if len(adapters) > 0 { |
| restrictedPods = append(restrictedPods, SettingPodBluetooth) |
| } |
| |
| return restrictedPods, nil |
| } |
| |
| // CommonElements returns a map that contains ui.FindParams for Quick Settings UI elements that are present in all sign-in states (signed in, signed out, screen locked). |
| // The keys of the map are descriptive names for the UI elements. |
| func CommonElements(ctx context.Context, tconn *chrome.TestConn, hasBattery, isLockedScreen bool) (map[string]*nodewith.Finder, error) { |
| // Associate the params with a descriptive name for better error reporting. |
| getNodes := map[string]*nodewith.Finder{ |
| "Shutdown button": ShutdownButton, |
| "Collapse button": CollapseButton, |
| "Volume slider": VolumeSlider, |
| "Brightness slider": BrightnessSlider, |
| "Date/time display": DateView, |
| } |
| |
| if hasBattery { |
| getNodes["Battery display"] = BatteryView |
| } |
| |
| if isLockedScreen { |
| // Check that the expected accessibility UI element is shown in Quick Settings. |
| accessibility := PodIconButton(SettingPodAccessibility) |
| getNodes["Accessibility pod"] = accessibility |
| } else { |
| // Get the restricted settings pods. |
| featuredPods, err := RestrictedSettingsPods(ctx) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to get the restricted pod param") |
| } |
| |
| // Add the accessibility and keyboard pods, specific to signIn screen in featuredPods List. |
| featuredPods = append(featuredPods, SettingPodAccessibility) |
| featuredPods = append(featuredPods, SettingPodKeyboard) |
| |
| // Loop through all the SettingsPod and generate the ui.FindParams for the specified quick settings pod. |
| for _, settingPod := range featuredPods { |
| podFinder := PodIconButton(settingPod) |
| getNodes[string(settingPod)+" pod"] = podFinder |
| } |
| } |
| return getNodes, nil |
| } |
| |
| // SignOut signouts by clicking signout button in Uber tray. |
| func SignOut(ctx context.Context, tconn *chrome.TestConn) error { |
| ui := uiauto.New(tconn) |
| |
| if err := Show(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to open Uber tray") |
| } |
| |
| buttonFound, err := ui.IsNodeFound(ctx, SignoutButton) |
| if err != nil { |
| return errors.Wrap(err, "failed to find the sign out button") |
| } |
| if !buttonFound { |
| return errors.New("signout button was not found") |
| } |
| |
| // We ignore errors here because when we click on "Sign out" button |
| // Chrome shuts down and the connection is closed. So we will always get an |
| // error. |
| ui.LeftClick(SignoutButton)(ctx) |
| return nil |
| } |