xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/rects/rects_component_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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