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 {FilesSource} from 'app/files_source';
24import {TracePipeline} from 'app/trace_pipeline';
25import {assertDefined} from 'common/assert_utils';
26import {UserNotificationsListenerStub} from 'messaging/user_notifications_listener_stub';
27import {UnitTestUtils} from 'test/unit/utils';
28import {LoadProgressComponent} from './load_progress_component';
29import {UploadTracesComponent} from './upload_traces_component';
30
31describe('UploadTracesComponent', () => {
32  let fixture: ComponentFixture<UploadTracesComponent>;
33  let component: UploadTracesComponent;
34  let htmlElement: HTMLElement;
35  let validSfFile: File;
36  let validWmFile: File;
37
38  beforeEach(async () => {
39    await TestBed.configureTestingModule({
40      imports: [
41        MatCardModule,
42        MatSnackBarModule,
43        MatListModule,
44        MatIconModule,
45        MatProgressBarModule,
46        MatTooltipModule,
47      ],
48      providers: [MatSnackBar],
49      declarations: [UploadTracesComponent, LoadProgressComponent],
50    }).compileComponents();
51    fixture = TestBed.createComponent(UploadTracesComponent);
52    component = fixture.componentInstance;
53    htmlElement = fixture.nativeElement;
54    component.tracePipeline = new TracePipeline();
55    validSfFile = await UnitTestUtils.getFixtureFile(
56      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
57    );
58    validWmFile = await UnitTestUtils.getFixtureFile(
59      'traces/elapsed_and_real_timestamp/WindowManager.pb',
60    );
61    fixture.detectChanges();
62  });
63
64  it('can be created', () => {
65    expect(component).toBeTruthy();
66  });
67
68  it('renders the expected card title', () => {
69    expect(htmlElement.querySelector('.title')?.innerHTML).toContain(
70      'Upload Traces',
71    );
72  });
73
74  it('handles file upload via drag and drop', () => {
75    const spy = spyOn(component.filesUploaded, 'emit');
76    const dropbox = assertDefined(htmlElement.querySelector('.drop-box'));
77
78    const dataTransfer = new DataTransfer();
79    dataTransfer.items.add(validSfFile);
80    dropbox?.dispatchEvent(new DragEvent('drop', {dataTransfer}));
81    fixture.detectChanges();
82    expect(spy).toHaveBeenCalledWith(Array.from(dataTransfer.files));
83  });
84
85  it('displays load progress bar', () => {
86    component.isLoadingFiles = true;
87    fixture.detectChanges();
88    assertDefined(htmlElement.querySelector('load-progress'));
89  });
90
91  it('can display uploaded traces', async () => {
92    await loadFiles([validSfFile]);
93    fixture.detectChanges();
94    assertDefined(htmlElement.querySelector('.uploaded-files'));
95    assertDefined(htmlElement.querySelector('.trace-actions-container'));
96  });
97
98  it('can remove one of two uploaded traces', async () => {
99    await loadFiles([validSfFile, validWmFile]);
100    fixture.detectChanges();
101    expect(component.tracePipeline.getTraces().getSize()).toBe(2);
102
103    const spy = spyOn(component, 'onOperationFinished');
104    const removeButton = assertDefined(
105      htmlElement.querySelector('.uploaded-files button'),
106    );
107    (removeButton as HTMLButtonElement).click();
108    fixture.detectChanges();
109    assertDefined(htmlElement.querySelector('.uploaded-files'));
110    expect(spy).toHaveBeenCalled();
111    expect(component.tracePipeline.getTraces().getSize()).toBe(1);
112  });
113
114  it('handles removal of the only uploaded trace', async () => {
115    await loadFiles([validSfFile]);
116    fixture.detectChanges();
117
118    const spy = spyOn(component, 'onOperationFinished');
119    const removeButton = assertDefined(
120      htmlElement.querySelector('.uploaded-files button'),
121    );
122    (removeButton as HTMLButtonElement).click();
123    fixture.detectChanges();
124    assertDefined(htmlElement.querySelector('.drop-info'));
125    expect(spy).toHaveBeenCalled();
126    expect(component.tracePipeline.getTraces().getSize()).toBe(0);
127  });
128
129  it('can remove all uploaded traces', async () => {
130    await loadFiles([validSfFile, validWmFile]);
131    fixture.detectChanges();
132    expect(component.tracePipeline.getTraces().getSize()).toBe(2);
133
134    const spy = spyOn(component, 'onOperationFinished');
135    const clearAllButton = assertDefined(
136      htmlElement.querySelector('.clear-all-btn'),
137    );
138    (clearAllButton as HTMLButtonElement).click();
139    fixture.detectChanges();
140    assertDefined(htmlElement.querySelector('.drop-info'));
141    expect(spy).toHaveBeenCalled();
142    expect(component.tracePipeline.getTraces().getSize()).toBe(0);
143  });
144
145  it('can triggers view traces event', async () => {
146    await loadFiles([validSfFile]);
147    fixture.detectChanges();
148
149    const spy = spyOn(component.viewTracesButtonClick, 'emit');
150    const viewTracesButton = assertDefined(
151      htmlElement.querySelector('.load-btn'),
152    );
153    (viewTracesButton as HTMLButtonElement).click();
154    fixture.detectChanges();
155    expect(spy).toHaveBeenCalled();
156  });
157
158  it('disables view traces button unless files with viewers uploaded', async () => {
159    const validEventlogFile = await UnitTestUtils.getFixtureFile(
160      'traces/eventlog.winscope',
161    );
162    await loadFiles([validEventlogFile]);
163    fixture.detectChanges();
164
165    const viewTracesButton = assertDefined(
166      htmlElement.querySelector('.load-btn'),
167    );
168    expect((viewTracesButton as HTMLButtonElement).disabled).toBeTrue();
169
170    await loadFiles([validSfFile]);
171    fixture.detectChanges();
172    expect((viewTracesButton as HTMLButtonElement).disabled).toBeFalse();
173  });
174
175  it('shows warning elements for traces without visualization', async () => {
176    const shellTransitionFile = await UnitTestUtils.getFixtureFile(
177      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
178    );
179    await loadFiles([shellTransitionFile]);
180    fixture.detectChanges();
181
182    expect(htmlElement.querySelector('.warning-icon')).toBeTruthy();
183    const viewTracesButton = assertDefined(
184      htmlElement.querySelector('.load-btn'),
185    );
186    expect((viewTracesButton as HTMLButtonElement).disabled).toBeTrue();
187  });
188
189  it('shows info elements for traces with upload info for the user', async () => {
190    const shellTransitionFile = await UnitTestUtils.getFixtureFile(
191      'traces/eventlog.winscope',
192    );
193    await loadFiles([shellTransitionFile]);
194    fixture.detectChanges();
195
196    expect(htmlElement.querySelector('.info-icon')).toBeTruthy();
197  });
198
199  async function loadFiles(files: File[]) {
200    component.tracePipeline.clear();
201    await component.tracePipeline.loadFiles(
202      files,
203      FilesSource.TEST,
204      new UserNotificationsListenerStub(),
205      undefined,
206    );
207  }
208});
209