blob: 1fc6baab3fba2815d76e6cf5ac1036d54f6070cf [file] [log] [blame]
/*
* 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<boolean> {
// 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(
viewerSelector: string,
scrollViewport: Function,
numberOfEntries: number,
scrollToBottomOffset?: number | undefined,
) {
if (scrollToBottomOffset !== undefined) {
const viewport = element(by.css(`${viewerSelector} .scroll`));
await browser.executeAsyncScript(
scrollViewport,
viewport,
scrollToBottomOffset,
);
}
const entries: ElementFinder[] = await element.all(
by.css(`${viewerSelector} .scroll .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};