blob: 71279c2e2f7b7de6d82ef04159bd30aac392af2c [file] [log] [blame]
// 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 arc
import (
"context"
"time"
"chromiumos/tast/ctxutil"
"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/ui/mouse"
"chromiumos/tast/local/coords"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: WindowOutsideDisplay,
Desc: "Ensures an ARC window can move outside the display",
Contacts: []string{"tetsui@chromium.org", "arc-framework+tast@google.com"},
SoftwareDeps: []string{"chrome"},
Attr: []string{"group:mainline", "informational"},
// TODO(yusukes): Change the timeout back to 4 min when we revert arc.go's BootTimeout to 120s.
Timeout: 5 * time.Minute,
Params: []testing.Param{{
ExtraSoftwareDeps: []string{"android_p"},
}, {
Name: "vm",
ExtraSoftwareDeps: []string{"android_vm"},
}},
})
}
func WindowOutsideDisplay(ctx context.Context, s *testing.State) {
cr, err := chrome.New(ctx, chrome.ARCEnabled(), chrome.ExtraArgs("--force-tablet-mode=clamshell"))
if err != nil {
s.Fatal("Failed to connect to Chrome: ", err)
}
defer cr.Close(ctx)
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to connect to test API: ", err)
}
a, err := arc.New(ctx, s.OutDir())
if err != nil {
s.Fatal("Failed to start ARC: ", err)
}
defer a.Close()
const (
pkg = "com.android.settings"
activityName = ".Settings"
dragDur = time.Second
marginPX = 2
)
act, err := arc.NewActivity(a, pkg, activityName)
if err != nil {
s.Fatal("Failed to create the settings activity: ", err)
}
defer act.Close()
if err := act.Start(ctx, tconn); err != nil {
s.Fatal("Failed to start the settings activity: ", err)
}
defer act.Stop(ctx, tconn)
window, err := ash.FindWindow(ctx, tconn, func(window *ash.Window) bool {
return window.ARCPackageName == pkg
})
if err != nil {
s.Fatal("Failed to find the ARC window: ", err)
}
info, err := display.FindInfo(ctx, tconn, func(info *display.Info) bool {
return info.ID == window.DisplayID
})
if err != nil {
s.Fatal("Failed to find the display: ", err)
}
dispMode, err := info.GetSelectedMode()
if err != nil {
s.Fatal("Failed to get the selected display mode: ", err)
}
if err := act.SetWindowState(ctx, tconn, arc.WindowStateNormal); err != nil {
s.Fatal("Failed to set the window state to normal: ", err)
}
if err := ash.WaitForCondition(ctx, tconn, func(cur *ash.Window) bool {
return cur.ID == window.ID && cur.State == ash.WindowStateNormal && !cur.IsAnimating
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
s.Fatal("Failed to wait for the window to finish animating: ", err)
}
nearlyEqual := func(a, b int) bool {
diff := a - b
if diff < 0 {
diff = -diff
}
return diff <= marginPX
}
// Waits for the window bounds to be updated on the Android side.
waitForWindowBounds := func(ctx context.Context, expected coords.Rect) error {
expected = coords.ConvertBoundsFromDPToPX(expected, dispMode.DeviceScaleFactor)
return testing.Poll(ctx, func(ctx context.Context) error {
actual, err := act.WindowBounds(ctx)
if err != nil {
return testing.PollBreak(err)
}
if !nearlyEqual(expected.Left, actual.Left) ||
!nearlyEqual(expected.Top, actual.Top) ||
!nearlyEqual(expected.Width, actual.Width) ||
!nearlyEqual(expected.Height, actual.Height) {
return errors.Errorf("window bounds doesn't match: got %v, want %v", actual, expected)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// Use Bounds instead of WorkArea as it is always divisible by 4.
initBounds := info.Bounds
// Use large enough bounds as the Settings activity has minimum height / width.
initBounds.Width = initBounds.Width / 4 * 3
initBounds.Height = initBounds.Height / 4 * 3
if actualBounds, _, err := ash.SetWindowBounds(ctx, tconn, window.ID, initBounds, window.DisplayID); err != nil {
s.Fatal("Failed to set window bounds: ", err)
} else if actualBounds != initBounds {
s.Fatalf("Failed to resize the activity: got %v; want %v", actualBounds, initBounds)
}
// Grab the center of the window caption bar.
initDst := coords.NewPoint(initBounds.Width/2, window.CaptionHeight/2)
if err := mouse.Move(ctx, tconn, initDst, 0); err != nil {
s.Fatal("Failed to move the mouse: ", err)
}
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, time.Second*5)
defer cancel()
if err := mouse.Press(ctx, tconn, mouse.LeftButton); err != nil {
s.Fatal("Failed to press the mouse button: ", err)
}
defer func(ctx context.Context) {
if err := mouse.Release(ctx, tconn, mouse.LeftButton); err != nil {
s.Error("Failed to release the mouse button: ", err)
}
}(cleanupCtx)
// Drag the window to the four corners of the work area minus the inset.
r := info.WorkArea.WithInset(window.CaptionHeight/2, window.CaptionHeight/2)
for _, dst := range []coords.Point{r.TopLeft(), r.TopRight(), r.BottomRight(), r.BottomLeft()} {
if err := mouse.Move(ctx, tconn, dst, dragDur); err != nil {
s.Fatal("Failed to move the mouse: ", err)
}
offset := dst.Sub(initDst)
expectedBounds := initBounds.WithOffset(offset.X, offset.Y)
if err := waitForWindowBounds(ctx, expectedBounds); err != nil {
s.Fatal("Failed to wait for the activity to move: ", err)
}
}
}