| // Copyright 2022 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 arc |
| |
| import ( |
| "context" |
| "time" |
| |
| "chromiumos/tast/common/android/adb" |
| "chromiumos/tast/common/android/ui" |
| "chromiumos/tast/ctxutil" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/arc" |
| "chromiumos/tast/local/bundles/cros/arc/wm" |
| "chromiumos/tast/local/chrome" |
| "chromiumos/tast/local/chrome/ash" |
| "chromiumos/tast/local/chrome/display" |
| "chromiumos/tast/local/chrome/uiauto" |
| "chromiumos/tast/local/chrome/uiauto/pointer" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/local/power" |
| ashwallpaper "chromiumos/tast/local/wallpaper" |
| ashwallpaperconstants "chromiumos/tast/local/wallpaper/constants" |
| "chromiumos/tast/testing" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: CompatSnap, |
| LacrosStatus: testing.LacrosVariantUnneeded, |
| Desc: "Tests compatible snapping works properly for resize-locked ARC apps", |
| Contacts: []string{"toshikikikuchi@chromium.org", "arc-framework+tast@google.com"}, |
| Attr: []string{"group:mainline", "informational"}, |
| SoftwareDeps: []string{"chrome", "android_vm"}, |
| Timeout: 4 * time.Minute, |
| // TODO(b/215063759): Replace this with arcBootedInClamshellMode after the feature is launched. |
| Fixture: "arcBootedInClamshellModeWithCompatSnap", |
| }) |
| } |
| |
| func windowDragPoint(ctx context.Context, tconn *chrome.TestConn, act *arc.Activity) (coords.Point, error) { |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, act.PackageName()) |
| if err != nil { |
| return coords.NewPoint(0, 0), errors.Wrap(err, "failed to get window info") |
| } |
| // As resize-locked windows have the compat mode button at the center of the caption, we need to drag on the right of the back button instead of the center point. |
| return coords.NewPoint(window.BoundsInRoot.Left+100, window.BoundsInRoot.Top+window.CaptionHeight/2), nil |
| } |
| |
| func waitForWindowState(ctx context.Context, tconn *chrome.TestConn, d *ui.Device, act *arc.Activity, arcWindowState arc.WindowState) error { |
| ashWindowState, err := arcWindowState.ToAshWindowState() |
| if err != nil { |
| return errors.Wrap(err, "failed to convert arc window state to ash window state") |
| } |
| |
| if err := d.WaitForIdle(ctx, 10*time.Second); err != nil { |
| return errors.Wrap(err, "failed to wait for Android to be idle") |
| } |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, act.PackageName()) |
| if err != nil { |
| return errors.Wrap(err, "failed to get window info") |
| } |
| if err := ash.WaitWindowFinishAnimating(ctx, tconn, window.ID); err != nil { |
| return errors.Wrap(err, "failed to wait for the window animation") |
| } |
| |
| if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ashWindowState); err != nil { |
| return errors.Wrapf(err, "failed to wait for ash-side window state: want %v", ashWindowState) |
| } |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| actualArcWindowState, err := act.GetWindowState(ctx) |
| if err != nil { |
| return testing.PollBreak(errors.Wrap(err, "could not get ARC window state")) |
| } |
| if actualArcWindowState != arcWindowState { |
| return errors.Errorf("unexpected ARC window state: got %v; want %v", actualArcWindowState, arcWindowState) |
| } |
| return nil |
| }, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil { |
| return errors.Wrap(err, "timed out waiting for ARC window state transition") |
| } |
| |
| return nil |
| } |
| |
| func checkCompatSnappedWindowState(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, cr *chrome.Chrome, d *ui.Device, act *arc.Activity, primary bool, stableWidth int) error { |
| snappedArcWindowState := arc.WindowStateSecondarySnapped |
| if primary { |
| snappedArcWindowState = arc.WindowStatePrimarySnapped |
| } |
| |
| if err := waitForWindowState(ctx, tconn, d, act, snappedArcWindowState); err != nil { |
| return errors.Wrap(err, "failed to wait until window state changes to snapped") |
| } |
| |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, act.PackageName()) |
| if err != nil { |
| return errors.Wrap(err, "failed to get window info") |
| } |
| |
| snappedWidth := window.BoundsInRoot.Width |
| if stableWidth != snappedWidth { |
| return errors.Wrapf(err, "incorrect compat-snapped window width: got %v; want %v", snappedWidth, stableWidth) |
| } |
| |
| if err := wm.CheckResizeLockState(ctx, tconn, a, d, cr, act, wm.PhoneResizeLockMode, false /* isSplashVisible */); err != nil { |
| return errors.Wrap(err, "failed to verify the resize lock state after snapping") |
| } |
| |
| return nil |
| } |
| |
| func testUnsnapByDragging(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, cr *chrome.Chrome, pc pointer.Context, displayInfo *display.Info, d *ui.Device, act *arc.Activity, stableWidth int) error { |
| dragPoint, err := windowDragPoint(ctx, tconn, act) |
| if err != nil { |
| return errors.Wrap(err, "failed to get window drag point") |
| } |
| if err := pc.Drag( |
| dragPoint, |
| pc.DragTo(displayInfo.Bounds.CenterPoint(), 2*time.Second))(ctx); err != nil { |
| return errors.Wrap(err, "failed to drag to unsnap") |
| } |
| |
| if err := waitForWindowState(ctx, tconn, d, act, arc.WindowStateNormal); err != nil { |
| return errors.Wrap(err, "failed to wait until window state changes to normal") |
| } |
| |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, act.PackageName()) |
| if err != nil { |
| return errors.Wrap(err, "failed to get window info") |
| } |
| snappedWidth := window.BoundsInRoot.Width |
| if stableWidth != snappedWidth { |
| return errors.Wrapf(err, "incorrect compat-snapped window width: got %v; want %v", snappedWidth, stableWidth) |
| } |
| |
| if err := wm.CheckResizeLockState(ctx, tconn, a, d, cr, act, wm.PhoneResizeLockMode, false /* isSplashVisible */); err != nil { |
| return errors.Wrap(err, "failed to verify the resize lock state of compat-snapped window") |
| } |
| return nil |
| } |
| |
| func testSnapFromOverview(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, cr *chrome.Chrome, pc pointer.Context, displayInfo *display.Info, d *ui.Device, act *arc.Activity, primary bool, stableWidth int) error { |
| if err := ash.SetOverviewModeAndWait(ctx, tconn, true); err != nil { |
| return errors.Wrap(err, "failed to enter overview") |
| } |
| |
| w, err := ash.FindFirstWindowInOverview(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to find window in overview grid") |
| } |
| |
| displayEdgeMargin := 20 |
| snapDestinationX := displayInfo.Bounds.Width - displayEdgeMargin |
| if primary { |
| snapDestinationX = displayEdgeMargin |
| } |
| if err := pc.Drag( |
| w.OverviewInfo.Bounds.CenterPoint(), |
| pc.DragTo(coords.NewPoint(snapDestinationX, displayInfo.Bounds.Height/2), 2*time.Second))(ctx); err != nil { |
| return errors.Wrap(err, "failed to drag to snap from overview") |
| } |
| |
| if err := checkCompatSnappedWindowState(ctx, tconn, a, cr, d, act, primary, stableWidth); err != nil { |
| return errors.Wrap(err, "failed to wait until window state change") |
| } |
| |
| return nil |
| } |
| |
| func testSnapByDragToSnap(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, cr *chrome.Chrome, pc pointer.Context, displayInfo *display.Info, d *ui.Device, act *arc.Activity, primary bool, stableWidth int) error { |
| snapDestinationX := displayInfo.Bounds.Width |
| if primary { |
| snapDestinationX = 0 |
| } |
| dragPoint, err := windowDragPoint(ctx, tconn, act) |
| if err != nil { |
| return errors.Wrap(err, "failed to get window drag point") |
| } |
| if err := pc.Drag( |
| dragPoint, |
| pc.DragTo(coords.NewPoint(snapDestinationX, displayInfo.Bounds.Height/2), 2*time.Second))(ctx); err != nil { |
| return errors.Wrap(err, "failed to drag to snap from overview") |
| } |
| |
| if err := checkCompatSnappedWindowState(ctx, tconn, a, cr, d, act, primary, stableWidth); err != nil { |
| return errors.Wrap(err, "failed to wait until window state change") |
| } |
| |
| return nil |
| } |
| |
| func testSnapViaKeyboardShortcut(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, cr *chrome.Chrome, d *ui.Device, act *arc.Activity, primary bool, stableWidth int) error { |
| ew, err := input.Keyboard(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to open keyboard device") |
| } |
| defer ew.Close() |
| |
| shortcutCommand := "Alt+]" |
| if primary { |
| shortcutCommand = "Alt+[" |
| } |
| if err := ew.Accel(ctx, shortcutCommand); err != nil { |
| return errors.Wrap(err, "failed to write keyboard events") |
| } |
| |
| if err := checkCompatSnappedWindowState(ctx, tconn, a, cr, d, act, primary, stableWidth); err != nil { |
| return errors.Wrap(err, "failed to wait until window state change") |
| } |
| |
| return nil |
| } |
| |
| func CompatSnap(ctx context.Context, s *testing.State) { |
| // Reserve ten seconds for various cleanup. |
| cleanupCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, time.Second*10) |
| defer cancel() |
| |
| if err := power.TurnOnDisplay(ctx); err != nil { |
| s.Fatal("Failed to turn on display: ", err) |
| } |
| |
| cr := s.FixtValue().(*arc.PreData).Chrome |
| a := s.FixtValue().(*arc.PreData).ARC |
| d := s.FixtValue().(*arc.PreData).UIDevice |
| |
| tconn, err := cr.TestAPIConn(ctx) |
| if err != nil { |
| s.Fatal("Creating test API connection Failed: ", err) |
| } |
| |
| pc := pointer.NewMouse(tconn) |
| defer pc.Close() |
| |
| displayInfo, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get the primary display info: ", err) |
| } |
| |
| // Ensures landscape orientation. |
| orientation, err := display.GetOrientation(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to obtain the orientation info: ", err) |
| } |
| if orientation.Type == display.OrientationPortraitPrimary { |
| if err = display.SetDisplayRotationSync(ctx, tconn, displayInfo.ID, display.Rotate90); err != nil { |
| s.Fatal("Failed to rotate display: ", err) |
| } |
| defer display.SetDisplayRotationSync(cleanupCtx, tconn, displayInfo.ID, display.Rotate0) |
| } |
| |
| // Set a pure white wallpaper to reduce the noises on a screenshot because currently wm.CheckResizeLockState checks the visibility of the translucent window border based on a screenshot. |
| // The wallpaper will exist continuous if the Chrome session gets reused. |
| ui := uiauto.New(tconn) |
| if err := ashwallpaper.OpenWallpaperPicker(ui)(ctx); err != nil { |
| s.Fatal("Failed to open wallpaper picker: ", err) |
| } |
| if err := ashwallpaper.SelectCollection(ui, ashwallpaperconstants.SolidColorsCollection)(ctx); err != nil { |
| s.Fatal("Failed to select wallpaper collection: ", err) |
| } |
| if err := ashwallpaper.SelectImage(ui, "White")(ctx); err != nil { |
| s.Fatal("Failed to select wallpaper image: ", err) |
| } |
| if err := ashwallpaper.CloseWallpaperPicker()(ctx); err != nil { |
| s.Fatal("Failed to close wallpaper picker: ", err) |
| } |
| |
| // Install the test app. |
| if err := a.Install(ctx, arc.APKPath(wm.ResizeLockApkName), adb.InstallOptionFromPlayStore); err != nil { |
| s.Fatal("Failed to install app from PlayStore: ", err) |
| } |
| defer a.Uninstall(cleanupCtx, wm.ResizeLockTestPkgName) |
| |
| // Launch the test app. |
| act, err := arc.NewActivity(a, wm.ResizeLockTestPkgName, wm.ResizeLockMainActivityName) |
| if err != nil { |
| s.Fatal("Failed to create a new activity: ", err) |
| } |
| defer act.Close() |
| if err := act.Start(ctx, tconn); err != nil { |
| s.Fatal("Failed to start the activity: ", err) |
| } |
| defer act.Stop(cleanupCtx, tconn) |
| if err := ash.WaitForVisible(ctx, tconn, act.PackageName()); err != nil { |
| s.Fatal("Failed to wait until the activity gets visible: ", err) |
| } |
| if err := d.WaitForIdle(ctx, 10*time.Second); err != nil { |
| s.Fatal("Failed to wait for Android to be idle: ", err) |
| } |
| |
| // Close the compat mode splash dialog. |
| if err := wm.CheckVisibility(ctx, tconn, wm.BubbleDialogClassName, true); err != nil { |
| s.Fatal("Failed to wait for splash: ", err) |
| } |
| if err := wm.CloseSplash(ctx, tconn, wm.InputMethodClick, nil); err != nil { |
| s.Fatal("Failed to close splash: ", err) |
| } |
| |
| // Measure the window width of the stable state. |
| window, err := ash.GetARCAppWindowInfo(ctx, tconn, act.PackageName()) |
| if err != nil { |
| s.Fatal("Failed to get window info: ", err) |
| } |
| stableWidth := window.BoundsInRoot.Width |
| |
| defer ash.SetOverviewModeAndWait(cleanupCtx, tconn, false) |
| for _, primary := range []bool{false, true} { |
| // Case A. Snap the resize-locked window from overview mode. |
| if err := testSnapFromOverview(ctx, tconn, a, cr, pc, displayInfo, d, act, primary, stableWidth); err != nil { |
| s.Fatalf("Failed to snap window from overview (primary=%t): %v", primary, err) |
| } |
| if err := testUnsnapByDragging(ctx, tconn, a, cr, pc, displayInfo, d, act, stableWidth); err != nil { |
| s.Fatalf("Failed to unsnap window (primary=%t): %v", primary, err) |
| } |
| |
| // Case B. Snap the resize-locked window by dragging the caption bar to the edge of the screen. |
| if err := testSnapByDragToSnap(ctx, tconn, a, cr, pc, displayInfo, d, act, primary, stableWidth); err != nil { |
| s.Fatalf("Failed to snap window by drag-to-snap (primary=%t): %v", primary, err) |
| } |
| if err := testUnsnapByDragging(ctx, tconn, a, cr, pc, displayInfo, d, act, stableWidth); err != nil { |
| s.Fatalf("Failed to unsnap window (primary=%t): %v", primary, err) |
| } |
| |
| // Case C. Snap the resize-locked window via keyboard shortcut. |
| if err := testSnapViaKeyboardShortcut(ctx, tconn, a, cr, d, act, primary, stableWidth); err != nil { |
| s.Fatalf("Failed to snap window via keyboard shortcut (primary=%t): %v", primary, err) |
| } |
| if err := testUnsnapByDragging(ctx, tconn, a, cr, pc, displayInfo, d, act, stableWidth); err != nil { |
| s.Fatalf("Failed to unsnap window (primary=%t): %v", primary, err) |
| } |
| } |
| } |