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 {CommonModule} from '@angular/common';
18import {HttpClientModule} from '@angular/common/http';
19import {ChangeDetectionStrategy} from '@angular/core';
20import {
21  ComponentFixture,
22  ComponentFixtureAutoDetect,
23  TestBed,
24} from '@angular/core/testing';
25import {
26  FormControl,
27  FormsModule,
28  ReactiveFormsModule,
29  Validators,
30} from '@angular/forms';
31import {MatButtonModule} from '@angular/material/button';
32import {MatCardModule} from '@angular/material/card';
33import {MatDialogModule} from '@angular/material/dialog';
34import {MatDividerModule} from '@angular/material/divider';
35import {MatFormFieldModule} from '@angular/material/form-field';
36import {MatIconModule} from '@angular/material/icon';
37import {MatInputModule} from '@angular/material/input';
38import {MatSelectModule} from '@angular/material/select';
39import {MatSliderModule} from '@angular/material/slider';
40import {MatSnackBarModule} from '@angular/material/snack-bar';
41import {MatToolbarModule} from '@angular/material/toolbar';
42import {MatTooltipModule} from '@angular/material/tooltip';
43import {Title} from '@angular/platform-browser';
44import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
45import {assertDefined} from 'common/assert_utils';
46import {FileUtils} from 'common/file_utils';
47import {
48  AppRefreshDumpsRequest,
49  ViewersLoaded,
50  ViewersUnloaded,
51} from 'messaging/winscope_event';
52import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
53import {TracesBuilder} from 'test/unit/traces_builder';
54import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
55import {AdbProxyComponent} from './adb_proxy_component';
56import {AppComponent} from './app_component';
57import {
58  MatDrawer,
59  MatDrawerContainer,
60  MatDrawerContent,
61} from './bottomnav/bottom_drawer_component';
62import {CollectTracesComponent} from './collect_traces_component';
63import {ShortcutsComponent} from './shortcuts_component';
64import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component';
65import {TimelineComponent} from './timeline/timeline_component';
66import {TraceConfigComponent} from './trace_config_component';
67import {TraceViewComponent} from './trace_view_component';
68import {UploadTracesComponent} from './upload_traces_component';
69import {WebAdbComponent} from './web_adb_component';
70
71describe('AppComponent', () => {
72  let fixture: ComponentFixture<AppComponent>;
73  let component: AppComponent;
74  let htmlElement: HTMLElement;
75
76  beforeEach(async () => {
77    await TestBed.configureTestingModule({
78      providers: [Title, {provide: ComponentFixtureAutoDetect, useValue: true}],
79      imports: [
80        CommonModule,
81        FormsModule,
82        MatCardModule,
83        MatButtonModule,
84        MatDividerModule,
85        MatFormFieldModule,
86        MatIconModule,
87        MatSelectModule,
88        MatSliderModule,
89        MatSnackBarModule,
90        MatToolbarModule,
91        MatTooltipModule,
92        ReactiveFormsModule,
93        MatInputModule,
94        BrowserAnimationsModule,
95        ClipboardModule,
96        MatDialogModule,
97        HttpClientModule,
98      ],
99      declarations: [
100        AdbProxyComponent,
101        AppComponent,
102        CollectTracesComponent,
103        MatDrawer,
104        MatDrawerContainer,
105        MatDrawerContent,
106        MiniTimelineComponent,
107        TimelineComponent,
108        TraceConfigComponent,
109        TraceViewComponent,
110        UploadTracesComponent,
111        ViewerSurfaceFlingerComponent,
112        WebAdbComponent,
113        ShortcutsComponent,
114      ],
115    })
116      .overrideComponent(AppComponent, {
117        set: {changeDetection: ChangeDetectionStrategy.Default},
118      })
119      .compileComponents();
120    fixture = TestBed.createComponent(AppComponent);
121    component = fixture.componentInstance;
122    htmlElement = fixture.nativeElement;
123    component.filenameFormControl = new FormControl(
124      'winscope',
125      Validators.compose([
126        Validators.required,
127        Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX),
128      ]),
129    );
130    fixture.detectChanges();
131  });
132
133  it('can be created', () => {
134    expect(component).toBeTruthy();
135  });
136
137  it('has the expected title', () => {
138    expect(component.title).toEqual('winscope');
139  });
140
141  it('shows permanent header items on homepage', () => {
142    checkPermanentHeaderItems();
143  });
144
145  it('displays correct elements when no data loaded', () => {
146    component.dataLoaded = false;
147    component.showDataLoadedElements = false;
148    fixture.detectChanges();
149    checkHomepage();
150  });
151
152  it('displays correct elements when data loaded', () => {
153    goToTraceView();
154    checkTraceViewPage();
155
156    spyOn(component, 'dumpsUploaded').and.returnValue(true);
157    fixture.detectChanges();
158    expect(htmlElement.querySelector('.refresh-dumps')).toBeTruthy();
159  });
160
161  it('returns to homepage on upload new button click', async () => {
162    goToTraceView();
163    checkTraceViewPage();
164
165    (htmlElement.querySelector('.upload-new') as HTMLButtonElement).click();
166    fixture.detectChanges();
167    await fixture.whenStable();
168    checkHomepage();
169  });
170
171  it('sends event on refresh dumps button click', async () => {
172    spyOn(component, 'dumpsUploaded').and.returnValue(true);
173    goToTraceView();
174    checkTraceViewPage();
175
176    const winscopeEventSpy = spyOn(
177      component.mediator,
178      'onWinscopeEvent',
179    ).and.callThrough();
180    (htmlElement.querySelector('.refresh-dumps') as HTMLButtonElement).click();
181    fixture.detectChanges();
182    await fixture.whenStable();
183    checkHomepage();
184    expect(winscopeEventSpy).toHaveBeenCalledWith(new AppRefreshDumpsRequest());
185  });
186
187  it('downloads traces on download button click', () => {
188    component.showDataLoadedElements = true;
189    fixture.detectChanges();
190    const spy = spyOn(component, 'downloadTraces');
191
192    clickDownloadTracesButton();
193    expect(spy).toHaveBeenCalledTimes(1);
194
195    clickDownloadTracesButton();
196    expect(spy).toHaveBeenCalledTimes(2);
197  });
198
199  it('downloads traces after valid file name change', () => {
200    component.showDataLoadedElements = true;
201    fixture.detectChanges();
202    const spy = spyOn(component, 'downloadTraces');
203
204    clickEditFilenameButton();
205    updateFilenameInputAndDownloadTraces('Winscope2', true);
206    expect(spy).toHaveBeenCalledTimes(1);
207
208    // check it works twice in a row
209    clickEditFilenameButton();
210    updateFilenameInputAndDownloadTraces('win_scope', true);
211    expect(spy).toHaveBeenCalledTimes(2);
212  });
213
214  it('changes page title based on archive name', async () => {
215    const pageTitle = TestBed.inject(Title);
216    component.timelineData.initialize(
217      new TracesBuilder().build(),
218      undefined,
219      TimestampConverterUtils.TIMESTAMP_CONVERTER,
220    );
221
222    await component.onWinscopeEvent(new ViewersUnloaded());
223    expect(pageTitle.getTitle()).toBe('Winscope');
224
225    component.tracePipeline.getDownloadArchiveFilename = jasmine
226      .createSpy()
227      .and.returnValue('test_archive');
228    await component.onWinscopeEvent(new ViewersLoaded([]));
229    fixture.detectChanges();
230    expect(pageTitle.getTitle()).toBe('Winscope | test_archive');
231  });
232
233  it('does not download traces if invalid file name chosen', () => {
234    component.showDataLoadedElements = true;
235    fixture.detectChanges();
236    const spy = spyOn(component, 'downloadTraces');
237
238    clickEditFilenameButton();
239    updateFilenameInputAndDownloadTraces('w?n$cope', false);
240    expect(spy).not.toHaveBeenCalled();
241  });
242
243  it('behaves as expected when entering valid then invalid then valid file names', () => {
244    component.showDataLoadedElements = true;
245    fixture.detectChanges();
246
247    const spy = spyOn(component, 'downloadTraces');
248
249    clickEditFilenameButton();
250    updateFilenameInputAndDownloadTraces('Winscope2', true);
251    expect(spy).toHaveBeenCalled();
252
253    clickEditFilenameButton();
254    updateFilenameInputAndDownloadTraces('w?n$cope', false);
255    expect(spy).toHaveBeenCalledTimes(1);
256
257    updateFilenameInputAndDownloadTraces('win.scope', true);
258    expect(spy).toHaveBeenCalledTimes(2);
259  });
260
261  it('validates filename on enter key', () => {
262    const spy = spyOn(component, 'onCheckIconClick');
263
264    component.showDataLoadedElements = true;
265    fixture.detectChanges();
266
267    clickEditFilenameButton();
268
269    const inputField = assertDefined(
270      htmlElement.querySelector('.file-name-input-field'),
271    );
272    const inputEl = assertDefined(
273      htmlElement.querySelector('.file-name-input-field input'),
274    );
275    (inputEl as HTMLInputElement).value = 'valid_file_name';
276    inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
277
278    fixture.detectChanges();
279    expect(spy).toHaveBeenCalled();
280  });
281
282  it('opens shortcuts dialog', () => {
283    expect(document.querySelector('shortcuts-panel')).toBeFalsy();
284    const shortcutsButton = assertDefined(
285      htmlElement.querySelector('.shortcuts'),
286    ) as HTMLElement;
287    shortcutsButton.click();
288    fixture.detectChanges();
289    expect(document.querySelector('shortcuts-panel')).toBeTruthy();
290  });
291
292  function goToTraceView() {
293    component.dataLoaded = true;
294    component.showDataLoadedElements = true;
295    component.timelineData.initialize(
296      new TracesBuilder().build(),
297      undefined,
298      TimestampConverterUtils.TIMESTAMP_CONVERTER,
299    );
300    fixture.detectChanges();
301  }
302
303  function updateFilenameInputAndDownloadTraces(name: string, valid: boolean) {
304    const inputEl = assertDefined(
305      htmlElement.querySelector('.file-name-input-field input'),
306    );
307    const checkButton = assertDefined(
308      htmlElement.querySelector('.check-button'),
309    );
310    (inputEl as HTMLInputElement).value = name;
311    inputEl.dispatchEvent(new Event('input'));
312    fixture.detectChanges();
313    checkButton.dispatchEvent(new Event('click'));
314    fixture.detectChanges();
315    if (valid) {
316      assertDefined(htmlElement.querySelector('.download-file-info'));
317      expect(
318        (htmlElement.querySelector('.save-button') as HTMLButtonElement)
319          .disabled,
320      ).toBeFalse();
321      clickDownloadTracesButton();
322    } else {
323      expect(htmlElement.querySelector('.download-file-info')).toBeFalsy();
324      expect(
325        (htmlElement.querySelector('.save-button') as HTMLButtonElement)
326          .disabled,
327      ).toBeTrue();
328    }
329  }
330
331  function clickDownloadTracesButton() {
332    const downloadButton = assertDefined(
333      htmlElement.querySelector('.save-button'),
334    );
335    downloadButton.dispatchEvent(new Event('click'));
336    fixture.detectChanges();
337  }
338
339  function clickEditFilenameButton() {
340    const pencilButton = assertDefined(
341      htmlElement.querySelector('.edit-button'),
342    );
343    pencilButton.dispatchEvent(new Event('click'));
344    fixture.detectChanges();
345  }
346
347  function checkHomepage() {
348    expect(htmlElement.querySelector('.welcome-info')).toBeTruthy();
349    expect(htmlElement.querySelector('.collect-traces-card')).toBeTruthy();
350    expect(htmlElement.querySelector('.upload-traces-card')).toBeTruthy();
351    expect(htmlElement.querySelector('.viewers')).toBeFalsy();
352    expect(htmlElement.querySelector('.upload-new')).toBeFalsy();
353    checkPermanentHeaderItems();
354  }
355
356  function checkTraceViewPage() {
357    expect(htmlElement.querySelector('.welcome-info')).toBeFalsy();
358    expect(htmlElement.querySelector('.save-button')).toBeTruthy();
359    expect(htmlElement.querySelector('.collect-traces-card')).toBeFalsy();
360    expect(htmlElement.querySelector('.upload-traces-card')).toBeFalsy();
361    expect(htmlElement.querySelector('.viewers')).toBeTruthy();
362    expect(htmlElement.querySelector('.upload-new')).toBeTruthy();
363    checkPermanentHeaderItems();
364  }
365
366  function checkPermanentHeaderItems() {
367    expect(htmlElement.querySelector('.app-title')).toBeTruthy();
368    expect(htmlElement.querySelector('.shortcuts')).toBeTruthy();
369    expect(htmlElement.querySelector('.documentation')).toBeTruthy();
370    expect(htmlElement.querySelector('.report-bug')).toBeTruthy();
371    expect(htmlElement.querySelector('.dark-mode')).toBeTruthy();
372  }
373});
374