xref: /aosp_15_r20/development/tools/winscope/src/app/mediator_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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 {assertDefined} from 'common/assert_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {InMemoryStorage} from 'common/in_memory_storage';
20import {TimezoneInfo} from 'common/time';
21import {TimestampConverter} from 'common/timestamp_converter';
22import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
23import {ProgressListener} from 'messaging/progress_listener';
24import {ProgressListenerStub} from 'messaging/progress_listener_stub';
25import {UserWarning} from 'messaging/user_warning';
26import {
27  FailedToCreateTracesParser,
28  IncompleteFrameMapping,
29  InvalidLegacyTrace,
30  NoTraceTargetsSelected,
31  NoValidFiles,
32  UnsupportedFileFormat,
33} from 'messaging/user_warnings';
34import {
35  ActiveTraceChanged,
36  AppFilesCollected,
37  AppFilesUploaded,
38  AppInitialized,
39  AppRefreshDumpsRequest,
40  AppResetRequest,
41  AppTraceViewRequest,
42  DarkModeToggled,
43  ExpandedTimelineToggled,
44  FilterPresetApplyRequest,
45  FilterPresetSaveRequest,
46  InitializeTraceSearchRequest,
47  NoTraceTargetsSelected as NoTraceTargetsSelectedEvent,
48  RemoteToolDownloadStart,
49  RemoteToolFilesReceived,
50  RemoteToolTimestampReceived,
51  TabbedViewSwitched,
52  TabbedViewSwitchRequest,
53  TraceAddRequest,
54  TracePositionUpdate,
55  TraceRemoveRequest,
56  TraceSearchCompleted,
57  TraceSearchFailed,
58  TraceSearchInitialized,
59  TraceSearchRequest,
60  ViewersLoaded,
61  ViewersUnloaded,
62  WinscopeEvent,
63  WinscopeEventType,
64} from 'messaging/winscope_event';
65import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
66import {WinscopeEventEmitterStub} from 'messaging/winscope_event_emitter_stub';
67import {WinscopeEventListener} from 'messaging/winscope_event_listener';
68import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub';
69import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
70import {TraceBuilder} from 'test/unit/trace_builder';
71import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
72import {UnitTestUtils} from 'test/unit/utils';
73import {Trace} from 'trace/trace';
74import {TracePosition} from 'trace/trace_position';
75import {TraceType} from 'trace/trace_type';
76import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
77import {ViewType} from 'viewers/viewer';
78import {ViewerFactory} from 'viewers/viewer_factory';
79import {ViewerStub} from 'viewers/viewer_stub';
80import {Mediator} from './mediator';
81import {TimelineData} from './timeline_data';
82import {TracePipeline} from './trace_pipeline';
83import {TraceSearchInitializer} from './trace_search/trace_search_initializer';
84
85describe('Mediator', () => {
86  const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
87  const TIMESTAMP_11 = TimestampConverterUtils.makeRealTimestamp(11n);
88
89  const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
90  const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
91
92  const traceSf = new TraceBuilder<HierarchyTreeNode>()
93    .setType(TraceType.SURFACE_FLINGER)
94    .setTimestamps([TIMESTAMP_10])
95    .build();
96  const traceWm = new TraceBuilder<HierarchyTreeNode>()
97    .setType(TraceType.WINDOW_MANAGER)
98    .setTimestamps([TIMESTAMP_11])
99    .build();
100  const traceDump = new TraceBuilder<HierarchyTreeNode>()
101    .setType(TraceType.SURFACE_FLINGER)
102    .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()])
103    .build();
104
105  let inputFiles: File[];
106  let eventLogFile: File;
107  let perfettoFile: File;
108  let tracePipeline: TracePipeline;
109  let timelineData: TimelineData;
110  let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
111  let crossToolProtocol: CrossToolProtocol;
112  let appComponent: WinscopeEventListener;
113  let timelineComponent: WinscopeEventEmitter & WinscopeEventListener;
114  let uploadTracesComponent: ProgressListenerStub;
115  let collectTracesComponent: ProgressListenerStub &
116    WinscopeEventEmitterStub &
117    WinscopeEventListenerStub;
118  let traceViewComponent: WinscopeEventEmitter & WinscopeEventListener;
119  let mediator: Mediator;
120  let spies: Array<jasmine.Spy<jasmine.Func>>;
121  let userNotifierChecker: UserNotifierChecker;
122  let createViewersSpy: jasmine.Spy;
123
124  const viewerStub0 = new ViewerStub('Title0', undefined, traceSf);
125  const viewerStub1 = new ViewerStub('Title1', undefined, traceWm);
126  const viewerOverlay = new ViewerStub(
127    'TitleOverlay',
128    undefined,
129    traceWm,
130    ViewType.OVERLAY,
131  );
132  const viewerDump = new ViewerStub('TitleDump', undefined, traceDump);
133  const viewers = [viewerStub0, viewerStub1, viewerOverlay, viewerDump];
134  let tracePositionUpdateListeners: WinscopeEventListener[];
135
136  beforeAll(async () => {
137    inputFiles = [
138      await UnitTestUtils.getFixtureFile(
139        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
140      ),
141      await UnitTestUtils.getFixtureFile(
142        'traces/elapsed_and_real_timestamp/WindowManager.pb',
143      ),
144      await UnitTestUtils.getFixtureFile(
145        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
146      ),
147    ];
148    perfettoFile = await UnitTestUtils.getFixtureFile(
149      'traces/perfetto/layers_trace.perfetto-trace',
150    );
151    eventLogFile = await UnitTestUtils.getFixtureFile(
152      'traces/eventlog_no_cujs.winscope',
153    );
154    userNotifierChecker = new UserNotifierChecker();
155  });
156
157  beforeEach(() => {
158    userNotifierChecker.reset();
159    jasmine.addCustomEqualityTester(tracePositionUpdateEqualityTester);
160    tracePipeline = new TracePipeline();
161    timelineData = new TimelineData();
162    abtChromeExtensionProtocol = FunctionUtils.mixin(
163      new WinscopeEventEmitterStub(),
164      new WinscopeEventListenerStub(),
165    );
166    crossToolProtocol = new CrossToolProtocol(
167      tracePipeline.getTimestampConverter(),
168    );
169    appComponent = new WinscopeEventListenerStub();
170    timelineComponent = FunctionUtils.mixin(
171      new WinscopeEventEmitterStub(),
172      new WinscopeEventListenerStub(),
173    );
174    uploadTracesComponent = new ProgressListenerStub();
175    collectTracesComponent = FunctionUtils.mixin(
176      FunctionUtils.mixin(
177        new ProgressListenerStub(),
178        new WinscopeEventListenerStub(),
179      ),
180      new WinscopeEventEmitterStub(),
181    );
182    traceViewComponent = FunctionUtils.mixin(
183      new WinscopeEventEmitterStub(),
184      new WinscopeEventListenerStub(),
185    );
186    mediator = new Mediator(
187      tracePipeline,
188      timelineData,
189      abtChromeExtensionProtocol,
190      crossToolProtocol,
191      appComponent,
192      new InMemoryStorage(),
193    );
194    mediator.setTimelineComponent(timelineComponent);
195    mediator.setUploadTracesComponent(uploadTracesComponent);
196    mediator.setCollectTracesComponent(collectTracesComponent);
197    mediator.setTraceViewComponent(traceViewComponent);
198
199    tracePositionUpdateListeners = [
200      ...viewers,
201      timelineComponent,
202      crossToolProtocol,
203    ];
204
205    createViewersSpy = spyOn(
206      ViewerFactory.prototype,
207      'createViewers',
208    ).and.returnValue(viewers);
209
210    spies = [
211      spyOn(abtChromeExtensionProtocol, 'onWinscopeEvent'),
212      spyOn(appComponent, 'onWinscopeEvent'),
213      spyOn(collectTracesComponent, 'onOperationFinished'),
214      spyOn(collectTracesComponent, 'onProgressUpdate'),
215      spyOn(collectTracesComponent, 'onWinscopeEvent'),
216      spyOn(crossToolProtocol, 'onWinscopeEvent'),
217      spyOn(timelineComponent, 'onWinscopeEvent'),
218      spyOn(timelineData, 'initialize').and.callThrough(),
219      spyOn(traceViewComponent, 'onWinscopeEvent'),
220      spyOn(uploadTracesComponent, 'onProgressUpdate'),
221      spyOn(uploadTracesComponent, 'onOperationFinished'),
222      spyOn(viewerStub0, 'onWinscopeEvent'),
223      spyOn(viewerStub1, 'onWinscopeEvent'),
224      spyOn(viewerOverlay, 'onWinscopeEvent'),
225      spyOn(viewerDump, 'onWinscopeEvent'),
226    ];
227  });
228
229  it('notifies ABT chrome extension about app initialization', async () => {
230    expect(abtChromeExtensionProtocol.onWinscopeEvent).not.toHaveBeenCalled();
231
232    await mediator.onWinscopeEvent(new AppInitialized());
233    expect(abtChromeExtensionProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
234      new AppInitialized(),
235    );
236  });
237
238  it('handles uploaded traces from Winscope', async () => {
239    await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
240
241    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled();
242    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
243    expect(timelineData.initialize).not.toHaveBeenCalled();
244    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
245    expect(viewerStub0.onWinscopeEvent).not.toHaveBeenCalled();
246
247    resetSpyCalls();
248    await mediator.onWinscopeEvent(new AppTraceViewRequest());
249    await checkLoadTraceViewEvents(uploadTracesComponent);
250    userNotifierChecker.expectNotified([]);
251  });
252
253  it('handles collected traces from Winscope', async () => {
254    await mediator.onWinscopeEvent(
255      new AppFilesCollected({
256        requested: [],
257        collected: [inputFiles[0], inputFiles[1]],
258      }),
259    );
260    userNotifierChecker.expectNone();
261    await checkLoadTraceViewEvents(collectTracesComponent);
262  });
263
264  it('handles invalid collected traces from Winscope', async () => {
265    await mediator.onWinscopeEvent(
266      new AppFilesCollected({
267        requested: [],
268        collected: [await UnitTestUtils.getFixtureFile('traces/empty.pb')],
269      }),
270    );
271    expect(
272      userNotifierChecker.expectNotified([
273        new UnsupportedFileFormat('empty.pb'),
274      ]),
275    );
276    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
277  });
278
279  it('handles collected traces with no entries from Winscope', async () => {
280    await mediator.onWinscopeEvent(
281      new AppFilesCollected({
282        requested: [],
283        collected: [
284          await UnitTestUtils.getFixtureFile(
285            'traces/no_entries_InputMethodClients.pb',
286          ),
287        ],
288      }),
289    );
290    expect(
291      userNotifierChecker.expectNotified([
292        new InvalidLegacyTrace(
293          'no_entries_InputMethodClients.pb',
294          'Trace has no entries',
295        ),
296      ]),
297    );
298    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
299  });
300
301  it('handles collected trace with no visualization from Winscope', async () => {
302    await mediator.onWinscopeEvent(
303      new AppFilesCollected({
304        requested: [],
305        collected: [eventLogFile],
306      }),
307    );
308    expect(
309      userNotifierChecker.expectNotified([
310        new FailedToCreateTracesParser(
311          TraceType.CUJS,
312          'eventlog_no_cujs.winscope has no relevant entries',
313        ),
314      ]),
315    );
316    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
317  });
318
319  it('handles empty collected traces from Winscope', async () => {
320    await mediator.onWinscopeEvent(
321      new AppFilesCollected({
322        requested: [],
323        collected: [],
324      }),
325    );
326    expect(userNotifierChecker.expectNotified([new NoValidFiles()]));
327    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
328  });
329
330  it('handles requested traces with missing collected traces from Winscope', async () => {
331    await mediator.onWinscopeEvent(
332      new AppFilesCollected({
333        requested: [
334          {
335            name: 'Collected Trace',
336            types: [TraceType.SURFACE_FLINGER],
337          },
338          {
339            name: 'Uncollected Trace',
340            types: [TraceType.TRANSITION],
341          },
342        ],
343        collected: [inputFiles[0]],
344      }),
345    );
346    expect(
347      userNotifierChecker.expectNotified([
348        new NoValidFiles(['Uncollected Trace']),
349      ]),
350    );
351    expect(appComponent.onWinscopeEvent).toHaveBeenCalled();
352  });
353
354  it('handles app reset request', async () => {
355    await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
356    const clearSpies = [
357      spyOn(tracePipeline, 'clear'),
358      spyOn(timelineData, 'clear'),
359    ];
360    await mediator.onWinscopeEvent(new AppResetRequest());
361    clearSpies.forEach((spy) => expect(spy).toHaveBeenCalled());
362    expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
363      new ViewersUnloaded(),
364    );
365  });
366
367  it('handles request to refresh dumps', async () => {
368    const dumpFiles = [
369      await UnitTestUtils.getFixtureFile(
370        'traces/elapsed_and_real_timestamp/dump_SurfaceFlinger.pb',
371      ),
372      await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
373    ];
374    await loadFiles(dumpFiles);
375    await mediator.onWinscopeEvent(new AppTraceViewRequest());
376    await checkLoadTraceViewEvents(uploadTracesComponent);
377
378    await mediator.onWinscopeEvent(new AppRefreshDumpsRequest());
379    expect(collectTracesComponent.onWinscopeEvent).toHaveBeenCalled();
380  });
381
382  //TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with
383  //      Node.js (b/262269229). FileUtils#unzipFile() currently can't execute on Node.js.
384
385  //TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js
386  //      (b/262269229).
387
388  it('handles start download event from remote tool', async () => {
389    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0);
390
391    await mediator.onWinscopeEvent(new RemoteToolDownloadStart());
392    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1);
393  });
394
395  it('handles empty downloaded files from remote tool', async () => {
396    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0);
397
398    // Pass files even if empty so that the upload component will update the progress bar
399    // and display error messages
400    await mediator.onWinscopeEvent(new RemoteToolFilesReceived([]));
401    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1);
402  });
403
404  it('notifies overlay viewer of expanded timeline toggle change', async () => {
405    await loadFiles();
406    await loadTraceView();
407    const event = new ExpandedTimelineToggled(true);
408    await mediator.onWinscopeEvent(new ExpandedTimelineToggled(true));
409    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(event);
410  });
411
412  it('propagates trace position update', async () => {
413    await loadFiles();
414    await loadTraceView();
415
416    // notify position
417    resetSpyCalls();
418    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
419    checkTracePositionUpdateEvents(
420      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
421      [],
422      POSITION_10,
423    );
424
425    // notify position
426    resetSpyCalls();
427    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_11));
428    checkTracePositionUpdateEvents(
429      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
430      [],
431      POSITION_11,
432    );
433  });
434
435  it('propagates trace position update according to timezone', async () => {
436    const timezoneInfo: TimezoneInfo = {
437      timezone: 'Asia/Kolkata',
438      locale: 'en-US',
439    };
440    const converter = new TimestampConverter(timezoneInfo, 0n);
441    spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter);
442    await loadFiles();
443    await loadTraceView();
444
445    // notify position
446    resetSpyCalls();
447    const expectedPosition = TracePosition.fromTimestamp(
448      converter.makeTimestampFromRealNs(10n),
449    );
450    await mediator.onWinscopeEvent(new TracePositionUpdate(expectedPosition));
451    checkTracePositionUpdateEvents(
452      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
453      [],
454      expectedPosition,
455      POSITION_10,
456    );
457  });
458
459  it('propagates trace position update and updates timeline data', async () => {
460    await loadFiles();
461    await loadTraceView();
462
463    // notify position
464    resetSpyCalls();
465    const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs();
466    const timestamp =
467      TimestampConverterUtils.makeRealTimestamp(finalTimestampNs);
468    const position = TracePosition.fromTimestamp(timestamp);
469
470    await mediator.onWinscopeEvent(new TracePositionUpdate(position, true));
471    checkTracePositionUpdateEvents(
472      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
473      [],
474      position,
475    );
476    expect(
477      assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs(),
478    ).toEqual(finalTimestampNs);
479  });
480
481  it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
482    const dumpFile = await UnitTestUtils.getFixtureFile(
483      'traces/dump_WindowManager.pb',
484    );
485    await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
486
487    resetSpyCalls();
488    await mediator.onWinscopeEvent(new AppTraceViewRequest());
489    await checkLoadTraceViewEvents(uploadTracesComponent);
490    userNotifierChecker.expectNotified([]);
491  });
492
493  it('filters traces without visualization on loading viewers', async () => {
494    const fileWithoutVisualization = await UnitTestUtils.getFixtureFile(
495      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
496    );
497    await loadFiles();
498    await mediator.onWinscopeEvent(
499      new AppFilesUploaded([fileWithoutVisualization]),
500    );
501    await loadTraceView();
502  });
503
504  it('warns user if frame mapping fails', async () => {
505    const errorMsg = 'frame mapping failed';
506    spyOn(tracePipeline, 'buildTraces').and.throwError(errorMsg);
507    const dumpFile = await UnitTestUtils.getFixtureFile(
508      'traces/dump_WindowManager.pb',
509    );
510    await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
511
512    resetSpyCalls();
513    await mediator.onWinscopeEvent(new AppTraceViewRequest());
514    await checkLoadTraceViewEvents(uploadTracesComponent, undefined, [
515      new IncompleteFrameMapping(errorMsg),
516    ]);
517  });
518
519  describe('timestamp received from remote tool', () => {
520    it('propagates trace position update', async () => {
521      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
522      await loadFiles();
523      await loadTraceView();
524      const traceSfEntry = assertDefined(
525        tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
526      ).getEntry(2);
527
528      // receive timestamp
529      resetSpyCalls();
530      await mediator.onWinscopeEvent(
531        new RemoteToolTimestampReceived(() => traceSfEntry.getTimestamp()),
532      );
533
534      checkTracePositionUpdateEvents(
535        [viewerStub0, viewerOverlay, timelineComponent],
536        [],
537        TracePosition.fromTraceEntry(traceSfEntry),
538      );
539    });
540
541    it("doesn't propagate timestamp back to remote tool", async () => {
542      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
543      await loadFiles();
544      await loadTraceView();
545
546      // receive timestamp
547      resetSpyCalls();
548      await mediator.onWinscopeEvent(
549        new RemoteToolTimestampReceived(() => TIMESTAMP_10),
550      );
551      checkTracePositionUpdateEvents(
552        [viewerStub0, viewerOverlay, timelineComponent],
553        [],
554      );
555    });
556
557    it('defers trace position propagation till traces are loaded and visualized', async () => {
558      // ensure converter has been used to create real timestamps
559      tracePipeline.getTimestampConverter().makeTimestampFromRealNs(0n);
560
561      // load files but do not load trace view
562      await loadFiles();
563      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
564      const traceSf = assertDefined(
565        tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
566      );
567
568      // keep timestamp for later
569      await mediator.onWinscopeEvent(
570        new RemoteToolTimestampReceived(() =>
571          traceSf.getEntry(1).getTimestamp(),
572        ),
573      );
574      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
575
576      // keep timestamp for later (replace previous one)
577      await mediator.onWinscopeEvent(
578        new RemoteToolTimestampReceived(() =>
579          traceSf.getEntry(2).getTimestamp(),
580        ),
581      );
582      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
583
584      // apply timestamp
585      await loadTraceView();
586
587      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
588        makeExpectedTracePositionUpdate(
589          TracePosition.fromTraceEntry(traceSf.getEntry(2)),
590        ),
591      );
592    });
593  });
594
595  describe('tab view switches', () => {
596    it('forwards switch notifications', async () => {
597      await loadFiles();
598      await loadTraceView();
599      resetSpyCalls();
600
601      const view = viewerStub1.getViews()[0];
602      await mediator.onWinscopeEvent(new TabbedViewSwitched(view));
603      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
604        new ActiveTraceChanged(view.traces[0]),
605      );
606      userNotifierChecker.expectNotified([]);
607      userNotifierChecker.reset();
608      const viewDump = viewerDump.getViews()[0];
609      await mediator.onWinscopeEvent(new TabbedViewSwitched(viewDump));
610      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalledWith(
611        new ActiveTraceChanged(viewDump.traces[0]),
612      );
613      userNotifierChecker.expectNotified([]);
614    });
615
616    it('forwards switch requests from viewers to trace view component', async () => {
617      await loadFiles();
618      await loadTraceView();
619      expect(traceViewComponent.onWinscopeEvent).not.toHaveBeenCalled();
620
621      await viewerStub0.emitAppEventForTesting(
622        new TabbedViewSwitchRequest(traceSf),
623      );
624      expect(traceViewComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
625        new TabbedViewSwitchRequest(traceSf),
626      );
627      userNotifierChecker.expectNotified([]);
628    });
629  });
630
631  it('notifies only visible viewers about trace position updates', async () => {
632    await loadFiles();
633    await loadTraceView();
634
635    // Position update -> update only visible viewers
636    // Note: Viewer 0 is visible (gets focus) upon UI initialization
637    resetSpyCalls();
638    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
639    checkTracePositionUpdateEvents(
640      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
641      [],
642      POSITION_10,
643    );
644
645    // Tab switch -> update only newly visible viewers
646    // Note: overlay viewer is considered always visible
647    resetSpyCalls();
648    await mediator.onWinscopeEvent(
649      new TabbedViewSwitched(viewerStub1.getViews()[0]),
650    );
651    userNotifierChecker.expectNone();
652    const tracePositionUpdate = makeExpectedTracePositionUpdate(undefined);
653    const activeTraceChanged = new ActiveTraceChanged(
654      viewerStub1.getViews()[0].traces[0],
655    );
656    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(
657      activeTraceChanged,
658    );
659    expect(viewerDump.onWinscopeEvent).toHaveBeenCalledOnceWith(
660      activeTraceChanged,
661    );
662
663    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith(
664      tracePositionUpdate,
665    );
666    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledWith(
667      activeTraceChanged,
668    );
669
670    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(
671      tracePositionUpdate,
672    );
673    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(
674      activeTraceChanged,
675    );
676
677    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
678      tracePositionUpdate,
679    );
680    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
681      activeTraceChanged,
682    );
683
684    expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
685      tracePositionUpdate,
686    );
687
688    // Position update -> update only visible viewers
689    // Note: overlay viewer is considered always visible
690    resetSpyCalls();
691    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
692    checkTracePositionUpdateEvents(
693      [viewerStub1, viewerOverlay, timelineComponent, crossToolProtocol],
694      [],
695    );
696  });
697
698  it('notifies timeline of dark mode toggle', async () => {
699    const event = new DarkModeToggled(true);
700    await mediator.onWinscopeEvent(event);
701    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(event);
702  });
703
704  it('notifies timeline and viewers of active trace change', async () => {
705    await loadFiles();
706    await loadTraceView();
707    resetSpyCalls();
708
709    const activeTraceChanged = new ActiveTraceChanged(traceWm);
710    await mediator.onWinscopeEvent(activeTraceChanged);
711    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
712      activeTraceChanged,
713    );
714    viewers.forEach((viewer) =>
715      expect(viewer.onWinscopeEvent).toHaveBeenCalledOnceWith(
716        activeTraceChanged,
717      ),
718    );
719  });
720
721  it('notifies user of no trace targets selected', async () => {
722    await mediator.onWinscopeEvent(new NoTraceTargetsSelectedEvent());
723    userNotifierChecker.expectNotified([new NoTraceTargetsSelected()]);
724  });
725
726  it('notifies correct viewer of filter preset requests', async () => {
727    await loadFiles();
728    await loadTraceView();
729    resetSpyCalls();
730
731    const saveRequest = new FilterPresetSaveRequest(
732      'test_preset',
733      TraceType.SURFACE_FLINGER,
734    );
735    await mediator.onWinscopeEvent(saveRequest);
736
737    const applyRequest = new FilterPresetApplyRequest(
738      'test_preset',
739      TraceType.WINDOW_MANAGER,
740    );
741    await mediator.onWinscopeEvent(applyRequest);
742
743    await mediator.onWinscopeEvent(
744      new FilterPresetSaveRequest('test_preset', TraceType.PROTO_LOG),
745    );
746    await mediator.onWinscopeEvent(
747      new FilterPresetApplyRequest('test_preset', TraceType.PROTO_LOG),
748    );
749
750    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(saveRequest);
751    expect(viewerStub1.onWinscopeEvent).toHaveBeenCalledOnceWith(applyRequest);
752  });
753
754  it('initializes trace search', async () => {
755    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
756    const spy = spyOn(
757      TraceSearchInitializer,
758      'createSearchViews',
759    ).and.returnValue(Promise.resolve(['test']));
760    const initializeRequest = new InitializeTraceSearchRequest();
761    await mediator.onWinscopeEvent(initializeRequest);
762    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
763      initializeRequest,
764    );
765    expect(spy).toHaveBeenCalledTimes(1);
766    const initializedEvent = new TraceSearchInitialized(['test']);
767    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
768      initializedEvent,
769    );
770    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(initializedEvent);
771  });
772
773  it('handles trace search request for successful queries', async () => {
774    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
775    await requestSearch('select ts from surfaceflinger_layers_snapshot');
776    checkNewSearchTracePropagation(searchViewer, true);
777    await requestSearch('select id from surfaceflinger_layers_snapshot');
778    checkNewSearchTracePropagation(searchViewer, false);
779  });
780
781  it('handles trace search request for unsuccessful query', async () => {
782    const searchViewer = await loadPerfettoFilesAndReturnSearchViewer();
783    await requestSearch('select * from fake_table');
784    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(
785      new TraceSearchFailed(),
786    );
787    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
788      new TraceSearchCompleted(),
789    );
790  });
791
792  it('handles trace removal requests', async () => {
793    await loadPerfettoFilesAndReturnSearchViewer();
794    await requestSearch('select ts from surfaceflinger_layers_snapshot');
795    removeSearchTraceAndCheckPropagation(true);
796    await requestSearch('select id from surfaceflinger_layers_snapshot');
797    removeSearchTraceAndCheckPropagation(false);
798  });
799
800  async function loadFiles(
801    files = inputFiles,
802    viewersToReassignTraces = [viewerStub0, viewerStub1],
803  ) {
804    for (const file of files) {
805      await mediator.onWinscopeEvent(new AppFilesUploaded([file]));
806    }
807    userNotifierChecker.expectNone();
808    viewersToReassignTraces.forEach((viewer) =>
809      reassignViewerStubTrace(viewer),
810    );
811  }
812
813  function reassignViewerStubTrace(viewerStub: ViewerStub) {
814    const viewerStubTraces = viewerStub.getViews()[0].traces;
815    viewerStubTraces[0] = tracePipeline
816      .getTraces()
817      .getTrace(viewerStubTraces[0].type) as Trace<object>;
818  }
819
820  async function loadTraceView(expectedViewers = viewers) {
821    // Simulate "View traces" button click
822    resetSpyCalls();
823    await mediator.onWinscopeEvent(new AppTraceViewRequest());
824
825    await checkLoadTraceViewEvents(uploadTracesComponent, expectedViewers);
826
827    // Simulate notification of TraceViewComponent about initially selected/focused tab
828    resetSpyCalls();
829    await mediator.onWinscopeEvent(
830      new TabbedViewSwitched(viewerStub0.getViews()[0]),
831    );
832
833    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(
834      makeExpectedTracePositionUpdate(),
835    );
836    expect(viewerStub1.onWinscopeEvent).not.toHaveBeenCalled();
837    userNotifierChecker.expectNotified([]);
838  }
839
840  async function checkLoadTraceViewEvents(
841    progressListener: ProgressListener,
842    expectedViewers = viewers,
843    notifications: UserWarning[] = [],
844  ) {
845    expect(progressListener.onProgressUpdate).toHaveBeenCalled();
846    expect(progressListener.onOperationFinished).toHaveBeenCalled();
847    expect(timelineData.initialize).toHaveBeenCalledTimes(1);
848    expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
849      new ViewersLoaded(expectedViewers),
850    );
851
852    // Mediator triggers the viewers initialization
853    // by sending them a "trace position update" event
854    checkTracePositionUpdateEvents(
855      (expectedViewers as WinscopeEventListener[]).concat([timelineComponent]),
856      notifications,
857    );
858  }
859
860  function checkTracePositionUpdateEvents(
861    listenersToBeNotified: WinscopeEventListener[],
862    userNotifications: UserWarning[],
863    position?: TracePosition,
864    crossToolProtocolPosition = position,
865  ) {
866    userNotifierChecker.expectNotified(userNotifications);
867    const event = makeExpectedTracePositionUpdate(position);
868    const crossToolProtocolEvent =
869      crossToolProtocolPosition !== position
870        ? makeExpectedTracePositionUpdate(crossToolProtocolPosition)
871        : event;
872    tracePositionUpdateListeners.forEach((listener) => {
873      const isVisible = listenersToBeNotified.includes(listener);
874      if (isVisible) {
875        const expected =
876          listener === crossToolProtocol ? crossToolProtocolEvent : event;
877        expect(listener.onWinscopeEvent).toHaveBeenCalledOnceWith(expected);
878      } else {
879        expect(listener.onWinscopeEvent).not.toHaveBeenCalled();
880      }
881    });
882  }
883
884  function resetSpyCalls() {
885    spies.forEach((spy) => {
886      spy.calls.reset();
887    });
888    userNotifierChecker.reset();
889  }
890
891  async function loadPerfettoFilesAndReturnSearchViewer(): Promise<ViewerStub> {
892    await loadFiles([perfettoFile], [viewerStub0]);
893    const searchViewer = new ViewerStub(
894      'search',
895      undefined,
896      undefined,
897      ViewType.GLOBAL_SEARCH,
898    );
899    spyOn(searchViewer, 'onWinscopeEvent');
900    const expectedViewers = [viewerStub0, searchViewer];
901    createViewersSpy.and.returnValue(expectedViewers);
902    await loadTraceView(expectedViewers);
903    resetSpyCalls();
904    return searchViewer;
905  }
906
907  async function requestSearch(query: string) {
908    const event = new TraceSearchRequest(query);
909    await mediator.onWinscopeEvent(event);
910    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(event);
911  }
912
913  function checkNewSearchTracePropagation(
914    searchViewer: ViewerStub,
915    hasTimestamps: boolean,
916  ) {
917    const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH);
918    const newTrace = searchTraces[searchTraces.length - 1];
919    const newTraceEvent = new TraceAddRequest(newTrace);
920    expect(searchViewer.onWinscopeEvent).toHaveBeenCalledWith(newTraceEvent);
921    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
922      new TraceSearchCompleted(),
923    );
924    expect(timelineData.hasTrace(newTrace)).toEqual(hasTimestamps);
925    const timelineComponentSpy = timelineComponent.onWinscopeEvent;
926    if (hasTimestamps) {
927      expect(timelineComponentSpy).toHaveBeenCalledWith(newTraceEvent);
928    } else {
929      expect(timelineComponentSpy).not.toHaveBeenCalledWith(newTraceEvent);
930    }
931  }
932
933  async function removeSearchTraceAndCheckPropagation(hasTimestamps: boolean) {
934    const searchTraces = tracePipeline.getTraces().getTraces(TraceType.SEARCH);
935    const newTrace = searchTraces[searchTraces.length - 1];
936    const removalRequest = new TraceRemoveRequest(newTrace);
937    await mediator.onWinscopeEvent(removalRequest);
938    expect(tracePipeline.getTraces().hasTrace(newTrace)).toBeFalse();
939    expect(timelineData.hasTrace(newTrace)).toBeFalse();
940    const timelineComponentSpy = timelineComponent.onWinscopeEvent;
941    if (hasTimestamps) {
942      expect(timelineComponentSpy).toHaveBeenCalledWith(removalRequest);
943    } else {
944      expect(timelineComponentSpy).not.toHaveBeenCalledWith(removalRequest);
945    }
946  }
947
948  function makeExpectedTracePositionUpdate(
949    tracePosition?: TracePosition,
950  ): WinscopeEvent {
951    if (tracePosition !== undefined) {
952      return new TracePositionUpdate(tracePosition);
953    }
954    return {type: WinscopeEventType.TRACE_POSITION_UPDATE} as WinscopeEvent;
955  }
956
957  function tracePositionUpdateEqualityTester(
958    first: any,
959    second: any,
960  ): boolean | undefined {
961    if (
962      first instanceof TracePositionUpdate &&
963      second instanceof TracePositionUpdate
964    ) {
965      return testTracePositionUpdates(first, second);
966    }
967    if (
968      first instanceof TracePositionUpdate &&
969      second.type === WinscopeEventType.TRACE_POSITION_UPDATE
970    ) {
971      return first.type === second.type;
972    }
973    return undefined;
974  }
975
976  function testTracePositionUpdates(
977    event: TracePositionUpdate,
978    expectedEvent: TracePositionUpdate,
979  ): boolean {
980    if (event.type !== expectedEvent.type) return false;
981    if (
982      event.position.timestamp.getValueNs() !==
983      expectedEvent.position.timestamp.getValueNs()
984    ) {
985      return false;
986    }
987    if (event.position.frame !== expectedEvent.position.frame) return false;
988    return true;
989  }
990});
991