1/* 2 * Copyright 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 ANYf 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/in_memory_storage'; 19import { 20 TabbedViewSwitchRequest, 21 TracePositionUpdate, 22} from 'messaging/winscope_event'; 23import {Transform} from 'parsers/surface_flinger/transform_utils'; 24import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; 25import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 26import {TracesBuilder} from 'test/unit/traces_builder'; 27import {TraceBuilder} from 'test/unit/trace_builder'; 28import {UnitTestUtils} from 'test/unit/utils'; 29import {CustomQueryType} from 'trace/custom_query'; 30import {Parser} from 'trace/parser'; 31import {Trace} from 'trace/trace'; 32import {Traces} from 'trace/traces'; 33import {TraceRectBuilder} from 'trace/trace_rect_builder'; 34import {TraceType} from 'trace/trace_type'; 35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 36import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 37import {NotifyLogViewCallbackType} from 'viewers/common/abstract_log_viewer_presenter'; 38import {AbstractLogViewerPresenterTest} from 'viewers/common/abstract_log_viewer_presenter_test'; 39import {VISIBLE_CHIP} from 'viewers/common/chip'; 40import {LogSelectFilter} from 'viewers/common/log_filters'; 41import {TextFilter} from 'viewers/common/text_filter'; 42import {LogField, LogHeader} from 'viewers/common/ui_data_log'; 43import {UserOptions} from 'viewers/common/user_options'; 44import {ViewerEvents} from 'viewers/common/viewer_events'; 45import {Presenter} from './presenter'; 46import {UiData} from './ui_data'; 47 48class PresenterInputTest extends AbstractLogViewerPresenterTest<UiData> { 49 override readonly expectedHeaders = [ 50 { 51 header: new LogHeader({ 52 name: 'Type', 53 cssClass: 'input-type inline', 54 }), 55 }, 56 { 57 header: new LogHeader({ 58 name: 'Source', 59 cssClass: 'input-source', 60 }), 61 }, 62 { 63 header: new LogHeader({ 64 name: 'Action', 65 cssClass: 'input-action', 66 }), 67 }, 68 { 69 header: new LogHeader({ 70 name: 'Device', 71 cssClass: 'input-device-id right-align', 72 }), 73 }, 74 { 75 header: new LogHeader({ 76 name: 'Display', 77 cssClass: 'input-display-id right-align', 78 }), 79 }, 80 { 81 header: new LogHeader({ 82 name: 'Details', 83 cssClass: 'input-details', 84 }), 85 }, 86 { 87 header: new LogHeader( 88 { 89 name: 'Target Windows', 90 cssClass: 'input-windows', 91 }, 92 new LogSelectFilter( 93 Array.from({length: 6}, () => ''), 94 true, 95 '100', 96 '100%', 97 ), 98 ), 99 options: [ 100 this.wrappedName('win-212'), 101 this.wrappedName('win-64'), 102 this.wrappedName('win-82'), 103 this.wrappedName('win-75'), 104 this.wrappedName('win-zero-not-98'), 105 this.wrappedName('98'), 106 ], 107 }, 108 ]; 109 private trace: Trace<PropertyTreeNode> | undefined; 110 private surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined; 111 private positionUpdate: TracePositionUpdate | undefined; 112 private layerIdToName: Array<{id: number; name: string}> = [ 113 {id: 0, name: 'win-zero-not-98'}, 114 {id: 212, name: 'win-212'}, 115 {id: 64, name: 'win-64'}, 116 {id: 82, name: 'win-82'}, 117 {id: 75, name: 'win-75'}, 118 // The layer name for window with id 98 is omitted to test incomplete mapping. 119 ]; 120 121 override async setUpTestEnvironment(): Promise<void> { 122 const parser = (await UnitTestUtils.getTracesParser([ 123 'traces/perfetto/input-events.perfetto-trace', 124 ])) as Parser<PropertyTreeNode>; 125 126 this.trace = new TraceBuilder<PropertyTreeNode>() 127 .setType(TraceType.INPUT_EVENT_MERGED) 128 .setParser(parser) 129 .build(); 130 131 this.surfaceFlingerTrace = new TraceBuilder<HierarchyTreeNode>() 132 .setType(TraceType.SURFACE_FLINGER) 133 .setEntries([]) 134 .setParserCustomQueryResult( 135 CustomQueryType.SF_LAYERS_ID_AND_NAME, 136 this.layerIdToName, 137 ) 138 .build(); 139 140 this.positionUpdate = TracePositionUpdate.fromTraceEntry( 141 this.trace.getEntry(0), 142 ); 143 } 144 145 override async createPresenterWithEmptyTrace( 146 callback: NotifyLogViewCallbackType<UiData>, 147 ): Promise<Presenter> { 148 const traces = new TracesBuilder() 149 .setEntries(TraceType.INPUT_EVENT_MERGED, []) 150 .build(); 151 if (this.surfaceFlingerTrace !== undefined) { 152 traces.addTrace(this.surfaceFlingerTrace); 153 } 154 return PresenterInputTest.createPresenterWithTraces(traces, callback); 155 } 156 157 override async createPresenter( 158 callback: NotifyLogViewCallbackType<UiData>, 159 ): Promise<Presenter> { 160 const traces = new Traces(); 161 traces.addTrace(assertDefined(this.trace)); 162 if (this.surfaceFlingerTrace !== undefined) { 163 traces.addTrace(this.surfaceFlingerTrace); 164 } 165 const presenter = PresenterInputTest.createPresenterWithTraces( 166 traces, 167 callback, 168 ); 169 await presenter.onAppEvent(this.getPositionUpdate()); // trigger initialization 170 return presenter; 171 } 172 173 private static createPresenterWithTraces( 174 traces: Traces, 175 callback: NotifyLogViewCallbackType<UiData>, 176 ): Presenter { 177 return new Presenter( 178 traces, 179 assertDefined(traces.getTrace(TraceType.INPUT_EVENT_MERGED)), 180 new InMemoryStorage(), 181 callback, 182 ); 183 } 184 185 override getPositionUpdate(): TracePositionUpdate { 186 return assertDefined(this.positionUpdate); 187 } 188 189 override executePropertiesChecksForEmptyTrace(uiData: UiData) { 190 expect(uiData.highlightedProperty).toBeFalsy(); 191 expect(uiData.dispatchPropertiesTree).toBeUndefined(); 192 expect(uiData.dispatchPropertiesFilter).toBeDefined(); 193 } 194 195 override executePropertiesChecksAfterPositionUpdate(uiData: UiData): void { 196 expect(uiData.entries.length).toEqual(8); 197 expect(uiData.currentIndex).toEqual(0); 198 expect(uiData.selectedIndex).toBeUndefined(); 199 const curEntry = uiData.entries[0]; 200 const expectedFields: LogField[] = [ 201 { 202 spec: uiData.headers[0].spec, 203 value: 'MOTION', 204 propagateEntryTimestamp: true, 205 }, 206 {spec: uiData.headers[1].spec, value: 'TOUCHSCREEN'}, 207 {spec: uiData.headers[2].spec, value: 'DOWN'}, 208 {spec: uiData.headers[3].spec, value: 4}, 209 {spec: uiData.headers[4].spec, value: 0}, 210 {spec: uiData.headers[5].spec, value: '[212, 64, 82, 75]'}, 211 { 212 spec: uiData.headers[6].spec, 213 value: [ 214 this.wrappedName('win-212'), 215 this.wrappedName('win-64'), 216 this.wrappedName('win-82'), 217 this.wrappedName('win-75'), 218 this.wrappedName('win-zero-not-98'), 219 ].join(', '), 220 }, 221 ]; 222 expectedFields.forEach((field) => { 223 expect(curEntry.fields).toContain(field); 224 }); 225 226 const motionEvent = assertDefined(uiData.propertiesTree); 227 expect(motionEvent.getChildByName('eventId')?.getValue()).toEqual( 228 330184796, 229 ); 230 expect(motionEvent.getChildByName('action')?.formattedValue()).toEqual( 231 'ACTION_DOWN', 232 ); 233 234 const dispatchProperties = assertDefined(uiData.dispatchPropertiesTree); 235 expect(dispatchProperties.getAllChildren().length).toEqual(5); 236 237 expect( 238 dispatchProperties 239 .getChildByName('0') 240 ?.getChildByName('windowId') 241 ?.getDisplayName(), 242 ).toEqual('TargetWindow'); 243 expect( 244 dispatchProperties 245 .getChildByName('0') 246 ?.getChildByName('windowId') 247 ?.formattedValue(), 248 ).toEqual('212 - win-212'); 249 } 250 251 private expectEventPresented( 252 uiData: UiData, 253 eventId: number, 254 action: string, 255 ) { 256 const properties = assertDefined(uiData.propertiesTree); 257 expect(properties.getChildByName('action')?.formattedValue()).toEqual( 258 action, 259 ); 260 expect(properties.getChildByName('eventId')?.getValue()).toEqual(eventId); 261 } 262 263 override executeSpecializedTests() { 264 describe('Specialized tests', () => { 265 const time0 = TimestampConverterUtils.makeRealTimestamp(0n); 266 const time10 = TimestampConverterUtils.makeRealTimestamp(10n); 267 const time19 = TimestampConverterUtils.makeRealTimestamp(19n); 268 const time20 = TimestampConverterUtils.makeRealTimestamp(20n); 269 const time25 = TimestampConverterUtils.makeRealTimestamp(25n); 270 const time30 = TimestampConverterUtils.makeRealTimestamp(30n); 271 const time35 = TimestampConverterUtils.makeRealTimestamp(35n); 272 const time36 = TimestampConverterUtils.makeRealTimestamp(36n); 273 const layerRect = new TraceRectBuilder() 274 .setX(0) 275 .setY(0) 276 .setWidth(1) 277 .setHeight(1) 278 .setId('layerRect') 279 .setName('layerRect') 280 .setCornerRadius(0) 281 .setTransform(Transform.EMPTY.matrix) 282 .setDepth(1) 283 .setGroupId(0) 284 .setIsVisible(true) 285 .setOpacity(1) 286 .setIsDisplay(false) 287 .setIsSpy(false) 288 .build(); 289 const inputRect = new TraceRectBuilder() 290 .setX(2) 291 .setY(2) 292 .setWidth(3) 293 .setHeight(3) 294 .setId('inputRect') 295 .setName('inputRect') 296 .setCornerRadius(0) 297 .setTransform(Transform.EMPTY.matrix) 298 .setDepth(1) 299 .setGroupId(0) 300 .setIsVisible(true) 301 .setOpacity(1) 302 .setIsDisplay(false) 303 .setIsSpy(false) 304 .build(); 305 const sfEntry0 = new HierarchyTreeBuilder() 306 .setId('LayerTraceEntry') 307 .setName('root') 308 .setChildren([ 309 { 310 id: 1, 311 name: 'layer1', 312 rects: [layerRect], 313 secondaryRects: [inputRect], 314 }, 315 ]) 316 .build(); 317 const sfEntry1 = new HierarchyTreeBuilder() 318 .setId('LayerTraceEntry') 319 .setName('root') 320 .setChildren([ 321 { 322 id: 1, 323 name: 'layer1', 324 rects: [layerRect], 325 secondaryRects: [inputRect], 326 children: [ 327 { 328 id: 2, 329 name: 'layer2', 330 rects: [layerRect], 331 secondaryRects: [inputRect], 332 }, 333 ], 334 }, 335 ]) 336 .build(); 337 const sfEntry2 = new HierarchyTreeBuilder() 338 .setId('LayerTraceEntry') 339 .setName('root') 340 .setChildren([ 341 { 342 id: 1, 343 name: 'layer1', 344 rects: [layerRect], 345 secondaryRects: [inputRect], 346 children: [ 347 { 348 id: 2, 349 name: 'layer2', 350 rects: [layerRect], 351 secondaryRects: [inputRect], 352 }, 353 ], 354 }, 355 { 356 id: 3, 357 name: 'layer3', 358 rects: [layerRect], 359 secondaryRects: [inputRect], 360 }, 361 ]) 362 .build(); 363 364 let uiData: UiData = UiData.createEmpty(); 365 366 beforeEach(async () => { 367 uiData = UiData.createEmpty(); 368 await this.setUpTestEnvironment(); 369 }); 370 371 it('adds events listeners', async () => { 372 const element = document.createElement('div'); 373 const presenter = await this.createPresenter( 374 (uiDataLog) => (uiData = uiDataLog as UiData), 375 ); 376 presenter.addEventListeners(element); 377 378 const testId = 'testId'; 379 380 let spy: jasmine.Spy = spyOn(presenter, 'onHighlightedPropertyChange'); 381 element.dispatchEvent( 382 new CustomEvent(ViewerEvents.HighlightedPropertyChange, { 383 detail: {id: testId}, 384 }), 385 ); 386 expect(spy).toHaveBeenCalledWith(testId); 387 388 spy = spyOn(presenter, 'onHighlightedIdChange'); 389 element.dispatchEvent( 390 new CustomEvent(ViewerEvents.HighlightedIdChange, { 391 detail: {id: testId}, 392 }), 393 ); 394 expect(spy).toHaveBeenCalledWith(testId); 395 396 spy = spyOn(presenter, 'onRectsUserOptionsChange'); 397 const userOptions = {}; 398 element.dispatchEvent( 399 new CustomEvent(ViewerEvents.RectsUserOptionsChange, { 400 detail: {userOptions}, 401 }), 402 ); 403 expect(spy).toHaveBeenCalledWith(userOptions); 404 405 spy = spyOn(presenter, 'onRectDoubleClick'); 406 element.dispatchEvent(new CustomEvent(ViewerEvents.RectsDblClick)); 407 expect(spy).toHaveBeenCalled(); 408 409 spy = spyOn(presenter, 'onDispatchPropertiesFilterChange'); 410 const filter = new TextFilter(); 411 element.dispatchEvent( 412 new CustomEvent(ViewerEvents.DispatchPropertiesFilterChange, { 413 detail: filter, 414 }), 415 ); 416 expect(spy).toHaveBeenCalledWith(filter); 417 }); 418 419 it('updates selected entry', async () => { 420 const presenter = await this.createPresenter( 421 (uiDataLog) => (uiData = uiDataLog as UiData), 422 ); 423 424 const keyEntry = assertDefined(this.trace).getEntry(7); 425 await presenter.onAppEvent( 426 TracePositionUpdate.fromTraceEntry(keyEntry), 427 ); 428 429 this.expectEventPresented(uiData, 894093732, 'ACTION_UP'); 430 431 const motionEntry = assertDefined(this.trace).getEntry(1); 432 await presenter.onAppEvent( 433 TracePositionUpdate.fromTraceEntry(motionEntry), 434 ); 435 436 this.expectEventPresented(uiData, 1327679296, 'ACTION_OUTSIDE'); 437 438 const motionDispatchProperties = assertDefined( 439 uiData.dispatchPropertiesTree, 440 ); 441 expect(motionDispatchProperties.getAllChildren().length).toEqual(1); 442 expect( 443 motionDispatchProperties 444 .getChildByName('0') 445 ?.getChildByName('windowId') 446 ?.getValue(), 447 ).toEqual(98n); 448 }); 449 450 it('finds entry by time', async () => { 451 const traces = new Traces(); 452 traces.addTrace(assertDefined(this.trace)); 453 454 const lastMotion = await assertDefined(this.trace).getEntry(5); 455 const firstKey = await assertDefined(this.trace).getEntry(6); 456 const diffNs = 457 firstKey.getTimestamp().getValueNs() - 458 lastMotion.getTimestamp().getValueNs(); 459 const belowLastMotionTime = lastMotion.getTimestamp().minus(1n); 460 const midpointTime = lastMotion.getTimestamp().add(diffNs / 2n); 461 const aboveFirstKeyTime = firstKey.getTimestamp().add(1n); 462 463 const otherTrace = new TraceBuilder<string>() 464 .setType(TraceType.TEST_TRACE_STRING) 465 .setEntries(['event-log-00', 'event-log-01', 'event-log-02']) 466 .setTimestamps([belowLastMotionTime, midpointTime, aboveFirstKeyTime]) 467 .build(); 468 traces.addTrace(otherTrace); 469 const presenter = PresenterInputTest.createPresenterWithTraces( 470 traces, 471 (uiDataLog) => (uiData = uiDataLog as UiData), 472 ); 473 474 await presenter.onAppEvent( 475 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(0)), 476 ); 477 this.expectEventPresented(uiData, 313395000, 'ACTION_MOVE'); 478 479 await presenter.onAppEvent( 480 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(1)), 481 ); 482 this.expectEventPresented(uiData, 436499943, 'ACTION_UP'); 483 484 await presenter.onAppEvent( 485 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(2)), 486 ); 487 this.expectEventPresented(uiData, 759309047, 'ACTION_DOWN'); 488 }); 489 490 it('finds closest input event by frame', async () => { 491 const parser = assertDefined(this.trace).getParser(); 492 const traces = new Traces(); 493 494 // FRAME: 0 1 2 495 // TEST(time): 0 19 35 496 // INPUT(time): 10 20,25 30,36 497 const trace = new TraceBuilder<PropertyTreeNode>() 498 .setType(TraceType.INPUT_EVENT_MERGED) 499 .setEntries([ 500 await parser.getEntry(0), 501 await parser.getEntry(1), 502 await parser.getEntry(2), 503 await parser.getEntry(3), 504 await parser.getEntry(4), 505 ]) 506 .setTimestamps([time10, time20, time25, time30, time36]) 507 .setFrame(0, 0) 508 .setFrame(1, 1) 509 .setFrame(2, 1) 510 .setFrame(3, 2) 511 .setFrame(4, 2) 512 .build(); 513 traces.addTrace(trace); 514 515 const otherTrace = new TraceBuilder<string>() 516 .setType(TraceType.TEST_TRACE_STRING) 517 .setEntries(['sf-event-00', 'sf-event-01', 'sf-event-02']) 518 .setTimestamps([time0, time19, time35]) 519 .setFrame(0, 0) 520 .setFrame(1, 1) 521 .setFrame(2, 2) 522 .build(); 523 traces.addTrace(otherTrace); 524 525 const presenter = PresenterInputTest.createPresenterWithTraces( 526 traces, 527 (uiDataLog) => (uiData = uiDataLog as UiData), 528 ); 529 530 await presenter.onAppEvent( 531 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(0)), 532 ); 533 this.expectEventPresented(uiData, 330184796, 'ACTION_DOWN'); 534 535 await presenter.onAppEvent( 536 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(1)), 537 ); 538 this.expectEventPresented(uiData, 1327679296, 'ACTION_OUTSIDE'); 539 540 await presenter.onAppEvent( 541 TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(2)), 542 ); 543 this.expectEventPresented(uiData, 106022695, 'ACTION_MOVE'); 544 }); 545 546 it('no rects defined without SF trace', async () => { 547 this.surfaceFlingerTrace = undefined; 548 549 const presenter = await this.createPresenter( 550 (uiDataLog) => (uiData = uiDataLog as UiData), 551 ); 552 await presenter.onAppEvent(this.getPositionUpdate()); 553 expect(uiData.rectsToDraw).toBeUndefined(); 554 }); 555 556 it('empty trace no rects defined without SF trace', async () => { 557 this.surfaceFlingerTrace = undefined; 558 559 const presenter = await this.createPresenterWithEmptyTrace( 560 (uiDataLog) => (uiData = uiDataLog as UiData), 561 ); 562 await presenter.onAppEvent(this.getPositionUpdate()); 563 expect(uiData.rectsToDraw).toBeUndefined(); 564 }); 565 566 it('rects defined with SF trace', async () => { 567 assertDefined(this.surfaceFlingerTrace); 568 const presenter = await this.createPresenter( 569 (uiDataLog) => (uiData = uiDataLog as UiData), 570 ); 571 await presenter.onAppEvent(this.getPositionUpdate()); 572 expect(uiData.rectsToDraw).toBeDefined(); 573 expect(uiData.rectsToDraw).toEqual([]); 574 }); 575 576 it('empty trace rects defined with SF trace', async () => { 577 assertDefined(this.surfaceFlingerTrace); 578 const presenter = await this.createPresenterWithEmptyTrace( 579 (uiDataLog) => (uiData = uiDataLog as UiData), 580 ); 581 await presenter.onAppEvent(this.getPositionUpdate()); 582 expect(uiData.rectsToDraw).toBeDefined(); 583 expect(uiData.rectsToDraw).toEqual([]); 584 }); 585 586 it('extracts corresponding input rects from SF trace', async () => { 587 const parser = assertDefined(this.trace).getParser(); 588 const traces = await getTracesWithSf(parser, this.layerIdToName); 589 const trace = assertDefined( 590 traces.getTrace(TraceType.INPUT_EVENT_MERGED), 591 ); 592 593 const presenter = PresenterInputTest.createPresenterWithTraces( 594 traces, 595 (uiDataLog) => (uiData = uiDataLog as UiData), 596 ); 597 598 await presenter.onAppEvent( 599 TracePositionUpdate.fromTraceEntry(trace.getEntry(0)), 600 ); 601 expect(uiData.rectsToDraw).toEqual([]); 602 603 await presenter.onAppEvent( 604 TracePositionUpdate.fromTraceEntry(trace.getEntry(1)), 605 ); 606 expect(uiData.rectsToDraw).toHaveSize(1); 607 expect(uiData.rectsToDraw?.at(0)?.id).toEqual('inputRect'); 608 609 await presenter.onAppEvent( 610 TracePositionUpdate.fromTraceEntry(trace.getEntry(2)), 611 ); 612 expect(uiData.rectsToDraw).toHaveSize(1); 613 expect(uiData.rectsToDraw?.at(0)?.id).toEqual('inputRect'); 614 615 await presenter.onAppEvent( 616 TracePositionUpdate.fromTraceEntry(trace.getEntry(3)), 617 ); 618 expect(uiData.rectsToDraw).toHaveSize(3); 619 uiData.rectsToDraw?.forEach((rect) => 620 expect(rect.id).toEqual('inputRect'), 621 ); 622 }); 623 624 it('filters dispatch properties tree', async () => { 625 const presenter = await this.createPresenter( 626 (uiDataLog) => (uiData = uiDataLog as UiData), 627 ); 628 await presenter.onAppEvent(this.getPositionUpdate()); 629 await presenter.onLogEntryClick(3); 630 expect( 631 assertDefined(uiData.dispatchPropertiesTree).getAllChildren().length, 632 ).toEqual(5); 633 await presenter.onDispatchPropertiesFilterChange(new TextFilter('212')); 634 expect( 635 assertDefined(uiData.dispatchPropertiesTree).getAllChildren().length, 636 ).toEqual(1); 637 }); 638 639 it('updates highlighted property', async () => { 640 const presenter = await this.createPresenter( 641 (uiDataLog) => (uiData = uiDataLog as UiData), 642 ); 643 expect(uiData.highlightedProperty).toEqual(''); 644 const id = '4'; 645 presenter.onHighlightedPropertyChange(id); 646 expect(uiData.highlightedProperty).toEqual(id); 647 presenter.onHighlightedPropertyChange(id); 648 expect(uiData.highlightedProperty).toEqual(''); 649 }); 650 651 it('updates highlighted rect', async () => { 652 const parser = assertDefined(this.trace).getParser(); 653 const traces = await getTracesWithSf(parser, this.layerIdToName); 654 const trace = assertDefined( 655 traces.getTrace(TraceType.INPUT_EVENT_MERGED), 656 ); 657 const presenter = PresenterInputTest.createPresenterWithTraces( 658 traces, 659 (uiDataLog) => (uiData = uiDataLog as UiData), 660 ); 661 await presenter.onAppEvent( 662 TracePositionUpdate.fromTraceEntry(trace.getEntry(1)), 663 ); 664 expect(uiData.rectsToDraw).toHaveSize(1); 665 666 const rect = assertDefined(uiData.rectsToDraw)[0]; 667 await presenter.onHighlightedIdChange(rect.id); 668 expect(uiData.highlightedRect).toEqual(rect.id); 669 await presenter.onHighlightedIdChange(rect.id); 670 expect(uiData.highlightedRect).toEqual(''); 671 }); 672 673 it('filters rects by having content or visibility', async () => { 674 const userOptions: UserOptions = { 675 showOnlyVisible: { 676 name: 'Show only', 677 chip: VISIBLE_CHIP, 678 enabled: false, 679 }, 680 showOnlyWithContent: { 681 name: 'Has input', 682 icon: 'pan_tool_alt', 683 enabled: true, 684 }, 685 }; 686 const parser = assertDefined(this.trace).getParser(); 687 const traces = await getTracesWithSf(parser, this.layerIdToName); 688 const trace = assertDefined( 689 traces.getTrace(TraceType.INPUT_EVENT_MERGED), 690 ); 691 const presenter = PresenterInputTest.createPresenterWithTraces( 692 traces, 693 (uiDataLog) => (uiData = uiDataLog as UiData), 694 ); 695 await presenter.onAppEvent( 696 TracePositionUpdate.fromTraceEntry(trace.getEntry(1)), 697 ); 698 expect(uiData.rectsToDraw).toHaveSize(1); 699 700 await presenter.onRectsUserOptionsChange(userOptions); 701 expect(uiData.rectsUserOptions).toEqual(userOptions); 702 expect(uiData.rectsToDraw).toHaveSize(0); 703 704 userOptions['showOnlyVisible'].enabled = true; 705 userOptions['showOnlyWithContent'].enabled = false; 706 await presenter.onRectsUserOptionsChange(userOptions); 707 expect(uiData.rectsToDraw).toHaveSize(1); 708 }); 709 710 it('emits event on rect double click', async () => { 711 const presenter = await this.createPresenter( 712 (uiDataLog) => (uiData = uiDataLog as UiData), 713 ); 714 const spy = jasmine.createSpy(); 715 presenter.setEmitEvent(spy); 716 await presenter.onRectDoubleClick(); 717 expect(spy).toHaveBeenCalledWith( 718 new TabbedViewSwitchRequest(assertDefined(this.surfaceFlingerTrace)), 719 ); 720 }); 721 722 async function getTracesWithSf( 723 parser: Parser<PropertyTreeNode>, 724 layerIdToName: Array<{ 725 id: number; 726 name: string; 727 }>, 728 ) { 729 const traces = new Traces(); 730 731 // FRAME: 0 1 2 3 732 // INPUT(index): 0 1,2 - 3 733 // SF(index): - 0 1 2 734 const trace = new TraceBuilder<PropertyTreeNode>() 735 .setType(TraceType.INPUT_EVENT_MERGED) 736 .setEntries([ 737 await parser.getEntry(0), 738 await parser.getEntry(1), 739 await parser.getEntry(2), 740 await parser.getEntry(3), 741 ]) 742 .setTimestamps([time10, time20, time25, time30]) 743 .setFrame(0, 0) 744 .setFrame(1, 1) 745 .setFrame(2, 1) 746 .setFrame(3, 3) 747 .build(); 748 traces.addTrace(trace); 749 750 const sfTrace = new TraceBuilder<HierarchyTreeNode>() 751 .setType(TraceType.SURFACE_FLINGER) 752 .setEntries([sfEntry0, sfEntry1, sfEntry2]) 753 .setTimestamps([time0, time19, time35]) 754 .setFrame(0, 1) 755 .setFrame(1, 2) 756 .setFrame(2, 3) 757 .setParserCustomQueryResult( 758 CustomQueryType.SF_LAYERS_ID_AND_NAME, 759 layerIdToName, 760 ) 761 .build(); 762 traces.addTrace(sfTrace); 763 return traces; 764 } 765 }); 766 } 767 768 private wrappedName(name: string): string { 769 return `\u{200C}${name}\u{200C}`; 770 } 771} 772 773describe('PresenterInput', async () => { 774 new PresenterInputTest().execute(); 775}); 776