blob: 64ee4d19823686f7c9d1e2a020444e2d650304fd [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/errors"
"chromiumos/tast/local/bundles/cros/ui/cuj"
"chromiumos/tast/local/chrome"
"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"
)
// ZoomConference implements the Conference interface.
type ZoomConference struct {
cr *chrome.Chrome
tconn *chrome.TestConn
tabletMode bool
roomSize int
account string
}
// Join joins a new conference room.
func (conf *ZoomConference) Join(ctx context.Context, room string) error {
ui := uiauto.New(conf.tconn)
openZoomAndSignIn := func(ctx context.Context) error {
const zoomURL = "https://zoom.us/"
conn, err := conf.cr.NewConn(ctx, zoomURL)
if err != nil {
return errors.Wrap(err, "failed to open the zoom website")
}
defer conn.Close()
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)
}
if err := ui.WaitUntilExists(nodewith.Name("SIGN IN").Role(role.Link))(ctx); err == nil {
testing.ContextLog(ctx, "Start to sign in")
if err := conn.Navigate(ctx, "https://zoom.us/google_oauth_signin"); err != nil {
return err
}
account := nodewith.Name(conf.account).First()
profilePicture := nodewith.Name("Profile picture").First()
// If the DUT has only one account, it would login to profile page directly.
// Otherwise, it would show list of accounts.
if err := uiauto.Combine("sign in",
ui.IfSuccessThen(ui.WithTimeout(5*time.Second).WaitUntilExists(account),
ui.LeftClickUntil(account, ui.Gone(account))),
ui.WaitUntilExists(profilePicture),
)(ctx); err != nil {
return err
}
} else {
testing.ContextLog(ctx, "It has been signed in")
}
if err := conn.Navigate(ctx, room); err != nil {
return err
}
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)
cameraPerm := nodewith.NameRegex(regexp.MustCompile("Use your camera")).ClassName("RootView").Role(role.AlertDialog).First()
microphonePerm := nodewith.NameRegex(regexp.MustCompile("Use your microphone")).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
}{
{"allow notifications", notiPerm, allowButton.Ancestor(notiPerm)},
{"allow microphone", microphonePerm, allowButton.Ancestor(microphonePerm)},
{"allow camera", cameraPerm, allowButton.Ancestor(cameraPerm)},
} {
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
}
// Checks the number of participants in the conference that
// for different tiers testing would ask for different size
checkParticipantsNum := func(ctx context.Context) error {
participant := nodewith.NameContaining("open the participants list pane").Role(role.Button)
participantInfo, err := ui.Info(ctx, participant)
if err != nil {
return errors.Wrap(err, "failed to get participant info")
}
testing.ContextLog(ctx, "Get participant info: ", participantInfo.Name)
strs := strings.Split(participantInfo.Name, "[")
strs = strings.Split(strs[1], "]")
num, err := strconv.ParseInt(strs[0], 10, 64)
if err != nil {
return errors.Wrap(err, "cannot parse number of participants")
}
if int(num) != conf.roomSize {
return errors.Wrapf(err, "meeting participant number is %d but %d is expected", num, conf.roomSize)
}
return nil
}
startVideo := func(ctx context.Context) error {
testing.ContextLog(ctx, "Start video")
cameraButton := nodewith.NameRegex(regexp.MustCompile("(stop|start) sending my video")).Role(role.Button)
startVideoButton := nodewith.Name("start sending my video").Role(role.Button)
stopVideoButton := nodewith.Name("stop sending my video").Role(role.Button)
webArea := nodewith.NameContaining("Zoom Meeting").Role(role.RootWebArea)
// Some DUTs start playing video for the first time.
// If there is a stop video button, do nothing.
return uiauto.Combine("start video",
allowPerm,
// Click web area in order to make the camera button reappear.
ui.IfSuccessThen(ui.Gone(cameraButton), ui.LeftClick(webArea)),
ui.WaitUntilExists(cameraButton),
ui.IfSuccessThen(ui.Exists(startVideoButton),
ui.LeftClickUntil(startVideoButton, ui.WithTimeout(time.Second).WaitUntilGone(startVideoButton))),
ui.WaitUntilExists(stopVideoButton),
)(ctx)
}
testing.ContextLog(ctx, "Join conference")
joinFromYourBrowser := nodewith.Name("Join from Your Browser").Role(role.StaticText)
joinButton := nodewith.Name("Join").Role(role.Button)
joinAudioButton := nodewith.Name("Join Audio by Computer").Role(role.Button)
// It seems zoom has different UI versions. One of the zoom version will open a new tab.
// Need to close the initial zoom web page to avoid problems when switching tabs.
closeLaunchMeetingTab := func(ctx context.Context) error {
zoomTab := nodewith.Name("Launch Meeting - Zoom").Role(role.Tab)
closeButton := nodewith.Name("Close").Role(role.Button).Ancestor(zoomTab)
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
}
}
if err := ui.LeftClick(closeButton)(ctx); err == nil {
testing.ContextLog(ctx, `Close "Launch Meeting - Zoom" tab`)
}
return nil
}
return uiauto.Combine("join conference",
openZoomAndSignIn,
ui.LeftClick(joinFromYourBrowser),
ui.WithTimeout(time.Minute).WaitUntilExists(joinButton),
ui.LeftClickUntil(joinButton, ui.WithTimeout(1*time.Second).WaitUntilGone(joinButton)),
ui.WithTimeout(30*time.Second).WaitUntilExists(joinAudioButton),
checkParticipantsNum,
ui.LeftClickUntil(joinAudioButton, ui.WithTimeout(time.Second).WaitUntilGone(joinAudioButton)),
// Launch Meeting page is useless so close it.
closeLaunchMeetingTab,
// Start video requires camera permission.
// Allow permission doesn't succeed every time. So add retry here.
ui.Retry(3, startVideo),
)(ctx)
}
// VideoAudioControl controls the video and audio during conference.
func (conf *ZoomConference) VideoAudioControl(ctx context.Context) error {
ui := uiauto.New(conf.tconn)
toggleVideo := func(ctx context.Context) error {
cameraButton := nodewith.NameRegex(regexp.MustCompile("(stop|start) sending my video")).Role(role.Button)
info, err := ui.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, "start") {
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("(mute|unmute) my microphone")).Role(role.Button)
info, err := ui.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, "unmute") {
testing.ContextLog(ctx, "Turn microphone from mute to unmute")
} else {
testing.ContextLog(ctx, "Turn microphone from unmute to mute")
}
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 *ZoomConference) 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()
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 *ZoomConference) ChangeLayout(ctx context.Context) error {
const (
view = "View"
speaker = "Speaker View"
gallery = "Gallery View"
)
ui := uiauto.New(conf.tconn)
viewButton := nodewith.Name(view).First()
webArea := nodewith.NameContaining("Zoom Meeting").Role(role.RootWebArea)
if err := uiauto.Combine("check view button",
ui.LeftClick(webArea),
ui.WaitUntilExists(viewButton),
)(ctx); err != nil {
// Some DUTs don't show 'View' button
testing.ContextLog(ctx, "This DUT doesn't show View button and layout will not be changed")
return nil
}
for _, mode := range []string{speaker, gallery} {
modeNode := nodewith.Name(mode).Role(role.MenuItem)
if err := uiauto.Combine("change layout to '"+mode+"'",
ui.LeftClick(webArea),
ui.LeftClick(viewButton),
ui.LeftClick(modeNode),
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 *ZoomConference) BackgroundBlurring(ctx context.Context) error {
// Zoom doesn't support background change in web. The common conference test will call this interface
// and return nil to make sure the test logic passes for zoom.
// TODO: Add detailed implementation when this feature is available in zoom web.
return nil
}
// ExtendedDisplayPresenting presents the screen on dextended display.
func (conf *ZoomConference) ExtendedDisplayPresenting(_ context.Context) error {
// Not required by test case yet.
return errors.New("extended display presenting for zoom is not implemented")
}
// PresentSlide presents the slides to the conference.
func (conf *ZoomConference) PresentSlide(ctx context.Context) error {
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()
shareScreen := func(ctx context.Context) error {
webArea := nodewith.NameContaining("Zoom Meeting").Role(role.RootWebArea)
shareScreenButton := nodewith.Name("Share Screen").Role(role.Button)
presentWindow := nodewith.ClassName("DesktopMediaSourceView").First()
shareButton := nodewith.Name("Share").Role(role.Button)
stopShareButton := nodewith.Name("Stop Share").Role(role.Button)
return uiauto.Combine("share Screen",
ui.LeftClick(webArea),
ui.LeftClickUntil(shareScreenButton, ui.WithTimeout(time.Second).WaitUntilExists(presentWindow)),
ui.LeftClick(presentWindow),
ui.LeftClick(shareButton),
ui.WaitUntilExists(stopShareButton),
)(ctx)
}
testing.ContextLog(ctx, "Create a new Google Slides")
if err := newGoogleSlides(ctx, conf.cr, false); err != nil {
return err
}
testing.ContextLog(ctx, "Switch back to conference page")
if err := conf.switchToChromeTab(ctx, "Zoom"); err != nil {
return errors.Wrap(err, "failed to switch tab to conference page")
}
testing.ContextLog(ctx, "Start to share screen")
if err := shareScreen(ctx); err != nil {
return errors.Wrap(err, "failed to share screen")
}
testing.ContextLog(ctx, "Switch to the slide")
if err := conf.switchToChromeTab(ctx, "Untitled presentation - Google Slides"); 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")
}
testing.ContextLog(ctx, "Switch back to conference page")
if err := conf.switchToChromeTab(ctx, "Zoom"); err != nil {
return errors.Wrap(err, "failed to switch tab to conference page")
}
return nil
}
// StopPresenting stops the presentation mode.
func (conf *ZoomConference) StopPresenting(ctx context.Context) error {
ui := uiauto.New(conf.tconn)
stopShareButton := nodewith.Name("Stop Share").Role(role.Button)
testing.ContextLog(ctx, "Stop share")
return ui.LeftClickUntil(stopShareButton, ui.Gone(stopShareButton))(ctx)
}
// End ends the conference.
func (conf *ZoomConference) End(ctx context.Context) error {
return cuj.CloseAllWindows(ctx, conf.tconn)
}
var _ Conference = (*ZoomConference)(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 *ZoomConference) 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)
}
// NewZoomConference creates Zoom conference room instance which implements Conference interface.
func NewZoomConference(cr *chrome.Chrome, tconn *chrome.TestConn, tabletMode bool,
roomSize int, account string) *ZoomConference {
return &ZoomConference{
cr: cr,
tconn: tconn,
tabletMode: tabletMode,
roomSize: roomSize,
account: account,
}
}