• 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 {
18  CdkVirtualScrollViewport,
19  ScrollingModule,
20} from '@angular/cdk/scrolling';
21import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
22import {
23  ComponentFixture,
24  ComponentFixtureAutoDetect,
25  TestBed,
26} from '@angular/core/testing';
27import {FormsModule} from '@angular/forms';
28import {MatDividerModule} from '@angular/material/divider';
29import {MatFormFieldModule} from '@angular/material/form-field';
30import {MatInputModule} from '@angular/material/input';
31import {MatSelectModule} from '@angular/material/select';
32import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
33import {assertDefined} from 'common/assert_utils';
34import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
35import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
36import {UnitTestUtils} from 'test/unit/utils';
37import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
38import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils';
39import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
40import {ViewerEvents} from 'viewers/common/viewer_events';
41import {CollapsedSectionsComponent} from 'viewers/components/collapsed_sections_component';
42import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component';
43import {PropertiesComponent} from 'viewers/components/properties_component';
44import {SelectWithFilterComponent} from 'viewers/components/select_with_filter_component';
45import {TransactionsScrollDirective} from './scroll_strategy/transactions_scroll_directive';
46import {UiData, UiDataEntry} from './ui_data';
47import {ViewerTransactionsComponent} from './viewer_transactions_component';
48
49describe('ViewerTransactionsComponent', () => {
50  describe('Main component', () => {
51    let fixture: ComponentFixture<ViewerTransactionsComponent>;
52    let component: ViewerTransactionsComponent;
53    let htmlElement: HTMLElement;
54
55    beforeEach(async () => {
56      await TestBed.configureTestingModule({
57        providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
58        imports: [
59          ScrollingModule,
60          MatFormFieldModule,
61          FormsModule,
62          MatInputModule,
63          BrowserAnimationsModule,
64          MatSelectModule,
65          MatDividerModule,
66        ],
67        declarations: [
68          ViewerTransactionsComponent,
69          TransactionsScrollDirective,
70          SelectWithFilterComponent,
71          CollapsedSectionsComponent,
72          CollapsibleSectionTitleComponent,
73          PropertiesComponent,
74        ],
75        schemas: [CUSTOM_ELEMENTS_SCHEMA],
76      }).compileComponents();
77
78      fixture = TestBed.createComponent(ViewerTransactionsComponent);
79      component = fixture.componentInstance;
80      htmlElement = fixture.nativeElement;
81
82      component.inputData = makeUiData(0);
83      fixture.detectChanges();
84    });
85
86    it('can be created', () => {
87      expect(component).toBeTruthy();
88    });
89
90    it('renders filters', () => {
91      expect(htmlElement.querySelector('.entries .filters .pid')).toBeTruthy();
92      expect(htmlElement.querySelector('.entries .filters .uid')).toBeTruthy();
93      expect(htmlElement.querySelector('.entries .filters .type')).toBeTruthy();
94      expect(htmlElement.querySelector('.entries .filters .id')).toBeTruthy();
95    });
96
97    it('renders entries', () => {
98      expect(htmlElement.querySelector('.scroll')).toBeTruthy();
99
100      const entry = assertDefined(htmlElement.querySelector('.scroll .entry'));
101      expect(entry.innerHTML).toContain('1ns');
102      expect(entry.innerHTML).toContain('-111');
103      expect(entry.innerHTML).toContain('PID_VALUE');
104      expect(entry.innerHTML).toContain('UID_VALUE');
105      expect(entry.innerHTML).toContain('TYPE_VALUE');
106      expect(entry.innerHTML).toContain('ID_VALUE');
107      expect(entry.innerHTML).toContain('flag1 | flag2');
108    });
109
110    it('renders properties', () => {
111      expect(htmlElement.querySelector('.properties-view')).toBeTruthy();
112    });
113
114    it('applies transaction id filter correctly', async () => {
115      const allEntries = makeUiData(0).entries;
116      htmlElement.addEventListener(
117        ViewerEvents.TransactionIdFilterChanged,
118        (event) => {
119          if ((event as CustomEvent).detail.length === 0) {
120            component.uiData.entries = allEntries;
121            return;
122          }
123          component.uiData.entries = allEntries.filter((entry) =>
124            (event as CustomEvent).detail.includes(entry.transactionId),
125          );
126        },
127      );
128      await checkSelectFilter('.transaction-id');
129    });
130
131    it('applies vsync id filter correctly', async () => {
132      const allEntries = makeUiData(0).entries;
133      htmlElement.addEventListener(
134        ViewerEvents.VSyncIdFilterChanged,
135        (event) => {
136          if ((event as CustomEvent).detail.length === 0) {
137            component.uiData.entries = allEntries;
138            return;
139          }
140          component.uiData.entries = allEntries.filter((entry) => {
141            return (event as CustomEvent).detail.includes(`${entry.vsyncId}`);
142          });
143        },
144      );
145      await checkSelectFilter('.vsyncid');
146    });
147
148    it('applies pid filter correctly', async () => {
149      const allEntries = makeUiData(0).entries;
150      htmlElement.addEventListener(ViewerEvents.PidFilterChanged, (event) => {
151        if ((event as CustomEvent).detail.length === 0) {
152          component.uiData.entries = allEntries;
153          return;
154        }
155        component.uiData.entries = allEntries.filter((entry) => {
156          return (event as CustomEvent).detail.includes(entry.pid);
157        });
158      });
159      await checkSelectFilter('.pid');
160    });
161
162    it('applies uid filter correctly', async () => {
163      const allEntries = makeUiData(0).entries;
164      htmlElement.addEventListener(ViewerEvents.UidFilterChanged, (event) => {
165        if ((event as CustomEvent).detail.length === 0) {
166          component.uiData.entries = allEntries;
167          return;
168        }
169        component.uiData.entries = allEntries.filter((entry) => {
170          return (event as CustomEvent).detail.includes(entry.uid);
171        });
172      });
173      await checkSelectFilter('.uid');
174    });
175
176    it('applies type filter correctly', async () => {
177      const allEntries = makeUiData(0).entries;
178      htmlElement.addEventListener(ViewerEvents.TypeFilterChanged, (event) => {
179        if ((event as CustomEvent).detail.length === 0) {
180          component.uiData.entries = allEntries;
181          return;
182        }
183        component.uiData.entries = allEntries.filter((entry) => {
184          return (event as CustomEvent).detail.includes(entry.type);
185        });
186      });
187      await checkSelectFilter('.type');
188    });
189
190    it('applies layer/display id filter correctly', async () => {
191      const allEntries = makeUiData(0).entries;
192      htmlElement.addEventListener(
193        ViewerEvents.LayerIdFilterChanged,
194        (event) => {
195          if ((event as CustomEvent).detail.length === 0) {
196            component.uiData.entries = allEntries;
197            return;
198          }
199          component.uiData.entries = allEntries.filter((entry) => {
200            return (event as CustomEvent).detail.includes(
201              entry.layerOrDisplayId,
202            );
203          });
204        },
205      );
206      await checkSelectFilter('.layer-or-display-id');
207    });
208
209    it('applies what filter correctly', async () => {
210      const allEntries = makeUiData(0).entries;
211      htmlElement.addEventListener(ViewerEvents.WhatFilterChanged, (event) => {
212        if ((event as CustomEvent).detail.length === 0) {
213          component.uiData.entries = allEntries;
214          return;
215        }
216        component.uiData.entries = allEntries.filter((entry) => {
217          return (event as CustomEvent).detail.some((allowed: string) => {
218            return entry.what.includes(allowed);
219          });
220        });
221      });
222      await checkSelectFilter('.what');
223    });
224
225    it('scrolls to current entry on button click', () => {
226      const goToCurrentTimeButton = assertDefined(
227        htmlElement.querySelector('.go-to-current-time'),
228      ) as HTMLButtonElement;
229      const spy = spyOn(
230        assertDefined(component.scrollComponent),
231        'scrollToIndex',
232      );
233      goToCurrentTimeButton.click();
234      expect(spy).toHaveBeenCalledWith(1);
235    });
236
237    it('changes selected entry on arrow key press', () => {
238      htmlElement.addEventListener(
239        ViewerEvents.LogChangedByKeyPress,
240        (event) => {
241          component.inputData = makeUiData((event as CustomEvent).detail);
242          fixture.detectChanges();
243        },
244      );
245
246      // does not do anything if no prev entry available
247      component.handleKeyboardEvent(
248        new KeyboardEvent('keydown', {key: 'ArrowUp'}),
249      );
250      expect(component.uiData.selectedEntryIndex).toEqual(0);
251
252      component.handleKeyboardEvent(
253        new KeyboardEvent('keydown', {key: 'ArrowDown'}),
254      );
255      expect(component.uiData.selectedEntryIndex).toEqual(1);
256
257      component.handleKeyboardEvent(
258        new KeyboardEvent('keydown', {key: 'ArrowUp'}),
259      );
260      expect(component.uiData.selectedEntryIndex).toEqual(0);
261    });
262
263    it('propagates timestamp on click', () => {
264      component.inputData = makeUiData(0);
265      fixture.detectChanges();
266      let index: number | undefined;
267      htmlElement.addEventListener(ViewerEvents.TimestampClick, (event) => {
268        index = (event as CustomEvent).detail.index;
269      });
270      const logTimestampButton = assertDefined(
271        htmlElement.querySelector('.time button'),
272      ) as HTMLButtonElement;
273      logTimestampButton.click();
274
275      expect(index).toEqual(0);
276    });
277
278    it('creates collapsed sections with no buttons', () => {
279      UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
280    });
281
282    it('handles properties section collapse/expand', () => {
283      UnitTestUtils.checkSectionCollapseAndExpand(
284        htmlElement,
285        fixture,
286        '.properties-view',
287        'PROPERTIES - PROTO DUMP',
288      );
289    });
290
291    function makeUiData(selectedEntryIndex: number): UiData {
292      const propertiesTree = new PropertyTreeBuilder()
293        .setRootId('Transactions')
294        .setName('tree')
295        .setValue(null)
296        .build();
297
298      const time = new PropertyTreeBuilder()
299        .setRootId(propertiesTree.id)
300        .setName('timestamp')
301        .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n))
302        .setFormatter(TIMESTAMP_NODE_FORMATTER)
303        .build();
304
305      const entry = new UiDataEntry(
306        0,
307        time,
308        -111,
309        'PID_VALUE',
310        'UID_VALUE',
311        'TYPE_VALUE',
312        'LAYER_OR_DISPLAY_ID_VALUE',
313        'TRANSACTION_ID_VALUE',
314        'flag1 | flag2',
315        propertiesTree,
316      );
317
318      const entry2 = new UiDataEntry(
319        1,
320        time,
321        -222,
322        'PID_VALUE_2',
323        'UID_VALUE_2',
324        'TYPE_VALUE_2',
325        'LAYER_OR_DISPLAY_ID_VALUE_2',
326        'TRANSACTION_ID_VALUE_2',
327        'flag3 | flag4',
328        propertiesTree,
329      );
330
331      return new UiData(
332        ['-111', '-222'],
333        ['PID_VALUE', 'PID_VALUE_2'],
334        ['UID_VALUE', 'UID_VALUE_2'],
335        ['TYPE_VALUE', 'TYPE_VALUE_2'],
336        ['LAYER_OR_DISPLAY_ID_VALUE', 'LAYER_OR_DISPLAY_ID_VALUE_2'],
337        ['TRANSACTION_ID_VALUE', 'TRANSACTION_ID_VALUE_2'],
338        ['flag1', 'flag2', 'flag3', 'flag4'],
339        [entry, entry2],
340        1,
341        selectedEntryIndex,
342        0,
343        UiPropertyTreeNode.from(propertiesTree),
344        {},
345      );
346    }
347
348    async function checkSelectFilter(filterSelector: string) {
349      component.inputData = makeUiData(0);
350      fixture.detectChanges();
351      expect(component.uiData.entries.length).toEqual(2);
352      const filterTrigger = assertDefined(
353        htmlElement.querySelector(
354          `.filters ${filterSelector} .mat-select-trigger`,
355        ),
356      ) as HTMLInputElement;
357      filterTrigger.click();
358      await fixture.whenStable();
359
360      const firstOption = assertDefined(
361        document.querySelector('.mat-select-panel .mat-option'),
362      ) as HTMLElement;
363      firstOption.click();
364      fixture.detectChanges();
365      expect(component.uiData.entries.length).toEqual(1);
366
367      firstOption.click();
368      fixture.detectChanges();
369      expect(component.uiData.entries.length).toEqual(2);
370    }
371  });
372
373  describe('Scroll component', () => {
374    executeScrollComponentTests('entry', setUpTestEnvironment);
375
376    function makeUiDataForScroll(): UiData {
377      const propertiesTree = new PropertyTreeBuilder()
378        .setRootId('Transactions')
379        .setName('tree')
380        .setValue(null)
381        .build();
382
383      const time = new PropertyTreeBuilder()
384        .setRootId(propertiesTree.id)
385        .setName('timestamp')
386        .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n))
387        .setFormatter(TIMESTAMP_NODE_FORMATTER)
388        .build();
389
390      const uiData = new UiData(
391        [],
392        [],
393        [],
394        [],
395        [],
396        [],
397        [],
398        [],
399        0,
400        0,
401        0,
402        UiPropertyTreeNode.from(propertiesTree),
403        {},
404      );
405      const shortMessage = 'flag1 | flag2';
406      const longMessage = shortMessage.repeat(20);
407      for (let i = 0; i < 200; i++) {
408        const entry = new UiDataEntry(
409          0,
410          time,
411          -111,
412          'PID_VALUE',
413          'UID_VALUE',
414          'TYPE_VALUE',
415          'LAYER_OR_DISPLAY_ID_VALUE',
416          'TRANSACTION_ID_VALUE',
417          i % 2 === 0 ? shortMessage : longMessage,
418          propertiesTree,
419        );
420        uiData.entries.push(entry);
421      }
422      return uiData;
423    }
424
425    async function setUpTestEnvironment(): Promise<
426      [
427        ComponentFixture<ViewerTransactionsComponent>,
428        HTMLElement,
429        CdkVirtualScrollViewport,
430      ]
431    > {
432      await TestBed.configureTestingModule({
433        providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
434        imports: [ScrollingModule],
435        declarations: [
436          ViewerTransactionsComponent,
437          TransactionsScrollDirective,
438        ],
439        schemas: [CUSTOM_ELEMENTS_SCHEMA],
440      }).compileComponents();
441      const fixture = TestBed.createComponent(ViewerTransactionsComponent);
442      const transactionsComponent = fixture.componentInstance;
443      const htmlElement = fixture.nativeElement;
444      const viewport = assertDefined(transactionsComponent.scrollComponent);
445      transactionsComponent.inputData = makeUiDataForScroll();
446      return [fixture, htmlElement, viewport];
447    }
448  });
449});
450