| // 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 wm provides Window Manager Helper functions. |
| package wm |
| |
| import ( |
| "context" |
| "image/color" |
| "math" |
| "time" |
| |
| "chromiumos/tast/common/android/ui" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/arc" |
| "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/nodewith" |
| "chromiumos/tast/local/chrome/uiauto/role" |
| "chromiumos/tast/local/colorcmp" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/local/media/imgcmp" |
| "chromiumos/tast/local/screenshot" |
| "chromiumos/tast/testing" |
| ) |
| |
| const ( |
| // ResizeLockTestPkgName is the package name of the resize lock app. |
| ResizeLockTestPkgName = "org.chromium.arc.testapp.resizelock" |
| // ResizeLockApkName is the apk name of the resize lock app. |
| ResizeLockApkName = "ArcResizeLockTest.apk" |
| // ResizeLockMainActivityName is the main activity name of the resize lock app. |
| ResizeLockMainActivityName = "org.chromium.arc.testapp.resizelock.MainActivity" |
| // ResizeLockUnresizableUnspecifiedActivityName is the name of the unresizable, unspecified activity of the resize lock app. |
| ResizeLockUnresizableUnspecifiedActivityName = "org.chromium.arc.testapp.resizelock.UnresizableUnspecifiedActivity" |
| // ResizeLockUnresizablePortraitActivityName is the name of the unresizable, portrait-only activity of the resize lock app. |
| ResizeLockUnresizablePortraitActivityName = "org.chromium.arc.testapp.resizelock.UnresizablePortraitActivity" |
| // ResizeLockResizableUnspecifiedMaximizedActivityName is the name of the resizable, maximized activity of the resize lock app. |
| ResizeLockResizableUnspecifiedMaximizedActivityName = "org.chromium.arc.testapp.resizelock.ResizableUnspecifiedMaximizedActivity" |
| // ResizeLockPipActivityName is the name of the PIP-able activity of the resize lock app. |
| ResizeLockPipActivityName = "org.chromium.arc.testapp.resizelock.PipActivity" |
| |
| // ResizeLock2PkgName is the package name of the second resize lock app. |
| ResizeLock2PkgName = "org.chromium.arc.testapp.resizelock2" |
| // ResizeLock3PkgName is the package name of the third resize lock app. |
| ResizeLock3PkgName = "org.chromium.arc.testapp.resizelock3" |
| // ResizeLock2ApkName is the apk name of the second resize lock app. |
| ResizeLock2ApkName = "ArcResizeLockTest2.apk" |
| // ResizeLock3ApkName is the apk name of the third resize lock app. |
| ResizeLock3ApkName = "ArcResizeLockTest3.apk" |
| |
| // Used to (i) find the resize lock mode buttons on the compat-mode menu and (ii) check the state of the compat-mode button |
| phoneButtonName = "Phone" |
| tabletButtonName = "Tablet" |
| resizableButtonName = "Resizable" |
| |
| // CenterButtonClassName is the class name of the caption center button. |
| CenterButtonClassName = "FrameCenterButton" |
| // BubbleDialogClassName is the class name of the bubble dialog. |
| BubbleDialogClassName = "BubbleDialogDelegateView" |
| checkBoxClassName = "Checkbox" |
| overlayDialogClassName = "OverlayDialog" |
| shelfIconClassName = "ash/ShelfAppButton" |
| menuItemViewClassName = "MenuItemView" |
| |
| // AppManagementSettingToggleName is the a11y name of the app-management setting toggle. |
| AppManagementSettingToggleName = "Preset window sizes" |
| splashCloseButtonName = "Got it" |
| confirmButtonName = "Allow" |
| cancelButtonName = "Cancel" |
| appInfoMenuItemViewName = "App info" |
| closeMenuItemViewName = "Close" |
| |
| // ResizeLockAppName is the name of the resize lock app. Used to identify the shelf icon of interest. |
| ResizeLockAppName = "ArcResizeLockTest" |
| settingsAppName = "Settings" |
| |
| // Used in test cases where screenshots are taken. |
| pixelColorDiffMargin = 15 |
| clientContentColorPixelPercentThreshold = 95 |
| // When shadow exists, the percentage will be 70~80%, and otherwise, it will be 0%. Let's use the intermediate value. |
| borderColorPixelPercentageThreshold = 35 |
| borderWidthPX = 6 |
| ) |
| |
| // Represents the size of a window. |
| type orientation int |
| |
| const ( |
| phoneOrientation orientation = iota |
| tabletOrientation |
| maximizedOrientation |
| ) |
| |
| func (mode orientation) String() string { |
| switch mode { |
| case phoneOrientation: |
| return "phone" |
| case tabletOrientation: |
| return "tablet" |
| case maximizedOrientation: |
| return "maximized" |
| default: |
| return "unknown" |
| } |
| } |
| |
| // ResizeLockMode represents the high-level state of the app from the resize-lock feature's perspective. |
| type ResizeLockMode int |
| |
| const ( |
| // PhoneResizeLockMode represents the state where an app is locked in a portrait size. |
| PhoneResizeLockMode ResizeLockMode = iota |
| // TabletResizeLockMode represents the state where an app is locked in a landscape size. |
| TabletResizeLockMode |
| // ResizableTogglableResizeLockMode represents the state where an app is not resize lock, and the resize lock state is togglable. |
| ResizableTogglableResizeLockMode |
| // NoneResizeLockMode represents the state where an app is not eligible for resize lock. |
| NoneResizeLockMode |
| ) |
| |
| func (mode ResizeLockMode) String() string { |
| switch mode { |
| case PhoneResizeLockMode: |
| return phoneButtonName |
| case TabletResizeLockMode: |
| return tabletButtonName |
| case ResizableTogglableResizeLockMode: |
| return resizableButtonName |
| default: |
| return "" |
| } |
| } |
| |
| // ConfirmationDialogAction represents the expected behavior and action to take for the resizability confirmation dialog. |
| type ConfirmationDialogAction int |
| |
| const ( |
| // DialogActionNoDialog represents the behavior where resize confirmation dialog isn't shown when a window is resized. |
| DialogActionNoDialog ConfirmationDialogAction = iota |
| // DialogActionCancel represents the behavior where resize confirmation dialog is shown, and the cancel button should be selected. |
| DialogActionCancel |
| // DialogActionConfirm represents the behavior where resize confirmation dialog is shown, and the confirm button should be selected. |
| DialogActionConfirm |
| // DialogActionConfirmWithDoNotAskMeAgainChecked represents the behavior where resize confirmation dialog is shown, and the confirm button should be selected with the "Don't ask me again" option on. |
| DialogActionConfirmWithDoNotAskMeAgainChecked |
| ) |
| |
| // InputMethodType represents how to interact with UI. |
| type InputMethodType int |
| |
| const ( |
| // InputMethodClick represents the state where UI should be interacted with mouse click. |
| InputMethodClick InputMethodType = iota |
| // InputMethodKeyEvent represents the state where UI should be interacted with keyboard. |
| InputMethodKeyEvent |
| ) |
| |
| func (mode InputMethodType) String() string { |
| switch mode { |
| case InputMethodClick: |
| return "click" |
| case InputMethodKeyEvent: |
| return "keyboard" |
| default: |
| return "unknown" |
| } |
| } |
| |
| // CheckResizeLockState verifies the various properties that depend on resize lock state. |
| func CheckResizeLockState(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode ResizeLockMode, isSplashVisible bool) error { |
| resizeLocked := mode == PhoneResizeLockMode || mode == TabletResizeLockMode |
| |
| if err := CheckResizability(ctx, tconn, a, d, activity.PackageName(), getExpectedResizability(activity, mode)); err != nil { |
| return errors.Wrapf(err, "failed to verify the resizability of %s", activity.ActivityName()) |
| } |
| |
| if err := CheckVisibility(ctx, tconn, BubbleDialogClassName, isSplashVisible); err != nil { |
| return errors.Wrapf(err, "failed to verify the visibility of the splash screen on %s", activity.ActivityName()) |
| } |
| |
| if err := CheckCompatModeButton(ctx, tconn, a, d, cr, activity, mode); err != nil { |
| return errors.Wrapf(err, "failed to verify the type of the compat mode button of %s", activity.ActivityName()) |
| } |
| |
| if err := checkBorder(ctx, tconn, a, d, cr, activity, resizeLocked /* shouldShowBorder */); err != nil { |
| return errors.Wrapf(err, "failed to verify the visibility of the resize lock window border of %s", activity.ActivityName()) |
| } |
| |
| // There's no orientation rule for non-resize-locked apps, so only check the phone and tablet modes. |
| if mode == TabletResizeLockMode { |
| if err := checkOrientation(ctx, tconn, a, d, cr, activity, tabletOrientation); err != nil { |
| return errors.Wrapf(err, "failed to verify %s has tablet orientation", activity.ActivityName()) |
| } |
| } else if mode == PhoneResizeLockMode { |
| if err := checkOrientation(ctx, tconn, a, d, cr, activity, phoneOrientation); err != nil { |
| return errors.Wrapf(err, "failed to verify %s has tablet orientation", activity.ActivityName()) |
| } |
| } |
| |
| if err := checkMaximizeRestoreButtonVisibility(ctx, tconn, a, d, cr, activity, mode); err != nil { |
| return errors.Wrapf(err, "failed to verify the visibility of maximize/restore button for %s", activity.ActivityName()) |
| } |
| |
| return nil |
| } |
| |
| // getExpectedResizability returns the resizability based on the resize lock state and the pure resizability of the app. |
| func getExpectedResizability(activity *arc.Activity, mode ResizeLockMode) bool { |
| // Resize-locked apps are unresizable. |
| if mode == PhoneResizeLockMode || mode == TabletResizeLockMode { |
| return false |
| } |
| |
| // The activity with resizability false in its manifest is unresizable. |
| if activity.ActivityName() == ResizeLockUnresizableUnspecifiedActivityName { |
| return false |
| } |
| |
| return true |
| } |
| |
| // checkMaximizeRestoreButtonVisibility verifies the visibility of the maximize/restore button of the given app. |
| func checkMaximizeRestoreButtonVisibility(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode ResizeLockMode) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| expected := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonClose |
| tabletModeStatus, err := ash.TabletModeEnabled(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to get tablet mode status") |
| } |
| // The visibility of the maximize/restore button matches the resizability of the app. |
| if getExpectedResizability(activity, mode) && !tabletModeStatus { |
| expected |= ash.CaptionButtonMaximizeAndRestore |
| } |
| return CompareCaption(ctx, tconn, activity.PackageName(), expected) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // checkOrientation verifies the orientation of the given app. |
| func checkOrientation(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, expectedOrientation orientation) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| actualOrientation, err := activityOrientation(ctx, tconn, activity) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the current orientation of %s", activity.PackageName()) |
| } |
| if actualOrientation != expectedOrientation { |
| errors.Errorf("failed to verify the orientation; want: %s, got: %s", expectedOrientation, actualOrientation) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // activityOrientation returns the current orientation of the given app. |
| func activityOrientation(ctx context.Context, tconn *chrome.TestConn, activity *arc.Activity) (orientation, error) { |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, activity.PackageName()) |
| if err != nil { |
| return maximizedOrientation, errors.Wrapf(err, "failed to ARC window infomation for package name %s", activity.PackageName()) |
| } |
| if window.State == ash.WindowStateMaximized || window.State == ash.WindowStateFullscreen { |
| return maximizedOrientation, nil |
| } |
| if window.BoundsInRoot.Width < window.BoundsInRoot.Height { |
| return phoneOrientation, nil |
| } |
| return tabletOrientation, nil |
| } |
| |
| // CheckCompatModeButton verifies the state of the compat-mode button of the given app. |
| func CheckCompatModeButton(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode ResizeLockMode) error { |
| if mode == NoneResizeLockMode { |
| return CheckVisibility(ctx, tconn, CenterButtonClassName, false /* visible */) |
| } |
| |
| uia := uiauto.New(tconn) |
| button := nodewith.HasClass(CenterButtonClassName) |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| info, err := uia.Info(ctx, button) |
| if err != nil { |
| return errors.Wrap(err, "failed to find the compat-mode button") |
| } |
| |
| if info.Name != mode.String() { |
| return errors.Errorf("failed to verify the name of compat-mode button; got: %s, want: %s", info.Name, mode) |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // checkClientContent verifies the client content fills the entire window. |
| // This is useful to check if switching between phone and tablet modes causes any UI glich. |
| func checkClientContent(ctx context.Context, tconn *chrome.TestConn, cr *chrome.Chrome, activity *arc.Activity) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| bounds, err := activity.WindowBounds(ctx) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the window bounds of %s", activity.ActivityName()) |
| } |
| |
| img, err := screenshot.GrabScreenshot(ctx, cr) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| |
| dispMode, err := ash.PrimaryDisplayMode(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to get display mode of the primary display") |
| } |
| windowInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, activity.PackageName()) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get arc app window info for %s", activity.PackageName()) |
| } |
| captionHeight := int(math.Round(float64(windowInfo.CaptionHeight) * dispMode.DeviceScaleFactor)) |
| |
| totalPixels := (bounds.Height - captionHeight) * bounds.Width |
| bluePixels := imgcmp.CountPixels(img, color.RGBA{0, 0, 255, 255}) |
| bluePercent := bluePixels * 100 / totalPixels |
| |
| if bluePercent < clientContentColorPixelPercentThreshold { |
| return errors.Errorf("failed to verify the number of the blue pixels exceeds the threshold (%d%%); contains %d / %d (%d%%) blue pixels", clientContentColorPixelPercentThreshold, bluePixels, totalPixels, bluePercent) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second, Interval: time.Second}) |
| } |
| |
| // checkBorder checks whether the special window border for compatibility mode is shown or not. |
| // This functions takes a screenshot of the display, and counts the number of pixels that are dark gray around the window border. |
| func checkBorder(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, shouldShowBorder bool) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| bounds, err := activity.WindowBounds(ctx) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the window bounds of %s", activity.ActivityName()) |
| } |
| |
| img, err := screenshot.GrabScreenshot(ctx, cr) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| |
| rect := img.Bounds() |
| shadowOnBorderPixels := 0 |
| borderPixels := 0 |
| for y := rect.Min.Y; y < rect.Max.Y; y++ { |
| for x := rect.Min.X; x < rect.Max.X; x++ { |
| p := coords.Point{X: x, Y: y} |
| onBorder := p.In(bounds.WithInset(-borderWidthPX, -borderWidthPX)) && !p.In(bounds) |
| if onBorder { |
| if colorcmp.ColorsMatch(img.At(x, y), color.RGBA{170, 170, 170, 255}, pixelColorDiffMargin) { |
| shadowOnBorderPixels++ |
| } |
| borderPixels++ |
| } |
| } |
| } |
| |
| // borderPixels is 0 if the window is maximized. There's nothing to verify in this case. |
| if borderPixels != 0 { |
| shadowOnBorderPercentage := int(float64(shadowOnBorderPixels) / float64(borderPixels) * 100.0) |
| if shouldShowBorder && shadowOnBorderPercentage < borderColorPixelPercentageThreshold { |
| return errors.Errorf("failed to verify that the window border is visible; Border has %d%% (%d/%d) of shadow pixels (threshold: %d%%)", shadowOnBorderPercentage, shadowOnBorderPixels, borderPixels, borderColorPixelPercentageThreshold) |
| } |
| if !shouldShowBorder && shadowOnBorderPercentage > borderColorPixelPercentageThreshold { |
| return errors.Errorf("failed to verify that the window border is invisible; Border has %d%% (%d/%d) of shadow pixels (threshold: %d%%)", shadowOnBorderPercentage, shadowOnBorderPixels, borderPixels, borderColorPixelPercentageThreshold) |
| } |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckVisibility checks whether the node specified by the given class name exists or not. |
| func CheckVisibility(ctx context.Context, tconn *chrome.TestConn, className string, visible bool) error { |
| uia := uiauto.New(tconn) |
| finder := nodewith.HasClass(className).First() |
| if visible { |
| return uia.WithTimeout(10 * time.Second).WaitUntilExists(finder)(ctx) |
| } |
| return uia.WithTimeout(10 * time.Second).WaitUntilGone(finder)(ctx) |
| } |
| |
| // CheckResizability verifies the given app's resizability. |
| func CheckResizability(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, pkgName string, expected bool) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, pkgName) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the ARC window infomation for package name %s", pkgName) |
| } |
| if window.CanResize != expected { |
| return errors.Errorf("failed to verify the resizability; got %t, want %t", window.CanResize, expected) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // ToggleCompatModeMenu toggles the compat-mode menu via the given method and verifies the expected visibility of the compat-mode menu. |
| func ToggleCompatModeMenu(ctx context.Context, tconn *chrome.TestConn, method InputMethodType, keyboard *input.KeyboardEventWriter, isMenuVisible bool) error { |
| switch method { |
| case InputMethodClick: |
| return toggleCompatModeMenuViaButtonClick(ctx, tconn, isMenuVisible) |
| case InputMethodKeyEvent: |
| return toggleCompatModeMenuViaKeyboard(ctx, tconn, keyboard, isMenuVisible) |
| } |
| return errors.Errorf("invalid InputMethodType is given: %s", method) |
| } |
| |
| // toggleCompatModeMenuViaButtonClick clicks on the compat-mode button and verifies the expected visibility of the compat-mode menu. |
| func toggleCompatModeMenuViaButtonClick(ctx context.Context, tconn *chrome.TestConn, isMenuVisible bool) error { |
| ui := uiauto.New(tconn) |
| icon := nodewith.Role(role.Button).HasClass(CenterButtonClassName) |
| if err := ui.WithTimeout(10 * time.Second).LeftClick(icon)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click on the compat-mode button") |
| } |
| |
| return CheckVisibility(ctx, tconn, BubbleDialogClassName, isMenuVisible) |
| } |
| |
| // toggleCompatModeMenuViaKeyboard injects the keyboard shortcut and verifies the expected visibility of the compat-mode menu. |
| func toggleCompatModeMenuViaKeyboard(ctx context.Context, tconn *chrome.TestConn, keyboard *input.KeyboardEventWriter, isMenuVisible bool) error { |
| ui := uiauto.New(tconn) |
| accel := func(ctx context.Context) error { |
| if err := keyboard.Accel(ctx, "Search+Alt+C"); err != nil { |
| return errors.Wrap(err, "failed to inject Search+Alt+C") |
| } |
| return nil |
| } |
| dialog := nodewith.Role(role.Window).HasClass(BubbleDialogClassName) |
| if isMenuVisible { |
| return ui.WithTimeout(10*time.Second).WithInterval(2*time.Second).RetryUntil(accel, ui.Exists(dialog))(ctx) |
| } |
| return nil |
| } |
| |
| // waitForCompatModeMenuToDisappear waits for the compat-mode menu to disappear. |
| // After one of the resize lock mode buttons are selected, the compat mode menu disappears after a few seconds of delay. |
| // Can't use chromeui.WaitUntilGone() for this purpose because this function also checks whether the dialog has the "Phone" button or not to ensure that we are checking the correct dialog. |
| func waitForCompatModeMenuToDisappear(ctx context.Context, tconn *chrome.TestConn) error { |
| ui := uiauto.New(tconn) |
| dialog := nodewith.ClassName(BubbleDialogClassName).Role(role.Window) |
| phoneButton := nodewith.HasClass(phoneButtonName).Ancestor(dialog) |
| return ui.WithTimeout(10 * time.Second).WaitUntilGone(phoneButton)(ctx) |
| } |
| |
| // CloseSplash closes the splash screen via the given method. |
| func CloseSplash(ctx context.Context, tconn *chrome.TestConn, method InputMethodType, keyboard *input.KeyboardEventWriter) error { |
| ui := uiauto.New(tconn) |
| splash := nodewith.ClassName(BubbleDialogClassName).Role(role.Window) |
| if err := ui.Exists(splash)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find the splash dialog") |
| } |
| |
| switch method { |
| case InputMethodClick: |
| return closeSplashViaClick(ctx, tconn, splash) |
| case InputMethodKeyEvent: |
| return closeSplashViaKeyboard(ctx, tconn, splash, keyboard) |
| } |
| return nil |
| } |
| |
| // closeSplashViaKeyboard presses the Enter key and closes the splash screen. |
| func closeSplashViaKeyboard(ctx context.Context, tconn *chrome.TestConn, splash *nodewith.Finder, keyboard *input.KeyboardEventWriter) error { |
| ui := uiauto.New(tconn) |
| enter := func(ctx context.Context) error { |
| if err := keyboard.Accel(ctx, "Enter"); err != nil { |
| return errors.Wrap(err, "failed to press the Enter key") |
| } |
| return nil |
| } |
| if err := ui.WithTimeout(10*time.Second).RetryUntil(enter, ui.Gone(splash))(ctx); err != nil { |
| return errors.Wrap(err, "failed to close splash via keyboard") |
| } |
| return nil |
| } |
| |
| // closeSplashViaClick clicks on the close button and closes the splash screen. |
| func closeSplashViaClick(ctx context.Context, tconn *chrome.TestConn, splash *nodewith.Finder) error { |
| ui := uiauto.New(tconn) |
| button := nodewith.Ancestor(splash).Role(role.Button).Name(splashCloseButtonName) |
| if err := ui.WithTimeout(10*time.Second).LeftClickUntil(button, ui.Gone(splash))(ctx); err != nil { |
| return errors.Wrap(err, "failed to close splash via click") |
| } |
| return nil |
| } |
| |
| // ToggleResizeLockMode shows the compat-mode menu, selects one of the resize lock mode buttons on the compat-mode menu via the given method, and verifies the post state. |
| func ToggleResizeLockMode(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, currentMode, nextMode ResizeLockMode, action ConfirmationDialogAction, method InputMethodType, keyboard *input.KeyboardEventWriter) error { |
| preToggleOrientation, err := activityOrientation(ctx, tconn, activity) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the pre-toggle orientation of %s", activity.PackageName()) |
| } |
| if err := ToggleCompatModeMenu(ctx, tconn, method, keyboard, true /* isMenuVisible */); err != nil { |
| return errors.Wrapf(err, "failed to show the compat-mode dialog of %s via %s", activity.ActivityName(), method) |
| } |
| |
| ui := uiauto.New(tconn) |
| compatModeMenuDialog := nodewith.Role(role.Window).HasClass(BubbleDialogClassName) |
| if err := ui.WithTimeout(10 * time.Second).WaitUntilExists(compatModeMenuDialog)(ctx); err != nil { |
| return errors.Wrapf(err, "failed to find the compat-mode menu dialog of %s", activity.ActivityName()) |
| } |
| |
| switch method { |
| case InputMethodClick: |
| if err := selectResizeLockModeViaClick(ctx, tconn, nextMode, compatModeMenuDialog); err != nil { |
| return errors.Wrapf(err, "failed to click on the compat-mode dialog of %s via click", activity.ActivityName()) |
| } |
| case InputMethodKeyEvent: |
| if err := shiftViaTabAndEnter(ctx, tconn, nodewith.Ancestor(compatModeMenuDialog).Role(role.MenuItem).Name(nextMode.String()), keyboard); err != nil { |
| return errors.Wrapf(err, "failed to click on the compat-mode dialog of %s via keyboard", activity.ActivityName()) |
| } |
| } |
| |
| expectedMode := nextMode |
| if action == DialogActionCancel { |
| expectedMode = currentMode |
| } |
| if action != DialogActionNoDialog { |
| if err := waitForCompatModeMenuToDisappear(ctx, tconn); err != nil { |
| return errors.Wrapf(err, "failed to wait for the compat-mode menu of %s to disappear", activity.ActivityName()) |
| } |
| |
| confirmationDialog := nodewith.HasClass(overlayDialogClassName) |
| if err := ui.WithTimeout(10 * time.Second).WaitUntilExists(confirmationDialog)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find the resizability confirmation dialog") |
| } |
| |
| switch method { |
| case InputMethodClick: |
| if err := handleConfirmationDialogViaClick(ctx, tconn, nextMode, confirmationDialog, action); err != nil { |
| return errors.Wrapf(err, "failed to handle the confirmation dialog of %s via click", activity.ActivityName()) |
| } |
| case InputMethodKeyEvent: |
| if err := handleConfirmationDialogViaKeyboard(ctx, tconn, nextMode, confirmationDialog, action, keyboard); err != nil { |
| return errors.Wrapf(err, "failed to handle the confirmation dialog of %s via keyboard", activity.ActivityName()) |
| } |
| } |
| } |
| |
| // The compat-mode dialog stays shown for two seconds by default after resize lock mode is toggled. |
| // Explicitly close the dialog using the Esc key. |
| if err := ui.WithTimeout(5*time.Second).RetryUntil(func(ctx context.Context) error { |
| if err := keyboard.Accel(ctx, "Esc"); err != nil { |
| return errors.Wrap(err, "failed to press the Esc key") |
| } |
| return nil |
| }, ui.Gone(nodewith.Role(role.Window).Name(BubbleDialogClassName)))(ctx); err != nil { |
| return errors.Wrap(err, "failed to verify that the resizability confirmation dialog is invisible") |
| } |
| |
| postToggleOrientation, err := activityOrientation(ctx, tconn, activity) |
| if err != nil { |
| return errors.Wrapf(err, "failed to get the post-toggle orientation of %s", activity.PackageName()) |
| } |
| |
| if preToggleOrientation != postToggleOrientation { |
| if err := checkClientContent(ctx, tconn, cr, activity); err != nil { |
| return errors.Wrapf(err, "failed to verify the client content fills the window of %s", activity.ActivityName()) |
| } |
| } |
| |
| return CheckResizeLockState(ctx, tconn, a, d, cr, activity, expectedMode, false /* isSplashVisible */) |
| } |
| |
| // handleConfirmationDialogViaKeyboard does the given action for the confirmation dialog via keyboard. |
| func handleConfirmationDialogViaKeyboard(ctx context.Context, tconn *chrome.TestConn, mode ResizeLockMode, confirmationDialog *nodewith.Finder, action ConfirmationDialogAction, keyboard *input.KeyboardEventWriter) error { |
| if action == DialogActionCancel { |
| return shiftViaTabAndEnter(ctx, tconn, nodewith.Ancestor(confirmationDialog).Role(role.Button).Name(cancelButtonName), keyboard) |
| } else if action == DialogActionConfirm || action == DialogActionConfirmWithDoNotAskMeAgainChecked { |
| if action == DialogActionConfirmWithDoNotAskMeAgainChecked { |
| if err := shiftViaTabAndEnter(ctx, tconn, nodewith.Ancestor(confirmationDialog).HasClass(checkBoxClassName), keyboard); err != nil { |
| return errors.Wrap(err, "failed to select the checkbox of the resizability confirmation dialog via keyboard") |
| } |
| } |
| return shiftViaTabAndEnter(ctx, tconn, nodewith.Ancestor(confirmationDialog).Role(role.Button).Name(confirmButtonName), keyboard) |
| } |
| return nil |
| } |
| |
| // handleConfirmationDialogViaClick does the given action for the confirmation dialog via click. |
| func handleConfirmationDialogViaClick(ctx context.Context, tconn *chrome.TestConn, mode ResizeLockMode, confirmationDialog *nodewith.Finder, action ConfirmationDialogAction) error { |
| ui := uiauto.New(tconn) |
| if action == DialogActionCancel { |
| cancelButton := nodewith.Ancestor(confirmationDialog).Role(role.Button).Name(cancelButtonName) |
| return ui.WithTimeout(10 * time.Second).LeftClick(cancelButton)(ctx) |
| } else if action == DialogActionConfirm || action == DialogActionConfirmWithDoNotAskMeAgainChecked { |
| if action == DialogActionConfirmWithDoNotAskMeAgainChecked { |
| checkbox := nodewith.HasClass(checkBoxClassName) |
| if err := ui.WithTimeout(10 * time.Second).LeftClick(checkbox)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click on the checkbox of the resizability confirmation dialog") |
| } |
| } |
| |
| confirmButton := nodewith.Ancestor(confirmationDialog).Role(role.Button).Name(confirmButtonName) |
| return ui.WithTimeout(10 * time.Second).LeftClick(confirmButton)(ctx) |
| } |
| return nil |
| } |
| |
| // selectResizeLockModeViaClick clicks on the given resize lock mode button. |
| func selectResizeLockModeViaClick(ctx context.Context, tconn *chrome.TestConn, mode ResizeLockMode, compatModeMenuDialog *nodewith.Finder) error { |
| ui := uiauto.New(tconn) |
| resizeLockModeButton := nodewith.Ancestor(compatModeMenuDialog).Role(role.MenuItem).Name(mode.String()) |
| if err := ui.WithTimeout(10 * time.Second).WaitUntilExists(resizeLockModeButton)(ctx); err != nil { |
| return errors.Wrapf(err, "failed to find the %s button on the compat mode menu", mode) |
| } |
| return ui.LeftClick(resizeLockModeButton)(ctx) |
| } |
| |
| // shiftViaTabAndEnter keeps pressing the Tab key until the UI element of interest gets focus, and press the Enter key. |
| func shiftViaTabAndEnter(ctx context.Context, tconn *chrome.TestConn, target *nodewith.Finder, keyboard *input.KeyboardEventWriter) error { |
| ui := uiauto.New(tconn) |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| if err := keyboard.Accel(ctx, "Tab"); err != nil { |
| return errors.Wrap(err, "failed to press the Tab key") |
| } |
| if err := ui.Exists(target)(ctx); err != nil { |
| return testing.PollBreak(errors.Wrap(err, "failed to find the node seeking focus")) |
| } |
| return ui.Exists(target.Focused())(ctx) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil { |
| return errors.Wrap(err, "failed to shift focus to the node to click on") |
| } |
| return keyboard.Accel(ctx, "Enter") |
| } |
| |
| // ToggleAppManagementSettingToggle opens the app-management page for the given app via the shelf icon, toggles the resize lock setting, and verifies the states of the app and the setting toggle. |
| func ToggleAppManagementSettingToggle(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, appName string, currentMode, nextMode ResizeLockMode, method InputMethodType, keyboard *input.KeyboardEventWriter) error { |
| // This check must be done before opening the Chrome OS settings page so it won't affect the screenshot taken in one of the checks. |
| if err := CheckResizeLockState(ctx, tconn, a, d, cr, activity, currentMode, false /* isSplashVisible */); err != nil { |
| return errors.Wrapf(err, "failed to verify resize lock state of %s", appName) |
| } |
| |
| if err := OpenAppManagementSetting(ctx, tconn, appName); err != nil { |
| return errors.Wrapf(err, "failed to open the app management page of %s", appName) |
| } |
| |
| if err := checkAppManagementSettingToggleState(ctx, tconn, currentMode); err != nil { |
| return errors.Wrap(err, "failed to verify the state of the setting toggle before toggling the setting") |
| } |
| |
| switch method { |
| case InputMethodClick: |
| if err := toggleAppManagementSettingToggleViaClick(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to toggle the resize-lock setting toggle on the Chrome OS settings via click") |
| } |
| case InputMethodKeyEvent: |
| if err := shiftViaTabAndEnter(ctx, tconn, nodewith.Name(AppManagementSettingToggleName).Role(role.ToggleButton), keyboard); err != nil { |
| return errors.Wrap(err, "failed to toggle the resize-lock setting toggle on the Chrome OS settings via keyboard") |
| } |
| } |
| |
| if err := checkAppManagementSettingToggleState(ctx, tconn, nextMode); err != nil { |
| return errors.Wrap(err, "failed to verify the state of the setting toggle after toggling the setting") |
| } |
| |
| if err := CloseAppManagementSetting(ctx, tconn); err != nil { |
| return errors.Wrapf(err, "failed to close the app management page of %s", appName) |
| } |
| |
| // This check must be done after closing the Chrome OS settings page so it won't affect the screenshot taken in one of the checks. |
| if err := CheckResizeLockState(ctx, tconn, a, d, cr, activity, nextMode, false /* isSplashVisible */); err != nil { |
| return errors.Wrapf(err, "failed to verify resize lock state of %s", activity.ActivityName()) |
| } |
| |
| return nil |
| } |
| |
| // toggleAppManagementSettingToggleViaClick toggles the resize-lock setting toggle via click. |
| func toggleAppManagementSettingToggleViaClick(ctx context.Context, tconn *chrome.TestConn) error { |
| return uiauto.New(tconn).WithTimeout(10 * time.Second).LeftClick(nodewith.Name(AppManagementSettingToggleName))(ctx) |
| } |
| |
| // OpenAppManagementSetting opens the app management page if the given app. |
| func OpenAppManagementSetting(ctx context.Context, tconn *chrome.TestConn, appName string) error { |
| uia := uiauto.New(tconn) |
| resizeLockShelfIcon := nodewith.Name(appName).HasClass(shelfIconClassName) |
| if err := uia.WithTimeout(10 * time.Second).RightClick(resizeLockShelfIcon)(ctx); err != nil { |
| return errors.Wrapf(err, "failed to click on the shelf icon of %s", appName) |
| } |
| |
| appInfoMenuItem := nodewith.Name(appInfoMenuItemViewName).HasClass(menuItemViewClassName) |
| if err := uia.WithTimeout(10 * time.Second).LeftClick(appInfoMenuItem)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find and click on the menu item for the app-management page") |
| } |
| return nil |
| } |
| |
| // CloseAppManagementSetting closes any open app management page. |
| func CloseAppManagementSetting(ctx context.Context, tconn *chrome.TestConn) error { |
| uia := uiauto.New(tconn) |
| settingShelfIcon := nodewith.Name(settingsAppName).HasClass(shelfIconClassName) |
| if err := uia.WithTimeout(10 * time.Second).RightClick(settingShelfIcon)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find and right click on the shelf icon of the settings app") |
| } |
| |
| closeMenuItem := nodewith.Name(closeMenuItemViewName).HasClass(menuItemViewClassName) |
| if err := uia.WithTimeout(10 * time.Second).LeftClick(closeMenuItem)(ctx); err != nil { |
| return errors.Wrap(err, "failed to find and click on the menu item for closing the settings app") |
| } |
| return nil |
| } |
| |
| // checkAppManagementSettingToggleState verifies the resize lock setting state on the app-management page. |
| // The app management page must be open when this function is called. |
| func checkAppManagementSettingToggleState(ctx context.Context, tconn *chrome.TestConn, mode ResizeLockMode) error { |
| uia := uiauto.New(tconn) |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| settingToggle, err := uia.WithTimeout(2*time.Second).Info(ctx, nodewith.Name(AppManagementSettingToggleName)) |
| if err != nil { |
| return errors.Wrap(err, "failed to find the resize lock setting toggle on the app-management page") |
| } |
| |
| if ((mode == PhoneResizeLockMode || mode == TabletResizeLockMode) && settingToggle.Checked == checked.False) || |
| (mode == ResizableTogglableResizeLockMode && settingToggle.Checked == checked.True) { |
| return errors.Errorf("the app-management resize lock setting value (%v) doesn't match the expected curent state (%s)", settingToggle.Checked, mode) |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |