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 {INVALID_TIME_NS, 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 {UserNotificationsListener} from 'messaging/user_notifications_listener';
26import {UserNotificationsListenerStub} from 'messaging/user_notifications_listener_stub';
27import {
28  ActiveTraceChanged,
29  AppFilesCollected,
30  AppFilesUploaded,
31  AppInitialized,
32  AppRefreshDumpsRequest,
33  AppTraceViewRequest,
34  ExpandedTimelineToggled,
35  RemoteToolDownloadStart,
36  RemoteToolFilesReceived,
37  RemoteToolTimestampReceived,
38  TabbedViewSwitched,
39  TabbedViewSwitchRequest,
40  TracePositionUpdate,
41  ViewersLoaded,
42  WinscopeEvent,
43  WinscopeEventType,
44} from 'messaging/winscope_event';
45import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
46import {WinscopeEventEmitterStub} from 'messaging/winscope_event_emitter_stub';
47import {WinscopeEventListener} from 'messaging/winscope_event_listener';
48import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub';
49import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
50import {TraceBuilder} from 'test/unit/trace_builder';
51import {UnitTestUtils} from 'test/unit/utils';
52import {Trace} from 'trace/trace';
53import {TracePosition} from 'trace/trace_position';
54import {TraceType} from 'trace/trace_type';
55import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
56import {ViewType} from 'viewers/viewer';
57import {ViewerFactory} from 'viewers/viewer_factory';
58import {ViewerStub} from 'viewers/viewer_stub';
59import {Mediator} from './mediator';
60import {TimelineData} from './timeline_data';
61import {TracePipeline} from './trace_pipeline';
62
63describe('Mediator', () => {
64  const traceSf = new TraceBuilder<HierarchyTreeNode>()
65    .setType(TraceType.SURFACE_FLINGER)
66    .setEntries([])
67    .build();
68  const traceWm = new TraceBuilder<HierarchyTreeNode>()
69    .setType(TraceType.WINDOW_MANAGER)
70    .setEntries([])
71    .build();
72  const traceDump = new TraceBuilder<HierarchyTreeNode>()
73    .setType(TraceType.SURFACE_FLINGER)
74    .setTimestamps([TimestampConverterUtils.makeRealTimestamp(INVALID_TIME_NS)])
75    .build();
76
77  let inputFiles: File[];
78  let userNotificationsListener: UserNotificationsListener;
79  let tracePipeline: TracePipeline;
80  let timelineData: TimelineData;
81  let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
82  let crossToolProtocol: CrossToolProtocol;
83  let appComponent: WinscopeEventListener;
84  let timelineComponent: WinscopeEventEmitter & WinscopeEventListener;
85  let uploadTracesComponent: ProgressListenerStub;
86  let collectTracesComponent: ProgressListenerStub & WinscopeEventListenerStub;
87  let traceViewComponent: WinscopeEventEmitter & WinscopeEventListener;
88  let mediator: Mediator;
89  let spies: Array<jasmine.Spy<any>>;
90
91  const viewerStub0 = new ViewerStub('Title0', undefined, traceSf);
92  const viewerStub1 = new ViewerStub('Title1', undefined, traceWm);
93  const viewerOverlay = new ViewerStub(
94    'TitleOverlay',
95    undefined,
96    traceWm,
97    ViewType.OVERLAY,
98  );
99  const viewerDump = new ViewerStub('TitleDump', undefined, traceDump);
100  const viewers = [viewerStub0, viewerStub1, viewerOverlay, viewerDump];
101  let tracePositionUpdateListeners: WinscopeEventListener[];
102
103  const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
104  const TIMESTAMP_11 = TimestampConverterUtils.makeRealTimestamp(11n);
105
106  const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
107  const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
108
109  beforeAll(async () => {
110    inputFiles = [
111      await UnitTestUtils.getFixtureFile(
112        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
113      ),
114      await UnitTestUtils.getFixtureFile(
115        'traces/elapsed_and_real_timestamp/WindowManager.pb',
116      ),
117      await UnitTestUtils.getFixtureFile(
118        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
119      ),
120    ];
121  });
122
123  beforeEach(async () => {
124    jasmine.addCustomEqualityTester(tracePositionUpdateEqualityTester);
125    userNotificationsListener = new UserNotificationsListenerStub();
126    tracePipeline = new TracePipeline();
127    timelineData = new TimelineData();
128    abtChromeExtensionProtocol = FunctionUtils.mixin(
129      new WinscopeEventEmitterStub(),
130      new WinscopeEventListenerStub(),
131    );
132    crossToolProtocol = new CrossToolProtocol(
133      tracePipeline.getTimestampConverter(),
134    );
135    appComponent = new WinscopeEventListenerStub();
136    timelineComponent = FunctionUtils.mixin(
137      new WinscopeEventEmitterStub(),
138      new WinscopeEventListenerStub(),
139    );
140    uploadTracesComponent = new ProgressListenerStub();
141    collectTracesComponent = FunctionUtils.mixin(
142      new ProgressListenerStub(),
143      new WinscopeEventListenerStub(),
144    );
145    traceViewComponent = FunctionUtils.mixin(
146      new WinscopeEventEmitterStub(),
147      new WinscopeEventListenerStub(),
148    );
149    mediator = new Mediator(
150      tracePipeline,
151      timelineData,
152      abtChromeExtensionProtocol,
153      crossToolProtocol,
154      appComponent,
155      userNotificationsListener,
156      new InMemoryStorage(),
157    );
158    mediator.setTimelineComponent(timelineComponent);
159    mediator.setUploadTracesComponent(uploadTracesComponent);
160    mediator.setCollectTracesComponent(collectTracesComponent);
161    mediator.setTraceViewComponent(traceViewComponent);
162
163    tracePositionUpdateListeners = [
164      ...viewers,
165      timelineComponent,
166      crossToolProtocol,
167    ];
168
169    spyOn(ViewerFactory.prototype, 'createViewers').and.returnValue(viewers);
170
171    spies = [
172      spyOn(abtChromeExtensionProtocol, 'onWinscopeEvent'),
173      spyOn(appComponent, 'onWinscopeEvent'),
174      spyOn(collectTracesComponent, 'onOperationFinished'),
175      spyOn(collectTracesComponent, 'onProgressUpdate'),
176      spyOn(collectTracesComponent, 'onWinscopeEvent'),
177      spyOn(crossToolProtocol, 'onWinscopeEvent'),
178      spyOn(timelineComponent, 'onWinscopeEvent'),
179      spyOn(timelineData, 'initialize').and.callThrough(),
180      spyOn(traceViewComponent, 'onWinscopeEvent'),
181      spyOn(uploadTracesComponent, 'onProgressUpdate'),
182      spyOn(uploadTracesComponent, 'onOperationFinished'),
183      spyOn(userNotificationsListener, 'onNotifications'),
184      spyOn(viewerStub0, 'onWinscopeEvent'),
185      spyOn(viewerStub1, 'onWinscopeEvent'),
186      spyOn(viewerOverlay, 'onWinscopeEvent'),
187      spyOn(viewerDump, 'onWinscopeEvent'),
188    ];
189  });
190
191  it('notifies ABT chrome extension about app initialization', async () => {
192    expect(abtChromeExtensionProtocol.onWinscopeEvent).not.toHaveBeenCalled();
193
194    await mediator.onWinscopeEvent(new AppInitialized());
195    expect(abtChromeExtensionProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
196      new AppInitialized(),
197    );
198  });
199
200  it('handles uploaded traces from Winscope', async () => {
201    await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
202
203    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled();
204    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
205    expect(timelineData.initialize).not.toHaveBeenCalled();
206    expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
207    expect(viewerStub0.onWinscopeEvent).not.toHaveBeenCalled();
208
209    resetSpyCalls();
210    await mediator.onWinscopeEvent(new AppTraceViewRequest());
211    await checkLoadTraceViewEvents(uploadTracesComponent);
212  });
213
214  it('handles collected traces from Winscope', async () => {
215    await mediator.onWinscopeEvent(new AppFilesCollected(inputFiles));
216    await checkLoadTraceViewEvents(collectTracesComponent);
217  });
218
219  it('handles request to refresh dumps', async () => {
220    const dumpFiles = [
221      await UnitTestUtils.getFixtureFile(
222        'traces/elapsed_and_real_timestamp/dump_SurfaceFlinger.pb',
223      ),
224      await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
225    ];
226    await loadFiles(dumpFiles);
227    await mediator.onWinscopeEvent(new AppTraceViewRequest());
228    await checkLoadTraceViewEvents(uploadTracesComponent);
229
230    await mediator.onWinscopeEvent(new AppRefreshDumpsRequest());
231    expect(collectTracesComponent.onWinscopeEvent).toHaveBeenCalled();
232  });
233
234  //TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with
235  //      Node.js (b/262269229). FileUtils#unzipFile() currently can't execute on Node.js.
236
237  //TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js
238  //      (b/262269229).
239
240  it('handles start download event from remote tool', async () => {
241    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0);
242
243    await mediator.onWinscopeEvent(new RemoteToolDownloadStart());
244    expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1);
245  });
246
247  it('handles empty downloaded files from remote tool', async () => {
248    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0);
249
250    // Pass files even if empty so that the upload component will update the progress bar
251    // and display error messages
252    await mediator.onWinscopeEvent(new RemoteToolFilesReceived([]));
253    expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1);
254  });
255
256  it('notifies overlay viewer of expanded timeline toggle change', async () => {
257    await loadFiles();
258    await loadTraceView();
259    const event = new ExpandedTimelineToggled(true);
260    await mediator.onWinscopeEvent(new ExpandedTimelineToggled(true));
261    expect(viewerOverlay.onWinscopeEvent).toHaveBeenCalledWith(event);
262  });
263
264  it('propagates trace position update', async () => {
265    await loadFiles();
266    await loadTraceView();
267
268    // notify position
269    resetSpyCalls();
270    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
271    checkTracePositionUpdateEvents(
272      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
273      POSITION_10,
274    );
275
276    // notify position
277    resetSpyCalls();
278    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_11));
279    checkTracePositionUpdateEvents(
280      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
281      POSITION_11,
282    );
283  });
284
285  it('propagates trace position update according to timezone', async () => {
286    const timezoneInfo: TimezoneInfo = {
287      timezone: 'Asia/Kolkata',
288      locale: 'en-US',
289    };
290    const converter = new TimestampConverter(timezoneInfo, 0n);
291    spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter);
292    await loadFiles();
293    await loadTraceView();
294
295    // notify position
296    resetSpyCalls();
297    const expectedPosition = TracePosition.fromTimestamp(
298      converter.makeTimestampFromRealNs(10n),
299    );
300    await mediator.onWinscopeEvent(new TracePositionUpdate(expectedPosition));
301    checkTracePositionUpdateEvents(
302      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
303      expectedPosition,
304      POSITION_10,
305    );
306  });
307
308  it('propagates trace position update and updates timeline data', async () => {
309    await loadFiles();
310    await loadTraceView();
311
312    // notify position
313    resetSpyCalls();
314    const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs();
315    const timestamp =
316      TimestampConverterUtils.makeRealTimestamp(finalTimestampNs);
317    const position = TracePosition.fromTimestamp(timestamp);
318
319    await mediator.onWinscopeEvent(new TracePositionUpdate(position, true));
320    checkTracePositionUpdateEvents(
321      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
322      position,
323    );
324    expect(
325      assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs(),
326    ).toEqual(finalTimestampNs);
327  });
328
329  it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
330    const dumpFile = await UnitTestUtils.getFixtureFile(
331      'traces/dump_WindowManager.pb',
332    );
333    await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
334
335    resetSpyCalls();
336    await mediator.onWinscopeEvent(new AppTraceViewRequest());
337    await checkLoadTraceViewEvents(uploadTracesComponent);
338  });
339
340  it('filters traces without visualization on loading viewers', async () => {
341    const fileWithoutVisualization = await UnitTestUtils.getFixtureFile(
342      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
343    );
344    await loadFiles();
345    await mediator.onWinscopeEvent(
346      new AppFilesUploaded([fileWithoutVisualization]),
347    );
348    await loadTraceView();
349  });
350
351  describe('timestamp received from remote tool', () => {
352    it('propagates trace position update', async () => {
353      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
354      await loadFiles();
355      await loadTraceView();
356
357      // receive timestamp
358      resetSpyCalls();
359      await mediator.onWinscopeEvent(
360        new RemoteToolTimestampReceived(() => TIMESTAMP_10),
361      );
362      checkTracePositionUpdateEvents(
363        [viewerStub0, viewerOverlay, timelineComponent],
364        POSITION_10,
365      );
366
367      // receive timestamp
368      resetSpyCalls();
369      await mediator.onWinscopeEvent(
370        new RemoteToolTimestampReceived(() => TIMESTAMP_11),
371      );
372      checkTracePositionUpdateEvents(
373        [viewerStub0, viewerOverlay, timelineComponent],
374        POSITION_11,
375      );
376    });
377
378    it("doesn't propagate timestamp back to remote tool", async () => {
379      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
380      await loadFiles();
381      await loadTraceView();
382
383      // receive timestamp
384      resetSpyCalls();
385      await mediator.onWinscopeEvent(
386        new RemoteToolTimestampReceived(() => TIMESTAMP_10),
387      );
388      checkTracePositionUpdateEvents([
389        viewerStub0,
390        viewerOverlay,
391        timelineComponent,
392      ]);
393    });
394
395    it('defers trace position propagation till traces are loaded and visualized', async () => {
396      // ensure converter has been used to create real timestamps
397      tracePipeline.getTimestampConverter().makeTimestampFromRealNs(0n);
398      // keep timestamp for later
399      await mediator.onWinscopeEvent(
400        new RemoteToolTimestampReceived(() => TIMESTAMP_10),
401      );
402      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
403
404      // keep timestamp for later (replace previous one)
405      await mediator.onWinscopeEvent(
406        new RemoteToolTimestampReceived(() => TIMESTAMP_11),
407      );
408      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
409
410      // apply timestamp
411      await loadFiles();
412      await loadTraceView();
413
414      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
415        makeExpectedTracePositionUpdate(POSITION_11),
416      );
417    });
418  });
419
420  describe('tab view switches', () => {
421    it('forwards switch notifications', async () => {
422      await loadFiles();
423      await loadTraceView();
424      resetSpyCalls();
425
426      const view = viewerStub1.getViews()[0];
427      await mediator.onWinscopeEvent(new TabbedViewSwitched(view));
428      expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
429        new ActiveTraceChanged(view.traces[0]),
430      );
431      const viewDump = viewerDump.getViews()[0];
432      await mediator.onWinscopeEvent(new TabbedViewSwitched(viewDump));
433      expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalledWith(
434        new ActiveTraceChanged(viewDump.traces[0]),
435      );
436    });
437
438    it('forwards switch requests from viewers to trace view component', async () => {
439      await loadFiles();
440      await loadTraceView();
441      expect(traceViewComponent.onWinscopeEvent).not.toHaveBeenCalled();
442
443      await viewerStub0.emitAppEventForTesting(
444        new TabbedViewSwitchRequest(traceSf),
445      );
446      expect(traceViewComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
447        new TabbedViewSwitchRequest(traceSf),
448      );
449    });
450  });
451
452  it('notifies only visible viewers about trace position updates', async () => {
453    await loadFiles();
454    await loadTraceView();
455
456    // Position update -> update only visible viewers
457    // Note: Viewer 0 is visible (gets focus) upon UI initialization
458    resetSpyCalls();
459    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
460    checkTracePositionUpdateEvents(
461      [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
462      POSITION_10,
463    );
464
465    // Tab switch -> update only newly visible viewers
466    // Note: overlay viewer is considered always visible
467    resetSpyCalls();
468    await mediator.onWinscopeEvent(
469      new TabbedViewSwitched(viewerStub1.getViews()[0]),
470    );
471    checkTracePositionUpdateEvents(
472      [viewerStub1, viewerOverlay, timelineComponent, crossToolProtocol],
473      undefined,
474      undefined,
475      true,
476    );
477    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledWith(
478      new ActiveTraceChanged(viewerStub1.getViews()[0].traces[0]),
479    );
480
481    // Position update -> update only visible viewers
482    // Note: overlay viewer is considered always visible
483    resetSpyCalls();
484    await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
485    checkTracePositionUpdateEvents([
486      viewerStub1,
487      viewerOverlay,
488      timelineComponent,
489      crossToolProtocol,
490    ]);
491  });
492
493  it('notifies timeline of active trace change', async () => {
494    expect(timelineComponent.onWinscopeEvent).not.toHaveBeenCalled();
495
496    await mediator.onWinscopeEvent(new ActiveTraceChanged(traceWm));
497    expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
498      new ActiveTraceChanged(traceWm),
499    );
500  });
501
502  async function loadFiles(files = inputFiles) {
503    await mediator.onWinscopeEvent(new AppFilesUploaded(files));
504    expect(userNotificationsListener.onNotifications).not.toHaveBeenCalled();
505    reassignViewerStubTrace(viewerStub0);
506    reassignViewerStubTrace(viewerStub1);
507  }
508
509  function reassignViewerStubTrace(viewerStub: ViewerStub) {
510    const viewerStubTraces = viewerStub.getViews()[0].traces;
511    viewerStubTraces[0] = tracePipeline
512      .getTraces()
513      .getTrace(viewerStubTraces[0].type) as Trace<object>;
514  }
515
516  async function loadTraceView() {
517    // Simulate "View traces" button click
518    resetSpyCalls();
519    await mediator.onWinscopeEvent(new AppTraceViewRequest());
520
521    await checkLoadTraceViewEvents(uploadTracesComponent);
522
523    // Simulate notification of TraceViewComponent about initially selected/focused tab
524    resetSpyCalls();
525    await mediator.onWinscopeEvent(
526      new TabbedViewSwitched(viewerStub0.getViews()[0]),
527    );
528
529    expect(viewerStub0.onWinscopeEvent).toHaveBeenCalledOnceWith(
530      makeExpectedTracePositionUpdate(),
531    );
532    expect(viewerStub1.onWinscopeEvent).not.toHaveBeenCalled();
533  }
534
535  async function checkLoadTraceViewEvents(progressListener: ProgressListener) {
536    expect(progressListener.onProgressUpdate).toHaveBeenCalled();
537    expect(progressListener.onOperationFinished).toHaveBeenCalled();
538    expect(timelineData.initialize).toHaveBeenCalledTimes(1);
539    expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
540      new ViewersLoaded([viewerStub0, viewerStub1, viewerOverlay, viewerDump]),
541    );
542
543    // Mediator triggers the viewers initialization
544    // by sending them a "trace position update" event
545    checkTracePositionUpdateEvents([
546      viewerStub0,
547      viewerStub1,
548      viewerOverlay,
549      viewerDump,
550      timelineComponent,
551    ]);
552  }
553
554  function checkTracePositionUpdateEvents(
555    listenersToBeNotified: WinscopeEventListener[],
556    position?: TracePosition,
557    crossToolProtocolPosition = position,
558    multipleTimelineEvents = false,
559  ) {
560    const event = makeExpectedTracePositionUpdate(position);
561    const crossToolProtocolEvent =
562      crossToolProtocolPosition !== position
563        ? makeExpectedTracePositionUpdate(crossToolProtocolPosition)
564        : event;
565    tracePositionUpdateListeners.forEach((listener) => {
566      const isVisible = listenersToBeNotified.includes(listener);
567      if (isVisible) {
568        const expected =
569          listener === crossToolProtocol ? crossToolProtocolEvent : event;
570        if (multipleTimelineEvents && listener === timelineComponent) {
571          expect(listener.onWinscopeEvent).toHaveBeenCalledWith(expected);
572        } else {
573          expect(listener.onWinscopeEvent).toHaveBeenCalledOnceWith(expected);
574        }
575      } else {
576        expect(listener.onWinscopeEvent).not.toHaveBeenCalled();
577      }
578    });
579  }
580
581  function resetSpyCalls() {
582    spies.forEach((spy) => {
583      spy.calls.reset();
584    });
585  }
586
587  function makeExpectedTracePositionUpdate(
588    tracePosition?: TracePosition,
589  ): WinscopeEvent {
590    if (tracePosition !== undefined) {
591      return new TracePositionUpdate(tracePosition);
592    }
593    return {type: WinscopeEventType.TRACE_POSITION_UPDATE} as WinscopeEvent;
594  }
595
596  function tracePositionUpdateEqualityTester(
597    first: any,
598    second: any,
599  ): boolean | undefined {
600    if (
601      first instanceof TracePositionUpdate &&
602      second instanceof TracePositionUpdate
603    ) {
604      return testTracePositionUpdates(first, second);
605    }
606    if (
607      first instanceof TracePositionUpdate &&
608      second.type === WinscopeEventType.TRACE_POSITION_UPDATE
609    ) {
610      return first.type === second.type;
611    }
612    return undefined;
613  }
614
615  function testTracePositionUpdates(
616    event: TracePositionUpdate,
617    expectedEvent: TracePositionUpdate,
618  ): boolean {
619    if (event.type !== expectedEvent.type) return false;
620    if (
621      event.position.timestamp.getValueNs() !==
622      expectedEvent.position.timestamp.getValueNs()
623    ) {
624      return false;
625    }
626    if (event.position.frame !== expectedEvent.position.frame) return false;
627    return true;
628  }
629});
630