/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as path from 'path'; import {browser, by, element, ElementFinder} from 'protractor'; class E2eTestUtils { static readonly WINSCOPE_URL = 'http://localhost:8080'; static readonly REMOTE_TOOL_MOCK_URL = 'http://localhost:8081'; static async checkServerIsUp(name: string, url: string) { try { await browser.get(url); } catch (error) { fail(`${name} server (${url}) looks down. Did you start it?`); } } static async loadTraceAndCheckViewer( fixturePath: string, viewerTabTitle: string, viewerSelector: string, ) { await E2eTestUtils.uploadFixture(fixturePath); await E2eTestUtils.closeSnackBar(); await E2eTestUtils.clickViewTracesButton(); await E2eTestUtils.clickViewerTabButton(viewerTabTitle); const viewerPresent = await element(by.css(viewerSelector)).isPresent(); expect(viewerPresent).toBeTruthy(); } static async loadBugReport(defaulttimeMs: number) { await E2eTestUtils.uploadFixture('bugreports/bugreport_stripped.zip'); await E2eTestUtils.checkHasLoadedTracesFromBugReport(); expect(await E2eTestUtils.areMessagesEmitted(defaulttimeMs)).toBeTruthy(); await E2eTestUtils.checkEmitsUnsupportedFileFormatMessages(); await E2eTestUtils.checkEmitsOldDataMessages(); await E2eTestUtils.closeSnackBar(); } static async areMessagesEmitted(defaultTimeoutMs: number): Promise { // Messages are emitted quickly. There is no Need to wait for the entire // default timeout to understand whether the messages where emitted or not. await browser.manage().timeouts().implicitlyWait(1000); const emitted = await element(by.css('snack-bar')).isPresent(); await browser.manage().timeouts().implicitlyWait(defaultTimeoutMs); return emitted; } static async clickViewTracesButton() { const button = element(by.css('.load-btn')); await button.click(); } static async clickClearAllButton() { const button = element(by.css('.clear-all-btn')); await button.click(); } static async clickCloseIcon() { const button = element.all(by.css('.uploaded-files button')).first(); await button.click(); } static async clickDownloadTracesButton() { const button = element(by.css('.save-button')); await button.click(); } static async clickUploadNewButton() { const button = element(by.css('.upload-new')); await button.click(); } static async closeSnackBar() { const closeButton = element(by.css('.snack-bar-action')); const isPresent = await closeButton.isPresent(); if (isPresent) { await closeButton.click(); } } static async clickViewerTabButton(title: string) { const tabs: ElementFinder[] = await element.all(by.css('trace-view .tab')); for (const tab of tabs) { const tabTitle = await tab.getText(); if (tabTitle.includes(title)) { await tab.click(); return; } } throw Error(`could not find tab corresponding to ${title}`); } static async checkTimelineTraceSelector(trace: { icon: string; color: string; }) { const traceSelector = element(by.css('#trace-selector')); const text = await traceSelector.getText(); expect(text).toContain(trace.icon); const icons: ElementFinder[] = await element.all( by.css('.shown-selection .mat-icon'), ); const iconColors: string[] = []; for (const icon of icons) { iconColors.push(await icon.getCssValue('color')); } expect( iconColors.some((iconColor) => iconColor === trace.color), ).toBeTruthy(); } static async checkInitialRealTimestamp(timestamp: string) { await E2eTestUtils.changeRealTimestampInWinscope(timestamp); await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); const prevEntryButton = element(by.css('#prev_entry_button')); const isDisabled = await prevEntryButton.getAttribute('disabled'); expect(isDisabled).toEqual('true'); } static async checkFinalRealTimestamp(timestamp: string) { await E2eTestUtils.changeRealTimestampInWinscope(timestamp); await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12)); const nextEntryButton = element(by.css('#next_entry_button')); const isDisabled = await nextEntryButton.getAttribute('disabled'); expect(isDisabled).toEqual('true'); } static async checkWinscopeRealTimestamp(timestamp: string) { const inputElement = element(by.css('input[name="humanTimeInput"]')); const value = await inputElement.getAttribute('value'); expect(value).toEqual(timestamp); } static async changeRealTimestampInWinscope(newTimestamp: string) { await E2eTestUtils.updateInputField('', 'humanTimeInput', newTimestamp); } static async checkWinscopeNsTimestamp(newTimestamp: string) { const inputElement = element(by.css('input[name="nsTimeInput"]')); const valueWithNsSuffix = await inputElement.getAttribute('value'); expect(valueWithNsSuffix).toEqual(newTimestamp + ' ns'); } static async changeNsTimestampInWinscope(newTimestamp: string) { await E2eTestUtils.updateInputField('', 'nsTimeInput', newTimestamp); } static async filterHierarchy(viewer: string, filterString: string) { await E2eTestUtils.updateInputField( `${viewer} hierarchy-view .title-section`, 'filter', filterString, ); } static async updateInputField( inputFieldSelector: string, inputFieldName: string, newInput: string, ) { const inputElement = element( by.css(`${inputFieldSelector} input[name="${inputFieldName}"]`), ); const inputStringStep1 = newInput.slice(0, -1); const inputStringStep2 = newInput.slice(-1) + '\r\n'; const script = `document.querySelector("${inputFieldSelector} input[name=\\"${inputFieldName}\\"]").value = "${inputStringStep1}"`; await browser.executeScript(script); await inputElement.sendKeys(inputStringStep2); } static async selectItemInHierarchy(viewer: string, itemName: string) { const nodes: ElementFinder[] = await element.all( by.css(`${viewer} hierarchy-view .node`), ); for (const node of nodes) { const id = await node.getAttribute('id'); if (id.includes(itemName)) { await node.click(); return; } } throw Error(`could not find item matching ${itemName} in hierarchy`); } static async applyStateToHierarchyOptions( viewerSelector: string, shouldEnable: boolean, ) { const options: ElementFinder[] = await element.all( by.css(`${viewerSelector} hierarchy-view .view-controls .user-option`), ); for (const option of options) { const isEnabled = !(await option.getAttribute('class')).includes( 'not-enabled', ); if (shouldEnable && !isEnabled) { await option.click(); } else if (!shouldEnable && isEnabled) { await option.click(); } } } static async checkItemInPropertiesTree( viewer: string, itemName: string, expectedText: string, ) { const nodes = await element.all(by.css(`${viewer} .properties-view .node`)); for (const node of nodes) { const id: string = await node.getAttribute('id'); if (id === 'node' + itemName) { const text = await node.getText(); expect(text).toEqual(expectedText); return; } } throw Error(`could not find item ${itemName} in properties tree`); } static async checkRectLabel(viewer: string, expectedLabel: string) { const labels = await element.all( by.css(`${viewer} rects-view .rect-label`), ); let foundLabel: ElementFinder | undefined; for (const label of labels) { const text = await label.getText(); if (text.includes(expectedLabel)) { foundLabel = label; break; } } expect(foundLabel).toBeTruthy(); } static async checkTotalScrollEntries( selectors: {viewer: string; scroll: string; entry: string}, scrollViewport: Function, numberOfEntries: number, scrollToBottomOffset?: number | undefined, ) { if (scrollToBottomOffset !== undefined) { const viewport = element( by.css(`${selectors.viewer} ${selectors.scroll}`), ); await browser.executeAsyncScript( scrollViewport, viewport, scrollToBottomOffset, ); } const entries: ElementFinder[] = await element.all( by.css(`${selectors.viewer} ${selectors.scroll} ${selectors.entry}`), ); expect(await entries[entries.length - 1].getAttribute('item-id')).toEqual( `${numberOfEntries - 1}`, ); } static async toggleSelectFilterOptions( viewerSelector: string, filterSelector: string, options: string[], ) { const selectFilter = element( by.css( `${viewerSelector} .filters ${filterSelector} .mat-select-trigger`, ), ); await selectFilter.click(); const optionElements: ElementFinder[] = await element.all( by.css('.mat-select-panel .mat-option'), ); for (const optionEl of optionElements) { const optionText = (await optionEl.getText()).trim(); if (options.some((option) => optionText === option)) { await optionEl.click(); } } const backdrop = element(by.css('.cdk-overlay-backdrop')); await browser.actions().mouseMove(backdrop, {x: 0, y: 0}).click().perform(); } static async uploadFixture(...paths: string[]) { const inputFile = element(by.css('input[type="file"]')); // Uploading multiple files is not properly supported but // chrome handles file paths joined with new lines await inputFile.sendKeys( paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'), ); } static getFixturePath(filename: string): string { if (path.isAbsolute(filename)) { return filename; } return path.join( E2eTestUtils.getProjectRootPath(), 'src/test/fixtures', filename, ); } private static getProjectRootPath(): string { let root = __dirname; while (path.basename(root) !== 'winscope') { root = path.dirname(root); } return root; } private static async checkHasLoadedTracesFromBugReport() { const text = await element(by.css('.uploaded-files')).getText(); expect(text).toContain('Window Manager'); expect(text).toContain('Surface Flinger'); expect(text).toContain('Transactions'); expect(text).toContain('Transitions'); // Should be merged into a single Transitions trace expect(text).not.toContain('WM Transitions'); expect(text).not.toContain('Shell Transitions'); expect(text).toContain('layers_trace_from_transactions.winscope'); expect(text).toContain('transactions_trace.winscope'); expect(text).toContain('wm_transition_trace.winscope'); expect(text).toContain('shell_transition_trace.winscope'); expect(text).toContain('window_CRITICAL.proto'); // discards some traces due to old data expect(text).not.toContain('ProtoLog'); expect(text).not.toContain('IME Service'); expect(text).not.toContain('IME system_server'); expect(text).not.toContain('IME Clients'); expect(text).not.toContain('wm_log.winscope'); expect(text).not.toContain('ime_trace_service.winscope'); expect(text).not.toContain('ime_trace_managerservice.winscope'); expect(text).not.toContain('wm_trace.winscope'); expect(text).not.toContain('ime_trace_clients.winscope'); } private static async checkEmitsUnsupportedFileFormatMessages() { const text = await element(by.css('snack-bar')).getText(); expect(text).toContain('unsupported format'); } private static async checkEmitsOldDataMessages() { const text = await element(by.css('snack-bar')).getText(); expect(text).toContain('discarded because data is old'); } } export {E2eTestUtils};