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