blob: ae87e77b14489082db1627cdf21fe31a8f875bef [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 conference
import (
"context"
"regexp"
"strconv"
"strings"
"time"
"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/uiauto"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/chrome/uiauto/role"
"chromiumos/tast/local/chrome/webutil"
"chromiumos/tast/local/input"
"chromiumos/tast/testing"
)
// GoogleMeetConference implements the Conference interface.
type GoogleMeetConference struct {
cr *chrome.Chrome
tconn *chrome.TestConn
tabletMode bool
roomSize int
account string
password string
}
// Join joins a new conference room.
func (conf *GoogleMeetConference) Join(ctx context.Context, room string) error {
const showJoinNowTimeout = time.Minute
tconn := conf.tconn
ui := uiauto.New(tconn)
meetAccount := conf.account
openConference := func(ctx context.Context) error {
conn, err := conf.cr.NewConn(ctx, room)
if err != nil {
return errors.Wrap(err, "failed to create chrome connection to join the conference")
}
if err := webutil.WaitForQuiescence(ctx, conn, 45*time.Second); err != nil {
return errors.Wrapf(err, "failed to wait for %q to be loaded and achieve quiescence", room)
}
return nil
}
// allowPerm allows camera, microphone and notification if browser asks for the permissions.
allowPerm := func(ctx context.Context) error {
allowButton := nodewith.Name("Allow").Role(role.Button)
dismissButton := nodewith.Name("Dismiss").Role(role.Button)
avPerm := nodewith.NameRegex(regexp.MustCompile(".*Use your (microphone|camera).*")).ClassName("RootView").Role(role.AlertDialog).First()
notiPerm := nodewith.NameContaining("Show notifications").ClassName("RootView").Role(role.AlertDialog)
for _, step := range []struct {
name string
finder *nodewith.Finder
button *nodewith.Finder
}{
{"dismiss permission prompt", dismissButton, dismissButton},
{"allow microphone and camera", avPerm, allowButton.Ancestor(avPerm)},
{"allow notifications", notiPerm, allowButton.Ancestor(notiPerm)},
} {
if err := ui.WithTimeout(4 * time.Second).WaitUntilExists(step.finder)(ctx); err == nil {
// Immediately clicking the allow button sometimes doesn't work. Sleep 2 seconds.
if err := uiauto.Combine(step.name, ui.Sleep(2*time.Second), ui.LeftClick(step.button), ui.WaitUntilGone(step.finder))(ctx); err != nil {
return err
}
} else {
testing.ContextLog(ctx, "No action is required to ", step.name)
}
}
return nil
}
joinConf := func(ctx context.Context) error {
testing.ContextLog(ctx, "Join Conference")
joinNowButton := nodewith.Name("Join now").Role(role.Button)
homeScreenLink := nodewith.Name("Return to home screen").Role(role.Link)
if err := ui.WithTimeout(showJoinNowTimeout).LeftClickUntil(joinNowButton, ui.WaitUntilGone(homeScreenLink))(ctx); err != nil {
return errors.Wrapf(err, "failed to click button to join conference within %v", showJoinNowTimeout)
}
return nil
}
// enterAccount enter account email and password.
enterAccount := func(ctx context.Context) error {
kb, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize keyboard input")
}
defer kb.Close()
emailContent := nodewith.NameContaining(meetAccount).Editable()
emailField := nodewith.Name("Email or phone").Role(role.TextField)
emailFieldFocused := nodewith.Name("Email or phone").Role(role.TextField).Focused()
nextButton := nodewith.Name("Next").Role(role.Button)
passwordField := nodewith.Name("Enter your password").Role(role.TextField)
passwordFieldFocused := nodewith.Name("Enter your password").Role(role.TextField).Focused()
iAgree := nodewith.Name("I agree").Role(role.Button)
var actions []uiauto.Action
if err := ui.WaitUntilExists(emailContent)(ctx); err != nil {
// Email has not been entered into the text box yet.
actions = append(actions,
// Make sure text area is focused before typing. This is especially necessary on low-end DUTs.
ui.LeftClickUntil(emailField, ui.Exists(emailFieldFocused)),
kb.TypeAction(meetAccount),
)
}
actions = append(actions,
ui.LeftClick(nextButton),
// Make sure text area is focused before typing. This is especially necessary on low-end DUTs.
ui.LeftClickUntil(passwordField, ui.Exists(passwordFieldFocused)),
kb.TypeAction(conf.password),
ui.LeftClick(nextButton),
ui.LeftClickUntil(iAgree, ui.WithTimeout(1*time.Second).WaitUntilGone(iAgree)),
)
if err := uiauto.Combine("enter email and password",
actions...,
)(ctx); err != nil {
return errors.Wrap(err, "failed to enter account info")
}
return nil
}
// Using existed conference-test account for Google Meet testing,
// and add the test account if it doesn't add in the DUT before.
addMeetAccount := func(ctx context.Context) error {
useAnotherAccount := nodewith.Name("Use another account").First()
if err := ui.LeftClick(useAnotherAccount)(ctx); err != nil {
return errors.Wrap(err, `failed to click "Use another account"`)
}
addAccPrompt := nodewith.NameStartingWith("Add another Google Account for").Role(role.Heading)
if err := ui.WithTimeout(5 * time.Second).WaitUntilExists(addAccPrompt)(ctx); err == nil {
// Close all notifications to prevent them from covering the ok button.
if err := ash.CloseNotifications(ctx, tconn); err != nil {
return errors.Wrap(err, "failed to close notifications")
}
dontReminder := nodewith.Name("Don't remind me next time").Role(role.CheckBox)
if err := ui.LeftClick(dontReminder)(ctx); err != nil {
return errors.Wrap(err, `failed to click "Don't remind me next time"`)
}
signInWebArea := nodewith.Name("Sign in to add a Google account").Role(role.RootWebArea)
okBtn := nodewith.Name("OK").Role(role.Button).Ancestor(signInWebArea)
if err := ui.LeftClick(okBtn)(ctx); err != nil {
return errors.Wrap(err, `failed to click "OK" for new account prompt`)
}
}
if err := enterAccount(ctx); err != nil {
return err
}
if err := apps.Close(ctx, tconn, apps.Settings.ID); err != nil {
return errors.Wrap(err, "failed to close settings page")
}
chooseAnAccount := nodewith.Name("Choose an account").First()
if err := ui.WaitUntilExists(chooseAnAccount)(ctx); err != nil {
return errors.Wrap(err, `failed to find "Choose an account"`)
}
return nil
}
// switchUser switches to the account that will be used to join the Google meet.
switchUser := func(ctx context.Context) error {
testing.ContextLog(ctx, "Switch account")
switchAccount := nodewith.Name("Switch account").Role(role.Link)
meetAccountText := nodewith.Name(meetAccount).First()
chooseAnAccount := nodewith.Name("Choose an account").First()
if err := uiauto.Combine("switch account",
ui.LeftClickUntil(switchAccount, ui.Gone(switchAccount)),
ui.WaitUntilExists(chooseAnAccount),
)(ctx); err != nil {
return errors.Wrap(err, "failed to switch account")
}
// If meet account doesn't exist, add the account first.
if err := ui.WithTimeout(5 * time.Second).WaitUntilExists(meetAccountText)(ctx); err != nil {
testing.ContextLogf(ctx, "Add additional account %s to existing account", meetAccount)
if err := addMeetAccount(ctx); err != nil {
return errors.Wrapf(err, "failed to add account %s", meetAccount)
}
}
testing.ContextLog(ctx, "Select meet account ", meetAccount)
nextUI := nodewith.NameRegex(regexp.MustCompile("(Join now|Email or phone)")).First()
if err := uiauto.Combine("select account",
ui.WaitUntilExists(meetAccountText),
ui.WithTimeout(showJoinNowTimeout).LeftClickUntil(meetAccountText, ui.WaitUntilExists(nextUI)),
)(ctx); err != nil {
return errors.Wrapf(err, "failed to switch account to %s", meetAccount)
}
// Check if signing into the meet account is required.
emailField := nodewith.Name("Email or phone").Role(role.TextField)
if err := ui.Exists(emailField)(ctx); err == nil {
testing.ContextLog(ctx, "Signin is required when switching account")
if err := enterAccount(ctx); err != nil {
return errors.Wrapf(err, "failed to enter account %s", meetAccount)
}
}
// Wait for the "Join now" button.
joinNowButton := nodewith.Name("Join now").Role(role.Button)
if err := ui.WithTimeout(showJoinNowTimeout).WaitUntilExists(joinNowButton)(ctx); err != nil {
return errors.Wrapf(err, "Join now button didn't show for account %s", meetAccount)
}
return nil
}
// Checks the number of participants in the conference that
// for different tiers testing would ask for different size.
checkParticipantsNum := func(ctx context.Context) error {
meetWebArea := nodewith.NameContaining("Meet").Role(role.RootWebArea)
participant := nodewith.NameRegex(regexp.MustCompile(`^[\d]+$`)).Role(role.StaticText).Ancestor(meetWebArea)
// Some DUT models have poor performance. When joining
// a large conference (over 15 participants), it would take much time
// to render DOM elements. Set a longer timer here.
if err := ui.WithTimeout(2 * time.Minute).WaitUntilExists(participant)(ctx); err != nil {
return errors.Wrap(err, "failed to wait participant info")
}
participantInfo, err := ui.Info(ctx, participant)
if err != nil {
return errors.Wrap(err, "failed to get participant info")
}
strs := strings.Split(participantInfo.Name, " ")
num, err := strconv.ParseInt(strs[0], 10, 64)
if err != nil {
return errors.Wrap(err, "cannot parse number of participants")
}
// Check number of participants following this logic:
// - Class size room: >= 38 participants
// - Large size room: 16 ~ 17 participants
// - Small size room: 5 ~ 6 participants
// - One to one room: 2
roomSize := conf.roomSize
participantNumber := int(num)
if participantNumber == 1 {
return errors.Wrapf(err, "there are no other participants in the conference room, meeting participant number got %v; want %v", num, roomSize)
}
switch roomSize {
case ClassRoomSize:
if participantNumber < roomSize {
return errors.Wrapf(err, "meeting participant number got %v; want at least %v", num, roomSize)
}
case SmallRoomSize, LargeRoomSize:
if participantNumber != roomSize && participantNumber != roomSize+1 {
return errors.Wrapf(err, "meeting participant number got %v; want %v ~ %v", num, roomSize, roomSize+1)
}
case TwoRoomSize:
if participantNumber != roomSize {
return errors.Wrapf(err, "meeting participant number got %v; want %v", num, roomSize)
}
}
return nil
}
targetMeetAccount := nodewith.Name(conf.account).Role(role.StaticText)
return uiauto.Combine("join conference",
openConference,
allowPerm,
// Check if the login account is the one for google meet. If not, switch to google meet account.
ui.IfSuccessThen(ui.Gone(targetMeetAccount), switchUser),
joinConf,
// Sometimes participants number caught at the beginning is wrong, it will be correct after a while.
// Add retry to get the correct participants number.
ui.WithInterval(1*time.Second).Retry(5, checkParticipantsNum),
)(ctx)
}
// VideoAudioControl controls the video and audio during conference.
func (conf *GoogleMeetConference) VideoAudioControl(ctx context.Context) error {
// It may take some time to detect the microphone or camera button from the meet UI.
const detectButtonTime = 30 * time.Second
ui := uiauto.New(conf.tconn)
toggleVideo := func(ctx context.Context) error {
cameraButton := nodewith.NameRegex(regexp.MustCompile("Turn (on|off) camera.*")).Role(role.Button)
info, err := ui.WithTimeout(detectButtonTime).Info(ctx, cameraButton)
if err != nil {
return errors.Wrap(err, "failed to wait for the meet camera switch button to show")
}
if strings.HasPrefix(info.Name, "Turn on") {
testing.ContextLog(ctx, "Turn camera from off to on")
} else {
testing.ContextLog(ctx, "Turn camera from on to off")
}
if err := ui.LeftClick(cameraButton)(ctx); err != nil {
return errors.Wrap(err, "failed to switch camera")
}
return nil
}
toggleAudio := func(ctx context.Context) error {
microphoneButton := nodewith.NameRegex(regexp.MustCompile("Turn (on|off) microphone.*")).Role(role.Button)
info, err := ui.WithTimeout(detectButtonTime).Info(ctx, microphoneButton)
if err != nil {
return errors.Wrap(err, "failed to wait for the meet microphone switch button to show")
}
if strings.HasPrefix(info.Name, "Turn on") {
testing.ContextLog(ctx, "Turn microphone from off to on")
} else {
testing.ContextLog(ctx, "Turn microphone from on to off")
}
if err := ui.LeftClick(microphoneButton)(ctx); err != nil {
return errors.Wrap(err, "failed to switch microphone")
}
return nil
}
return uiauto.Combine("toggle video and audio",
// Remain in the state for 5 seconds after each action.
toggleVideo, ui.Sleep(5*time.Second),
toggleVideo, ui.Sleep(5*time.Second),
toggleAudio, ui.Sleep(5*time.Second),
toggleAudio, ui.Sleep(5*time.Second),
)(ctx)
}
// SwitchTabs switches the chrome tabs.
func (conf *GoogleMeetConference) SwitchTabs(ctx context.Context) error {
kb, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize keyboard input")
}
defer kb.Close()
testing.ContextLog(ctx, "Open wiki page")
const wikiURL = "https://www.wikipedia.org/"
wikiConn, err := conf.cr.NewConn(ctx, wikiURL)
if err != nil {
return errors.Wrap(err, "failed to open the wiki url")
}
defer wikiConn.Close()
// Switch tab.
if err := kb.Accel(ctx, "Ctrl+Tab"); err != nil {
return errors.Wrap(err, "failed to switch tab")
}
return nil
}
// ChangeLayout changes the conference UI layout.
func (conf *GoogleMeetConference) ChangeLayout(ctx context.Context) error {
tconn := conf.tconn
ui := uiauto.New(tconn)
// Close all notifications to prevent them from covering the print button.
if err := ash.CloseNotifications(ctx, tconn); err != nil {
return errors.Wrap(err, "failed to close otifications")
}
moreOptions := nodewith.Name("More options").First()
menu := nodewith.Name("Call options").Role(role.Menu)
changeLayoutButton := nodewith.Name("Change layout").Role(role.MenuItem)
changeLayoutPanel := nodewith.Name("Change layout").Role(role.Dialog)
closeButton := nodewith.Name("Close").Role(role.Button).Ancestor(changeLayoutPanel)
for _, mode := range []string{"Tiled", "Spotlight"} {
modeNode := nodewith.Name(mode).Role(role.RadioButton)
changeLayout := func(ctx context.Context) error {
testing.ContextLog(ctx, "Change layout to ", mode)
return uiauto.Combine("change layout to "+mode,
expandMenu(conf.tconn, moreOptions, menu, 433),
ui.LeftClick(changeLayoutButton),
ui.LeftClick(modeNode),
)(ctx)
}
if err := uiauto.Combine("change layout",
ui.Retry(3, changeLayout),
ui.LeftClick(closeButton), // Close the layout panel.
ui.Sleep(10*time.Second), // After applying new layout, give it 10 seconds for viewing before applying next one.
)(ctx); err != nil {
return err
}
}
return nil
}
// BackgroundBlurring blurs the background.
func (conf *GoogleMeetConference) BackgroundBlurring(ctx context.Context) error {
const (
blurBackground = "Blur your background"
skyBackground = "Blurry sky with purple horizon background"
turnOffBackground = "Turn off background effects"
)
ui := uiauto.New(conf.tconn)
changeBackground := func(background string) error {
moreOptions := nodewith.Name("More options").First()
menu := nodewith.Name("Call options").Role(role.Menu)
changeBackground := nodewith.Name("Change background").Role(role.MenuItem)
backgroundButton := nodewith.Name(background).First()
webArea := nodewith.NameContaining("Meet").Role(role.RootWebArea)
closeButton := nodewith.Name("Close").Role(role.Button).Ancestor(webArea)
testing.ContextLog(ctx, "Change background to ", background)
return uiauto.Combine("change background",
expandMenu(conf.tconn, moreOptions, menu, 433),
ui.LeftClick(changeBackground), // Open "Background" panel.
ui.WithTimeout(30*time.Second).LeftClick(backgroundButton),
ui.LeftClick(closeButton), // Close "Background" panel.
ui.Sleep(5*time.Second), // After applying new background, give it 5 seconds for viewing before applying next one.
)(ctx)
}
if err := ui.LeftClick(nodewith.Name("Pin yourself to your main screen."))(ctx); err != nil {
return errors.Wrap(err, "failed to switch to your main screen")
}
for _, background := range []string{blurBackground, skyBackground, turnOffBackground} {
if err := changeBackground(background); err != nil {
return err
}
}
return nil
}
// PresentSlide presents the slides to the conference.
func (conf *GoogleMeetConference) PresentSlide(ctx context.Context) error {
const slideTitle = "Untitled presentation - Google Slides"
tconn := conf.tconn
kb, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize keyboard input")
}
defer kb.Close()
deletePerformed := false
// slideCleanup switches to the slide page and deletes it.
slideCleanup := func(ctx context.Context) error {
if deletePerformed {
return nil
}
deletePerformed = true // Set it to true because we only try to delete once.
testing.ContextLog(ctx, "Switch to the slide to do cleanup")
if err := conf.switchToChromeTab(ctx, slideTitle); err != nil {
return errors.Wrap(err, "failed to switch tab to slide page")
}
testing.ContextLog(ctx, "Delete slide")
if err := deleteSlide(ctx, conf.tconn); err != nil {
return errors.Wrap(err, "failed to delete slide")
}
return nil
}
// Shorten the context to cleanup slide.
cleanUpSlideCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
testing.ContextLog(ctx, "Create a new Google Slides")
if err := newGoogleSlides(ctx, conf.cr, false); err != nil {
return err
}
// Delete slide if any error occures.
defer func() {
if err := slideCleanup(cleanUpSlideCtx); err != nil {
// Only log the error.
testing.ContextLog(ctx, "Failed to clean up the slide: ", err)
}
}()
testing.ContextLog(ctx, "Switch to conference page")
if err := conf.switchToChromeTab(ctx, "Meet"); err != nil {
return errors.Wrap(err, "failed to switch tab to conference page")
}
testing.ContextLog(ctx, "Start to share screen")
if err := conf.shareScreen(ctx, tconn, false); err != nil {
return err
}
testing.ContextLog(ctx, "Switch to the slide")
if err := conf.switchToChromeTab(ctx, slideTitle); err != nil {
return errors.Wrap(err, "failed to switch tab to slide page")
}
testing.ContextLog(ctx, "Start present slide")
if err := presentSlide(ctx, tconn, kb); err != nil {
return err
}
testing.ContextLog(ctx, "Edit slide")
if err := editSlide(ctx, tconn, kb); err != nil {
return errors.Wrap(err, "failed to edit slide when leave presentation mode")
}
if err := slideCleanup(ctx); err != nil {
// Only log the error.
testing.ContextLog(ctx, "Failed to clean up the slide: ", err)
}
testing.ContextLog(ctx, "Switch to conference page")
if err := conf.switchToChromeTab(ctx, "Meet"); err != nil {
return errors.Wrap(err, "failed to switch tab to conference page")
}
return nil
}
// ExtendedDisplayPresenting presents the screen on dextended display.
func (conf *GoogleMeetConference) ExtendedDisplayPresenting(ctx context.Context) error {
const slideTitle = "Untitled presentation - Google Slides"
tconn := conf.tconn
ui := uiauto.New(tconn)
kb, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize keyboard input")
}
defer kb.Close()
moveConferenceTab := func(ctx context.Context) error {
return uiauto.Combine("move conference to exteneded display",
kb.AccelAction("Alt+Tab"),
ui.Sleep(400*time.Millisecond),
kb.AccelAction("Search+Alt+M"),
ui.Sleep(400*time.Millisecond),
)(ctx)
}
deletePerformed := false
// slideCleanup switches to the slide page and deletes it.
slideCleanup := func(ctx context.Context) error {
if deletePerformed {
return nil
}
deletePerformed = true // Set it to true because we only try to delete once.
webArea := nodewith.Name(slideTitle).Role(role.RootWebArea)
if err := ui.Exists(webArea); err != nil {
testing.ContextLog(ctx, "Switch to the slide browser tab to do cleanup")
if err := kb.Accel(ctx, "Alt+Tab"); err != nil {
return errors.Wrap(err, "failed to press Alt+Tab to switch to slide page")
}
}
testing.ContextLog(ctx, "Delete slide")
if err := deleteSlide(ctx, conf.tconn); err != nil {
return errors.Wrap(err, "failed to delete slide")
}
return nil
}
// Shorten the context to cleanup slide.
cleanUpSlideCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
testing.ContextLog(ctx, "Create a new Google Slides")
if err := newGoogleSlides(ctx, conf.cr, true); err != nil {
return err
}
// Delete slide if any error occures.
defer func() {
if err := slideCleanup(cleanUpSlideCtx); err != nil {
// Only log the error.
testing.ContextLog(ctx, "Failed to clean up the slide: ", err)
}
}()
testing.ContextLog(ctx, "Switch to conference page")
if err := kb.Accel(ctx, "Alt+Tab"); err != nil {
return errors.Wrap(err, "failed to press Alt+Tab to switch to conference page")
}
testing.ContextLog(ctx, "Start to share screen")
if err := conf.shareScreen(ctx, tconn, true); err != nil {
return err
}
testing.ContextLog(ctx, "Move conference tab to extended display")
if err := moveConferenceTab(ctx); err != nil {
return err
}
testing.ContextLog(ctx, "Switch to the slide")
if err := kb.Accel(ctx, "Alt+Tab"); err != nil {
return errors.Wrap(err, "failed to press Alt+Tab to switch to slide page")
}
testing.ContextLog(ctx, "Start present slide")
if err := presentSlide(ctx, tconn, kb); err != nil {
return err
}
testing.ContextLog(ctx, "Edit slide")
if err := editSlide(ctx, tconn, kb); err != nil {
return errors.Wrap(err, "failed to edit slide when leave presentation mode")
}
if err := slideCleanup(ctx); err != nil {
// Only log the error.
testing.ContextLog(ctx, "Failed to clean up the slide: ", err)
}
testing.ContextLog(ctx, "Switch to conference page")
if err := kb.Accel(ctx, "Alt+Tab"); err != nil {
return errors.Wrap(err, "failed to press Alt+Tab to switch to conference page")
}
return nil
}
// StopPresenting stops the presentation mode.
func (conf *GoogleMeetConference) StopPresenting(ctx context.Context) error {
ui := uiauto.New(conf.tconn)
meetWebArea := nodewith.NameContaining("Meet").Role(role.RootWebArea)
// There are two "Stop presenting" buttons on the screen with the same ancestor, role and name that we can't use unique finder.
stopPresentingButton := nodewith.Name("Stop presenting").Role(role.Button).Ancestor(meetWebArea).First()
testing.ContextLog(ctx, "Stop presenting")
return ui.LeftClickUntil(stopPresentingButton, ui.WithTimeout(3*time.Second).WaitUntilGone(stopPresentingButton))(ctx)
}
// End ends the conference.
func (conf *GoogleMeetConference) End(ctx context.Context) error {
return cuj.CloseAllWindows(ctx, conf.tconn)
}
var _ Conference = (*GoogleMeetConference)(nil)
// switchToChromeTab switch to the given chrome tab.
//
// TODO: Merge to cuj.UIActionHandler and introduce UIActionHandler in this test. See
// https://chromium-review.googlesource.com/c/chromiumos/platform/tast-tests/+/2779315/
func (conf *GoogleMeetConference) switchToChromeTab(ctx context.Context, tabName string) error {
ui := uiauto.New(conf.tconn)
if conf.tabletMode {
// If in tablet mode, it should toggle tab strip to show tab list.
if err := ui.LeftClick(nodewith.NameContaining("toggle tab strip").Role(role.Button).First())(ctx); err != nil {
return err
}
}
return ui.LeftClick(nodewith.NameContaining(tabName).Role(role.Tab))(ctx)
}
// shareScreen share screen from google meet.
func (conf *GoogleMeetConference) shareScreen(ctx context.Context, tconn *chrome.TestConn, extendedDisplay bool) error {
const slideTitle = "Untitled presentation - Google Slides"
ui := uiauto.New(tconn)
meetWebArea := nodewith.NameContaining("Meet").Role(role.RootWebArea)
presentNowButton := nodewith.Name("Present now").Ancestor(meetWebArea)
aWindow := nodewith.Name("A window").Role(role.MenuItem)
presentWindow := nodewith.ClassName("DesktopMediaSourceView").First()
shareButton := nodewith.Name("Share").Role(role.Button)
// There are two "Stop presenting" buttons on the screen with the same ancestor, role and name that we can't use unique finder.
stopPresenting := nodewith.Name("Stop presenting").Role(role.Button).Ancestor(meetWebArea).First()
if extendedDisplay {
presentWindow = nodewith.ClassName("DesktopMediaSourceView").NameRegex(regexp.MustCompile("My Drive|" + slideTitle))
}
return uiauto.Combine("share screen",
ui.LeftClick(presentNowButton),
ui.WithTimeout(time.Minute).LeftClickUntil(aWindow, ui.WaitUntilExists(presentWindow)),
ui.LeftClick(presentWindow),
ui.LeftClickUntil(shareButton, ui.Gone(shareButton)),
ui.WithTimeout(time.Minute).WaitUntilExists(stopPresenting),
)(ctx)
}
// NewGoogleMeetConference creates Google Meet conference room instance which implements Conference interface.
func NewGoogleMeetConference(cr *chrome.Chrome, tconn *chrome.TestConn, tabletMode bool,
roomSize int, account, password string) *GoogleMeetConference {
return &GoogleMeetConference{
cr: cr,
tconn: tconn,
tabletMode: tabletMode,
roomSize: roomSize,
account: account,
password: password,
}
}
// expandMenu returns a function that clicks the button and waits for the menu to expand to the given height.
// This function is useful when the target menu will expand to its full size with animation. On Low end DUTs
// the expansion animation might stuck for some time. The node might have returned a stable location if
// checking with a fixed interval before the animiation completes. This function ensures animation completes
// by checking the menu height.
func expandMenu(tconn *chrome.TestConn, button, menu *nodewith.Finder, height int) action.Action {
ui := uiauto.New(tconn)
startTime := time.Now()
return func(ctx context.Context) error {
if err := ui.LeftClick(button)(ctx); err != nil {
return errors.Wrap(err, "failed to click button")
}
return testing.Poll(ctx, func(ctx context.Context) error {
menuInfo, err := ui.Info(ctx, menu)
if err != nil {
return errors.Wrap(err, "failed to get menu info")
}
if menuInfo.Location.Height < height {
return errors.Errorf("got menu height %d, want %d", menuInfo.Location.Height, height)
}
// Examine this log regularly to see how fast the menu is expanded and determine if
// we still need to keep this expandMenu() function.
testing.ContextLog(ctx, "Menu expanded to full height in ", time.Now().Sub(startTime))
return nil
}, &testing.PollOptions{Timeout: 15 * time.Second, Interval: time.Second})
}
}