• 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 {ClipboardModule} from '@angular/cdk/clipboard';
17import {OverlayModule} from '@angular/cdk/overlay';
18import {CommonModule} from '@angular/common';
19import {HttpClientModule} from '@angular/common/http';
20import {ChangeDetectionStrategy} from '@angular/core';
21import {
22  ComponentFixture,
23  ComponentFixtureAutoDetect,
24  TestBed,
25} from '@angular/core/testing';
26import {
27  FormControl,
28  FormsModule,
29  ReactiveFormsModule,
30  Validators,
31} from '@angular/forms';
32import {MatButtonModule} from '@angular/material/button';
33import {MatCardModule} from '@angular/material/card';
34import {MatDialogModule} from '@angular/material/dialog';
35import {MatDividerModule} from '@angular/material/divider';
36import {MatFormFieldModule} from '@angular/material/form-field';
37import {MatIconModule} from '@angular/material/icon';
38import {MatInputModule} from '@angular/material/input';
39import {MatListModule} from '@angular/material/list';
40import {MatProgressBarModule} from '@angular/material/progress-bar';
41import {MatSelectModule} from '@angular/material/select';
42import {MatSliderModule} from '@angular/material/slider';
43import {MatSnackBarModule} from '@angular/material/snack-bar';
44import {MatTabsModule} from '@angular/material/tabs';
45import {MatToolbarModule} from '@angular/material/toolbar';
46import {MatTooltipModule} from '@angular/material/tooltip';
47import {Title} from '@angular/platform-browser';
48import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
49import {assertDefined} from 'common/assert_utils';
50import {Download} from 'common/download';
51import {FileUtils} from 'common/file_utils';
52import {TimestampConverterUtils} from 'common/time/test_utils';
53import {UserNotifier} from 'common/user_notifier';
54import {
55  FailedToInitializeTimelineData,
56  NoValidFiles,
57} from 'messaging/user_warnings';
58import {
59  AppRefreshDumpsRequest,
60  ViewersLoaded,
61  ViewersUnloaded,
62} from 'messaging/winscope_event';
63import {TracesBuilder} from 'test/unit/traces_builder';
64import {waitToBeCalled} from 'test/utils';
65import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
66import {AppComponent} from './app_component';
67import {
68  MatDrawer,
69  MatDrawerContainer,
70  MatDrawerContent,
71} from './bottomnav/bottom_drawer_component';
72import {CollectTracesComponent} from './collect_traces_component';
73import {ShortcutsComponent} from './shortcuts_component';
74import {SnackBarComponent} from './snack_bar_component';
75import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component';
76import {TimelineComponent} from './timeline/timeline_component';
77import {TraceConfigComponent} from './trace_config_component';
78import {TraceViewComponent} from './trace_view_component';
79import {UploadTracesComponent} from './upload_traces_component';
80import {WdpSetupComponent} from './wdp_setup_component';
81import {WinscopeProxySetupComponent} from './winscope_proxy_setup_component';
82
83describe('AppComponent', () => {
84  let fixture: ComponentFixture<AppComponent>;
85  let component: AppComponent;
86  let htmlElement: HTMLElement;
87  let downloadTracesSpy: jasmine.Spy;
88
89  beforeEach(async () => {
90    await TestBed.configureTestingModule({
91      providers: [Title, {provide: ComponentFixtureAutoDetect, useValue: true}],
92      imports: [
93        CommonModule,
94        FormsModule,
95        MatCardModule,
96        MatButtonModule,
97        MatDividerModule,
98        MatFormFieldModule,
99        MatIconModule,
100        MatSelectModule,
101        MatSliderModule,
102        MatSnackBarModule,
103        MatToolbarModule,
104        MatTooltipModule,
105        ReactiveFormsModule,
106        MatInputModule,
107        BrowserAnimationsModule,
108        ClipboardModule,
109        MatDialogModule,
110        HttpClientModule,
111        MatListModule,
112        MatProgressBarModule,
113        OverlayModule,
114        MatTabsModule,
115      ],
116      declarations: [
117        WinscopeProxySetupComponent,
118        WdpSetupComponent,
119        AppComponent,
120        CollectTracesComponent,
121        MatDrawer,
122        MatDrawerContainer,
123        MatDrawerContent,
124        MiniTimelineComponent,
125        TimelineComponent,
126        TraceConfigComponent,
127        TraceViewComponent,
128        UploadTracesComponent,
129        ViewerSurfaceFlingerComponent,
130        ShortcutsComponent,
131        SnackBarComponent,
132      ],
133    })
134      .overrideComponent(AppComponent, {
135        set: {changeDetection: ChangeDetectionStrategy.Default},
136      })
137      .compileComponents();
138    fixture = TestBed.createComponent(AppComponent);
139    component = fixture.componentInstance;
140    htmlElement = fixture.nativeElement;
141    component.filenameFormControl = new FormControl(
142      'winscope',
143      Validators.compose([
144        Validators.required,
145        Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX),
146      ]),
147    );
148    downloadTracesSpy = spyOn(Download, 'fromUrl');
149    fixture.detectChanges();
150  });
151
152  it('can be created', () => {
153    expect(component).toBeTruthy();
154  });
155
156  it('has the expected title', () => {
157    expect(component.title).toEqual('winscope');
158  });
159
160  it('shows permanent header items on homepage', () => {
161    checkPermanentHeaderItems();
162  });
163
164  it('displays correct elements when no data loaded', () => {
165    component.dataLoaded = false;
166    component.showDataLoadedElements = false;
167    fixture.detectChanges();
168    checkHomepage();
169  });
170
171  it('displays correct elements when data loaded', () => {
172    goToTraceView();
173    checkTraceViewPage();
174
175    spyOn(component, 'dumpsUploaded').and.returnValue(true);
176    fixture.detectChanges();
177    expect(htmlElement.querySelector('.refresh-dumps')).toBeTruthy();
178  });
179
180  it('returns to homepage on upload new button click', async () => {
181    goToTraceView();
182    checkTraceViewPage();
183
184    assertDefined(
185      htmlElement.querySelector<HTMLButtonElement>('.upload-new'),
186    ).click();
187    fixture.detectChanges();
188    await fixture.whenStable();
189    fixture.detectChanges();
190    await fixture.whenStable();
191    checkHomepage();
192  });
193
194  it('sends event on refresh dumps button click', async () => {
195    spyOn(component, 'dumpsUploaded').and.returnValue(true);
196    goToTraceView();
197    checkTraceViewPage();
198
199    const winscopeEventSpy = spyOn(
200      component.mediator,
201      'onWinscopeEvent',
202    ).and.callThrough();
203    assertDefined(
204      htmlElement.querySelector<HTMLButtonElement>('.refresh-dumps'),
205    ).click();
206    fixture.detectChanges();
207    await fixture.whenStable();
208    fixture.detectChanges();
209    await fixture.whenStable();
210    checkHomepage();
211    expect(winscopeEventSpy).toHaveBeenCalledWith(new AppRefreshDumpsRequest());
212  });
213
214  it('shows download progress bar', () => {
215    component.showDataLoadedElements = true;
216    fixture.detectChanges();
217    expect(
218      htmlElement.querySelector('.download-files-section mat-progress-bar'),
219    ).toBeNull();
220
221    component.onProgressUpdate('Progress update', 10);
222    fixture.detectChanges();
223    expect(
224      htmlElement.querySelector('.download-files-section mat-progress-bar'),
225    ).toBeTruthy();
226
227    component.onOperationFinished(true);
228    fixture.detectChanges();
229    expect(
230      htmlElement.querySelector('.download-files-section mat-progress-bar'),
231    ).toBeNull();
232  });
233
234  it('downloads traces on download button click and shows download progress bar', async () => {
235    component.showDataLoadedElements = true;
236    fixture.detectChanges();
237    clickDownloadTracesButton();
238    expect(
239      htmlElement.querySelector('.download-files-section mat-progress-bar'),
240    ).toBeTruthy();
241    await waitToBeCalled(downloadTracesSpy);
242  });
243
244  it('downloads traces after valid file name change', async () => {
245    component.showDataLoadedElements = true;
246    fixture.detectChanges();
247
248    clickEditFilenameButton();
249    updateFilenameInputAndDownloadTraces('Winscope2', true);
250    await waitToBeCalled(downloadTracesSpy);
251    expect(downloadTracesSpy).toHaveBeenCalledOnceWith(
252      jasmine.any(String),
253      'Winscope2.zip',
254    );
255
256    downloadTracesSpy.calls.reset();
257
258    // check it works twice in a row
259    clickEditFilenameButton();
260    updateFilenameInputAndDownloadTraces('win_scope', true);
261    await waitToBeCalled(downloadTracesSpy);
262    expect(downloadTracesSpy).toHaveBeenCalledOnceWith(
263      jasmine.any(String),
264      'win_scope.zip',
265    );
266  });
267
268  it('changes page title based on archive name', async () => {
269    const pageTitle = TestBed.inject(Title);
270    component.timelineData.initialize(
271      new TracesBuilder().build(),
272      undefined,
273      TimestampConverterUtils.TIMESTAMP_CONVERTER,
274    );
275
276    await component.onWinscopeEvent(new ViewersUnloaded());
277    expect(pageTitle.getTitle()).toBe('Winscope');
278
279    component.tracePipeline.getDownloadArchiveFilename = jasmine
280      .createSpy()
281      .and.returnValue('test_archive');
282    await component.onWinscopeEvent(new ViewersLoaded([]));
283    fixture.detectChanges();
284    expect(pageTitle.getTitle()).toBe('Winscope | test_archive');
285  });
286
287  it('does not download traces if invalid file name chosen', () => {
288    component.showDataLoadedElements = true;
289    fixture.detectChanges();
290
291    clickEditFilenameButton();
292    updateFilenameInputAndDownloadTraces('w?n$cope', false);
293    expect(downloadTracesSpy).not.toHaveBeenCalled();
294  });
295
296  it('behaves as expected when entering valid then invalid then valid file names', async () => {
297    component.showDataLoadedElements = true;
298    fixture.detectChanges();
299
300    clickEditFilenameButton();
301    updateFilenameInputAndDownloadTraces('Winscope2', true);
302    await waitToBeCalled(downloadTracesSpy);
303    expect(downloadTracesSpy).toHaveBeenCalledOnceWith(
304      jasmine.any(String),
305      'Winscope2.zip',
306    );
307    downloadTracesSpy.calls.reset();
308
309    clickEditFilenameButton();
310    updateFilenameInputAndDownloadTraces('w?n$cope', false);
311    expect(downloadTracesSpy).not.toHaveBeenCalled();
312
313    updateFilenameInputAndDownloadTraces('win.scope', true);
314    await waitToBeCalled(downloadTracesSpy);
315    expect(downloadTracesSpy).toHaveBeenCalledOnceWith(
316      jasmine.any(String),
317      'win.scope.zip',
318    );
319  });
320
321  it('validates filename on enter key, escape key or focus out', () => {
322    const spy = spyOn(component, 'trySubmitFilename');
323
324    component.showDataLoadedElements = true;
325    fixture.detectChanges();
326    clickEditFilenameButton();
327    const inputField = assertDefined(
328      htmlElement.querySelector('.file-name-input-field'),
329    );
330    const inputEl = assertDefined(
331      htmlElement.querySelector<HTMLInputElement>(
332        '.file-name-input-field input',
333      ),
334    );
335    inputEl.value = 'valid_file_name';
336
337    inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
338    fixture.detectChanges();
339    expect(spy).toHaveBeenCalledTimes(1);
340
341    inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'}));
342    fixture.detectChanges();
343    expect(spy).toHaveBeenCalledTimes(2);
344
345    inputField.dispatchEvent(new FocusEvent('focusout'));
346    fixture.detectChanges();
347    expect(spy).toHaveBeenCalledTimes(3);
348  });
349
350  it('downloads traces from upload traces section', () => {
351    const traces = assertDefined(component.tracePipeline.getTraces());
352    spyOn(traces, 'getSize').and.returnValue(1);
353    fixture.detectChanges();
354    const downloadButtonClickSpy = spyOn(
355      component,
356      'onDownloadTracesButtonClick',
357    );
358
359    const downloadButton = assertDefined(
360      htmlElement.querySelector<HTMLElement>('upload-traces .download-btn'),
361    );
362    downloadButton.click();
363    fixture.detectChanges();
364    expect(downloadButtonClickSpy).toHaveBeenCalledOnceWith(
365      component.uploadTracesComponent,
366    );
367  });
368
369  it('opens shortcuts dialog', () => {
370    expect(document.querySelector('shortcuts-panel')).toBeFalsy();
371    const shortcutsButton = assertDefined(
372      htmlElement.querySelector<HTMLElement>('.shortcuts'),
373    );
374    shortcutsButton.click();
375    fixture.detectChanges();
376    expect(document.querySelector('shortcuts-panel')).toBeTruthy();
377  });
378
379  it('sets snackbar opener to global user notifier', () => {
380    expect(document.querySelector('snack-bar')).toBeFalsy();
381    UserNotifier.add(new NoValidFiles());
382    UserNotifier.notify();
383    expect(document.querySelector('snack-bar')).toBeTruthy();
384  });
385
386  it('does not open new snackbar until existing snackbar has been dismissed', async () => {
387    expect(document.querySelector('snack-bar')).toBeFalsy();
388    const firstMessage = new NoValidFiles();
389    UserNotifier.add(firstMessage);
390    UserNotifier.notify();
391    fixture.detectChanges();
392    await fixture.whenRenderingDone();
393    let snackbar = assertDefined(document.querySelector('snack-bar'));
394    expect(snackbar.textContent).toContain(firstMessage.getMessage());
395
396    const secondMessage = new FailedToInitializeTimelineData();
397    UserNotifier.add(secondMessage);
398    UserNotifier.notify();
399    fixture.detectChanges();
400    await fixture.whenRenderingDone();
401    snackbar = assertDefined(document.querySelector('snack-bar'));
402    expect(snackbar.textContent).toContain(firstMessage.getMessage());
403
404    const closeButton = assertDefined(
405      snackbar.querySelector<HTMLElement>('.snack-bar-action'),
406    );
407    closeButton.click();
408    fixture.detectChanges();
409    await fixture.whenRenderingDone();
410    snackbar = assertDefined(document.querySelector('snack-bar'));
411    expect(snackbar.textContent).toContain(secondMessage.getMessage());
412  });
413
414  function goToTraceView() {
415    component.dataLoaded = true;
416    component.showDataLoadedElements = true;
417    component.timelineData.initialize(
418      new TracesBuilder().build(),
419      undefined,
420      TimestampConverterUtils.TIMESTAMP_CONVERTER,
421    );
422    fixture.detectChanges();
423  }
424
425  function updateFilenameInputAndDownloadTraces(name: string, valid: boolean) {
426    const inputEl = assertDefined(
427      htmlElement.querySelector<HTMLInputElement>(
428        '.file-name-input-field input',
429      ),
430    );
431    const checkButton = assertDefined(
432      htmlElement.querySelector('.check-button'),
433    );
434    inputEl.value = name;
435    inputEl.dispatchEvent(new Event('input'));
436    fixture.detectChanges();
437    checkButton.dispatchEvent(new Event('click'));
438    fixture.detectChanges();
439
440    const saveButton = assertDefined(
441      htmlElement.querySelector<HTMLButtonElement>('.save-button'),
442    );
443    if (valid) {
444      assertDefined(htmlElement.querySelector('.download-file-info'));
445      expect(saveButton.disabled).toBeFalse();
446      clickDownloadTracesButton();
447    } else {
448      expect(htmlElement.querySelector('.download-file-info')).toBeFalsy();
449      expect(saveButton.disabled).toBeTrue();
450    }
451  }
452
453  function clickDownloadTracesButton() {
454    const downloadButton = assertDefined(
455      htmlElement.querySelector('.save-button'),
456    );
457    downloadButton.dispatchEvent(new Event('click'));
458    fixture.detectChanges();
459  }
460
461  function clickEditFilenameButton() {
462    const pencilButton = assertDefined(
463      htmlElement.querySelector('.edit-button'),
464    );
465    pencilButton.dispatchEvent(new Event('click'));
466    fixture.detectChanges();
467  }
468
469  function checkHomepage() {
470    expect(htmlElement.querySelector('.welcome-info')).toBeTruthy();
471    expect(htmlElement.querySelector('.collect-traces-card')).toBeTruthy();
472    expect(htmlElement.querySelector('.upload-traces-card')).toBeTruthy();
473    expect(htmlElement.querySelector('.viewers')).toBeFalsy();
474    expect(htmlElement.querySelector('.upload-new')).toBeFalsy();
475    expect(htmlElement.querySelector('timeline')).toBeFalsy();
476    checkPermanentHeaderItems();
477  }
478
479  function checkTraceViewPage() {
480    expect(htmlElement.querySelector('.welcome-info')).toBeFalsy();
481    expect(htmlElement.querySelector('.save-button')).toBeTruthy();
482    expect(htmlElement.querySelector('.collect-traces-card')).toBeFalsy();
483    expect(htmlElement.querySelector('.upload-traces-card')).toBeFalsy();
484    expect(htmlElement.querySelector('.viewers')).toBeTruthy();
485    expect(htmlElement.querySelector('.upload-new')).toBeTruthy();
486    expect(htmlElement.querySelector('timeline')).toBeTruthy();
487    checkPermanentHeaderItems();
488  }
489
490  function checkPermanentHeaderItems() {
491    expect(htmlElement.querySelector('.app-title')).toBeTruthy();
492    expect(htmlElement.querySelector('.shortcuts')).toBeTruthy();
493    expect(htmlElement.querySelector('.documentation')).toBeTruthy();
494    expect(htmlElement.querySelector('.report-bug')).toBeTruthy();
495    expect(htmlElement.querySelector('.dark-mode')).toBeTruthy();
496  }
497});
498