blob: 4f09a581b1e705497e3a57a777f81274742cd647 [file] [log] [blame]
// Copyright 2019 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 cca provides utilities to interact with Chrome Camera App.
package cca
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/mafredri/cdp/protocol/target"
"chromiumos/tast/errors"
"chromiumos/tast/local/apps"
"chromiumos/tast/local/camera/testutil"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/cryptohome"
"chromiumos/tast/local/screenshot"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
// Facing is camera facing from JavaScript VideoFacingModeEnum.
type Facing string
const (
// FacingBack is the constant string from JavaScript VideoFacingModeEnum.
FacingBack Facing = "environment"
// FacingFront is the constant string from JavaScript VideoFacingModeEnum.
FacingFront = "user"
// FacingExternal is the constant string indicating external camera facing.
FacingExternal = "external"
)
// DeviceID is video device id from JavaScript navigator.mediaDevices.enumerateDevices.
type DeviceID string
// Mode is capture mode in CCA.
type Mode string
const (
// ID is the app id of CCA.
ID string = "hfhhnacclhffhdffklopdkcgdhifgngh"
// Video is the mode used to record video.
Video Mode = "video"
// Photo is the mode used to take photo.
Photo = "photo"
// Square is the mode used to take square photo.
Square = "square"
// Portrait is the mode used to take portrait photo.
Portrait = "portrait"
// Expert is the state used to indicate expert mode.
Expert string = "expert"
// SaveMetadata is the state used to indicate save metadata.
SaveMetadata = "save-metadata"
)
// TimerState is the information of whether shutter timer is on.
type TimerState bool
const (
// TimerOn means shutter timer is on.
TimerOn TimerState = true
// TimerOff means shutter timer is off.
TimerOff = false
)
var (
// PhotoPattern is the filename format of photos taken by CCA.
PhotoPattern = regexp.MustCompile(`^IMG_\d{8}_\d{6}[^.]*\.jpg$`)
// VideoPattern is the filename format of videos recorded by CCA.
VideoPattern = regexp.MustCompile(`^VID_\d{8}_\d{6}[^.]*\.(mkv|mp4)$`)
// PortraitPattern is the filename format of portrait-mode photos taken by CCA.
PortraitPattern = regexp.MustCompile(`^IMG_\d{8}_\d{6}[^.]*\_BURST\d{5}_COVER.jpg$`)
// PortraitRefPattern is the filename format of the reference photo captured in portrait-mode.
PortraitRefPattern = regexp.MustCompile(`^IMG_\d{8}_\d{6}[^.]*\_BURST\d{5}.jpg$`)
// ErrVideoNotActive indicates that video is not active.
ErrVideoNotActive = "Video is not active within given time"
)
// Orientation is the screen orientation from JavaScript window.screen.orientation.type.
type Orientation string
const (
// PortraitPrimary is the primary portrait orientation.
PortraitPrimary Orientation = "portrait-primary"
// PortraitSecondary is the secondary portrait orientation.
PortraitSecondary Orientation = "portrait-secondary"
// LandscapePrimary is the primary landscape orientation.
LandscapePrimary Orientation = "landscape-primary"
// LandscapeSecondary is the secondary landscape orientation.
LandscapeSecondary Orientation = "landscape-secondary"
)
// TimerDelay is default timer delay of CCA.
const TimerDelay time.Duration = 3 * time.Second
// Profile is type of encoder profile.
type Profile struct {
Name string
Value int
}
var (
// ProfileH264Baseline is h264 baseline profile.
ProfileH264Baseline = Profile{"baseline", 66}
// ProfileH264Main is h264 main profile.
ProfileH264Main = Profile{"main", 77}
// ProfileH264High is h264 high profile.
ProfileH264High = Profile{"high", 100}
)
// Option returns the value of corresponding select-option.
func (p Profile) Option() string {
return strconv.Itoa(int(p.Value))
}
// UIComponent represents a CCA UI component.
type UIComponent struct {
Name string
Selectors []string
}
var (
// CancelResultButton is button for canceling intent review result.
CancelResultButton = UIComponent{"cancel result button", []string{"#cancel-result"}}
// ConfirmResultButton is button for confirming intent review result.
ConfirmResultButton = UIComponent{"confirm result button", []string{"#confirm-result"}}
// MirrorButton is button used for toggling preview mirroring option.
MirrorButton = UIComponent{"mirror button", []string{"#toggle-mirror"}}
// ModeSelector is selection bar for different capture modes.
ModeSelector = UIComponent{"mode selector", []string{"#modes-group"}}
// SettingsButton is button for opening primary setting menu.
SettingsButton = UIComponent{"settings", []string{"#open-settings"}}
// SwitchDeviceButton is button for switching camera device.
SwitchDeviceButton = UIComponent{"switch device button", []string{"#switch-device"}}
// VideoSnapshotButton is button for taking video snapshot during recording.
VideoSnapshotButton = UIComponent{"video snapshot button", []string{"#video-snapshot"}}
// VideoPauseResumeButton is button for pausing or resuming video recording.
VideoPauseResumeButton = UIComponent{"video pause/resume button", []string{"#pause-recordvideo"}}
// GalleryButton is button for entering the Backlight app as a gallery for captured files.
GalleryButton = UIComponent{"gallery button", []string{"#gallery-enter"}}
// ResolutionSettingButton is button for opening resolution setting menu.
ResolutionSettingButton = UIComponent{"resolution setting button", []string{"#settings-resolution"}}
// ExpertModeButton is button used for opening expert mode setting menu.
ExpertModeButton = UIComponent{"expert mode button", []string{"#settings-expert"}}
// PhotoResolutionOption is option for each available photo capture resolution.
PhotoResolutionOption = UIComponent{"photo resolution option", []string{
"#view-photo-resolution-settings input"}}
// VideoResolutionOption is option for each available video capture resolution.
VideoResolutionOption = UIComponent{"video resolution option", []string{
"#view-video-resolution-settings input"}}
// FeedbackButton is the feedback button showing in the settings menu.
FeedbackButton = UIComponent{"feedback button", []string{"#settings-feedback"}}
// HelpButton is the help button showing in the settings menu.
HelpButton = UIComponent{"help button", []string{"#settings-help"}}
// GridTypeSettingsButton is the button showing in the settings menu which is used for entering the grid type settings menu.
GridTypeSettingsButton = UIComponent{"grid type settings button", []string{"#settings-gridtype"}}
// GoldenGridButton is the button to enable golden grid type.
GoldenGridButton = UIComponent{"golden grid type button", []string{"#grid-golden"}}
// TimerSettingsButton is the button showing in the settings menu which is used for entering the timer settings menu.
TimerSettingsButton = UIComponent{"timer settings button", []string{"#settings-timerdur"}}
// Timer10sButton is the button to enable 10s timer.
Timer10sButton = UIComponent{"timer 10s button", []string{"#timer-10s"}}
// BarcodeChipURL is chip for url detected from barcode.
BarcodeChipURL = UIComponent{"barcode chip url", []string{".barcode-chip-url a"}}
// BarcodeChipText is chip for text detected from barcode.
BarcodeChipText = UIComponent{"barcode chip text", []string{".barcode-chip-text"}}
// BarcodeCopyURLButton is button to copy url detected from barcode.
BarcodeCopyURLButton = UIComponent{"barcode copy url button",
[]string{"#barcode-chip-url-container .barcode-copy-button"}}
// BarcodeCopyTextButton is button to copy text detected from barcode.
BarcodeCopyTextButton = UIComponent{"barcode copy text button",
[]string{"#barcode-chip-text-container .barcode-copy-button"}}
// VideoProfileSelect is select-options for selecting video profile.
VideoProfileSelect = UIComponent{"video profile select", []string{"#video-profile"}}
// BitrateMultiplierRangeInput is range input for selecting bitrate multiplier.
BitrateMultiplierRangeInput = UIComponent{"bitrate multiplier range input", []string{"#bitrate-slider input[type=range]"}}
// OpenPTZPanelButton is the button for opening PTZ panel.
OpenPTZPanelButton = UIComponent{"open ptz panel button", []string{"#open-ptz-panel"}}
// PanLeftButton is the button for panning left preview.
PanLeftButton = UIComponent{"pan left button", []string{"#pan-left"}}
// PanRightButton is the button for panning right preview.
PanRightButton = UIComponent{"pan right button", []string{"#pan-right"}}
// TiltUpButton is the button for tilting up preview.
TiltUpButton = UIComponent{"tilt up button", []string{"#tilt-up"}}
// TiltDownButton is the button for tilting down preview.
TiltDownButton = UIComponent{"tilt down button", []string{"#tilt-down"}}
// ZoomInButton is the button for zoom in preview.
ZoomInButton = UIComponent{"zoom in button", []string{"#zoom-in"}}
// ZoomOutButton is the button for zoom out preview.
ZoomOutButton = UIComponent{"zoom out button", []string{"#zoom-out"}}
// PTZResetAllButton is the button for reset PTZ to default value.
PTZResetAllButton = UIComponent{"ptz reset all button", []string{"#ptz-reset-all"}}
)
// ResolutionType is different capture resolution type.
type ResolutionType string
const (
// PhotoResolution represents photo resolution type.
PhotoResolution ResolutionType = "photo"
// VideoResolution represents video resolution type.
VideoResolution = "video"
)
// App represents a CCA (Chrome Camera App) instance.
type App struct {
conn *chrome.Conn
cr *chrome.Chrome
scriptPaths []string
outDir string // Output directory to save the execution result
appLauncher testutil.AppLauncher
appWindow *testutil.AppWindow
cameraType testutil.UseCameraType
}
// ErrJS represents an error occurs when executing JavaScript.
type ErrJS struct {
msg string
}
// Error returns the wrapped message of a ErrJS.
func (e *ErrJS) Error() string {
return e.msg
}
// Resolution represents dimension of video or photo.
type Resolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// Range represents valid range of range type input element.
type Range struct {
Max int `json:"max"`
Min int `json:"min"`
}
// AspectRatio returns width divided by height as the aspect ratio of the resolution.
func (r *Resolution) AspectRatio() float64 {
return float64(r.Width) / float64(r.Height)
}
// Init launches a CCA instance, evaluates the helper script within it and waits
// until its AppWindow interactable. The scriptPath should be the data path to
// the helper script cca_ui.js. The returned App instance must be closed when
// the test is finished.
func Init(ctx context.Context, cr *chrome.Chrome, scriptPaths []string, outDir string, appLauncher testutil.AppLauncher, tb *testutil.TestBridge) (*App, error) {
// Since we don't use "cros-camera" service for fake camera, there is no need
// to ensure it is running.
if tb.CameraType != testutil.UseFakeCamera {
// Ensure that cros-camera service is running, because the service
// might stopped due to the errors from some previous tests, and failed
// to restart for some reasons.
if err := upstart.EnsureJobRunning(ctx, "cros-camera"); err != nil {
return nil, err
}
}
conn, appWindow, err := testutil.LaunchApp(ctx, cr, tb, appLauncher)
if err != nil {
return nil, errors.Wrap(err, "failed when launching app")
}
if err := func() error {
// Let CCA perform some one-time initialization after launched. Otherwise
// the first CheckVideoActive() might timed out because it's still
// initializing, especially on low-end devices and when the system is busy.
// Fail the test early if it's timed out to make it easier to figure out
// the real reason of a test failure.
if err := conn.Eval(ctx, `(async () => {
const deadline = await new Promise(
(resolve) => requestIdleCallback(resolve, {timeout: 30000}));
if (deadline.didTimeout) {
throw new Error('Timed out initializing CCA');
}
})()`, nil); err != nil {
return err
}
return loadScripts(ctx, conn, scriptPaths)
}(); err != nil {
if closeErr := testutil.CloseApp(ctx, cr, conn, appLauncher.UseSWAWindow); closeErr != nil {
testing.ContextLog(ctx, "Failed to close app: ", closeErr)
}
if closeErr := conn.Close(); closeErr != nil {
testing.ContextLog(ctx, "Failed to close app connection: ", closeErr)
}
if releaseErr := appWindow.Release(ctx); releaseErr != nil {
testing.ContextLog(ctx, "Failed to release app window: ", releaseErr)
}
return nil, err
}
testing.ContextLog(ctx, "CCA launched")
app := &App{conn, cr, scriptPaths, outDir, appLauncher, appWindow, tb.CameraType}
waitForWindowReady := func() error {
if err := app.WaitForVideoActive(ctx); err != nil {
return errors.Wrap(err, ErrVideoNotActive)
}
return app.WaitForState(ctx, "view-camera", true)
}
if err := waitForWindowReady(); err != nil {
if err2 := app.Close(ctx); err2 != nil {
testing.ContextLog(ctx, "Failed to close app: ", err2)
}
return nil, errors.Wrap(err, "CCA window is not ready after launching app")
}
testing.ContextLog(ctx, "CCA window is ready")
return app, nil
}
func loadScripts(ctx context.Context, conn *chrome.Conn, scriptPaths []string) error {
for _, scriptPath := range scriptPaths {
script, err := ioutil.ReadFile(scriptPath)
if err != nil {
return err
}
if err := conn.Eval(ctx, string(script), nil); err != nil {
return err
}
}
return nil
}
// New launches a CCA instance. The returned App instance must be closed when the test is finished.
func New(ctx context.Context, cr *chrome.Chrome, scriptPaths []string, outDir string, tb *testutil.TestBridge) (*App, error) {
return Init(ctx, cr, scriptPaths, outDir, testutil.AppLauncher{
LaunchApp: func(ctx context.Context, tconn *chrome.TestConn) error {
return apps.LaunchSystemWebApp(ctx, tconn, "Camera", "chrome://camera-app/views/main.html")
},
UseSWAWindow: true,
}, tb)
}
// InstanceExists checks if there is any running CCA instance.
func InstanceExists(ctx context.Context, cr *chrome.Chrome) (bool, error) {
checkPrefix := func(t *target.Info) bool {
url := "chrome://camera-app/views/main.html"
return strings.HasPrefix(t.URL, url)
}
return cr.IsTargetAvailable(ctx, checkPrefix)
}
// ClosingItself checks if CCA intends to close itself.
func (a *App) ClosingItself(ctx context.Context) (bool, error) {
return a.appWindow.ClosingItself(ctx)
}
// checkJSError checks javascript error emitted by CCA error callback.
func (a *App) checkJSError(ctx context.Context) error {
if a.appWindow == nil {
// It might be closed already. Do nothing.
return nil
}
errorInfos, err := a.appWindow.Errors(ctx)
if err != nil {
return err
}
jsErrors := make([]testutil.ErrorInfo, 0)
jsWarnings := make([]testutil.ErrorInfo, 0)
for _, err := range errorInfos {
if err.Level == testutil.ErrorLevelWarning {
jsWarnings = append(jsWarnings, err)
} else if err.Level == testutil.ErrorLevelError {
jsErrors = append(jsErrors, err)
} else {
return errors.Errorf("unknown error level: %v", err.Level)
}
}
writeLogFile := func(lv testutil.ErrorLevel, errs []testutil.ErrorInfo) error {
filename := fmt.Sprintf("CCA_JS_%v.log", lv)
logPath := filepath.Join(a.outDir, filename)
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
for _, err := range errs {
t := time.Unix(0, err.Time*1e6).Format("2006/01/02 15:04:05 [15:04:05.000]")
f.WriteString(fmt.Sprintf("%v %v:\n", t, err.ErrorType))
f.WriteString(err.Stack + "\n")
}
return nil
}
if err := writeLogFile(testutil.ErrorLevelWarning, jsWarnings); err != nil {
return err
}
if err := writeLogFile(testutil.ErrorLevelError, jsErrors); err != nil {
return err
}
if len(jsErrors) > 0 {
return &ErrJS{fmt.Sprintf("there are %d JS errors, first error: type=%v. name=%v",
len(jsErrors), jsErrors[0].ErrorType, jsErrors[0].ErrorName)}
}
return nil
}
// Close closes the App and the associated connection.
func (a *App) Close(ctx context.Context) (retErr error) {
if a.conn == nil {
// It's already closed. Do nothing.
return nil
}
defer func(ctx context.Context) {
reportOrLogError := func(err error) {
if retErr == nil {
retErr = err
} else {
testing.ContextLog(ctx, "Failed to close app: ", err)
}
}
if err := testutil.CloseApp(ctx, a.cr, a.conn, a.appLauncher.UseSWAWindow); err != nil {
reportOrLogError(errors.Wrap(err, "failed to close app"))
}
if err := a.conn.Close(); err != nil {
reportOrLogError(errors.Wrap(err, "failed to Conn.Close()"))
}
if err := a.appWindow.WaitUntilClosed(ctx); err != nil {
reportOrLogError(errors.Wrap(err, "failed to wait for appWindow close"))
}
if err := a.checkJSError(ctx); err != nil {
reportOrLogError(errors.Wrap(err, "There are JS errors when running CCA"))
}
if err := a.appWindow.Release(ctx); err != nil {
reportOrLogError(errors.Wrap(err, "failed to release app window"))
}
testing.ContextLog(ctx, "CCA closed")
a.conn = nil
a.appWindow = nil
}(ctx)
if err := a.conn.Eval(ctx, "Tast.removeCacheData()", nil); err != nil {
return errors.Wrap(err, "failed to clear cached data in local storage")
}
// TODO(b/144747002): Some tests (e.g. CCUIIntent) might trigger auto closing of CCA before
// calling Close(). We should handle it gracefully to get the coverage report for them.
err := a.OutputCodeCoverage(ctx)
if err != nil {
return err
}
return nil
}
// Restart restarts the App and resets the associated connection.
func (a *App) Restart(ctx context.Context, tb *testutil.TestBridge) error {
if err := a.Close(ctx); err != nil {
return err
}
newApp, err := Init(ctx, a.cr, a.scriptPaths, a.outDir, a.appLauncher, tb)
if err != nil {
return err
}
*a = *newApp
return nil
}
func (a *App) checkVideoState(ctx context.Context, active bool, duration time.Duration) error {
cleanupCtx := ctx
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
code := fmt.Sprintf("Tast.isVideoActive() === %t", active)
if err := a.conn.WaitForExpr(ctx, code); err != nil {
if a.cameraType != testutil.UseFakeCamera {
if jobErr := upstart.CheckJob(cleanupCtx, "cros-camera"); jobErr != nil {
return errors.Wrap(jobErr, err.Error())
}
}
return err
}
// Due to the pipeline delay in camera stack, animation delay, and other
// reasons, sometimes a bug would be triggered after several frames. Wait
// duration here and check that the state does not change afterwards.
if err := testing.Sleep(ctx, duration); err != nil {
return err
}
var ok bool
if err := a.conn.Eval(ctx, code, &ok); err != nil {
return err
}
if !ok {
return errors.Errorf("video state changed after %v", duration.Round(time.Millisecond))
}
return nil
}
// IsWindowMinimized returns true if the current app window is minimized.
func (a *App) IsWindowMinimized(ctx context.Context) (bool, error) {
var isMinimized bool
err := a.conn.Eval(ctx, "Tast.isMinimized()", &isMinimized)
return isMinimized, err
}
// WaitForVideoActive waits for the video to become active for 1 second.
func (a *App) WaitForVideoActive(ctx context.Context) error {
return a.checkVideoState(ctx, true, time.Second)
}
// WaitForFileSaved waits for the presence of the captured file with file name matching the specified
// pattern, size larger than zero, and modified time after the specified timestamp.
func (a *App) WaitForFileSaved(ctx context.Context, dirs []string, pat *regexp.Regexp, ts time.Time) (os.FileInfo, error) {
const timeout = 5 * time.Second
var result os.FileInfo
seen := make(map[string]struct{})
if err := testing.Poll(ctx, func(ctx context.Context) error {
var allFiles []os.FileInfo
var lastErr error
for _, dir := range dirs {
files, err := ioutil.ReadDir(dir)
if err != nil {
lastErr = err
continue
}
allFiles = append(allFiles, files...)
}
if lastErr != nil && len(allFiles) == 0 {
return errors.Wrap(lastErr, "failed to read the directory where media files are saved")
}
for _, file := range allFiles {
if file.Size() == 0 || file.ModTime().Before(ts) {
continue
}
if _, ok := seen[file.Name()]; ok {
continue
}
seen[file.Name()] = struct{}{}
testing.ContextLog(ctx, "New file found: ", file.Name())
if pat.MatchString(file.Name()) {
testing.ContextLog(ctx, "Found a match: ", file.Name())
result = file
return nil
}
}
return errors.New("no matching output file found")
}, &testing.PollOptions{Timeout: timeout}); err != nil {
return nil, errors.Wrapf(err, "no matching output file found after %v", timeout)
}
return result, nil
}
// CheckVideoInactive checks the video is inactive for 1 second.
func (a *App) CheckVideoInactive(ctx context.Context) error {
return a.checkVideoState(ctx, false, time.Second)
}
// RestoreWindow restores the window, exiting a maximized, minimized, or fullscreen state.
func (a *App) RestoreWindow(ctx context.Context) error {
return a.conn.Eval(ctx, "Tast.restoreWindow()", nil)
}
// MinimizeWindow minimizes the window.
func (a *App) MinimizeWindow(ctx context.Context) error {
return a.conn.Eval(ctx, "Tast.minimizeWindow()", nil)
}
// MaximizeWindow maximizes the window.
func (a *App) MaximizeWindow(ctx context.Context) error {
return a.conn.Eval(ctx, "Tast.maximizeWindow()", nil)
}
// FullscreenWindow fullscreens the window.
func (a *App) FullscreenWindow(ctx context.Context) error {
return a.conn.Eval(ctx, "Tast.fullscreenWindow()", nil)
}
// GetNumOfCameras returns number of camera devices.
func (a *App) GetNumOfCameras(ctx context.Context) (int, error) {
var numCameras int
err := a.conn.Eval(ctx, "Tast.getNumOfCameras()", &numCameras)
return numCameras, err
}
// GetFacing returns the active camera facing.
func (a *App) GetFacing(ctx context.Context) (Facing, error) {
var facing Facing
if err := a.conn.Eval(ctx, "Tast.getFacing()", &facing); err != nil {
return "", err
}
return facing, nil
}
// GetPreviewResolution returns resolution of preview video.
func (a *App) GetPreviewResolution(ctx context.Context) (Resolution, error) {
r := Resolution{-1, -1}
if err := a.conn.Eval(ctx, "Tast.getPreviewResolution()", &r); err != nil {
return r, errors.Wrap(err, "failed to get preview resolution")
}
return r, nil
}
// GetScreenOrientation returns screen orientation.
func (a *App) GetScreenOrientation(ctx context.Context) (Orientation, error) {
var orientation Orientation
if err := a.conn.Eval(ctx, "Tast.getScreenOrientation()", &orientation); err != nil {
return "", errors.Wrap(err, "failed to get screen orientation")
}
return orientation, nil
}
// GetPhotoResolutions returns available photo resolutions of active camera on HALv3 device.
func (a *App) GetPhotoResolutions(ctx context.Context) ([]Resolution, error) {
var rs []Resolution
if err := a.conn.Eval(ctx, "Tast.getPhotoResolutions()", &rs); err != nil {
return nil, errors.Wrap(err, "failed to get photo resolution")
}
return rs, nil
}
// GetVideoResolutions returns available video resolutions of active camera on HALv3 device.
func (a *App) GetVideoResolutions(ctx context.Context) ([]Resolution, error) {
var rs []Resolution
if err := a.conn.Eval(ctx, "Tast.getVideoResolutions()", &rs); err != nil {
return nil, errors.Wrap(err, "failed to get video resolution")
}
return rs, nil
}
// GetDeviceID returns the active camera device id.
func (a *App) GetDeviceID(ctx context.Context) (DeviceID, error) {
var id DeviceID
if err := a.conn.Eval(ctx, "Tast.getDeviceId()", &id); err != nil {
return "", err
}
return id, nil
}
// GetState returns whether a state is active in CCA.
func (a *App) GetState(ctx context.Context, state string) (bool, error) {
var result bool
if err := a.conn.Call(ctx, &result, "Tast.getState", state); err != nil {
return false, errors.Wrapf(err, "failed to get state: %v", state)
}
return result, nil
}
// PreviewFrame grabs a frame from preview. The caller should be responsible for releasing the frame.
func (a *App) PreviewFrame(ctx context.Context) (*Frame, error) {
if err := a.WaitForVideoActive(ctx); err != nil {
return nil, errors.Wrap(err, "failed to wait for preview active")
}
var f chrome.JSObject
if err := a.conn.Call(ctx, &f, "Tast.getPreviewFrame"); err != nil {
return nil, errors.Wrap(err, "failed to get preview frame")
}
return &Frame{&f}, nil
}
// PortraitModeSupported returns whether portrait mode is supported by the current active video device.
func (a *App) PortraitModeSupported(ctx context.Context) (bool, error) {
var result bool
if err := a.conn.Eval(ctx, "Tast.isPortraitModeSupported()", &result); err != nil {
return false, err
}
return result, nil
}
// TakeSinglePhoto takes a photo and save to default location.
func (a *App) TakeSinglePhoto(ctx context.Context, timerState TimerState) ([]os.FileInfo, error) {
var patterns []*regexp.Regexp
isPortrait, err := a.GetState(ctx, string(Portrait))
if err != nil {
return nil, err
}
if isPortrait {
// TODO(b/183366604): Check for |PortraitPattern| once we can ensure there is human face in the
// frame content.
patterns = append(patterns, PortraitRefPattern)
} else {
patterns = append(patterns, PhotoPattern)
}
if err = a.SetTimerOption(ctx, timerState); err != nil {
return nil, err
}
start := time.Now()
testing.ContextLog(ctx, "Click on start shutter")
if err = a.ClickShutter(ctx); err != nil {
return nil, err
}
if err = a.WaitForState(ctx, "taking", false); err != nil {
return nil, errors.Wrap(err, "capturing hasn't ended")
}
dirs, err := a.SavedDirs(ctx)
if err != nil {
return nil, err
}
var fileInfos []os.FileInfo
for _, pattern := range patterns {
info, err := a.WaitForFileSaved(ctx, dirs, pattern, start)
if err != nil {
return nil, errors.Wrapf(err, "cannot find result picture with regexp: %v", pattern)
}
if elapsed := info.ModTime().Sub(start); timerState == TimerOn && elapsed < TimerDelay {
return nil, errors.Errorf("the capture should happen after timer of %v, actual elapsed time %v", TimerDelay, elapsed)
}
fileInfos = append(fileInfos, info)
}
isExpert, err := a.GetState(ctx, Expert)
if err != nil {
return nil, err
}
isSaveMetadata, err := a.GetState(ctx, SaveMetadata)
if err != nil {
return nil, err
}
if !isExpert || !isSaveMetadata {
return fileInfos, nil
}
metadataPatterns := getMetadataPatterns(fileInfos)
for _, pattern := range metadataPatterns {
info, err := a.WaitForFileSaved(ctx, dirs, pattern, start)
if err != nil {
return nil, errors.Wrapf(err, "cannot find result metadata with regexp: %v", pattern)
}
if info.Size() == 0 {
return nil, errors.Errorf("saved file %v is empty", info.Name())
}
if err != nil {
return nil, err
}
path, err := a.FilePathInSavedDirs(ctx, info.Name())
if err != nil {
return nil, err
}
var jsonString map[string]interface{}
if content, err := ioutil.ReadFile(path); err != nil {
return nil, errors.Wrapf(err, "failed to read metadata file %v", info.Name())
} else if err := json.Unmarshal(content, &jsonString); err != nil {
return nil, errors.Wrapf(err, "not a valid json file %v", info.Name())
}
fileInfos = append(fileInfos, info)
}
return fileInfos, nil
}
func getMetadataPatterns(fileInfos []os.FileInfo) []*regexp.Regexp {
// Matches the extension and the potential number suffix such as " (2).jpg".
re := regexp.MustCompile(`( \(\d+\))?\.jpg$`)
var patterns []*regexp.Regexp
for _, info := range fileInfos {
pattern := `^` + regexp.QuoteMeta(re.ReplaceAllString(info.Name(), "")) + `.*\.json$`
patterns = append(patterns, regexp.MustCompile(pattern))
}
return patterns
}
// StartRecording starts recording a video.
func (a *App) StartRecording(ctx context.Context, timerState TimerState) (time.Time, error) {
startTime := time.Now()
if err := a.SetTimerOption(ctx, timerState); err != nil {
return startTime, err
}
testing.ContextLog(ctx, "Click on start shutter")
if err := a.ClickShutter(ctx); err != nil {
return startTime, err
}
// Wait for end of timer and start of recording.
if err := a.WaitForState(ctx, "recording", true); err != nil {
return startTime, errors.Wrap(err, "recording is not started")
}
recordStartTime := time.Now()
if timerState == TimerOn {
// Assume that the delay between recording state being set and
// time.Now() getting timestamp is small enough to be
// neglected. Otherwise, it may miss the failure cases if
// |autual recording start time|+ |this delay| > |startTime| +
// |TimerDelay|.
if delay := recordStartTime.Sub(startTime); delay < TimerDelay {
return startTime, errors.Errorf("recording starts %v before timer finished", TimerDelay-delay)
}
}
return startTime, nil
}
// StopRecording stops recording a video.
func (a *App) StopRecording(ctx context.Context, timerState TimerState, startTime time.Time) (os.FileInfo, time.Time, error) {
testing.ContextLog(ctx, "Click on stop shutter")
stopTime, err := a.TriggerStateChange(ctx, "recording", false, func() error {
return a.ClickShutter(ctx)
})
if err != nil {
return nil, time.Time{}, err
}
if err := a.WaitForState(ctx, "taking", false); err != nil {
return nil, time.Time{}, errors.Wrap(err, "shutter is not ended")
}
dirs, err := a.SavedDirs(ctx)
if err != nil {
return nil, time.Time{}, err
}
info, err := a.WaitForFileSaved(ctx, dirs, VideoPattern, startTime)
if err != nil {
return nil, time.Time{}, errors.Wrap(err, "cannot find result video")
} else if elapsed := info.ModTime().Sub(startTime); timerState == TimerOn && elapsed < TimerDelay {
return nil, time.Time{}, errors.Errorf("the capture happen after elapsed time %v, should be after %v timer", elapsed, TimerDelay)
}
return info, stopTime, nil
}
// RecordVideo records a video with duration length and save to default location.
func (a *App) RecordVideo(ctx context.Context, timerState TimerState, duration time.Duration) (os.FileInfo, error) {
startTime, err := a.StartRecording(ctx, timerState)
if err != nil {
return nil, err
}
if err := testing.Sleep(ctx, duration); err != nil {
return nil, err
}
info, _, err := a.StopRecording(ctx, timerState, startTime)
if err != nil {
return nil, err
}
return info, err
}
// savedDirs returns the paths to the folder where captured files might be saved.
func savedDirs(ctx context.Context, cr *chrome.Chrome) ([]string, error) {
path, err := cryptohome.UserPath(ctx, cr.NormalizedUser())
if err != nil {
return nil, err
}
myFiles := filepath.Join(path, "MyFiles")
return []string{filepath.Join(myFiles, "Downloads"), filepath.Join(myFiles, "Camera")}, nil
}
// ClearSavedDirs clears all files in the folders where captured files might be saved.
func ClearSavedDirs(ctx context.Context, cr *chrome.Chrome) error {
clearDir := func(ctx context.Context, cr *chrome.Chrome, dir string) error {
files, err := ioutil.ReadDir(dir)
if err != nil {
return errors.Wrap(err, "failed to read saved directory")
}
// Pattern for metadata files of all different kinds of photos.
metadataPattern := regexp.MustCompile(`^IMG_\d{8}_\d{6}.*\.json$`)
capturedPatterns := []*regexp.Regexp{PhotoPattern, VideoPattern, PortraitPattern, PortraitRefPattern, metadataPattern}
for _, file := range files {
for _, pat := range capturedPatterns {
if pat.MatchString(file.Name()) {
path := filepath.Join(dir, file.Name())
if err := os.Remove(path); err != nil {
return errors.Wrapf(err, "failed to remove file %v from saved directory", path)
}
break
}
}
}
return nil
}
dirs, err := savedDirs(ctx, cr)
if err != nil {
return errors.Wrap(err, "failed to get saved directorys")
}
for _, dir := range dirs {
if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
continue
} else {
return err
}
}
if err := clearDir(ctx, cr, dir); err != nil {
return err
}
}
return nil
}
// SavedDirs returns the path to the folder where captured files are saved.
func (a *App) SavedDirs(ctx context.Context) ([]string, error) {
return savedDirs(ctx, a.cr)
}
// FilePathInSavedDirs finds and returns the path of the target file in saved directories.
func (a *App) FilePathInSavedDirs(ctx context.Context, name string) (string, error) {
dirs, err := savedDirs(ctx, a.cr)
if err != nil {
return "", err
}
for _, dir := range dirs {
path := filepath.Join(dir, name)
_, err := os.Stat(path)
if err == nil {
return path, nil
}
if !os.IsNotExist(err) {
return "", err
}
}
return "", errors.New("file not found in saved path")
}
// CheckFacing returns an error if the active camera facing is not expected.
func (a *App) CheckFacing(ctx context.Context, expected Facing) error {
return a.conn.Call(ctx, nil, "Tast.checkFacing", expected)
}
// Mirrored returns whether mirroring is on.
func (a *App) Mirrored(ctx context.Context) (bool, error) {
var actual bool
err := a.conn.Eval(ctx, "Tast.getState('mirror')", &actual)
return actual, err
}
func (a *App) selectorExist(ctx context.Context, selector string) (bool, error) {
var exist bool
if err := a.conn.Call(ctx, &exist, "Tast.isExist", selector); err != nil {
return false, errors.Wrapf(err, "failed to check selector %v exist", selector)
}
return exist, nil
}
// resolveUISelector resolves ui to its correct selector.
func (a *App) resolveUISelector(ctx context.Context, ui UIComponent) (string, error) {
for _, s := range ui.Selectors {
if exist, err := a.selectorExist(ctx, s); err != nil {
return "", err
} else if exist {
return s, nil
}
}
return "", errors.Errorf("failed to resolved ui %v to its correct selector", ui.Name)
}
// Style returns the value of an CSS attribute of an UI component.
func (a *App) Style(ctx context.Context, ui UIComponent, attribute string) (string, error) {
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return "", errors.Wrapf(err, "failed to get the selector of UI: %v", ui.Name)
}
var style string
if err := a.conn.Call(ctx, &style, "Tast.getStyle", selector, attribute); err != nil {
return "", errors.Wrapf(err, "failed to get the style of attribute: %v of UI: %v", attribute, ui.Name)
}
return style, nil
}
// Visible returns whether a UI component is visible on the screen.
func (a *App) Visible(ctx context.Context, ui UIComponent) (bool, error) {
wrapError := func(err error) error {
return errors.Wrapf(err, "failed to check visibility state of %v", ui.Name)
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return false, wrapError(err)
}
var visible bool
if err := a.conn.Call(ctx, &visible, "Tast.isVisible", selector); err != nil {
return false, wrapError(err)
}
return visible, nil
}
// CheckVisible returns an error if visibility state of ui is not expected.
func (a *App) CheckVisible(ctx context.Context, ui UIComponent, expected bool) error {
if visible, err := a.Visible(ctx, ui); err != nil {
return err
} else if visible != expected {
return errors.Errorf("unexpected %v visibility state: got %v, want %v", ui.Name, visible, expected)
}
return nil
}
// WaitForVisibleState waits until the visibility of ui becomes expected.
func (a *App) WaitForVisibleState(ctx context.Context, ui UIComponent, expected bool) error {
return testing.Poll(ctx, func(ctx context.Context) error {
visible, err := a.Visible(ctx, ui)
if err != nil {
return testing.PollBreak(err)
}
if visible != expected {
return errors.Errorf("failed to wait visibility state for %v: got %v, want %v", ui.Name, visible, expected)
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second})
}
// Disabled returns disabled attribute of HTMLElement of |ui|.
func (a *App) Disabled(ctx context.Context, ui UIComponent) (bool, error) {
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return false, errors.Wrapf(err, "failed to resolve ui %v to correct selector", ui.Name)
}
var disabled bool
if err := a.conn.Call(ctx, &disabled, "(selector) => document.querySelector(selector).disabled", selector); err != nil {
return false, errors.Wrapf(err, "failed to get disabled state of %v", ui.Name)
}
return disabled, nil
}
// WaitForDisabled waits until the disabled state of ui becomes |expected|.
func (a *App) WaitForDisabled(ctx context.Context, ui UIComponent, expected bool) error {
return testing.Poll(ctx, func(ctx context.Context) error {
disabled, err := a.Disabled(ctx, ui)
if err != nil {
return testing.PollBreak(errors.Wrapf(err, "failed to wait disabled state of %v to be %v", ui.Name, expected))
}
if disabled != expected {
return errors.Errorf("failed to wait disabled state for %v: got %v, want %v", ui.Name, disabled, expected)
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second})
}
// CheckConfirmUIExists returns whether the confirm UI exists.
func (a *App) CheckConfirmUIExists(ctx context.Context, mode Mode) error {
var reviewElementID string
if mode == Photo {
reviewElementID = "#review-photo-result"
} else if mode == Video {
reviewElementID = "#review-video-result"
} else {
return errors.Errorf("unrecognized mode: %s", mode)
}
var visible bool
if err := a.conn.Call(ctx, &visible, "Tast.isVisible", reviewElementID); err != nil {
return err
} else if !visible {
return errors.New("review result is not shown")
}
if visible, err := a.Visible(ctx, ConfirmResultButton); err != nil {
return err
} else if !visible {
return errors.New("confirm button is not shown")
}
if visible, err := a.Visible(ctx, CancelResultButton); err != nil {
return err
} else if !visible {
return errors.New("cancel button is not shown")
}
return nil
}
// CountUI returns the number of ui element.
func (a *App) CountUI(ctx context.Context, ui UIComponent) (int, error) {
wrapError := func(err error) error {
return errors.Wrapf(err, "failed to count number of %v", ui.Name)
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return 0, wrapError(err)
}
var number int
if err := a.conn.Call(ctx, &number, `(selector) => document.querySelectorAll(selector).length`, selector); err != nil {
return 0, wrapError(err)
}
return number, nil
}
// AttributeWithIndex returns the attr attribute of the index th ui.
func (a *App) AttributeWithIndex(ctx context.Context, ui UIComponent, index int, attr string) (string, error) {
wrapError := func(err error) error {
return errors.Wrapf(err, "failed to get %v attribute of %v th %v", attr, index, ui.Name)
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return "", wrapError(err)
}
var value string
if err := a.conn.Call(
ctx, &value,
`(selector, index, attr) => document.querySelectorAll(selector)[index].getAttribute(attr)`,
selector, index, attr); err != nil {
return "", wrapError(err)
}
return value, nil
}
// ConfirmResult clicks the confirm button or the cancel button according to the given isConfirmed.
func (a *App) ConfirmResult(ctx context.Context, isConfirmed bool, mode Mode) error {
if err := a.WaitForState(ctx, "review-result", true); err != nil {
return errors.Wrap(err, "does not enter review result state")
}
if err := a.CheckConfirmUIExists(ctx, mode); err != nil {
return errors.Wrap(err, "check confirm UI failed")
}
var expr string
if isConfirmed {
// TODO(b/144547749): Since CCA will close automatically after clicking the button, sometimes it
// will report connection lost error when executing. Removed the setTimeout wrapping once the
// flakiness got resolved.
expr = "setTimeout(() => Tast.click('#confirm-result'), 0)"
} else {
expr = "Tast.click('#cancel-result')"
}
if err := a.conn.Eval(ctx, expr, nil); err != nil {
return errors.Wrap(err, "failed to click confirm/cancel button")
}
return nil
}
func (a *App) toggleOption(ctx context.Context, option, toggleSelector string) (bool, error) {
prev, err := a.GetState(ctx, option)
if err != nil {
return false, err
}
if err := a.ClickWithSelector(ctx, toggleSelector); err != nil {
return false, errors.Wrapf(err, "failed to click on toggle button of selector %s", toggleSelector)
}
code := fmt.Sprintf("Tast.getState(%q) !== %t", option, prev)
if err := a.conn.WaitForExpr(ctx, code); err != nil {
return false, errors.Wrapf(err, "failed to wait for toggling option %s", option)
}
return a.GetState(ctx, option)
}
// ToggleGridOption toggles the grid option and returns whether it's enabled after toggling.
func (a *App) ToggleGridOption(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "grid", "#toggle-grid")
}
// ToggleMirroringOption toggles the mirroring option.
func (a *App) ToggleMirroringOption(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "mirror", "#toggle-mirror")
}
// ToggleQRCodeOption toggles the barcode scanning option.
func (a *App) ToggleQRCodeOption(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "enable-scan-barcode", "#toggle-barcode")
}
// SetTimerOption sets the timer option to on/off.
func (a *App) SetTimerOption(ctx context.Context, state TimerState) error {
active := state == TimerOn
if cur, err := a.GetState(ctx, "timer"); err != nil {
return err
} else if cur != active {
if _, err := a.toggleOption(ctx, "timer", "#toggle-timer"); err != nil {
return err
}
}
// Fix timer to 3 seconds for saving test time.
if active {
if delay3, err := a.GetState(ctx, "timer-3s"); err != nil {
return err
} else if !delay3 {
return errors.New("default timer is not set to 3 seconds")
}
}
return nil
}
// ToggleExpertMode toggles expert mode and returns whether it's enabled after toggling.
func (a *App) ToggleExpertMode(ctx context.Context) (bool, error) {
prev, err := a.GetState(ctx, Expert)
if err != nil {
return false, err
}
if err := a.conn.Eval(ctx, "Tast.toggleExpertMode()", nil); err != nil {
return false, errors.Wrap(err, "failed to toggle expert mode")
}
if err := a.WaitForState(ctx, "expert", !prev); err != nil {
return false, errors.Wrap(err, "failed to wait for toggling expert mode")
}
return a.GetState(ctx, Expert)
}
// CheckMetadataVisibility checks if metadata is shown/hidden on screen given enabled.
func (a *App) CheckMetadataVisibility(ctx context.Context, enabled bool) error {
code := fmt.Sprintf("Tast.isVisible('#preview-exposure-time') === %t", enabled)
if err := a.conn.WaitForExpr(ctx, code); err != nil {
return errors.Wrapf(err, "failed to wait for metadata visibility set to %v", enabled)
}
return nil
}
// ToggleShowMetadata toggles show metadata and returns whether it's enabled after toggling.
func (a *App) ToggleShowMetadata(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "show-metadata", "#expert-show-metadata")
}
// ToggleSaveMetadata toggles save metadata and returns whether it's enabled after toggling.
func (a *App) ToggleSaveMetadata(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "save-metadata", "#expert-save-metadata")
}
// ToggleEnableExpertMode toggles enable expert mode and returns whether it's enabled after toggling.
func (a *App) ToggleEnableExpertMode(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "expert", "#expert-enable-expert-mode")
}
// ToggleCustomVideoParameters customize video parameters options and returns whether it's enabled after toggling.
func (a *App) ToggleCustomVideoParameters(ctx context.Context) (bool, error) {
return a.toggleOption(ctx, "custom-video-parameters", "#custom-video-parameters")
}
// ClickShutter clicks the shutter button.
func (a *App) ClickShutter(ctx context.Context) error {
if err := a.conn.Eval(ctx, "Tast.click('.shutter')", nil); err != nil {
return errors.Wrap(err, "failed to click shutter button")
}
return nil
}
// SwitchCamera switches to next camera device.
func (a *App) SwitchCamera(ctx context.Context) error {
if err := a.TriggerConfiguration(ctx, func() error {
return a.Click(ctx, SwitchDeviceButton)
}); err != nil {
return errors.Wrap(err, "failed to switch camera")
}
return nil
}
// SwitchMode switches to specified capture mode.
func (a *App) SwitchMode(ctx context.Context, mode Mode) error {
if active, err := a.GetState(ctx, string(mode)); err != nil {
return err
} else if active {
return nil
}
if err := a.conn.Call(ctx, nil, "Tast.switchMode", mode); err != nil {
return errors.Wrapf(err, "failed to switch to mode %s", mode)
}
if err := a.WaitForState(ctx, "mode-switching", false); err != nil {
return errors.Wrap(err, "failed to wait for finishing of mode switching")
}
if err := a.WaitForVideoActive(ctx); err != nil {
return errors.Wrapf(err, "preview is inactive after switching to mode %s", mode)
}
// Owing to the mode retry mechanism in CCA, it may fallback to other mode when failing to
// switch to specified mode. Verify the mode value again after switching.
if active, err := a.GetState(ctx, string(mode)); err != nil {
return errors.Wrapf(err, "failed to get mode state after switching to mode %s", mode)
} else if !active {
return errors.Wrapf(err, "failed to switch to mode %s", mode)
}
return nil
}
// WaitForState waits until state become active/inactive.
func (a *App) WaitForState(ctx context.Context, state string, active bool) error {
code := fmt.Sprintf("Tast.getState(%q) === %t", state, active)
if err := a.conn.WaitForExpr(ctx, code); err != nil {
return errors.Wrapf(err, "failed to wait for state %s to set to %v", state, active)
}
return nil
}
// WaitForMinimized waits for app window to be minimized/restored.
func (a *App) WaitForMinimized(ctx context.Context, minimized bool) error {
const timeout = 5 * time.Second
return testing.Poll(ctx, func(ctx context.Context) error {
actual, err := a.IsWindowMinimized(ctx)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to check if window is minimized"))
}
if actual != minimized {
return errors.New("failed to wait for window minimized/restored")
}
return nil
}, &testing.PollOptions{Timeout: timeout})
}
// CheckGridOption checks whether grid option enable state is as expected.
func (a *App) CheckGridOption(ctx context.Context, expected bool) error {
var actual bool
if err := a.conn.Eval(ctx, "Tast.getState('grid')", &actual); err != nil {
return err
}
if actual != expected {
return errors.Errorf("unexpected grid option enablement: got %v, want %v", actual, expected)
}
return nil
}
// Click clicks on ui.
func (a *App) Click(ctx context.Context, ui UIComponent) error {
wrapError := func(err error) error {
return errors.Wrapf(err, "failed to click on %v", ui.Name)
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return wrapError(err)
}
if err := a.ClickWithSelector(ctx, selector); err != nil {
return wrapError(err)
}
return nil
}
// ClickWithIndex clicks nth ui.
func (a *App) ClickWithIndex(ctx context.Context, ui UIComponent, index int) error {
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return err
}
if err := a.conn.Call(ctx, nil, `(selector, index) => document.querySelectorAll(selector)[index].click()`, selector, index); err != nil {
return errors.Wrapf(err, "failed to click on %v(th) %v", index, ui.Name)
}
return nil
}
// Hold holds on |ui| by sending pointerdown and pointerup for |d| duration.
func (a *App) Hold(ctx context.Context, ui UIComponent, d time.Duration) error {
wrapError := func(err error) error {
return errors.Wrapf(err, "failed to hold on %v", ui.Name)
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return wrapError(err)
}
return a.conn.Call(ctx, nil, `Tast.hold`, selector, d.Milliseconds())
}
// ClickPTZButton clicks on PTZ Button.
func (a *App) ClickPTZButton(ctx context.Context, ui UIComponent) error {
// Hold for 0ms to trigger PTZ minimal step movement.
return a.Hold(ctx, ui, 0)
}
// IsCheckedWithIndex gets checked state of nth ui.
func (a *App) IsCheckedWithIndex(ctx context.Context, ui UIComponent, index int) (bool, error) {
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return false, err
}
var checked bool
if err := a.conn.Call(ctx, &checked, `(selector, index) => document.querySelectorAll(selector)[index].checked`, selector, index); err != nil {
return false, errors.Wrapf(err, "failed to get checked state on %v(th) %v", index, ui.Name)
}
return checked, nil
}
// ClickWithSelector clicks an element with given selector.
func (a *App) ClickWithSelector(ctx context.Context, selector string) error {
return a.conn.Call(ctx, nil, `Tast.click`, selector)
}
// SelectOption selects the target option in HTMLSelectElement.
func (a *App) SelectOption(ctx context.Context, ui UIComponent, value string) error {
if err := a.WaitForVisibleState(ctx, ui, true); err != nil {
return err
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return err
}
return a.conn.Call(ctx, nil, "Tast.selectOption", selector, value)
}
// InputRange returns the range of valid value for range type input element.
func (a *App) InputRange(ctx context.Context, ui UIComponent) (*Range, error) {
if err := a.WaitForVisibleState(ctx, ui, true); err != nil {
return nil, err
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return nil, err
}
var r Range
if err := a.conn.Call(ctx, &r, "Tast.getInputRange", selector); err != nil {
return nil, errors.Wrapf(err, "failed to get input range of %v", ui.Name)
}
return &r, nil
}
// SetRangeInput set value of range input.
func (a *App) SetRangeInput(ctx context.Context, ui UIComponent, value int) error {
if err := a.WaitForVisibleState(ctx, ui, true); err != nil {
return err
}
selector, err := a.resolveUISelector(ctx, ui)
if err != nil {
return err
}
if err := a.conn.Call(ctx, nil, "Tast.setInputValue", selector, value); err != nil {
return errors.Wrapf(err, "failed to set range input %v to %v", ui.Name, value)
}
return nil
}
// RunThroughCameras runs function f in app after switching to each available camera.
// The f is called with paramter of the switched camera facing.
// The error returned by f is passed to caller of this function.
func (a *App) RunThroughCameras(ctx context.Context, f func(Facing) error) error {
numCameras, err := a.GetNumOfCameras(ctx)
if err != nil {
return errors.Wrap(err, "can't get number of cameras")
}
devices := make(map[DeviceID]Facing)
for cam := 0; cam < numCameras; cam++ {
if cam != 0 {
if err := a.SwitchCamera(ctx); err != nil {
return errors.Wrap(err, "failed to switch camera")
}
}
id, err := a.GetDeviceID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get device id")
}
facing, err := a.GetFacing(ctx)
if err != nil {
return errors.Wrap(err, "failed to get facing")
}
if _, ok := devices[id]; ok {
continue
}
devices[id] = facing
testing.ContextLogf(ctx, "Run f() on camera facing %q", facing)
if err := f(facing); err != nil {
return err
}
}
if numCameras > 1 {
// Switch back to the original camera.
if err := a.SwitchCamera(ctx); err != nil {
return errors.Wrap(err, "failed to switch to next camera")
}
}
if len(devices) != numCameras {
return errors.Errorf("failed to switch to some camera (tested cameras: %v)", devices)
}
return nil
}
// CheckMojoConnection checks if mojo connection works.
func (a *App) CheckMojoConnection(ctx context.Context) error {
return a.conn.Call(ctx, nil, "Tast.checkMojoConnection", upstart.JobExists(ctx, "cros-camera"))
}
// OutputCodeCoverage stops the profiling and output the code coverage information to the output
// directory.
func (a *App) OutputCodeCoverage(ctx context.Context) error {
reply, err := a.conn.StopProfiling(ctx)
if err != nil {
return err
}
coverageData, err := json.Marshal(reply)
if err != nil {
return err
}
coverageDirPath := filepath.Join(a.outDir, fmt.Sprintf("coverage"))
if _, err := os.Stat(coverageDirPath); os.IsNotExist(err) {
if err := os.MkdirAll(coverageDirPath, 0755); err != nil {
return err
}
} else if err != nil {
return err
}
for idx := 0; ; idx++ {
coverageFilePath := filepath.Join(coverageDirPath, fmt.Sprintf("coverage-%d.json", idx))
if _, err := os.Stat(coverageFilePath); os.IsNotExist(err) {
if err := ioutil.WriteFile(coverageFilePath, coverageData, 0644); err != nil {
return err
}
break
} else if err != nil {
return err
}
}
return nil
}
// TriggerConfiguration triggers configuration by calling trigger() and waits for camera configuration finishing.
func (a *App) TriggerConfiguration(ctx context.Context, trigger func() error) error {
// waitNextConfiguration() returns a Promise instance, so Eval waits for its settled state.
// For its workaround, wrap by a closure.
var waiting chrome.JSObject
if err := a.conn.Eval(ctx, `(p => () => p)(Tast.waitNextConfiguration())`, &waiting); err != nil {
return errors.Wrap(err, "failed to start watching congiruation update")
}
defer waiting.Release(ctx)
if err := trigger(); err != nil {
return err
}
// And then unwrap the promise to wait its settled state.
if err := a.conn.Call(ctx, nil, `(p) => p()`, &waiting); err != nil {
return errors.Wrap(err, "failed to waiting for the completion configuration update")
}
return nil
}
// TriggerStateChange triggers |state| change by calling |trigger()|, waits for
// its value changing from |!expected| to |expected| and returns when the
// change happens.
func (a *App) TriggerStateChange(ctx context.Context, state string, expected bool, trigger func() error) (time.Time, error) {
var wrappedPromise chrome.JSObject
if err := a.conn.Call(ctx, &wrappedPromise, `
(state, expected) => {
const p = Tast.observeStateChange(state, expected);
return () => p;
}
`, state, expected); err != nil {
return time.Time{}, errors.Wrapf(err, "failed to observe %v state with %v expected value", state, expected)
}
defer wrappedPromise.Release(ctx)
if err := trigger(); err != nil {
return time.Time{}, err
}
var ts int64
if err := a.conn.Call(ctx, &ts, `(p) => p()`, &wrappedPromise); err != nil {
return time.Time{}, errors.Wrapf(err, "failed to wait for %v state changing to %v", state, expected)
}
return time.Unix(0, ts*1e6), nil
}
// EnsureTabletModeEnabled makes sure that the tablet mode states of both
// device and app are enabled, and returns a function which reverts back to the
// original state.
func (a *App) EnsureTabletModeEnabled(ctx context.Context, enabled bool) (func(ctx context.Context) error, error) {
tconn, err := a.cr.TestAPIConn(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get test api connection")
}
originallyEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
return nil, errors.Wrap(err, "failed to get tablet mode state")
}
cleanup, err := ash.EnsureTabletModeEnabled(ctx, tconn, enabled)
if err != nil {
return nil, errors.Wrapf(err, "failed to ensure tablet mode enabled(%v)", enabled)
}
cleanupAll := func(ctx context.Context) error {
if err := cleanup(ctx); err != nil {
return errors.Wrap(err, "failed to clean up tablet mode state")
}
if err := a.WaitForState(ctx, "tablet", originallyEnabled); err != nil {
return errors.Wrapf(err, "failed to wait for original tablet mode enabled(%v)", originallyEnabled)
}
return nil
}
if err := a.WaitForState(ctx, "tablet", enabled); err != nil {
if err := cleanupAll(ctx); err != nil {
testing.ContextLog(ctx, "Failed to restore tablet mode state: ", err)
}
return nil, errors.Wrapf(err, "failed to wait for tablet mode enabled(%v)", enabled)
}
return cleanupAll, nil
}
// Focus sets focus on CCA App window.
func (a *App) Focus(ctx context.Context) error {
return a.conn.Eval(ctx, "Tast.focusWindow()", nil)
}
// InnerResolutionSetting returns setting menu for toggle |rt| resolution of |facing| camera.
func (a *App) InnerResolutionSetting(ctx context.Context, facing Facing, rt ResolutionType) (*SettingMenu, error) {
view := fmt.Sprintf("view-%s-resolution-settings", rt)
fname, ok := (map[Facing]string{
FacingBack: "back",
FacingFront: "front",
FacingExternal: "external",
})[facing]
if !ok {
return nil, errors.Errorf("cannot get resolution of unsuppport facing %v", facing)
}
ariaPrefix := fname
if facing == FacingExternal {
// Assumes already switched to target external camera.
id, err := a.GetDeviceID(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get device id of external camera")
}
ariaPrefix = string(id)
}
selector := fmt.Sprintf("button[aria-describedby='%s-%sres-desc']", ariaPrefix, rt)
openUI := &UIComponent{
Name: fmt.Sprintf("%v camera %v resolution settings button", fname, rt),
Selectors: []string{selector},
}
return &SettingMenu{view, openUI}, nil
}
// Refresh refreshes CCA.
func (a *App) Refresh(ctx context.Context, tb *testutil.TestBridge) error {
newAppWindow, err := testutil.RefreshApp(ctx, a.conn, tb)
if err != nil {
return err
}
// Releases the previous app window.
if err := a.appWindow.Release(ctx); err != nil {
return errors.Wrap(err, "failed to release app window")
}
a.appWindow = newAppWindow
if err := loadScripts(ctx, a.conn, a.scriptPaths); err != nil {
return errors.Wrap(err, "failed to load scripts")
}
return nil
}
// SaveScreenshot saves a screenshot in the outDir.
func (a *App) SaveScreenshot(ctx context.Context) error {
filename := fmt.Sprintf("screenshot_%d.png", time.Now().UnixNano())
path := filepath.Join(a.outDir, filename)
return screenshot.CaptureChrome(ctx, a.cr, path)
}