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} 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 checkServerIsUp(name: string, url: string) {
24    try {
25      await browser.get(url);
26    } catch (error) {
27      fail(`${name} server (${url}) looks down. Did you start it?`);
28    }
29  }
30
31  static async loadTraceAndCheckViewer(
32    fixturePath: string,
33    viewerTabTitle: string,
34    viewerSelector: string,
35  ) {
36    await E2eTestUtils.uploadFixture(fixturePath);
37    await E2eTestUtils.closeSnackBar();
38    await E2eTestUtils.clickViewTracesButton();
39    await E2eTestUtils.clickViewerTabButton(viewerTabTitle);
40
41    const viewerPresent = await element(by.css(viewerSelector)).isPresent();
42    expect(viewerPresent).toBeTruthy();
43  }
44
45  static async loadBugReport(defaulttimeMs: number) {
46    await E2eTestUtils.uploadFixture('bugreports/bugreport_stripped.zip');
47    await E2eTestUtils.checkHasLoadedTracesFromBugReport();
48    expect(await E2eTestUtils.areMessagesEmitted(defaulttimeMs)).toBeTruthy();
49    await E2eTestUtils.checkEmitsUnsupportedFileFormatMessages();
50    await E2eTestUtils.checkEmitsOldDataMessages();
51    await E2eTestUtils.closeSnackBar();
52  }
53
54  static async areMessagesEmitted(defaultTimeoutMs: number): Promise<boolean> {
55    // Messages are emitted quickly. There is no Need to wait for the entire
56    // default timeout to understand whether the messages where emitted or not.
57    await browser.manage().timeouts().implicitlyWait(1000);
58    const emitted = await element(by.css('snack-bar')).isPresent();
59    await browser.manage().timeouts().implicitlyWait(defaultTimeoutMs);
60    return emitted;
61  }
62
63  static async clickViewTracesButton() {
64    const button = element(by.css('.load-btn'));
65    await button.click();
66  }
67
68  static async clickClearAllButton() {
69    const button = element(by.css('.clear-all-btn'));
70    await button.click();
71  }
72
73  static async clickCloseIcon() {
74    const button = element.all(by.css('.uploaded-files button')).first();
75    await button.click();
76  }
77
78  static async clickDownloadTracesButton() {
79    const button = element(by.css('.save-button'));
80    await button.click();
81  }
82
83  static async clickUploadNewButton() {
84    const button = element(by.css('.upload-new'));
85    await button.click();
86  }
87
88  static async closeSnackBar() {
89    const closeButton = element(by.css('.snack-bar-action'));
90    const isPresent = await closeButton.isPresent();
91    if (isPresent) {
92      await closeButton.click();
93    }
94  }
95
96  static async clickViewerTabButton(title: string) {
97    const tabs: ElementFinder[] = await element.all(by.css('trace-view .tab'));
98    for (const tab of tabs) {
99      const tabTitle = await tab.getText();
100      if (tabTitle.includes(title)) {
101        await tab.click();
102        return;
103      }
104    }
105    throw Error(`could not find tab corresponding to ${title}`);
106  }
107
108  static async checkTimelineTraceSelector(trace: {
109    icon: string;
110    color: string;
111  }) {
112    const traceSelector = element(by.css('#trace-selector'));
113    const text = await traceSelector.getText();
114    expect(text).toContain(trace.icon);
115
116    const icons: ElementFinder[] = await element.all(
117      by.css('.shown-selection .mat-icon'),
118    );
119    const iconColors: string[] = [];
120    for (const icon of icons) {
121      iconColors.push(await icon.getCssValue('color'));
122    }
123    expect(
124      iconColors.some((iconColor) => iconColor === trace.color),
125    ).toBeTruthy();
126  }
127
128  static async checkInitialRealTimestamp(timestamp: string) {
129    await E2eTestUtils.changeRealTimestampInWinscope(timestamp);
130    await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12));
131    const prevEntryButton = element(by.css('#prev_entry_button'));
132    const isDisabled = await prevEntryButton.getAttribute('disabled');
133    expect(isDisabled).toEqual('true');
134  }
135
136  static async checkFinalRealTimestamp(timestamp: string) {
137    await E2eTestUtils.changeRealTimestampInWinscope(timestamp);
138    await E2eTestUtils.checkWinscopeRealTimestamp(timestamp.slice(12));
139    const nextEntryButton = element(by.css('#next_entry_button'));
140    const isDisabled = await nextEntryButton.getAttribute('disabled');
141    expect(isDisabled).toEqual('true');
142  }
143
144  static async checkWinscopeRealTimestamp(timestamp: string) {
145    const inputElement = element(by.css('input[name="humanTimeInput"]'));
146    const value = await inputElement.getAttribute('value');
147    expect(value).toEqual(timestamp);
148  }
149
150  static async changeRealTimestampInWinscope(newTimestamp: string) {
151    await E2eTestUtils.updateInputField('', 'humanTimeInput', newTimestamp);
152  }
153
154  static async checkWinscopeNsTimestamp(newTimestamp: string) {
155    const inputElement = element(by.css('input[name="nsTimeInput"]'));
156    const valueWithNsSuffix = await inputElement.getAttribute('value');
157    expect(valueWithNsSuffix).toEqual(newTimestamp + ' ns');
158  }
159
160  static async changeNsTimestampInWinscope(newTimestamp: string) {
161    await E2eTestUtils.updateInputField('', 'nsTimeInput', newTimestamp);
162  }
163
164  static async filterHierarchy(viewer: string, filterString: string) {
165    await E2eTestUtils.updateInputField(
166      `${viewer} hierarchy-view .title-section`,
167      'filter',
168      filterString,
169    );
170  }
171
172  static async updateInputField(
173    inputFieldSelector: string,
174    inputFieldName: string,
175    newInput: string,
176  ) {
177    const inputElement = element(
178      by.css(`${inputFieldSelector} input[name="${inputFieldName}"]`),
179    );
180    const inputStringStep1 = newInput.slice(0, -1);
181    const inputStringStep2 = newInput.slice(-1) + '\r\n';
182    const script = `document.querySelector("${inputFieldSelector} input[name=\\"${inputFieldName}\\"]").value = "${inputStringStep1}"`;
183    await browser.executeScript(script);
184    await inputElement.sendKeys(inputStringStep2);
185  }
186
187  static async selectItemInHierarchy(viewer: string, itemName: string) {
188    const nodes: ElementFinder[] = await element.all(
189      by.css(`${viewer} hierarchy-view .node`),
190    );
191    for (const node of nodes) {
192      const id = await node.getAttribute('id');
193      if (id.includes(itemName)) {
194        await node.click();
195        return;
196      }
197    }
198    throw Error(`could not find item matching ${itemName} in hierarchy`);
199  }
200
201  static async applyStateToHierarchyOptions(
202    viewerSelector: string,
203    shouldEnable: boolean,
204  ) {
205    const options: ElementFinder[] = await element.all(
206      by.css(`${viewerSelector} hierarchy-view .view-controls .user-option`),
207    );
208    for (const option of options) {
209      const isEnabled = !(await option.getAttribute('class')).includes(
210        'not-enabled',
211      );
212      if (shouldEnable && !isEnabled) {
213        await option.click();
214      } else if (!shouldEnable && isEnabled) {
215        await option.click();
216      }
217    }
218  }
219
220  static async checkItemInPropertiesTree(
221    viewer: string,
222    itemName: string,
223    expectedText: string,
224  ) {
225    const nodes = await element.all(by.css(`${viewer} .properties-view .node`));
226    for (const node of nodes) {
227      const id: string = await node.getAttribute('id');
228      if (id === 'node' + itemName) {
229        const text = await node.getText();
230        expect(text).toEqual(expectedText);
231        return;
232      }
233    }
234    throw Error(`could not find item ${itemName} in properties tree`);
235  }
236
237  static async checkRectLabel(viewer: string, expectedLabel: string) {
238    const labels = await element.all(
239      by.css(`${viewer} rects-view .rect-label`),
240    );
241
242    let foundLabel: ElementFinder | undefined;
243
244    for (const label of labels) {
245      const text = await label.getText();
246      if (text.includes(expectedLabel)) {
247        foundLabel = label;
248        break;
249      }
250    }
251
252    expect(foundLabel).toBeTruthy();
253  }
254
255  static async checkTotalScrollEntries(
256    selectors: {viewer: string; scroll: string; entry: string},
257    scrollViewport: Function,
258    numberOfEntries: number,
259    scrollToBottomOffset?: number | undefined,
260  ) {
261    if (scrollToBottomOffset !== undefined) {
262      const viewport = element(
263        by.css(`${selectors.viewer} ${selectors.scroll}`),
264      );
265      await browser.executeAsyncScript(
266        scrollViewport,
267        viewport,
268        scrollToBottomOffset,
269      );
270    }
271    const entries: ElementFinder[] = await element.all(
272      by.css(`${selectors.viewer} ${selectors.scroll} ${selectors.entry}`),
273    );
274    expect(await entries[entries.length - 1].getAttribute('item-id')).toEqual(
275      `${numberOfEntries - 1}`,
276    );
277  }
278
279  static async toggleSelectFilterOptions(
280    viewerSelector: string,
281    filterSelector: string,
282    options: string[],
283  ) {
284    const selectFilter = element(
285      by.css(
286        `${viewerSelector} .filters ${filterSelector} .mat-select-trigger`,
287      ),
288    );
289    await selectFilter.click();
290
291    const optionElements: ElementFinder[] = await element.all(
292      by.css('.mat-select-panel .mat-option'),
293    );
294    for (const optionEl of optionElements) {
295      const optionText = (await optionEl.getText()).trim();
296      if (options.some((option) => optionText === option)) {
297        await optionEl.click();
298      }
299    }
300
301    const backdrop = element(by.css('.cdk-overlay-backdrop'));
302    await browser.actions().mouseMove(backdrop, {x: 0, y: 0}).click().perform();
303  }
304
305  static async uploadFixture(...paths: string[]) {
306    const inputFile = element(by.css('input[type="file"]'));
307
308    // Uploading multiple files is not properly supported but
309    // chrome handles file paths joined with new lines
310    await inputFile.sendKeys(
311      paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'),
312    );
313  }
314
315  static getFixturePath(filename: string): string {
316    if (path.isAbsolute(filename)) {
317      return filename;
318    }
319    return path.join(
320      E2eTestUtils.getProjectRootPath(),
321      'src/test/fixtures',
322      filename,
323    );
324  }
325
326  private static getProjectRootPath(): string {
327    let root = __dirname;
328    while (path.basename(root) !== 'winscope') {
329      root = path.dirname(root);
330    }
331    return root;
332  }
333
334  private static async checkHasLoadedTracesFromBugReport() {
335    const text = await element(by.css('.uploaded-files')).getText();
336    expect(text).toContain('Window Manager');
337    expect(text).toContain('Surface Flinger');
338    expect(text).toContain('Transactions');
339    expect(text).toContain('Transitions');
340
341    // Should be merged into a single Transitions trace
342    expect(text).not.toContain('WM Transitions');
343    expect(text).not.toContain('Shell Transitions');
344
345    expect(text).toContain('layers_trace_from_transactions.winscope');
346    expect(text).toContain('transactions_trace.winscope');
347    expect(text).toContain('wm_transition_trace.winscope');
348    expect(text).toContain('shell_transition_trace.winscope');
349    expect(text).toContain('window_CRITICAL.proto');
350
351    // discards some traces due to old data
352    expect(text).not.toContain('ProtoLog');
353    expect(text).not.toContain('IME Service');
354    expect(text).not.toContain('IME system_server');
355    expect(text).not.toContain('IME Clients');
356    expect(text).not.toContain('wm_log.winscope');
357    expect(text).not.toContain('ime_trace_service.winscope');
358    expect(text).not.toContain('ime_trace_managerservice.winscope');
359    expect(text).not.toContain('wm_trace.winscope');
360    expect(text).not.toContain('ime_trace_clients.winscope');
361  }
362
363  private static async checkEmitsUnsupportedFileFormatMessages() {
364    const text = await element(by.css('snack-bar')).getText();
365    expect(text).toContain('unsupported format');
366  }
367
368  private static async checkEmitsOldDataMessages() {
369    const text = await element(by.css('snack-bar')).getText();
370    expect(text).toContain('discarded because data is old');
371  }
372}
373
374export {E2eTestUtils};
375