• 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 {ComponentFixture, TestBed} from '@angular/core/testing';
17import {MatCardModule} from '@angular/material/card';
18import {MatIconModule} from '@angular/material/icon';
19import {MatListModule} from '@angular/material/list';
20import {MatProgressBarModule} from '@angular/material/progress-bar';
21import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';
22import {MatTooltipModule} from '@angular/material/tooltip';
23import {By} from '@angular/platform-browser';
24import {FilesSource} from 'app/files_source';
25import {TracePipeline} from 'app/trace_pipeline';
26import {assertDefined} from 'common/assert_utils';
27import {TimestampConverterUtils} from 'common/time/test_utils';
28import {
29  AppTraceViewRequest,
30  AppTraceViewRequestHandled,
31} from 'messaging/winscope_event';
32import {getFixtureFile} from 'test/unit/fixture_utils';
33import {TraceBuilder} from 'test/unit/trace_builder';
34import {Traces} from 'trace/traces';
35import {LoadProgressComponent} from './load_progress_component';
36import {UploadTracesComponent} from './upload_traces_component';
37
38describe('UploadTracesComponent', () => {
39  const uploadSelector = '.upload-btn';
40  const clearAllSelector = '.clear-all-btn';
41  const viewTracesSelector = '.load-btn';
42  const removeTraceSelector = '.uploaded-files button';
43  let fixture: ComponentFixture<UploadTracesComponent>;
44  let component: UploadTracesComponent;
45  let htmlElement: HTMLElement;
46  let validSfFile: File;
47  let validWmFile: File;
48
49  beforeEach(async () => {
50    await TestBed.configureTestingModule({
51      imports: [
52        MatCardModule,
53        MatSnackBarModule,
54        MatListModule,
55        MatIconModule,
56        MatProgressBarModule,
57        MatTooltipModule,
58      ],
59      providers: [MatSnackBar],
60      declarations: [UploadTracesComponent, LoadProgressComponent],
61    }).compileComponents();
62    fixture = TestBed.createComponent(UploadTracesComponent);
63    component = fixture.componentInstance;
64    htmlElement = fixture.nativeElement;
65    component.tracePipeline = new TracePipeline();
66    validSfFile = await getFixtureFile(
67      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
68    );
69    validWmFile = await getFixtureFile(
70      'traces/elapsed_and_real_timestamp/WindowManager.pb',
71    );
72    fixture.detectChanges();
73  });
74
75  it('can be created', () => {
76    expect(component).toBeTruthy();
77  });
78
79  it('renders the expected card title', () => {
80    expect(htmlElement.querySelector('.title')?.innerHTML).toContain(
81      'Upload Traces',
82    );
83  });
84
85  it('handles file upload via drag and drop', () => {
86    const spy = spyOn(component.filesUploaded, 'emit');
87    dropFileAndGetTransferredFiles(false);
88    expect(spy).not.toHaveBeenCalled();
89    const files = dropFileAndGetTransferredFiles();
90    expect(spy).toHaveBeenCalledOnceWith(files);
91  });
92
93  it('handles file upload via upload button click', async () => {
94    await loadFiles([validSfFile]);
95    const spy = spyOn(component.filesUploaded, 'emit');
96    addFileByClickAndGetTransferredFiles(false);
97    expect(spy).not.toHaveBeenCalled();
98    const files = addFileByClickAndGetTransferredFiles();
99    expect(spy).toHaveBeenCalledOnceWith(files);
100  });
101
102  it('displays only load progress bar on progress update (no existing files)', () => {
103    component.onProgressUpdate(undefined, undefined);
104    fixture.detectChanges();
105    checkOnlyProgressBarShowing();
106
107    component.onOperationFinished();
108    fixture.detectChanges();
109    expect(htmlElement.querySelector('load-progress')).toBeNull();
110    assertDefined(htmlElement.querySelector('.drop-info'));
111  });
112
113  it('displays only load progress bar on progress update (existing files)', async () => {
114    await loadFiles([validSfFile]);
115    component.onProgressUpdate(undefined, undefined);
116    fixture.detectChanges();
117    checkOnlyProgressBarShowing();
118
119    component.onOperationFinished();
120    fixture.detectChanges();
121    expect(htmlElement.querySelector('load-progress')).toBeNull();
122    assertDefined(htmlElement.querySelector('.trace-actions-container'));
123    assertDefined(htmlElement.querySelector('.uploaded-files'));
124  });
125
126  it('shows progress bar with custom message', () => {
127    component.onProgressUpdate('Updating', undefined);
128    fixture.detectChanges();
129    checkOnlyProgressBarShowing('Updating');
130  });
131
132  it('updates progress bar percentage only if sufficient time has passed', () => {
133    component.onProgressUpdate(undefined, 10);
134    fixture.detectChanges();
135    const progressBar = fixture.debugElement.query(
136      By.directive(LoadProgressComponent),
137    ).componentInstance as LoadProgressComponent;
138    expect(progressBar.progressPercentage).toEqual(10);
139
140    component.onProgressUpdate(undefined, 20);
141    fixture.detectChanges();
142    expect(progressBar.progressPercentage).toEqual(10);
143
144    const now = Date.now();
145    spyOn(Date, 'now').and.returnValue(now + 500);
146    component.onProgressUpdate(undefined, 20);
147    fixture.detectChanges();
148    expect(progressBar.progressPercentage).toEqual(20);
149  });
150
151  it('can display uploaded traces', async () => {
152    await loadFiles([validSfFile]);
153    assertDefined(htmlElement.querySelector('.uploaded-files'));
154    assertDefined(htmlElement.querySelector('.trace-actions-container'));
155  });
156
157  it('can remove one of two uploaded traces', async () => {
158    await loadFiles([validSfFile, validWmFile]);
159    expect(component.tracePipeline?.getTraces().getSize()).toBe(2);
160
161    const spy = spyOn(component, 'onOperationFinished');
162    removeTrace();
163    assertDefined(htmlElement.querySelector('.uploaded-files'));
164    expect(spy).toHaveBeenCalled();
165    expect(component.tracePipeline?.getTraces().getSize()).toBe(1);
166  });
167
168  it('handles removal of the only uploaded trace', async () => {
169    await loadFiles([validSfFile]);
170
171    const spy = spyOn(component, 'onOperationFinished');
172    removeTrace();
173    assertDefined(htmlElement.querySelector('.drop-info'));
174    expect(spy).toHaveBeenCalled();
175    expect(component.tracePipeline?.getTraces().getSize()).toBe(0);
176  });
177
178  it('can remove all uploaded traces', async () => {
179    await loadFiles([validSfFile, validWmFile]);
180    expect(component.tracePipeline?.getTraces().getSize()).toBe(2);
181
182    const spy = spyOn(component, 'onOperationFinished');
183    const clearAllButton = getButton(clearAllSelector);
184    clearAllButton.click();
185    fixture.detectChanges();
186    assertDefined(htmlElement.querySelector('.drop-info'));
187    expect(spy).toHaveBeenCalled();
188    expect(component.tracePipeline?.getTraces().getSize()).toBe(0);
189  });
190
191  it('can emit view traces event', async () => {
192    await loadFiles([validSfFile]);
193
194    const spy = spyOn(component.viewTracesButtonClick, 'emit');
195    getButton(viewTracesSelector).click();
196    fixture.detectChanges();
197    expect(spy).toHaveBeenCalled();
198  });
199
200  it('shows warning elements for traces without visualization', async () => {
201    const shellTransitionFile = await getFixtureFile(
202      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
203    );
204    await loadFiles([shellTransitionFile]);
205
206    expect(htmlElement.querySelector('.warning-icon')).toBeTruthy();
207    expect(getButton(viewTracesSelector).disabled).toBeTrue();
208  });
209
210  it('shows error elements for corrupted traces', async () => {
211    const corruptedTrace = new TraceBuilder<string>()
212      .setEntries(['entry-0'])
213      .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()])
214      .build();
215    corruptedTrace.setCorruptedState(true);
216    const traces = new Traces();
217    traces.addTrace(corruptedTrace);
218    spyOn(assertDefined(component.tracePipeline), 'getTraces').and.returnValue(
219      traces,
220    );
221    fixture.detectChanges();
222
223    expect(htmlElement.querySelector('.error-icon')).toBeTruthy();
224    expect(getButton(viewTracesSelector).disabled).toBeTrue();
225  });
226
227  it('emits download traces event', async () => {
228    await loadFiles([validSfFile]);
229
230    const spy = spyOn(component.downloadTracesClick, 'emit');
231    const downloadTracesButton = assertDefined(
232      htmlElement.querySelector<HTMLElement>('.download-btn'),
233    );
234    downloadTracesButton.click();
235    fixture.detectChanges();
236    expect(spy).toHaveBeenCalled();
237  });
238
239  it('disables edit/view traces functionality on trace view request events', async () => {
240    await loadFiles([validSfFile]);
241    const buttons = [
242      getButton(viewTracesSelector),
243      getButton(removeTraceSelector),
244      getButton(clearAllSelector),
245      getButton(uploadSelector),
246    ];
247    const dropBox = assertDefined(
248      htmlElement.querySelector<HTMLElement>('.drop-box'),
249    );
250    const spy = spyOn(component.filesUploaded, 'emit');
251
252    await component.onWinscopeEvent(new AppTraceViewRequest());
253    fixture.detectChanges();
254    buttons.forEach((button) => {
255      expect(button.disabled).toBeTrue();
256    });
257    dropFileAndGetTransferredFiles();
258    addFileByClickAndGetTransferredFiles(true, dropBox);
259    expect(spy).not.toHaveBeenCalled();
260
261    await component.onWinscopeEvent(new AppTraceViewRequestHandled());
262    fixture.detectChanges();
263    buttons.forEach((button) => {
264      expect(button.disabled).toBeFalse();
265    });
266    const files = dropFileAndGetTransferredFiles();
267    expect(spy).toHaveBeenCalledOnceWith(files);
268    spy.calls.reset();
269    addFileByClickAndGetTransferredFiles(true, dropBox);
270    expect(spy).toHaveBeenCalledOnceWith(files);
271  });
272
273  async function loadFiles(files: File[]) {
274    const tracePipeline = assertDefined(component.tracePipeline);
275    tracePipeline.clear();
276    await tracePipeline.loadFiles(files, FilesSource.TEST, undefined);
277    fixture.detectChanges();
278  }
279
280  function dropFileAndGetTransferredFiles(withFile = true): File[] {
281    const dropbox = assertDefined(htmlElement.querySelector('.drop-box'));
282    let dataTransfer: DataTransfer | undefined;
283    if (withFile) {
284      dataTransfer = new DataTransfer();
285      dataTransfer.items.add(validSfFile);
286    }
287    dropbox.dispatchEvent(new DragEvent('drop', {dataTransfer}));
288    fixture.detectChanges();
289    return Array.from(dataTransfer?.files ?? []);
290  }
291
292  function addFileByClickAndGetTransferredFiles(
293    withFile = true,
294    clickEl: HTMLElement = getButton(uploadSelector),
295  ): File[] {
296    const dataTransfer = new DataTransfer();
297    if (withFile) dataTransfer.items.add(validSfFile);
298    const fileList = dataTransfer.files;
299
300    const fileInput = assertDefined(
301      htmlElement.querySelector<HTMLInputElement>('.drop-box input'),
302    );
303    clickEl.addEventListener('click', () => {
304      fileInput.files = fileList;
305    });
306
307    clickEl.click();
308    fixture.detectChanges();
309    fileInput.dispatchEvent(new Event('change'));
310    fixture.detectChanges();
311    return Array.from(fileList);
312  }
313
314  function removeTrace() {
315    getButton(removeTraceSelector).click();
316    fixture.detectChanges();
317  }
318
319  function getButton(selector: string): HTMLButtonElement {
320    return assertDefined(
321      htmlElement.querySelector<HTMLButtonElement>(selector),
322    );
323  }
324
325  function checkOnlyProgressBarShowing(expectedMessage = 'Loading...') {
326    const progressBar = assertDefined(
327      htmlElement.querySelector('load-progress'),
328    );
329    expect(progressBar.textContent).toEqual(expectedMessage);
330    expect(htmlElement.querySelector('.trace-actions-container')).toBeNull();
331    expect(htmlElement.querySelector('.uploaded-files')).toBeNull();
332    expect(htmlElement.querySelector('.drop-info')).toBeNull();
333  }
334});
335