1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16import * as path from 'path'; 17import {browser, by, element, ElementFinder} from 'protractor'; 18 19class E2eTestUtils { 20 static readonly WINSCOPE_URL = 'http://localhost:8080'; 21 static readonly REMOTE_TOOL_MOCK_URL = 'http://localhost:8081'; 22 23 static async checkServerIsUp(name: string, url: string) { 24 try { 25 await browser.get(url); 26 } catch (error) { 27 fail(`${name} server (${url}) looks down. Did you start it?`); 28 } 29 } 30 31 static async loadTraceAndCheckViewer( 32 fixturePath: string, 33 viewerTabTitle: string, 34 viewerSelector: string, 35 ) { 36 await E2eTestUtils.uploadFixture(fixturePath); 37 await E2eTestUtils.closeSnackBar(); 38 await E2eTestUtils.clickViewTracesButton(); 39 await E2eTestUtils.clickViewerTabButton(viewerTabTitle); 40 41 const viewerPresent = await element(by.css(viewerSelector)).isPresent(); 42 expect(viewerPresent).toBeTruthy(); 43 } 44 45 static async loadBugReport(defaulttimeMs: number) { 46 await E2eTestUtils.uploadFixture('bugreports/bugreport_stripped.zip'); 47 await E2eTestUtils.checkHasLoadedTracesFromBugReport(); 48 expect(await E2eTestUtils.areMessagesEmitted(defaulttimeMs)).toBeTruthy(); 49 await E2eTestUtils.checkEmitsUnsupportedFileFormatMessages(); 50 await E2eTestUtils.checkEmitsOldDataMessages(); 51 await E2eTestUtils.closeSnackBar(); 52 } 53 54 static async areMessagesEmitted(defaultTimeoutMs: number): Promise<boolean> { 55 // Messages are emitted quickly. There is no Need to wait for the entire 56 // default timeout to understand whether the messages where emitted or not. 57 await browser.manage().timeouts().implicitlyWait(1000); 58 const emitted = await element(by.css('snack-bar')).isPresent(); 59 await browser.manage().timeouts().implicitlyWait(defaultTimeoutMs); 60 return emitted; 61 } 62 63 static async clickViewTracesButton() { 64 const button = element(by.css('.load-btn')); 65 await button.click(); 66 } 67 68 static async clickClearAllButton() { 69 const button = element(by.css('.clear-all-btn')); 70 await button.click(); 71 } 72 73 static async clickCloseIcon() { 74 const button = element.all(by.css('.uploaded-files button')).first(); 75 await button.click(); 76 } 77 78 static async clickDownloadTracesButton() { 79 const button = element(by.css('.save-button')); 80 await button.click(); 81 } 82 83 static async clickUploadNewButton() { 84 const button = element(by.css('.upload-new')); 85 await button.click(); 86 } 87 88 static async closeSnackBar() { 89 const closeButton = element(by.css('.snack-bar-action')); 90 const isPresent = await closeButton.isPresent(); 91 if (isPresent) { 92 await closeButton.click(); 93 } 94 } 95 96 static async clickViewerTabButton(title: string) { 97 const tabs: ElementFinder[] = await element.all(by.css('trace-view .tab')); 98 for (const tab of tabs) { 99 const tabTitle = await tab.getText(); 100 if (tabTitle.includes(title)) { 101 await tab.click(); 102 return; 103 } 104 } 105 throw Error(`could not find tab corresponding to ${title}`); 106 } 107 108 static async checkTimelineTraceSelector(trace: { 109 icon: string; 110 color: string; 111 }) { 112 const traceSelector = element(by.css('#trace-selector')); 113 const text = await traceSelector.getText(); 114 expect(text).toContain(trace.icon); 115 116 const icons: ElementFinder[] = await element.all( 117 by.css('.shown-selection .mat-icon'), 118 ); 119 const iconColors: string[] = []; 120 for (const icon of icons) { 121 iconColors.push(await icon.getCssValue('color')); 122 } 123 expect( 124 iconColors.some((iconColor) => iconColor === trace.color), 125 ).toBeTruthy(); 126 } 127 128 static async checkInitialRealTimestamp(timestamp: string) { 129 await E2eTestUtils.changeRealTimestampInWinscope(timestamp); 130 await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); 131 const prevEntryButton = element(by.css('#prev_entry_button')); 132 const isDisabled = await prevEntryButton.getAttribute('disabled'); 133 expect(isDisabled).toEqual('true'); 134 } 135 136 static async checkFinalRealTimestamp(timestamp: string) { 137 await E2eTestUtils.changeRealTimestampInWinscope(timestamp); 138 await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); 139 const nextEntryButton = element(by.css('#next_entry_button')); 140 const isDisabled = await nextEntryButton.getAttribute('disabled'); 141 expect(isDisabled).toEqual('true'); 142 } 143 144 static async checkWinscopeRealTimestamp(timestamp: string) { 145 const inputElement = element(by.css('input[name="humanTimeInput"]')); 146 const value = await inputElement.getAttribute('value'); 147 expect(value).toEqual(timestamp); 148 } 149 150 static async changeRealTimestampInWinscope(newTimestamp: string) { 151 await E2eTestUtils.updateInputField('', 'humanTimeInput', newTimestamp); 152 } 153 154 static async checkWinscopeNsTimestamp(newTimestamp: string) { 155 const inputElement = element(by.css('input[name="nsTimeInput"]')); 156 const valueWithNsSuffix = await inputElement.getAttribute('value'); 157 expect(valueWithNsSuffix).toEqual(newTimestamp + ' ns'); 158 } 159 160 static async changeNsTimestampInWinscope(newTimestamp: string) { 161 await E2eTestUtils.updateInputField('', 'nsTimeInput', newTimestamp); 162 } 163 164 static async filterHierarchy(viewer: string, filterString: string) { 165 await E2eTestUtils.updateInputField( 166 `${viewer} hierarchy-view .title-section`, 167 'filter', 168 filterString, 169 ); 170 } 171 172 static async updateInputField( 173 inputFieldSelector: string, 174 inputFieldName: string, 175 newInput: string, 176 ) { 177 const inputElement = element( 178 by.css(`${inputFieldSelector} input[name="${inputFieldName}"]`), 179 ); 180 const inputStringStep1 = newInput.slice(0, -1); 181 const inputStringStep2 = newInput.slice(-1) + '\r\n'; 182 const script = `document.querySelector("${inputFieldSelector} input[name=\\"${inputFieldName}\\"]").value = "${inputStringStep1}"`; 183 await browser.executeScript(script); 184 await inputElement.sendKeys(inputStringStep2); 185 } 186 187 static async selectItemInHierarchy(viewer: string, itemName: string) { 188 const nodes: ElementFinder[] = await element.all( 189 by.css(`${viewer} hierarchy-view .node`), 190 ); 191 for (const node of nodes) { 192 const id = await node.getAttribute('id'); 193 if (id.includes(itemName)) { 194 await node.click(); 195 return; 196 } 197 } 198 throw Error(`could not find item matching ${itemName} in hierarchy`); 199 } 200 201 static async applyStateToHierarchyOptions( 202 viewerSelector: string, 203 shouldEnable: boolean, 204 ) { 205 const options: ElementFinder[] = await element.all( 206 by.css(`${viewerSelector} hierarchy-view .view-controls .user-option`), 207 ); 208 for (const option of options) { 209 const isEnabled = !(await option.getAttribute('class')).includes( 210 'not-enabled', 211 ); 212 if (shouldEnable && !isEnabled) { 213 await option.click(); 214 } else if (!shouldEnable && isEnabled) { 215 await option.click(); 216 } 217 } 218 } 219 220 static async checkItemInPropertiesTree( 221 viewer: string, 222 itemName: string, 223 expectedText: string, 224 ) { 225 const nodes = await element.all(by.css(`${viewer} .properties-view .node`)); 226 for (const node of nodes) { 227 const id: string = await node.getAttribute('id'); 228 if (id === 'node' + itemName) { 229 const text = await node.getText(); 230 expect(text).toEqual(expectedText); 231 return; 232 } 233 } 234 throw Error(`could not find item ${itemName} in properties tree`); 235 } 236 237 static async checkRectLabel(viewer: string, expectedLabel: string) { 238 const labels = await element.all( 239 by.css(`${viewer} rects-view .rect-label`), 240 ); 241 242 let foundLabel: ElementFinder | undefined; 243 244 for (const label of labels) { 245 const text = await label.getText(); 246 if (text.includes(expectedLabel)) { 247 foundLabel = label; 248 break; 249 } 250 } 251 252 expect(foundLabel).toBeTruthy(); 253 } 254 255 static async checkTotalScrollEntries( 256 selectors: {viewer: string; scroll: string; entry: string}, 257 scrollViewport: Function, 258 numberOfEntries: number, 259 scrollToBottomOffset?: number | undefined, 260 ) { 261 if (scrollToBottomOffset !== undefined) { 262 const viewport = element( 263 by.css(`${selectors.viewer} ${selectors.scroll}`), 264 ); 265 await browser.executeAsyncScript( 266 scrollViewport, 267 viewport, 268 scrollToBottomOffset, 269 ); 270 } 271 const entries: ElementFinder[] = await element.all( 272 by.css(`${selectors.viewer} ${selectors.scroll} ${selectors.entry}`), 273 ); 274 expect(await entries[entries.length - 1].getAttribute('item-id')).toEqual( 275 `${numberOfEntries - 1}`, 276 ); 277 } 278 279 static async toggleSelectFilterOptions( 280 viewerSelector: string, 281 filterSelector: string, 282 options: string[], 283 ) { 284 const selectFilter = element( 285 by.css( 286 `${viewerSelector} .filters ${filterSelector} .mat-select-trigger`, 287 ), 288 ); 289 await selectFilter.click(); 290 291 const optionElements: ElementFinder[] = await element.all( 292 by.css('.mat-select-panel .mat-option'), 293 ); 294 for (const optionEl of optionElements) { 295 const optionText = (await optionEl.getText()).trim(); 296 if (options.some((option) => optionText === option)) { 297 await optionEl.click(); 298 } 299 } 300 301 const backdrop = element(by.css('.cdk-overlay-backdrop')); 302 await browser.actions().mouseMove(backdrop, {x: 0, y: 0}).click().perform(); 303 } 304 305 static async uploadFixture(...paths: string[]) { 306 const inputFile = element(by.css('input[type="file"]')); 307 308 // Uploading multiple files is not properly supported but 309 // chrome handles file paths joined with new lines 310 await inputFile.sendKeys( 311 paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'), 312 ); 313 } 314 315 static getFixturePath(filename: string): string { 316 if (path.isAbsolute(filename)) { 317 return filename; 318 } 319 return path.join( 320 E2eTestUtils.getProjectRootPath(), 321 'src/test/fixtures', 322 filename, 323 ); 324 } 325 326 private static getProjectRootPath(): string { 327 let root = __dirname; 328 while (path.basename(root) !== 'winscope') { 329 root = path.dirname(root); 330 } 331 return root; 332 } 333 334 private static async checkHasLoadedTracesFromBugReport() { 335 const text = await element(by.css('.uploaded-files')).getText(); 336 expect(text).toContain('Window Manager'); 337 expect(text).toContain('Surface Flinger'); 338 expect(text).toContain('Transactions'); 339 expect(text).toContain('Transitions'); 340 341 // Should be merged into a single Transitions trace 342 expect(text).not.toContain('WM Transitions'); 343 expect(text).not.toContain('Shell Transitions'); 344 345 expect(text).toContain('layers_trace_from_transactions.winscope'); 346 expect(text).toContain('transactions_trace.winscope'); 347 expect(text).toContain('wm_transition_trace.winscope'); 348 expect(text).toContain('shell_transition_trace.winscope'); 349 expect(text).toContain('window_CRITICAL.proto'); 350 351 // discards some traces due to old data 352 expect(text).not.toContain('ProtoLog'); 353 expect(text).not.toContain('IME Service'); 354 expect(text).not.toContain('IME system_server'); 355 expect(text).not.toContain('IME Clients'); 356 expect(text).not.toContain('wm_log.winscope'); 357 expect(text).not.toContain('ime_trace_service.winscope'); 358 expect(text).not.toContain('ime_trace_managerservice.winscope'); 359 expect(text).not.toContain('wm_trace.winscope'); 360 expect(text).not.toContain('ime_trace_clients.winscope'); 361 } 362 363 private static async checkEmitsUnsupportedFileFormatMessages() { 364 const text = await element(by.css('snack-bar')).getText(); 365 expect(text).toContain('unsupported format'); 366 } 367 368 private static async checkEmitsOldDataMessages() { 369 const text = await element(by.css('snack-bar')).getText(); 370 expect(text).toContain('discarded because data is old'); 371 } 372} 373 374export {E2eTestUtils}; 375