David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 1 | # Adding Multiwindow EG Tests |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This document is a simple guidance on adding Multiwindow EG tests, using a |
| 6 | limited set of helper functions. |
| 7 | |
| 8 | ## Window Number |
| 9 | |
| 10 | Windows are numbered, through their `accessibilityIdentifier`, in the order they |
Arthur Milchior | 192120b | 2022-02-15 15:32:07 | [diff] [blame] | 11 | are created. The first window will be `@"0"`, the second window will be `@"1"`, |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 12 | etc. In most helper functions, the integer is used to identify windows. |
| 13 | |
| 14 | Windows are not automatically renumbered so it is possible to end up with two |
| 15 | windows with the same number. You can use |
| 16 | `[ChromeEarlGrey changeWindowWithNumber:toNewNumber:]` to fix that in the |
| 17 | unlikely case it is needed. Depending on the needs of the test, you can decide |
| 18 | on how to proceed, for example wanting to keep the left window as 0 and the |
| 19 | right window as 1. See |
John Palmer | 046f987 | 2021-05-24 01:24:56 | [diff] [blame] | 20 | [`[BrowserViewControllerTestCase testMultiWindowURLLoading]`](https://source.chromium.org/chromium/chromium/src/+/main:ios/chrome/browser/ui/browser_view/browser_view_controller_egtest.mm;l=209) |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 21 | as an example of this. |
| 22 | |
| 23 | ## Helpers |
| 24 | |
| 25 | Multiwindow helpers are divided into two groups: window management functions and |
| 26 | tabs management functions, the latter being similar to their previously existing |
| 27 | single window counterpart versions but with an extra `inWindowWithNumber` |
| 28 | parameter. |
| 29 | |
| 30 | The helpers all live in |
John Palmer | 046f987 | 2021-05-24 01:24:56 | [diff] [blame] | 31 | [ios/chrome/test/earl_grey/chrome_earl_grey.h](https://source.chromium.org/chromium/chromium/src/+/main:ios/chrome/test/earl_grey/chrome_earl_grey.h) |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 32 | |
| 33 | ### Window Management |
| 34 | |
| 35 | #### Window Creation |
| 36 | |
| 37 | You can artificially create a new window, as if the user had done it through |
| 38 | the dock: |
| 39 | |
| 40 | ``` |
| 41 | // Opens a new window. |
| 42 | - (void)openNewWindow; |
| 43 | ``` |
| 44 | |
| 45 | Or by triggering any chrome function that opens one. Either way, it is strongly |
| 46 | recommended to call the following function to wait and verify that the action |
| 47 | worked (It can also be a good idea to call it before as well as after the |
| 48 | action): |
| 49 | |
| 50 | ``` |
| 51 | // Waits for there to be |count| number of browsers within a timeout, |
| 52 | // or a GREYAssert is induced. |
| 53 | - (void)waitForForegroundWindowCount:(NSUInteger)count; |
| 54 | ```` |
| 55 | |
| 56 | Two helpers allow getting the current window counts. Note that calling |
| 57 | `[ChromeEarlGrey waitForForegroundWindowCount:]` above is probably a better |
| 58 | choice than asserting on these counts. |
| 59 | |
| 60 | ``` |
| 61 | // Returns the number of windows, including background and disconnected or |
| 62 | // archived windows. |
| 63 | - (NSUInteger)windowCount WARN_UNUSED_RESULT; |
| 64 | |
| 65 | // Returns the number of foreground (visible on screen) windows. |
| 66 | - (NSUInteger)foregroundWindowCount WARN_UNUSED_RESULT; |
| 67 | ``` |
| 68 | |
| 69 | #### Window destruction |
| 70 | |
| 71 | You can manually destroy a window: |
| 72 | |
| 73 | ``` |
| 74 | // Closes the window with given number. Note that numbering doesn't change and |
| 75 | // if a new window is to be added in a test, a renumbering might be needed. |
| 76 | - (void)closeWindowWithNumber:(int)windowNumber; |
| 77 | ``` |
| 78 | |
| 79 | Or destroy all but one, leaving the test application ready for the next test. |
| 80 | |
| 81 | ``` |
| 82 | // Closes all but one window, including all non-foreground windows. No-op if |
| 83 | // only one window presents. |
| 84 | - (void)closeAllExtraWindows; |
| 85 | ``` |
| 86 | |
| 87 | #### Window renumbering |
| 88 | |
| 89 | As discussed before, it is possible to renumber a window. Note that if more |
| 90 | than one window has the same number, only the first one found will be renamed. |
| 91 | |
| 92 | ``` |
| 93 | // Renumbers given window with current number to new number. For example, if |
| 94 | // you have windows 0 and 1, close window 0, then add new window, then both |
| 95 | // windows would be 1. Therefore you should renumber the remaining ones |
| 96 | // before adding new ones. |
| 97 | - (void)changeWindowWithNumber:(int)windowNumber |
| 98 | toNewNumber:(int)newWindowNumber; |
| 99 | ``` |
| 100 | |
| 101 | ### Tab Management |
| 102 | |
| 103 | All the following functions work like their non multiwindow counterparts. Note |
| 104 | that those non multiwindow counterparts can still be called in a multiwindow |
| 105 | test when only one window is visible, but their result becomes undefined if |
| 106 | more than one window exists. |
| 107 | |
| 108 | ``` |
| 109 | // Opens a new tab in window with given number and waits for the new tab |
| 110 | // animation to complete within a timeout, or a GREYAssert is induced. |
| 111 | - (void)openNewTabInWindowWithNumber:(int)windowNumber; |
| 112 | |
| 113 | // Loads |URL| in the current WebState for window with given number, with |
| 114 | // transition type ui::PAGE_TRANSITION_TYPED, and if waitForCompletion is YES |
| 115 | // waits for the loading to complete within a timeout. |
| 116 | // Returns nil on success, or else an NSError indicating why the operation |
| 117 | // failed. |
| 118 | - (void)loadURL:(const GURL&)URL |
| 119 | inWindowWithNumber:(int)windowNumber |
| 120 | waitForCompletion:(BOOL)wait; |
| 121 | |
| 122 | // Loads |URL| in the current WebState for window with given number, with |
| 123 | // transition type ui::PAGE_TRANSITION_TYPED, and waits for the loading to |
| 124 | // complete within a timeout. If the condition is not met within a timeout |
| 125 | // returns an NSError indicating why the operation failed, otherwise nil. |
| 126 | - (void)loadURL:(const GURL&)URL inWindowWithNumber:(int)windowNumber; |
| 127 | |
| 128 | // Waits for the page to finish loading for window with given number, within a |
| 129 | // timeout, or a GREYAssert is induced. |
| 130 | - (void)waitForPageToFinishLoadingInWindowWithNumber:(int)windowNumber; |
| 131 | |
| 132 | // Returns YES if the window with given number's current WebState is loading. |
| 133 | - (BOOL)isLoadingInWindowWithNumber:(int)windowNumber WARN_UNUSED_RESULT; |
| 134 | |
| 135 | // Waits for the current web state for window with given number, to contain |
| 136 | // |UTF8Text|. If the condition is not met within a timeout a GREYAssert is |
| 137 | // induced. |
| 138 | - (void)waitForWebStateContainingText:(const std::string&)UTF8Text |
| 139 | inWindowWithNumber:(int)windowNumber; |
| 140 | |
| 141 | // Waits for the current web state for window with given number, to contain |
| 142 | // |UTF8Text|. If the condition is not met within the given |timeout| a |
| 143 | // GREYAssert is induced. |
| 144 | - (void)waitForWebStateContainingText:(const std::string&)UTF8Text |
| 145 | timeout:(NSTimeInterval)timeout |
| 146 | inWindowWithNumber:(int)windowNumber; |
| 147 | |
| 148 | // Waits for there to be |count| number of non-incognito tabs within a timeout, |
| 149 | // or a GREYAssert is induced. |
| 150 | - (void)waitForMainTabCount:(NSUInteger)count |
| 151 | inWindowWithNumber:(int)windowNumber; |
| 152 | |
| 153 | // Waits for there to be |count| number of incognito tabs within a timeout, or a |
| 154 | // GREYAssert is induced. |
| 155 | - (void)waitForIncognitoTabCount:(NSUInteger)count |
| 156 | inWindowWithNumber:(int)windowNumber; |
| 157 | ``` |
| 158 | |
| 159 | ## Matchers |
| 160 | |
| 161 | Most existing matchers can be used when multiple windows are present by setting |
| 162 | a global root matcher with |
| 163 | `[EarlGrey setRootMatcherForSubsequentInteractions:]`. For example, in the |
| 164 | following blurb, the `BackButton` is tapped in window 0, then later the |
| 165 | `TabGridDoneButton` is tapped in window 1: |
| 166 | |
| 167 | ``` |
| 168 | [EarlGrey setRootMatcherForSubsequentInteractions:WindowWithNumber(1)]; |
| 169 | [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()] |
| 170 | performAction:grey_tap()]; |
| 171 | [ChromeEarlGrey waitForWebStateContainingText:kResponse1 |
| 172 | inWindowWithNumber:1]; |
| 173 | |
| 174 | [EarlGrey setRootMatcherForSubsequentInteractions:WindowWithNumber(0)]; |
| 175 | [[EarlGrey selectElementWithMatcher:chrome_test_util::TabGridDoneButton()] |
| 176 | performAction:grey_tap()]; |
| 177 | ``` |
| 178 | |
| 179 | If `grey_tap()` fails unexpectedly and unexplainably, see Actions section below. |
| 180 | |
| 181 | Thanks to the root matcher, a limited number of matchers, require the window |
| 182 | number to be specified. `WindowWithNumber` is useful as a root matcher and |
| 183 | `MatchInWindowWithNumber` if you want to match without using a root matcher: |
| 184 | |
John Palmer | 046f987 | 2021-05-24 01:24:56 | [diff] [blame] | 185 | [ios/chrome/test/earl_grey/chrome_matchers.h](https://source.chromium.org/chromium/chromium/src/+/main:ios/chrome/test/earl_grey/chrome_matchers.h) |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 186 | |
| 187 | ``` |
| 188 | // Matcher for a window with a given number. |
| 189 | // Window numbers are assigned at scene creation. Normally, each EGTest will |
| 190 | // start with exactly one window with number 0. Each time a window is created, |
| 191 | // it is assigned an accessibility identifier equal to the number of connected |
| 192 | // scenes (stored as NSString). This means typically any windows created in a |
| 193 | // test will have consecutive numbers. |
| 194 | id<GREYMatcher> WindowWithNumber(int window_number); |
| 195 | |
| 196 | // Shorthand matcher for creating a matcher that ensures the given matcher |
| 197 | // matches elements under the given window. |
| 198 | id<GREYMatcher> MatchInWindowWithNumber(int window_number, |
| 199 | id<GREYMatcher> matcher); |
| 200 | |
| 201 | The settings back button is the hardest matcher to get right. Their |
| 202 | characteristics change based on iOS versions, on device types and on an |
| 203 | unexplainable situation where the label appears or not. For this reason, |
| 204 | there’s a special matcher for Multiwindow: |
| 205 | |
| 206 | // Returns matcher for the back button on a settings menu in given window |
| 207 | // number. |
| 208 | id<GREYMatcher> SettingsMenuBackButton(int window_number); |
| 209 | ``` |
| 210 | |
David Jean | 08027bb | 2021-05-27 08:10:04 | [diff] [blame] | 211 | ## Chrome Matchers |
| 212 | Some chrome matchers have a version where the window number needs to be |
| 213 | specified. On those, the root matcher will be set and left set on return to |
| 214 | allow less verbosity at call site. |
| 215 | |
| 216 | ``` |
| 217 | // Makes the toolbar visible by swiping downward, if necessary. Then taps on |
| 218 | // the Tools menu button. At least one tab needs to be open and visible when |
| 219 | // calling this method. |
| 220 | // Sets and Leaves the root matcher to the given window with |windowNumber|. |
| 221 | - (void)openToolsMenuInWindowWithNumber:(int)windowNumber; |
| 222 | |
| 223 | // Opens the settings menu by opening the tools menu, and then tapping the |
| 224 | // Settings button. There will be a GREYAssert if the tools menu is open when |
| 225 | // calling this method. |
| 226 | // Sets and Leaves the root matcher to the given window with |windowNumber|. |
| 227 | - (void)openSettingsMenuInWindowWithNumber:(int)windowNumber; |
| 228 | ``` |
| 229 | |
| 230 | For example, the following code: |
| 231 | |
| 232 | ``` |
| 233 | [EarlGrey setRootMatcherForSubsequentInteractions: |
| 234 | chrome_test_util::WindowWithNumber(windowNumber)]; |
| 235 | [ChromeEarlGreyUI openToolsMenu]; |
Benjamin Williams | 26d955d | 2022-06-27 13:55:52 | [diff] [blame] | 236 | [ChromeEarlGreyUI tapToolsMenuButton:HistoryDestinationButton()]; |
David Jean | 08027bb | 2021-05-27 08:10:04 | [diff] [blame] | 237 | ``` |
| 238 | |
| 239 | Can be reduced to: |
| 240 | |
| 241 | ``` |
| 242 | [ChromeEarlGreyUI openToolsMenuInWindowWithNumber:windowNumber]; |
Benjamin Williams | 26d955d | 2022-06-27 13:55:52 | [diff] [blame] | 243 | [ChromeEarlGreyUI tapToolsMenuButton:HistoryDestinationButton()]; |
David Jean | 08027bb | 2021-05-27 08:10:04 | [diff] [blame] | 244 | ``` |
| 245 | |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 246 | ## Actions |
| 247 | |
| 248 | There are actions that cannot be done using `EarlGrey` (yet?). One of those is |
| 249 | Drag and drop. But XCUI is good at that, so there are two new |
| 250 | client-side-triggered actions that can be used, that work across multiple |
| 251 | windows: |
| 252 | |
John Palmer | 046f987 | 2021-05-24 01:24:56 | [diff] [blame] | 253 | [ios/chrome/test/earl_grey/chrome_xcui_actions.h](https://source.chromium.org/chromium/chromium/src/+/main:ios/chrome/test/earl_grey/chrome_xcui_actions.h) |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 254 | |
| 255 | ``` |
| 256 | // Action (XCUI, hence local) to long press a cell item with |
| 257 | // |accessibility_identifier| in |window_number| and drag it to the given |edge| |
| 258 | // of the app screen (can trigger a new window) before dropping it. Returns YES |
| 259 | // on success (finding the item). |
| 260 | BOOL LongPressCellAndDragToEdge(NSString* accessibility_identifier, |
| 261 | GREYContentEdge edge, |
| 262 | int window_number); |
| 263 | |
| 264 | // Action (XCUI, hence local) to long press a cell item with |
| 265 | // |src_accessibility_identifier| in |src_window_number| and drag it to the |
| 266 | // given normalized offset of the cell or window with |
| 267 | // |dst_accessibility_identifier| in |dst_window_number| before dropping it. To |
| 268 | // target a window, pass nil as |dst_accessibility_identifier|. Returns YES on |
| 269 | // success (finding both items). |
| 270 | BOOL LongPressCellAndDragToOffsetOf(NSString* src_accessibility_identifier, |
| 271 | int src_window_number, |
| 272 | NSString* dst_accessibility_identifier, |
| 273 | int dst_window_number, |
| 274 | CGVector dst_normalized_offset); |
| 275 | ``` |
| 276 | |
| 277 | Also there’s a bug in `EarlGrey` that makes some actions fail without a reason |
| 278 | on a second or third window due to a false negative visibility computation. |
| 279 | To palliate this for now, this XCUI action helper can be used: |
| 280 | |
| 281 | ``` |
| 282 | // Action (XCUI, hence local) to tap item with |accessibility_identifier| in |
| 283 | // |window_number|. |
| 284 | BOOL TapAtOffsetOf(NSString* accessibility_identifier, |
| 285 | int window_number, |
| 286 | CGVector normalized_offset); |
| 287 | ``` |
| 288 | |
| 289 | It’s hard to know when you will need it, but unexpected and unexplainable |
| 290 | failures in the simulator are a good clue... |
| 291 | |
| 292 | If you need window resizing, the following allows you to: |
| 293 | |
| 294 | ``` |
| 295 | // Action (XCUI, hence local) to resize split windows by dragging the splitter. |
| 296 | // This action requires two windows (|first_window_number| and |
| 297 | // |second_window_number|, in any order) to find where the splitter is located. |
| 298 | // A given |first_window_normalized_screen_size| defines the normalized size |
| 299 | // [0.0 - 1.0] wanted for the |first_window_number|. Returns NO if any window |
| 300 | // is not found or if one of them is a floating window. |
| 301 | // Notes: The size requested |
| 302 | // will be matched by the OS to the closest available multiwindow layout. This |
| 303 | // function works with any device orientation and with either LTR or RTL |
| 304 | // languages. Example of use: |
| 305 | // [ChromeEarlGrey openNewWindow]; |
| 306 | // [ChromeEarlGrey waitForForegroundWindowCount:2]; |
| 307 | // chrome_test_util::DragWindowSplitterToSize(0, 1, 0.25); |
| 308 | // Starting with window sizes 438 and 320 pts, this will resize |
| 309 | // them to 320pts and 438 pts respectively. |
| 310 | BOOL DragWindowSplitterToSize(int first_window_number, |
| 311 | int second_window_number, |
| 312 | CGFloat first_window_normalized_screen_size); |
| 313 | ``` |
| 314 | |
| 315 | ## setUp/tearDown/test |
| 316 | |
David Jean | 0874f71 | 2021-03-11 15:37:29 | [diff] [blame] | 317 | In multiwindow tests, a failure to clear extra windows and root matcher at the |
| 318 | end of a test, would mean a more than likely failure on the next one. To do so, |
| 319 | the setUp/tearDown methods do not need any changes. ```closeAllExtraWindows``` |
| 320 | and ```[EarlGrey setRootMatcherForSubsequentInteractions:nil]``` are called on |
| 321 | ```[ChromeTestCase tearDown]``` and ```[ChromeTestCase setUpForTestCase]```. |
David Jean | 4ddad6b | 2021-02-26 12:46:49 | [diff] [blame] | 322 | |
| 323 | Tests should check if multiwindow is available on their first lines, to avoid |
| 324 | failing on iPhones: |
| 325 | |
| 326 | ``` |
| 327 | - (void)testDragAndDropAtEdgeToCreateNewWindow { |
| 328 | if (![ChromeEarlGrey areMultipleWindowsSupported]) |
| 329 | EARL_GREY_TEST_DISABLED(@"Multiple windows can't be opened."); |
| 330 | ... |
| 331 | ``` |