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, protractor} 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 beforeEach(defaultTimeoutMs: number) { 24 await browser.manage().timeouts().implicitlyWait(defaultTimeoutMs); 25 await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL); 26 await browser.driver.manage().window().maximize(); 27 } 28 29 static async checkServerIsUp(name: string, url: string) { 30 try { 31 await browser.get(url); 32 } catch (error) { 33 fail(`${name} server (${url}) looks down. Did you start it?`); 34 } 35 } 36 37 static async loadTraceAndCheckViewer( 38 fixturePath: string, 39 viewerTabTitle: string, 40 viewerSelector: string, 41 ) { 42 await E2eTestUtils.uploadFixture(fixturePath); 43 await E2eTestUtils.closeSnackBar(); 44 await E2eTestUtils.clickViewTracesButton(); 45 await E2eTestUtils.clickViewerTabButton(viewerTabTitle); 46 47 const viewerPresent = await element(by.css(viewerSelector)).isPresent(); 48 expect(viewerPresent).toBeTruthy(); 49 } 50 51 static async loadBugReport(defaulttimeMs: number) { 52 await E2eTestUtils.uploadFixture('bugreports/bugreport_stripped.zip'); 53 await E2eTestUtils.checkHasLoadedTracesFromBugReport(); 54 expect(await E2eTestUtils.areMessagesEmitted(defaulttimeMs)).toBeTruthy(); 55 await E2eTestUtils.checkEmitsUnsupportedFileFormatMessages(); 56 await E2eTestUtils.checkEmitsOldDataMessages(); 57 await E2eTestUtils.closeSnackBar(); 58 } 59 60 static async areMessagesEmitted(defaultTimeoutMs: number): Promise<boolean> { 61 // Messages are emitted quickly. There is no Need to wait for the entire 62 // default timeout to understand whether the messages where emitted or not. 63 await browser.manage().timeouts().implicitlyWait(1000); 64 const emitted = await element(by.css('snack-bar')).isPresent(); 65 await browser.manage().timeouts().implicitlyWait(defaultTimeoutMs); 66 return emitted; 67 } 68 69 static async clickViewTracesButton() { 70 const button = element(by.css('.load-btn')); 71 await button.click(); 72 } 73 74 static async clickClearAllButton() { 75 const button = element(by.css('.clear-all-btn')); 76 await button.click(); 77 } 78 79 static async clickCloseIcon() { 80 const button = element.all(by.css('.uploaded-files button')).first(); 81 await button.click(); 82 } 83 84 static async clickDownloadTracesButton() { 85 const button = element(by.css('.save-button')); 86 await button.click(); 87 } 88 89 static async clickUploadNewButton() { 90 const button = element(by.css('.upload-new')); 91 await button.click(); 92 } 93 94 static async closeSnackBar() { 95 const closeButton = element(by.css('.snack-bar-action')); 96 const isPresent = await closeButton.isPresent(); 97 if (isPresent) { 98 await closeButton.click(); 99 } 100 } 101 102 static async clickViewerTabButton(title: string) { 103 const tabs: ElementFinder[] = await element.all(by.css('trace-view .tab')); 104 for (const tab of tabs) { 105 const tabTitle = await tab.getText(); 106 if (tabTitle.includes(title)) { 107 await tab.click(); 108 return; 109 } 110 } 111 throw new Error(`could not find tab corresponding to ${title}`); 112 } 113 114 static async checkTimelineTraceSelector(trace: { 115 icon: string; 116 color: string; 117 }) { 118 const traceSelector = element(by.css('#trace-selector')); 119 const text = await traceSelector.getText(); 120 expect(text).toContain(trace.icon); 121 122 const icons = await element.all(by.css('.shown-selection .mat-icon')); 123 const iconColors: string[] = []; 124 for (const icon of icons) { 125 iconColors.push(await icon.getCssValue('color')); 126 } 127 expect( 128 iconColors.some((iconColor) => iconColor === trace.color), 129 ).toBeTruthy(); 130 } 131 132 static async checkInitialRealTimestamp(timestamp: string) { 133 await E2eTestUtils.changeRealTimestampInWinscope(timestamp); 134 await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); 135 const prevEntryButton = element(by.css('#prev_entry_button')); 136 const isDisabled = await prevEntryButton.getAttribute('disabled'); 137 expect(isDisabled).toEqual('true'); 138 } 139 140 static async checkFinalRealTimestamp(timestamp: string) { 141 await E2eTestUtils.changeRealTimestampInWinscope(timestamp); 142 await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); 143 const nextEntryButton = element(by.css('#next_entry_button')); 144 const isDisabled = await nextEntryButton.getAttribute('disabled'); 145 expect(isDisabled).toEqual('true'); 146 } 147 148 static async checkWinscopeRealTimestamp(timestamp: string) { 149 const inputElement = element(by.css('input[name="humanTimeInput"]')); 150 const value = await inputElement.getAttribute('value'); 151 expect(value).toEqual(timestamp); 152 } 153 154 static async changeRealTimestampInWinscope(newTimestamp: string) { 155 await E2eTestUtils.updateInputField('', 'humanTimeInput', newTimestamp); 156 } 157 158 static async checkWinscopeNsTimestamp(newTimestamp: string) { 159 const inputElement = element(by.css('input[name="nsTimeInput"]')); 160 const valueWithNsSuffix = await inputElement.getAttribute('value'); 161 expect(valueWithNsSuffix).toEqual(newTimestamp + ' ns'); 162 } 163 164 static async changeNsTimestampInWinscope(newTimestamp: string) { 165 await E2eTestUtils.updateInputField('', 'nsTimeInput', newTimestamp); 166 } 167 168 static async filterHierarchy(viewer: string, filterString: string) { 169 await E2eTestUtils.updateInputField( 170 `${viewer} hierarchy-view .title-section`, 171 'filter', 172 filterString, 173 ); 174 } 175 176 static async updateInputField( 177 inputFieldSelector: string, 178 inputFieldName: string, 179 newInput: string, 180 ) { 181 const inputElement = element( 182 by.css(`${inputFieldSelector} input[name="${inputFieldName}"]`), 183 ); 184 const inputStringStep1 = newInput.slice(0, -1); 185 const inputStringStep2 = newInput.slice(-1) + '\r\n'; 186 const script = `document.querySelector("${inputFieldSelector} input[name=\\"${inputFieldName}\\"]").value = "${inputStringStep1}"`; 187 await browser.executeScript(script); 188 await inputElement.sendKeys(inputStringStep2); 189 } 190 191 static async selectItemInHierarchy(viewer: string, itemName: string) { 192 const nodes: ElementFinder[] = await element.all( 193 by.css(`${viewer} hierarchy-view .node`), 194 ); 195 for (const node of nodes) { 196 const id = await node.getAttribute('id'); 197 if (id.includes(itemName)) { 198 const desc = node.element(by.css('.description')); 199 await desc.click(); 200 return; 201 } 202 } 203 throw new Error(`could not find item matching ${itemName} in hierarchy`); 204 } 205 206 static async applyStateToHierarchyOptions( 207 viewerSelector: string, 208 shouldEnable: boolean, 209 ) { 210 const options: ElementFinder[] = await element.all( 211 by.css(`${viewerSelector} hierarchy-view .view-controls .user-option`), 212 ); 213 for (const option of options) { 214 const isEnabled = !(await option.getAttribute('class')).includes( 215 'not-enabled', 216 ); 217 if (shouldEnable && !isEnabled) { 218 await option.click(); 219 } else if (!shouldEnable && isEnabled) { 220 await option.click(); 221 } 222 } 223 } 224 225 static async checkItemInPropertiesTree( 226 viewer: string, 227 itemName: string, 228 expectedText: string, 229 ) { 230 const nodes = await element.all(by.css(`${viewer} .properties-view .node`)); 231 for (const node of nodes) { 232 const id: string = await node.getAttribute('id'); 233 if (id === 'node' + itemName) { 234 const text = await node.getText(); 235 expect(text).toEqual(expectedText); 236 return; 237 } 238 } 239 throw new Error(`could not find item ${itemName} in properties tree`); 240 } 241 242 static async checkRectLabel(viewer: string, expectedLabel: string) { 243 const labels = await element.all( 244 by.css(`${viewer} rects-view .rect-label`), 245 ); 246 247 let foundLabel: ElementFinder | undefined; 248 249 for (const label of labels) { 250 const text = await label.getText(); 251 if (text.includes(expectedLabel)) { 252 foundLabel = label; 253 break; 254 } 255 } 256 257 expect(foundLabel).toBeTruthy(); 258 } 259 260 static async checkScrollPresent(viewerSelector: string) { 261 await browser.wait( 262 async () => { 263 return await element(by.css(`${viewerSelector} .scroll`)).isPresent(); 264 }, 265 1000, 266 'Fetching data timeout', 267 ); 268 } 269 270 static async checkTotalScrollEntries( 271 viewerSelector: string, 272 numberOfEntries: number, 273 scrollToBottom = false, 274 ) { 275 if (scrollToBottom) { 276 const viewport = element(by.css(`${viewerSelector} .scroll`)); 277 let lastId: string | undefined; 278 let lastScrollEntryItemId = await E2eTestUtils.getLastScrollEntryItemId( 279 viewerSelector, 280 ); 281 while (lastId !== lastScrollEntryItemId) { 282 lastId = lastScrollEntryItemId; 283 await viewport.sendKeys(protractor.Key.END); 284 await new Promise<void>((resolve) => setTimeout(resolve, 500)); 285 lastScrollEntryItemId = await E2eTestUtils.getLastScrollEntryItemId( 286 viewerSelector, 287 ); 288 } 289 } 290 const lastId = await E2eTestUtils.getLastScrollEntryItemId(viewerSelector); 291 expect(lastId).toEqual(`${numberOfEntries - 1}`); 292 } 293 294 static async getLastScrollEntryItemId( 295 viewerSelector: string, 296 ): Promise<string> { 297 const entries = await element.all( 298 by.css(`${viewerSelector} .scroll .entry`), 299 ); 300 return await entries[entries.length - 1].getAttribute('item-id'); 301 } 302 303 static async checkSelectFilter( 304 viewerSelector: string, 305 filterSelector: string, 306 options: string[], 307 expectedFilteredEntries: number, 308 totalEntries: number, 309 ) { 310 await E2eTestUtils.toggleSelectFilterOptions( 311 viewerSelector, 312 filterSelector, 313 options, 314 ); 315 await E2eTestUtils.checkTotalScrollEntries( 316 viewerSelector, 317 expectedFilteredEntries, 318 ); 319 320 await E2eTestUtils.toggleSelectFilterOptions( 321 viewerSelector, 322 filterSelector, 323 options, 324 ); 325 await E2eTestUtils.checkTotalScrollEntries( 326 viewerSelector, 327 totalEntries, 328 true, 329 ); 330 } 331 332 static async uploadFixture(...paths: string[]) { 333 const inputFile = element(by.css('input[type="file"]')); 334 335 // Uploading multiple files is not properly supported but 336 // chrome handles file paths joined with new lines 337 await inputFile.sendKeys( 338 paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'), 339 ); 340 } 341 342 static getFixturePath(filename: string): string { 343 if (path.isAbsolute(filename)) { 344 return filename; 345 } 346 return path.join( 347 E2eTestUtils.getProjectRootPath(), 348 'src/test/fixtures', 349 filename, 350 ); 351 } 352 353 private static getProjectRootPath(): string { 354 let root = __dirname; 355 while (path.basename(root) !== 'winscope') { 356 root = path.dirname(root); 357 } 358 return root; 359 } 360 361 private static async checkHasLoadedTracesFromBugReport() { 362 const text = await element(by.css('.uploaded-files')).getText(); 363 expect(text).toContain('Window Manager'); 364 expect(text).toContain('Surface Flinger'); 365 expect(text).toContain('Transactions'); 366 expect(text).toContain('Transitions'); 367 368 // Should be merged into a single Transitions trace 369 expect(text).not.toContain('WM Transitions'); 370 expect(text).not.toContain('Shell Transitions'); 371 372 expect(text).toContain('layers_trace_from_transactions.winscope'); 373 expect(text).toContain('transactions_trace.winscope'); 374 expect(text).toContain('wm_transition_trace.winscope'); 375 expect(text).toContain('shell_transition_trace.winscope'); 376 expect(text).toContain('window_CRITICAL.proto'); 377 378 // discards some traces due to old data 379 expect(text).not.toContain('ProtoLog'); 380 expect(text).not.toContain('IME Service'); 381 expect(text).not.toContain('IME system_server'); 382 expect(text).not.toContain('IME Clients'); 383 expect(text).not.toContain('wm_log.winscope'); 384 expect(text).not.toContain('ime_trace_service.winscope'); 385 expect(text).not.toContain('ime_trace_managerservice.winscope'); 386 expect(text).not.toContain('wm_trace.winscope'); 387 expect(text).not.toContain('ime_trace_clients.winscope'); 388 } 389 390 private static async checkEmitsUnsupportedFileFormatMessages() { 391 const text = await element(by.css('snack-bar')).getText(); 392 expect(text).toContain('unsupported format'); 393 } 394 395 private static async checkEmitsOldDataMessages() { 396 const text = await element(by.css('snack-bar')).getText(); 397 expect(text).toContain('discarded because data is old'); 398 } 399 400 private static async toggleSelectFilterOptions( 401 viewerSelector: string, 402 filterSelector: string, 403 options: string[], 404 ) { 405 await element( 406 by.css( 407 `${viewerSelector} .headers ${filterSelector} .mat-select-trigger`, 408 ), 409 ).click(); 410 const optionElements: ElementFinder[] = await element.all( 411 by.css('.mat-select-panel .option'), 412 ); 413 for (const optionEl of optionElements) { 414 const optionText = (await optionEl.getText()).trim(); 415 if (options.some((option) => optionText === option)) { 416 await optionEl.click(); 417 options = options.filter((option) => option !== optionText); 418 if (options.length === 0) { 419 break; 420 } 421 } 422 } 423 const backdrop = await element( 424 by.css('.cdk-overlay-backdrop'), 425 ).getWebElement(); 426 await browser.actions().mouseMove(backdrop, {x: 0, y: 0}).click().perform(); 427 } 428} 429 430export {E2eTestUtils}; 431