1/* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {assertDefined} from 'common/assert_utils'; 18import {IDENTITY_MATRIX} from 'common/geometry/transform_matrix'; 19import {InMemoryStorage} from 'common/store/in_memory_storage'; 20import {TimestampConverterUtils} from 'common/time/test_utils'; 21import { 22 DarkModeToggled, 23 FilterPresetApplyRequest, 24 FilterPresetSaveRequest, 25 TracePositionUpdate, 26} from 'messaging/winscope_event'; 27import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; 28import {MockPresenter} from 'test/unit/mock_hierarchy_viewer_presenter'; 29import {TraceBuilder} from 'test/unit/trace_builder'; 30import {TreeNodeUtils} from 'test/unit/tree_node_utils'; 31import {UnitTestUtils} from 'test/unit/utils'; 32import {Trace} from 'trace/trace'; 33import {Traces} from 'trace/traces'; 34import {TraceType} from 'trace/trace_type'; 35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 36import {TextFilter} from 'viewers/common/text_filter'; 37import {UiRectBuilder} from 'viewers/components/rects/ui_rect_builder'; 38import {DiffType} from './diff_type'; 39import {RectShowState} from './rect_show_state'; 40import {UiDataHierarchy} from './ui_data_hierarchy'; 41import {UiHierarchyTreeNode} from './ui_hierarchy_tree_node'; 42import {UserOptions} from './user_options'; 43import {ViewerEvents} from './viewer_events'; 44 45describe('AbstractHierarchyViewerPresenter', () => { 46 const timestamp1 = TimestampConverterUtils.makeElapsedTimestamp(1n); 47 const timestamp2 = TimestampConverterUtils.makeElapsedTimestamp(2n); 48 let uiData: UiDataHierarchy; 49 let presenter: MockPresenter; 50 let trace: Trace<HierarchyTreeNode>; 51 let traces: Traces; 52 let positionUpdate: TracePositionUpdate; 53 let secondPositionUpdate: TracePositionUpdate; 54 let selectedTree: UiHierarchyTreeNode; 55 let storage: InMemoryStorage; 56 57 beforeAll(async () => { 58 jasmine.addCustomEqualityTester(TreeNodeUtils.treeNodeEqualityTester); 59 trace = new TraceBuilder<HierarchyTreeNode>() 60 .setType(TraceType.SURFACE_FLINGER) 61 .setEntries([ 62 new HierarchyTreeBuilder() 63 .setId('Test Trace') 64 .setName('entry') 65 .setChildren([ 66 { 67 id: '1', 68 name: 'p1', 69 properties: {isComputedVisible: true, testProp: true}, 70 children: [ 71 {id: '3', name: 'c3', properties: {isComputedVisible: true}}, 72 ], 73 }, 74 {id: '2', name: 'p2', properties: {isComputedVisible: false}}, 75 ]) 76 .build(), 77 new HierarchyTreeBuilder() 78 .setId('Test Trace') 79 .setName('entry') 80 .setChildren([ 81 { 82 id: '1', 83 name: 'p1', 84 properties: {isComputedVisible: true, testProp: false}, 85 }, 86 {id: '2', name: 'p2'}, 87 ]) 88 .build(), 89 ]) 90 .setTimestamps([timestamp1, timestamp2]) 91 .build(); 92 selectedTree = UiHierarchyTreeNode.from( 93 assertDefined((await trace.getEntry(0).getValue()).getChildByName('p1')), 94 ); 95 positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0)); 96 secondPositionUpdate = TracePositionUpdate.fromTraceEntry( 97 trace.getEntry(1), 98 ); 99 traces = new Traces(); 100 traces.addTrace(trace); 101 }); 102 103 beforeEach(() => { 104 storage = new InMemoryStorage(); 105 presenter = new MockPresenter( 106 trace, 107 traces, 108 storage, 109 (newData) => { 110 uiData = newData; 111 }, 112 undefined, 113 ); 114 }); 115 116 it('clears ui data before throwing error on corrupted trace', async () => { 117 const notifyViewCallback = (newData: UiDataHierarchy) => { 118 uiData = newData; 119 }; 120 const trace = new TraceBuilder<HierarchyTreeNode>() 121 .setType(TraceType.SURFACE_FLINGER) 122 .setEntries([selectedTree]) 123 .setTimestamps([timestamp1]) 124 .setIsCorrupted(true) 125 .build(); 126 const traces = new Traces(); 127 traces.addTrace(trace); 128 const presenter = new MockPresenter( 129 trace, 130 traces, 131 new InMemoryStorage(), 132 notifyViewCallback, 133 undefined, 134 ); 135 initializeRectsPresenter(presenter); 136 137 try { 138 await presenter.onAppEvent( 139 TracePositionUpdate.fromTraceEntry(trace.getEntry(0)), 140 ); 141 fail('error should be thrown for corrupted trace'); 142 } catch (e) { 143 expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan( 144 0, 145 ); 146 expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan( 147 0, 148 ); 149 expect(uiData.hierarchyTrees).toBeUndefined(); 150 expect(uiData.propertiesTree).toBeUndefined(); 151 expect(uiData.highlightedItem).toEqual(''); 152 expect(uiData.highlightedProperty).toEqual(''); 153 expect(uiData.pinnedItems.length).toEqual(0); 154 expect( 155 Object.keys(assertDefined(uiData?.rectsUserOptions)).length, 156 ).toBeGreaterThan(0); 157 expect(uiData.rectsToDraw).toEqual([]); 158 } 159 }); 160 161 it('processes trace position updates', async () => { 162 initializeRectsPresenter(); 163 pinNode(selectedTree); 164 await presenter.onAppEvent(positionUpdate); 165 166 expect(uiData.highlightedItem?.length).toEqual(0); 167 expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0); 168 expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0); 169 assertDefined(uiData.hierarchyTrees).forEach((tree) => { 170 expect(tree.getAllChildren().length > 0).toBeTrue(); 171 }); 172 expect(uiData.pinnedItems.length).toBeGreaterThan(0); 173 expect( 174 Object.keys(assertDefined(uiData.rectsUserOptions)).length, 175 ).toBeGreaterThan(0); 176 expect(uiData.rectsToDraw?.length).toBeGreaterThan(0); 177 expect(uiData.displays?.length).toBeGreaterThan(0); 178 }); 179 180 it('adds event listeners', () => { 181 const element = document.createElement('div'); 182 presenter.addEventListeners(element); 183 184 let spy: jasmine.Spy = spyOn(presenter, 'onPinnedItemChange'); 185 const node = TreeNodeUtils.makeUiHierarchyNode({name: 'test'}); 186 element.dispatchEvent( 187 new CustomEvent(ViewerEvents.HierarchyPinnedChange, { 188 detail: {pinnedItem: node}, 189 }), 190 ); 191 expect(spy).toHaveBeenCalledWith(node); 192 193 spy = spyOn(presenter, 'onHighlightedIdChange'); 194 element.dispatchEvent( 195 new CustomEvent(ViewerEvents.HighlightedIdChange, { 196 detail: {id: 'test'}, 197 }), 198 ); 199 expect(spy).toHaveBeenCalledWith('test'); 200 201 spy = spyOn(presenter, 'onHighlightedPropertyChange'); 202 element.dispatchEvent( 203 new CustomEvent(ViewerEvents.HighlightedPropertyChange, { 204 detail: {id: 'test'}, 205 }), 206 ); 207 expect(spy).toHaveBeenCalledWith('test'); 208 209 spy = spyOn(presenter, 'onHierarchyUserOptionsChange'); 210 element.dispatchEvent( 211 new CustomEvent(ViewerEvents.HierarchyUserOptionsChange, { 212 detail: {userOptions: {}}, 213 }), 214 ); 215 expect(spy).toHaveBeenCalledWith({}); 216 217 spy = spyOn(presenter, 'onHierarchyFilterChange'); 218 const filter = new TextFilter(); 219 element.dispatchEvent( 220 new CustomEvent(ViewerEvents.HierarchyFilterChange, {detail: filter}), 221 ); 222 expect(spy).toHaveBeenCalledWith(filter); 223 224 spy = spyOn(presenter, 'onPropertiesUserOptionsChange'); 225 element.dispatchEvent( 226 new CustomEvent(ViewerEvents.PropertiesUserOptionsChange, { 227 detail: {userOptions: {}}, 228 }), 229 ); 230 expect(spy).toHaveBeenCalledWith({}); 231 232 spy = spyOn(presenter, 'onPropertiesFilterChange'); 233 element.dispatchEvent( 234 new CustomEvent(ViewerEvents.PropertiesFilterChange, { 235 detail: filter, 236 }), 237 ); 238 expect(spy).toHaveBeenCalledWith(filter); 239 240 spy = spyOn(presenter, 'onHighlightedNodeChange'); 241 element.dispatchEvent( 242 new CustomEvent(ViewerEvents.HighlightedNodeChange, {detail: {node}}), 243 ); 244 expect(spy).toHaveBeenCalledWith(node); 245 246 spy = spyOn(presenter, 'onRectShowStateChange'); 247 element.dispatchEvent( 248 new CustomEvent(ViewerEvents.RectShowStateChange, { 249 detail: {rectId: 'test', state: RectShowState.HIDE}, 250 }), 251 ); 252 expect(spy).toHaveBeenCalledWith('test', RectShowState.HIDE); 253 254 spy = spyOn(presenter, 'onRectsUserOptionsChange'); 255 element.dispatchEvent( 256 new CustomEvent(ViewerEvents.RectsUserOptionsChange, { 257 detail: {userOptions: {}}, 258 }), 259 ); 260 expect(spy).toHaveBeenCalledWith({}); 261 262 spy = spyOn(presenter, 'onArrowPress'); 263 element.dispatchEvent( 264 new CustomEvent(ViewerEvents.ArrowDownPress, {detail: storage}), 265 ); 266 expect(spy).toHaveBeenCalledWith(storage, false); 267 268 element.dispatchEvent( 269 new CustomEvent(ViewerEvents.ArrowUpPress, {detail: storage}), 270 ); 271 expect(spy).toHaveBeenCalledWith(storage, true); 272 }); 273 274 it('is robust to empty trace', async () => { 275 const callback = (newData: UiDataHierarchy) => { 276 uiData = newData; 277 }; 278 const trace = UnitTestUtils.makeEmptyTrace(TraceType.WINDOW_MANAGER); 279 const traces = new Traces(); 280 traces.addTrace(trace); 281 const presenter = new MockPresenter( 282 trace, 283 traces, 284 new InMemoryStorage(), 285 callback, 286 undefined, 287 ); 288 presenter.initializeRectsPresenter(); 289 290 const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp( 291 TimestampConverterUtils.makeRealTimestamp(0n), 292 ); 293 await presenter.onAppEvent(positionUpdateWithoutTraceEntry); 294 295 expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0); 296 expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0); 297 expect(uiData.hierarchyTrees).toBeUndefined(); 298 expect( 299 Object.keys(assertDefined(uiData?.rectsUserOptions)).length, 300 ).toBeGreaterThan(0); 301 }); 302 303 it('handles filter preset requests', async () => { 304 initializeRectsPresenter(); 305 await presenter.onAppEvent(positionUpdate); 306 const saveEvent = new FilterPresetSaveRequest( 307 'TestPreset', 308 TraceType.TEST_TRACE_STRING, 309 ); 310 expect(storage.get(saveEvent.name)).toBeUndefined(); 311 await presenter.onAppEvent(saveEvent); 312 expect(storage.get(saveEvent.name)).toBeDefined(); 313 314 await presenter.onHierarchyFilterChange(new TextFilter('Test Filter')); 315 await presenter.onHierarchyUserOptionsChange({}); 316 await presenter.onPropertiesUserOptionsChange({}); 317 await presenter.onPropertiesFilterChange(new TextFilter('Test Filter')); 318 presenter.onRectsUserOptionsChange({}); 319 await presenter.onRectShowStateChange( 320 assertDefined(uiData.rectsToDraw)[0].id, 321 RectShowState.HIDE, 322 ); 323 const currentUiData = uiData; 324 325 const applyEvent = new FilterPresetApplyRequest( 326 saveEvent.name, 327 TraceType.TEST_TRACE_STRING, 328 ); 329 await presenter.onAppEvent(applyEvent); 330 expect(uiData).not.toEqual(currentUiData); 331 }); 332 333 it('updates dark mode', async () => { 334 expect(uiData.isDarkMode).toBeFalse(); 335 await presenter.onAppEvent(new DarkModeToggled(true)); 336 expect(uiData.isDarkMode).toBeTrue(); 337 }); 338 339 it('disables show diff if no prev entry available', async () => { 340 const userOptions: UserOptions = { 341 showDiff: {name: '', enabled: false, isUnavailable: false}, 342 }; 343 await presenter.onHierarchyUserOptionsChange(userOptions); 344 await presenter.onPropertiesUserOptionsChange(userOptions); 345 await presenter.onAppEvent(positionUpdate); 346 expect(uiData.hierarchyUserOptions['showDiff'].isUnavailable).toBeTrue(); 347 expect(uiData.propertiesUserOptions['showDiff'].isUnavailable).toBeTrue(); 348 }); 349 350 it('shows correct hierarchy tree name for entry', async () => { 351 const spy = spyOn( 352 assertDefined(positionUpdate.position.entry?.getFullTrace()), 353 'isDumpWithoutTimestamp', 354 ); 355 spy.and.returnValue(false); 356 await presenter.onAppEvent(positionUpdate); 357 const entryNode = assertDefined(uiData.hierarchyTrees?.at(0)); 358 expect(entryNode.getDisplayName()).toContain( 359 positionUpdate.position.timestamp.format(), 360 ); 361 362 pinNode(entryNode); 363 spy.and.returnValue(true); 364 await presenter.onAppEvent(positionUpdate); 365 const newEntryNode = assertDefined(uiData.hierarchyTrees?.at(0)); 366 expect(newEntryNode.getDisplayName()).toContain('Dump'); 367 expect(uiData.pinnedItems).toEqual([newEntryNode]); 368 }); 369 370 it('handles pinned item change', () => { 371 expect(uiData.pinnedItems).toEqual([]); 372 const item = TreeNodeUtils.makeUiHierarchyNode({id: '', name: ''}); 373 presenter.onPinnedItemChange(item); 374 expect(uiData.pinnedItems).toEqual([item]); 375 presenter.onPinnedItemChange(item); 376 expect(uiData.pinnedItems).toEqual([]); 377 }); 378 379 it('updates and applies hierarchy user options', async () => { 380 await presenter.onAppEvent(positionUpdate); 381 const userOptions: UserOptions = {flat: {name: '', enabled: true}}; 382 await presenter.onHierarchyUserOptionsChange(userOptions); 383 expect(uiData.hierarchyUserOptions).toEqual(userOptions); 384 expect(uiData.hierarchyTrees?.at(0)?.getAllChildren().length).toEqual(3); 385 }); 386 387 it('updates highlighted property', () => { 388 const id = '4'; 389 presenter.onHighlightedPropertyChange(id); 390 expect(uiData.highlightedProperty).toEqual(id); 391 presenter.onHighlightedPropertyChange(id); 392 expect(uiData.highlightedProperty).toEqual(''); 393 }); 394 395 it('sets properties tree and associated ui data from tree node', async () => { 396 await presenter.onAppEvent(positionUpdate); 397 await presenter.onHighlightedNodeChange(selectedTree); 398 const propertiesTree = assertDefined(uiData.propertiesTree); 399 expect(propertiesTree.id).toContain(selectedTree.id); 400 expect(propertiesTree.getAllChildren().length).toEqual(2); 401 }); 402 403 it('updates and applies properties user options, calculating diffs from prev hierarchy tree', async () => { 404 await presenter.onAppEvent(positionUpdate); 405 await presenter.onHighlightedIdChange(selectedTree.id); 406 await presenter.onAppEvent(secondPositionUpdate); 407 expect( 408 uiData.propertiesTree?.getChildByName('testProp')?.getDiff(), 409 ).toEqual(DiffType.NONE); 410 411 const userOptions: UserOptions = {showDiff: {name: '', enabled: true}}; 412 await presenter.onPropertiesUserOptionsChange(userOptions); 413 expect(uiData.propertiesUserOptions).toEqual(userOptions); 414 expect( 415 uiData.propertiesTree?.getChildByName('testProp')?.getDiff(), 416 ).toEqual(DiffType.MODIFIED); 417 }); 418 419 it('is robust to attempts to change rect user data if no rects presenter', async () => { 420 expect(() => presenter.onRectsUserOptionsChange({})).not.toThrowError(); 421 await expectAsync( 422 presenter.onRectShowStateChange('', RectShowState.SHOW), 423 ).not.toBeRejected(); 424 }); 425 426 it('creates input data for rects view', async () => { 427 initializeRectsPresenter(); 428 await presenter.onAppEvent(positionUpdate); 429 const rectsToDraw = assertDefined(uiData.rectsToDraw); 430 const expectedFirstRect = presenter.uiRects[0]; 431 expect(rectsToDraw[0].x).toEqual(expectedFirstRect.x); 432 expect(rectsToDraw[0].y).toEqual(expectedFirstRect.y); 433 expect(rectsToDraw[0].w).toEqual(expectedFirstRect.w); 434 expect(rectsToDraw[0].h).toEqual(expectedFirstRect.h); 435 checkRectUiData(uiData, 3, 3, 3); 436 }); 437 438 it('filters rects by visibility', async () => { 439 initializeRectsPresenter(); 440 const userOptions: UserOptions = { 441 showOnlyVisible: {name: '', enabled: false}, 442 }; 443 await presenter.onAppEvent(positionUpdate); 444 presenter.onRectsUserOptionsChange(userOptions); 445 expect(uiData.rectsUserOptions).toEqual(userOptions); 446 checkRectUiData(uiData, 3, 3, 3); 447 448 userOptions['showOnlyVisible'].enabled = true; 449 presenter.onRectsUserOptionsChange(userOptions); 450 checkRectUiData(uiData, 2, 3, 2); 451 }); 452 453 it('filters rects by show/hide state', async () => { 454 initializeRectsPresenter(); 455 const userOptions: UserOptions = { 456 ignoreRectShowState: { 457 name: 'Ignore', 458 icon: 'visibility', 459 enabled: true, 460 }, 461 }; 462 await presenter.onAppEvent(positionUpdate); 463 presenter.onRectsUserOptionsChange(userOptions); 464 checkRectUiData(uiData, 3, 3, 3); 465 466 await presenter.onRectShowStateChange( 467 assertDefined(uiData.rectsToDraw)[0].id, 468 RectShowState.HIDE, 469 ); 470 checkRectUiData(uiData, 3, 3, 2); 471 472 userOptions['ignoreRectShowState'].enabled = false; 473 presenter.onRectsUserOptionsChange(userOptions); 474 checkRectUiData(uiData, 2, 3, 2); 475 }); 476 477 it('handles both visibility and show/hide state in rects', async () => { 478 initializeRectsPresenter(); 479 const userOptions: UserOptions = { 480 ignoreRectShowState: {name: '', enabled: true}, 481 showOnlyVisible: {name: '', enabled: false}, 482 }; 483 presenter.onRectsUserOptionsChange(userOptions); 484 await presenter.onAppEvent(positionUpdate); 485 checkRectUiData(uiData, 3, 3, 3); 486 487 await presenter.onRectShowStateChange( 488 assertDefined(uiData.rectsToDraw)[0].id, 489 RectShowState.HIDE, 490 ); 491 checkRectUiData(uiData, 3, 3, 2); 492 493 userOptions['ignoreRectShowState'].enabled = false; 494 presenter.onRectsUserOptionsChange(userOptions); 495 checkRectUiData(uiData, 2, 3, 2); 496 497 userOptions['showOnlyVisible'].enabled = true; 498 presenter.onRectsUserOptionsChange(userOptions); 499 checkRectUiData(uiData, 1, 3, 1); 500 501 userOptions['ignoreRectShowState'].enabled = true; 502 presenter.onRectsUserOptionsChange(userOptions); 503 checkRectUiData(uiData, 2, 3, 1); 504 }); 505 506 it('handles arrow up/down press', async () => { 507 await presenter.onAppEvent(positionUpdate); 508 await presenter.onArrowPress(storage, false); 509 expect(uiData.propertiesTree?.id).toContain('Test Trace entry'); 510 await presenter.onArrowPress(storage, false); 511 expect(uiData.propertiesTree?.id).toContain('1 p1'); 512 await presenter.onArrowPress(storage, true); 513 expect(uiData.propertiesTree?.id).toContain('Test Trace entry'); 514 }); 515 516 function pinNode(node: UiHierarchyTreeNode) { 517 presenter.onPinnedItemChange(node); 518 expect(uiData.pinnedItems).toEqual([node]); 519 } 520 521 function initializeRectsPresenter(p = presenter) { 522 p.initializeRectsPresenter(); 523 p.uiRects = [ 524 new UiRectBuilder() 525 .setX(0) 526 .setY(0) 527 .setWidth(1) 528 .setHeight(1) 529 .setLabel('test rect') 530 .setTransform(IDENTITY_MATRIX) 531 .setIsVisible(true) 532 .setIsDisplay(false) 533 .setIsActiveDisplay(true) 534 .setId('1 p1') 535 .setGroupId(0) 536 .setIsClickable(true) 537 .setCornerRadius(0) 538 .setDepth(0) 539 .build(), 540 new UiRectBuilder() 541 .setX(0) 542 .setY(0) 543 .setWidth(1) 544 .setHeight(1) 545 .setLabel('test rect 2') 546 .setTransform(IDENTITY_MATRIX) 547 .setIsVisible(true) 548 .setIsDisplay(false) 549 .setIsActiveDisplay(true) 550 .setId('3 c3') 551 .setGroupId(0) 552 .setIsClickable(true) 553 .setCornerRadius(0) 554 .setDepth(1) 555 .build(), 556 new UiRectBuilder() 557 .setX(0) 558 .setY(0) 559 .setWidth(1) 560 .setHeight(1) 561 .setLabel('test rect 3') 562 .setTransform(IDENTITY_MATRIX) 563 .setIsVisible(false) 564 .setIsDisplay(false) 565 .setIsActiveDisplay(true) 566 .setId('2 p2') 567 .setGroupId(0) 568 .setIsClickable(true) 569 .setCornerRadius(0) 570 .setDepth(2) 571 .build(), 572 ]; 573 p.displays = [{displayId: 0, groupId: 0, name: 'Display', isActive: true}]; 574 } 575 576 function checkRectUiData( 577 uiData: UiDataHierarchy, 578 rectsToDraw: number, 579 allRects: number, 580 shownRects: number, 581 ) { 582 expect(assertDefined(uiData.rectsToDraw).length).toEqual(rectsToDraw); 583 const showStates = Array.from( 584 assertDefined(uiData.rectIdToShowState).values(), 585 ); 586 expect(showStates.length).toEqual(allRects); 587 expect(showStates.filter((s) => s === RectShowState.SHOW).length).toEqual( 588 shownRects, 589 ); 590 } 591}); 592