• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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