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 {HttpClientModule} from '@angular/common/http'; 19import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core'; 20import {ComponentFixture, TestBed} from '@angular/core/testing'; 21import {MatButtonModule} from '@angular/material/button'; 22import {MatDividerModule} from '@angular/material/divider'; 23import {MatFormFieldModule} from '@angular/material/form-field'; 24import {MatIconModule} from '@angular/material/icon'; 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 {PersistentStore} from 'common/persistent_store'; 31import {TraceType} from 'trace/trace_type'; 32import {VISIBLE_CHIP} from 'viewers/common/chip'; 33import {DisplayIdentifier} from 'viewers/common/display_identifier'; 34import {ViewerEvents} from 'viewers/common/viewer_events'; 35import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component'; 36import {RectsComponent} from 'viewers/components/rects/rects_component'; 37import {UiRect} from 'viewers/components/rects/types2d'; 38import {UserOptionsComponent} from 'viewers/components/user_options_component'; 39import {Canvas} from './canvas'; 40import {ColorType, ShadingMode} from './types3d'; 41import {UiRectBuilder} from './ui_rect_builder'; 42 43describe('RectsComponent', () => { 44 let component: TestHostComponent; 45 let fixture: ComponentFixture<TestHostComponent>; 46 let htmlElement: HTMLElement; 47 48 beforeEach(async () => { 49 localStorage.clear(); 50 51 await TestBed.configureTestingModule({ 52 imports: [ 53 CommonModule, 54 MatDividerModule, 55 MatSliderModule, 56 MatButtonModule, 57 MatTooltipModule, 58 MatIconModule, 59 HttpClientModule, 60 MatSelectModule, 61 BrowserAnimationsModule, 62 MatFormFieldModule, 63 ], 64 declarations: [ 65 TestHostComponent, 66 RectsComponent, 67 CollapsibleSectionTitleComponent, 68 UserOptionsComponent, 69 ], 70 schemas: [CUSTOM_ELEMENTS_SCHEMA], 71 }).compileComponents(); 72 73 fixture = TestBed.createComponent(TestHostComponent); 74 component = fixture.componentInstance; 75 htmlElement = fixture.nativeElement; 76 }); 77 78 it('can be created', () => { 79 expect(component).toBeTruthy(); 80 }); 81 82 it('renders rotation slider', () => { 83 const slider = htmlElement.querySelector('mat-slider.slider-rotation'); 84 expect(slider).toBeTruthy(); 85 }); 86 87 it('renders separation slider', () => { 88 const slider = htmlElement.querySelector('mat-slider.slider-spacing'); 89 expect(slider).toBeTruthy(); 90 }); 91 92 it('renders canvas', () => { 93 const rectsCanvas = htmlElement.querySelector('.large-rects-canvas'); 94 expect(rectsCanvas).toBeTruthy(); 95 }); 96 97 it('draws scene when input data changes', async () => { 98 fixture.detectChanges(); 99 spyOn(Canvas.prototype, 'draw').and.callThrough(); 100 101 const inputRect = makeRectWithGroupId(0); 102 103 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(0); 104 component.rects = [inputRect]; 105 fixture.detectChanges(); 106 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(1); 107 component.rects = [inputRect]; 108 fixture.detectChanges(); 109 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(2); 110 }); 111 112 it('draws scene when rotation slider changes', () => { 113 fixture.detectChanges(); 114 spyOn(Canvas.prototype, 'draw').and.callThrough(); 115 const slider = assertDefined(htmlElement.querySelector('.slider-rotation')); 116 117 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(0); 118 119 slider.dispatchEvent(new MouseEvent('mousedown')); 120 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(1); 121 }); 122 123 it('draws scene when spacing slider changes', () => { 124 fixture.detectChanges(); 125 spyOn(Canvas.prototype, 'draw').and.callThrough(); 126 const slider = assertDefined(htmlElement.querySelector('.slider-spacing')); 127 128 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(0); 129 130 slider.dispatchEvent(new MouseEvent('mousedown')); 131 expect(Canvas.prototype.draw).toHaveBeenCalledTimes(1); 132 }); 133 134 it('unfocuses spacing slider on click', () => { 135 fixture.detectChanges(); 136 const spacingSlider = assertDefined( 137 htmlElement.querySelector('.slider-spacing'), 138 ); 139 checkSliderUnfocusesOnClick(spacingSlider, 0.02); 140 }); 141 142 it('unfocuses rotation slider on click', () => { 143 fixture.detectChanges(); 144 const rotationSlider = assertDefined( 145 htmlElement.querySelector('.slider-rotation'), 146 ); 147 checkSliderUnfocusesOnClick(rotationSlider, 1); 148 }); 149 150 it('renders display selector', () => { 151 component.displays = [ 152 {displayId: 0, groupId: 0, name: 'Display 0'}, 153 {displayId: 1, groupId: 1, name: 'Display 1'}, 154 {displayId: 2, groupId: 2, name: 'Display 2'}, 155 ]; 156 fixture.detectChanges(); 157 checkSelectedDisplay('Display 0'); 158 }); 159 160 it('handles display change', () => { 161 component.displays = [ 162 {displayId: 0, groupId: 0, name: 'Display 0'}, 163 {displayId: 1, groupId: 1, name: 'Display 1'}, 164 {displayId: 2, groupId: 2, name: 'Display 2'}, 165 ]; 166 fixture.detectChanges(); 167 checkSelectedDisplay('Display 0'); 168 169 const trigger = assertDefined( 170 htmlElement.querySelector('.displays-section .mat-select-trigger'), 171 ); 172 (trigger as HTMLElement).click(); 173 fixture.detectChanges(); 174 175 let groupId = 0; 176 htmlElement.addEventListener(ViewerEvents.RectGroupIdChange, (event) => { 177 groupId = (event as CustomEvent).detail.groupId; 178 }); 179 180 const option = document 181 .querySelectorAll('.mat-select-panel .mat-option') 182 .item(1); 183 (option as HTMLElement).click(); 184 fixture.detectChanges(); 185 checkSelectedDisplay('Display 1'); 186 expect(groupId).toEqual(1); 187 }); 188 189 it('tracks selected display', () => { 190 component.displays = [ 191 {displayId: 10, groupId: 0, name: 'Display 0'}, 192 {displayId: 20, groupId: 1, name: 'Display 1'}, 193 ]; 194 fixture.detectChanges(); 195 checkSelectedDisplay('Display 0'); 196 197 component.displays = [ 198 {displayId: 20, groupId: 2, name: 'Display 1'}, 199 {displayId: 10, groupId: 1, name: 'Display 0'}, 200 ]; 201 fixture.detectChanges(); 202 checkSelectedDisplay('Display 0'); 203 }); 204 205 it('updates scene on separation slider change', () => { 206 const inputRect = makeRectWithGroupId(0); 207 component.rects = [inputRect, inputRect]; 208 const spy = spyOn(Canvas.prototype, 'draw').and.callThrough(); 209 fixture.detectChanges(); 210 updateSeparationSlider(); 211 212 expect(spy).toHaveBeenCalledTimes(2); 213 const sceneBefore = assertDefined(spy.calls.first().args.at(0)); 214 const sceneAfter = assertDefined(spy.calls.mostRecent().args.at(0)); 215 216 expect(sceneBefore.rects[1].topLeft.z).toEqual(5); 217 expect(sceneAfter.rects[1].topLeft.z).toEqual(0.3); 218 }); 219 220 it('updates scene on rotation slider change', () => { 221 const inputRect = makeRectWithGroupId(0); 222 component.rects = [inputRect]; 223 const spy = spyOn(Canvas.prototype, 'draw').and.callThrough(); 224 fixture.detectChanges(); 225 updateRotationSlider(); 226 227 expect(spy).toHaveBeenCalledTimes(2); 228 const sceneBefore = assertDefined(spy.calls.first().args.at(0)); 229 const sceneAfter = assertDefined(spy.calls.mostRecent().args.at(0)); 230 231 expect(sceneBefore.camera.rotationFactor).toEqual(1); 232 expect(sceneAfter.camera.rotationFactor).toEqual(0.5); 233 }); 234 235 it('updates scene on shading mode change', () => { 236 const inputRect = makeRectWithGroupId(0); 237 component.rects = [inputRect]; 238 const spy = spyOn(Canvas.prototype, 'draw').and.callThrough(); 239 fixture.detectChanges(); 240 241 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 242 updateShadingMode(ShadingMode.WIRE_FRAME, ShadingMode.OPACITY); 243 244 expect(spy).toHaveBeenCalledTimes(3); 245 const sceneGradient = assertDefined(spy.calls.first().args.at(0)); 246 const sceneWireFrame = assertDefined(spy.calls.argsFor(1).at(0)); 247 const sceneOpacity = assertDefined(spy.calls.mostRecent().args.at(0)); 248 249 expect(sceneGradient.rects[0].colorType).toEqual(ColorType.VISIBLE); 250 expect(sceneGradient.rects[0].darkFactor).toEqual(1); 251 252 expect(sceneWireFrame.rects[0].colorType).toEqual(ColorType.EMPTY); 253 expect(sceneWireFrame.rects[0].darkFactor).toEqual(1); 254 255 expect(sceneOpacity.rects[0].colorType).toEqual( 256 ColorType.VISIBLE_WITH_OPACITY, 257 ); 258 expect(sceneOpacity.rects[0].darkFactor).toEqual(0.5); 259 }); 260 261 it('uses stored rects view settings', () => { 262 fixture.detectChanges(); 263 264 updateSeparationSlider(); 265 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 266 267 const newFixture = TestBed.createComponent(TestHostComponent); 268 newFixture.detectChanges(); 269 const newRectsComponent = assertDefined( 270 newFixture.componentInstance.rectsComponent, 271 ); 272 expect(newRectsComponent.getZSpacingFactor()).toEqual(0.06); 273 expect(newRectsComponent.getShadingMode()).toEqual(ShadingMode.WIRE_FRAME); 274 }); 275 276 it('defaults initial selection to first display with non-display rects and groupId 0', () => { 277 const inputRect = makeRectWithGroupId(0); 278 component.rects = [inputRect]; 279 component.displays = [ 280 {displayId: 10, groupId: 1, name: 'Display 0'}, 281 {displayId: 20, groupId: 0, name: 'Display 1'}, 282 ]; 283 fixture.detectChanges(); 284 checkSelectedDisplay('Display 1'); 285 }); 286 287 it('defaults initial selection to first display with non-display rects and groupId non-zero', () => { 288 const inputRect = makeRectWithGroupId(1); 289 component.rects = [inputRect]; 290 component.displays = [ 291 {displayId: 10, groupId: 0, name: 'Display 0'}, 292 {displayId: 20, groupId: 1, name: 'Display 1'}, 293 ]; 294 fixture.detectChanges(); 295 checkSelectedDisplay('Display 1'); 296 }); 297 298 it('draws mini rects with non-present group id', () => { 299 fixture.detectChanges(); 300 const inputRect = makeRectWithGroupId(0); 301 const miniRect = makeRectWithGroupId(2); 302 component.rects = [inputRect]; 303 component.displays = [{displayId: 10, groupId: 0, name: 'Display 0'}]; 304 component.miniRects = [miniRect]; 305 const spy = spyOn(Canvas.prototype, 'draw').and.callThrough(); 306 fixture.detectChanges(); 307 expect(spy).toHaveBeenCalledTimes(2); 308 expect( 309 spy.calls 310 .all() 311 .forEach((call) => expect(call.args[0].rects.length).toEqual(1)), 312 ); 313 }); 314 315 it('draws mini rects with default spacing, rotation and shading mode', () => { 316 fixture.detectChanges(); 317 318 updateSeparationSlider(); 319 updateRotationSlider(); 320 updateShadingMode(ShadingMode.GRADIENT, ShadingMode.WIRE_FRAME); 321 322 const inputRect = makeRectWithGroupId(0); 323 component.rects = [inputRect, inputRect]; 324 component.displays = [{displayId: 10, groupId: 0, name: 'Display 0'}]; 325 component.miniRects = [inputRect, inputRect]; 326 const spy = spyOn(Canvas.prototype, 'draw').and.callThrough(); 327 fixture.detectChanges(); 328 expect(spy).toHaveBeenCalledTimes(2); 329 330 const largeRectsScene = assertDefined(spy.calls.first().args.at(0)); 331 const miniRectsScene = assertDefined(spy.calls.mostRecent().args.at(0)); 332 333 expect(largeRectsScene.camera.rotationFactor).toEqual(0.5); 334 expect(miniRectsScene.camera.rotationFactor).toEqual(1); 335 336 expect(largeRectsScene.rects[0].colorType).toEqual(ColorType.EMPTY); 337 expect(miniRectsScene.rects[0].colorType).toEqual(ColorType.VISIBLE); 338 339 expect(largeRectsScene.rects[1].topLeft.z).toEqual(0.3); 340 expect(miniRectsScene.rects[1].topLeft.z).toEqual(5); 341 }); 342 343 it('handles collapse button click', () => { 344 fixture.detectChanges(); 345 const spy = spyOn( 346 assertDefined(component.rectsComponent).collapseButtonClicked, 347 'emit', 348 ); 349 const collapseButton = assertDefined( 350 htmlElement.querySelector('collapsible-section-title button'), 351 ) as HTMLButtonElement; 352 collapseButton.click(); 353 fixture.detectChanges(); 354 expect(spy).toHaveBeenCalled(); 355 }); 356 357 function checkSelectedDisplay(selectedDisplay: string) { 358 const displaySelect = assertDefined( 359 htmlElement.querySelector('.displays-select'), 360 ); 361 expect(displaySelect.innerHTML).toContain(selectedDisplay); 362 } 363 364 function findAndClickElement(selector: string) { 365 const el = assertDefined( 366 htmlElement.querySelector(selector), 367 ) as HTMLElement; 368 el.click(); 369 fixture.detectChanges(); 370 } 371 372 function checkSliderUnfocusesOnClick(slider: Element, expectedValue: number) { 373 const rectsComponent = assertDefined(component.rectsComponent); 374 slider.dispatchEvent(new MouseEvent('mousedown')); 375 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 376 htmlElement.dispatchEvent( 377 new KeyboardEvent('keydown', {key: 'ArrowRight'}), 378 ); 379 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 380 htmlElement.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 381 expect(rectsComponent.getZSpacingFactor()).toEqual(expectedValue); 382 } 383 384 function updateSeparationSlider() { 385 const rectsComponent = assertDefined(component.rectsComponent); 386 expect(rectsComponent.getZSpacingFactor()).toEqual(1); 387 rectsComponent.onSeparationSliderChange(0.06); 388 fixture.detectChanges(); 389 expect(rectsComponent.getZSpacingFactor()).toEqual(0.06); 390 } 391 392 function updateRotationSlider() { 393 const rectsComponent = assertDefined(component.rectsComponent); 394 rectsComponent.onRotationSliderChange(0.5); 395 fixture.detectChanges(); 396 } 397 398 function updateShadingMode(before: ShadingMode, after: ShadingMode) { 399 const rectsComponent = assertDefined(component.rectsComponent); 400 expect(rectsComponent.getShadingMode()).toEqual(before); 401 findAndClickElement('.right-btn-container button.shading-mode'); 402 expect(rectsComponent.getShadingMode()).toEqual(after); 403 } 404 405 function makeRectWithGroupId(groupId: number, isVisible = true): UiRect { 406 return new UiRectBuilder() 407 .setX(0) 408 .setY(0) 409 .setWidth(1) 410 .setHeight(1) 411 .setLabel('rectangle1') 412 .setTransform({ 413 dsdx: 1, 414 dsdy: 0, 415 dtdx: 0, 416 dtdy: 1, 417 tx: 0, 418 ty: 0, 419 }) 420 .setIsVisible(isVisible) 421 .setIsDisplay(false) 422 .setId('test-id-1234') 423 .setGroupId(groupId) 424 .setIsVirtual(false) 425 .setIsClickable(false) 426 .setCornerRadius(0) 427 .setDepth(0) 428 .setOpacity(0.5) 429 .build(); 430 } 431 432 @Component({ 433 selector: 'host-component', 434 template: ` 435 <rects-view 436 title="TestRectsView" 437 [store]="store" 438 [rects]="rects" 439 [isStackBased]="isStackBased" 440 [displays]="displays" 441 [miniRects]="miniRects" 442 [shadingModes]="shadingModes" 443 [userOptions]="userOptions" 444 [dependencies]="dependencies"></rects-view> 445 `, 446 }) 447 class TestHostComponent { 448 store = new PersistentStore(); 449 rects: UiRect[] = []; 450 displays: DisplayIdentifier[] = []; 451 miniRects: UiRect[] = []; 452 isStackBased = false; 453 shadingModes = [ 454 ShadingMode.GRADIENT, 455 ShadingMode.WIRE_FRAME, 456 ShadingMode.OPACITY, 457 ]; 458 userOptions = { 459 showOnlyVisible: { 460 name: 'Show only', 461 chip: VISIBLE_CHIP, 462 enabled: false, 463 }, 464 }; 465 dependencies = [TraceType.SURFACE_FLINGER]; 466 467 @ViewChild(RectsComponent) 468 rectsComponent: RectsComponent | undefined; 469 } 470}); 471