blob: ea8afee4a563facb28fbe8a46e372d7b0112c866 [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 videocuj contains the test code for VideoCUJ.
package videocuj
import (
"context"
"path/filepath"
"time"
"chromiumos/tast/common/perf"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/apps"
"chromiumos/tast/local/arc"
"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/lacros/lacrosfixt"
"chromiumos/tast/local/chrome/uiauto"
"chromiumos/tast/local/chrome/uiauto/faillog"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/chrome/uiauto/role"
"chromiumos/tast/local/chrome/webutil"
"chromiumos/tast/local/input"
"chromiumos/tast/testing"
)
const (
// YoutubeWeb indicates to test against Youtube web.
YoutubeWeb = "YoutubeWeb"
// YoutubeApp indicates to test against Youtube app.
YoutubeApp = "YoutubeApp"
)
// TestResources holds the cuj test resources passed in from main test case.
type TestResources struct {
Cr *chrome.Chrome
LFixtVal lacrosfixt.FixtValue
Tconn *chrome.TestConn
A *arc.ARC
Kb *input.KeyboardEventWriter
UIHandler cuj.UIActionHandler
}
// TestParams holds the cuj test parameters passed in from main test case.
type TestParams struct {
OutDir string
App string
TabletMode bool
Tier cuj.Tier
ExtendedDisplay bool
}
// VideoApp declares video operation.
type VideoApp interface {
// OpenAndPlayVideo opens a video.
OpenAndPlayVideo(ctx context.Context) error
// EnterFullscreen switches video to fullscreen.
EnterFullscreen(ctx context.Context) error
// PauseAndPlayVideo verifies video playback.
PauseAndPlayVideo(ctx context.Context) error
// Close closes the resources related to video.
Close(ctx context.Context)
}
// VideoSrc struct defines video src for testing.
type VideoSrc struct {
URL string
Title string
// Quality is the string that test will look for in youtube
// "Settings / Quality" menu to change video playback quality.
Quality string
}
var basicVideoSrc = []VideoSrc{
{
cuj.YoutubeGoogleTVVideoURL,
"Chris Paul | Watch With Me | Google TV",
"1080p",
},
{
cuj.YoutubeDeveloperKeynoteVideoURL,
"Developer Keynote (Google I/O '21) - American Sign Language",
"720p60",
},
{
cuj.YoutubeStadiaGDCVideoURL,
"Stadia GDC 2019 Gaming Announcement",
"1080p60",
},
}
var premiumVideoSrc = []VideoSrc{
{
cuj.YoutubeStadiaGDCVideoURL,
"Stadia GDC 2019 Gaming Announcement",
"2160p60",
},
}
// Run runs the VideoCUJ test.
func Run(ctx context.Context, resources TestResources, param TestParams) error {
var (
cr = resources.Cr
tconn = resources.Tconn
a = resources.A
kb = resources.Kb
uiHandler = resources.UIHandler
outDir = param.OutDir
appName = param.App
tabletMode = param.TabletMode
tier = param.Tier
extendedDisplay = param.ExtendedDisplay
)
testing.ContextLogf(ctx, "Run app appName: %s tabletMode: %t, extendedDisplay: %t", appName, tabletMode, extendedDisplay)
if appName == YoutubeApp {
if err := installYoutubeApp(ctx, tconn, a); err != nil {
return errors.Wrapf(err, "failed to install %s", youtubePkg)
}
}
tabChecker, err := cuj.NewTabCrashChecker(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to create TabCrashChecker")
}
ui := uiauto.New(tconn)
// Give 10 seconds to set initial settings. It is critical to ensure
// cleanupSetting can be executed with a valid context so it has its
// own cleanup context from other cleanup functions. This is to avoid
// other cleanup functions executed earlier to use up the context time.
cleanupSettingsCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 10*time.Second)
defer cancel()
cleanupSetting, err := cuj.InitializeSetting(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to set initial settings")
}
defer cleanupSetting(cleanupSettingsCtx)
testing.ContextLog(ctx, "Start to get browser start time")
l, browserStartTime, err := cuj.GetBrowserStartTime(ctx, tconn, true, tabletMode, resources.LFixtVal != nil)
if err != nil {
return errors.Wrap(err, "failed to get browser start time")
}
// If lacros exists, close lacros finally.
if l != nil {
defer l.Close(ctx)
}
br := cr.Browser()
tconns := []*chrome.TestConn{tconn}
var bTconn *chrome.TestConn
if resources.LFixtVal != nil {
br = l.Browser()
bTconn, err = l.TestAPIConn(ctx)
if err != nil {
return errors.Wrap(err, "failed to create test API conn")
}
tconns = append(tconns, bTconn)
}
videoSources := basicVideoSrc
if tier == cuj.Premium {
videoSources = premiumVideoSrc
}
// Give 5 seconds to clean up device objects connected to UI Automator server resources.
cleanupDeviceCtx := ctx
ctx, cancel = ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
d, err := a.NewUIDevice(ctx)
if err != nil {
return errors.Wrap(err, "failed to create new ARC device")
}
defer func(ctx context.Context) {
if d.Alive(ctx) {
testing.ContextLog(ctx, "UI device is still alive")
d.Close(ctx)
}
}(cleanupDeviceCtx)
openGmailWeb := func(ctx context.Context) (*chrome.Conn, error) {
// If there's a lacros browser, bring it to active.
lacrosWin, err := ash.FindWindow(ctx, tconn, func(w *ash.Window) bool {
return w.WindowType == ash.WindowTypeLacros
})
if err != nil && err != ash.ErrWindowNotFound {
return nil, errors.Wrap(err, "failed to find lacros window")
}
if err == nil {
if err := lacrosWin.ActivateWindow(ctx, tconn); err != nil {
return nil, errors.Wrap(err, "failed to activate lacros window")
}
}
conn, err := uiHandler.NewChromeTab(ctx, br, cuj.GmailURL, true)
if err != nil {
return conn, errors.Wrap(err, "failed to open gmail web page")
}
if err := webutil.WaitForQuiescence(ctx, conn, 2*time.Minute); err != nil {
return conn, errors.Wrap(err, "failed to wait for gmail page to finish loading")
}
// YouTube sometimes pops up a prompt to notice users how to operate YouTube
// if there're new features. Dismiss prompt if it exist.
gotItPrompt := nodewith.Name("Got it").Role(role.Button)
uiauto.IfSuccessThen(
ui.WaitUntilExists(gotItPrompt),
uiHandler.ClickUntil(
gotItPrompt,
ui.WithTimeout(2*time.Second).WaitUntilGone(gotItPrompt),
),
)
return conn, nil
}
// Give 5 seconds to cleanup recorder.
cleanupRecorderCtx := ctx
ctx, cancel = ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
recorder, err := cuj.NewRecorder(ctx, cr, a, cuj.RecorderOptions{}, cuj.MetricConfigs(tconns)...)
if err != nil {
return errors.Wrap(err, "failed to create a recorder")
}
defer recorder.Close(cleanupRecorderCtx)
for _, videoSource := range videoSources {
// Repeat the run for different video source.
if err = recorder.Run(ctx, func(ctx context.Context) (retErr error) {
var videoApp VideoApp
switch appName {
case YoutubeWeb:
videoApp = NewYtWeb(br, tconn, kb, videoSource, extendedDisplay, ui, uiHandler)
case YoutubeApp:
videoApp = NewYtApp(tconn, kb, a, d, videoSource)
}
// Give time to cleanup videoApp resources.
cleanupResourceCtx := ctx
ctx, cancel = ctxutil.Shorten(ctx, 15*time.Second)
defer cancel()
defer func(ctx context.Context) {
// Make sure to close the arc UI device before calling the function. Otherwise uiautomator might have errors.
if appName == YoutubeApp && retErr != nil {
if err := d.Close(ctx); err != nil {
testing.ContextLog(ctx, "Failed to close ARC UI device: ", err)
}
a.DumpUIHierarchyOnError(ctx, filepath.Join(outDir, "arc"), func() bool { return retErr != nil })
}
faillog.DumpUITreeWithScreenshotOnError(ctx, outDir, func() bool { return retErr != nil }, cr, "ui_dump")
if appName == YoutubeWeb && resources.LFixtVal != nil {
// For lacros, leave a new tab to keep the browser alive for further testing.
if err := cuj.KeepNewTab(ctx, bTconn); err != nil {
testing.ContextLog(ctx, "Failed to keep new tab: ", err)
}
} else {
videoApp.Close(ctx)
}
}(cleanupResourceCtx)
if err := videoApp.OpenAndPlayVideo(ctx); err != nil {
return errors.Wrapf(err, "failed to open %s", appName)
}
// Play video at fullscreen.
if err := videoApp.EnterFullscreen(ctx); err != nil {
return errors.Wrap(err, "failed to play video in fullscreen")
}
// Let the video play in fullscreen for some time.
if err := testing.Sleep(ctx, time.Second); err != nil {
return errors.Wrap(err, "failed to sleep")
}
// Open Gmail web.
testing.ContextLog(ctx, "Open Gmail web")
gConn, err := openGmailWeb(ctx)
if err != nil {
return errors.Wrap(err, "failed to open Gmail website")
}
if appName == YoutubeApp && resources.LFixtVal != nil {
// For Youtube App, the current lacros with Gmail is the only lacros window.
// Leave a new tab to keep the browser alive for further testing.
defer func() {
gConn.Close()
if err := cuj.KeepNewTab(ctx, bTconn); err != nil {
testing.ContextLog(ctx, "Failed to keep new tab: ", err)
}
}()
} else {
defer gConn.Close()
defer gConn.CloseTarget(cleanupResourceCtx)
}
ytApp, ok := videoApp.(*YtApp)
// Only do PiP testing for YT APP and when logged in as premium user.
if ok && ytApp.isPremiumAccount() {
if err = ytApp.checkYoutubeAppPIP(ctx); err != nil {
return errors.Wrap(err, "youtube app smaller video preview window is not shown")
}
}
// Switch back to video playing.
if tabletMode && appName == YoutubeApp {
if err := uiHandler.SwitchToAppWindow("YouTube")(ctx); err != nil {
return errors.Wrap(err, "failed to click app from Hotseat")
}
if err := kb.Accel(ctx, "F4"); err != nil {
return errors.Wrap(err, "failed to type the tab key")
}
} else {
if err := uiHandler.SwitchWindow()(ctx); err != nil {
return errors.Wrap(err, "failed to switch back to video playing")
}
}
// Pause and resume video playback.
if err := videoApp.PauseAndPlayVideo(ctx); err != nil {
return errors.Wrap(err, "failed to pause and play video")
}
if extendedDisplay {
if err := moveGmailWindow(ctx, tconn, resources); err != nil {
return errors.Wrap(err, "failed to move Gmail window between main display and extended display")
}
if appName == YoutubeWeb {
if err := moveYTWebWindow(ctx, tconn, resources); err != nil {
return errors.Wrap(err, "failed to move YT Web window to internal display")
}
}
}
// Before recording the metrics, check if there is any tab crashed.
if err := tabChecker.Check(ctx); err != nil {
return errors.Wrap(err, "tab renderer crashed")
}
return nil
}); err != nil {
return errors.Wrapf(err, "failed to run %q video playback", appName)
}
}
pv := perf.NewValues()
// We'll collect Browser.StartTime for both YouTube-Web and YouTube-App
pv.Set(perf.Metric{
Name: "Browser.StartTime",
Unit: "ms",
Direction: perf.SmallerIsBetter,
Multiple: true,
}, float64(browserStartTime.Milliseconds()))
if appName == YoutubeApp {
pv.Set(perf.Metric{
Name: "Apps.StartTime",
Unit: "ms",
Direction: perf.SmallerIsBetter,
}, float64(appStartTime.Milliseconds()))
}
// Use a short timeout value so it can return fast in case of failure.
recordCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
if err := recorder.Record(recordCtx, pv); err != nil {
return errors.Wrap(err, "failed to record the performance metrics")
}
if err := pv.Save(outDir); err != nil {
return errors.Wrap(err, "failed to save performance metrics")
}
if err := recorder.SaveHistograms(outDir); err != nil {
return errors.Wrap(err, "failed to save histogram raw data")
}
return nil
}
func waitWindowStateFullscreen(ctx context.Context, tconn *chrome.TestConn, winID int) error {
if err := ash.WaitForCondition(ctx, tconn, func(w *ash.Window) bool {
return w.ID == winID && w.State == ash.WindowStateFullscreen
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return errors.Wrap(err, "failed to wait for fullscreen")
}
return nil
}
func getWindowID(ctx context.Context, tconn *chrome.TestConn) (int, error) {
all, err := ash.GetAllWindows(ctx, tconn)
if err != nil {
return -1, errors.Wrap(err, "failed to get all windows")
}
if len(all) != 1 {
return -1, errors.Errorf("expect 1 window, got %d", len(all))
}
return all[0].ID, nil
}
// moveGmailWindow switches Gmail to the extended display and switches back to internal display.
func moveGmailWindow(ctx context.Context, tconn *chrome.TestConn, testRes TestResources) error {
return uiauto.Combine("switch to gmail and move it between two displays",
testRes.UIHandler.SwitchWindow(),
cuj.SwitchWindowToDisplay(ctx, tconn, testRes.Kb, true), // Move to external display.
uiauto.Sleep(2*time.Second), // Keep the window in external display for 2 second.
cuj.SwitchWindowToDisplay(ctx, tconn, testRes.Kb, false), // Move to internal display.
)(ctx)
}
// moveYTWebWindow switches Youtube Web to the internal display.
func moveYTWebWindow(ctx context.Context, tconn *chrome.TestConn, testRes TestResources) error {
return uiauto.Combine("switch to YT Web and move it to internal display",
testRes.UIHandler.SwitchWindow(),
cuj.SwitchWindowToDisplay(ctx, tconn, testRes.Kb, false), // Move to internal display.
)(ctx)
}
func installYoutubeApp(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC) error {
cleanupCtx := ctx
ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
device, err := a.NewUIDevice(ctx)
if err != nil {
return errors.Wrap(err, "failed to set up ARC device")
}
defer device.Close(cleanupCtx)
installErr := playstore.InstallOrUpdateApp(ctx, a, device, youtubePkg, &playstore.Options{TryLimit: -1})
if err := apps.Close(cleanupCtx, tconn, apps.PlayStore.ID); err != nil {
// Leaving PlayStore open will impact the logic of detecting fullscreen
// mode in this test case. We fail the test if this happens.
return errors.Wrap(err, "failed to close Play Store")
}
return installErr
}