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 checkTotalScrollEntries( 261 viewerSelector: string, 262 numberOfEntries: number, 263 scrollToBottom = false, 264 ) { 265 if (scrollToBottom) { 266 const viewport = element(by.css(`${viewerSelector} .scroll`)); 267 let lastId: string | undefined; 268 let lastScrollEntryItemId = await E2eTestUtils.getLastScrollEntryItemId( 269 viewerSelector, 270 ); 271 while (lastId !== lastScrollEntryItemId) { 272 lastId = lastScrollEntryItemId; 273 await viewport.sendKeys(protractor.Key.END); 274 await new Promise<void>((resolve) => setTimeout(resolve, 500)); 275 lastScrollEntryItemId = await E2eTestUtils.getLastScrollEntryItemId( 276 viewerSelector, 277 ); 278 } 279 } 280 const entries = await element.all( 281 by.css(`${viewerSelector} .scroll .entry`), 282 ); 283 expect(await entries[entries.length - 1].getAttribute('item-id')).toEqual( 284 `${numberOfEntries - 1}`, 285 ); 286 } 287 288 static async getLastScrollEntryItemId( 289 viewerSelector: string, 290 ): Promise<string> { 291 const entries = await element.all( 292 by.css(`${viewerSelector} .scroll .entry`), 293 ); 294 return await entries[entries.length - 1].getAttribute('item-id'); 295 } 296 297 static async toggleSelectFilterOptions( 298 viewerSelector: string, 299 filterSelector: string, 300 options: string[], 301 ) { 302 await element( 303 by.css( 304 `${viewerSelector} .headers ${filterSelector} .mat-select-trigger`, 305 ), 306 ).click(); 307 308 const optionElements: ElementFinder[] = await element.all( 309 by.css('.mat-select-panel .mat-option'), 310 ); 311 for (const optionEl of optionElements) { 312 const optionText = (await optionEl.getText()).trim(); 313 if (options.some((option) => optionText === option)) { 314 await optionEl.click(); 315 } 316 } 317 318 const backdrop = await element( 319 by.css('.cdk-overlay-backdrop'), 320 ).getWebElement(); 321 await browser.actions().mouseMove(backdrop, {x: 0, y: 0}).click().perform(); 322 } 323 324 static async uploadFixture(...paths: string[]) { 325 const inputFile = element(by.css('input[type="file"]')); 326 327 // Uploading multiple files is not properly supported but 328 // chrome handles file paths joined with new lines 329 await inputFile.sendKeys( 330 paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'), 331 ); 332 } 333 334 static getFixturePath(filename: string): string { 335 if (path.isAbsolute(filename)) { 336 return filename; 337 } 338 return path.join( 339 E2eTestUtils.getProjectRootPath(), 340 'src/test/fixtures', 341 filename, 342 ); 343 } 344 345 private static getProjectRootPath(): string { 346 let root = __dirname; 347 while (path.basename(root) !== 'winscope') { 348 root = path.dirname(root); 349 } 350 return root; 351 } 352 353 private static async checkHasLoadedTracesFromBugReport() { 354 const text = await element(by.css('.uploaded-files')).getText(); 355 expect(text).toContain('Window Manager'); 356 expect(text).toContain('Surface Flinger'); 357 expect(text).toContain('Transactions'); 358 expect(text).toContain('Transitions'); 359 360 // Should be merged into a single Transitions trace 361 expect(text).not.toContain('WM Transitions'); 362 expect(text).not.toContain('Shell Transitions'); 363 364 expect(text).toContain('layers_trace_from_transactions.winscope'); 365 expect(text).toContain('transactions_trace.winscope'); 366 expect(text).toContain('wm_transition_trace.winscope'); 367 expect(text).toContain('shell_transition_trace.winscope'); 368 expect(text).toContain('window_CRITICAL.proto'); 369 370 // discards some traces due to old data 371 expect(text).not.toContain('ProtoLog'); 372 expect(text).not.toContain('IME Service'); 373 expect(text).not.toContain('IME system_server'); 374 expect(text).not.toContain('IME Clients'); 375 expect(text).not.toContain('wm_log.winscope'); 376 expect(text).not.toContain('ime_trace_service.winscope'); 377 expect(text).not.toContain('ime_trace_managerservice.winscope'); 378 expect(text).not.toContain('wm_trace.winscope'); 379 expect(text).not.toContain('ime_trace_clients.winscope'); 380 } 381 382 private static async checkEmitsUnsupportedFileFormatMessages() { 383 const text = await element(by.css('snack-bar')).getText(); 384 expect(text).toContain('unsupported format'); 385 } 386 387 private static async checkEmitsOldDataMessages() { 388 const text = await element(by.css('snack-bar')).getText(); 389 expect(text).toContain('discarded because data is old'); 390 } 391} 392 393export {E2eTestUtils}; 394