| // 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" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "time" |
| |
| "chromiumos/tast/common/bond" |
| "chromiumos/tast/common/media/caps" |
| "chromiumos/tast/common/perf" |
| "chromiumos/tast/ctxutil" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/action" |
| "chromiumos/tast/local/apps" |
| "chromiumos/tast/local/bundles/cros/ui/cuj" |
| "chromiumos/tast/local/chrome" |
| "chromiumos/tast/local/chrome/ash" |
| "chromiumos/tast/local/chrome/cdputil" |
| "chromiumos/tast/local/chrome/display" |
| "chromiumos/tast/local/chrome/uiauto" |
| "chromiumos/tast/local/chrome/uiauto/faillog" |
| "chromiumos/tast/local/chrome/uiauto/mouse" |
| "chromiumos/tast/local/chrome/uiauto/nodewith" |
| "chromiumos/tast/local/chrome/uiauto/pointer" |
| "chromiumos/tast/local/chrome/uiauto/role" |
| "chromiumos/tast/local/coords" |
| "chromiumos/tast/local/graphics" |
| "chromiumos/tast/local/input" |
| "chromiumos/tast/local/lacros/launcher" |
| "chromiumos/tast/local/power" |
| "chromiumos/tast/local/power/setup" |
| "chromiumos/tast/local/profiler" |
| "chromiumos/tast/testing" |
| "chromiumos/tast/testing/hwdep" |
| ) |
| |
| type meetLayoutType string |
| |
| const ( |
| meetLayoutSpotlight meetLayoutType = "Spotlight" |
| meetLayoutTiled meetLayoutType = "Tiled" |
| meetLayoutSidebar meetLayoutType = "Sidebar" |
| meetLayoutAuto meetLayoutType = "Auto" |
| ) |
| |
| // meetTest specifies the setting of a Hangouts Meet journey. More info at go/cros-meet-tests. |
| type meetTest struct { |
| num int // Number of the participants in the meeting. |
| layout meetLayoutType // Type of the layout in the meeting. |
| present bool // Whether it is presenting the Google Docs or not. It can not be true if docs is false. |
| docs bool // Whether it is running with a Google Docs window. |
| jamboard bool // Whether it is running with a Jamboard window. |
| split bool // Whether it is in split screen mode. It can not be true if docs is false. |
| cam bool // Whether the camera is on or not. |
| power bool // Whether to collect power metrics. |
| duration time.Duration // Duration of the meet call. Must be less than test timeout. |
| useLacros bool // Whether to use lacros browser. |
| } |
| |
| const defaultTestTimeout = 7 * time.Minute |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: MeetCUJ, |
| Desc: "Measures the performance of critical user journey for Google Meet", |
| Contacts: []string{"mukai@chromium.org", "tclaiborne@chromium.org"}, |
| Attr: []string{"group:crosbolt", "crosbolt_perbuild"}, |
| SoftwareDeps: []string{"chrome", "arc", caps.BuiltinOrVividCamera}, |
| Vars: []string{ |
| "mute", |
| "record", |
| "meeting_code", |
| "ui.MeetCUJ.doc", |
| }, |
| VarDeps: []string{ |
| "ui.MeetCUJ.bond_credentials", |
| }, |
| Params: []testing.Param{{ |
| // Base case. Note this runs a 30 min meet call. |
| Name: "4p", |
| Timeout: 37 * time.Minute, |
| Val: meetTest{ |
| num: 4, |
| layout: meetLayoutTiled, |
| cam: true, |
| duration: 30 * time.Minute, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // Small meeting. |
| Name: "4p_present_notes_split", |
| Timeout: defaultTestTimeout, |
| Val: meetTest{ |
| num: 4, |
| layout: meetLayoutTiled, |
| present: true, |
| docs: true, |
| split: true, |
| cam: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // Big meeting. |
| Name: "16p", |
| Timeout: defaultTestTimeout, |
| Val: meetTest{ |
| num: 16, |
| layout: meetLayoutTiled, |
| cam: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // Big meeting with notes. |
| Name: "16p_notes", |
| Timeout: defaultTestTimeout, |
| Val: meetTest{ |
| num: 16, |
| layout: meetLayoutTiled, |
| docs: true, |
| split: true, |
| cam: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // 4p power test. |
| Name: "power_4p", |
| Timeout: defaultTestTimeout, |
| ExtraHardwareDeps: hwdep.D(hwdep.ForceDischarge()), |
| Val: meetTest{ |
| num: 4, |
| layout: meetLayoutTiled, |
| cam: true, |
| power: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // 16p power test. |
| Name: "power_16p", |
| Timeout: defaultTestTimeout, |
| ExtraHardwareDeps: hwdep.D(hwdep.ForceDischarge()), |
| Val: meetTest{ |
| num: 16, |
| layout: meetLayoutTiled, |
| cam: true, |
| power: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // 16p with jamboard test. |
| Name: "16p_jamboard", |
| Timeout: defaultTestTimeout, |
| Val: meetTest{ |
| num: 16, |
| layout: meetLayoutTiled, |
| jamboard: true, |
| split: true, |
| cam: true, |
| }, |
| Fixture: "loggedInToCUJUser", |
| }, { |
| // Lacros 4p |
| Name: "lacros_4p", |
| Timeout: defaultTestTimeout, |
| Val: meetTest{ |
| num: 4, |
| layout: meetLayoutTiled, |
| cam: true, |
| useLacros: true, |
| }, |
| Fixture: "loggedInToCUJUserLacros", |
| ExtraData: []string{launcher.DataArtifact}, |
| ExtraSoftwareDeps: []string{"lacros"}, |
| }}, |
| }) |
| } |
| |
| // MeetCUJ measures the performance of critical user journeys for Google Meet. |
| // Journeys for Google Meet are specified by testing parameters. |
| // |
| // Pre-preparation: |
| // - Open a Meet window. |
| // - Create and enter the meeting code. |
| // - Open a Google Docs window (if necessary). |
| // - Enter split mode (if necessary). |
| // - Turn off camera (if necessary). |
| // During recording: |
| // - Join the meeting. |
| // - Add participants(bots) to the meeting. |
| // - Set up the layout. |
| // - Max out the number of the maximum tiles (if necessary). |
| // - Start to present (if necessary). |
| // - Input notes to Google Docs file (if necessary). |
| // - Wait for 30 seconds before ending the meeting. |
| // After recording: |
| // - Record and save metrics. |
| func MeetCUJ(ctx context.Context, s *testing.State) { |
| const ( |
| timeout = 10 * time.Second |
| defaultDocsURL = "https://docs.new/" |
| jamboardURL = "https://jamboard.google.com" |
| notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." |
| newTabTitle = "New Tab" |
| ) |
| |
| pollOpts := testing.PollOptions{Interval: time.Second, Timeout: timeout} |
| |
| // Ensure display on to record ui performance correctly. |
| if err := power.TurnOnDisplay(ctx); err != nil { |
| s.Fatal("Failed to turn on display: ", err) |
| } |
| |
| meet := s.Param().(meetTest) |
| if meet.docs && meet.jamboard { |
| s.Fatal("Tried to open both Google Docs and Jamboard at the same time") |
| } |
| |
| // Determines the meet call duration. Use the meet duration specified in |
| // test param if there is one. Otherwise, default to 2 minutes for the base |
| // calls, 3 min for calls with doc or jamboard, or 5 min for power tests. |
| meetTimeout := 2 * time.Minute |
| if meet.duration != 0 { |
| meetTimeout = meet.duration |
| } else { |
| if meet.docs || meet.jamboard { |
| meetTimeout = 3 * time.Minute |
| } |
| if meet.power { |
| meetTimeout = 5 * time.Minute |
| } |
| } |
| s.Log("Run meeting for ", meetTimeout) |
| |
| // Shorten context to allow for cleanup. Reserve one minute in case of power |
| // test. |
| closeCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, time.Minute) |
| defer cancel() |
| |
| var tconn *chrome.TestConn |
| var cs ash.ConnSource |
| |
| { |
| // Keep `cr` inside to avoid accidental access of ash-chrome in lacros |
| // variation. |
| var cr *chrome.Chrome |
| if meet.useLacros { |
| cr = s.FixtValue().(launcher.FixtData).Chrome |
| } else { |
| cr = s.FixtValue().(cuj.FixtureData).Chrome |
| cs = cr |
| } |
| |
| var err error |
| tconn, err = cr.TestAPIConn(ctx) |
| if err != nil { |
| s.Fatal("Failed to connect to the test API connection: ", err) |
| } |
| } |
| |
| if meet.useLacros { |
| // Launch lacros via shelf. |
| f := s.FixtValue().(launcher.FixtData) |
| |
| // TODO(crbug.com/1127165): Remove this when we can use Data in fixtures. |
| if err := launcher.EnsureLacrosChrome(ctx, f, s.DataPath(launcher.DataArtifact)); err != nil { |
| s.Fatal("Failed to extract lacros binary: ", err) |
| } |
| s.Log("Launch lacros via Shelf") |
| if err := ash.LaunchAppFromShelf(ctx, tconn, apps.Lacros.Name, apps.Lacros.ID); err != nil { |
| s.Fatal("Failed to launch lacros: ", err) |
| } |
| s.Log("Wait for Lacros window") |
| if err := launcher.WaitForLacrosWindow(ctx, tconn, newTabTitle); err != nil { |
| s.Fatal("Failed to wait for lacros: ", err) |
| } |
| l, err := launcher.ConnectToLacrosChrome(ctx, f.LacrosPath, launcher.LacrosUserDataDir) |
| if err != nil { |
| s.Fatal("Failed to connect to lacros: ", err) |
| } |
| defer l.Close(ctx) |
| cs = l |
| } |
| |
| creds := s.RequiredVar("ui.MeetCUJ.bond_credentials") |
| bc, err := bond.NewClient(ctx, bond.WithCredsJSON([]byte(creds))) |
| if err != nil { |
| s.Fatal("Failed to create a bond client: ", err) |
| } |
| defer bc.Close() |
| |
| var meetingCode string |
| customCode, codeOk := s.Var("meeting_code") |
| if codeOk { |
| meetingCode = customCode |
| } else { |
| func() { |
| sctx, cancel := context.WithTimeout(ctx, timeout) |
| defer cancel() |
| meetingCode, err = bc.CreateConference(sctx) |
| if err != nil { |
| s.Fatal("Failed to create a conference room: ", err) |
| } |
| }() |
| s.Log("Created a room with the code ", meetingCode) |
| } |
| |
| tabChecker, err := cuj.NewTabCrashChecker(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to create TabCrashChecker: ", err) |
| } |
| |
| if _, ok := s.Var("record"); ok { |
| screenRecorder, err := uiauto.NewScreenRecorder(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to create ScreenRecorder: ", err) |
| } |
| defer func() { |
| screenRecorder.Stop(ctx) |
| dir, ok := testing.ContextOutDir(ctx) |
| if ok && dir != "" { |
| if _, err := os.Stat(dir); err == nil { |
| testing.ContextLogf(ctx, "Saving screen record to %s", dir) |
| if err := screenRecorder.SaveInString(ctx, filepath.Join(dir, "screen_record.txt")); err != nil { |
| s.Fatal("Failed to save screen record in string: ", err) |
| } |
| if err := screenRecorder.SaveInBytes(ctx, filepath.Join(dir, "screen_record.webm")); err != nil { |
| s.Fatal("Failed to save screen record in bytes: ", err) |
| } |
| } |
| } |
| screenRecorder.Release(ctx) |
| }() |
| screenRecorder.Start(ctx, tconn) |
| } |
| |
| tweakPerfValues := func(pv *perf.Values) error { return nil } |
| if meet.power { |
| s.Log("Preparing for power metrics collection") |
| |
| // Setup needs to happen before power.TestMetrics() to disable wifi first |
| // so that the thermal sensor for wifi is excluded from the metrics. |
| sup, cleanup := setup.New("meet call power") |
| sup.Add(setup.PowerTest(ctx, tconn, setup.PowerTestOptions{ |
| Wifi: setup.DisableWifiInterfaces, |
| Battery: setup.ForceBatteryDischarge, |
| NightLight: setup.DisableNightLight})) |
| defer func() { |
| if err := cleanup(closeCtx); err != nil { |
| s.Error("Cleanup meet power setup failed: ", err) |
| } |
| }() |
| |
| // Power tests need to record power metrics; they are separated from |
| // cuj.Recorder's timeline as it is for a different purpose and mixing them |
| // might cause a risk of taking too much time of collecting data. |
| timeline, err := perf.NewTimeline(ctx, power.TestMetrics(), perf.Prefix("Power.")) |
| if err != nil { |
| s.Fatal("Failed to create power metrics: ", err) |
| } |
| if err = timeline.Start(ctx); err != nil { |
| s.Fatal("Failed to start power timeline: ", err) |
| } |
| if err = timeline.StartRecording(ctx); err != nil { |
| s.Fatal("Failed to start recording the power metrics: ", err) |
| } |
| tweakPerfValues = func(pv *perf.Values) error { |
| values, err := timeline.StopRecording(ctx) |
| if err != nil { |
| return err |
| } |
| pv.Merge(values) |
| return nil |
| } |
| } |
| |
| configs := []cuj.MetricConfig{cuj.NewCustomMetricConfig( |
| "Graphics.Smoothness.PercentDroppedFrames.CompositorThread.Video", |
| "percent", perf.SmallerIsBetter, []int64{5, 10})} |
| for _, suffix := range []string{"Capturer", "Encoder", "EncoderQueue", "RateLimiter"} { |
| configs = append(configs, cuj.NewCustomMetricConfig( |
| "WebRTC.Video.DroppedFrames."+suffix, "percent", perf.SmallerIsBetter, |
| []int64{50, 80})) |
| } |
| // Jank criteria for input event latencies. The 1st number is the |
| // threshold to be marked as jank and the 2nd one is to be marked |
| // very jank. |
| jankCriteria := []int64{80000, 400000} |
| if meet.docs { |
| configs = append(configs, cuj.NewCustomMetricConfig( |
| "Event.Latency.EndToEnd.KeyPress", "microsecond", perf.SmallerIsBetter, |
| jankCriteria)) |
| } else if meet.jamboard { |
| configs = append(configs, cuj.NewCustomMetricConfig( |
| "Event.Latency.EndToEnd.Mouse", "microsecond", perf.SmallerIsBetter, |
| jankCriteria)) |
| } |
| |
| recorder, err := cuj.NewRecorder(ctx, tconn, configs...) |
| if err != nil { |
| s.Fatal("Failed to create the recorder: ", err) |
| } |
| defer func() { |
| if err := recorder.Close(closeCtx); err != nil { |
| s.Error("Failed to stop recorder: ", err) |
| } |
| }() |
| |
| meetConn, err := cs.NewConn(ctx, "https://meet.google.com/", cdputil.WithNewWindow()) |
| if err != nil { |
| s.Fatal("Failed to open the hangout meet website: ", err) |
| } |
| defer meetConn.Close() |
| defer faillog.DumpUITreeOnError(ctx, s.OutDir(), s.HasError, tconn) |
| |
| // Lacros specific setup. |
| if meet.useLacros { |
| // Close "New Tab" window after creating the meet window. |
| w, err := ash.FindWindow(ctx, tconn, func(w *ash.Window) bool { |
| return strings.HasPrefix(w.Title, newTabTitle) && 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) |
| } |
| } |
| |
| inTabletMode, err := ash.TabletModeEnabled(ctx, tconn) |
| s.Logf("Is in tablet-mode: %t", inTabletMode) |
| if err != nil { |
| s.Fatal("Failed to detect it is in tablet-mode or not: ", err) |
| } |
| var pc pointer.Context |
| if inTabletMode { |
| // If it is in tablet mode, ensure it it in landscape orientation. |
| // TODO(crbug/1135239): test portrait orientation as well. |
| orientation, err := display.GetOrientation(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get display orientation: ", err) |
| } |
| if orientation.Type == display.OrientationPortraitPrimary { |
| info, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get the primary display info: ", err) |
| } |
| s.Log("Rotating display 90 degrees") |
| if err := display.SetDisplayRotationSync(ctx, tconn, info.ID, display.Rotate90); err != nil { |
| s.Fatal("Failed to rotate display: ", err) |
| } |
| defer display.SetDisplayRotationSync(ctx, tconn, info.ID, display.Rotate0) |
| } |
| pc, err = pointer.NewTouch(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to create a touch controller: ", err) |
| } |
| } else { |
| // Make it into a maximized window if it is in clamshell-mode. |
| if err := ash.ForEachWindow(ctx, tconn, func(w *ash.Window) error { |
| return ash.SetWindowStateAndWait(ctx, tconn, w.ID, ash.WindowStateMaximized) |
| }); err != nil { |
| s.Fatal("Failed to turn all windows into maximized state: ", err) |
| } |
| pc = pointer.NewMouse(tconn) |
| } |
| defer pc.Close() |
| |
| ui := uiauto.New(tconn) |
| |
| kw, err := input.Keyboard(ctx) |
| if err != nil { |
| s.Fatal("Failed to create a keyboard: ", err) |
| } |
| defer kw.Close() |
| |
| // Find the web view of Meet window. |
| webview := nodewith.ClassName("ContentsWebView").Role(role.WebView) |
| if err := action.Combine( |
| "click and type meeting code", |
| // Assume that the meeting code is the first textfield in the webpage. |
| ui.LeftClick(nodewith.Role(role.TextField).Ancestor(webview).First()), |
| kw.TypeAction(meetingCode), |
| kw.AccelAction("Enter"), |
| )(ctx); err != nil { |
| s.Fatal("Failed to input the meeting code: ", err) |
| } |
| |
| bubble := nodewith.ClassName("PermissionPromptBubbleView").First() |
| allow := nodewith.Name("Allow").Role(role.Button).Ancestor(bubble) |
| // Check and grant permissions. |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| needPermission, err := needToGrantPermission(ctx, meetConn) |
| if err != nil { |
| return testing.PollBreak(errors.Wrap(err, "failed to check if it needs to grant permissions")) |
| } |
| if !needPermission { |
| return nil |
| } |
| if err := pc.Click(allow)(ctx); err != nil { |
| return errors.Wrap(err, "failed to click the allow button") |
| } |
| return errors.New("granting permissions") |
| }, &testing.PollOptions{Interval: time.Second, Timeout: 2 * time.Minute}); err != nil { |
| s.Fatal("Failed to grant permissions: ", err) |
| } |
| |
| defer func() { |
| // Close the Meet window to finish meeting. |
| if err := meetConn.CloseTarget(closeCtx); err != nil { |
| s.Error("Failed to close the meeting: ", err) |
| } |
| }() |
| |
| if meet.docs { |
| docsURL := defaultDocsURL |
| if docsURLOverride, ok := s.Var("ui.MeetCUJ.doc"); ok { |
| docsURL = docsURLOverride |
| } |
| |
| // Create another browser window and open a Google Docs file. |
| docsConn, err := cs.NewConn(ctx, docsURL, cdputil.WithNewWindow()) |
| if err != nil { |
| s.Fatal("Failed to open the google docs website: ", err) |
| } |
| defer docsConn.Close() |
| s.Log("Creating a Google Docs window") |
| } else if meet.jamboard { |
| // Create another browser window and open a new Jamboard file. |
| jamboardConn, err := cs.NewConn(ctx, jamboardURL, cdputil.WithNewWindow()) |
| if err != nil { |
| s.Fatal("Failed to open the jamboard website: ", err) |
| } |
| defer jamboardConn.Close() |
| s.Log("Creating a Jamboard window") |
| if err := ui.LeftClick(nodewith.Name("New Jam").Role(role.Button))(ctx); err != nil { |
| s.Fatal("Failed to click the new jam button: ", err) |
| } |
| } |
| |
| if meet.split { |
| // If it is in split mode, snap Meet window to the left and Google Docs window to the right. |
| // Enter overview mode to enter split mode. |
| if err := ash.SetOverviewModeAndWait(ctx, tconn, true); err != nil { |
| s.Fatal("Failed to set overview mode: ", err) |
| } |
| ws, err := ash.GetAllWindows(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get the window list: ", err) |
| } |
| var meetWindow, docsWindow *ash.Window |
| re := regexp.MustCompile(`\bMeet\b`) |
| for _, w := range ws { |
| if re.MatchString(w.Title) { |
| meetWindow = w |
| } else { |
| docsWindow = w |
| } |
| } |
| // There should be always a Hangouts Meet window. |
| if meetWindow == nil { |
| s.Fatal("Failed to find Meet window") |
| } |
| info, err := display.GetPrimaryInfo(ctx, tconn) |
| if err != nil { |
| s.Fatal("Failed to get the primary display info: ", err) |
| } |
| snapLeftPoint := coords.NewPoint(info.WorkArea.Left+1, info.WorkArea.CenterY()) |
| snapRightPoint := coords.NewPoint(info.WorkArea.Right()-1, info.WorkArea.CenterY()) |
| if inTabletMode { |
| if docsWindow != nil { |
| if err := pc.Drag( |
| docsWindow.OverviewInfo.Bounds.CenterPoint(), |
| // Sleep is needed in tablet mode |
| action.Sleep(time.Second), |
| pc.DragTo(snapLeftPoint, time.Second), |
| )(ctx); err != nil { |
| s.Fatal("Failed to drag the Google Docs window to the left: ", err) |
| } |
| } |
| if err := pc.Drag( |
| meetWindow.OverviewInfo.Bounds.CenterPoint(), |
| action.Sleep(time.Second), |
| pc.DragTo(snapRightPoint, time.Second), |
| )(ctx); err != nil { |
| s.Fatal("Failed to drag the Meet window to the right: ", err) |
| } |
| } else { |
| if docsWindow != nil { |
| if err := pc.Drag(docsWindow.OverviewInfo.Bounds.CenterPoint(), pc.DragTo(snapLeftPoint, time.Second))(ctx); err != nil { |
| s.Fatal("Failed to drag the Docs window to snap to the left: ", err) |
| } |
| } |
| if err := pc.Drag(meetWindow.OverviewInfo.Bounds.CenterPoint(), pc.DragTo(snapRightPoint, time.Second))(ctx); err != nil { |
| s.Fatal("Failed to drag the Meet window to snap to the right: ", err) |
| } |
| } |
| } else { |
| // If it is not in split screen mode, alt-tab to switch to Meet window on top. |
| if err := kw.Accel(ctx, "Alt+Tab"); err != nil { |
| s.Fatal("Failed to hit alt-tab and switch to Meet window: ", err) |
| } |
| } |
| |
| pv := perf.NewValues() |
| if err := recorder.Run(ctx, func(ctx context.Context) error { |
| // Hide notifications so that they won't overlap with other UI components. |
| if err := ash.CloseNotifications(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to close all notifications") |
| } |
| shareMessage := "Share this info with people you want in the meeting" |
| if err := ui.WaitUntilExists(nodewith.Name(shareMessage).Ancestor(webview))(ctx); err == nil { |
| // "Share this code" popup appears, dismissing by close button. |
| if err := uiauto.Combine( |
| "click the close button and wait for the popup to disappear", |
| pc.Click(nodewith.Name("Close").Role(role.Button).Ancestor(webview)), |
| ui.WaitUntilGone(nodewith.Name(shareMessage).Ancestor(webview)), |
| )(ctx); err != nil { |
| return err |
| } |
| } |
| |
| sctx, cancel := context.WithTimeout(ctx, timeout) |
| defer cancel() |
| // Add 30 seconds to the bot duration to make sure that bots do not leave |
| // slightly earlier than the test scenario. |
| if !codeOk { |
| if _, err := bc.AddBots(sctx, meetingCode, meet.num, meetTimeout+30*time.Second); err != nil { |
| return errors.Wrap(err, "failed to create bots") |
| } |
| } |
| if err := meetConn.WaitForExpr(ctx, "hrTelemetryApi.isInMeeting() === true"); err != nil { |
| return errors.Wrap(err, "failed to wait for entering meeting") |
| } |
| |
| if !meet.cam { |
| if err := meetConn.Eval(ctx, "hrTelemetryApi.setCameraMuted(true)", nil); err != nil { |
| return errors.Wrap(err, "failed to turn off camera") |
| } |
| } |
| |
| // Hide notifications so that they won't overlap with other UI components. |
| if err := ash.CloseNotifications(ctx, tconn); err != nil { |
| return errors.Wrap(err, "failed to close all notifications") |
| } |
| if err := meetConn.Eval(ctx, fmt.Sprintf("hrTelemetryApi.set%sLayout()", string(meet.layout)), nil); err != nil { |
| return errors.Wrapf(err, "failed to set %s layout", string(meet.layout)) |
| } |
| |
| if meet.present { |
| if !meet.docs && !meet.jamboard { |
| return errors.New("need a Google Docs or Jamboard tab to present") |
| } |
| // Start presenting the tab. |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| if err := ui.Exists(nodewith.Name("Chrome Tab").Role(role.ListGrid))(ctx); err == nil { |
| return nil |
| } |
| if err := meetConn.Eval(ctx, "hrTelemetryApi.presentation.presentTab()", nil); err != nil { |
| return errors.Wrap(err, "failed to start to present a tab") |
| } |
| return errors.New("presentation hasn't started yet") |
| }, &pollOpts); err != nil { |
| return errors.Wrap(err, "failed to start presentation") |
| } |
| |
| // Select the second tab (Google Docs tab) to present. |
| if err := action.Combine( |
| "select Google Docs tab", |
| pc.Click(nodewith.Name("Chrome Tab").Role(role.ListGrid)), |
| // Press down twice to select the second tab, which is Google Docs. |
| kw.AccelAction("Down"), |
| kw.AccelAction("Down"), |
| kw.AccelAction("Enter"), |
| )(ctx); err != nil { |
| return errors.Wrap(err, "failed to select the Google Docs tab") |
| } |
| } |
| |
| prof, err := profiler.Start(ctx, s.OutDir(), profiler.Perf(profiler.PerfRecordOpts())) |
| if err != nil { |
| return errors.Wrap(err, "failed to start the profiler") |
| } |
| if prof != nil { |
| defer func() { |
| if err := prof.End(); err != nil { |
| s.Error("Failed to stop profiler: ", err) |
| } |
| }() |
| } |
| |
| errc := make(chan error) |
| s.Log("Keeping the meet session for ", meetTimeout) |
| go func() { |
| // Using goroutine to measure GPU counters asynchronously because: |
| // - we will add some other test scenarios (controlling windows / meet sessions). |
| // - graphics.MeasureGPUCounters may quit immediately when the hardware or |
| // kernel does not support the reporting mechanism. |
| errc <- graphics.MeasureGPUCounters(ctx, meetTimeout, pv) |
| }() |
| |
| if meet.docs { |
| if err := action.Combine( |
| "select Google Docs", |
| kw.AccelAction("Alt+Tab"), |
| pc.Click(nodewith.Name("Document content").Role(role.TextField)), |
| kw.AccelAction("Ctrl+Alt+["), |
| kw.AccelAction("Ctrl+A"), |
| )(ctx); err != nil { |
| return errors.Wrap(err, "failed to select Google Docs") |
| } |
| end := time.Now().Add(meetTimeout) |
| // Wait for 5 seconds, type notes for 12.4 seconds then until the time is |
| // elapsed (3 times by default). Wait before the first typing to reduce |
| // the overlap between typing and joining the meeting. |
| for end.Sub(time.Now()).Seconds() > 18 { |
| if err := uiauto.Combine( |
| "sleep and type", |
| action.Sleep(5*time.Second), |
| kw.TypeAction(notes), |
| )(ctx); err != nil { |
| return err |
| } |
| } |
| if err := kw.Accel(ctx, "Alt+Tab"); err != nil { |
| return errors.Wrap(err, "failed to hit alt-tab and focus back to Meet tab") |
| } |
| meetTimeout = end.Sub(time.Now()) |
| } else if meet.jamboard { |
| // Simulate mouse input on jamboard. |
| if err := ui.LeftClick(nodewith.Name("Pen").Role(role.ToggleButton))(ctx); err != nil { |
| s.Fatal("Failed to click the pen toggle button: ", err) |
| } |
| contentArea, err := ui.Location(ctx, nodewith.ClassName("jam-content-area").Role(role.GenericContainer)) |
| if err != nil { |
| s.Fatal("Failed to find the location of jamboard content area: ", err) |
| } |
| centerX, centerY, offsetX, offsetY := contentArea.CenterPoint().X, contentArea.CenterPoint().Y, 10, 10 |
| end := time.Now().Add(meetTimeout) |
| for end.Sub(time.Now()).Seconds() > 42 { |
| for i := 1; i <= 10; i++ { |
| if err := uiauto.Combine( |
| "simulate mouse movement", |
| mouse.Move(tconn, coords.NewPoint(centerX-i*offsetX, centerY-i*offsetY), 0), |
| mouse.Press(tconn, mouse.LeftButton), |
| mouse.Move(tconn, coords.NewPoint(centerX-i*offsetX, centerY+i*offsetY), time.Second), |
| mouse.Move(tconn, coords.NewPoint(centerX+i*offsetX, centerY+i*offsetY), time.Second), |
| mouse.Move(tconn, coords.NewPoint(centerX+i*offsetX, centerY-i*offsetY), time.Second), |
| mouse.Move(tconn, coords.NewPoint(centerX-i*offsetX, centerY-i*offsetY), time.Second), |
| mouse.Release(tconn, mouse.LeftButton), |
| )(ctx); err != nil { |
| s.Fatal("Failed to simulate mouse movement on jamboard: ", err) |
| } |
| } |
| } |
| meetTimeout = end.Sub(time.Now()) |
| } |
| |
| // Ensures that meet session is long enough. graphics.MeasureGPUCounters |
| // exits early without errors on ARM where there is no i915 counters. |
| if err := testing.Sleep(ctx, meetTimeout); err != nil { |
| return errors.Wrap(err, "failed to wait") |
| } |
| |
| if err := <-errc; err != nil { |
| return errors.Wrap(err, "failed to collect GPU counters") |
| } |
| return nil |
| }); err != nil { |
| s.Fatal("Failed to conduct the recorder task: ", err) |
| } |
| |
| // Before recording the metrics, check if there is any tab crashed. |
| if err := tabChecker.Check(ctx); err != nil { |
| s.Fatal("Tab renderer crashed: ", err) |
| } |
| |
| if err := tweakPerfValues(pv); err != nil { |
| s.Fatal("Failed to tweak the perf values: ", err) |
| } |
| if err := recorder.Record(ctx, pv); err != nil { |
| s.Fatal("Failed to record the data: ", err) |
| } |
| if pv.Save(s.OutDir()); err != nil { |
| s.Error("Failed to save the perf data: ", err) |
| } |
| } |
| |
| // needToGrantPermission checks if we need to grant permission before joining meetings. |
| // If camera/microphone/notifications permissions are not granted, we need to skip |
| // the permission bubbles later. |
| func needToGrantPermission(ctx context.Context, conn *chrome.Conn) (bool, error) { |
| perms := []string{"microphone", "camera", "notifications"} |
| for _, perm := range perms { |
| var state string |
| if err := conn.Eval(ctx, fmt.Sprintf( |
| `new Promise(function(resolve, reject) { |
| navigator.permissions.query({name: '%v'}) |
| .then((permission) => { |
| resolve(permission.state); |
| }) |
| .catch((error) => { |
| reject(error); |
| }); |
| })`, perm), &state); err != nil { |
| return true, errors.Errorf("failed to query %v permission", perm) |
| } |
| if state != "granted" { |
| return true, nil |
| } |
| } |
| return false, nil |
| } |