• 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 */
16
17import {OverlayModule} from '@angular/cdk/overlay';
18import {CommonModule} from '@angular/common';
19import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core';
20import {ComponentFixture, TestBed} from '@angular/core/testing';
21import {ReactiveFormsModule} from '@angular/forms';
22import {MatButtonModule} from '@angular/material/button';
23import {MatCardModule} from '@angular/material/card';
24import {MatDividerModule} from '@angular/material/divider';
25import {MatFormFieldModule} from '@angular/material/form-field';
26import {MatIconModule} from '@angular/material/icon';
27import {MatInputModule} from '@angular/material/input';
28import {MatTabsModule} from '@angular/material/tabs';
29import {MatTooltipModule} from '@angular/material/tooltip';
30import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
31import {assertDefined} from 'common/assert_utils';
32import {InMemoryStorage} from 'common/store/in_memory_storage';
33import {TimestampConverterUtils} from 'common/time/test_utils';
34import {
35  FilterPresetApplyRequest,
36  FilterPresetSaveRequest,
37  TabbedViewSwitchRequest,
38  WinscopeEvent,
39  WinscopeEventType,
40} from 'messaging/winscope_event';
41import {TraceBuilder} from 'test/unit/trace_builder';
42import {UnitTestUtils} from 'test/unit/utils';
43import {TraceType} from 'trace/trace_type';
44import {Viewer, ViewType} from 'viewers/viewer';
45import {ViewerStub} from 'viewers/viewer_stub';
46import {TraceViewComponent} from './trace_view_component';
47
48describe('TraceViewComponent', () => {
49  const traceSf = UnitTestUtils.makeEmptyTrace(TraceType.SURFACE_FLINGER);
50  const traceWm = new TraceBuilder<object>()
51    .setType(TraceType.WINDOW_MANAGER)
52    .setEntries([{}])
53    .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()])
54    .setDescriptors(['file_1', 'file_1'])
55    .build();
56  const traceSr = UnitTestUtils.makeEmptyTrace(TraceType.SCREEN_RECORDING);
57  const traceProtolog = UnitTestUtils.makeEmptyTrace(TraceType.PROTO_LOG);
58
59  let fixture: ComponentFixture<TestHostComponent>;
60  let component: TestHostComponent;
61  let htmlElement: HTMLElement;
62
63  beforeEach(async () => {
64    await TestBed.configureTestingModule({
65      declarations: [TestHostComponent, TraceViewComponent],
66      imports: [
67        CommonModule,
68        MatCardModule,
69        MatDividerModule,
70        MatTabsModule,
71        MatTooltipModule,
72        OverlayModule,
73        MatButtonModule,
74        MatIconModule,
75        MatFormFieldModule,
76        BrowserAnimationsModule,
77        MatInputModule,
78        ReactiveFormsModule,
79      ],
80      schemas: [CUSTOM_ELEMENTS_SCHEMA],
81    }).compileComponents();
82    fixture = TestBed.createComponent(TestHostComponent);
83    htmlElement = fixture.nativeElement;
84    component = fixture.componentInstance;
85    component.viewers = [
86      new ViewerStub('Title0', 'Content0', traceSf, ViewType.TRACE_TAB),
87      new ViewerStub('Title1', 'Content1', traceWm, ViewType.TRACE_TAB),
88      new ViewerStub('Title2', 'Content2', traceSr, ViewType.OVERLAY),
89      new ViewerStub('Title3', 'Content3', traceProtolog, ViewType.TRACE_TAB),
90    ];
91    fixture.detectChanges();
92  });
93
94  it('can be created', () => {
95    expect(component).toBeTruthy();
96  });
97
98  it('creates viewer tabs', () => {
99    const tabs = htmlElement.querySelectorAll('.tab');
100    expect(tabs.length).toEqual(3);
101    expect(tabs.item(0).textContent).toContain('Title0');
102    expect(tabs.item(1).textContent).toContain('Title1 Dump');
103  });
104
105  it('creates viewer overlay', () => {
106    const overlayContainer = assertDefined(
107      htmlElement.querySelector('.overlay-container'),
108    );
109    expect(overlayContainer.textContent).toContain('Content2');
110  });
111
112  it('throws error if more than one overlay present', () => {
113    expect(() => {
114      component.viewers = [
115        new ViewerStub('Title0', 'Content0', traceSf, ViewType.TRACE_TAB),
116        new ViewerStub('Title1', 'Content1', traceWm, ViewType.OVERLAY),
117        new ViewerStub('Title2', 'Content2', traceSr, ViewType.OVERLAY),
118      ];
119      fixture.detectChanges();
120    }).toThrowError();
121  });
122
123  it('switches view on click', () => {
124    const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab');
125
126    // Initially tab 0
127    fixture.detectChanges();
128    let visibleTabContents = getVisibleTabContents();
129    expect(visibleTabContents.length).toEqual(1);
130    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
131
132    // Switch to tab 1
133    tabButtons.item(1).click();
134    fixture.detectChanges();
135    visibleTabContents = getVisibleTabContents();
136    expect(visibleTabContents.length).toEqual(1);
137    expect(visibleTabContents[0].innerHTML).toEqual('Content1');
138
139    // Switch to tab 0
140    tabButtons.item(0).click();
141    fixture.detectChanges();
142    visibleTabContents = getVisibleTabContents();
143    expect(visibleTabContents.length).toEqual(1);
144    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
145  });
146
147  it("emits 'view switched' events", () => {
148    const traceViewComponent = assertDefined(component.traceViewComponent);
149    const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab');
150
151    const emitAppEvent = jasmine.createSpy();
152    traceViewComponent.setEmitEvent(emitAppEvent);
153
154    expect(emitAppEvent).not.toHaveBeenCalled();
155
156    tabButtons.item(1).click();
157    expect(emitAppEvent).toHaveBeenCalledTimes(1);
158    expect(emitAppEvent).toHaveBeenCalledWith(
159      jasmine.objectContaining({
160        type: WinscopeEventType.TABBED_VIEW_SWITCHED,
161      } as WinscopeEvent),
162    );
163
164    tabButtons.item(0).click();
165    expect(emitAppEvent).toHaveBeenCalledTimes(2);
166    expect(emitAppEvent).toHaveBeenCalledWith(
167      jasmine.objectContaining({
168        type: WinscopeEventType.TABBED_VIEW_SWITCHED,
169      } as WinscopeEvent),
170    );
171  });
172
173  it("handles 'view switch' requests", async () => {
174    const traceViewComponent = assertDefined(component.traceViewComponent);
175
176    // Initially tab 0
177    let visibleTabContents = getVisibleTabContents();
178    expect(visibleTabContents.length).toEqual(1);
179    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
180
181    // Switch to tab 1
182    await traceViewComponent.onWinscopeEvent(
183      new TabbedViewSwitchRequest(traceWm),
184    );
185    fixture.detectChanges();
186    visibleTabContents = getVisibleTabContents();
187    expect(visibleTabContents.length).toEqual(1);
188    expect(visibleTabContents[0].innerHTML).toEqual('Content1');
189
190    // Switch to tab 0
191    await traceViewComponent.onWinscopeEvent(
192      new TabbedViewSwitchRequest(traceSf),
193    );
194    fixture.detectChanges();
195    visibleTabContents = getVisibleTabContents();
196    expect(visibleTabContents.length).toEqual(1);
197    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
198  });
199
200  it('emits TabbedViewSwitched event on viewer changes', () => {
201    const traceViewComponent = assertDefined(component.traceViewComponent);
202    const emitAppEvent = jasmine.createSpy();
203    traceViewComponent.setEmitEvent(emitAppEvent);
204
205    expect(emitAppEvent).not.toHaveBeenCalled();
206
207    component.viewers = [new ViewerStub('Title1', 'Content1', traceWm)];
208    fixture.detectChanges();
209
210    expect(emitAppEvent).toHaveBeenCalledTimes(1);
211    expect(emitAppEvent).toHaveBeenCalledWith(
212      jasmine.objectContaining({
213        type: WinscopeEventType.TABBED_VIEW_SWITCHED,
214      } as WinscopeEvent),
215    );
216  });
217
218  it('disables filter presets button for viewers without presets', () => {
219    const filterPresets = assertDefined(
220      htmlElement.querySelector<HTMLButtonElement>('.filter-presets'),
221    );
222    expect(filterPresets.textContent).toContain('Filter Presets');
223    expect(filterPresets.disabled).toBeFalse();
224    const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab');
225    tabButtons.item(2).click();
226    fixture.detectChanges();
227    expect(filterPresets.disabled).toBeTrue();
228  });
229
230  it('saves preset by button', () => {
231    const emitAppEvent = jasmine.createSpy();
232    component.traceViewComponent?.setEmitEvent(emitAppEvent);
233    openFilterPresets();
234
235    const overlayPanel = assertDefined(
236      document.querySelector('.overlay-panel'),
237    );
238    const existingPresets = assertDefined(
239      overlayPanel.querySelector('.existing-presets-section'),
240    );
241    expect(existingPresets.textContent).toContain('No existing presets found');
242
243    const saveButton = assertDefined(
244      overlayPanel.querySelector<HTMLButtonElement>('.save-field button'),
245    );
246    expect(saveButton.disabled).toBeTrue();
247
248    const inputEl = assertDefined(
249      overlayPanel.querySelector<HTMLInputElement>('.save-field input'),
250    );
251    updateInputField(inputEl, 'Test Preset');
252    saveButton.click();
253    fixture.detectChanges();
254
255    expect(emitAppEvent).toHaveBeenCalledWith(
256      new FilterPresetSaveRequest(
257        'Test Preset.Surface Flinger',
258        TraceType.SURFACE_FLINGER,
259      ),
260    );
261    expect(existingPresets.textContent).toContain('Test Preset');
262    expect(inputEl.value).toEqual('');
263    expect(saveButton.disabled).toBeTrue();
264  });
265
266  it('saves preset by keydown', () => {
267    const emitAppEvent = jasmine.createSpy();
268    component.traceViewComponent?.setEmitEvent(emitAppEvent);
269    openFilterPresets();
270
271    const overlayPanel = assertDefined(
272      document.querySelector('.overlay-panel'),
273    );
274
275    const inputEl = assertDefined(
276      overlayPanel.querySelector<HTMLInputElement>('.save-field input'),
277    );
278    inputEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
279    fixture.detectChanges();
280    expect(emitAppEvent).not.toHaveBeenCalled();
281
282    updateInputField(inputEl, 'Test Preset');
283    inputEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
284    fixture.detectChanges();
285
286    expect(emitAppEvent).toHaveBeenCalledWith(
287      new FilterPresetSaveRequest(
288        'Test Preset.Surface Flinger',
289        TraceType.SURFACE_FLINGER,
290      ),
291    );
292  });
293
294  it('saves preset between sessions', () => {
295    savePresetByButton('Test Preset');
296
297    component.showSecondComponent = true;
298    fixture.detectChanges();
299
300    openFilterPresets();
301    const existingPresets = assertDefined(
302      document.querySelector('.overlay-panel .existing-presets-section'),
303    );
304    expect(existingPresets.textContent).toContain('Test Preset');
305  });
306
307  it('deletes preset', () => {
308    savePresetByButton('Test Preset');
309    const saveButton = assertDefined(
310      document.querySelector<HTMLButtonElement>('.save-field button'),
311    );
312    updateInputField(
313      assertDefined(
314        document.querySelector<HTMLInputElement>('.save-field input'),
315      ),
316      'Test Preset',
317    );
318    expect(saveButton.disabled).toBeTrue();
319
320    assertDefined(
321      document.querySelector<HTMLElement>('.delete-button'),
322    ).click();
323    fixture.detectChanges();
324    expect(
325      document.querySelector<HTMLElement>('.existing-presets-section')
326        ?.textContent,
327    ).toContain('No existing presets found');
328    expect(saveButton.disabled).toBeFalse();
329  });
330
331  it('does not show presets for different trace', () => {
332    savePresetByButton('Test Preset');
333    closeFilterPresets();
334
335    const tabs = htmlElement.querySelectorAll<HTMLElement>('.tab');
336    tabs.item(1).click();
337    fixture.detectChanges();
338
339    openFilterPresets();
340    const existingPresets = assertDefined(
341      document.querySelector('.overlay-panel'),
342    );
343    expect(existingPresets.textContent).toContain('No existing presets found');
344  });
345
346  it('emits apply preset request', () => {
347    const emitAppEvent = jasmine.createSpy();
348    component.traceViewComponent?.setEmitEvent(emitAppEvent);
349    savePresetByButton('Test Preset');
350
351    const preset = assertDefined(
352      document.querySelector<HTMLElement>(
353        '.overlay-panel .existing-preset button',
354      ),
355    );
356    preset.click();
357    fixture.detectChanges();
358
359    expect(emitAppEvent).toHaveBeenCalledWith(
360      new FilterPresetApplyRequest(
361        'Test Preset.Surface Flinger',
362        TraceType.SURFACE_FLINGER,
363      ),
364    );
365  });
366
367  it('does not show global tab first', () => {
368    component.viewers = [
369      new ViewerStub('Title0', 'Content0', undefined, ViewType.GLOBAL_SEARCH),
370      new ViewerStub('Title1', 'Content1', traceWm, ViewType.TRACE_TAB),
371    ];
372    fixture.detectChanges();
373    const visibleTabContents = getVisibleTabContents();
374    expect(visibleTabContents.length).toEqual(1);
375    expect(visibleTabContents[0].innerHTML).toEqual('Content1');
376  });
377
378  it('shows tooltips for tabs with trace descriptors', async () => {
379    const tabs = htmlElement.querySelectorAll('.tab');
380    const wmTab = tabs.item(1);
381    await UnitTestUtils.checkTooltips([wmTab], ['file_1'], fixture);
382  });
383
384  function getVisibleTabContents() {
385    const contents: HTMLElement[] = [];
386    htmlElement
387      .querySelectorAll<HTMLElement>('.trace-view-content div')
388      .forEach((content) => {
389        if (content.style.display !== 'none') {
390          contents.push(content);
391        }
392      });
393    return contents;
394  }
395
396  function savePresetByButton(presetName: string) {
397    openFilterPresets();
398    const overlayPanel = assertDefined(
399      document.querySelector('.overlay-panel'),
400    );
401    const saveButton = assertDefined(
402      overlayPanel.querySelector<HTMLButtonElement>('.save-field button'),
403    );
404
405    const inputEl = assertDefined(
406      overlayPanel.querySelector<HTMLInputElement>('.save-field input'),
407    );
408    updateInputField(inputEl, presetName);
409    saveButton.click();
410    fixture.detectChanges();
411  }
412
413  function openFilterPresets() {
414    const filterPresets = assertDefined(
415      htmlElement.querySelector<HTMLButtonElement>('.filter-presets'),
416    );
417    filterPresets.click();
418    fixture.detectChanges();
419  }
420
421  function closeFilterPresets() {
422    assertDefined(
423      document.querySelector<HTMLElement>('.cdk-overlay-backdrop'),
424    ).click();
425    fixture.detectChanges();
426  }
427
428  function updateInputField(inputEl: HTMLInputElement, value: string) {
429    inputEl.value = value;
430    inputEl.dispatchEvent(new Event('input'));
431    fixture.detectChanges();
432  }
433
434  @Component({
435    selector: 'host-component',
436    template: `
437      <trace-view
438        *ngIf="!showSecondComponent"
439        [viewers]="viewers"
440        [store]="store"></trace-view>
441
442      <trace-view
443        *ngIf="showSecondComponent"
444        [viewers]="viewers"
445        [store]="store"></trace-view>
446    `,
447  })
448  class TestHostComponent {
449    viewers: Viewer[] = [];
450    store = new InMemoryStorage();
451    showSecondComponent = false;
452
453    @ViewChild(TraceViewComponent)
454    traceViewComponent: TraceViewComponent | undefined;
455  }
456});
457