blob: ea39845d2f5b3b34754d4a68d2e504118f846fb8 [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"
"encoding/json"
"fmt"
"math"
"strings"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/arc/ui"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/display"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: CompanionLibrary,
Desc: "Test all ARC++ companion library",
Contacts: []string{"sstan@google.com", "arc-framework+tast@google.com"},
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"android_p", "chrome"},
Data: []string{"ArcCompanionLibDemo.apk"},
Pre: arc.Booted(),
Timeout: 5 * time.Minute,
})
}
const pkg = "org.chromium.arc.companionlibdemo"
type companionLibMessage struct {
MessageID int `json:"mid"`
Type string `json:"type"`
API string `json:"api"`
LogMsg *struct {
Msg string `json:"msg"`
} `json:"LogMsg"`
CaptionHeightMsg *struct {
CaptionHeight int `json:"caption_height"`
} `json:"CaptionHeightMsg"`
DeviceModeMsg *struct {
DeviceMode string `json:"device_mode"`
} `json:"DeviceModeMsg"`
WorkspaceInsetMsg *struct {
InsetBound string `json:"inset_bound"`
} `json:"WorkspaceInsetMsg"`
}
func CompanionLibrary(ctx context.Context, s *testing.State) {
const (
apk = "ArcCompanionLibDemo.apk"
mainActivity = ".MainActivity"
)
cr := s.PreValue().(arc.PreData).Chrome
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create Test API connection: ", err)
}
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
// Restore tablet mode to its original state on exit.
defer ash.SetTabletModeEnabled(ctx, tconn, tabletModeEnabled)
// Force Chrome to be in clamshell mode, where windows are resizable.
if err := ash.SetTabletModeEnabled(ctx, tconn, false); err != nil {
s.Fatal("Failed to disable tablet mode: ", err)
}
a := s.PreValue().(arc.PreData).ARC
if err := a.Install(ctx, s.DataPath(apk)); err != nil {
s.Fatal("Failed installing app: ", err)
}
act, err := arc.NewActivity(a, pkg, mainActivity)
if err != nil {
s.Fatal("Failed to create new activity: ", err)
}
defer act.Close()
if err := act.Start(ctx); err != nil {
s.Fatal("Failed start Settings activity: ", err)
}
d, err := ui.NewDevice(ctx, a)
if err != nil {
s.Fatal("Failed to get device: ", err)
}
defer d.Close()
if err := act.WaitForResumed(ctx, time.Second); err != nil {
s.Fatal("Failed to wait for activity to resume: ", err)
}
type testFunc func(context.Context, *chrome.Conn, *arc.Activity, *ui.Device, *testing.State) error
for _, test := range []struct {
name string
fn testFunc
}{
{"Window State", testWindowState},
{"Get Workspace Insets", testWorkspaceInsets},
{"Caption Button", testCaptionButton},
{"Get Device Mode", testDeviceMode},
{"Get Caption Height", testCaptionHeight},
} {
s.Logf("Running %q", test.name)
if err := act.Start(ctx); err != nil {
s.Fatal("Failed to start context: ", err)
}
if err := act.WaitForResumed(ctx, time.Second); err != nil {
s.Fatal("Failed to wait for activity to resuyme: ", err)
}
if err := test.fn(ctx, tconn, act, d, s); err != nil {
s.Errorf("%s test failed: %v", test.name, err)
}
if err := act.Stop(ctx); err != nil {
s.Fatal("Failed to stop context: ", err)
}
}
}
// testCaptionHeight verifies that the caption height length getting from ChromeOS companion library is correct.
func testCaptionHeight(ctx context.Context, tconn *chrome.Conn, act *arc.Activity, d *ui.Device, s *testing.State) error {
const getCaptionHeightButtonID = pkg + ":id/get_caption_height"
dispMode, err := ash.InternalDisplayMode(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to get display mode")
}
// Read JSON format window caption height infomation.
baseMessage, err := getLastJSONMessage(ctx, d)
if err != nil {
return errors.Wrap(err, "failed to get base json message")
}
if err := d.Object(ui.ID(getCaptionHeightButtonID)).Click(ctx); err != nil {
return errors.Wrap(err, "failed to click Get Caption Height button")
}
var msg *companionLibMessage
if err := testing.Poll(ctx, func(ctx context.Context) error {
var err error
msg, err = getLastJSONMessage(ctx, d)
if err != nil {
return testing.PollBreak(err)
}
// Waiting for new message coming
if baseMessage.MessageID == msg.MessageID {
return errors.New("still waiting the new json message")
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return errors.Wrap(err, "failed to get new message of caption height")
}
if msg.CaptionHeightMsg == nil {
return errors.Errorf("unexpected JSON message format: no CaptionHeightMsg; got %v", msg)
}
appWindow, err := getArcAppWindowInfo(ctx, tconn, pkg)
if err != nil {
return errors.Wrap(err, "failed to get arc app window")
}
actualHeight := int(math.Round(float64(appWindow.CaptionHeight) * dispMode.DeviceScaleFactor))
if actualHeight != msg.CaptionHeightMsg.CaptionHeight {
return errors.Errorf("wrong caption height: got %v, want %v", msg.CaptionHeightMsg.CaptionHeight, actualHeight)
}
return nil
}
func testWorkspaceInsets(ctx context.Context, tconn *chrome.Conn, act *arc.Activity, d *ui.Device, s *testing.State) error {
const getWorkspaceInsetsButtonID = pkg + ":id/get_workspace_insets"
parseRectString := func(rectShortString string, mode *display.DisplayMode) (ash.Rect, error) {
// The rectangle short string generated by android /frameworks/base/graphics/java/android/graphics/Rect.java
// Parse it to rectangle format with native pixel size.
var left, top, right, bottom int
if n, err := fmt.Sscanf(rectShortString, "[%d,%d][%d,%d]", &left, &top, &right, &bottom); err != nil {
return ash.Rect{}, errors.Wrap(err, "Error on parse Rect text")
} else if n != 4 {
return ash.Rect{}, errors.Errorf("The format of Rect text is not valid: %q", rectShortString)
}
return ash.Rect{
Left: left,
Top: top,
Width: mode.WidthInNativePixels - left - right,
Height: mode.HeightInNativePixels - top - bottom,
}, nil
}
dispMode, err := ash.InternalDisplayMode(ctx, tconn)
if err != nil {
s.Fatal("Failed to get display mode: ", err)
}
dispInfo, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get internal display info: ", err)
}
for _, test := range []struct {
shelfAlignment ash.ShelfAlignment
shelfBehavior ash.ShelfBehavior
}{
{ash.ShelfAlignmentLeft, ash.ShelfBehaviorAlwaysAutoHide},
{ash.ShelfAlignmentLeft, ash.ShelfBehaviorNeverAutoHide},
{ash.ShelfAlignmentRight, ash.ShelfBehaviorAlwaysAutoHide},
{ash.ShelfAlignmentRight, ash.ShelfBehaviorNeverAutoHide},
{ash.ShelfAlignmentBottom, ash.ShelfBehaviorAlwaysAutoHide},
{ash.ShelfAlignmentBottom, ash.ShelfBehaviorNeverAutoHide},
} {
if err := ash.SetShelfBehavior(ctx, tconn, dispInfo.ID, test.shelfBehavior); err != nil {
s.Fatalf("Failed to set shelf behavior to %v: %v", test.shelfBehavior, err)
}
if err := ash.SetShelfAlignment(ctx, tconn, dispInfo.ID, test.shelfAlignment); err != nil {
s.Fatalf("Failed to set shelf alignment to %v: %v", test.shelfAlignment, err)
}
var expectedShelfRect arc.Rect
if err := testing.Poll(ctx, func(ctx context.Context) error {
// Confirm the shelf attribute has changed.
if actualShelfAlignment, err := ash.GetShelfAlignment(ctx, tconn, dispInfo.ID); err != nil {
return errors.Wrap(err, "failed to get shelf alignment")
} else if actualShelfAlignment != test.shelfAlignment {
return errors.Errorf("shelf alignment has not changed yet: got %v, want %v", actualShelfAlignment, test.shelfAlignment)
}
dispInfo, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get internal display info: ", err)
}
// The unit of WorkArea is DP.
expectedShelfRect = arc.Rect{
Left: dispInfo.WorkArea.Left,
Top: dispInfo.WorkArea.Top,
Width: dispInfo.WorkArea.Width,
Height: dispInfo.WorkArea.Height,
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
s.Fatal("Could not change the system shelf alignment: ", err)
}
// Read JSON format window insets size from CompanionLib Demo.
baseMessage, err := getLastJSONMessage(ctx, d)
if err != nil {
return errors.Wrap(err, "failed to get basement json message")
}
if err := d.Object(ui.ID(getWorkspaceInsetsButtonID)).Click(ctx); err != nil {
s.Fatal("Failed to click Get Workspace Insets button: ", err)
}
var msg *companionLibMessage
if err := testing.Poll(ctx, func(ctx context.Context) error {
var err error
msg, err = getLastJSONMessage(ctx, d)
if err != nil {
return testing.PollBreak(err)
}
// Waiting for new message coming
if baseMessage.MessageID == msg.MessageID {
return errors.New("still waiting the new json message")
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return errors.Wrap(err, "failed to get new message of device mode")
}
if msg.WorkspaceInsetMsg == nil {
return errors.Errorf("unexpected JSON message format: no WorkspaceInsetMsg; got %v", msg)
}
parsedShelfRect, err := parseRectString(msg.WorkspaceInsetMsg.InsetBound, dispMode)
if err != nil {
s.Fatal("Failed to parse message: ", err)
}
// Convert two rectangle to same unit.
expectedShelfRectPX := ash.ConvertBoundsFromDpToPx(ash.Rect(expectedShelfRect), dispMode.DeviceScaleFactor)
if expectedShelfRectPX != parsedShelfRect {
s.Fatalf("Workspace Inset is not expected: got %v, want %v", parsedShelfRect, expectedShelfRectPX)
}
}
return nil
}
func testCaptionButton(ctx context.Context, tconn *chrome.Conn, act *arc.Activity, d *ui.Device, s *testing.State) error {
const (
setCaptionButtonID = pkg + ":id/set_caption_buttons_visibility"
checkCaptionButtonMinimizeBox = pkg + ":id/caption_button_minimize"
checkCaptionButtonMaximizeAndRestoreBox = pkg + ":id/caption_button_maximize_and_restore"
checkCaptionButtonLegacyMenuBox = pkg + ":id/caption_button_legacy_menu"
checkCaptionButtonGoBackBox = pkg + ":id/caption_button_go_back"
checkCaptionButtonCloseBox = pkg + ":id/caption_button_close"
)
resetCaptionCheckboxes := func() error {
for _, checkboxID := range []string{
checkCaptionButtonMinimizeBox,
checkCaptionButtonMaximizeAndRestoreBox,
checkCaptionButtonLegacyMenuBox,
checkCaptionButtonGoBackBox,
checkCaptionButtonCloseBox,
} {
checked, err := d.Object(ui.ID(checkboxID)).IsChecked(ctx)
if err != nil {
return errors.Wrap(err, "could not get the checkbox statement")
}
if checked != false {
s.Logf("Clean %s checkbox statements", checkboxID)
if err := d.Object(ui.ID(checkboxID)).Click(ctx); err != nil {
return err
}
}
}
return nil
}
for _, test := range []struct {
buttonCheckboxID string
buttonVisibleStatusMask ash.CaptionButtonStatus
}{
{checkCaptionButtonMinimizeBox, ash.CaptionButtonMinimize},
{checkCaptionButtonMaximizeAndRestoreBox, ash.CaptionButtonMaximizeAndRestore},
{checkCaptionButtonLegacyMenuBox, ash.CaptionButtonMenu},
{checkCaptionButtonGoBackBox, ash.CaptionButtonBack},
{checkCaptionButtonCloseBox, ash.CaptionButtonClose},
} {
s.Logf("Test hiding %v caption button", test.buttonCheckboxID)
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := d.Object(ui.ID(setCaptionButtonID)).Click(ctx); err != nil {
return errors.Wrap(err, "could not click the setCaptionButton")
}
if err := resetCaptionCheckboxes(); err != nil {
return errors.Wrap(err, "could not clean the button checkboxes setting")
}
if err := d.Object(ui.ID(test.buttonCheckboxID)).Click(ctx); err != nil {
return errors.Wrap(err, "could not check the checkbox")
}
if err := d.Object(ui.Text("OK")).Click(ctx); err != nil {
return errors.Wrap(err, "could not click the OK button")
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return errors.New("Error while changing hidden caption button")
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
window, err := getArcAppWindowInfo(ctx, tconn, pkg)
if err != nil {
return errors.Wrap(err, "error while get ARC window")
}
if window.CaptionButtonVisibleStatus&int(test.buttonVisibleStatusMask) != 0 {
return errors.Errorf("Caption Button %v still visible", test.buttonCheckboxID)
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return errors.Wrap(err, "failed to waiting caption button changed")
}
}
return nil
}
func testDeviceMode(ctx context.Context, tconn *chrome.Conn, act *arc.Activity, d *ui.Device, s *testing.State) error {
const getDeviceModeButtonID = pkg + ":id/get_device_mode_button"
for _, test := range []struct {
// isTabletMode represents current mode of system which is Tablet mode or clamshell mode.
isTabletMode bool
// modeStatus represents the expection of device mode string getting from companion library.
modeStatus string
}{
{isTabletMode: true, modeStatus: "TABLET"},
{isTabletMode: false, modeStatus: "CLAMSHELL"},
} {
// Force Chrome to be in specific system mode.
if err := ash.SetTabletModeEnabled(ctx, tconn, test.isTabletMode); err != nil {
s.Fatal("Failed to set the system mode: ", err)
}
// Read JSON format window caption height infomation.
baseMessage, err := getLastJSONMessage(ctx, d)
if err != nil {
return errors.Wrap(err, "failed to get basement json message")
}
if err := d.Object(ui.ID(getDeviceModeButtonID)).Click(ctx); err != nil {
s.Fatal("Could not click the getDeviceMode button: ", err)
}
var msg *companionLibMessage
if err := testing.Poll(ctx, func(ctx context.Context) error {
var err error
msg, err = getLastJSONMessage(ctx, d)
if err != nil {
return testing.PollBreak(err)
}
// Waiting for new message coming
if baseMessage.MessageID == msg.MessageID {
return errors.New("still waiting the new json message")
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return errors.Wrap(err, "failed to get new message of device mode")
}
if msg.DeviceModeMsg == nil {
return errors.Errorf("unexpected JSON message format: no DeviceModeMsg; got %v", msg)
}
if msg.DeviceModeMsg.DeviceMode != test.modeStatus {
return errors.Errorf("unexpected getDeviceMode result: got %s; want %s", msg.DeviceModeMsg.DeviceMode, test.modeStatus)
}
}
return nil
}
func testWindowState(ctx context.Context, tconn *chrome.Conn, act *arc.Activity, d *ui.Device, s *testing.State) error {
const (
setWindowStateButtonID = pkg + ":id/set_task_window_state_button"
getWindowStateButtonID = pkg + ":id/get_task_window_state_button"
)
// TODO(sstan): Add testcase of "Always on top" setting
for _, test := range []struct {
windowStateStr string
windowStateExp arc.WindowState
isAppManaged bool
}{
{windowStateStr: "Minimize", windowStateExp: arc.WindowStateMinimized, isAppManaged: false},
{windowStateStr: "Maximize", windowStateExp: arc.WindowStateMaximized, isAppManaged: false},
{windowStateStr: "Normal", windowStateExp: arc.WindowStateNormal, isAppManaged: false},
} {
s.Logf("Testing windowState=%v, appManaged=%t", test.windowStateStr, test.isAppManaged)
if err := act.Start(ctx); err != nil {
s.Fatal("Failed to start context: ", err)
}
if err := act.WaitForResumed(ctx, time.Second); err != nil {
s.Fatal("Failed to wait for Resumed: ", err)
}
if err := d.Object(ui.ID(setWindowStateButtonID)).Click(ctx); err != nil {
s.Fatal("Failed to click Set Task Window State button: ", err)
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
if isClickable, err := d.Object(ui.Text(test.windowStateStr)).IsClickable(ctx); err != nil {
return errors.Wrap(err, "failed check the radio clickable")
} else if isClickable {
// If isClickable = false, it will do nothing because the test application logic will automatically check the current window state radio. It can't be clicked if the state radio has been clicked.
if err := d.Object(ui.Text(test.windowStateStr)).Click(ctx); err != nil {
s.Fatalf("Failed to click %v radio: %v", test.windowStateStr, err)
}
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
s.Fatal("Failed to waiting click radio: ", err)
}
if err := d.Object(ui.Text("OK")).Click(ctx); err != nil {
s.Fatal("Failed to click OK button: ", err)
}
err := testing.Poll(ctx, func(ctx context.Context) error {
actualWindowState, err := act.GetWindowState(ctx)
if err != nil {
return errors.Wrap(err, "could not get window state")
}
if actualWindowState != test.windowStateExp {
return errors.Errorf("unexpected window state: got %v; want %v", actualWindowState, test.windowStateExp)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
if err != nil {
s.Fatal("Error while waiting window state setting up: ", err)
}
if err := act.Stop(ctx); err != nil {
s.Fatal("Failed to stop context: ", err)
}
}
return nil
}
// getArcAppWindowInfo returns corresponding arc window infomation.
func getArcAppWindowInfo(ctx context.Context, tconn *chrome.Conn, pkgName string) (*ash.Window, error) {
var appWindow *ash.Window
if windows, err := ash.GetAllWindows(ctx, tconn); err != nil {
return nil, errors.Wrap(err, "get all windows info")
} else if len(windows) > 0 {
for _, w := range windows {
if w.WindowType == ash.WindowTypeArc && w.ARCPackageName == pkgName {
appWindow = w
}
}
}
if appWindow == nil {
return nil, errors.New("can not find corresponding ARC window")
}
return appWindow, nil
}
// getTextViewContent returns all text in status textview.
func getTextViewContent(ctx context.Context, d *ui.Device) ([]string, error) {
const statusTextViewID = pkg + ":id/status_text_view"
text, err := d.Object(ui.ID(statusTextViewID)).GetText(ctx)
if err != nil {
// It not always success when get object, poll is necessary.
return nil, errors.Wrap(err, "StatusTextView not ready yet")
}
return strings.Split(text, "\n"), nil
}
// getJSONTextViewContent returns all text in JSON textview.
func getJSONTextViewContent(ctx context.Context, d *ui.Device) ([]string, error) {
const JSONTextViewID = pkg + ":id/status_jsontext_view"
text, err := d.Object(ui.ID(JSONTextViewID)).GetText(ctx)
if err != nil {
return nil, errors.Wrap(err, "JSONStatusTextView not ready yet")
}
return strings.Split(text, "\n"), nil
}
// getLastJSONMessage return last JSON format output message of ChromeOS Companion Library Demo
func getLastJSONMessage(ctx context.Context, d *ui.Device) (*companionLibMessage, error) {
var lines []string
if err := testing.Poll(ctx, func(ctx context.Context) error {
var err error
lines, err = getJSONTextViewContent(ctx, d)
// Using poll here to avoid get text failure because UI compontent isn't stable.
if err != nil {
return errors.Wrap(err, "failed to get JSON message text")
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return nil, errors.Wrap(err, "failed to get a new line in status text view")
}
var msg companionLibMessage
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &msg); err != nil {
return nil, errors.Wrap(err, "parse JSON format message failure")
}
return &msg, nil
}