blob: 22a0ccbf9a627aacefe74d47e82c267445e14087 [file] [log] [blame]
// Copyright 2021 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 firmware
import (
"context"
"regexp"
"time"
"github.com/golang/protobuf/ptypes/empty"
"chromiumos/tast/common/servo"
"chromiumos/tast/errors"
"chromiumos/tast/remote/firmware"
"chromiumos/tast/remote/firmware/fixture"
fwpb "chromiumos/tast/services/cros/firmware"
pb "chromiumos/tast/services/cros/ui"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
type testParamsTablet struct {
canDoTabletSwitch bool
formFactor string
tabletModeOn string
tabletModeOff string
}
func init() {
testing.AddTest(&testing.Test{
Func: ECVerifyVK,
LacrosStatus: testing.LacrosVariantNeeded,
Desc: "Verify whether virtual keyboard window is present during change in tablet mode",
Contacts: []string{"cienet-firmware@cienet.corp-partner.google.com", "chromeos-firmware@google.com"},
Attr: []string{"group:firmware", "firmware_unstable"},
SoftwareDeps: []string{"chrome"},
ServiceDeps: []string{"tast.cros.ui.CheckVirtualKeyboardService", "tast.cros.firmware.UtilsService", "tast.cros.ui.ChromeUIService"},
Fixture: fixture.NormalMode,
HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.TouchScreen()),
Params: []testing.Param{{
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Convertible)),
Val: &testParamsTablet{
canDoTabletSwitch: true,
formFactor: "convertible",
tabletModeOn: "tabletmode on",
tabletModeOff: "tabletmode off",
},
}, {
Name: "detachable",
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Detachable)),
Val: &testParamsTablet{
canDoTabletSwitch: true,
formFactor: "detachable",
tabletModeOn: "basestate detach",
tabletModeOff: "basestate attach",
},
}, {
Name: "chromeslate",
ExtraHardwareDeps: hwdep.D(hwdep.FormFactor(hwdep.Chromeslate)),
Val: &testParamsTablet{
canDoTabletSwitch: false,
formFactor: "chromeslate",
},
}},
})
}
func ECVerifyVK(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to init servo: ", err)
}
if err := h.RequireConfig(ctx); err != nil {
s.Fatal("Failed to get config: ", err)
}
// Perform a hard reset on DUT to ensure removal of any
// old settings that might potentially have an impact on
// this test.
if err := h.Servo.SetPowerState(ctx, servo.PowerStateReset); err != nil {
s.Fatal("Failed to cold reset DUT at the beginning of test: ", err)
}
// Wait for DUT to reconnect.
waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, 2*time.Minute)
defer cancelWaitConnect()
if err := s.DUT().WaitConnect(waitConnectCtx); err != nil {
s.Fatal("Failed to reconnect to DUT: ", err)
}
if err := h.RequireRPCClient(ctx); err != nil {
s.Fatal("Failed to connect to the RPC service on the DUT: ", err)
}
cvkc := pb.NewCheckVirtualKeyboardServiceClient(h.RPCClient.Conn)
s.Log("Starting a new Chrome session and logging in as test user")
if _, err := cvkc.NewChromeLoggedIn(ctx, &empty.Empty{}); err != nil {
s.Fatal("Failed to login: ", err)
}
defer cvkc.CloseChrome(ctx, &empty.Empty{})
s.Log("Opening a Chrome page")
if _, err := cvkc.OpenChromePage(ctx, &empty.Empty{}); err != nil {
s.Fatal("Failed to open chrome: ", err)
}
args := s.Param().(*testParamsTablet)
// Chromeslates are already in tablet mode, and for this reason,
// we could skip switching to tablet mode, and just verify that
// virtual keyboard is present after a click on the address bar.
if args.canDoTabletSwitch == false {
if err := verifyVKIsPresent(ctx, h, cvkc, s, true, "", args.formFactor); err != nil {
s.Fatal("Failed to verify virtual keyboard status: ", err)
}
} else {
for _, dut := range []struct {
tabletMode bool
tabletState string
}{
{true, args.tabletModeOn},
{false, args.tabletModeOff},
} {
s.Logf("Run test with tablet mode on: %t", dut.tabletMode)
if err := verifyVKIsPresent(ctx, h, cvkc, s, dut.tabletMode, dut.tabletState, args.formFactor); err != nil {
s.Fatal("Failed to verify virtual keyboard status: ", err)
}
}
}
}
func checkAndSetTabletMode(ctx context.Context, h *firmware.Helper, s *testing.State, action string) error {
// regular expressions.
var (
tabletmodeNotFound = `Command 'tabletmode' not found or ambiguous`
tabletmodeStatus = `\[\S+ tablet mode (enabled|disabled)\]`
basestateNotFound = `Command 'basestate' not found or ambiguous`
basestateStatus = `\[\S+ base state: (attached|detached)\]`
bdStatus = `\[\S+ BD forced (connected|disconnected)\]`
checkTabletMode = `(` + tabletmodeNotFound + `|` + tabletmodeStatus + `|` + basestateNotFound + `|` + basestateStatus + `|` + bdStatus + `)`
)
// Run EC command to turn on/off tablet mode.
s.Logf("Check command %q exists", action)
out, err := h.Servo.RunECCommandGetOutput(ctx, action, []string{checkTabletMode})
if err != nil {
return errors.Wrapf(err, "failed to run command %q", action)
}
tabletModeUnavailable := []*regexp.Regexp{regexp.MustCompile(tabletmodeNotFound), regexp.MustCompile(basestateNotFound)}
for _, v := range tabletModeUnavailable {
if match := v.FindStringSubmatch(out[0][0]); match != nil {
return errors.Errorf("device does not support tablet mode: %q", match)
}
}
s.Logf("Current tabletmode status: %q", out[0][1])
return nil
}
func verifyVKIsPresent(ctx context.Context, h *firmware.Helper, cvkc pb.CheckVirtualKeyboardServiceClient, s *testing.State, tabletMode bool, command, dutFormFactor string) error {
// Run EC command to put DUT in clamshell/tablet mode.
if command != "" {
if err := checkAndSetTabletMode(ctx, h, s, command); err != nil {
if dutFormFactor == "convertible" {
testing.ContextLogf(ctx, "Failed to set DUT tablet mode state, and got: %v. Attempting to set tablet_mode_angle with ectool instead", err)
cmd := firmware.NewECTool(s.DUT(), firmware.ECToolNameMain)
// Save initial tablet mode angle settings to restore at the end of verifyVKIsPresent.
tabletModeAngleInit, hysInit, err := cmd.SaveTabletModeAngles(ctx)
if err != nil {
return errors.Wrap(err, "failed to save initial tablet mode angles")
}
defer func() error {
testing.ContextLogf(ctx, "Restoring DUT's tablet mode angles to the original settings: lid_angle=%s, hys=%s", tabletModeAngleInit, hysInit)
if err := cmd.ForceTabletModeAngle(ctx, tabletModeAngleInit, hysInit); err != nil {
return errors.Wrap(err, "failed to restore tablet mode angle to the initial angles")
}
return nil
}()
if tabletMode {
// Setting tabletModeAngle to 0s will force DUT into tablet mode.
if err := cmd.ForceTabletModeAngle(ctx, "0", "0"); err != nil {
return errors.Wrap(err, "failed to force DUT into tablet mode")
}
} else {
// Setting tabletModeAngle to 360 will force DUT into clamshell mode.
if err := cmd.ForceTabletModeAngle(ctx, "360", "0"); err != nil {
return errors.Wrap(err, "failed to force DUT into clamshell mode")
}
}
} else {
return errors.Wrap(err, "failed to set DUT tablet mode state")
}
}
}
// Wait for the command on switching to tablet mode to fully propagate,
// before clicking on the address bar.
if err := testing.Sleep(ctx, time.Second); err != nil {
return errors.Wrap(err, "failed in sleeping for one second before clicking on the address bar")
}
// Create a Chrome instance for the utilsService by reusing one that's
// already been created above under cvkc. The utilsService is required
// by EvalTabletMode in checking tablet mode status. Close this chrome
// at the end of verifyVKIsPresent.
utilsService := fwpb.NewUtilsServiceClient(h.RPCClient.Conn)
if _, err := utilsService.ReuseChrome(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to reuse Chrome session for the utils service")
}
defer utilsService.CloseChrome(ctx, &empty.Empty{})
// Log tablet mode status from the ChromeOS perspective.
res, err := utilsService.EvalTabletMode(ctx, &empty.Empty{})
if err != nil {
return errors.Wrap(err, "unable to evaluate whether ChromeOS is in tablet mode")
}
testing.ContextLogf(ctx, "ChromeOS in tabletmode: %t", res.TabletModeEnabled)
// Use polling here to wait till the UI tree has fully updated,
// and check if virtual keyboard is present. Start by sending
// a tap on the touch screen, and if this fails in triggering the
// vk, retry a few more times with a left-click.
testing.ContextLogf(ctx, "Expecting virtual keyboard present: %t", tabletMode)
if err := triggerAndCheckVK(ctx, h, cvkc, tabletMode, "byTouchScreen"); err != nil {
testing.ContextLogf(ctx, "Checking virtual keyboard in tablet mode failed: %v. Retry a few more times with a left click", err)
testing.ContextLog(ctx, "Restarting UI and logging in again")
if err := refreshChromeAndLogin(ctx, h, cvkc); err != nil {
return err
}
testing.ContextLog(ctx, "Opening a new Chrome page")
if _, err := cvkc.OpenChromePage(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to open a new chrome page")
}
// Left click on the new chrome page till vk appears.
if err := triggerAndCheckVK(ctx, h, cvkc, tabletMode, "byLeftClick"); err != nil {
return err
}
return nil
}
return nil
}
func triggerAndCheckVK(ctx context.Context, h *firmware.Helper, cvkc pb.CheckVirtualKeyboardServiceClient, tabletMode bool, option string) error {
req := pb.CheckVirtualKeyboardRequest{
IsDutTabletMode: tabletMode,
}
return testing.Poll(ctx, func(ctx context.Context) error {
switch option {
case "byTouchScreen":
testing.ContextLog(ctx, "Sending a touch on the address bar of a Chrome page")
if _, err := cvkc.TouchChromeAddressBar(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to touch on chrome address bar")
}
case "byLeftClick":
testing.ContextLog(ctx, "Clicking on the address bar of a Chrome page")
if _, err := cvkc.ClickChromeAddressBar(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to click on chrome address bar")
}
}
if err := testing.Sleep(ctx, 1*time.Second); err != nil {
return errors.Wrap(err, "failed to sleep after clicking on the Chrome address bar")
}
res, err := cvkc.CheckVirtualKeyboardIsPresent(ctx, &req)
if err != nil {
return errors.Wrap(err, "failed to check whether virtual keyboard is present")
} else if tabletMode != res.IsVirtualKeyboardPresent {
return errors.Errorf(
"found unexpected behavior, and got tabletmode: %t, VirtualKeyboardPresent: %t",
tabletMode, res.IsVirtualKeyboardPresent)
}
return nil
}, &testing.PollOptions{Timeout: 1 * time.Minute, Interval: 3 * time.Second})
}
func refreshChromeAndLogin(ctx context.Context, h *firmware.Helper, cvkc pb.CheckVirtualKeyboardServiceClient) error {
// Before restarting UI, close the chrome instance that was previously initiated.
if _, err := cvkc.CloseChrome(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to close chrome")
}
// Restart UI, which would create a new Chrome session at login.
serviceClient := pb.NewChromeUIServiceClient(h.RPCClient.Conn)
if _, err := serviceClient.EnsureLoginScreen(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to restart ui")
}
// Delay for a few seconds to ensure that a new Chrome session has fully settled down.
if err := testing.Sleep(ctx, 3*time.Second); err != nil {
return errors.Wrap(err, "failed to sleep")
}
// Log in again.
if _, err := cvkc.NewChromeLoggedIn(ctx, &empty.Empty{}); err != nil {
return errors.Wrap(err, "failed to log in")
}
return nil
}