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