xref: /aosp_15_r20/development/tools/winscope/src/test/e2e/utils.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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