• 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 {assertDefined} from 'common/assert_utils';
18import {InMemoryStorage} from 'common/store/in_memory_storage';
19import {TimestampConverterUtils} from 'common/time/test_utils';
20import {TimeUtils} from 'common/time/time_utils';
21import {
22  ActiveTraceChanged,
23  DarkModeToggled,
24  TracePositionUpdate,
25} from 'messaging/winscope_event';
26import {MockPresenter} from 'test/unit/mock_log_viewer_presenter';
27import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
28import {TraceBuilder} from 'test/unit/trace_builder';
29import {UnitTestUtils} from 'test/unit/utils';
30import {Trace} from 'trace/trace';
31import {TracePosition} from 'trace/trace_position';
32import {TraceType} from 'trace/trace_type';
33import {DEFAULT_PROPERTY_FORMATTER} from 'trace/tree_node/formatters';
34import {
35  PropertySource,
36  PropertyTreeNode,
37} from 'trace/tree_node/property_tree_node';
38import {TextFilter} from 'viewers/common/text_filter';
39import {LogSelectFilter, LogTextFilter} from './log_filters';
40import {LogHeader, UiDataLog} from './ui_data_log';
41import {UserOptions} from './user_options';
42import {
43  LogFilterChangeDetail,
44  LogTextFilterChangeDetail,
45  TimestampClickDetail,
46  ViewerEvents,
47} from './viewer_events';
48
49describe('AbstractLogViewerPresenter', () => {
50  let uiData: UiDataLog;
51  let presenter: MockPresenter;
52  let trace: Trace<PropertyTreeNode>;
53  let positionUpdate: TracePositionUpdate;
54  let secondPositionUpdate: TracePositionUpdate;
55  let lastEntryPositionUpdate: TracePositionUpdate;
56
57  beforeAll(async () => {
58    const timestamp1 = TimestampConverterUtils.makeElapsedTimestamp(1n);
59    const timestamp2 = TimestampConverterUtils.makeElapsedTimestamp(2n);
60    const timestamp3 = TimestampConverterUtils.makeElapsedTimestamp(3n);
61    const timestamp4 = TimestampConverterUtils.makeElapsedTimestamp(4n);
62    trace = new TraceBuilder<PropertyTreeNode>()
63      .setType(TraceType.TRANSACTIONS)
64      .setEntries([
65        new PropertyTreeBuilder()
66          .setRootId('Test Trace')
67          .setName('entry 1')
68          .setChildren([
69            {
70              name: 'pass1',
71              value: 'pass',
72              formatter: DEFAULT_PROPERTY_FORMATTER,
73            },
74            {
75              name: 'pass2',
76              value: 'fail',
77              formatter: DEFAULT_PROPERTY_FORMATTER,
78              source: PropertySource.DEFAULT,
79            },
80            {
81              name: 'fail1',
82              value: 'pass',
83              formatter: DEFAULT_PROPERTY_FORMATTER,
84            },
85            {
86              name: 'fail2',
87              value: 'fail',
88              formatter: DEFAULT_PROPERTY_FORMATTER,
89            },
90          ])
91          .build(),
92        new PropertyTreeBuilder()
93          .setRootId('Test Trace')
94          .setName('entry 2')
95          .build(),
96        new PropertyTreeBuilder()
97          .setRootId('Test Trace')
98          .setName('entry 3')
99          .build(),
100        new PropertyTreeBuilder()
101          .setRootId('Test Trace')
102          .setName('entry 4')
103          .build(),
104      ])
105      .setTimestamps([timestamp1, timestamp2, timestamp3, timestamp4])
106      .build();
107    positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
108    secondPositionUpdate = TracePositionUpdate.fromTraceEntry(
109      trace.getEntry(1),
110    );
111    lastEntryPositionUpdate = TracePositionUpdate.fromTraceEntry(
112      trace.getEntry(3),
113    );
114  });
115
116  beforeEach(() => {
117    presenter = new MockPresenter(trace, new InMemoryStorage(), (newData) => {
118      uiData = newData;
119    });
120  });
121
122  it('adds event listeners', async () => {
123    const element = makeElement();
124    presenter.addEventListeners(element);
125
126    const testHeader = new LogHeader(
127      {name: 'Test Column', cssClass: 'test-class'},
128      new LogSelectFilter([]),
129    );
130
131    let spy: jasmine.Spy = spyOn(presenter, 'onSelectFilterChange');
132    const filterDetail = new LogFilterChangeDetail(testHeader, ['']);
133    element.dispatchEvent(
134      new CustomEvent(ViewerEvents.LogFilterChange, {
135        detail: filterDetail,
136      }),
137    );
138    expect(spy).toHaveBeenCalledWith(testHeader, filterDetail.value);
139
140    spy = spyOn(presenter, 'onTextFilterChange');
141    const textFilterDetail = new LogTextFilterChangeDetail(
142      testHeader,
143      new TextFilter(),
144    );
145    element.dispatchEvent(
146      new CustomEvent(ViewerEvents.LogTextFilterChange, {
147        detail: textFilterDetail,
148      }),
149    );
150    expect(spy).toHaveBeenCalledWith(testHeader, textFilterDetail.filter);
151
152    spy = spyOn(presenter, 'onLogEntryClick');
153    element.dispatchEvent(
154      new CustomEvent(ViewerEvents.LogEntryClick, {
155        detail: 0,
156      }),
157    );
158    expect(spy).toHaveBeenCalledWith(0);
159
160    spy = spyOn(presenter, 'onArrowDownPress');
161    element.dispatchEvent(new CustomEvent(ViewerEvents.ArrowDownPress));
162    expect(spy).toHaveBeenCalled();
163
164    spy = spyOn(presenter, 'onArrowUpPress');
165    element.dispatchEvent(new CustomEvent(ViewerEvents.ArrowUpPress));
166    expect(spy).toHaveBeenCalled();
167
168    await sendPositionUpdate(positionUpdate, true);
169    spy = spyOn(presenter, 'onLogTimestampClick');
170    element.dispatchEvent(
171      new CustomEvent(ViewerEvents.TimestampClick, {
172        detail: new TimestampClickDetail(uiData.entries[0].traceEntry),
173      }),
174    );
175    expect(spy).toHaveBeenCalledWith(uiData.entries[0].traceEntry);
176
177    spy = spyOn(presenter, 'onRawTimestampClick');
178    const ts = TimestampConverterUtils.makeZeroTimestamp();
179    element.dispatchEvent(
180      new CustomEvent(ViewerEvents.TimestampClick, {
181        detail: new TimestampClickDetail(undefined, ts),
182      }),
183    );
184    expect(spy).toHaveBeenCalledWith(ts);
185
186    spy = spyOn(presenter, 'onPropertiesUserOptionsChange');
187    element.dispatchEvent(
188      new CustomEvent(ViewerEvents.PropertiesUserOptionsChange, {
189        detail: {userOptions: {}},
190      }),
191    );
192    expect(spy).toHaveBeenCalledWith({});
193
194    spy = spyOn(presenter, 'onPropertiesFilterChange');
195    const filter = new TextFilter();
196    element.dispatchEvent(
197      new CustomEvent(ViewerEvents.PropertiesFilterChange, {
198        detail: filter,
199      }),
200    );
201    expect(spy).toHaveBeenCalledWith(filter);
202
203    spy = spyOn(presenter, 'onPositionChangeByKeyPress');
204    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
205    pressRightArrowKey();
206    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
207    expect(spy).not.toHaveBeenCalled();
208
209    document.body.append(element);
210    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
211    pressRightArrowKey();
212    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'}));
213    expect(spy).toHaveBeenCalledTimes(2);
214  });
215
216  it('initializes entries and filters with options', async () => {
217    expect(uiData.scrollToIndex).toBeUndefined();
218    expect(uiData.currentIndex).toBeUndefined();
219    expect(uiData.selectedIndex).toBeUndefined();
220    expect(uiData.entries.length).toEqual(0);
221    expect(uiData.propertiesTree).toBeUndefined();
222    expect(uiData.headers).toEqual([]);
223
224    await sendPositionUpdate(positionUpdate, true);
225
226    expect(uiData.scrollToIndex).toBeDefined();
227    expect(uiData.currentIndex).toBeDefined();
228    expect(uiData.selectedIndex).toBeUndefined();
229    expect(uiData.entries.length).toEqual(4);
230    expect(assertDefined(uiData.propertiesTree).id).toEqual(
231      assertDefined(uiData.entries[0].propertiesTree).id,
232    );
233    expect(uiData.headers.length).toEqual(3);
234    expect((uiData.headers[0].filter as LogSelectFilter).options).toEqual([
235      'stringValue',
236      'differentValue',
237    ]);
238  });
239
240  it('processes trace position update and updates ui data', async () => {
241    await sendPositionUpdate(secondPositionUpdate, true);
242    expect(uiData.currentIndex).toEqual(1);
243    expect(assertDefined(uiData.propertiesTree).id).toEqual(
244      assertDefined(uiData.entries[1].propertiesTree).id,
245    );
246  });
247
248  it('allows arrow keydown event to propagate if presenter trace not active or current index not defined', async () => {
249    const element = makeElement();
250    document.body.append(element);
251    presenter.addEventListeners(element);
252    const listenerSpy = jasmine.createSpy();
253    document.addEventListener('keydown', listenerSpy);
254
255    await sendPositionUpdate(
256      new TracePositionUpdate(
257        TracePosition.fromTimestamp(
258          TimestampConverterUtils.makeElapsedTimestamp(-1n),
259        ),
260      ),
261      true,
262    );
263    expect(uiData.currentIndex).toBeUndefined();
264
265    pressRightArrowKey();
266    expect(listenerSpy).toHaveBeenCalledTimes(1);
267
268    await presenter.onAppEvent(
269      new ActiveTraceChanged(
270        assertDefined(positionUpdate.position.entry).getFullTrace(),
271      ),
272    );
273    pressRightArrowKey();
274    expect(listenerSpy).toHaveBeenCalledTimes(2);
275
276    await sendPositionUpdate(positionUpdate);
277    pressRightArrowKey();
278    expect(listenerSpy).toHaveBeenCalledTimes(2);
279
280    await presenter.onAppEvent(
281      new ActiveTraceChanged(
282        UnitTestUtils.makeEmptyTrace(TraceType.TRANSACTIONS),
283      ),
284    );
285    pressRightArrowKey();
286    expect(listenerSpy).toHaveBeenCalledTimes(3);
287
288    document.removeEventListener('keydown', listenerSpy);
289  });
290
291  it('propagates position with next trace entry of different timestamp on right arrow key press', async () => {
292    const positionUpdateEntry = assertDefined(positionUpdate.position.entry);
293    const trace = positionUpdateEntry.getFullTrace();
294    await presenter.onAppEvent(new ActiveTraceChanged(trace));
295
296    const emitEventSpy = jasmine.createSpy();
297    presenter.setEmitEvent(emitEventSpy);
298    await sendPositionUpdate(positionUpdate, true);
299
300    await presenter.onPositionChangeByKeyPress(
301      new KeyboardEvent('keydown', {key: 'ArrowRight'}),
302    );
303    const nextEntry = assertDefined(
304      uiData.entries.find(
305        (entry) =>
306          entry.traceEntry.getTimestamp() > positionUpdateEntry.getTimestamp(),
307      ),
308    );
309    expect(emitEventSpy).toHaveBeenCalledWith(
310      new TracePositionUpdate(
311        TracePosition.fromTraceEntry(nextEntry.traceEntry),
312        true,
313      ),
314    );
315  });
316
317  it('does not propagate any position on right arrow key press if on last entry', async () => {
318    const trace = assertDefined(
319      lastEntryPositionUpdate.position.entry,
320    ).getFullTrace();
321    await presenter.onAppEvent(new ActiveTraceChanged(trace));
322
323    const emitEventSpy = jasmine.createSpy();
324    presenter.setEmitEvent(emitEventSpy);
325    await sendPositionUpdate(lastEntryPositionUpdate, true);
326
327    await presenter.onPositionChangeByKeyPress(
328      new KeyboardEvent('keydown', {key: 'ArrowRight'}),
329    );
330    expect(emitEventSpy).not.toHaveBeenCalled();
331  });
332
333  it('propagates position with first prev trace entry with valid timestamp on left arrow key press', async () => {
334    const trace = assertDefined(
335      lastEntryPositionUpdate.position.entry,
336    ).getFullTrace();
337    await presenter.onAppEvent(new ActiveTraceChanged(trace));
338
339    const emitEventSpy = jasmine.createSpy();
340    presenter.setEmitEvent(emitEventSpy);
341    await sendPositionUpdate(lastEntryPositionUpdate, true);
342
343    const prevIndex = assertDefined(uiData.currentIndex) - 1;
344    spyOn(
345      uiData.entries[prevIndex].traceEntry,
346      'hasValidTimestamp',
347    ).and.returnValue(false);
348    await presenter.onPositionChangeByKeyPress(
349      new KeyboardEvent('keydown', {key: 'ArrowLeft'}),
350    );
351    expect(emitEventSpy).toHaveBeenCalledWith(
352      new TracePositionUpdate(
353        TracePosition.fromTraceEntry(uiData.entries[prevIndex - 1].traceEntry),
354        true,
355      ),
356    );
357  });
358
359  it('does not propagate any position on left arrow key press if on first entry', async () => {
360    const trace = assertDefined(positionUpdate.position.entry).getFullTrace();
361    await presenter.onAppEvent(new ActiveTraceChanged(trace));
362
363    const emitEventSpy = jasmine.createSpy();
364    presenter.setEmitEvent(emitEventSpy);
365    await sendPositionUpdate(positionUpdate, true);
366
367    await presenter.onPositionChangeByKeyPress(
368      new KeyboardEvent('keydown', {key: 'ArrowLeft'}),
369    );
370    expect(emitEventSpy).not.toHaveBeenCalled();
371  });
372
373  it('filters entries on select filter change', async () => {
374    await sendPositionUpdate(positionUpdate, true);
375    const header = uiData.headers[1];
376
377    await presenter.onSelectFilterChange(header, ['0']);
378    expect(
379      new Set(uiData.entries.map((entry) => entry.fields[1].value)),
380    ).toEqual(new Set([0]));
381
382    await presenter.onSelectFilterChange(header, ['0', '2', '3']);
383    expect(
384      new Set(uiData.entries.map((entry) => entry.fields[1].value)),
385    ).toEqual(new Set([0, 2, 3]));
386
387    await presenter.onSelectFilterChange(header, []);
388    expect(
389      new Set(uiData.entries.map((entry) => entry.fields[1].value)),
390    ).toEqual(new Set([0, 1, 2, 3]));
391  });
392
393  it('filters entries on text filter change', async () => {
394    await sendPositionUpdate(positionUpdate, true);
395    const header = uiData.headers[0];
396    const filter = header.filter as LogTextFilter;
397
398    filter.updateFilterValue(['stringValue']);
399    await presenter.onTextFilterChange(header, filter.textFilter);
400    expect(
401      new Set(uiData.entries.map((entry) => entry.fields[0].value)),
402    ).toEqual(new Set(['stringValue']));
403
404    filter.updateFilterValue(['value']);
405    await presenter.onTextFilterChange(header, filter.textFilter);
406    expect(
407      new Set(uiData.entries.map((entry) => entry.fields[0].value)),
408    ).toEqual(new Set(['stringValue', 'differentValue']));
409
410    filter.updateFilterValue(['']);
411    await presenter.onTextFilterChange(header, filter.textFilter);
412    expect(
413      new Set(uiData.entries.map((entry) => entry.fields[0].value)),
414    ).toEqual(new Set(['stringValue', 'differentValue']));
415  });
416
417  it('updates indices when filters change', async () => {
418    await sendPositionUpdate(lastEntryPositionUpdate, true);
419    presenter.onLogEntryClick(1);
420    expect(uiData.currentIndex).toEqual(3);
421    expect(uiData.selectedIndex).toEqual(1);
422
423    const header = uiData.headers[1];
424    await presenter.onSelectFilterChange(header, ['0']);
425    expect(uiData.currentIndex).toEqual(0);
426    expect(uiData.selectedIndex).toEqual(0);
427
428    await presenter.onSelectFilterChange(header, ['0', '2']);
429    expect(uiData.currentIndex).toEqual(1);
430    expect(uiData.selectedIndex).toEqual(0);
431
432    await presenter.onSelectFilterChange(header, []);
433    expect(uiData.currentIndex).toEqual(3);
434    expect(uiData.selectedIndex).toEqual(0);
435  });
436
437  it('updates properties tree when entry clicked', async () => {
438    await sendPositionUpdate(positionUpdate, true);
439
440    await presenter.onLogEntryClick(2);
441    expect(assertDefined(uiData.propertiesTree).id).toEqual(
442      assertDefined(uiData.entries[2].propertiesTree).id,
443    );
444
445    // does not remove selection when entry clicked again
446    await presenter.onLogEntryClick(2);
447    expect(assertDefined(uiData.propertiesTree).id).toEqual(
448      assertDefined(uiData.entries[2].propertiesTree).id,
449    );
450  });
451
452  it('updates properties tree when changed by key press', async () => {
453    await sendPositionUpdate(positionUpdate, true);
454    await presenter.onLogEntryClick(0);
455
456    await presenter.onArrowDownPress();
457    expect(uiData.selectedIndex).toEqual(1);
458    expect(assertDefined(uiData.propertiesTree).id).toEqual(
459      assertDefined(uiData.entries[1].propertiesTree).id,
460    );
461
462    await presenter.onArrowUpPress();
463    expect(uiData.selectedIndex).toEqual(0);
464    expect(assertDefined(uiData.propertiesTree).id).toEqual(
465      assertDefined(uiData.entries[0].propertiesTree).id,
466    );
467
468    // does not remove selection if index out of range
469    await presenter.onArrowUpPress();
470    expect(uiData.selectedIndex).toEqual(0);
471    expect(assertDefined(uiData.propertiesTree).id).toEqual(
472      assertDefined(uiData.entries[0].propertiesTree).id,
473    );
474
475    // does not remove selection if index out of range
476    await presenter.onLogEntryClick(3);
477    await presenter.onArrowDownPress();
478    expect(uiData.selectedIndex).toEqual(3);
479    expect(assertDefined(uiData.propertiesTree).id).toEqual(
480      assertDefined(uiData.entries[3].propertiesTree).id,
481    );
482  });
483
484  it('emits event on log timestamp click', async () => {
485    await sendPositionUpdate(positionUpdate, true);
486    const spy = jasmine.createSpy();
487    presenter.setEmitEvent(spy);
488
489    await presenter.onLogTimestampClick(uiData.entries[0].traceEntry);
490    expect(spy).toHaveBeenCalledWith(
491      TracePositionUpdate.fromTraceEntry(uiData.entries[0].traceEntry, true),
492    );
493  });
494
495  it('emits event on raw timestamp click', async () => {
496    await sendPositionUpdate(positionUpdate, true);
497    const spy = jasmine.createSpy();
498    presenter.setEmitEvent(spy);
499
500    const ts = TimestampConverterUtils.makeZeroTimestamp();
501    await presenter.onRawTimestampClick(ts);
502    expect(spy).toHaveBeenCalledWith(
503      TracePositionUpdate.fromTimestamp(ts, true),
504    );
505  });
506
507  it('filters properties tree', async () => {
508    await sendPositionUpdate(positionUpdate, true);
509    expect(
510      assertDefined(uiData.propertiesTree).getAllChildren().length,
511    ).toEqual(3);
512    await presenter.onPropertiesFilterChange(new TextFilter('pass'));
513    expect(
514      assertDefined(uiData.propertiesTree).getAllChildren().length,
515    ).toEqual(2);
516  });
517
518  it('shows/hides defaults', async () => {
519    await sendPositionUpdate(positionUpdate, true);
520    expect(
521      assertDefined(uiData.propertiesTree).getAllChildren().length,
522    ).toEqual(3);
523    const userOptions: UserOptions = {
524      showDefaults: {
525        name: 'Show defaults',
526        enabled: true,
527      },
528    };
529    await presenter.onPropertiesUserOptionsChange(userOptions);
530    expect(uiData.propertiesUserOptions).toEqual(userOptions);
531    expect(
532      assertDefined(uiData.propertiesTree).getAllChildren().length,
533    ).toEqual(4);
534  });
535
536  it('updates dark mode', async () => {
537    expect(uiData.isDarkMode).toBeFalse();
538    await presenter.onAppEvent(new DarkModeToggled(true));
539    expect(uiData.isDarkMode).toBeTrue();
540  });
541
542  it('is robust to empty trace', async () => {
543    const trace = UnitTestUtils.makeEmptyTrace(TraceType.TRANSACTIONS);
544    const presenter = new MockPresenter(
545      trace,
546      new InMemoryStorage(),
547      (newData) => (uiData = newData),
548    );
549
550    await sendPositionUpdate(
551      TracePositionUpdate.fromTimestamp(
552        TimestampConverterUtils.makeRealTimestamp(0n),
553      ),
554      true,
555      presenter,
556    );
557
558    expect(uiData.entries).toEqual([]);
559    expect(uiData.selectedIndex).toBeUndefined();
560    expect(uiData.scrollToIndex).toBeUndefined();
561    expect(uiData.currentIndex).toBeUndefined();
562    expect(uiData.headers.length).toEqual(3);
563    expect(uiData.propertiesTree).toBeUndefined();
564    expect(uiData.propertiesUserOptions).toBeDefined();
565    expect(uiData.propertiesFilter).toBeDefined();
566  });
567
568  function makeElement(): HTMLElement {
569    const element = document.createElement('div');
570    element.style.height = '5px';
571    element.style.width = '5px';
572    return element;
573  }
574
575  function pressRightArrowKey() {
576    document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
577  }
578
579  async function sendPositionUpdate(
580    update: TracePositionUpdate,
581    isFirst = false,
582    p = presenter,
583  ) {
584    await assertDefined(p).onAppEvent(update);
585    if (isFirst) {
586      expect(uiData.isFetchingData).toBeTrue(); // fetches data asynchronously
587      await TimeUtils.wait(() => !uiData.isFetchingData);
588    }
589    expect(uiData.isFetchingData).toBeFalse();
590  }
591});
592