blob: e6f7ca80e3f99cd6e11945d9207dc29b19c0cd39 [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 ui
import (
func init() {
Func: WindowCycle,
Desc: "Checks Alt+Tab and Alt+Shift+Tab functionality for cycling windows",
Contacts: []string{
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome"},
Pre: chrome.LoggedIn(),
// waitForWindowActiveAndFinishAnimating waits for the window specified with title to be active and no longer animating.
// Checking that the window is active seems to be the best indicator that it is focused and on top.
func waitForWindowActiveAndFinishAnimating(ctx context.Context, tconn *chrome.TestConn, windowID int) error {
return ash.WaitForCondition(ctx, tconn, func(w *ash.Window) bool {
return w.IsActive && w.IsAnimating == false && w.ID == windowID
}, &testing.PollOptions{Timeout: 10 * time.Second})
// WindowCycle verifies that we can cycle through open windows using Alt+Tab and Alt+Shift+Tab.
func WindowCycle(ctx context.Context, s *testing.State) {
cr := s.PreValue().(*chrome.Chrome)
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to connect to test API: ", err)
// Launch the apps for the test. Different launch functions are used for Settings and Files,
// since they include additional waiting for UI elements to load. This prevents the window
// order from changing while cycling. If a window has not completely loaded, it may appear on
// top once it finishes loading. If this happens while cycling windows, the test will likely fail.
const numApps = 3
if err := apps.Launch(ctx, tconn, apps.Chrome.ID); err != nil {
s.Fatal("Failed to launch Chrome: ", err)
settings, err := ossettings.Launch(ctx, tconn)
if err != nil {
s.Fatal("Failed to launch Settings: ", err)
if err := settings.WaitForSearchBox()(ctx); err != nil {
s.Fatal("Failed waiting for Settings to load: ", err)
if _, err := filesapp.Launch(ctx, tconn); err != nil {
s.Fatal("Failed to launch Files app: ", err)
// GetAllWindows returns windows in the order of most recent, matching the order they will appear in the cycle window.
// We'll use this slice to find the expected window that should be brought to the top after each Alt+Tab cycle,
// based on the number of times Tab was pressed. After successfully cycling windows, we'll update this to reflect
// the expected order of the windows for the next Alt+Tab cycle.
windows, err := ash.GetAllWindows(ctx, tconn)
if err != nil {
s.Fatal("Failed to get windows: ", err)
if len(windows) != numApps {
s.Fatalf("Unexpected number of windows open; wanted %v, got %v", numApps, len(windows))
// Get the keyboard
keyboard, err := input.Keyboard(ctx)
if err != nil {
s.Fatal("Failed to get keyboard: ", err)
defer keyboard.Close()
// Index of the window we'll cycle to.
var target int
// Params for the window cycle menu.
cycleMenuParams := ui.FindParams{ClassName: "WindowCycleList (Alt+Tab)"}
// Cycle forwards (Alt + Tab) and backwards (Alt + Shift + Tab).
for _, direction := range []string{"forward", "backward"} {
s.Log("Cycle direction: ", direction)
// Press 'tab' 1, 2, 3, and 4 times to verify cycling behavior.
// This verifies we can tab to all open windows, and checks the
// cycling behavior since 4 tab presses will cycle back around.
for i := 0; i < 4; i++ {
func() {
// Make sure the cycle menu isn't open already before we try to alt+tab.
if err := ui.WaitUntilGone(ctx, tconn, cycleMenuParams, 5*time.Second); err != nil {
s.Fatal("Cycle menu unexpectedly open before pressing alt+tab: ", err)
// Open cycle window and get app order.
if err := keyboard.AccelPress(ctx, "Alt"); err != nil {
s.Fatal("Failed to long press Alt: ", err)
defer keyboard.AccelRelease(ctx, "Alt")
if direction == "backward" {
if err := keyboard.AccelPress(ctx, "Shift"); err != nil {
s.Fatal("Failed to long press Shift: ", err)
defer keyboard.AccelRelease(ctx, "Shift")
testing.Sleep(ctx, 500*time.Millisecond)
if err := keyboard.Accel(ctx, "Tab"); err != nil {
s.Fatal("Failed to press Tab: ", err)
// Verify that the cycle window appears in the UI with the right number of windows.
cycleWindow, err := ui.FindWithTimeout(ctx, tconn, cycleMenuParams, 5*time.Second)
if err != nil {
s.Fatal("Failed to get Alt+Tab cycle menu: ", err)
defer cycleWindow.Release(ctx)
// Check that there are 3 windows in the cycle menu.
openApps, err := cycleWindow.Descendants(ctx, ui.FindParams{
Role: ui.RoleTypeWindow,
State: map[ui.StateType]bool{ui.StateTypeFocusable: true},
if err != nil {
s.Fatal("Failed to get open windows in the cycle menu: ", err)
defer openApps.Release(ctx)
if len(openApps) != len(windows) {
s.Fatalf("Wrong number of apps in cycle window. Expected %v, got %v", len(windows), len(openApps))
s.Log("Number of apps in the cycle window matches number of open apps")
// Find the index of the window that is i tab presses away and cycle to it.
if direction == "forward" {
// The second window from the left (i.e. windows[1]) is highlighted
// after the first Tab press, and we advance by i windows, wrapping around to the front.
target = (i + 1) % len(windows)
} else if direction == "backward" {
// The rightmost window (i.e. windows[len(windows)-1]) is highlighted
// after the first Shift+Tab press, and we go back by i windows.
target = (len(windows) - 1) - (i % len(windows))
for j := 0; j < i; j++ {
if err := keyboard.Accel(ctx, "Tab"); err != nil {
s.Fatal("Failed to press Tab: ", err)
if err := waitForWindowActiveAndFinishAnimating(ctx, tconn, windows[target].ID); err != nil {
s.Fatalf("Window (ID: %v) not focused after cycling to it: %v", windows[target].ID, err)
// The expected app order after cycling - the target window is moved to the front,
// while the order of the other windows is preserved.
tmp := []*ash.Window{windows[target]}
windows = append(tmp, append(windows[:target], windows[target+1:]...)...)