| // 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 ( |
| "context" |
| "strings" |
| "time" |
| |
| "chromiumos/tast/common/action" |
| "chromiumos/tast/common/android/ui" |
| "chromiumos/tast/common/perf" |
| "chromiumos/tast/ctxutil" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/arc/playstore" |
| "chromiumos/tast/local/bundles/cros/ui/cuj" |
| "chromiumos/tast/local/chrome" |
| "chromiumos/tast/local/chrome/ash" |
| "chromiumos/tast/local/chrome/browser" |
| "chromiumos/tast/local/chrome/lacros" |
| "chromiumos/tast/local/chrome/uiauto" |
| "chromiumos/tast/local/chrome/uiauto/nodewith" |
| "chromiumos/tast/local/chrome/uiauto/pointer" |
| "chromiumos/tast/local/chrome/uiauto/touch" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/testing" |
| "chromiumos/tast/testing/hwdep" |
| ) |
| |
| type taskSwitchCUJTestParam struct { |
| tablet bool |
| useLacros bool |
| tracing bool // If true, collect and store trace data during recording. |
| validation bool // If true, add extra cpu loads before recording. |
| } |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: TaskSwitchCUJ, |
| LacrosStatus: testing.LacrosVariantExists, |
| Desc: "Measures the performance of tab-switching CUJ", |
| Contacts: []string{"yichenz@chromium.org", "chromeos-perfmetrics-eng@google.com"}, |
| Attr: []string{"group:crosbolt", "crosbolt_perbuild", "group:cuj"}, |
| SoftwareDeps: []string{"chrome"}, |
| Timeout: 8 * time.Minute, |
| Vars: []string{"mute"}, |
| Params: []testing.Param{ |
| { |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{}, |
| }, |
| { |
| Name: "validation", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{validation: true}, |
| }, |
| { |
| Name: "trace", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{tracing: true}, |
| }, |
| { |
| Name: "vm", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_vm"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{}, |
| }, |
| { |
| Name: "tablet_mode", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{ |
| tablet: true, |
| }, |
| }, |
| { |
| Name: "tablet_mode_vm", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_vm"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{ |
| tablet: true, |
| }, |
| }, |
| { |
| Name: "lacros_clamshell", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p", "lacros"}, |
| Fixture: "loggedInToCUJUserLacrosWithARC", |
| Val: taskSwitchCUJTestParam{ |
| useLacros: true, |
| }, |
| }, |
| { |
| Name: "lacros_clamshell_vm", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_vm", "lacros"}, |
| Fixture: "loggedInToCUJUserLacrosWithARC", |
| Val: taskSwitchCUJTestParam{ |
| useLacros: true, |
| }, |
| }, |
| { |
| Name: "lacros_tablet", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_p", "lacros"}, |
| Fixture: "loggedInToCUJUserLacrosWithARC", |
| Val: taskSwitchCUJTestParam{ |
| useLacros: true, |
| tablet: true, |
| }, |
| }, |
| { |
| Name: "lacros_tablet_vm", |
| ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()), |
| ExtraSoftwareDeps: []string{"android_vm", "lacros"}, |
| Fixture: "loggedInToCUJUserLacrosWithARC", |
| Val: taskSwitchCUJTestParam{ |
| useLacros: true, |
| tablet: true, |
| }, |
| }, |
| { |
| // Pilot test on "noibat" that has HDMI dongle installed. |
| Name: "noibat", |
| ExtraHardwareDeps: hwdep.D(hwdep.Model("noibat")), |
| ExtraSoftwareDeps: []string{"android_vm"}, |
| Fixture: "loggedInToCUJUser", |
| Val: taskSwitchCUJTestParam{}, |
| }, |
| }, |
| }) |
| } |
| |
| func TaskSwitchCUJ(ctx context.Context, s *testing.State) { |
| const ( |
| playStorePackageName = "com.android.vending" |
| gmailPackageName = "com.google.android.gm" |
| timeout = 10 * time.Second |
| ) |
| |
| // Shorten context a bit to allow for cleanup. |
| closeCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, 2*time.Second) |
| defer cancel() |
| |
| testParam := s.Param().(taskSwitchCUJTestParam) |
| |
| cr := s.FixtValue().(chrome.HasChrome).Chrome() |
| a := s.FixtValue().(cuj.FixtureData).ARC |
| |
| tconn, err := cr.TestAPIConn(ctx) |
| if err != nil { |
| s.Fatal("Failed to connect to the test API connection: ", err) |
| } |
| |
| var cs ash.ConnSource |
| if testParam.useLacros { |
| // Launch lacros. |
| l, err := lacros.Launch(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to launch lacros: ", err) |
| } |
| defer l.Close(ctx) |
| cs = l |
| } else { |
| cs = cr |
| } |
| |
| kw, err := input.Keyboard(ctx) |
| if err != nil { |
| s.Fatal("Failed to open the keyboard: ", err) |
| } |
| defer kw.Close() |
| |
| tabletMode := testParam.tablet |
| cleanup, err := ash.EnsureTabletModeEnabled(ctx, tconn, tabletMode) |
| if err != nil { |
| s.Fatal("Failed to ensure the tablet mode state: ", err) |
| } |
| defer cleanup(ctx) |
| |
| d, err := a.NewUIDevice(ctx) |
| if err != nil { |
| s.Fatal("Failed to setup ARC and Play Store: ", err) |
| } |
| defer d.Close(ctx) |
| |
| // Install android apps for the everyday works: Gmail. |
| // Google Calendar and youtube are not installed to reduce the flakiness |
| // around the app installation. |
| pkgs, err := a.InstalledPackages(ctx) |
| if err != nil { |
| s.Fatal("Failed to list the installed packages: ", err) |
| } |
| for _, pkgName := range []string{gmailPackageName} { |
| if _, ok := pkgs[pkgName]; ok { |
| s.Logf("%s is already installed", pkgName) |
| continue |
| } |
| s.Log("Installing ", pkgName) |
| installCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) |
| if err = playstore.InstallApp(installCtx, a, d, pkgName, &playstore.Options{TryLimit: -1}); err != nil { |
| cancel() |
| s.Fatalf("Failed to install %s: %v", pkgName, err) |
| } |
| cancel() |
| } |
| |
| ac := uiauto.New(tconn) |
| var pc pointer.Context |
| var setOverviewModeAndWait func(ctx context.Context) error |
| var openAppList func(ctx context.Context) error |
| type subtest struct { |
| name string |
| desc string |
| f func(ctx context.Context, s *testing.State, i int) error |
| } |
| browserWindows := map[int]bool{} |
| var ws []*ash.Window |
| var subtest2 subtest |
| if tabletMode { |
| pc, err = pointer.NewTouch(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to create a touch controller") |
| } |
| tsew, tcc, err := touch.NewTouchscreenAndConverter(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to access to the touchscreen: ", err) |
| } |
| defer tsew.Close() |
| stw, err := tsew.NewSingleTouchWriter() |
| if err != nil { |
| s.Fatal("Failed to create the single touch writer: ", err) |
| } |
| setOverviewModeAndWait = func(ctx context.Context) error { |
| return ash.DragToShowOverview(ctx, tsew, stw, tconn) |
| } |
| openAppList = func(ctx context.Context) error { |
| return ash.DragToShowHomescreen(ctx, tsew.Width(), tsew.Height(), stw, tconn) |
| } |
| subtest2 = subtest{ |
| "hotseat", |
| "Switching the focused window through clicking the hotseat", |
| func(ctx context.Context, s *testing.State, i int) error { |
| // In this subtest, update the active window through hotseat. First, |
| // swipe-up quickly to reveal the hotseat, and then tap the app icon |
| // for the next active window. In case there are multiple windows in |
| // an app, it will show up a pop-up, so tap on the menu item. |
| if err := ash.SwipeUpHotseatAndWaitForCompletion(ctx, tconn, stw, tcc); err != nil { |
| return errors.Wrap(err, "failed to show the hotseat") |
| } |
| |
| // Get the bounds of the shelf icons. The shelf icon bounds are |
| // available from ScrollableShelfInfo, while the metadata for ShelfItems |
| // are in another place (ShelfItem). Use ShelfItem to filter out |
| // the apps with no windows, and fetch its icon bounds from |
| // ScrollableShelfInfo. |
| items, err := ash.ShelfItems(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to obtain the shelf items") |
| } |
| shelfInfo, err := ash.FetchScrollableShelfInfoForState(ctx, tconn, &ash.ShelfState{}) |
| if err != nil { |
| return errors.Wrap(err, "failed to obtain the shelf UI info") |
| } |
| if len(items) != len(shelfInfo.IconsBoundsInScreen) { |
| return errors.Errorf("mismatch count: %d vs %d", len(items), len(shelfInfo.IconsBoundsInScreen)) |
| } |
| |
| iconBounds := make([]coords.Rect, 0, len(items)) |
| hasYoutubeIcon := false |
| for i, item := range items { |
| if item.Status == ash.ShelfItemClosed { |
| continue |
| } |
| if strings.HasPrefix(strings.ToLower(item.Title), "youtube") { |
| hasYoutubeIcon = true |
| } |
| iconBounds = append(iconBounds, *shelfInfo.IconsBoundsInScreen[i]) |
| } |
| |
| // browserPopupItemsCount is the number of browser windows to be chosen |
| // from the popup menu shown by tapping the browser icon. Basically all |
| // of the browser windows should be there, but when youtube icon |
| // appears, youtube should be chosen from its own icon, so the number |
| // should be decremented. |
| browserPopupItemsCount := len(browserWindows) |
| if hasYoutubeIcon { |
| browserPopupItemsCount-- |
| } |
| |
| // Find the correct-icon for i-th run. Assumptions: |
| // - each app icon has 1 window, except for the browser icon (there are len(browserWindows)) |
| // - browser icon is the leftmost (iconIdx == 0) |
| // With these assumptions, it selects the icons from the right, and |
| // when it reaches to browser icons, it selects a window from the popup |
| // menu from the top. In other words, there would be icons of |
| // [browser] [play store] [gmail] ... |
| // and selecting [gmail] -> [play store] -> [browser] |
| // and selecting browser icon shows a popup. |
| iconIdx := len(ws) - (browserPopupItemsCount - 1) - i - 1 |
| var isPopup bool |
| var popupIdx int |
| if iconIdx <= 0 { |
| isPopup = true |
| // This assumes the order of menu items of window seleciton popup is |
| // stable. Selecting from the top, but offset-by-one since the first |
| // menu item is just a title, not clickable. |
| popupIdx = -iconIdx |
| iconIdx = 0 |
| } |
| if err := pc.ClickAt(iconBounds[iconIdx].CenterPoint())(ctx); err != nil { |
| return errors.Wrapf(err, "failed to click icon at %d", iconIdx) |
| } |
| if isPopup { |
| menus := nodewith.ClassName("MenuItemView") |
| if err := ac.WaitUntilExists(menus.First())(ctx); err != nil { |
| return errors.Wrap(err, "failed to wait for the menu to appear") |
| } |
| menuInfo, err := ac.NodesInfo(ctx, menus) |
| if err != nil { |
| return errors.Wrap(err, "failed to find the menu items") |
| } |
| targetIdx := popupIdx + 1 |
| for i := 1; i < len(menuInfo); i++ { |
| if hasYoutubeIcon && strings.HasPrefix(strings.ToLower(menuInfo[i].Name), "youtube") { |
| targetIdx++ |
| } |
| } |
| if err := pc.Click(menus.Nth(targetIdx))(ctx); err != nil { |
| return errors.Wrapf(err, "failed to click menu item %d", popupIdx) |
| } |
| } |
| if err := ash.ForEachWindow(ctx, tconn, func(w *ash.Window) error { |
| return ash.WaitWindowFinishAnimating(ctx, tconn, w.ID) |
| }); err != nil { |
| return errors.Wrap(err, "failed to wait for the window animation") |
| } |
| return ash.WaitForHotseatAnimatingToIdealState(ctx, tconn, ash.ShelfHidden) |
| }, |
| } |
| } else { |
| pc = pointer.NewMouse(tconn) |
| topRow, err := input.KeyboardTopRowLayout(ctx, kw) |
| if err != nil { |
| s.Fatal("Failed to obtain the top-row layout: ", err) |
| } |
| setOverviewModeAndWait = func(ctx context.Context) error { |
| if err := kw.Accel(ctx, topRow.SelectTask); err != nil { |
| return errors.Wrap(err, "failed to hit overview key") |
| } |
| return ash.WaitForOverviewState(ctx, tconn, ash.Shown, timeout) |
| } |
| openAppList = func(ctx context.Context) error { |
| return kw.Accel(ctx, "search") |
| } |
| subtest2 = subtest{ |
| "alt-tab", |
| "Switching the focused window through Alt-Tab", |
| func(ctx context.Context, s *testing.State, i int) error { |
| // Press alt -> hit tabs for the number of windows to choose the last used |
| // window -> release alt. |
| if err := kw.AccelPress(ctx, "Alt"); err != nil { |
| return errors.Wrap(err, "failed to press alt") |
| } |
| defer kw.AccelRelease(ctx, "Alt") |
| if err := testing.Sleep(ctx, 500*time.Millisecond); err != nil { |
| return errors.Wrap(err, "failed to wait") |
| } |
| for j := 0; j < len(ws)-1; j++ { |
| if err := kw.Accel(ctx, "Tab"); err != nil { |
| return errors.Wrap(err, "failed to type tab") |
| } |
| if err := testing.Sleep(ctx, 200*time.Millisecond); err != nil { |
| return errors.Wrap(err, "failed to wait") |
| } |
| } |
| if err := testing.Sleep(ctx, time.Second); err != nil { |
| return errors.Wrap(err, "failed to wait") |
| } |
| return nil |
| }, |
| } |
| } |
| defer pc.Close() |
| |
| // Set up the cuj.Recorder: this test will measure the combinations of |
| // animation smoothness for window-cycles (alt-tab selection), launcher, |
| // and overview. |
| configs := []cuj.MetricConfig{ |
| cuj.NewSmoothnessMetricConfig("Ash.WindowCycleView.AnimationSmoothness.Container"), |
| cuj.NewLatencyMetricConfig("Ash.DragWindowFromShelf.PresentationTime"), |
| cuj.NewSmoothnessMetricConfig("Ash.Homescreen.AnimationSmoothness"), |
| cuj.NewLatencyMetricConfig("Ash.HotseatTransition.Drag.PresentationTime"), |
| cuj.NewCustomMetricConfig( |
| "Ash.Smoothness.PercentDroppedFrames_1sWindow", "percent", |
| perf.SmallerIsBetter, []int64{50, 80}), |
| cuj.NewCustomMetricConfig( |
| "Browser.Responsiveness.JankyIntervalsPerThirtySeconds3", "janks", |
| perf.SmallerIsBetter, []int64{0, 3}), |
| } |
| for _, suffix := range []string{"HideLauncherForWindow", "EnterFullscreenAllApps", "EnterFullscreenSearch", "FadeInOverview", "FadeOutOverview"} { |
| configs = append(configs, cuj.NewSmoothnessMetricConfig( |
| "Apps.HomeLauncherTransition.AnimationSmoothness."+suffix)) |
| } |
| for _, state := range []string{"Peeking", "Close", "Half"} { |
| configs = append(configs, cuj.NewSmoothnessMetricConfig( |
| "Apps.StateTransition.AnimationSmoothness."+state+".ClamshellMode")) |
| } |
| for _, suffix := range []string{"SingleClamshellMode", "ClamshellMode", "TabletMode"} { |
| configs = append(configs, |
| cuj.NewSmoothnessMetricConfig("Ash.Overview.AnimationSmoothness.Enter."+suffix), |
| cuj.NewSmoothnessMetricConfig("Ash.Overview.AnimationSmoothness.Exit."+suffix), |
| ) |
| } |
| for _, suffix := range []string{"TransitionToShownHotseat", "TransitionToExtendedHotseat", "TransitionToHiddenHotseat"} { |
| configs = append(configs, |
| cuj.NewSmoothnessMetricConfig("Ash.HotseatTransition.AnimationSmoothness."+suffix)) |
| } |
| recorder, err := cuj.NewRecorder(ctx, cr, nil, cuj.RecorderOptions{}, configs...) |
| if err != nil { |
| s.Fatal("Failed to create a recorder: ", err) |
| } |
| if testParam.tracing { |
| recorder.EnableTracing(s.OutDir()) |
| } |
| defer recorder.Close(closeCtx) |
| |
| if testParam.validation { |
| validationHelper := cuj.NewTPSValidationHelper(closeCtx) |
| if err := validationHelper.Stress(); err != nil { |
| s.Fatal("Failed to stress: ", err) |
| } |
| defer func() { |
| if err := validationHelper.Release(); err != nil { |
| s.Fatal("Failed to release validationHelper: ", err) |
| } |
| }() |
| } |
| |
| // Launch arc apps from the app launcher; first open the app-launcher, type |
| // the query and select the first search result, and wait for the app window |
| // to appear. When the app has the splash screen, skip it. |
| // TODO(mukai): make sure that the initial search result is the intended |
| // one. |
| for _, app := range []struct { |
| query string |
| packageName string |
| skipSplash func(ctx context.Context) error |
| }{ |
| {"play store", playStorePackageName, func(ctx context.Context) error { |
| return nil |
| }}, |
| {"gmail", gmailPackageName, func(ctx context.Context) error { |
| // Dismiss the ARC compatibility mode splash from ash if any. |
| if err := cuj.DismissMobilePrompt(ctx, tconn); err != nil { |
| return errors.Wrap(err, `failed to dismiss compat splash`) |
| } |
| |
| // Dismiss gmail app splashes. |
| const ( |
| dialogID = "com.google.android.gm:id/customPanel" |
| dismissID = "com.google.android.gm:id/gm_dismiss_button" |
| customPanelMaxCount = 10 |
| ) |
| gotIt := d.Object(ui.Text("GOT IT")) |
| if err := gotIt.WaitForExists(ctx, timeout); err != nil { |
| s.Log(`Failed to find "GOT IT" button, believing splash screen has been dismissed already`) |
| return nil |
| } |
| if err := gotIt.Click(ctx); err != nil { |
| return errors.Wrap(err, `failed to click "GOT IT" button`) |
| } |
| // Sometimes, the account information might not be ready yet. In that case |
| // a warning dialog appears. If the warning message does not appear, it |
| // is fine. |
| pleaseAdd := d.Object(ui.Text("Please add at least one email address")) |
| if err := pleaseAdd.WaitForExists(ctx, timeout); err == nil { |
| // Even though the warning dialog appears, the email address should |
| // appear already. Therefore, here simply clicks the 'OK' button to |
| // dismiss the warning dialog and moves on. |
| if err := testing.Sleep(ctx, timeout); err != nil { |
| return errors.Wrap(err, "failed to wait for the email address appearing") |
| } |
| okButton := d.Object(ui.ClassName("android.widget.Button"), ui.Text("OK")) |
| if err := okButton.Exists(ctx); err != nil { |
| return errors.Wrap(err, "failed to find the ok button") |
| } |
| if err := okButton.Click(ctx); err != nil { |
| return errors.Wrap(err, "failed to click the OK button") |
| } |
| } |
| takeMe := d.Object(ui.Text("TAKE ME TO GMAIL")) |
| if err := takeMe.WaitForExists(ctx, timeout); err != nil { |
| return errors.Wrap(err, `"TAKE ME TO GMAIL" is not shown`) |
| } |
| if err := takeMe.Click(ctx); err != nil { |
| return errors.Wrap(err, `failed to click "TAKE ME TO GMAIL" button`) |
| } |
| // After clicking 'take me to gmail', it might show a series of dialogs to |
| // finalize the setup. Here skips those dialogs by clicking their 'ok' |
| // buttons. |
| for i := 0; i < customPanelMaxCount; i++ { |
| dialog := d.Object(ui.ID(dialogID)) |
| if err := dialog.WaitForExists(ctx, timeout); err != nil { |
| return nil |
| } |
| dismiss := d.Object(ui.ID(dismissID)) |
| if err := dismiss.Exists(ctx); err != nil { |
| return errors.Wrap(err, "dismiss button not found") |
| } |
| if err := dismiss.Click(ctx); err != nil { |
| return errors.Wrap(err, "failed to click the dismiss button") |
| } |
| } |
| return errors.New("too many dialog popups") |
| }}, |
| } { |
| if err = recorder.Run(ctx, func(ctx context.Context) error { |
| launchCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) |
| defer cancel() |
| if _, err := ash.GetARCAppWindowInfo(ctx, tconn, app.packageName); err == nil { |
| testing.ContextLogf(ctx, "Package %s is already visible, skipping", app.packageName) |
| return nil |
| } |
| return uiauto.Combine( |
| "launch-app", |
| openAppList, |
| action.Sleep(time.Second), |
| kw.TypeAction(app.query), |
| // Finds the first search result tile and assume it is the app. Not |
| // searching by name because launcher could append suffix to the app |
| // name. E.g. gmail app has "Gmail, Installed app" as its name. The |
| // suffix is for chromevox and could change. |
| ac.WithTimeout(5*time.Second).LeftClick(nodewith.ClassName("SearchResultTileItemView").First()), |
| func(ctx context.Context) error { return ash.WaitForVisible(ctx, tconn, app.packageName) }, |
| func(ctx context.Context) error { |
| s.Log("Skipping the splash screen of ", app.query) |
| return nil |
| }, |
| app.skipSplash, |
| // Waits some time to stabilize the result of launcher animations. |
| action.Sleep(timeout), |
| )(launchCtx) |
| }); err != nil { |
| s.Fatalf("Failed to launch %s: %v", app.query, err) |
| } |
| } |
| |
| // Whether to close "New Tab" window. Needed for lacros variation and need |
| // to do the close after creating other lacros browser windows. |
| needCloseNewTabWindow := testParam.useLacros |
| |
| // Here adds browser windows: |
| // 1. webGL aquarium -- adding considerable load on graphics. |
| // 2. chromium issue tracker -- considerable amount of elements. |
| // 3. youtube -- the substitute of youtube app. |
| for _, url := range []string{ |
| "https://webglsamples.org/aquarium/aquarium.html", |
| "https://bugs.chromium.org/p/chromium/issues/list", |
| "https://youtube.com/", |
| } { |
| conn, err := cs.NewConn(ctx, url, browser.WithNewWindow()) |
| if err != nil { |
| s.Fatalf("Failed to open %s: %v", url, err) |
| } |
| // We don't need to keep the connection, so close it now. |
| if err = conn.Close(); err != nil { |
| s.Fatalf("Failed to close the connection to %s: %v", url, err) |
| } |
| |
| // Lacros specific setup to close "New Tab" window. |
| if needCloseNewTabWindow { |
| w, err := ash.FindWindow(ctx, tconn, func(w *ash.Window) bool { |
| return strings.HasPrefix(w.Title, "New Tab") && strings.HasPrefix(w.Name, "ExoShellSurface") |
| }) |
| if err != nil { |
| s.Fatal("Failed to find New Tab window: ", err) |
| } |
| if err := w.CloseWindow(ctx, tconn); err != nil { |
| s.Fatal("Failed to close New Tab window: ", err) |
| } |
| needCloseNewTabWindow = false |
| } |
| |
| w, err := ash.FindWindow(ctx, tconn, func(w *ash.Window) bool { |
| if w.WindowType != ash.WindowTypeBrowser && w.WindowType != ash.WindowTypeLacros { |
| return false |
| } |
| return !browserWindows[w.ID] |
| }) |
| if err != nil { |
| s.Fatalf("Failed to find the browser window for %s: %v", url, err) |
| } |
| browserWindows[w.ID] = true |
| if !tabletMode { |
| if err := ash.SetWindowStateAndWait(ctx, tconn, w.ID, ash.WindowStateNormal); err != nil { |
| s.Fatalf("Failed to change the window (%s) into the normal state: %v", url, err) |
| } |
| } |
| } |
| |
| subtests := []subtest{ |
| { |
| "overview", |
| "Switching the focused window through the overview mode", |
| func(ctx context.Context, s *testing.State, i int) error { |
| if err := setOverviewModeAndWait(ctx); err != nil { |
| return errors.Wrap(err, "failed to enter into the overview mode") |
| } |
| done := false |
| defer func() { |
| // In case of errornerous operations; finish the overview mode. |
| if !done { |
| if err := ash.SetOverviewModeAndWait(ctx, tconn, false); err != nil { |
| s.Error("Failed to finish the overview mode: ", err) |
| } |
| } |
| }() |
| ws, err := ash.GetAllWindows(ctx, tconn) |
| if err != nil { |
| return errors.Wrap(err, "failed to get the overview windows") |
| } |
| // Find the bottom-right overview item; which is the bottom of the LRU |
| // list of the windows. |
| var targetWindow *ash.Window |
| for _, w := range ws { |
| if w.OverviewInfo == nil { |
| continue |
| } |
| if targetWindow == nil { |
| targetWindow = w |
| } else { |
| overviewBounds := w.OverviewInfo.Bounds |
| targetBounds := targetWindow.OverviewInfo.Bounds |
| // Assumes the window is arranged in the grid and pick up the bottom |
| // right one. |
| if overviewBounds.Top > targetBounds.Top || (overviewBounds.Top == targetBounds.Top && overviewBounds.Left > targetBounds.Left) { |
| targetWindow = w |
| } |
| } |
| } |
| if targetWindow == nil { |
| return errors.New("no windows are in overview mode") |
| } |
| if err := pc.ClickAt(targetWindow.OverviewInfo.Bounds.CenterPoint())(ctx); err != nil { |
| return errors.Wrap(err, "failed to click") |
| } |
| if err := ash.WaitForCondition(ctx, tconn, func(w *ash.Window) bool { |
| return w.ID == targetWindow.ID && w.OverviewInfo == nil && w.IsActive |
| }, &testing.PollOptions{Timeout: timeout}); err != nil { |
| return errors.Wrap(err, "failed to wait") |
| } |
| done = true |
| return nil |
| }, |
| }, |
| subtest2, |
| } |
| |
| ws, err = ash.GetAllWindows(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get window list: ", err) |
| } |
| |
| for _, st := range subtests { |
| s.Log(st.desc) |
| s.Run(ctx, st.name, func(ctx context.Context, s *testing.State) { |
| for i := 0; i < len(ws); i++ { |
| if err := recorder.Run(ctx, func(ctx context.Context) error { return st.f(ctx, s, i) }); err != nil { |
| s.Error("Failed to run the scenario: ", err) |
| } |
| } |
| }) |
| } |
| |
| pv := perf.NewValues() |
| if err = recorder.Record(ctx, pv); err != nil { |
| s.Fatal("Failed to report: ", err) |
| } |
| if err = pv.Save(s.OutDir()); err != nil { |
| s.Error("Failed to store values: ", err) |
| } |
| } |