1/* 2 * Copyright (C) 2024 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, NgTemplateOutlet} from '@angular/common'; 18import {Component} from '@angular/core'; 19import {ComponentFixture, TestBed} from '@angular/core/testing'; 20import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; 21import {MatButtonModule} from '@angular/material/button'; 22import {MatFormFieldModule} from '@angular/material/form-field'; 23import {MatIconModule} from '@angular/material/icon'; 24import {MatInputModule} from '@angular/material/input'; 25import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 26import {MatTooltipModule} from '@angular/material/tooltip'; 27import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 28import {assertDefined} from 'common/assert_utils'; 29import { 30 SearchQueryClickDetail, 31 ViewerEvents, 32} from 'viewers/common/viewer_events'; 33import {ActiveSearchComponent} from './active_search_component'; 34 35describe('ActiveSearchComponent', () => { 36 const testQuery = 'select * from table'; 37 let fixture: ComponentFixture<ActiveSearchComponent>; 38 let component: ActiveSearchComponent; 39 let htmlElement: HTMLElement; 40 41 beforeEach(async () => { 42 await TestBed.configureTestingModule({ 43 declarations: [ActiveSearchComponent, TestHostComponent], 44 imports: [ 45 MatFormFieldModule, 46 MatInputModule, 47 BrowserAnimationsModule, 48 FormsModule, 49 ReactiveFormsModule, 50 MatButtonModule, 51 MatIconModule, 52 MatProgressSpinnerModule, 53 MatTooltipModule, 54 CommonModule, 55 NgTemplateOutlet, 56 ], 57 }).compileComponents(); 58 fixture = TestBed.createComponent(ActiveSearchComponent); 59 component = fixture.componentInstance; 60 htmlElement = fixture.nativeElement; 61 62 component.isSearchInitialized = true; 63 component.lastTraceFailed = false; 64 component.saveQueryNameControl = new FormControl(); 65 fixture.detectChanges(); 66 }); 67 68 it('can be created', () => { 69 expect(component).toBeTruthy(); 70 }); 71 72 it('handles search via button', () => { 73 runSearchAndCheckHandled(runSearchByQueryButton); 74 }); 75 76 it('handles search via enter key', () => { 77 const runSearch = () => { 78 const textInput = getTextInput(); 79 changeInput(textInput, testQuery); 80 pressEnter(textInput); 81 }; 82 runSearchAndCheckHandled(runSearch); 83 }); 84 85 it('does not handle search on enter key + shift key', () => { 86 let query: string | undefined; 87 htmlElement 88 .querySelector('viewer-search') 89 ?.addEventListener(ViewerEvents.SearchQueryClick, (event) => { 90 const detail: SearchQueryClickDetail = (event as CustomEvent).detail; 91 query = detail.query; 92 }); 93 const textInput = getTextInput(); 94 changeInput(textInput, testQuery); 95 pressEnter(textInput, true); 96 expect(query).toBeUndefined(); 97 }); 98 99 it('handles running query complete', () => { 100 runSearchByQueryButton(); 101 component.canAdd = true; 102 component.executedQuery = testQuery; 103 fixture.detectChanges(); 104 expect(htmlElement.querySelector('.running-query-message')).toBeNull(); 105 expect( 106 htmlElement.querySelector<HTMLButtonElement>('.add-button')?.disabled, 107 ).toBeFalse(); 108 }); 109 110 it('handles running query failure', () => { 111 runSearchByQueryButton(); 112 component.canAdd = true; 113 component.lastTraceFailed = true; 114 fixture.detectChanges(); 115 expect(htmlElement.querySelector('.running-query-message')).toBeNull(); 116 expect( 117 htmlElement.querySelector<HTMLButtonElement>('.add-button')?.disabled, 118 ).toBeTrue(); 119 }); 120 121 it('disables search query until initialized', () => { 122 component.isSearchInitialized = false; 123 fixture.detectChanges(); 124 changeInput(getTextInput(), testQuery); 125 expect(getSearchQueryButton().disabled).toBeTrue(); 126 127 component.isSearchInitialized = true; 128 fixture.detectChanges(); 129 expect(getSearchQueryButton().disabled).toBeFalse(); 130 }); 131 132 it('clears query', () => { 133 expect(htmlElement.querySelector('.clear-button')).toBeNull(); 134 component.canClear = true; 135 fixture.detectChanges(); 136 const clearButton = assertDefined( 137 htmlElement.querySelector<HTMLElement>('.clear-button'), 138 ); 139 spyOn(component.clearQueryClick, 'emit'); 140 expect(clearButton.textContent?.trim()).toContain('Clear'); 141 clearButton.click(); 142 fixture.detectChanges(); 143 expect(component.clearQueryClick.emit).toHaveBeenCalledTimes(1); 144 }); 145 146 it('adds query', () => { 147 expect(htmlElement.querySelector('.add-button')).toBeNull(); 148 component.canAdd = true; 149 fixture.detectChanges(); 150 const addButton = assertDefined( 151 htmlElement.querySelector<HTMLButtonElement>('.add-button'), 152 ); 153 expect(addButton.textContent?.trim()).toContain('+ Add Query'); 154 expect(addButton.disabled).toBeTrue(); 155 156 spyOn(component.addQueryClick, 'emit'); 157 component.executedQuery = testQuery; 158 fixture.detectChanges(); 159 addButton.click(); 160 fixture.detectChanges(); 161 expect(component.addQueryClick.emit).toHaveBeenCalledTimes(1); 162 }); 163 164 it('labels section', () => { 165 component.label = 'test label'; 166 fixture.detectChanges(); 167 expect(htmlElement.querySelector('.header')?.textContent?.trim()).toEqual( 168 'test label', 169 ); 170 }); 171 172 it('shows last query execution time', () => { 173 expect(htmlElement.querySelector('.query-execution-time')).toBeNull(); 174 175 component.lastQueryExecutionTime = '10 ms'; 176 fixture.detectChanges(); 177 expect( 178 htmlElement.querySelector('.query-execution-time')?.textContent?.trim(), 179 ).toEqual('Executed in 10 ms'); 180 }); 181 182 it('shows current search information and save query field', () => { 183 const hostFixture = TestBed.createComponent(TestHostComponent); 184 const hostComponent = hostFixture.componentInstance; 185 const hostElement = hostFixture.nativeElement; 186 hostFixture.detectChanges(); 187 188 expect(hostElement.querySelector('.current-search')).toBeNull(); 189 expect(hostElement.querySelector('.test-query')).toBeNull(); 190 expect(hostElement.querySelector('.test-control-value')).toBeNull(); 191 hostComponent.control.setValue('test name'); 192 hostComponent.executedQuery = 'test query'; 193 hostFixture.detectChanges(); 194 195 const currentSearch = assertDefined( 196 hostElement.querySelector('.current-search'), 197 ); 198 expect(currentSearch.querySelector('.query')?.textContent?.trim()).toEqual( 199 'Last executed: test query', 200 ); 201 expect( 202 currentSearch.querySelector('.test-query')?.textContent?.trim(), 203 ).toEqual('test query'); 204 expect( 205 currentSearch.querySelector('.test-control-value')?.textContent?.trim(), 206 ).toEqual('test name'); 207 }); 208 209 function getTextInput(): HTMLTextAreaElement { 210 return assertDefined( 211 htmlElement.querySelector<HTMLTextAreaElement>('.query-field textarea'), 212 ); 213 } 214 215 function changeInput( 216 input: HTMLInputElement | HTMLTextAreaElement, 217 query: string, 218 ) { 219 input.value = query; 220 input.dispatchEvent(new Event('input')); 221 fixture.detectChanges(); 222 } 223 224 function getSearchQueryButton(): HTMLButtonElement { 225 return assertDefined( 226 htmlElement.querySelector<HTMLButtonElement>( 227 '.query-actions .search-button', 228 ), 229 ); 230 } 231 232 function runSearchByQueryButton() { 233 changeInput(getTextInput(), testQuery); 234 getSearchQueryButton().click(); 235 fixture.detectChanges(); 236 } 237 238 function runSearchAndCheckHandled(runSearch: () => void) { 239 spyOn(component.searchQueryClick, 'emit'); 240 runSearch(); 241 component.runningQuery = true; 242 fixture.detectChanges(); 243 expect(component.searchQueryClick.emit).toHaveBeenCalledOnceWith(testQuery); 244 expect(getSearchQueryButton().disabled).toBeTrue(); 245 const runningQueryMessage = assertDefined( 246 htmlElement.querySelector('.running-query-message'), 247 ); 248 expect(runningQueryMessage.textContent?.trim()).toEqual( 249 'timer Calculating results', 250 ); 251 expect(runningQueryMessage.querySelector('mat-spinner')).toBeTruthy(); 252 } 253 254 function pressEnter( 255 input: HTMLInputElement | HTMLTextAreaElement, 256 shiftKey = false, 257 ) { 258 input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', shiftKey})); 259 fixture.detectChanges(); 260 } 261 262 @Component({ 263 selector: 'test-component', 264 template: ` 265 <active-search [saveQueryField]="testTemplate" [executedQuery]=executedQuery [saveQueryNameControl]="control"></active-search> 266 <ng-template #testTemplate let-search="search" let-query="query"> 267 <span class="test-query"> {{query}} </span> 268 <span class="test-control-value"> {{control?.value}} </span> 269 </ng-template> 270 `, 271 }) 272 class TestHostComponent { 273 control = new FormControl(); 274 executedQuery: string | undefined; 275 } 276}); 277