github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/docs/codelab_3.md (about)

     1  # Tast Codelab: Chrome UI Automation (go/tast-codelab-3)
     2  
     3  > This document assumes that you've already gone through [Codelab #1].
     4  
     5  This codelab follows the creation of a Tast test that uses the the
     6  chrome.Automation library to change the wallpaper. It goes over the background
     7  of chrome.Automation, how to use it, and some common issues.
     8  
     9  [Codelab #1]: codelab_1.md
    10  
    11  
    12  ## Background
    13  
    14  The [chrome.automation] library uses the Chrome Accessibility Tree to view and
    15  control the current state of the UI. The Accessibility Tree has access to:
    16  * The Chrome Browser
    17  * The ChromeOS Desktop UI
    18  * ChromeOS packaged apps
    19  * Web Apps/PWAs
    20  
    21  That being said, it does not have access to UI elements in containers or VMs
    22  (like ARC and Crostini).
    23  
    24  The Accessibility Tree is a collection of nodes that map out the entire desktop.
    25  Accessibility Tree nodes are similar to HTML nodes, but definitely do not map to
    26  HTML nodes. An [Accessibility Node] has many attributes, including but not limited
    27  to:
    28  * ID -> This changes between tests runs and cannot be used in tests.
    29  * [Role]
    30  * Class
    31  * Name -> This is language dependent but often the only unique identifier.
    32  * [Location]
    33  * Parent Node
    34  * Children Nodes
    35  * [States List]
    36  
    37  In Tast, [chrome.automation] is wrapped in [chrome/uiauto] and can be imported like so:
    38  ```go
    39  import "go.chromium.org/tast-tests/cros/local/chrome/uiauto"
    40  ```
    41  [Accessibility Node]: https://developer.chrome.com/docs/extensions/reference/automation/#type-AutomationNode
    42  [chrome.automation]: https://developer.chrome.com/docs/extensions/reference/automation/
    43  [chrome/uiauto]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto
    44  [Role]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/role#Role
    45  [Location]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/coords#Rect
    46  [States List]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/state#State
    47  
    48  
    49  ## Simple Starter Test
    50  
    51  A good starting point for most chrome.Automation tests is to use the
    52  "chromeLoggedIn" fixture and then force the test to fail and give you a
    53  dump of the Accessibility tree. That way you can look at the tree and decide what
    54  node you want to interact with. Here is some sample code:
    55  ```go
    56  func init() {
    57  	testing.AddTest(&testing.Test{
    58  		Func: Change,
    59  		Desc: "Follows the user flow to change the wallpaper",
    60  		Contacts: []string{
    61  			"my-group@chromium.org",
    62  			"my-ldap@chromium.org",
    63  		},
    64  		BugComponent: "b:1034625",
    65  		Attr:         []string{"group:mainline", "informational"},
    66  		SoftwareDeps: []string{"chrome"},
    67  		Fixture:      "chromeLoggedIn",
    68  	})
    69  }
    70  
    71  func Change(ctx context.Context, s *testing.State) {
    72  	cr := s.FixtValue().(*chrome.Chrome)
    73  	tconn, err := cr.TestAPIConn(ctx)
    74  	if err != nil {
    75  		s.Fatal("Failed to create Test API connection: ", err)
    76  	}
    77  	defer faillog.DumpUITreeOnError(ctx, s.OutDir(), s.HasError, tconn)
    78  
    79  	// Put test code here.
    80  
    81  	s.Fatal("I would like a UI dump")
    82  }
    83  ```
    84  
    85  # Interacting with the Accessibility Tree
    86  
    87  After running the test on a device, you should be able to find the UI dump at:
    88  `${CHROMEOS_SRC}/chroot/tmp/tast/results/latest/tests/${TEST_NAME}/faillog/ui_tree.txt`
    89  
    90  The tree can be a little complex and unintuitive at times, but it should have
    91  nodes for anything we are looking for.
    92  
    93  > Note: You can inspect the standard UI by enabling
    94  chrome://flags/#enable-ui-devtools on your device, going to
    95  chrome://inspect/#other, and clicking inspect under UiDevToolsClient. More
    96  details available [here].
    97  
    98  > Note: You can interact directly with chrome.Automation on your device by:
    99  Opening chrome, clicking Test Api Extension(T in top right) > Manage extensions,
   100  Enabling Developer mode toggle, Clicking background page > Console. It has a
   101  [Codelab].
   102  
   103  [here]: https://sites.google.com/a/chromium.org/dev/developers/how-tos/inspecting-ash
   104  [Codelab]: https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/docs/chrome-automation-codelab.md?q=chrome-automation-codelab.md
   105  
   106  
   107  
   108  In this case, we want to start by right clicking on the wallpaper. Looking at
   109  the tree, it looks like we will want to right click
   110  `node id=37 role=unknown state={} parentID=36 childIds=[] className=WallpaperView`.
   111  It looks like its class name is a unique identifier we
   112  can use to find it, so let's find and right click that node:
   113  ```go
   114  ui := uiauto.New(tconn)
   115  if err := ui.RightClick(nodewith.ClassName("WallpaperView"))(ctx); err != nil {
   116    s.Fatal("Failed to right click the wallpaper view: ", err)
   117  }
   118  ```
   119  Now those few lines are pretty simple, but introduce a lot of library specific information.
   120  Lets break that down some.
   121  
   122  Firstly, there is the [nodewith] package that is used to describe a way to find a node.
   123  With it, you can specify things like the [Name("")], [Role(role.Button)], or [Focused()].
   124  A chain of nodes can be defined by using [Ancestor(ancestorNode)].
   125  
   126  [nodewith]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith
   127  [Name("")]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith#Name
   128  [Role(role.Button)]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith#Role
   129  [Focused()]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith#Focused
   130  [Ancestor(ancestorNode)]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith#Ancestor
   131  
   132  The a11y tree can sometimes be hard to interact with directly.
   133  From nodes moving around to parts of the tree temporarily disappearing,
   134  this instability can often lead to flakes in tests.
   135  [uiauto.Context] is focused on creating a flake resistant way to interact with a11y tree.
   136  By default, it uses polling to wait for stability before performing actions.
   137  These actions include things like [LeftClick], [WaitUntilExists], and [FocusAndWait].
   138  If for some reason the default polling options do not work for your test case,
   139  you can modify them with [WithTimeout], [WithInterval], and [WithPollOpts].
   140  For example, if we needed a longer timeout to ensure the location was stable before
   141  right clicking, we could write:
   142  ```go
   143  ui.WithTimeout(time.Minute).RightClick(nodewith.ClassName("WallpaperView"))
   144  ```
   145  
   146  [uiauto.Context]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context
   147  [LeftClick]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.LeftClick
   148  [WaitUntilExists]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.WaitUntilExists
   149  [FocusAndWait]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.FocusAndWait
   150  [WithTimeout]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.WithTimeout
   151  [WithInterval]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.WithInterval
   152  [WithPollOpts]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Context.WithPollOpts
   153  
   154  Finally, you may have noticed the slightly strange syntax `(ctx)` after
   155  `ui.RightClick(nodewith.ClassName("WallpaperView"))`.
   156  This is because `ui.RightClick` returns a `uiauto.Action`.
   157  A [uiauto.Action] is just a `func(context.Context) error`.
   158  It is used to enable easy chaining of multiple actions.
   159  For example, if you wanted to right click a node, left click a different node,
   160  and then wait for a third node to exist, you could write:
   161  ```go
   162  if err := ui.RightClick(node1)(ctx); err != nil {
   163    s.Fatal("Failed to right click node1: ", err)
   164  }
   165  if err := ui.LeftClick(node2)(ctx); err != nil {
   166    s.Fatal("Failed to left click node2: ", err)
   167  }
   168  if err := ui.WaitUntilExists(node3)(ctx); err != nil {
   169    s.Fatal("Failed to wait for node3: ", err)
   170  }
   171  ```
   172  Or, you could use [uiauto.Combine] to deal with these actions as a group:
   173  ```go
   174  if err := uiauto.Combine("do some bigger action",
   175    ui.RightClick(node1),
   176    ui.LeftClick(node2),
   177    ui.WaitUntilExists(node3),
   178  )(ctx); err != nil {
   179    s.Fatal("Failed to do some bigger action: ", err)
   180  }
   181  ```
   182  
   183  > Note: I generally advise using [uiauto.Combine] if you are doing more
   184  than one action in a row.
   185  
   186  [uiauto.Action]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Action
   187  [uiauto.Combine]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Combine
   188  
   189  ## Dealing With a Race Condition
   190  
   191  Now if we look at `ui_tree.txt`, we can see the right click menu:
   192  ```
   193  node id=219 role=menu state={"vertical":true} parentID=218 childIds=[220,222,225] className=SubmenuView
   194    node id=220 role=menuItem state={"focusable":true} parentID=219 childIds=[] name=Autohide shelf className=MenuItemView
   195    node id=222 role=menuItem state={"focusable":true} parentID=219 childIds=[] name=Shelf position className=MenuItemView
   196    node id=225 role=menuItem state={"focusable":true} parentID=219 childIds=[] name=Set wallpaper  style className=MenuItemView
   197  ```
   198  
   199  > Note: If you don't see an update to `ui_tree.txt`, you may need to add
   200  `testing.Sleep(time.Second)` before causing the test to fail. Events are
   201  asynchronous and might not immediately update the UI tree.
   202  
   203  Next, we want to click on the "Set wallpaper & style" menu item:
   204  ```go
   205  if err := ui.LeftClick(nodewith.Name("Set wallpaper  style").Role(role.MenuItem))(ctx); err != nil {
   206    s.Fatal(...)
   207  }
   208  ```
   209  > Warning: Getting nodes by human-readable name is **strongly discouraged** in general
   210  because it requires to keep updating the UI string and the literal in the test in sync.
   211  We use `nodewith.Name()` to get the menu item only because all the menu items share the
   212  same class name.
   213  
   214  When you run the test, depending on the speed of your device and your luck, the
   215  "Set wallpaper & style" menu item may or may not have been clicked. We have just hit a
   216  race condition where the menu may not be fully ready to be clicked by
   217  the time that we try to click it. To fix this, we will simply keep clicking the
   218  menu item until it no longer exists:
   219  ```go
   220  personalizeMenu := nodewith.Name("Set wallpaper  style").Role(role.MenuItem)
   221  if err := ui.LeftClickUntil(personalizeMenu, ui.Gone(personalizeMenu))(ctx); err != nil {
   222    s.Fatal(...)
   223  }
   224  ```
   225  
   226  > Note: Most nodes will not have race conditions and do not require this extra
   227  work. The issue is that we do not have a indicator for when the menu
   228  button is ready to be clicked.
   229  
   230  After opening the personalization app, we will proceed to the wallpaper subpage to change the wallpaper.
   231  ```
   232  changeWallpaperButton := nodewith.Role(role.Button).Name("Change wallpaper")
   233  uiauto.Combine("change the wallpaper",
   234  	ui.WaitUntilExists(changeWallpaperButton),
   235  	ui.LeftClick(changeWallpaperButton),
   236  )(ctx)
   237  ```
   238  
   239  ## More Basic Interactions
   240  
   241  Now that the wallpaper subpage is open, let's set the background to a solid color.
   242  We left click for the node corresponding to the 'Solid colors' tab in `ui_tree.txt`:
   243  ```
   244  node id=245 role=genericContainer state={} parentID=243 childIds=[250,251]
   245    node id=250 role=paragraph state={} parentID=245 childIds=[252] name=Solid colors
   246      node id=252 role=staticText state={} parentID=250 childIds=[362] name=Solid colors
   247        node id=362 role=inlineTextBox state={} parentID=252 childIds=[] name=Solid colors
   248  ```
   249  ```go
   250  if err := ui.LeftClick(nodewith.Name("Solid colors").Role(role.StaticText))(ctx); err != nil {
   251    s.Fatal(...)
   252  }
   253  ```
   254  
   255  Personally, I am a fan of the 'Deep Purple' background, so that is what I am going
   256  to pick:
   257  ```
   258  node id=410 role=listBoxOption state={"focusable":true} parentID=409 childIds=[477] name=Deep Purple
   259  ```
   260  ```go
   261  if err := ui.LeftClick(nodewith.Name("Deep Purple").Role(role.ListBoxOption))(ctx); err != nil {
   262    s.Fatal(...)
   263  }
   264  ```
   265  
   266  ## Scrolling to Target
   267  
   268  We found the above code fails to find the "Deep Purple" node on some device
   269  models. We examined and found that the "Solid color" list item was not visible
   270  without scrolling. This could be verified either by seeing the DUT screen or
   271  by seeing the node having "offscreen" state true:
   272  ```
   273  node id=252 role=staticText state={"offscreen":true} parentID=250 childIds=[362] name=Solid colors
   274    node id=362 role=inlineTextBox state={"offscreen":true} parentID=252 childIds=[] name=Solid colors
   275  ```
   276  
   277  This happened due to different screen sizes of devices, which affects the
   278  window size. In order to make this test more robust, we need to make the item
   279  visible before clicking:
   280  
   281  ```go
   282  if err := ui.MakeVisible(nodewith.Name("Solid colors").Role(role.StaticText))(ctx); err != nil {
   283    s.Fatal(...)
   284  }
   285  // same as the previsous section
   286  if err := ui.LeftClick(nodewith.Name("Solid colors").Role(role.StaticText))(ctx); err != nil {
   287    s.Fatal(...)
   288  }
   289  ```
   290  
   291  However, there is still a race with this. The list items are loaded
   292  asynchronously. (You may be able to see only the first item is shown in the
   293  list and then the others are loaded few seconds later.)
   294  So the item may not exist in the accessibility tree yet, right after previous
   295  step. Therefore we will wait until the item appears:
   296  
   297  ```go
   298  solidColorsMenu := nodewith.Name("Solid colors").Role(role.StaticText)
   299  if err := ui.WaitUntilExists(solidColorsMenu)(ctx); err != nil {
   300    s.Fatal(...)
   301  }
   302  if err := ui.MakeVisible(solidColorsMenu)(ctx); err != nil {
   303    s.Fatal(...)
   304  }
   305  if err := ui.LeftClick(solidColorsMenu)(ctx); err != nil {
   306    s.Fatal(...)
   307  }
   308  ```
   309  
   310  Note that `ui.LeftClick` has integrated logic to wait until the target is
   311  stable (i.e. exists and its position kept unchanged) but `MakeVisible`
   312  doesn't.
   313  
   314  ## Ensuring the Background Changed
   315  
   316  Checking that a test succeeded can often be harder than expected. In this case,
   317  we have to decide what demonstrates a successful wallpaper change. A good solution
   318  would probably be to check a pixel in the background and make sure it is the
   319  same color as deep purple. Sadly, that is not currently easy to do in Tast. A
   320  simpler solution for now is to check for the text 'Deep Purple' in the heading
   321  because the wallpaper picker displays the name of the currently selected wallpaper:
   322  ```
   323  node id=109 role=heading state={} parentID=34 childIds=[] name=Currently set Deep Purple
   324  ```
   325  ```go
   326  if err := ui.WaitUntilExists(nodewith.NameContaining("Deep Purple").Role(role.Heading))(ctx); err != nil {
   327    s.Fatal(...)
   328  }
   329  ```
   330  
   331  ## Full Code
   332  
   333  > Note: The code below is using [uiauto.Combine] to simplify all of the steps above into
   334  one chain of operations.
   335  
   336  [uiauto.Combine]: https://pkg.go.dev/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/uiauto#Combine
   337  
   338  ```go
   339  // Copyright <copyright_year> The ChromiumOS Authors
   340  // Use of this source code is governed by a BSD-style license that can be
   341  // found in the LICENSE file.
   342  
   343  package wallpaper
   344  
   345  import (
   346  	"context"
   347  	"time"
   348  
   349  	"go.chromium.org/tast-tests/cros/local/chrome"
   350  	"go.chromium.org/tast-tests/cros/local/chrome/uiauto"
   351  	"go.chromium.org/tast-tests/cros/local/chrome/uiauto/faillog"
   352  	"go.chromium.org/tast-tests/cros/local/chrome/uiauto/nodewith"
   353  	"go.chromium.org/tast-tests/cros/local/chrome/uiauto/role"
   354  	"go.chromium.org/tast/core/testing"
   355  )
   356  
   357  func init() {
   358  	testing.AddTest(&testing.Test{
   359  		Func: Change,
   360  		LacrosStatus: testing.LacrosVariantUnknown,
   361  		Desc: "Follows the user flow to change the wallpaper",
   362  		Contacts: []string{
   363  			"chromeos-sw-engprod@google.com",
   364  		},
   365  		BugComponent: "b:1034625",
   366  		Attr:         []string{"group:mainline", "informational"},
   367  		SoftwareDeps: []string{"chrome"},
   368  		Fixture:      "chromeLoggedIn",
   369  	})
   370  }
   371  
   372  func Change(ctx context.Context, s *testing.State) {
   373  	cr := s.FixtValue().(*chrome.Chrome)
   374  	tconn, err := cr.TestAPIConn(ctx)
   375  	if err != nil {
   376  		s.Fatal("Failed to create Test API connection: ", err)
   377  	}
   378  	defer faillog.DumpUITreeOnError(ctx, s.OutDir(), s.HasError, tconn)
   379  
   380  	ui := uiauto.New(tconn)
   381  	personalizeMenu := nodewith.Name("Set wallpaper  style").Role(role.MenuItem)
   382  	changeWallpaperButton := nodewith.Role(role.Button).Name("Change wallpaper")
   383  	solidColorsMenu := nodewith.Name("Solid colors").Role(role.StaticText)
   384  	if err := uiauto.Combine("change the wallpaper",
   385  		ui.RightClick(nodewith.ClassName("WallpaperView")),
   386  		// This button takes a bit before it is clickable.
   387  		// Keep clicking it until the click is received and the menu closes.
   388  		ui.WithInterval(1*time.Second).LeftClickUntil(personalizeMenu, ui.Gone(personalizeMenu)),
   389  		ui.Exists(nodewith.NameContaining("Wallpaper & style").Role(role.Window).First()),
   390  		ui.WaitUntilExists(changeWallpaperButton),
   391  		ui.LeftClick(changeWallpaperButton),
   392  		ui.WaitUntilExists(solidColorsMenu),
   393  		ui.MakeVisible(solidColorsMenu),
   394  		ui.LeftClick(solidColorsMenu),
   395  		ui.LeftClick(nodewith.Name("Deep Purple").Role(role.ListBoxOption)),
   396  		// Ensure that "Deep Purple" text is displayed.
   397  		// The UI displays the name of the currently set wallpaper.
   398  		ui.WaitUntilExists(nodewith.NameContaining("Deep Purple").Role(role.Heading)),
   399  	)(ctx); err != nil {
   400  		s.Fatal("Failed to change the wallpaper: ", err)
   401  	}
   402  }
   403  ```