| // Copyright 2020 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" |
| "encoding/json" |
| "fmt" |
| "strconv" |
| "strings" |
| "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/display" |
| "chromiumos/tast/local/chrome/uiauto/mouse" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/local/screenshot" |
| "chromiumos/tast/testing" |
| ) |
| |
| const ( |
| // Pkg23 Apk compiled against target SDK 23 (Pre-N) |
| Pkg23 = "org.chromium.arc.testapp.windowmanager23" |
| // Pkg24 Apk compiled against target SDK 24 (N) |
| Pkg24 = "org.chromium.arc.testapp.windowmanager24" |
| // Pkg24InPhoneSizeList is an app whose package name is allowlisted so it will be launched in phone size by default. |
| Pkg24InPhoneSizeList = "org.chromium.arc.testapp.windowmanager24.inphonesizelist" |
| // Pkg24InTabletSizeList is an app whose package name is allowlisted so it will be launched in tablet size by default. |
| Pkg24InTabletSizeList = "org.chromium.arc.testapp.windowmanager24.intabletsizelist" |
| // Pkg24InMaximizedList is an app whose package name is allowlisted so it will be launched in maximized by default. |
| Pkg24InMaximizedList = "org.chromium.arc.testapp.windowmanager24.inmaximizedlist" |
| |
| // APKNameArcWMTestApp23 APK name for ArcWMTestApp_23.apk |
| APKNameArcWMTestApp23 = "ArcWMTestApp_23.apk" |
| |
| // APKNameArcWMTestApp24 APK name for ArcWMTestApp_24.apk |
| APKNameArcWMTestApp24 = "ArcWMTestApp_24.apk" |
| |
| // APKNameArcPipSimpleTastTest APK name for ArcPipSimpleTastTest.apk |
| APKNameArcPipSimpleTastTest = "ArcPipSimpleTastTest.apk" |
| |
| // APKNameArcWMTestApp24PhoneSize APK name for ArcWMTestApp_24_InPhoneSizeList.apk. |
| APKNameArcWMTestApp24PhoneSize = "ArcWMTestApp_24_InPhoneSizeList.apk" |
| // APKNameArcWMTestApp24TabletSize APK name for ArcWMTestApp_24_InTabletSizeList.apk. |
| APKNameArcWMTestApp24TabletSize = "ArcWMTestApp_24_InTabletSizeList.apk" |
| // APKNameArcWMTestApp24Maximized APK name for ArcWMTestApp_24_InMaximizedList.apk. |
| APKNameArcWMTestApp24Maximized = "ArcWMTestApp_24_InMaximizedList.apk" |
| |
| // ResizableLandscapeActivity used by the subtests. |
| ResizableLandscapeActivity = "org.chromium.arc.testapp.windowmanager.ResizeableLandscapeActivity" |
| // NonResizableLandscapeActivity used by the subtests. |
| NonResizableLandscapeActivity = "org.chromium.arc.testapp.windowmanager.NonResizeableLandscapeActivity" |
| // ResizableUnspecifiedActivity used by the subtests. |
| ResizableUnspecifiedActivity = "org.chromium.arc.testapp.windowmanager.ResizeableUnspecifiedActivity" |
| // NonResizableUnspecifiedActivity used by the subtests. |
| NonResizableUnspecifiedActivity = "org.chromium.arc.testapp.windowmanager.NonResizeableUnspecifiedActivity" |
| // ResizablePortraitActivity used by the subtests. |
| ResizablePortraitActivity = "org.chromium.arc.testapp.windowmanager.ResizeablePortraitActivity" |
| // NonResizablePortraitActivity used by the subtests. |
| NonResizablePortraitActivity = "org.chromium.arc.testapp.windowmanager.NonResizeablePortraitActivity" |
| // LandscapeActivity used by the subtests. |
| LandscapeActivity = "org.chromium.arc.testapp.windowmanager.LandscapeActivity" |
| // UnspecifiedActivity used by the subtests. |
| UnspecifiedActivity = "org.chromium.arc.testapp.windowmanager.UnspecifiedActivity" |
| // PortraitActivity used by the subtests. |
| PortraitActivity = "org.chromium.arc.testapp.windowmanager.PortraitActivity" |
| |
| // Landscape and Portrait constraints come from: |
| // http://cs/android/vendor/google_arc/packages/development/ArcWMTestApp/src/org/chromium/arc/testapp/windowmanager/BaseActivity.java?l=411 |
| // Landscape used by the subtests. |
| Landscape = "landscape" |
| // Portrait used by the subtests. |
| Portrait = "portrait" |
| |
| // TimeReservedForStop is the time that is reserved to stop an activity after the execution is complete. |
| TimeReservedForStop = 500 * time.Millisecond |
| // RotationAnimationDuration is the time to wait for an animation to complete. |
| RotationAnimationDuration = 750 * time.Millisecond |
| |
| // SplitScreenDividerThickness is the width of the divider when in tablet mode. This comes from: |
| // http://cs/eureka_internal/chromium/src/ash/wm/splitview/split_view_constants.h;l=32;rcl=62c9f9769fdd621050662f3cde82d5672e75271f |
| // Window widths may be adjusted by up to this amount when in split screen mode. |
| SplitScreenDividerThickness = 8 |
| ) |
| |
| // CheckFunc represents a function that checks certain criteria for tests. |
| type CheckFunc func(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error |
| |
| // TestFunc represents a function that tests if the window is in a certain state. |
| type TestFunc func(context.Context, *chrome.TestConn, *arc.ARC, *ui.Device) error |
| |
| // TestCase represents a struct for test names and their func. |
| type TestCase struct { |
| Name string |
| Func TestFunc |
| } |
| |
| // CheckCase represents a struct for activtiy names and their assert func. |
| type CheckCase struct { |
| Name string |
| Func CheckFunc |
| } |
| |
| // CheckMaximizeResizable checks that the window is both maximized and resizable. |
| func CheckMaximizeResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateMaximized); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonMaximizeAndRestore | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckMaximizeNonResizable checks that the window is both maximized and not resizable. |
| func CheckMaximizeNonResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateMaximized); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckRestoreResizable checks that the window is both in restore mode and is resizable. |
| func CheckRestoreResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateNormal); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonMaximizeAndRestore | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckRestoreNonResizable checks that the window is both in restore mode and is not resizable. |
| func CheckRestoreNonResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateNormal); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckPillarboxResizable checks that the window is both in pillar-box mode and is resizable. |
| func CheckPillarboxResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := CheckPillarbox(ctx, tconn, act, d); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonMaximizeAndRestore | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckPillarboxNonResizable checks that the window is both in pillar-box mode and is not resizable. |
| func CheckPillarboxNonResizable(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := CheckPillarbox(ctx, tconn, act, d); err != nil { |
| return testing.PollBreak(err) |
| } |
| wanted := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonClose |
| return CompareCaption(ctx, tconn, act.PackageName(), wanted) |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckPillarbox checks that the window is in pillar-box mode. |
| func CheckPillarbox(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateMaximized); err != nil { |
| return testing.PollBreak(err) |
| } |
| |
| const wanted = Portrait |
| o, err := UIOrientation(ctx, act, d) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| if o != wanted { |
| return errors.Errorf("invalid orientation %v; want %v", o, wanted) |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckMaximizeToFullscreenToggle checks window's bounds transitioning from max to fullscreen. |
| func CheckMaximizeToFullscreenToggle(ctx context.Context, tconn *chrome.TestConn, maxWindowCoords coords.Rect, pkgName string) error { |
| // We need to move the cursor, otherwise the caption keeps visible even in fullscreen if the cursor is on the top edge of the screen. |
| if err := mouse.Move(tconn, maxWindowCoords.CenterPoint(), time.Second)(ctx); err != nil { |
| return errors.Wrap(err, "failed to move mouse to the center") |
| } |
| |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| fullscreenWindow, err := ash.GetARCAppWindowInfo(ctx, tconn, pkgName) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| if maxWindowCoords.Left != fullscreenWindow.TargetBounds.Left || |
| maxWindowCoords.Top != fullscreenWindow.TargetBounds.Top || |
| maxWindowCoords.Width != fullscreenWindow.TargetBounds.Width || |
| maxWindowCoords.Height >= fullscreenWindow.TargetBounds.Height { |
| return errors.Errorf("invalid fullscreen window bounds compared to maximize window bounds, got: %s, want bigger than: %s", fullscreenWindow.TargetBounds, maxWindowCoords) |
| } |
| |
| if fullscreenWindow.IsFrameVisible { |
| return errors.Errorf("invalid frame visibility, got: %t, want: false", fullscreenWindow.IsFrameVisible) |
| } |
| |
| primaryDisplayInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| if primaryDisplayInfo == nil { |
| return testing.PollBreak(errors.New("no primary display info found")) |
| } |
| |
| if primaryDisplayInfo.WorkArea.Top != fullscreenWindow.TargetBounds.Top || |
| primaryDisplayInfo.WorkArea.Height != fullscreenWindow.TargetBounds.Height { |
| return errors.Errorf("invalid fullscreen window bounds compared to display work area, got: Top=%d, Height=%d, want: Top=%d, Height=%d", fullscreenWindow.TargetBounds.Top, fullscreenWindow.TargetBounds.Height, primaryDisplayInfo.WorkArea.Top, primaryDisplayInfo.WorkArea.Height) |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // CheckMaximizeWindowInTabletMode checks the activtiy covers display's work area in maximize mode. |
| func CheckMaximizeWindowInTabletMode(ctx context.Context, tconn *chrome.TestConn, pkgName string) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| // Store activity's window info when tablet mode is enabled to make sure it is in Maximized state. |
| maximizeWindow, err := ash.GetARCAppWindowInfo(ctx, tconn, pkgName) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| primaryDisplayInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return testing.PollBreak(errors.New("failed to get display info")) |
| } |
| if primaryDisplayInfo == nil { |
| return testing.PollBreak(errors.New("no primary display info found")) |
| } |
| |
| if maximizeWindow.IsFrameVisible { |
| return errors.Errorf("invalid frame visibility, got: %t, want: false", maximizeWindow.IsFrameVisible) |
| } |
| |
| if primaryDisplayInfo.WorkArea.Left != maximizeWindow.TargetBounds.Left || |
| primaryDisplayInfo.WorkArea.Top != maximizeWindow.TargetBounds.Top || |
| primaryDisplayInfo.WorkArea.Width != maximizeWindow.TargetBounds.Width || |
| primaryDisplayInfo.WorkArea.Height != maximizeWindow.TargetBounds.Height { |
| return errors.Errorf("invalid maximize window bounds compared to display work area, got: %s, want: %s", maximizeWindow.TargetBounds, primaryDisplayInfo.WorkArea) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // WaitForShelfAnimationComplete waits for 5 seconds to shelf animation complete. |
| func WaitForShelfAnimationComplete(ctx context.Context, tconn *chrome.TestConn) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| shelfInfo, err := ash.FetchScrollableShelfInfoForState(ctx, tconn, &ash.ShelfState{}) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| if shelfInfo.IsShelfWidgetAnimating { |
| return errors.New("shelf is still animating") |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 5 * time.Second}) |
| } |
| |
| // WaitForARCAppWindowState waits for conditions to make sure ARC App window is in correct state and animation finished. |
| func WaitForARCAppWindowState(ctx context.Context, tconn *chrome.TestConn, windowState ash.WindowStateType, windowID int, isFrameVisible bool) error { |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, Pkg24, windowState); err != nil { |
| return errors.Wrap(err, "failed to wait for ARC App window state") |
| } |
| if err := ash.WaitWindowFinishAnimating(ctx, tconn, windowID); err != nil { |
| return errors.Wrap(err, "failed to wait for window finish animating") |
| } |
| if err := ash.WaitForCondition(ctx, tconn, func(w *ash.Window) bool { |
| return w.ID == windowID && w.IsFrameVisible == isFrameVisible |
| }, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil { |
| return errors.Wrap(err, "failed to wait for frame to get hidden") |
| } |
| |
| return nil |
| } |
| |
| // CompareCaption compares the activity caption buttons with the wanted one. |
| // Returns nil only if they are equal. |
| func CompareCaption(ctx context.Context, tconn *chrome.TestConn, pkgName string, wantedCaption ash.CaptionButtonStatus) error { |
| info, err := ash.GetARCAppWindowInfo(ctx, tconn, pkgName) |
| if err != nil { |
| return err |
| } |
| // We should compare both visible and enabled buttons. |
| if info.CaptionButtonEnabledStatus != wantedCaption { |
| return errors.Errorf("unexpected CaptionButtonEnabledStatus value: want %q, got %q", |
| wantedCaption.String(), info.CaptionButtonEnabledStatus.String()) |
| } |
| if info.CaptionButtonVisibleStatus != wantedCaption { |
| return errors.Errorf("unexpected CaptionButtonVisibleStatus value: want %q, got %q", |
| wantedCaption.String(), info.CaptionButtonVisibleStatus.String()) |
| } |
| return nil |
| } |
| |
| // WaitForDisplayOrientation waits for the display to rotate to the desired orientation. |
| func WaitForDisplayOrientation(ctx context.Context, tconn *chrome.TestConn, desiredOrientation display.OrientationType) error { |
| rotationAngle := display.Rotate0 |
| if desiredOrientation == display.OrientationPortraitPrimary { |
| rotationAngle = display.Rotate270 |
| } |
| info, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to get the display info") |
| } |
| return display.WaitForDisplayRotation(ctx, tconn, info.ID, rotationAngle) |
| } |
| |
| // RotateDisplay rotates the screen by the given rotation angle. It returns a cleanup function that should be called to restore the device rotation to the original state. |
| func RotateDisplay(ctx context.Context, tconn *chrome.TestConn, angle display.RotationAngle) (func() error, error) { |
| primaryDisplayInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := display.SetDisplayRotationSync(ctx, tconn, primaryDisplayInfo.ID, angle); err != nil { |
| return nil, err |
| } |
| |
| return func() error { |
| return display.SetDisplayRotationSync(ctx, tconn, primaryDisplayInfo.ID, display.Rotate0) |
| }, nil |
| } |
| |
| // OrientationFromBounds returns orientation from the given bounds. |
| func OrientationFromBounds(bounds coords.Rect) string { |
| if bounds.Height >= bounds.Width { |
| return Portrait |
| } |
| return Landscape |
| } |
| |
| // ToggleFullscreen toggles fullscreen by injecting the Zoom Toggle keycode. |
| func ToggleFullscreen(ctx context.Context, tconn *chrome.TestConn) error { |
| ew, err := input.Keyboard(ctx) |
| if err != nil { |
| return err |
| } |
| l, err := input.KeyboardTopRowLayout(ctx, ew) |
| if err != nil { |
| return err |
| } |
| k := l.ZoomToggle |
| return ew.Accel(ctx, k) |
| } |
| |
| // Helper UI functions |
| // These functions use UI Automator to get / change the state of ArcWMTest activity. |
| |
| // uiState represents the state of ArcWMTestApp activity. See: |
| // http://cs/pi-arc-dev/vendor/google_arc/packages/development/ArcWMTestApp/src/org/chromium/arc/testapp/windowmanager/JsonHelper.java |
| type uiState struct { |
| Orientation string `json:"orientation"` |
| ActivityNr int `json:"activityNr"` |
| Rotation int `json:"rotation"` |
| Accel interface{} `json:"accel"` |
| } |
| |
| // getUIState returns the state from the ArcWMTest activity. |
| // The state is taken by parsing the activity's TextView which contains the state in JSON format. |
| func getUIState(ctx context.Context, act *arc.Activity, d *ui.Device) (*uiState, error) { |
| // Before fetching the UI data, click on "Refresh" button to make sure the data is updated. |
| if err := UIClick(ctx, d, |
| ui.ID("org.chromium.arc.testapp.windowmanager:id/button_refresh"), |
| ui.ClassName("android.widget.Button")); err != nil { |
| return nil, errors.Wrap(err, "failed to click on Refresh button") |
| } |
| |
| // In case the application is still refreshing, let it finish before fetching the data. |
| if err := d.WaitForIdle(ctx, 10*time.Second); err != nil { |
| return nil, errors.Wrap(err, "failed to wait for idle") |
| } |
| |
| obj := d.Object( |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.TextView"), |
| ui.ResourceIDMatches(".+?(/caption_text_view)$")) |
| if err := obj.WaitForExists(ctx, 10*time.Second); err != nil { |
| return nil, err |
| } |
| s, err := obj.GetText(ctx) |
| if err != nil { |
| return nil, err |
| } |
| var state uiState |
| if err := json.Unmarshal([]byte(s), &state); err != nil { |
| return nil, errors.Wrap(err, "failed unmarshalling state") |
| } |
| return &state, nil |
| } |
| |
| // UIOrientation returns the current orientation of the ArcWMTestApp window. |
| func UIOrientation(ctx context.Context, act *arc.Activity, d *ui.Device) (string, error) { |
| s, err := getUIState(ctx, act, d) |
| if err != nil { |
| return "", err |
| } |
| return s.Orientation, nil |
| } |
| |
| // UINumberActivities returns the number of activities present in the ArcWMTestApp stack. |
| func UINumberActivities(ctx context.Context, act *arc.Activity, d *ui.Device) (int, error) { |
| s, err := getUIState(ctx, act, d) |
| if err != nil { |
| return 0, err |
| } |
| return s.ActivityNr, nil |
| } |
| |
| // UIClick sends a "Click" message to an UI Object. |
| // The UI Object is selected from opts, which are the selectors. |
| func UIClick(ctx context.Context, d *ui.Device, opts ...ui.SelectorOption) error { |
| obj := d.Object(opts...) |
| if err := obj.WaitForExists(ctx, 10*time.Second); err != nil { |
| return err |
| } |
| if err := obj.Click(ctx); err != nil { |
| return errors.Wrap(err, "could not click on widget") |
| } |
| return nil |
| } |
| |
| // UIClickUnspecified clicks on the "Unspecified" radio button that is present in the ArcWMTest activity. |
| func UIClickUnspecified(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.RadioButton"), |
| ui.TextMatches("(?i)Unspecified")); err != nil { |
| return errors.Wrap(err, "failed to click on Unspecified radio button") |
| } |
| return nil |
| } |
| |
| // UIClickLandscape clicks on the "Landscape" radio button that is present in the ArcWMTest activity. |
| func UIClickLandscape(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.RadioButton"), |
| ui.TextMatches("(?i)Landscape")); err != nil { |
| return errors.Wrap(err, "failed to click on Landscape radio button") |
| } |
| return nil |
| } |
| |
| // UIClickPortrait clicks on the "Portrait" radio button that is present in the ArcWMTest activity. |
| func UIClickPortrait(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.RadioButton"), |
| ui.TextMatches("(?i)Portrait")); err != nil { |
| return errors.Wrap(err, "failed to click on Portrait radio button") |
| } |
| return nil |
| } |
| |
| // UIClickRootActivity clicks on the "Root Activity" checkbox that is present on the ArcWMTest activity. |
| func UIClickRootActivity(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.CheckBox"), |
| ui.TextMatches("(?i)Root Activity")); err != nil { |
| return errors.Wrap(err, "failed to click on Root Activity checkbox") |
| } |
| return nil |
| } |
| |
| // UIClickImmersive clicks on the "Immersive" button that is present on the ArcWMTest activity. |
| func UIClickImmersive(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.Button"), |
| ui.TextMatches("(?i)Immersive")); err != nil { |
| return errors.Wrap(err, "failed to click on Immersive button") |
| } |
| return nil |
| } |
| |
| // UIClickNormal clicks on the "Normal" button that is present on the ArcWMTest activity. |
| func UIClickNormal(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.Button"), |
| ui.TextMatches("(?i)Normal")); err != nil { |
| return errors.Wrap(err, "failed to click on Normal button") |
| } |
| return nil |
| } |
| |
| // UIClickLaunchActivity clicks on the "Launch Activity" button that is present in the ArcWMTest activity. |
| func UIClickLaunchActivity(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.Button"), |
| ui.TextMatches("(?i)Launch Activity")); err != nil { |
| return errors.Wrap(err, "failed to click on Launch Activity button") |
| } |
| return d.WaitForIdle(ctx, 10*time.Second) |
| } |
| |
| // UIWaitForRestartDialogAndRestart waits for the "Application needs to restart to resize" dialog. |
| // This dialog appears when a Pre-N application tries to switch between maximized / restored window states. |
| // See: http://cs/pi-arc-dev/frameworks/base/core/java/com/android/internal/policy/DecorView.java |
| func UIWaitForRestartDialogAndRestart(ctx context.Context, act *arc.Activity, d *ui.Device) error { |
| if err := UIClick(ctx, d, |
| ui.ClassName("android.widget.Button"), |
| ui.ID("android:id/button1"), |
| ui.TextMatches("(?i)Restart")); err != nil { |
| return errors.Wrap(err, "failed to click on Restart button") |
| } |
| return d.WaitForIdle(ctx, 10*time.Second) |
| } |
| |
| // WaitUntilActivityIsReady waits until the given activity is ready. The "wait" is performed both |
| // at the Ash and Android sides. Additionally, it waits until the "Refresh" button exists. |
| // act must be a "org.chromium.arc.testapp.windowmanager" activity, otherwise the "Refresh" button check |
| // will fail. |
| func WaitUntilActivityIsReady(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity, d *ui.Device) error { |
| if err := ash.WaitForVisible(ctx, tconn, act.PackageName()); err != nil { |
| return err |
| } |
| if err := d.WaitForIdle(ctx, 10*time.Second); err != nil { |
| return err |
| } |
| obj := d.Object( |
| ui.PackageName(act.PackageName()), |
| ui.ClassName("android.widget.Button"), |
| ui.ID("org.chromium.arc.testapp.windowmanager:id/button_refresh")) |
| if err := obj.WaitForExists(ctx, 10*time.Second); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // WaitUntilFrameMatchesCondition waits until the package's window has a frame that matches the given condition. |
| func WaitUntilFrameMatchesCondition(ctx context.Context, tconn *chrome.TestConn, pkgName string, visible bool, mode ash.FrameMode) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| info, err := ash.GetARCAppWindowInfo(ctx, tconn, pkgName) |
| if err != nil { |
| // The window may not yet be known to the Chrome side, so don't stop polling here. |
| return errors.Wrap(err, "failed to get ARC window info") |
| } |
| |
| if info.IsFrameVisible != visible { |
| return errors.Errorf("unwanted window frame visibility: %t", info.IsFrameVisible) |
| } |
| |
| if info.FrameMode != mode { |
| return errors.Errorf("unwanted window frame mode: got %s, want %s", info.FrameMode, mode) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}) |
| } |
| |
| // ChangeDisplayZoomFactor changes the ChromeOS display zoom factor. |
| func ChangeDisplayZoomFactor(ctx context.Context, tconn *chrome.TestConn, dispID string, zoomFactor float64) error { |
| p := display.DisplayProperties{DisplayZoomFactor: &zoomFactor} |
| if err := display.SetDisplayProperties(ctx, tconn, dispID, p); err != nil { |
| return errors.Wrap(err, "failed to set zoom factor") |
| } |
| return nil |
| } |
| |
| // SetupAndRunTestCases sets up the environment for tests and runs testcases. |
| func SetupAndRunTestCases(ctx context.Context, s *testing.State, isTabletMode bool, testCases []TestCase) { |
| cr := s.FixtValue().(*arc.PreData).Chrome |
| a := s.FixtValue().(*arc.PreData).ARC |
| d := s.FixtValue().(*arc.PreData).UIDevice |
| |
| if err := a.Install(ctx, arc.APKPath(APKNameArcWMTestApp24)); err != nil { |
| s.Fatal("Failed to install APK: ", err) |
| } |
| |
| tconn, err := cr.TestAPIConn(ctx) |
| if err != nil { |
| s.Fatal("Failed to create Test API connection: ", err) |
| } |
| |
| cleanup, err := ash.EnsureTabletModeEnabled(ctx, tconn, isTabletMode) |
| if err != nil { |
| |
| s.Fatal("Failed to ensure if tablet mode is ", |
| func() string { |
| if isTabletMode { |
| return "enabled:" |
| } |
| return "disabled:" |
| }(), err) |
| } |
| defer cleanup(ctx) |
| |
| for _, test := range testCases { |
| s.Logf("Running test %q", test.Name) |
| |
| if err := test.Func(ctx, tconn, a, d); err != nil { |
| path := fmt.Sprintf("%s/screenshot-cuj-failed-test-%s.png", s.OutDir(), test.Name) |
| if err := screenshot.CaptureChrome(ctx, cr, path); err != nil { |
| s.Log("Failed to capture screenshot: ", err) |
| } |
| s.Errorf("%s test failed: %v", test.Name, err) |
| } |
| } |
| } |
| |
| // GetButtonBounds is used to get button bounds in a given package name. |
| func GetButtonBounds(ctx context.Context, d *ui.Device, actPkgName string) (coords.Rect, error) { |
| // Get a buttons info. |
| button := d.Object(ui.PackageName(actPkgName), |
| ui.ClassName("android.widget.Button"), |
| ui.ID("org.chromium.arc.testapp.windowmanager:id/button_show")) |
| |
| if err := button.WaitForExists(ctx, 10*time.Second); err != nil { |
| return coords.Rect{}, err |
| } |
| buttonBounds, err := button.GetBounds(ctx) |
| if err != nil { |
| return coords.Rect{}, err |
| } |
| |
| return buttonBounds, nil |
| } |
| |
| // EnsureARCFontScaleChanged changes the android font scale via settings and waits until the font scale changes completely. |
| func EnsureARCFontScaleChanged(ctx context.Context, a *arc.ARC, fontScale float64) error { |
| cmd := a.Command(ctx, "settings", "put", "system", "font_scale", fmt.Sprintf("%f", fontScale)) |
| if err := cmd.Run(); err != nil { |
| return errors.Wrap(err, "unable to run adb shell command") |
| } |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| fs, err := GetARCFontScale(ctx, a) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| |
| if fs != fontScale { |
| return testing.PollBreak(errors.Errorf("unable to wait for font scale to change: got %f, want %f", fs, fontScale)) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 5 * time.Second}) |
| } |
| |
| // GetARCFontScale gets the font scale from the android settings. |
| func GetARCFontScale(ctx context.Context, a *arc.ARC) (float64, error) { |
| cmd := a.Command(ctx, "settings", "get", "system", "font_scale") |
| output, err := cmd.CombinedOutput() |
| if err != nil { |
| return 0, errors.Wrap(err, "unable to run adb shell command") |
| } |
| outStr := strings.TrimSpace(string(output)) |
| fs, err := strconv.ParseFloat(outStr, 64) |
| if err != nil { |
| return 0, errors.Wrapf(err, "invalid font_scale: %q", outStr) |
| } |
| |
| return fs, nil |
| } |
| |
| // CheckVerticalTabletSplit helps to assert window bounds in vertical split mode. |
| func CheckVerticalTabletSplit(ctx context.Context, tconn *chrome.TestConn) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| overActivityWInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, Pkg24InMaximizedList) |
| if err != nil { |
| return errors.Wrap(err, "failed to get arc app window info for over activity") |
| } |
| |
| underActivityWInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, Pkg24) |
| if err != nil { |
| return errors.Wrap(err, "failed to get arc app window info for under activity") |
| } |
| |
| pdInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| displayWorkArea := pdInfo.WorkArea |
| |
| // Over activity must be snapped to the left. The right position can vary up to divider thickness. |
| lWant := coords.NewRect(0, 0, displayWorkArea.Width/2, displayWorkArea.Height) |
| if !coords.CompareBoundsWithMargins(overActivityWInfo.BoundsInRoot, lWant, 0, 0, SplitScreenDividerThickness, 0) { |
| return errors.Errorf("invalid snapped to the left activity bounds: got %+v; want %+v", overActivityWInfo.BoundsInRoot, lWant) |
| } |
| |
| // Under activity must be snapped to the right. The left position can vary up to divider thickness. |
| rWant := coords.NewRect(displayWorkArea.Width/2, 0, displayWorkArea.Width/2, displayWorkArea.Height) |
| if !coords.CompareBoundsWithMargins(underActivityWInfo.BoundsInRoot, rWant, SplitScreenDividerThickness, 0, 0, 0) { |
| return errors.Errorf("invalid snapped to the right activity bounds: got %+v; want %+v", underActivityWInfo, rWant) |
| } |
| |
| // The right window must extend to the end of the screen. |
| rEnd := underActivityWInfo.BoundsInRoot.Left + underActivityWInfo.BoundsInRoot.Width |
| if rEnd != displayWorkArea.Width { |
| return errors.Errorf("right window doesn't extend to end of the screen: got %d; want %d", rEnd, displayWorkArea.Width) |
| } |
| |
| return nil |
| }, &testing.PollOptions{Timeout: 5 * time.Second, Interval: 500 * time.Millisecond}) |
| } |
| |
| // CheckHorizontalTabletSplit helps to assert window bounds in horizontal split mode. |
| func CheckHorizontalTabletSplit(ctx context.Context, tconn *chrome.TestConn) error { |
| return testing.Poll(ctx, func(ctx context.Context) error { |
| underActivityWInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, Pkg24) |
| if err != nil { |
| return errors.Wrap(err, "failed to get arc app window info for under activity") |
| } |
| overActivityWInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, Pkg24InMaximizedList) |
| if err != nil { |
| return errors.Wrap(err, "failed to get arc app window info for over activity") |
| } |
| |
| pdInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| return testing.PollBreak(err) |
| } |
| displayWorkArea := pdInfo.WorkArea |
| |
| // Over activity must be snapped to the top. |
| if overActivityWInfo.BoundsInRoot.Left != 0 || |
| overActivityWInfo.BoundsInRoot.Top != 0 || |
| overActivityWInfo.BoundsInRoot.Width != displayWorkArea.Width || |
| overActivityWInfo.BoundsInRoot.Height >= displayWorkArea.Height/2 { |
| return errors.Errorf("invalid snapped to the top activity bounds: got Left = %d, Top = %d, Width = %d, Height = %d; want Left = 0, Top = 0, Width = %d, Height < %d", |
| overActivityWInfo.BoundsInRoot.Left, overActivityWInfo.BoundsInRoot.Top, overActivityWInfo.BoundsInRoot.Width, overActivityWInfo.BoundsInRoot.Height, |
| displayWorkArea.Width, displayWorkArea.Height/2) |
| } |
| // Under activity must be snapped to the bottom. |
| if underActivityWInfo.BoundsInRoot.Left != 0 || |
| underActivityWInfo.BoundsInRoot.Top <= displayWorkArea.Height/2 || |
| underActivityWInfo.BoundsInRoot.Width != displayWorkArea.Width || |
| underActivityWInfo.BoundsInRoot.Height >= displayWorkArea.Height/2 || |
| underActivityWInfo.BoundsInRoot.Top+underActivityWInfo.BoundsInRoot.Height != displayWorkArea.Height { |
| return errors.Errorf("invalid snapped to the bottom activity bounds: got Left = %d, Top = %d, Width = %d, Height = %d; want Left = 0, Top > %d, Width = %d, Height < %d", |
| underActivityWInfo.BoundsInRoot.Left, underActivityWInfo.BoundsInRoot.Top, underActivityWInfo.BoundsInRoot.Width, underActivityWInfo.BoundsInRoot.Height, |
| displayWorkArea.Height/2, displayWorkArea.Width, displayWorkArea.Height/2) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 5 * time.Second, Interval: 500 * time.Millisecond}) |
| } |