xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/log_component_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 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 {ScrollingModule} from '@angular/cdk/scrolling';
18import {
19  ComponentFixture,
20  ComponentFixtureAutoDetect,
21  TestBed,
22} from '@angular/core/testing';
23import {FormsModule} from '@angular/forms';
24import {MatButtonModule} from '@angular/material/button';
25import {MatDividerModule} from '@angular/material/divider';
26import {MatFormFieldModule} from '@angular/material/form-field';
27import {MatIconModule} from '@angular/material/icon';
28import {MatInputModule} from '@angular/material/input';
29import {MatSelectModule} from '@angular/material/select';
30import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
31import {assertDefined} from 'common/assert_utils';
32import {Timestamp} from 'common/time';
33import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
34import {TraceBuilder} from 'test/unit/trace_builder';
35import {TraceEntry} from 'trace/trace';
36import {TraceType} from 'trace/trace_type';
37import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
38import {LogSelectFilter, LogTextFilter} from 'viewers/common/log_filters';
39import {TextFilter} from 'viewers/common/text_filter';
40import {
41  ColumnSpec,
42  LogEntry,
43  LogField,
44  LogHeader,
45} from 'viewers/common/ui_data_log';
46import {
47  LogFilterChangeDetail,
48  LogTextFilterChangeDetail,
49  TimestampClickDetail,
50  ViewerEvents,
51} from 'viewers/common/viewer_events';
52import {CollapsedSectionsComponent} from 'viewers/components/collapsed_sections_component';
53import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component';
54import {PropertiesComponent} from 'viewers/components/properties_component';
55import {SearchBoxComponent} from 'viewers/components/search_box_component';
56import {SelectWithFilterComponent} from 'viewers/components/select_with_filter_component';
57import {LogComponent} from './log_component';
58
59describe('LogComponent', () => {
60  const testColumn1: ColumnSpec = {name: 'test1', cssClass: 'test-1'};
61  const testColumn2: ColumnSpec = {name: 'test2', cssClass: 'test-2'};
62  const testColumn3: ColumnSpec = {name: 'test3', cssClass: 'test-3'};
63
64  let fixture: ComponentFixture<LogComponent>;
65  let component: LogComponent;
66  let htmlElement: HTMLElement;
67
68  beforeEach(async () => {
69    await TestBed.configureTestingModule({
70      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
71      imports: [
72        ScrollingModule,
73        MatFormFieldModule,
74        FormsModule,
75        MatInputModule,
76        BrowserAnimationsModule,
77        MatSelectModule,
78        MatDividerModule,
79        MatButtonModule,
80        MatIconModule,
81      ],
82      declarations: [
83        LogComponent,
84        SelectWithFilterComponent,
85        CollapsedSectionsComponent,
86        CollapsibleSectionTitleComponent,
87        PropertiesComponent,
88        SearchBoxComponent,
89      ],
90    }).compileComponents();
91
92    fixture = TestBed.createComponent(LogComponent);
93    component = fixture.componentInstance;
94    htmlElement = fixture.nativeElement;
95    setComponentInputData();
96    fixture.detectChanges();
97  });
98
99  it('can be created', () => {
100    expect(component).toBeTruthy();
101  });
102
103  it('renders filters', () => {
104    const filtersInTable = htmlElement.querySelectorAll('.entries .filter');
105    expect(filtersInTable.length).toEqual(2);
106    const filtersInTitle = htmlElement.querySelectorAll(
107      '.title-section .filter',
108    );
109    expect(filtersInTitle.length).toEqual(0);
110  });
111
112  it('renders filters in title', () => {
113    component.title = 'Test';
114    component.showFiltersInTitle = true;
115    fixture.detectChanges();
116    const filtersInTable = htmlElement.querySelectorAll('.entries .filter');
117    expect(filtersInTable.length).toEqual(0);
118    const filtersInTitle = htmlElement.querySelectorAll(
119      '.title-section .filter',
120    );
121    expect(filtersInTitle.length).toEqual(2);
122  });
123
124  it('renders entries', () => {
125    expect(htmlElement.querySelector('.scroll')).toBeTruthy();
126
127    const entryText = assertDefined(
128      htmlElement.querySelector('.scroll .entry'),
129    ).textContent;
130    expect(entryText).toContain('Test tag');
131    expect(entryText).toContain('123');
132    expect(entryText).toContain('2ns');
133  });
134
135  it('scrolls to current entry on button click', () => {
136    component.currentIndex = 1;
137    fixture.detectChanges();
138    const goToCurrentTimeButton = assertDefined(
139      htmlElement.querySelector<HTMLElement>('.go-to-current-time'),
140    );
141    const spy = spyOn(
142      assertDefined(component.scrollComponent),
143      'scrollToIndex',
144    );
145    goToCurrentTimeButton.click();
146    expect(spy).toHaveBeenCalledWith(1);
147  });
148
149  it('applies select filter correctly', async () => {
150    const allEntries = component.entries.slice();
151    htmlElement.addEventListener(ViewerEvents.LogFilterChange, (event) => {
152      const detail: LogFilterChangeDetail = (event as CustomEvent).detail;
153      if (detail.value.length === 0) {
154        component.entries = allEntries;
155        return;
156      }
157      component.entries = allEntries.filter((entry) => {
158        const entryValue = assertDefined(
159          entry.fields.find((f) => f.spec === detail.header.spec),
160        ).value.toString();
161        if (Array.isArray(detail.value)) {
162          return detail.value.includes(entryValue);
163        }
164        return entryValue.includes(detail.value);
165      });
166    });
167    expect(htmlElement.querySelectorAll('.entry').length).toEqual(2);
168    const filterTrigger = assertDefined(
169      htmlElement.querySelector<HTMLElement>('.headers .mat-select-trigger'),
170    );
171    filterTrigger.click();
172    await fixture.whenStable();
173
174    const firstOption = assertDefined(
175      document.querySelector<HTMLElement>('.mat-select-panel .mat-option'),
176    );
177    firstOption.click();
178    fixture.detectChanges();
179    expect(htmlElement.querySelectorAll('.entry').length).toEqual(1);
180
181    firstOption.click();
182    fixture.detectChanges();
183    expect(htmlElement.querySelectorAll('.entry').length).toEqual(2);
184  });
185
186  it('applies text filter correctly', async () => {
187    const allEntries = component.entries.slice();
188    htmlElement.addEventListener(ViewerEvents.LogTextFilterChange, (event) => {
189      const detail: LogTextFilterChangeDetail = (event as CustomEvent).detail;
190      if (detail.filter.filterString.length === 0) {
191        component.entries = allEntries;
192        return;
193      }
194      component.entries = allEntries.filter((entry) => {
195        const entryValue = assertDefined(
196          entry.fields.find((f) => f.spec === detail.header.spec),
197        ).value.toString();
198        return entryValue.includes(detail.filter.filterString);
199      });
200    });
201    expect(htmlElement.querySelectorAll('.entry').length).toEqual(2);
202
203    const inputEl = assertDefined(
204      htmlElement.querySelector<HTMLInputElement>('.headers input'),
205    );
206
207    inputEl.value = '123';
208    inputEl.dispatchEvent(new Event('input'));
209    fixture.detectChanges();
210    expect(htmlElement.querySelectorAll('.entry').length).toEqual(2);
211
212    inputEl.value = '1234';
213    inputEl.dispatchEvent(new Event('input'));
214    fixture.detectChanges();
215    expect(htmlElement.querySelectorAll('.entry').length).toEqual(1);
216
217    inputEl.value = '12345';
218    inputEl.dispatchEvent(new Event('input'));
219    fixture.detectChanges();
220    expect(htmlElement.querySelectorAll('.entry').length).toEqual(0);
221
222    inputEl.value = '';
223    inputEl.dispatchEvent(new Event('input'));
224    fixture.detectChanges();
225    expect(htmlElement.querySelectorAll('.entry').length).toEqual(2);
226  });
227
228  it('emits event on arrow key press', () => {
229    let downArrowPressedTimes = 0;
230    htmlElement.addEventListener(ViewerEvents.ArrowDownPress, (event) => {
231      downArrowPressedTimes++;
232    });
233    let upArrowPressedTimes = 0;
234    htmlElement.addEventListener(ViewerEvents.ArrowUpPress, (event) => {
235      upArrowPressedTimes++;
236    });
237
238    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
239    expect(upArrowPressedTimes).toEqual(1);
240
241    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
242    expect(downArrowPressedTimes).toEqual(1);
243
244    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
245    expect(upArrowPressedTimes).toEqual(2);
246
247    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
248    expect(downArrowPressedTimes).toEqual(2);
249  });
250
251  it('propagates entry on trace entry timestamp click', () => {
252    const logTimestampButton = assertDefined(
253      htmlElement.querySelectorAll<HTMLElement>('.time-button').item(1),
254    );
255    checkEntryPropagatedOnTimestampClick(logTimestampButton);
256  });
257
258  it('propagates entry on timestamp click with propagateEntryTimestamp set', () => {
259    const logTimestampButton = assertDefined(
260      htmlElement
261        .querySelectorAll<HTMLElement>(`.${testColumn3.cssClass} button`)
262        .item(1),
263    );
264    checkEntryPropagatedOnTimestampClick(logTimestampButton);
265  });
266
267  it('propagates timestamp on raw timestamp click', () => {
268    let timestamp: Timestamp | undefined;
269    htmlElement.addEventListener(ViewerEvents.TimestampClick, (event) => {
270      const detail: TimestampClickDetail = (event as CustomEvent).detail;
271      timestamp = detail.timestamp;
272    });
273    const logTimestampButton = assertDefined(
274      htmlElement.querySelector<HTMLElement>(`.${testColumn3.cssClass} button`),
275    );
276    logTimestampButton.click();
277
278    expect(timestamp).toBeDefined();
279  });
280
281  it('changes css class on entry click and does not scroll', () => {
282    htmlElement.addEventListener(ViewerEvents.LogEntryClick, (event) => {
283      const index = (event as CustomEvent).detail;
284      component.selectedIndex = index;
285      fixture.detectChanges();
286    });
287
288    const entry = assertDefined(
289      htmlElement.querySelector<HTMLElement>('.entry[item-id="1"]'),
290    );
291    expect(entry.className).not.toContain('selected');
292    const spy = spyOn(
293      assertDefined(component.scrollComponent),
294      'scrollToIndex',
295    );
296    entry.click();
297    expect(spy).not.toHaveBeenCalled();
298    expect(entry.className).toContain('selected');
299  });
300
301  it('shows placeholder text', () => {
302    expect(htmlElement.querySelector('.placeholder-text')).toBeNull();
303    component.entries = [];
304    fixture.detectChanges();
305    expect(htmlElement.querySelector('.placeholder-text')).toBeTruthy();
306  });
307
308  it('formats timestamp without date unless multiple dates present', () => {
309    const entry = assertDefined(htmlElement.querySelector('.scroll .entry'));
310    expect(entry.textContent?.trim()).toEqual('1ns Test tag 1123 2ns');
311
312    const spy = spyOn(component, 'areMultipleDatesPresent').and.returnValue(
313      true,
314    );
315    fixture.detectChanges();
316    expect(entry.textContent?.trim()).toEqual('1ns Test tag 1123 2ns');
317
318    setComponentInputData(false);
319    fixture.detectChanges();
320    expect(entry.textContent?.trim()).toEqual(
321      '1970-01-01, 00:00:00.000 Test tag 21234 1970-01-01, 00:00:00.000',
322    );
323
324    spy.and.returnValue(false);
325    fixture.detectChanges();
326    expect(entry.textContent?.trim()).toEqual(
327      '00:00:00.000 Test tag 21234 00:00:00.000',
328    );
329  });
330
331  function setComponentInputData(elapsed = true) {
332    let entryTime: Timestamp;
333    let fieldTime: Timestamp;
334    if (elapsed) {
335      entryTime = TimestampConverterUtils.makeElapsedTimestamp(1n);
336      fieldTime = TimestampConverterUtils.makeElapsedTimestamp(2n);
337    } else {
338      entryTime = TimestampConverterUtils.makeRealTimestamp(1n);
339      fieldTime = TimestampConverterUtils.makeRealTimestamp(2n);
340    }
341
342    const fields1: LogField[] = [
343      {spec: testColumn1, value: 'Test tag 1'},
344      {spec: testColumn2, value: 123},
345      {spec: testColumn3, value: fieldTime},
346    ];
347    const fields2 = [
348      {spec: testColumn1, value: 'Test tag 2'},
349      {spec: testColumn2, value: 1234},
350      {spec: testColumn3, value: fieldTime, propagateEntryTimestamp: true},
351    ];
352
353    const trace = new TraceBuilder<PropertyTreeNode>()
354      .setTimestamps([entryTime, entryTime])
355      .build();
356
357    const entry1: LogEntry = {
358      traceEntry: trace.getEntry(0),
359      fields: fields1,
360    };
361    const entry2: LogEntry = {
362      traceEntry: trace.getEntry(1),
363      fields: fields2,
364    };
365
366    const entries = [entry1, entry2];
367
368    const headers = [
369      new LogHeader(
370        testColumn1,
371        new LogSelectFilter(['Test tag 1', 'Test tag 2']),
372      ),
373      new LogHeader(testColumn2, new LogTextFilter(new TextFilter())),
374    ];
375
376    component.entries = entries;
377    component.headers = headers;
378    component.selectedIndex = 0;
379    component.traceType = TraceType.CUJS;
380  }
381
382  function checkEntryPropagatedOnTimestampClick(button: HTMLElement) {
383    let entry: TraceEntry<object> | undefined;
384    htmlElement.addEventListener(ViewerEvents.TimestampClick, (event) => {
385      const detail: TimestampClickDetail = (event as CustomEvent).detail;
386      entry = detail.entry;
387    });
388    button.click();
389    expect(entry).toBeDefined();
390  }
391});
392