1/* 2 * Copyright (C) 2023 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 {ScrollingModule} from '@angular/cdk/scrolling'; 18import { 19 ComponentFixture, 20 ComponentFixtureAutoDetect, 21 TestBed, 22} from '@angular/core/testing'; 23import {MatDividerModule} from '@angular/material/divider'; 24import {MatIconModule} from '@angular/material/icon'; 25import {assertDefined} from 'common/assert_utils'; 26import {TracePositionUpdate} from 'messaging/winscope_event'; 27import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; 28import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 29import {UnitTestUtils} from 'test/unit/utils'; 30import {Parser} from 'trace/parser'; 31import {Trace} from 'trace/trace'; 32import {Traces} from 'trace/traces'; 33import {TracePosition} from 'trace/trace_position'; 34import {TraceType} from 'trace/trace_type'; 35import {Transition} from 'trace/transition'; 36import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters'; 37import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 38import {TimestampClickDetail, ViewerEvents} from 'viewers/common/viewer_events'; 39import {CollapsedSectionsComponent} from 'viewers/components/collapsed_sections_component'; 40import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component'; 41import {PropertiesComponent} from 'viewers/components/properties_component'; 42import {PropertyTreeNodeDataViewComponent} from 'viewers/components/property_tree_node_data_view_component'; 43import {TreeComponent} from 'viewers/components/tree_component'; 44import {TreeNodeComponent} from 'viewers/components/tree_node_component'; 45import {Presenter} from './presenter'; 46import {UiData} from './ui_data'; 47import {ViewerTransitionsComponent} from './viewer_transitions_component'; 48 49describe('ViewerTransitionsComponent', () => { 50 let fixture: ComponentFixture<ViewerTransitionsComponent>; 51 let component: ViewerTransitionsComponent; 52 let htmlElement: HTMLElement; 53 54 beforeEach(async () => { 55 await TestBed.configureTestingModule({ 56 providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], 57 imports: [MatDividerModule, ScrollingModule, MatIconModule], 58 declarations: [ 59 ViewerTransitionsComponent, 60 TreeComponent, 61 TreeNodeComponent, 62 PropertyTreeNodeDataViewComponent, 63 PropertiesComponent, 64 CollapsedSectionsComponent, 65 CollapsibleSectionTitleComponent, 66 ], 67 schemas: [], 68 }).compileComponents(); 69 70 fixture = TestBed.createComponent(ViewerTransitionsComponent); 71 component = fixture.componentInstance; 72 htmlElement = fixture.nativeElement; 73 74 component.uiData = makeUiData(); 75 fixture.detectChanges(); 76 }); 77 78 it('can be created', () => { 79 expect(component).toBeTruthy(); 80 }); 81 82 it('renders entries', () => { 83 expect(htmlElement.querySelector('.scroll')).toBeTruthy(); 84 85 const entry = assertDefined(htmlElement.querySelector('.scroll .entry')); 86 expect(entry.innerHTML).toContain('TO_FRONT'); 87 expect(entry.innerHTML).toContain('10ns'); 88 }); 89 90 it('shows message when no transition is selected', () => { 91 expect( 92 htmlElement.querySelector('.properties-view .placeholder-text') 93 ?.innerHTML, 94 ).toContain('No selected transition'); 95 }); 96 97 it('emits TransitionSelected event on transition clicked', () => { 98 const emitEventSpy = spyOn(component, 'emitEvent'); 99 100 const entries = htmlElement.querySelectorAll('.entry.table-row'); 101 const entry1 = assertDefined(entries[0]) as HTMLElement; 102 const entry2 = assertDefined(entries[1]) as HTMLElement; 103 const treeView = assertDefined( 104 htmlElement.querySelector('.properties-view'), 105 ) as HTMLElement; 106 expect( 107 assertDefined(treeView.querySelector('.placeholder-text')).textContent, 108 ).toContain('No selected transition'); 109 110 expect(emitEventSpy).not.toHaveBeenCalled(); 111 112 const id0 = assertDefined(entry1.querySelector('.id')).textContent; 113 expect(id0).toEqual('0'); 114 entry1.click(); 115 fixture.detectChanges(); 116 117 expect(emitEventSpy).toHaveBeenCalled(); 118 expect(emitEventSpy).toHaveBeenCalledWith( 119 ViewerEvents.TransitionSelected, 120 jasmine.any(Object), 121 ); 122 expect( 123 (emitEventSpy.calls.mostRecent().args[1] as PropertyTreeNode) 124 .getChildByName('id') 125 ?.getValue(), 126 ).toEqual(0); 127 128 const id1 = assertDefined(entry2.querySelector('.id')).textContent; 129 expect(id1).toEqual('1'); 130 entry2.click(); 131 fixture.detectChanges(); 132 133 expect(emitEventSpy).toHaveBeenCalled(); 134 expect(emitEventSpy).toHaveBeenCalledWith( 135 ViewerEvents.TransitionSelected, 136 jasmine.any(Object), 137 ); 138 expect( 139 (emitEventSpy.calls.mostRecent().args[1] as PropertyTreeNode) 140 .getChildByName('id') 141 ?.getValue(), 142 ).toEqual(1); 143 }); 144 145 it('updates tree view on TracePositionUpdate event', async () => { 146 const parser = (await UnitTestUtils.getTracesParser([ 147 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb', 148 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb', 149 ])) as Parser<PropertyTreeNode>; 150 const trace = Trace.fromParser(parser); 151 const traces = new Traces(); 152 traces.addTrace(trace); 153 154 let treeView = assertDefined( 155 htmlElement.querySelector('.properties-view'), 156 ) as any as HTMLElement; 157 expect( 158 assertDefined(treeView.querySelector('.placeholder-text')).textContent, 159 ).toContain('No selected transition'); 160 161 const presenter = new Presenter(trace, traces, (data) => { 162 component.inputData = data; 163 }); 164 const selectedTransitionEntry = assertDefined( 165 traces.getTrace(TraceType.TRANSITION)?.getEntry(2), 166 ); 167 const selectedTransition = await selectedTransitionEntry.getValue(); 168 const selectedTransitionId = assertDefined( 169 selectedTransition.getChildByName('id'), 170 ).getValue(); 171 await presenter.onAppEvent( 172 new TracePositionUpdate( 173 TracePosition.fromTraceEntry(selectedTransitionEntry), 174 ), 175 ); 176 177 expect( 178 assertDefined( 179 component.uiData.selectedTransition 180 ?.getChildByName('wmData') 181 ?.getChildByName('id'), 182 ).getValue(), 183 ).toEqual(selectedTransitionId); 184 185 fixture.detectChanges(); 186 187 treeView = assertDefined( 188 fixture.nativeElement.querySelector('.properties-view'), 189 ) as any as HTMLElement; 190 const textContentWithoutWhitespaces = treeView.textContent?.replace( 191 /(\s|\t|\n)*/g, 192 '', 193 ); 194 expect(textContentWithoutWhitespaces).toContain( 195 `id:${selectedTransitionId}`, 196 ); 197 }); 198 199 it('propagates timestamp on click', () => { 200 let timestamp: string | undefined; 201 let index: number | undefined; 202 htmlElement.addEventListener(ViewerEvents.TimestampClick, (event) => { 203 const detail: TimestampClickDetail = (event as CustomEvent).detail; 204 timestamp = detail.timestamp?.format(); 205 index = detail.index; 206 }); 207 const sendTimeButton = assertDefined( 208 htmlElement.querySelector('.send-time button'), 209 ) as HTMLButtonElement; 210 sendTimeButton.click(); 211 212 expect(index).toBeUndefined(); 213 expect(timestamp).toEqual('20ns'); 214 215 const dispatchTimeButton = assertDefined( 216 htmlElement.querySelector('.dispatch-time button'), 217 ) as HTMLButtonElement; 218 dispatchTimeButton.click(); 219 220 expect(index).toEqual(0); 221 expect(timestamp).toEqual('25ns'); 222 }); 223 224 it('creates collapsed sections with no buttons', () => { 225 UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement); 226 }); 227 228 it('handles properties section collapse/expand', () => { 229 UnitTestUtils.checkSectionCollapseAndExpand( 230 htmlElement, 231 fixture, 232 '.properties-view', 233 'SELECTED TRANSITION', 234 ); 235 }); 236}); 237 238function makeUiData(): UiData { 239 let mockTransitionIdCounter = 0; 240 241 const transitions = [ 242 createMockTransition(20, 30, mockTransitionIdCounter++), 243 createMockTransition(42, 50, mockTransitionIdCounter++), 244 createMockTransition(46, 49, mockTransitionIdCounter++), 245 createMockTransition(58, 70, mockTransitionIdCounter++), 246 ]; 247 248 return new UiData(transitions, undefined); 249} 250 251function createMockTransition( 252 sendTimeNanos: number, 253 finishTimeNanos: number, 254 id: number, 255): Transition { 256 const transitionTree = new PropertyTreeBuilder() 257 .setIsRoot(true) 258 .setRootId('TransitionTraceEntry') 259 .setName('transition') 260 .setChildren([{name: 'id', value: id}]) 261 .build(); 262 263 const sendTimeNode = new PropertyTreeBuilder() 264 .setRootId(transitionTree.id) 265 .setName('sendTimeNs') 266 .setValue( 267 TimestampConverterUtils.makeElapsedTimestamp(BigInt(sendTimeNanos)), 268 ) 269 .setFormatter(TIMESTAMP_NODE_FORMATTER) 270 .build(); 271 272 const dispatchTimeNode = new PropertyTreeBuilder() 273 .setRootId(transitionTree.id) 274 .setName('dispatchTimeNs') 275 .setValue( 276 TimestampConverterUtils.makeElapsedTimestamp(BigInt(sendTimeNanos) + 5n), 277 ) 278 .setFormatter(TIMESTAMP_NODE_FORMATTER) 279 .build(); 280 281 return { 282 id, 283 type: 'TO_FRONT', 284 sendTime: sendTimeNode, 285 dispatchTime: dispatchTimeNode, 286 duration: (finishTimeNanos - sendTimeNanos).toString() + 'ns', 287 merged: false, 288 aborted: false, 289 played: false, 290 propertiesTree: transitionTree, 291 traceIndex: 0, 292 }; 293} 294