blob: 97498100e68a58386ccaf17cab97460c8f8dc77f [file] [log] [blame]
// Copyright 2019 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"
"image"
"image/color"
"path/filepath"
"time"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/display"
"chromiumos/tast/local/coords"
"chromiumos/tast/local/media/imgcmp"
"chromiumos/tast/local/screenshot"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
func init() {
testing.AddTest(&testing.Test{
Func: ResizeActivity,
Desc: "Checks that resizing ARC applications works without generating black background",
Contacts: []string{"ruanc@chromium.org", "arc-framework+tast@google.com"},
// TODO(http://b/166827552): Test is disabled until it can be fixed
// Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome"},
HardwareDeps: hwdep.D(hwdep.InternalDisplay()),
Fixture: "arcBooted",
Params: []testing.Param{{
ExtraSoftwareDeps: []string{"android_p"},
}, {
Name: "vm",
ExtraSoftwareDeps: []string{"android_vm"},
}},
})
}
func ResizeActivity(ctx context.Context, s *testing.State) {
cr := s.FixtValue().(*arc.PreData).Chrome
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create Test API connection: ", err)
}
dispInfo, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get internal display info: ", err)
}
origShelfBehavior, err := ash.GetShelfBehavior(ctx, tconn, dispInfo.ID)
if err != nil {
s.Fatal("Failed to get shelf behavior: ", err)
}
// Hide shelf. Maximum screen real-estate is needed, especially for devices where its height is as high
// as the default height of freeform applications.
if err := ash.SetShelfBehavior(ctx, tconn, dispInfo.ID, ash.ShelfBehaviorAlwaysAutoHide); err != nil {
s.Fatal("Failed to set shelf behavior to Always Auto Hide: ", err)
}
// Be nice and restore shelf behavior to its original state on exit.
defer ash.SetShelfBehavior(ctx, tconn, dispInfo.ID, origShelfBehavior)
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if tabletModeEnabled {
// Be nice and restore tablet mode to its original state on exit.
defer ash.SetTabletModeEnabled(ctx, tconn, tabletModeEnabled)
if err := ash.SetTabletModeEnabled(ctx, tconn, false); err != nil {
s.Fatal("Failed to set tablet mode disabled: ", err)
}
// TODO(crbug.com/1002958): Wait for "tablet mode animation is finished" in a reliable way.
// If an activity is launched while the tablet mode animation is active, the activity
// will be launched in un undefined state, making the test flaky.
if err := testing.Sleep(ctx, 5*time.Second); err != nil {
s.Fatal("Failed to wait until tablet-mode animation finished: ", err)
}
}
a := s.FixtValue().(*arc.PreData).ARC
act, err := arc.NewActivity(a, "com.android.settings", ".Settings")
if err != nil {
s.Fatal("Failed to create new activity: ", err)
}
defer act.Close()
if err := act.Start(ctx, tconn); err != nil {
s.Fatal("Failed start Settings activity: ", err)
}
// This is an issue to re-enable the tablet mode at the end of the test when
// there is a freeform app still open. See: https://crbug.com/1002666
defer act.Stop(ctx, tconn)
if err := ash.WaitForVisible(ctx, tconn, act.PackageName()); err != nil {
s.Fatal("Failed to wait for Setting activity visible: ", err)
}
if err := act.SetWindowState(ctx, tconn, arc.WindowStateNormal); err != nil {
s.Fatal("Failed to set window state to Normal: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, act.PackageName(), ash.WindowStateNormal); err != nil {
s.Fatal("Failed to wait for window state: ", err)
}
bounds, err := act.WindowBounds(ctx)
if err != nil {
s.Fatal("Failed to get activity bounds: ", err)
}
disp, err := arc.NewDisplay(a, arc.DefaultDisplayID)
if err != nil {
s.Fatal("Failed to obtain a default display: ", err)
}
dispSize, err := disp.Size(ctx)
if err != nil {
s.Fatal("Failed to get display bounds")
}
// Make it as small as possible before the resizing, since maximum screen real-estate is needed for the test.
// And then place it on the left-top corner.
// Resizing from TopLeft corner, since BottomRight corner might trigger the shelf, even if it is hidden.
if err := act.ResizeWindow(ctx, tconn, arc.BorderTopLeft, coords.NewPoint(bounds.Left+bounds.Width, bounds.Top+bounds.Height), 300*time.Millisecond); err != nil {
s.Fatal("Failed to resize window: ", err)
}
// b/150731172: swipe gesture goes out of the screen bounds. Wait until window gets stable after resizing.
// TODO(crbug.com/1062920): Wait for bounds here.
if err := testing.Sleep(ctx, 500*time.Millisecond); err != nil {
s.Fatal("Failed to sleep: ", err)
}
bounds, err = act.WindowBounds(ctx)
if err != nil {
s.Fatal("Failed to get activity bounds after initial resizing: ", err)
}
// When the window move too close to the top-left corner of the display, it will be maximized.
// So set up margin for the top-left cornner to prevent the window switching to maximized state.
const marginForDispTopLeft = 50
destBounds := coords.NewRect(marginForDispTopLeft, marginForDispTopLeft, bounds.Width, bounds.Height)
dispTopLeft := coords.NewPoint(marginForDispTopLeft, marginForDispTopLeft)
// Moving the window slowly (in one second) to prevent triggering any kind of gesture like "snap to border", or "maximize".
if err := act.MoveWindow(ctx, tconn, time.Second, destBounds, bounds); err != nil {
s.Fatal("Failed to move window: ", err)
}
// TODO(crbug.com/1062920): Wait for bounds here.
if err := testing.Sleep(ctx, 500*time.Millisecond); err != nil {
s.Fatal("Failed to sleep: ", err)
}
restoreBounds, err := act.WindowBounds(ctx)
if err != nil {
s.Fatal("Failed to get window bounds: ", err)
}
// Make sure the window is located at the top-left corner.
curTopLeft := coords.NewPoint(restoreBounds.Left, restoreBounds.Top)
// There are some pixels off due to the calculation.
const pixelErrorMargin = 2
if curTopLeft.X < dispTopLeft.X-pixelErrorMargin || curTopLeft.X > dispTopLeft.X+pixelErrorMargin || curTopLeft.Y < dispTopLeft.Y-pixelErrorMargin || curTopLeft.Y > dispTopLeft.Y+pixelErrorMargin {
s.Fatalf("Unexpected window position after moving window: got %+v; want %+v +/- (2, 2)", curTopLeft, dispTopLeft)
}
// Perform 3 different subtests: resize from right border, from bottom border and from bottom-right border.
// If one of these subtests fail, the test fails and the remaining subtests are not executed.
// The bug is not 100% reproducible. It might happen that the test pass even if the bug is not fixed.
// Resize should be as big as possible in order to have higher changes to trigger the bug.
// But we should leave some margin to resize it back to its original size. That means the
// window should not overlap the shelf; and we should leave some extra room to place the touches.
// Leaving room for the touch + extra space to prevent any kind of "resize to fullscreen" gesture.
const marginForTouch = 100
for _, entry := range []struct {
desc string
border arc.BorderType // resize origin (from which border)
dst coords.Point
duration time.Duration
}{
{"right", arc.BorderRight, coords.NewPoint(dispSize.Width-marginForTouch-restoreBounds.Left, restoreBounds.Top+restoreBounds.Height/2), 100 * time.Millisecond},
{"bottom", arc.BorderBottom, coords.NewPoint(restoreBounds.Left+restoreBounds.Width/2, dispSize.Height-marginForTouch-restoreBounds.Top), 300 * time.Millisecond},
{"bottom-right", arc.BorderBottomRight, coords.NewPoint(dispSize.Width-marginForTouch-restoreBounds.Left, dispSize.Height-marginForTouch-restoreBounds.Top), 100 * time.Millisecond},
} {
s.Logf("Resizing window from %s border to %+v", entry.desc, entry.dst)
if err := act.ResizeWindow(ctx, tconn, entry.border, entry.dst, entry.duration); err != nil {
s.Fatal("Failed to resize activity: ", err)
}
// Not waiting on purpose. We have to grab the screenshot as soon as ResizeWindow() returns.
img, err := screenshot.GrabScreenshot(ctx, cr)
if err != nil {
s.Fatal("Failed to grab screenshot: ", err)
}
bounds, err = act.WindowBounds(ctx)
if err != nil {
s.Fatal("Failed to get activity bounds: ", err)
}
subImage := img.(interface {
SubImage(r image.Rectangle) image.Image
}).SubImage(image.Rect(bounds.Left, bounds.Top, bounds.Width, bounds.Height))
blackPixels := imgcmp.CountPixels(subImage, color.RGBA{0, 0, 0, 255})
rect := subImage.Bounds()
totalPixels := (rect.Max.Y - rect.Min.Y) * (rect.Max.X - rect.Min.X)
percent := blackPixels * 100 / totalPixels
s.Logf("Black pixels = %d / %d (%d%%)", blackPixels, totalPixels, percent)
// "3 percent" is arbitrary. It shouldn't have any black pixel. But in case
// the Settings app changes its default theme, we use 3% as a margin.
if percent > 3 {
// Save image with black pixels.
path := filepath.Join(s.OutDir(), "screenshot_fail.png")
if err := imgcmp.DumpImageToPNG(ctx, &subImage, path); err != nil {
s.Fatal("Failed to create screenshot: ", err)
}
s.Logf("Image containing the black pixels: %s", path)
s.Fatalf("Test failed. Contains %d / %d (%d%%) black pixels", blackPixels, totalPixels, percent)
}
// Restore the activity bounds.
if err := act.ResizeWindow(ctx, tconn, entry.border, coords.NewPoint(restoreBounds.Left, restoreBounds.Top), 500*time.Millisecond); err != nil {
s.Fatal("Failed to resize activity: ", err)
}
// TODO(crbug.com/1062920): Wait for bounds here.
if err := testing.Sleep(ctx, 500*time.Millisecond); err != nil {
s.Fatal("Failed to sleep: ", err)
}
}
}