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 {CommonModule} from '@angular/common'; 18import {Component, ViewChild} from '@angular/core'; 19import {ComponentFixture, TestBed} from '@angular/core/testing'; 20import {MatButtonModule} from '@angular/material/button'; 21import {MatDividerModule} from '@angular/material/divider'; 22import {MatFormFieldModule} from '@angular/material/form-field'; 23import {MatIconModule} from '@angular/material/icon'; 24import {MatIconTestingModule} from '@angular/material/icon/testing'; 25import {MatSelectModule} from '@angular/material/select'; 26import {MatSliderModule} from '@angular/material/slider'; 27import {MatTooltipModule} from '@angular/material/tooltip'; 28import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 29import {assertDefined} from 'common/assert_utils'; 30import {Box3D} from 'common/geometry/box3d'; 31import {TransformMatrix} from 'common/geometry/transform_matrix'; 32import {PersistentStore} from 'common/persistent_store'; 33import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; 34import {waitToBeCalled} from 'test/utils'; 35import {TraceType} from 'trace/trace_type'; 36import {VISIBLE_CHIP} from 'viewers/common/chip'; 37import {DisplayIdentifier} from 'viewers/common/display_identifier'; 38import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 39import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events'; 40import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component'; 41import {RectsComponent} from 'viewers/components/rects/rects_component'; 42import {UiRect} from 'viewers/components/rects/ui_rect'; 43import {UserOptionsComponent} from 'viewers/components/user_options_component'; 44import {Camera} from './camera'; 45import {Canvas} from './canvas'; 46import {ColorType} from './color_type'; 47import {RectLabel} from './rect_label'; 48import {ShadingMode} from './shading_mode'; 49import {UiRect3D} from './ui_rect3d'; 50import {UiRectBuilder} from './ui_rect_builder'; 51 52describe('RectsComponent', () => { 53 const rectGroup0 = makeRectWithGroupId(0); 54 const rectGroup1 = makeRectWithGroupId(1); 55 const rectGroup2 = makeRectWithGroupId(2); 56 57 let component: TestHostComponent; 58 let fixture: ComponentFixture<TestHostComponent>; 59 let htmlElement: HTMLElement; 60 let updateViewPositionSpy: jasmine.Spy<(camera: Camera, box: Box3D) => void>; 61 let updateRectsSpy: jasmine.Spy<(rects: UiRect3D[]) => void>; 62 let updateLabelsSpy: jasmine.Spy<(labels: RectLabel[]) => void>; 63 let renderViewSpy: jasmine.Spy<() => void>; 64 65 beforeAll(() => { 66 localStorage.clear(); 67 }); 68 69 beforeEach(async () => { 70 updateViewPositionSpy = spyOn(Canvas.prototype, 'updateViewPosition'); 71 updateRectsSpy = spyOn(Canvas.prototype, 'updateRects'); 72 updateLabelsSpy = spyOn(Canvas.prototype, 'updateLabels'); 73 renderViewSpy = spyOn(Canvas.prototype, 'renderView'); 74 75 await TestBed.configureTestingModule({ 76 imports: [ 77 CommonModule, 78 MatDividerModule, 79 MatSliderModule, 80 MatButtonModule, 81 MatTooltipModule, 82 MatIconModule, 83 MatIconTestingModule, 84 MatSelectModule, 85 BrowserAnimationsModule, 86 MatFormFieldModule, 87 ], 88 declarations: [ 89 TestHostComponent, 90 RectsComponent, 91 CollapsibleSectionTitleComponent, 92 UserOptionsComponent, 93 ], 94 }).compileComponents(); 95 96 fixture = TestBed.createComponent(TestHostComponent); 97 component = fixture.componentInstance; 98 htmlElement = fixture.nativeElement; 99 }); 100 101 afterEach(() => { 102 localStorage.clear(); 103 }); 104 105 it('can be created', () => { 106 expect(component).toBeTruthy(); 107 }); 108 109 it('renders rotation slider', () => { 110 const slider = htmlElement.querySelector('mat-slider.slider-rotation'); 111 expect(slider).toBeTruthy(); 112 }); 113 114 it('renders separation slider', () => { 115 const slider = htmlElement.querySelector('mat-slider.slider-spacing'); 116 expect(slider).toBeTruthy(); 117 }); 118 119 it('renders canvas', () => { 120 const rectsCanvas = htmlElement.querySelector('.large-rects-canvas'); 121 expect(rectsCanvas).toBeTruthy(); 122 }); 123 124 it('draws scene when input data changes', async () => { 125 fixture.detectChanges(); 126 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 127 resetSpies(); 128 129 checkAllSpiesCalled(0); 130 component.rects = [rectGroup0]; 131 fixture.detectChanges(); 132 checkAllSpiesCalled(1); 133 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 134 boundingBox, 135 ); 136 137 component.rects = [rectGroup0]; 138 fixture.detectChanges(); 139 checkAllSpiesCalled(2); 140 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 141 boundingBox, 142 ); 143 }); 144 145 it('draws scene when rotation slider changes', () => { 146 fixture.detectChanges(); 147 resetSpies(); 148 const slider = assertDefined(htmlElement.querySelector('.slider-rotation')); 149 150 checkAllSpiesCalled(0); 151 slider.dispatchEvent(new MouseEvent('mousedown')); 152 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 153 expect(updateRectsSpy).toHaveBeenCalledTimes(0); 154 expect(updateLabelsSpy).toHaveBeenCalledTimes(1); 155 expect(renderViewSpy).toHaveBeenCalledTimes(1); 156 }); 157 158 it('draws scene when spacing slider changes', () => { 159 fixture.detectChanges(); 160 resetSpies(); 161 const slider = assertDefined(htmlElement.querySelector('.slider-spacing')); 162 163 checkAllSpiesCalled(0); 164 slider.dispatchEvent(new MouseEvent('mousedown')); 165 checkAllSpiesCalled(1); 166 }); 167 168 it('unfocuses spacing slider on click', () => { 169 fixture.detectChanges(); 170 const spacingSlider = assertDefined( 171 htmlElement.querySelector('.slider-spacing'), 172 ); 173 checkSliderUnfocusesOnClick(spacingSlider, 0.02); 174 }); 175 176 it('unfocuses rotation slider on click', () => { 177 fixture.detectChanges(); 178 const rotationSlider = assertDefined( 179 htmlElement.querySelector('.slider-rotation'), 180 ); 181 checkSliderUnfocusesOnClick(rotationSlider, 1); 182 }); 183 184 it('renders display selector', async () => { 185 component.rects = [rectGroup0]; 186 component.displays = [ 187 {displayId: 0, groupId: 0, name: 'Display 0', isActive: false}, 188 {displayId: 1, groupId: 1, name: 'Display 1', isActive: false}, 189 {displayId: 2, groupId: 2, name: 'Display 2', isActive: false}, 190 ]; 191 await checkSelectedDisplay([0], [0]); 192 }); 193 194 it('handles display change by checkbox', async () => { 195 component.rects = [rectGroup0, rectGroup1]; 196 component.displays = [ 197 {displayId: 0, groupId: 0, name: 'Display 0', isActive: false}, 198 {displayId: 1, groupId: 1, name: 'Display 1', isActive: false}, 199 {displayId: 2, groupId: 2, name: 'Display 2', isActive: false}, 200 ]; 201 await checkSelectedDisplay([0], [0]); 202 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 203 204 openDisplaysSelect(); 205 const options = getDisplayOptions(); 206 207 options.item(1).click(); 208 await checkSelectedDisplay([0, 1], [0, 1], true); 209 expect(updateViewPositionSpy.calls.mostRecent().args[1]).not.toEqual( 210 boundingBox, 211 ); 212 213 options.item(0).click(); 214 await checkSelectedDisplay([1], [1], true); 215 216 options.item(1).click(); 217 await checkSelectedDisplay([], [], true); 218 const placeholder = assertDefined( 219 htmlElement.querySelector('.placeholder-text'), 220 ); 221 expect(placeholder.textContent?.trim()).toEqual('No displays selected.'); 222 }); 223 224 it('handles display change by "only" button', async () => { 225 component.rects = [rectGroup0, rectGroup1]; 226 component.displays = [ 227 {displayId: 0, groupId: 0, name: 'Display 0', isActive: false}, 228 {displayId: 1, groupId: 1, name: 'Display 1', isActive: false}, 229 {displayId: 2, groupId: 2, name: 'Display 2', isActive: false}, 230 ]; 231 await checkSelectedDisplay([0], [0]); 232 233 openDisplaysSelect(); 234 235 const onlyButtons = document.querySelectorAll<HTMLElement>( 236 '.mat-select-panel .mat-option .option-only-button', 237 ); 238 239 const display0Button = onlyButtons.item(0); 240 const display1Button = onlyButtons.item(1); 241 242 // no change 243 display0Button.click(); 244 await checkSelectedDisplay([0], [0]); 245 246 display1Button.click(); 247 await checkSelectedDisplay([1], [1]); 248 249 assertDefined(display0Button.parentElement).click(); 250 await checkSelectedDisplay([0, 1], [0, 1], true); 251 display0Button.click(); 252 await checkSelectedDisplay([0], [0], true); 253 }); 254 255 it('tracks selected display', async () => { 256 component.displays = [ 257 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 258 {displayId: 20, groupId: 1, name: 'Display 1', isActive: false}, 259 ]; 260 component.rects = [rectGroup0, rectGroup1]; 261 await checkSelectedDisplay([0], [0]); 262 263 component.displays = [ 264 {displayId: 20, groupId: 2, name: 'Display 1', isActive: false}, 265 {displayId: 10, groupId: 1, name: 'Display 0', isActive: false}, 266 ]; 267 await checkSelectedDisplay([0], [1], false); 268 }); 269 270 it('updates scene on separation slider change', () => { 271 component.rects = [rectGroup0, rectGroup0]; 272 fixture.detectChanges(); 273 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 274 updateSeparationSlider(); 275 276 checkAllSpiesCalled(2); 277 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 278 boundingBox, 279 ); 280 const rectsBefore = assertDefined(updateRectsSpy.calls.first().args[0]); 281 const rectsAfter = assertDefined(updateRectsSpy.calls.mostRecent().args[0]); 282 283 expect(rectsBefore[0].topLeft.z).toEqual(200); 284 expect(rectsAfter[0].topLeft.z).toEqual(12); 285 }); 286 287 it('updates scene on rotation slider change', () => { 288 component.rects = [rectGroup0]; 289 fixture.detectChanges(); 290 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 291 updateRotationSlider(); 292 293 expect(updateViewPositionSpy).toHaveBeenCalledTimes(2); 294 expect(updateRectsSpy).toHaveBeenCalledTimes(1); 295 expect(updateLabelsSpy).toHaveBeenCalledTimes(2); 296 expect(renderViewSpy).toHaveBeenCalledTimes(2); 297 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 298 boundingBox, 299 ); 300 301 const cameraBefore = assertDefined( 302 updateViewPositionSpy.calls.first().args[0], 303 ); 304 const cameraAfter = assertDefined( 305 updateViewPositionSpy.calls.mostRecent().args[0], 306 ); 307 308 expect(cameraAfter.rotationAngleX).toEqual( 309 cameraBefore.rotationAngleX * 0.5, 310 ); 311 expect(cameraAfter.rotationAngleY).toEqual( 312 cameraBefore.rotationAngleY * 0.5, 313 ); 314 }); 315 316 it('updates scene on shading mode change', () => { 317 component.rects = [rectGroup0]; 318 fixture.detectChanges(); 319 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 320 321 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 322 updateShadingMode(ShadingMode.WIRE_FRAME, ShadingMode.OPACITY); 323 324 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 325 expect(updateRectsSpy).toHaveBeenCalledTimes(3); 326 expect(updateLabelsSpy).toHaveBeenCalledTimes(1); 327 expect(renderViewSpy).toHaveBeenCalledTimes(3); 328 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 329 boundingBox, 330 ); 331 332 const rectsGradient = assertDefined(updateRectsSpy.calls.first().args[0]); 333 const rectsWireFrame = assertDefined(updateRectsSpy.calls.argsFor(1).at(0)); 334 const rectsOpacity = assertDefined( 335 updateRectsSpy.calls.mostRecent().args[0], 336 ); 337 338 expect(rectsGradient[0].colorType).toEqual(ColorType.VISIBLE); 339 expect(rectsGradient[0].darkFactor).toEqual(1); 340 341 expect(rectsWireFrame[0].colorType).toEqual(ColorType.EMPTY); 342 expect(rectsWireFrame[0].darkFactor).toEqual(1); 343 344 expect(rectsOpacity[0].colorType).toEqual(ColorType.VISIBLE_WITH_OPACITY); 345 expect(rectsOpacity[0].darkFactor).toEqual(0.5); 346 347 updateShadingMode(ShadingMode.OPACITY, ShadingMode.GRADIENT); // cycles back to original 348 }); 349 350 it('uses stored rects view settings', () => { 351 fixture.detectChanges(); 352 353 updateSeparationSlider(); 354 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 355 356 const newFixture = TestBed.createComponent(TestHostComponent); 357 newFixture.detectChanges(); 358 const newRectsComponent = assertDefined( 359 newFixture.componentInstance.rectsComponent, 360 ); 361 expect(newRectsComponent.getZSpacingFactor()).toEqual(0.06); 362 expect(newRectsComponent.getShadingMode()).toEqual(ShadingMode.WIRE_FRAME); 363 }); 364 365 it('uses stored selected displays if present in new trace', async () => { 366 component.rects = [rectGroup0, rectGroup1]; 367 component.displays = [ 368 {displayId: 10, groupId: 0, name: 'Display 0', isActive: true}, 369 {displayId: 20, groupId: 1, name: 'Display 1', isActive: true}, 370 ]; 371 await checkSelectedDisplay([0], [0]); 372 373 openDisplaysSelect(); 374 const options = getDisplayOptions(); 375 options.item(1).click(); 376 await checkSelectedDisplay([0, 1], [0, 1]); 377 378 const fixtureWithSameDisplays = TestBed.createComponent(TestHostComponent); 379 const componentWithSameDisplays = fixtureWithSameDisplays.componentInstance; 380 componentWithSameDisplays.rects = component.rects; 381 componentWithSameDisplays.displays = component.displays; 382 await checkSelectedDisplay( 383 [0, 1], 384 [0, 1], 385 false, 386 fixtureWithSameDisplays, 387 fixtureWithSameDisplays.nativeElement, 388 ); 389 390 const fixtureWithDisplay1 = TestBed.createComponent(TestHostComponent); 391 const componentWithDisplay1 = fixtureWithDisplay1.componentInstance; 392 componentWithDisplay1.rects = [rectGroup1]; 393 componentWithDisplay1.displays = [ 394 {displayId: 20, groupId: 1, name: 'Display 1', isActive: true}, 395 ]; 396 await checkSelectedDisplay( 397 [1], 398 [1], 399 false, 400 fixtureWithDisplay1, 401 fixtureWithDisplay1.nativeElement, 402 ); 403 }); 404 405 it('defaults initial selection to first active display with rects', async () => { 406 component.rects = [rectGroup0, rectGroup1]; 407 component.displays = [ 408 {displayId: 10, groupId: 1, name: 'Display 0', isActive: false}, 409 {displayId: 20, groupId: 0, name: 'Display 1', isActive: true}, 410 ]; 411 await checkSelectedDisplay([1], [0]); 412 }); 413 414 it('defaults initial selection to first display with non-display rects and groupId 0', async () => { 415 component.displays = [ 416 {displayId: 10, groupId: 1, name: 'Display 0', isActive: true}, 417 {displayId: 20, groupId: 0, name: 'Display 1', isActive: false}, 418 ]; 419 component.rects = [rectGroup0]; 420 await checkSelectedDisplay([1], [0]); 421 }); 422 423 it('defaults initial selection to first display with non-display rects and groupId non-zero', async () => { 424 component.displays = [ 425 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 426 {displayId: 20, groupId: 1, name: 'Display 1', isActive: false}, 427 ]; 428 component.rects = [rectGroup1]; 429 await checkSelectedDisplay([1], [1]); 430 }); 431 432 it('handles change from zero to one display and back to zero', async () => { 433 component.displays = []; 434 component.rects = []; 435 await checkSelectedDisplay([], []); 436 const placeholder = assertDefined( 437 htmlElement.querySelector('.placeholder-text'), 438 ); 439 expect(placeholder.textContent?.trim()).toEqual('No rects found.'); 440 441 component.rects = [rectGroup0]; 442 component.displays = [ 443 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 444 ]; 445 await checkSelectedDisplay([0], [0], true); 446 447 component.displays = []; 448 component.rects = []; 449 await checkSelectedDisplay([], []); 450 }); 451 452 it('handles current display group id no longer present', async () => { 453 component.rects = [rectGroup0]; 454 component.displays = [ 455 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 456 ]; 457 await checkSelectedDisplay([0], [0]); 458 459 component.rects = [rectGroup1]; 460 component.displays = [ 461 {displayId: 20, groupId: 1, name: 'Display 1', isActive: false}, 462 ]; 463 await checkSelectedDisplay([1], [1]); 464 }); 465 466 it('draws mini rects with non-present group id', () => { 467 component.displays = [ 468 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 469 ]; 470 fixture.detectChanges(); 471 component.rects = [rectGroup0]; 472 component.miniRects = [rectGroup2]; 473 resetSpies(); 474 fixture.detectChanges(); 475 checkAllSpiesCalled(2); 476 expect( 477 updateRectsSpy.calls 478 .all() 479 .forEach((call) => expect(call.args[0].length).toEqual(1)), 480 ); 481 }); 482 483 it('draws mini rects with default spacing, rotation and shading mode', () => { 484 component.displays = [ 485 {displayId: 10, groupId: 0, name: 'Display 0', isActive: false}, 486 ]; 487 fixture.detectChanges(); 488 489 updateSeparationSlider(); 490 updateRotationSlider(); 491 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 492 493 component.rects = [rectGroup0, rectGroup0]; 494 component.miniRects = [rectGroup0, rectGroup0]; 495 resetSpies(); 496 fixture.detectChanges(); 497 checkAllSpiesCalled(2); 498 499 const largeRectsCamera = assertDefined( 500 updateViewPositionSpy.calls.first().args[0], 501 ); 502 const miniRectsCamera = assertDefined( 503 updateViewPositionSpy.calls.mostRecent().args[0], 504 ); 505 506 expect(largeRectsCamera.rotationAngleX).toEqual( 507 miniRectsCamera.rotationAngleX * 0.5, 508 ); 509 expect(largeRectsCamera.rotationAngleY).toEqual( 510 miniRectsCamera.rotationAngleY * 0.5, 511 ); 512 const largeRects = assertDefined(updateRectsSpy.calls.first().args[0]); 513 const miniRects = assertDefined(updateRectsSpy.calls.mostRecent().args[0]); 514 515 expect(largeRects[0].colorType).toEqual(ColorType.EMPTY); 516 expect(miniRects[0].colorType).toEqual(ColorType.VISIBLE); 517 518 expect(largeRects[0].topLeft.z).toEqual(12); 519 expect(miniRects[0].topLeft.z).toEqual(200); 520 }); 521 522 it('redraws mini rects on change', () => { 523 component.miniRects = [rectGroup0, rectGroup0]; 524 fixture.detectChanges(); 525 resetSpies(); 526 527 component.miniRects = [rectGroup0, rectGroup0]; 528 fixture.detectChanges(); 529 checkAllSpiesCalled(1); 530 }); 531 532 it('handles collapse button click', () => { 533 fixture.detectChanges(); 534 const spy = spyOn( 535 assertDefined(component.rectsComponent).collapseButtonClicked, 536 'emit', 537 ); 538 findAndClickElement('collapsible-section-title button'); 539 expect(spy).toHaveBeenCalled(); 540 }); 541 542 it('updates scene on pinned items change', () => { 543 component.rects = [rectGroup0]; 544 fixture.detectChanges(); 545 resetSpies(); 546 547 component.pinnedItems = [ 548 UiHierarchyTreeNode.from( 549 new HierarchyTreeBuilder().setId('test-id').setName('0').build(), 550 ), 551 ]; 552 fixture.detectChanges(); 553 expect(updateViewPositionSpy).toHaveBeenCalledTimes(0); 554 expect(updateRectsSpy).toHaveBeenCalledTimes(1); 555 expect(updateLabelsSpy).toHaveBeenCalledTimes(0); 556 expect(renderViewSpy).toHaveBeenCalledTimes(1); 557 expect(updateRectsSpy.calls.mostRecent().args[0][0].isPinned).toBeTrue(); 558 }); 559 560 it('emits rect id on rect click', () => { 561 component.rects = [rectGroup0]; 562 fixture.detectChanges(); 563 564 const testString = 'test_id'; 565 let id: string | undefined; 566 htmlElement.addEventListener(ViewerEvents.HighlightedIdChange, (event) => { 567 id = (event as CustomEvent).detail.id; 568 }); 569 570 const spy = spyOn(Canvas.prototype, 'getClickedRectId').and.returnValue( 571 undefined, 572 ); 573 clickLargeRectsCanvas(); 574 expect(id).toBeUndefined(); 575 spy.and.returnValue(testString); 576 clickLargeRectsCanvas(); 577 expect(id).toEqual(testString); 578 }); 579 580 it('pans view without emitting rect id', () => { 581 component.rects = [rectGroup0]; 582 fixture.detectChanges(); 583 const cameraBefore = updateViewPositionSpy.calls.mostRecent().args[0]; 584 expect(cameraBefore.panScreenDistance.dx).toEqual(0); 585 expect(cameraBefore.panScreenDistance.dy).toEqual(0); 586 const boundingBoxBefore = updateViewPositionSpy.calls.mostRecent().args[1]; 587 resetSpies(); 588 589 const testString = 'test_id'; 590 spyOn(Canvas.prototype, 'getClickedRectId').and.returnValue(testString); 591 let id: string | undefined; 592 htmlElement.addEventListener(ViewerEvents.HighlightedIdChange, (event) => { 593 id = (event as CustomEvent).detail.id; 594 }); 595 596 panView(); 597 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 598 expect(updateRectsSpy).not.toHaveBeenCalled(); 599 expect(updateLabelsSpy).not.toHaveBeenCalled(); 600 expect(renderViewSpy).toHaveBeenCalled(); 601 602 const [cameraAfter, boundingBoxAfter] = 603 updateViewPositionSpy.calls.mostRecent().args; 604 expect(cameraAfter.panScreenDistance.dx).toEqual(5); 605 expect(cameraAfter.panScreenDistance.dy).toEqual(10); 606 expect(boundingBoxAfter).toEqual(boundingBoxBefore); 607 608 clickLargeRectsCanvas(); 609 expect(id).toBeUndefined(); 610 611 clickLargeRectsCanvas(); 612 expect(id).toEqual(testString); 613 }); 614 615 it('handles window resize', async () => { 616 component.rects = [rectGroup0]; 617 fixture.detectChanges(); 618 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 619 resetSpies(); 620 621 spyOnProperty(window, 'innerWidth').and.returnValue(window.innerWidth / 2); 622 window.dispatchEvent(new Event('resize')); 623 fixture.detectChanges(); 624 await fixture.whenStable(); 625 await waitToBeCalled(renderViewSpy, 1); 626 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 627 expect(updateRectsSpy).not.toHaveBeenCalled(); 628 expect(updateLabelsSpy).not.toHaveBeenCalled(); 629 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 630 boundingBox, 631 ); 632 }); 633 634 it('handles change in dark mode', async () => { 635 component.rects = [rectGroup0]; 636 component.miniRects = [rectGroup0]; 637 fixture.detectChanges(); 638 resetSpies(); 639 640 component.isDarkMode = true; 641 fixture.detectChanges(); 642 expect(updateRectsSpy).toHaveBeenCalledTimes(2); 643 expect(updateLabelsSpy).toHaveBeenCalledTimes(2); 644 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); // only for mini rects 645 expect(renderViewSpy).toHaveBeenCalledTimes(2); 646 }); 647 648 it('handles zoom button clicks', () => { 649 component.rects = [rectGroup0]; 650 fixture.detectChanges(); 651 const boundingBox = updateViewPositionSpy.calls.mostRecent().args[1]; 652 const zoomFactor = 653 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor; 654 resetSpies(); 655 656 clickZoomInButton(); 657 checkZoomedIn(zoomFactor); 658 const zoomedInFactor = 659 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor; 660 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 661 boundingBox, 662 ); 663 resetSpies(); 664 665 findAndClickElement('.zoom-out-button'); 666 checkZoomedOut(zoomedInFactor); 667 expect(updateViewPositionSpy.calls.mostRecent().args[1]).toEqual( 668 boundingBox, 669 ); 670 }); 671 672 it('handles zoom change via scroll event', () => { 673 component.rects = [rectGroup0]; 674 fixture.detectChanges(); 675 const zoomFactor = 676 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor; 677 resetSpies(); 678 679 const rectsElement = assertDefined(htmlElement.querySelector('rects-view')); 680 681 const zoomInEvent = new WheelEvent('wheel'); 682 Object.defineProperty(zoomInEvent, 'target', { 683 value: htmlElement.querySelector('.large-rects-canvas'), 684 }); 685 Object.defineProperty(zoomInEvent, 'deltaY', {value: 0}); 686 rectsElement.dispatchEvent(zoomInEvent); 687 fixture.detectChanges(); 688 689 checkZoomedIn(zoomFactor); 690 const zoomedInFactor = 691 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor; 692 resetSpies(); 693 694 const zoomOutEvent = new WheelEvent('wheel'); 695 Object.defineProperty(zoomOutEvent, 'target', { 696 value: htmlElement.querySelector('.large-rects-canvas'), 697 }); 698 Object.defineProperty(zoomOutEvent, 'deltaY', {value: 1}); 699 rectsElement.dispatchEvent(zoomOutEvent); 700 fixture.detectChanges(); 701 checkZoomedOut(zoomedInFactor); 702 }); 703 704 it('handles reset button click', () => { 705 component.rects = [rectGroup0]; 706 fixture.detectChanges(); 707 const [camera, boundingBox] = updateViewPositionSpy.calls.mostRecent().args; 708 709 updateRotationSlider(); 710 updateSeparationSlider(); 711 clickZoomInButton(); 712 panView(); 713 resetSpies(); 714 715 findAndClickElement('.reset-button'); 716 checkAllSpiesCalled(1); 717 const [newCamera, newBoundingBox] = 718 updateViewPositionSpy.calls.mostRecent().args; 719 expect(newCamera).toEqual(camera); 720 expect(newBoundingBox).toEqual(boundingBox); 721 }); 722 723 it('handles change in highlighted item', () => { 724 component.rects = [rectGroup0]; 725 fixture.detectChanges(); 726 expect(updateRectsSpy.calls.mostRecent().args[0][0].colorType).toEqual( 727 ColorType.VISIBLE, 728 ); 729 resetSpies(); 730 731 component.highlightedItem = rectGroup0.id; 732 fixture.detectChanges(); 733 734 expect(updateViewPositionSpy).not.toHaveBeenCalled(); 735 expect(updateRectsSpy).toHaveBeenCalledTimes(1); 736 expect(updateLabelsSpy).toHaveBeenCalledTimes(1); 737 expect(renderViewSpy).toHaveBeenCalledTimes(1); 738 expect(updateRectsSpy.calls.mostRecent().args[0][0].colorType).toEqual( 739 ColorType.HIGHLIGHTED, 740 ); 741 }); 742 743 it('handles rect double click', () => { 744 component.rects = [rectGroup0]; 745 fixture.detectChanges(); 746 resetSpies(); 747 748 const testString = 'test_id'; 749 const spy = spyOn(Canvas.prototype, 'getClickedRectId').and.returnValue( 750 undefined, 751 ); 752 let detail: RectDblClickDetail | undefined; 753 htmlElement.addEventListener(ViewerEvents.RectsDblClick, (event) => { 754 detail = (event as CustomEvent).detail; 755 }); 756 757 const canvas = assertDefined( 758 htmlElement.querySelector<HTMLElement>('.large-rects-canvas'), 759 ); 760 canvas.dispatchEvent(new MouseEvent('dblclick')); 761 fixture.detectChanges(); 762 expect(detail).toBeUndefined(); 763 spy.and.returnValue(testString); 764 765 canvas.dispatchEvent(new MouseEvent('dblclick')); 766 fixture.detectChanges(); 767 expect(detail).toEqual(new RectDblClickDetail(testString)); 768 }); 769 770 it('handles mini rect double click', () => { 771 component.rects = [rectGroup0]; 772 fixture.detectChanges(); 773 resetSpies(); 774 775 let miniRectDoubleClick = false; 776 htmlElement.addEventListener(ViewerEvents.MiniRectsDblClick, (event) => { 777 miniRectDoubleClick = true; 778 }); 779 780 const canvas = assertDefined( 781 htmlElement.querySelector<HTMLElement>('.mini-rects-canvas'), 782 ); 783 canvas.dispatchEvent(new MouseEvent('dblclick')); 784 fixture.detectChanges(); 785 expect(miniRectDoubleClick).toBeTrue(); 786 }); 787 788 it('does not render more that selected label if over 30 rects', () => { 789 component.rects = Array.from({length: 30}, () => rectGroup0); 790 fixture.detectChanges(); 791 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(30); 792 793 const newRect = makeRectWithGroupId(0, true, 'new rect'); 794 component.rects = component.rects.concat([newRect]); 795 fixture.detectChanges(); 796 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(0); 797 798 component.highlightedItem = newRect.id; 799 fixture.detectChanges(); 800 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(1); 801 }); 802 803 it('does not render more that selected label if multiple group ids', async () => { 804 component.rects = [rectGroup0]; 805 component.displays = [ 806 {displayId: 0, groupId: 0, name: 'Display 0', isActive: false}, 807 {displayId: 1, groupId: 1, name: 'Display 1', isActive: false}, 808 ]; 809 fixture.detectChanges(); 810 await checkSelectedDisplay([0], [0]); 811 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(1); 812 813 component.rects = component.rects.concat([rectGroup1]); 814 fixture.detectChanges(); 815 openDisplaysSelect(); 816 getDisplayOptions().item(1).click(); 817 await checkSelectedDisplay([0, 1], [0, 1], true); 818 819 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(0); 820 821 component.highlightedItem = rectGroup0.id; 822 fixture.detectChanges(); 823 expect(updateLabelsSpy.calls.mostRecent().args[0].length).toEqual(1); 824 }); 825 826 function resetSpies() { 827 [ 828 updateViewPositionSpy, 829 updateRectsSpy, 830 updateLabelsSpy, 831 renderViewSpy, 832 ].forEach((spy) => spy.calls.reset()); 833 } 834 835 async function checkSelectedDisplay( 836 displayNumbers: number[], 837 testIds: number[], 838 changeInBoundingBox?: boolean, 839 f = fixture, 840 el = htmlElement, 841 ) { 842 f.detectChanges(); 843 await f.whenStable(); 844 f.detectChanges(); 845 const displaySelect = assertDefined(el.querySelector('.displays-select')); 846 expect(displaySelect.textContent?.trim()).toEqual( 847 displayNumbers 848 .map((displayNumber) => `Display ${displayNumber}`) 849 .join(', '), 850 ); 851 const drawnRects = updateRectsSpy.calls.mostRecent().args[0]; 852 expect(drawnRects.length).toEqual(displayNumbers.length); 853 drawnRects.forEach((rect, index) => { 854 expect(rect.id).toEqual(`test-id ${testIds[index]}`); 855 if (index > 0) expect(rect.transform.ty).toBeGreaterThan(0); 856 }); 857 if (changeInBoundingBox) { 858 expect(updateViewPositionSpy.calls.mostRecent().args[1]).not.toEqual( 859 updateViewPositionSpy.calls.argsFor( 860 updateViewPositionSpy.calls.count() - 2, 861 )[1], 862 ); 863 } 864 } 865 866 function findAndClickElement(selector: string) { 867 const el = assertDefined(htmlElement.querySelector<HTMLElement>(selector)); 868 el.click(); 869 fixture.detectChanges(); 870 } 871 872 function checkSliderUnfocusesOnClick(slider: Element, expectedValue: number) { 873 const rectsComponent = assertDefined(component.rectsComponent); 874 slider.dispatchEvent(new MouseEvent('mousedown')); 875 slider.dispatchEvent(new MouseEvent('mouseup')); 876 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 877 htmlElement.dispatchEvent( 878 new KeyboardEvent('keydown', {key: 'ArrowRight'}), 879 ); 880 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 881 htmlElement.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 882 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 883 } 884 885 function updateSeparationSlider() { 886 const rectsComponent = assertDefined(component.rectsComponent); 887 expect(rectsComponent.getZSpacingFactor()).toEqual(1); 888 rectsComponent.onSeparationSliderChange(0.06); 889 fixture.detectChanges(); 890 expect(rectsComponent.getZSpacingFactor()).toEqual(0.06); 891 } 892 893 function updateRotationSlider() { 894 const rectsComponent = assertDefined(component.rectsComponent); 895 rectsComponent.onRotationSliderChange(0.5); 896 fixture.detectChanges(); 897 } 898 899 function updateShadingMode(before: ShadingMode, after: ShadingMode) { 900 const rectsComponent = assertDefined(component.rectsComponent); 901 expect(rectsComponent.getShadingMode()).toEqual(before); 902 findAndClickElement('.right-btn-container button.shading-mode'); 903 expect(rectsComponent.getShadingMode()).toEqual(after); 904 } 905 906 function makeRectWithGroupId( 907 groupId: number, 908 isVisible = true, 909 id?: string, 910 ): UiRect { 911 return new UiRectBuilder() 912 .setX(0) 913 .setY(0) 914 .setWidth(1) 915 .setHeight(1) 916 .setLabel('rectangle1') 917 .setTransform( 918 TransformMatrix.from({ 919 dsdx: 1, 920 dsdy: 0, 921 dtdx: 0, 922 dtdy: 1, 923 tx: 0, 924 ty: 0, 925 }), 926 ) 927 .setIsVisible(isVisible) 928 .setIsDisplay(false) 929 .setIsActiveDisplay(false) 930 .setId(id ?? 'test-id ' + groupId) 931 .setGroupId(groupId) 932 .setIsClickable(true) 933 .setCornerRadius(0) 934 .setDepth(0) 935 .setOpacity(0.5) 936 .build(); 937 } 938 939 function panView() { 940 const canvas = assertDefined( 941 htmlElement.querySelector<HTMLElement>('.large-rects-canvas'), 942 ); 943 canvas.dispatchEvent(new MouseEvent('mousedown')); 944 const mouseMoveEvent = new MouseEvent('mousemove'); 945 Object.defineProperty(mouseMoveEvent, 'movementX', {value: 5}); 946 Object.defineProperty(mouseMoveEvent, 'movementY', {value: 10}); 947 document.dispatchEvent(mouseMoveEvent); 948 document.dispatchEvent(new MouseEvent('mouseup')); 949 fixture.detectChanges(); 950 } 951 952 function clickZoomInButton() { 953 findAndClickElement('.zoom-in-button'); 954 } 955 956 function clickLargeRectsCanvas() { 957 findAndClickElement('.large-rects-canvas'); 958 } 959 960 function checkZoomedIn(oldZoomFactor: number) { 961 expect(updateRectsSpy).toHaveBeenCalledTimes(0); 962 expect(updateLabelsSpy).toHaveBeenCalledTimes(1); 963 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 964 expect(renderViewSpy).toHaveBeenCalledTimes(1); 965 expect( 966 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor, 967 ).toBeGreaterThan(oldZoomFactor); 968 } 969 970 function checkZoomedOut(oldZoomFactor: number) { 971 expect(updateRectsSpy).toHaveBeenCalledTimes(0); 972 expect(updateLabelsSpy).toHaveBeenCalledTimes(1); 973 expect(updateViewPositionSpy).toHaveBeenCalledTimes(1); 974 expect(renderViewSpy).toHaveBeenCalledTimes(1); 975 expect( 976 updateViewPositionSpy.calls.mostRecent().args[0].zoomFactor, 977 ).toBeLessThan(oldZoomFactor); 978 } 979 980 function openDisplaysSelect() { 981 findAndClickElement('.displays-section .mat-select-trigger'); 982 } 983 984 function getDisplayOptions() { 985 return document.querySelectorAll<HTMLElement>( 986 '.mat-select-panel .mat-option', 987 ); 988 } 989 990 function checkAllSpiesCalled(times: number) { 991 [ 992 updateViewPositionSpy, 993 updateRectsSpy, 994 updateLabelsSpy, 995 renderViewSpy, 996 ].forEach((spy) => expect(spy).toHaveBeenCalledTimes(times)); 997 } 998 999 @Component({ 1000 selector: 'host-component', 1001 template: ` 1002 <rects-view 1003 title="TestRectsView" 1004 [store]="store" 1005 [rects]="rects" 1006 [miniRects]="miniRects" 1007 [displays]="displays" 1008 [isStackBased]="isStackBased" 1009 [shadingModes]="shadingModes" 1010 [userOptions]="userOptions" 1011 [dependencies]="dependencies" 1012 [pinnedItems]="pinnedItems" 1013 [isDarkMode]="isDarkMode" 1014 [highlightedItem]="highlightedItem"></rects-view> 1015 `, 1016 }) 1017 class TestHostComponent { 1018 store = new PersistentStore(); 1019 rects: UiRect[] = []; 1020 displays: DisplayIdentifier[] = []; 1021 miniRects: UiRect[] = []; 1022 isStackBased = false; 1023 shadingModes = [ 1024 ShadingMode.GRADIENT, 1025 ShadingMode.WIRE_FRAME, 1026 ShadingMode.OPACITY, 1027 ]; 1028 userOptions = { 1029 showOnlyVisible: { 1030 name: 'Show only', 1031 chip: VISIBLE_CHIP, 1032 enabled: false, 1033 }, 1034 }; 1035 dependencies = [TraceType.SURFACE_FLINGER]; 1036 pinnedItems: UiHierarchyTreeNode[] = []; 1037 isDarkMode = false; 1038 highlightedItem = ''; 1039 1040 @ViewChild(RectsComponent) 1041 rectsComponent: RectsComponent | undefined; 1042 } 1043}); 1044