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