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/in_memory_storage'; 20import { 21 DarkModeToggled, 22 FilterPresetApplyRequest, 23 FilterPresetSaveRequest, 24 TracePositionUpdate, 25} from 'messaging/winscope_event'; 26import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; 27import {MockPresenter} from 'test/unit/mock_hierarchy_viewer_presenter'; 28import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 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 events 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 263 it('is robust to empty trace', async () => { 264 const callback = (newData: UiDataHierarchy) => { 265 uiData = newData; 266 }; 267 const trace = UnitTestUtils.makeEmptyTrace(TraceType.WINDOW_MANAGER); 268 const traces = new Traces(); 269 traces.addTrace(trace); 270 const presenter = new MockPresenter( 271 trace, 272 traces, 273 new InMemoryStorage(), 274 callback, 275 undefined, 276 ); 277 presenter.initializeRectsPresenter(); 278 279 const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp( 280 TimestampConverterUtils.makeRealTimestamp(0n), 281 ); 282 await presenter.onAppEvent(positionUpdateWithoutTraceEntry); 283 284 expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0); 285 expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0); 286 expect(uiData.hierarchyTrees).toBeUndefined(); 287 expect( 288 Object.keys(assertDefined(uiData?.rectsUserOptions)).length, 289 ).toBeGreaterThan(0); 290 }); 291 292 it('handles filter preset requests', async () => { 293 initializeRectsPresenter(); 294 await presenter.onAppEvent(positionUpdate); 295 const saveEvent = new FilterPresetSaveRequest( 296 'TestPreset', 297 TraceType.TEST_TRACE_STRING, 298 ); 299 expect(storage.get(saveEvent.name)).toBeUndefined(); 300 await presenter.onAppEvent(saveEvent); 301 expect(storage.get(saveEvent.name)).toBeDefined(); 302 303 await presenter.onHierarchyFilterChange(new TextFilter('Test Filter')); 304 await presenter.onHierarchyUserOptionsChange({}); 305 await presenter.onPropertiesUserOptionsChange({}); 306 await presenter.onPropertiesFilterChange(new TextFilter('Test Filter')); 307 presenter.onRectsUserOptionsChange({}); 308 await presenter.onRectShowStateChange( 309 assertDefined(uiData.rectsToDraw)[0].id, 310 RectShowState.HIDE, 311 ); 312 const currentUiData = uiData; 313 314 const applyEvent = new FilterPresetApplyRequest( 315 saveEvent.name, 316 TraceType.TEST_TRACE_STRING, 317 ); 318 await presenter.onAppEvent(applyEvent); 319 expect(uiData).not.toEqual(currentUiData); 320 }); 321 322 it('updates dark mode', async () => { 323 expect(uiData.isDarkMode).toBeFalse(); 324 await presenter.onAppEvent(new DarkModeToggled(true)); 325 expect(uiData.isDarkMode).toBeTrue(); 326 }); 327 328 it('disables show diff if no prev entry available', async () => { 329 const userOptions: UserOptions = { 330 showDiff: {name: '', enabled: false, isUnavailable: false}, 331 }; 332 await presenter.onHierarchyUserOptionsChange(userOptions); 333 await presenter.onPropertiesUserOptionsChange(userOptions); 334 await presenter.onAppEvent(positionUpdate); 335 expect(uiData.hierarchyUserOptions['showDiff'].isUnavailable).toBeTrue(); 336 expect(uiData.propertiesUserOptions['showDiff'].isUnavailable).toBeTrue(); 337 }); 338 339 it('shows correct hierarchy tree name for entry', async () => { 340 const spy = spyOn( 341 assertDefined(positionUpdate.position.entry?.getFullTrace()), 342 'isDumpWithoutTimestamp', 343 ); 344 spy.and.returnValue(false); 345 await presenter.onAppEvent(positionUpdate); 346 const entryNode = assertDefined(uiData.hierarchyTrees?.at(0)); 347 expect(entryNode.getDisplayName()).toContain( 348 positionUpdate.position.timestamp.format(), 349 ); 350 351 pinNode(entryNode); 352 spy.and.returnValue(true); 353 await presenter.onAppEvent(positionUpdate); 354 const newEntryNode = assertDefined(uiData.hierarchyTrees?.at(0)); 355 expect(newEntryNode.getDisplayName()).toContain('Dump'); 356 expect(uiData.pinnedItems).toEqual([newEntryNode]); 357 }); 358 359 it('handles pinned item change', () => { 360 expect(uiData.pinnedItems).toEqual([]); 361 const item = TreeNodeUtils.makeUiHierarchyNode({id: '', name: ''}); 362 presenter.onPinnedItemChange(item); 363 expect(uiData.pinnedItems).toEqual([item]); 364 presenter.onPinnedItemChange(item); 365 expect(uiData.pinnedItems).toEqual([]); 366 }); 367 368 it('updates and applies hierarchy user options', async () => { 369 await presenter.onAppEvent(positionUpdate); 370 const userOptions: UserOptions = {flat: {name: '', enabled: true}}; 371 await presenter.onHierarchyUserOptionsChange(userOptions); 372 expect(uiData.hierarchyUserOptions).toEqual(userOptions); 373 expect(uiData.hierarchyTrees?.at(0)?.getAllChildren().length).toEqual(3); 374 }); 375 376 it('updates highlighted property', () => { 377 const id = '4'; 378 presenter.onHighlightedPropertyChange(id); 379 expect(uiData.highlightedProperty).toEqual(id); 380 presenter.onHighlightedPropertyChange(id); 381 expect(uiData.highlightedProperty).toEqual(''); 382 }); 383 384 it('sets properties tree and associated ui data from tree node', async () => { 385 await presenter.onAppEvent(positionUpdate); 386 await presenter.onHighlightedNodeChange(selectedTree); 387 const propertiesTree = assertDefined(uiData.propertiesTree); 388 expect(propertiesTree.id).toContain(selectedTree.id); 389 expect(propertiesTree.getAllChildren().length).toEqual(2); 390 }); 391 392 it('updates and applies properties user options, calculating diffs from prev hierarchy tree', async () => { 393 await presenter.onAppEvent(positionUpdate); 394 await presenter.onHighlightedIdChange(selectedTree.id); 395 await presenter.onAppEvent(secondPositionUpdate); 396 expect( 397 uiData.propertiesTree?.getChildByName('testProp')?.getDiff(), 398 ).toEqual(DiffType.NONE); 399 400 const userOptions: UserOptions = {showDiff: {name: '', enabled: true}}; 401 await presenter.onPropertiesUserOptionsChange(userOptions); 402 expect(uiData.propertiesUserOptions).toEqual(userOptions); 403 expect( 404 uiData.propertiesTree?.getChildByName('testProp')?.getDiff(), 405 ).toEqual(DiffType.MODIFIED); 406 }); 407 408 it('is robust to attempts to change rect user data if no rects presenter', async () => { 409 expect(() => presenter.onRectsUserOptionsChange({})).not.toThrowError(); 410 await expectAsync( 411 presenter.onRectShowStateChange('', RectShowState.SHOW), 412 ).not.toBeRejected(); 413 }); 414 415 it('creates input data for rects view', async () => { 416 initializeRectsPresenter(); 417 await presenter.onAppEvent(positionUpdate); 418 const rectsToDraw = assertDefined(uiData.rectsToDraw); 419 const expectedFirstRect = presenter.uiRects[0]; 420 expect(rectsToDraw[0].x).toEqual(expectedFirstRect.x); 421 expect(rectsToDraw[0].y).toEqual(expectedFirstRect.y); 422 expect(rectsToDraw[0].w).toEqual(expectedFirstRect.w); 423 expect(rectsToDraw[0].h).toEqual(expectedFirstRect.h); 424 checkRectUiData(uiData, 3, 3, 3); 425 }); 426 427 it('filters rects by visibility', async () => { 428 initializeRectsPresenter(); 429 const userOptions: UserOptions = { 430 showOnlyVisible: {name: '', enabled: false}, 431 }; 432 await presenter.onAppEvent(positionUpdate); 433 presenter.onRectsUserOptionsChange(userOptions); 434 expect(uiData.rectsUserOptions).toEqual(userOptions); 435 checkRectUiData(uiData, 3, 3, 3); 436 437 userOptions['showOnlyVisible'].enabled = true; 438 presenter.onRectsUserOptionsChange(userOptions); 439 checkRectUiData(uiData, 2, 3, 2); 440 }); 441 442 it('filters rects by show/hide state', async () => { 443 initializeRectsPresenter(); 444 const userOptions: UserOptions = { 445 ignoreRectShowState: { 446 name: 'Ignore', 447 icon: 'visibility', 448 enabled: true, 449 }, 450 }; 451 await presenter.onAppEvent(positionUpdate); 452 presenter.onRectsUserOptionsChange(userOptions); 453 checkRectUiData(uiData, 3, 3, 3); 454 455 await presenter.onRectShowStateChange( 456 assertDefined(uiData.rectsToDraw)[0].id, 457 RectShowState.HIDE, 458 ); 459 checkRectUiData(uiData, 3, 3, 2); 460 461 userOptions['ignoreRectShowState'].enabled = false; 462 presenter.onRectsUserOptionsChange(userOptions); 463 checkRectUiData(uiData, 2, 3, 2); 464 }); 465 466 it('handles both visibility and show/hide state in rects', async () => { 467 initializeRectsPresenter(); 468 const userOptions: UserOptions = { 469 ignoreRectShowState: {name: '', enabled: true}, 470 showOnlyVisible: {name: '', enabled: false}, 471 }; 472 presenter.onRectsUserOptionsChange(userOptions); 473 await presenter.onAppEvent(positionUpdate); 474 checkRectUiData(uiData, 3, 3, 3); 475 476 await presenter.onRectShowStateChange( 477 assertDefined(uiData.rectsToDraw)[0].id, 478 RectShowState.HIDE, 479 ); 480 checkRectUiData(uiData, 3, 3, 2); 481 482 userOptions['ignoreRectShowState'].enabled = false; 483 presenter.onRectsUserOptionsChange(userOptions); 484 checkRectUiData(uiData, 2, 3, 2); 485 486 userOptions['showOnlyVisible'].enabled = true; 487 presenter.onRectsUserOptionsChange(userOptions); 488 checkRectUiData(uiData, 1, 3, 1); 489 490 userOptions['ignoreRectShowState'].enabled = true; 491 presenter.onRectsUserOptionsChange(userOptions); 492 checkRectUiData(uiData, 2, 3, 1); 493 }); 494 495 function pinNode(node: UiHierarchyTreeNode) { 496 presenter.onPinnedItemChange(node); 497 expect(uiData.pinnedItems).toEqual([node]); 498 } 499 500 function initializeRectsPresenter(p = presenter) { 501 p.initializeRectsPresenter(); 502 p.uiRects = [ 503 new UiRectBuilder() 504 .setX(0) 505 .setY(0) 506 .setWidth(1) 507 .setHeight(1) 508 .setLabel('test rect') 509 .setTransform(IDENTITY_MATRIX) 510 .setIsVisible(true) 511 .setIsDisplay(false) 512 .setIsActiveDisplay(true) 513 .setId('1 p1') 514 .setGroupId(0) 515 .setIsClickable(true) 516 .setCornerRadius(0) 517 .setDepth(0) 518 .build(), 519 new UiRectBuilder() 520 .setX(0) 521 .setY(0) 522 .setWidth(1) 523 .setHeight(1) 524 .setLabel('test rect 2') 525 .setTransform(IDENTITY_MATRIX) 526 .setIsVisible(true) 527 .setIsDisplay(false) 528 .setIsActiveDisplay(true) 529 .setId('3 c3') 530 .setGroupId(0) 531 .setIsClickable(true) 532 .setCornerRadius(0) 533 .setDepth(1) 534 .build(), 535 new UiRectBuilder() 536 .setX(0) 537 .setY(0) 538 .setWidth(1) 539 .setHeight(1) 540 .setLabel('test rect 3') 541 .setTransform(IDENTITY_MATRIX) 542 .setIsVisible(false) 543 .setIsDisplay(false) 544 .setIsActiveDisplay(true) 545 .setId('2 p2') 546 .setGroupId(0) 547 .setIsClickable(true) 548 .setCornerRadius(0) 549 .setDepth(2) 550 .build(), 551 ]; 552 p.displays = [{displayId: 0, groupId: 0, name: 'Display', isActive: true}]; 553 } 554 555 function checkRectUiData( 556 uiData: UiDataHierarchy, 557 rectsToDraw: number, 558 allRects: number, 559 shownRects: number, 560 ) { 561 expect(assertDefined(uiData.rectsToDraw).length).toEqual(rectsToDraw); 562 const showStates = Array.from( 563 assertDefined(uiData.rectIdToShowState).values(), 564 ); 565 expect(showStates.length).toEqual(allRects); 566 expect(showStates.filter((s) => s === RectShowState.SHOW).length).toEqual( 567 shownRects, 568 ); 569 } 570}); 571