github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/docs/howto.md (about) 1 # Tast How-To: (go/tast-howto) 2 3 > This document assumes that you've already gone through [Codelab #1]. 4 5 This document is intended to give an overview of some of the possibilities you have for the set up and evaluation of your Tast test. This is not an exhaustive list but contains some of the most used techniques that are available to create tests. 6 7 [Codelab #1]: codelab_1.md 8 9 ## Evaluation 10 ### Checking wrapped errors 11 Go offers the functionality to wrap errors in other errors to allow returning all occurred error messages from a function call. To check if any of these wrapped errors is of a specific type that should be handled differently [errors.Is] can be used. 12 ``` 13 var ErrWindowNotFound = errors.New("window not found") 14 // FindMinimizedWindow returns a minimized window, if any. If there is no minimized 15 // window, ErrWindowNotFound is returned. 16 func FindMinimizedWindow() (*Window, error) { 17 ws, err := findAllWindows() 18 if err != nil { 19 return nil, err 20 } 21 for _, w := range ws { 22 if w.Minimized { 23 return w, nil 24 } 25 } 26 return nil, ErrWindowNotFound 27 } 28 29 func someFunction(...) error { 30 w, err := FindMinimizedWindow() 31 if err != nil { 32 if errors.Is(err, ErrWindowNotFound) { 33 return nil 34 } 35 return err 36 } 37 ... 38 } 39 ``` 40 41 [errors.Is]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/errors#Is 42 43 ### Command line 44 As ChromeOS is based on Linux we can execute Linux commands on the command line that can give us the needed information of the state of the system. This is done with the [testexec.CommandContext] function. 45 The `CommandContext()` function wraps the standard go exec package to honor the timeout of the context in which the test is running. 46 ``` 47 out, err := testexec.CommandContext(ctx, "lshw", "-C", "multimedia").Output(testexec.DumpLogOnError) 48 if err != nil { 49 // Do error handling here. 50 } 51 ``` 52 In the example the command `lshw -C multimedia` is executed on the command line. The output of the execution is written into the out variable by calling the `Output` function and should then contain a list of all connected multimedia devices. 53 By passing `testexec.DumpLogOnError` we also get the stderr output in case the execution fails. 54 55 [testexec.CommandContext]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/common/testexec#CommandContext 56 57 ### Checking windows 58 In some cases checking if certain windows have been opened, or a certain number of windows have been opened can be enough to check if a test was successful or not. To do that [ash.GetAllWindows] can be used. See the [ash package documentation] for more information. 59 It requires a context and a test connection, which is obtained by a call to [chrome.TestAPIConn], and will return an array of all open windows. This array can then be checked for example for the title of the windows to see if a desired window is open. 60 ``` 61 tconn, err := cr.TestAPIConn(ctx) 62 if err != nil { 63 // Do error handling here. 64 } 65 66 ws, err := ash.GetAllWindows(ctx, tconn) 67 if err != nil { 68 // Do error handling here. 69 } 70 71 // Find the desired window with expectedTitle. 72 for _, w := range ws { 73 if strings.Contains(w.Title, expectedTitle) { 74 // The test was successful. 75 } 76 } 77 ``` 78 79 [ash.GetAllWindows]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome/ash#GetAllWindows 80 [chrome.TestAPIConn]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome#Chrome.TestAPIConn 81 [ash package documentation]: https://chromium.googlesource.com/chromium/src/+/HEAD/ash/README.md 82 83 ### Checking files 84 Similar to the windows the existence or non exsitence of a file might be the needed information to determine if the test was successful. 85 Go offers the [os.Stat] function for checking the existence or attributes of files. 86 ``` 87 fileInfo, err := os.Stat("/path/to/file/myfile") 88 if os.IsNotExist(err) { 89 return ... // File was not found 90 } 91 if err != nil { 92 return ... // Unknown error occurred 93 } 94 // File exists, fileInfo is valid 95 ``` 96 If [os.Stat] doesn't return an error, the file exists and additional information about the file is written to `fileInfo`. 97 98 [os.Stat]: https://golang.org/pkg/os/#Stat 99 100 ### JavaScript evaluation 101 With [chrome.Conn.Eval] arbitrary JavaScript expressions can be evaluated. The function takes a context, a JavaScript expression and an interface as arguments. If the JavaScript expression returns a value, it will be unmarshaled into the given interface parameter. If umarshalling fails an error will be returned. 102 103 ``` 104 conn, err := cr.NewConn(ctx, URL) 105 if err != nil { 106 // Do error handling here. 107 } 108 defer conn.Close() 109 110 var message string 111 if err := conn.Eval(ctx, `document.getElementById('element_id').innerText`, &message); err != nil { 112 // Do error handling here. 113 } 114 if strings.Contains(message, 'this is the element_id') { 115 // The test was successful. 116 } 117 ``` 118 In this example we open a new Chrome window with some URL and then we evaluate the JavaScript expression `document.getElementById('element_id').innerText` in this Chrome window. The result is written into the string message and is then checked if it contains a desired text. 119 This can also be used for expressions returning a promise, in which case the function will wait until the promise is settled. 120 ``` 121 const code = `return new Promise((resolve, reject) => { 122 const element = document.getElementById('element_id'); 123 if (element === null) { 124 resolve(false); 125 return; 126 } 127 if (element.innerText !== 'some text') { 128 reject(new Error('Unexpected inner text: want some text; got ' + element.innerText)); 129 return; 130 } 131 resolve(true); 132 })` 133 134 var found bool 135 if err := conn.Eval(ctx, code, &found); err != nil { 136 // Do error handling here. 137 } 138 ``` 139 The [chrome.Conn.Eval] can also be used on the connection to Tast's test extension which gives access to other APIs. A connection can be created with [chrome.TestAPIConn]. The connection to Tast's test extension should not be closed as it is shared. 140 141 [chrome.Conn.Eval]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/local/chrome#Conn.Eval 142 143 ### Find the JavaScript path for an element 144 In the previous paragraph we took a look at how to evaluate JavaScript expressions in a Tast test, however finding the JavaScript expression you need for a certain test can be difficult. 145 The Developer Tools of Chrome can be very helpful for that. Open them by pressing CTRL + SHIFT + I (or by opening the menu -> more Tools -> Developer Tools) in a Chrome window. In the Elements tab you can browse through the elements of a page and expand them. The selected element will be highlighted. Once you got to the element you want to check right click it in the Elements tab and select Copy -> Copy JS path. This gives you the expression to get the desired element. In the Console Tab you can try beforehand if the JavaScript expression you want to use delivers the desired output. 146 147 ### Interacting with the UI 148 It is also possible to directly interact with the elements of the UI (like clicking on them, or just hovering over the mouse), or just to get information about their status. This can be done through the Test API with the help of the automation library. The basics of the usage of this library can be found in [Codelab #3]. 149 150 [Codelab #3]: codelab_3.md 151 152 ### Waiting in tests 153 To check some condition it is sometimes necessary to wait until certain changes have been processed in ChromeOS. For such cases the [testing.Poll] function should be used instead of sleeping in tests, as it does not introduce unnecessary delays and race conditions in integration tests. See also [Context and timeouts]. 154 ``` 155 // Wait until the condition is true. 156 if err := testing.Poll(ctx, func(ctx context.Context) error { 157 158 if err := doSomething(); err != nil { 159 160 // In case something went wrong we can stop waiting and return an error with testing.PollBreak(). 161 return testing.PollBreak(errors.Wrap(err, "failed to do something critical")) 162 } 163 164 // Get the current state of our condition. 165 condition, err := checkCondition() 166 if err != nil { 167 // Do error handling here. 168 } 169 if condition != expectedCondition { 170 return errors.Errorf("unexpected condition: got %q; want %q", condition, expectedCondition) 171 } 172 173 return nil 174 175 }, &testing.PollOptions{ 176 Timeout: 30 * time.Second, 177 Interval: 5 * time.Second, 178 }); err != nil { 179 s.Fatal("Did not reach expected state: ", err) 180 } 181 ``` 182 183 [testing.Poll]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/testing#Poll 184 [Context and timeouts]: https://chromium.googlesource.com/chromiumos/platform/tast/+/refs/heads/main/docs/writing_tests.md#contexts-and-timeouts 185 186 ## Setup 187 ### Using the Launcher 188 We can use the Chrome Launcher in our tests to search and start applications or websites. 189 ``` 190 tconn, err := cr.TestAPIConn(ctx) 191 if err != nil { 192 // Do error handling here. 193 } 194 195 if err := launcher.SearchAndLaunch(ctx, tconn, appName); err != nil { 196 // Do error handling here. 197 } 198 ``` 199 The launcher requires a context, a test connection and a string that will be typed into the launcher. This example will start the application defined in `appName`, or if it isn't found a Google search for `appName` will be opened in a new Chrome window. 200 201 ### HTTP Server 202 In many tests having a local http server can be helpful to avoid being dependent on a network connection. The [httptest.NewServer] function can be used to start your own http server from within the test. 203 ``` 204 func init() { 205 testing.AddTest(&testing.Test{ 206 Func: MyTest, 207 Data: []string{"my_test.html", "my_test.js"}, 208 }) 209 } 210 211 func MyTest(ctx context.Context, s *testing.State) { 212 213 server := httptest.NewServer(http.FileServer(s.DataFileSystem())) 214 defer server.Close() 215 216 conn, err := cr.NewConn(ctx, server.URL+"/my_test.html") 217 if err != nil { 218 // Do error handling here. 219 } 220 defer conn.Close() 221 ... 222 } 223 ``` 224 In this example we created a small website with two files: `my_test.html` and `my_test.js`, and added the files to the test in the definition of the metadata for the test. 225 In the test we start a HTTP server as a `http.FileServer` which serves requests for the files located in the folder given as argument. The used folder, `s.DataFileSystem()`, is the folder where additional files for the test are copied to on the test device, which is where our files for the website end up. Then we open the website in a new Chrome window. 226 227 [httptest.NewServer]: https://golang.org/pkg/os/