| // 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 ash |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "image" |
| "image/png" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "time" |
| |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/chrome" |
| "chromiumos/tast/local/chrome/internal/cdputil" |
| "chromiumos/tast/local/chrome/internal/extension" |
| ) |
| |
| // AppListBubbleClassName is the automation API class name of the bubble launcher. |
| const AppListBubbleClassName = "AppListBubbleView" |
| |
| // LauncherState represents the launcher (a.k.a AppList) state. |
| type LauncherState string |
| |
| // LauncherState as defined in |
| // https://cs.chromium.org/chromium/src/ash/public/cpp/app_list/app_list_types.h |
| const ( |
| Peeking LauncherState = "Peeking" |
| FullscreenAllApps LauncherState = "FullscreenAllApps" |
| FullscreenSearch LauncherState = "FullscreenSearch" |
| Half LauncherState = "Half" |
| Closed LauncherState = "Closed" |
| ) |
| |
| // Accelerator represents the accelerator key to trigger certain actions. |
| type Accelerator struct { |
| KeyCode string `json:"keyCode"` |
| Shift bool `json:"shift"` |
| Control bool `json:"control"` |
| Alt bool `json:"alt"` |
| Search bool `json:"search"` |
| } |
| |
| // Accelerator key used to trigger launcher state change. |
| var ( |
| AccelSearch = Accelerator{KeyCode: "search", Shift: false, Control: false, Alt: false, Search: false} |
| AccelShiftSearch = Accelerator{KeyCode: "search", Shift: true, Control: false, Alt: false, Search: false} |
| ) |
| |
| // WaitForLauncherState waits until the launcher state becomes state. It waits |
| // up to 10 seconds and fail if the launcher doesn't have the desired state. |
| // Expected to fail with "Not supported for bubble launcher" error when waiting |
| // for state different from "Closed" if called for clamshell productivity (bubble) |
| // launcher. Note that the autotest API is expected to return immediately, but still |
| // asynchronously, in this case. |
| // NOTE: Waiting for "Closed" state will always wait for the fullscreen launcher to |
| // hide, even if one would otherwise expect bubble launcher to be used for the current |
| // session state - this supports waiting for launcher UI hide animation to complete |
| // after transitioning from tablet mode to clamshell. |
| func WaitForLauncherState(ctx context.Context, tconn *chrome.TestConn, state LauncherState) error { |
| ctx, cancel := context.WithTimeout(ctx, 10*time.Second) |
| defer cancel() |
| if err := tconn.Call(ctx, nil, "tast.promisify(chrome.autotestPrivate.waitForLauncherState)", state); err != nil { |
| return errors.Wrap(err, "failed to wait for launcher state") |
| } |
| return nil |
| } |
| |
| // TriggerLauncherStateChange will cause the launcher state change via accelerator. |
| func TriggerLauncherStateChange(ctx context.Context, tconn *chrome.TestConn, accel Accelerator) error { |
| // Send the press event to store it in the history. It'll not be handled, so ignore the result. |
| if err := tconn.Call(ctx, nil, `async (acceleratorKey) => { |
| acceleratorKey.pressed = true; |
| chrome.autotestPrivate.activateAccelerator(acceleratorKey, () => {}); |
| acceleratorKey.pressed = false; |
| await tast.promisify(chrome.autotestPrivate.activateAccelerator)(acceleratorKey); |
| }`, accel); err != nil { |
| return errors.Wrap(err, "failed to execute accelerator") |
| } |
| return nil |
| } |
| |
| func scaleImage(src image.Image, siz int) image.Image { |
| srcSize := src.Bounds().Size().X |
| scaled := image.NewRGBA(image.Rect(0, 0, siz, siz)) |
| for x := 0; x < siz; x++ { |
| for y := 0; y < siz; y++ { |
| scaled.Set(x, y, src.At(x*srcSize/siz, y*srcSize/siz)) |
| } |
| } |
| return scaled |
| } |
| |
| func saveImageAsPng(filename string, img image.Image) error { |
| w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644) |
| if err != nil { |
| return err |
| } |
| defer w.Close() |
| return png.Encode(w, img) |
| } |
| |
| // generateFakeAppNames generates default names for fake apps. |
| func generateFakeAppNames(numFakeApps int) []string { |
| fakeAppNames := make([]string, numFakeApps) |
| for i := 0; i < numFakeApps; i++ { |
| fakeAppNames[i] = fmt.Sprintf("fake app %d", i) |
| } |
| return fakeAppNames |
| } |
| |
| // GeneratePrepareFakeAppsWithNamesOptions calls PrepareDefaultFakeApps() and |
| // returns options to be used by chrome.New() for logging in with the newly |
| // created fake apps. baseDir is the path to the directory for keeping app data. |
| // The function caller should always clean baseDir regardless of function |
| // execution results. names specify app names. |
| func GeneratePrepareFakeAppsWithNamesOptions(baseDir string, names []string) ([]chrome.Option, error) { |
| dirs, err := PrepareDefaultFakeApps(baseDir, names, true) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to create fake apps") |
| } |
| |
| opts := make([]chrome.Option, 0, len(names)) |
| for _, dir := range dirs { |
| opts = append(opts, chrome.UnpackedExtension(dir)) |
| } |
| return opts, nil |
| } |
| |
| // GeneratePrepareFakeAppsWithIconDataOptions is similar with GeneratePrepareFakeAppsWithNamesOptions, |
| // with a difference that GeneratePrepareFakeAppsWithIconDataOptions allows the |
| // caller to specify both app names and icon data. The caller has the duty to |
| // clean baseDir. |
| func GeneratePrepareFakeAppsWithIconDataOptions(baseDir string, names []string, iconData [][]byte) ([]chrome.Option, error) { |
| if len(names) != len(iconData) { |
| return nil, errors.Errorf("unexpected count of icon data: got %d, expecting %d", len(iconData), len(names)) |
| } |
| |
| dirs, err := prepareFakeAppsWithIconData(baseDir, names, iconData) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to prepare data for fake apps") |
| } |
| |
| opts := make([]chrome.Option, 0, len(names)) |
| for _, dir := range dirs { |
| opts = append(opts, chrome.UnpackedExtension(dir)) |
| } |
| return opts, nil |
| } |
| |
| // GeneratePrepareFakeAppsOptions is similar with GeneratePrepareFakeAppsWithNamesOptions, |
| // with a difference that GeneratePrepareFakeAppsOptions accepts the fake app |
| // count as the parameter. |
| func GeneratePrepareFakeAppsOptions(baseDir string, numFakeApps int) ([]chrome.Option, error) { |
| return GeneratePrepareFakeAppsWithNamesOptions(baseDir, generateFakeAppNames(numFakeApps)) |
| } |
| |
| // prepareFakeApp creates data for a fake app with the specified app name and |
| // icon (if any). |
| func prepareFakeApp(baseDir, appName, iconDir string, iconFileMap map[int]string) (string, error) { |
| // The manifest.json data for the fake hosted app; it just opens google.com |
| // page on launch. |
| const manifestTmpl = `{ |
| "description": "fake", |
| "name": "%s", |
| "manifest_version": 2, |
| "version": "0", |
| %s |
| "app": { |
| "launch": { |
| "web_url": "https://www.google.com/" |
| } |
| } |
| }` |
| |
| extDir := filepath.Join(baseDir, appName) |
| if err := os.Mkdir(extDir, 0755); err != nil { |
| return "", errors.Wrapf(err, "failed to create the directory for %s", appName) |
| } |
| |
| var iconJSON string |
| if iconDir != "" { |
| for _, iconFileName := range iconFileMap { |
| if err := os.Symlink(filepath.Join(iconDir, iconFileName), filepath.Join(extDir, iconFileName)); err != nil { |
| return "", errors.Wrapf(err, "failed to create link of icon %s", iconFileName) |
| } |
| } |
| |
| iconJSONData, err := json.Marshal(iconFileMap) |
| if err != nil { |
| return "", errors.Wrap(err, "failed to turn the mapptings between icon sizes and icon names into a JSON string") |
| } |
| iconJSON = fmt.Sprintf(`"icons": %s,`, string(iconJSONData)) |
| } |
| |
| if err := ioutil.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(fmt.Sprintf(manifestTmpl, appName, iconJSON)), 0644); err != nil { |
| return "", errors.Wrapf(err, "failed to prepare manifest.json for %s", appName) |
| } |
| |
| return extDir, nil |
| } |
| |
| // prepareFakeAppIcon creates icon images in different scales with the given |
| // icon data. These images are stored in a directory created under baseDir. |
| // iconFolder specifies the directory's name. |
| func prepareFakeAppIcon(baseDir, iconFolder string, iconData []byte) (string, map[int]string, error) { |
| iconDir := filepath.Join(baseDir, iconFolder) |
| if err := os.Mkdir(iconDir, 0755); err != nil { |
| return "", nil, errors.Wrapf(err, "failed to create the icon directory %q", iconDir) |
| } |
| |
| img, err := png.Decode(bytes.NewReader(iconData)) |
| if err != nil { |
| return "", nil, errors.Wrap(err, "failed to decode icon data") |
| } |
| |
| iconFiles := map[int]string{} |
| for _, siz := range []int{32, 48, 64, 96, 128, 192} { |
| var imgToSave image.Image |
| if siz == img.Bounds().Size().X { |
| imgToSave = img |
| } else { |
| imgToSave = scaleImage(img, siz) |
| } |
| iconFile := fmt.Sprintf("icon%d.png", siz) |
| iconFileFullPath := filepath.Join(iconDir, iconFile) |
| if err := saveImageAsPng(iconFileFullPath, imgToSave); err != nil { |
| return "", nil, errors.Wrapf(err, "failed to save the icon file to %q", iconFileFullPath) |
| } |
| iconFiles[siz] = iconFile |
| } |
| |
| return iconDir, iconFiles, nil |
| } |
| |
| // PrepareDefaultFakeApps creates directories for fake apps (hosted apps) under |
| // the directory of baseDir and returns their path names. Fake app names are |
| // specified by the parameter. hasIcon specifies whether a default icon should |
| // be used. The intermediate data may remain even when an error is returned. It |
| // is the caller's responsibility to clean up the contents under the baseDir. |
| // This also may update the ownership of baseDir. |
| func PrepareDefaultFakeApps(baseDir string, appNames []string, hasIcon bool) ([]string, error) { |
| if err := extension.ChownContentsToChrome(baseDir); err != nil { |
| return nil, errors.Wrapf(err, "failed to change ownership of %q", baseDir) |
| } |
| |
| var iconDir string |
| var iconFiles map[int]string |
| var err error |
| if hasIcon { |
| iconDir, iconFiles, err = prepareFakeAppIcon(baseDir, "defaultIcons", fakeIconData) |
| if err != nil { |
| return nil, errors.Wrap(err, "failed to parepare the shared icon for fake apps") |
| } |
| } |
| |
| var dirs []string |
| for _, appName := range appNames { |
| dir, err := prepareFakeApp(baseDir, appName, iconDir, iconFiles) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to prepare data for %q", appName) |
| } |
| dirs = append(dirs, dir) |
| } |
| |
| return dirs, nil |
| } |
| |
| // prepareFakeAppsWithIconData is similar with PrepareDefaultFakeApps, but with |
| // the difference that app icons are specified by the parameter. |
| func prepareFakeAppsWithIconData(baseDir string, appNames []string, iconData [][]byte) ([]string, error) { |
| if len(appNames) != len(iconData) { |
| return nil, errors.Errorf("unexpected count of icon data: got %d, expecting %d", len(iconData), len(appNames)) |
| } |
| |
| if err := extension.ChownContentsToChrome(baseDir); err != nil { |
| return nil, errors.Wrapf(err, "failed to change ownership of %q", baseDir) |
| } |
| |
| var dirs []string |
| for index, appName := range appNames { |
| iconDir, iconFiles, err := prepareFakeAppIcon(baseDir, appName+"Icons", iconData[index]) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to parepare icons for the fake app %q", appName) |
| } |
| |
| dir, err := prepareFakeApp(baseDir, appName, iconDir, iconFiles) |
| if err != nil { |
| return nil, errors.Wrapf(err, "failed to prepare data for %q", appName) |
| } |
| dirs = append(dirs, dir) |
| } |
| |
| return dirs, nil |
| } |
| |
| // The remaining definitions are needed only for faillog & CaptureCDP. |
| // TODO(crbug.com/1271473): Get rid of them. |
| // They expose cdputil types and values. See the cdputil package for details. |
| |
| // DebuggingPortPath is a file where Chrome writes debugging port. |
| const DebuggingPortPath = cdputil.DebuggingPortPath |
| |
| // DevtoolsConn is the connection to a web content view, e.g. a tab. |
| type DevtoolsConn = cdputil.Conn |
| |
| // Session maintains the connection to talk to the browser in Chrome DevTools Protocol |
| // over WebSocket. |
| type Session = cdputil.Session |
| |
| // PortWaitOption controls whether the NewSession should wait for the port file |
| // to be created. |
| type PortWaitOption = cdputil.PortWaitOption |
| |
| // PortWaitOption values. |
| const ( |
| NoWaitPort PortWaitOption = cdputil.NoWaitPort |
| WaitPort PortWaitOption = cdputil.WaitPort |
| ) |
| |
| // NewDevtoolsSession establishes a Chrome DevTools Protocol WebSocket connection to the browser. |
| func NewDevtoolsSession(ctx context.Context, debuggingPortPath string, portWait PortWaitOption) (sess *Session, retErr error) { |
| return cdputil.NewSession(ctx, debuggingPortPath, portWait) |
| } |