Adjust behaviour of pinned nodes/rect clicks.

Auto-expand and scroll to nodes in main tree when selected from pinned
list or rect.

Fixes: 399470013
Test: npm run test:unit:ci
Change-Id: I29536a10bf5d1c73c8137c5b16deba782beb9856
diff --git a/tools/winscope/src/viewers/components/tree_component.ts b/tools/winscope/src/viewers/components/tree_component.ts
index 1b0950b..9753941 100644
--- a/tools/winscope/src/viewers/components/tree_component.ts
+++ b/tools/winscope/src/viewers/components/tree_component.ts
@@ -15,6 +15,7 @@
  */
 import {
   ChangeDetectionStrategy,
+  ChangeDetectorRef,
   Component,
   ElementRef,
   EventEmitter,
@@ -88,7 +89,8 @@
         (highlightedChange)="propagateNewHighlightedItem($event)"
         (pinnedItemChange)="propagateNewPinnedItem($event)"
         (hoverStart)="childHover = true"
-        (hoverEnd)="childHover = false"></tree-view>
+        (hoverEnd)="childHover = false"
+        (expandParent)="expandTree()"></tree-view>
     </div>
   `,
   styles: [nodeStyles, treeNodeDataViewStyles, nodeInnerItemStyles],
@@ -116,12 +118,13 @@
   @Output() readonly pinnedItemChange = new EventEmitter<UiHierarchyTreeNode>();
   @Output() readonly hoverStart = new EventEmitter<void>();
   @Output() readonly hoverEnd = new EventEmitter<void>();
+  @Output() readonly expandParent = new EventEmitter<void>();
 
-  localExpandedState = true;
   childHover = false;
   readonly levelOffset = 24;
   nodeElement: HTMLElement;
 
+  private localExpandedState = true;
   private storeKeyCollapsedState = '';
 
   childTrackById(
@@ -131,7 +134,10 @@
     return child.id;
   }
 
-  constructor(@Inject(ElementRef) public elementRef: ElementRef) {
+  constructor(
+    @Inject(ElementRef) public elementRef: ElementRef,
+    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
+  ) {
     this.nodeElement = elementRef.nativeElement.querySelector('.node');
     this.nodeElement?.addEventListener(
       'mousedown',
@@ -237,6 +243,8 @@
 
   expandTree() {
     this.setExpandedValue(true);
+    this.changeDetectorRef.detectChanges();
+    this.expandParent.emit();
   }
 
   isExpanded() {
diff --git a/tools/winscope/src/viewers/components/tree_component_test.ts b/tools/winscope/src/viewers/components/tree_component_test.ts
index 80728c7..ae12116 100644
--- a/tools/winscope/src/viewers/components/tree_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_component_test.ts
@@ -15,7 +15,7 @@
  */
 
 import {Clipboard, ClipboardModule} from '@angular/cdk/clipboard';
-import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core';
+import {Component, ViewChild} from '@angular/core';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {MatIconModule} from '@angular/material/icon';
 import {MatTooltipModule} from '@angular/material/tooltip';
@@ -49,7 +49,6 @@
         PropertyTreeNodeDataViewComponent,
       ],
       imports: [MatTooltipModule, MatIconModule, ClipboardModule],
-      schemas: [CUSTOM_ELEMENTS_SCHEMA],
     }).compileComponents();
     fixture = TestBed.createComponent(TestHostComponent);
     component = fixture.componentInstance;
@@ -63,17 +62,18 @@
 
   it('shows node', () => {
     fixture.detectChanges();
-    const treeNode = htmlElement.querySelector('tree-node');
-    expect(treeNode).toBeTruthy();
+    expect(htmlElement.querySelector('tree-node')).toBeTruthy();
   });
 
   it('can identify if a parent node has a selected child', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    expect(treeComponent.hasSelectedChild()).toBeFalse();
+    const treeNode = assertDefined(
+      htmlElement.querySelector<HTMLElement>('tree-node'),
+    );
+    expect(treeNode.className.includes('child-selected')).toBeFalse();
     component.highlightedItem = '3 Child3';
     fixture.detectChanges();
-    expect(treeComponent.hasSelectedChild()).toBeTrue();
+    expect(treeNode.className.includes('child-selected')).toBeTrue();
   });
 
   it('highlights node and inner node upon click', () => {
@@ -97,29 +97,23 @@
 
   it('toggles tree upon node double click', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
+    const toggleButton = assertDefined(
+      htmlElement.querySelector('.toggle-tree-btn'),
     );
-    const currLocalExpandedState = treeComponent.localExpandedState;
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(!currLocalExpandedState).toEqual(treeComponent.localExpandedState);
+    expect(toggleButton.textContent?.trim()).toEqual('arrow_drop_down');
+    checkIsExpanded(true);
+
+    doubleClickFirstNode();
+    expect(toggleButton.textContent?.trim()).toEqual('chevron_right');
+    checkIsExpanded(false);
   });
 
   it('does not toggle tree in flat mode on double click', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
     component.isFlattened = true;
     fixture.detectChanges();
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
-    );
-
-    const currLocalExpandedState = treeComponent.localExpandedState;
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(currLocalExpandedState).toEqual(treeComponent.localExpandedState);
+    doubleClickFirstNode();
+    checkIsExpanded(true);
   });
 
   it('pins node on click', () => {
@@ -138,66 +132,73 @@
 
   it('expands tree on expand tree button click', () => {
     fixture.detectChanges();
-    const treeNode = assertDefined(
-      htmlElement.querySelector<HTMLElement>('tree-node'),
-    );
-    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
-    fixture.detectChanges();
-    expect(component.treeComponent?.localExpandedState).toEqual(false);
+    doubleClickFirstNode();
+    checkIsExpanded(false);
+
     assertDefined(
       htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
     ).click();
     fixture.detectChanges();
-    expect(component.treeComponent?.localExpandedState).toEqual(true);
+    checkIsExpanded(true);
+  });
+
+  it('expands tree recursively on node selection', () => {
+    fixture.detectChanges();
+    doubleClickFirstNode();
+    checkIsExpanded(false);
+    component.highlightedItem = '79 Child79';
+    fixture.detectChanges();
+    checkIsExpanded(true);
   });
 
   it('scrolls selected node only if not in view', () => {
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
-    const treeNode = assertDefined(
-      treeComponent.elementRef.nativeElement.querySelector(`#nodeChild79`),
-    );
+    checkNodeScrolling();
+  });
 
-    component.highlightedItem = 'Root node';
+  it('scrolls selected node if not in view even if pinned', () => {
+    component.pinnedItems = [
+      assertDefined(component.tree.getChildByName('Child78')),
+      assertDefined(component.tree.getChildByName('Child79')),
+    ];
     fixture.detectChanges();
-
-    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
-    component.highlightedItem = '79 Child79';
-    fixture.detectChanges();
-    expect(spy).toHaveBeenCalledTimes(1);
-
-    component.highlightedItem = '78 Child78';
-    fixture.detectChanges();
-    expect(spy).toHaveBeenCalledTimes(1);
+    checkNodeScrolling();
   });
 
   it('sets initial expanded state to true by default for leaf', () => {
     fixture.detectChanges();
-    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
+    checkIsExpanded(true);
   });
 
   it('sets initial expanded state to true by default for non root', () => {
-    component.tree = component.tree.getAllChildren()[0];
+    const child = component.tree.getAllChildren()[0] as UiHierarchyTreeNode;
+    const innerChild = UiHierarchyTreeNode.from(
+      new HierarchyTreeBuilder()
+        .setId('InnerChild')
+        .setName('child')
+        .setChildren([])
+        .build(),
+    );
+    child.addOrReplaceChild(innerChild);
+    component.tree = child;
     fixture.detectChanges();
-    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
+    checkIsExpanded(true);
   });
 
   it('sets initial expanded state to false if collapse state exists in store', () => {
     component.useStoredExpandedState = true;
     fixture.detectChanges();
-    const treeComponent = assertDefined(component.treeComponent);
     // tree expanded by default
-    expect(treeComponent.isExpanded()).toBeTrue();
+    checkIsExpanded(true);
 
     // tree collapsed
-    treeComponent.toggleTree();
-    fixture.detectChanges();
-    expect(treeComponent.isExpanded()).toBeFalse();
+    doubleClickFirstNode();
+    checkIsExpanded(false);
 
     // tree collapsed state retained
     component.tree = makeTree();
     fixture.detectChanges();
-    expect(treeComponent.isExpanded()).toBeFalse();
+    checkIsExpanded(false);
   });
 
   it('renders show state button if applicable', () => {
@@ -313,6 +314,35 @@
     );
   }
 
+  function doubleClickFirstNode() {
+    assertDefined(
+      htmlElement.querySelector<HTMLElement>('tree-node'),
+    ).dispatchEvent(new MouseEvent('click', {detail: 2}));
+    fixture.detectChanges();
+  }
+
+  function checkIsExpanded(isExpanded: boolean) {
+    expect(htmlElement.querySelector<HTMLElement>('.children')?.hidden).toEqual(
+      !isExpanded,
+    );
+  }
+
+  function checkNodeScrolling() {
+    const treeNode = assertDefined(htmlElement.querySelector(`#nodeChild79`));
+    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
+
+    component.highlightedItem = 'Root node';
+    fixture.detectChanges();
+
+    component.highlightedItem = '79 Child79';
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+
+    component.highlightedItem = '78 Child78';
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+  }
+
   @Component({
     selector: 'host-component',
     template: `
@@ -320,7 +350,7 @@
       <tree-view
         [node]="tree"
         [isFlattened]="isFlattened"
-        [isPinned]="false"
+        [pinnedItems]="pinnedItems"
         [highlightedItem]="highlightedItem"
         [useStoredExpandedState]="useStoredExpandedState"
         [itemsClickable]="true"
@@ -342,6 +372,7 @@
     isFlattened = false;
     useStoredExpandedState = false;
     rectIdToShowState: Map<string, RectShowState> | undefined;
+    pinnedItems: Array<UiHierarchyTreeNode | UiPropertyTreeNode> = [];
 
     constructor() {
       this.tree = makeTree();
diff --git a/tools/winscope/src/viewers/components/tree_node_component.ts b/tools/winscope/src/viewers/components/tree_node_component.ts
index 9de6728..820f4ac 100644
--- a/tools/winscope/src/viewers/components/tree_node_component.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component.ts
@@ -106,7 +106,7 @@
 
   @Output() readonly toggleTreeChange = new EventEmitter<void>();
   @Output() readonly rectShowStateChange = new EventEmitter<void>();
-  @Output() readonly expandTreeChange = new EventEmitter<boolean>();
+  @Output() readonly expandTreeChange = new EventEmitter<void>();
   @Output() readonly pinNodeChange = new EventEmitter<UiHierarchyTreeNode>();
 
   collapseDiffClass = '';
@@ -123,8 +123,11 @@
   }
 
   ngOnChanges() {
+    if (!this.isInPinnedSection && this.isSelected) {
+      this.expandTreeChange.emit();
+    }
     this.collapseDiffClass = this.updateCollapseDiffClass();
-    if (!this.isPinned && this.isSelected && !this.isNodeInView()) {
+    if (!this.isInPinnedSection && this.isSelected && !this.isNodeInView()) {
       this.el.scrollIntoView({block: 'center', inline: 'nearest'});
     }
   }
@@ -184,7 +187,7 @@
     this.pinNodeChange.emit(assertDefined(this.node) as UiHierarchyTreeNode);
   }
 
-  updateCollapseDiffClass() {
+  updateCollapseDiffClass(): string {
     if (this.isExpanded) {
       return '';
     }
@@ -199,7 +202,7 @@
       return '';
     }
     if (childrenDiffClasses.size === 1) {
-      const diffType = childrenDiffClasses.values().next().value;
+      const diffType = assertDefined(childrenDiffClasses.values().next().value);
       return diffType;
     }
     return DiffType.MODIFIED;
diff --git a/tools/winscope/src/viewers/components/tree_node_component_test.ts b/tools/winscope/src/viewers/components/tree_node_component_test.ts
index b5cf8ae..9df330b 100644
--- a/tools/winscope/src/viewers/components/tree_node_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component_test.ts
@@ -117,6 +117,24 @@
     expect(spy).toHaveBeenCalled();
   });
 
+  it('can trigger tree expansion if node is selected and not in pinned section', () => {
+    const spy = spyOn(
+      assertDefined(component.treeNodeComponent).expandTreeChange,
+      'emit',
+    );
+    component.isInPinnedSection = true;
+    component.isSelected = true;
+    fixture.detectChanges();
+    expect(spy).not.toHaveBeenCalled();
+
+    component.isSelected = false;
+    component.isInPinnedSection = false;
+    fixture.detectChanges();
+    component.isSelected = true;
+    fixture.detectChanges();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
   it('assigns diff css classes to expand tree button', () => {
     const expandButton = assertDefined(
       htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
@@ -221,7 +239,7 @@
         [node]="node"
         [isExpanded]="isExpanded"
         [isPinned]="false"
-        [isInPinnedSection]="false"
+        [isInPinnedSection]="isInPinnedSection"
         [isSelected]="isSelected"
         [isLeaf]="isLeaf"></tree-node>
     `,
@@ -238,6 +256,7 @@
     isSelected = false;
     isLeaf = false;
     isExpanded = false;
+    isInPinnedSection = false;
 
     @ViewChild(TreeNodeComponent)
     treeNodeComponent: TreeNodeComponent | undefined;