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 {InMemoryStorage} from 'common/in_memory_storage'; 18import { 19 InitializeTraceSearchRequest, 20 TraceAddRequest, 21 TracePositionUpdate, 22 TraceRemoveRequest, 23 TraceSearchFailed, 24 TraceSearchInitialized, 25 TraceSearchRequest, 26} from 'messaging/winscope_event'; 27import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 28import {TraceBuilder} from 'test/unit/trace_builder'; 29import {UserNotifierChecker} from 'test/unit/user_notifier_checker'; 30import {UnitTestUtils} from 'test/unit/utils'; 31import {Trace} from 'trace/trace'; 32import {Traces} from 'trace/traces'; 33import {TraceType} from 'trace/trace_type'; 34import {QueryResult} from 'trace_processor/query_result'; 35import { 36 DeleteSavedQueryClickDetail, 37 QueryClickDetail, 38 SaveQueryClickDetail, 39 TimestampClickDetail, 40 ViewerEvents, 41} from 'viewers/common/viewer_events'; 42import {Presenter} from './presenter'; 43import {Search, SearchResult, UiData} from './ui_data'; 44 45describe('PresenterSearch', () => { 46 let presenter: Presenter; 47 let uiData: UiData; 48 let userNotifierChecker: UserNotifierChecker; 49 let element: HTMLElement; 50 let emitEventSpy: jasmine.Spy; 51 52 beforeAll(() => { 53 userNotifierChecker = new UserNotifierChecker(); 54 jasmine.addCustomEqualityTester(searchEqualityTester); 55 }); 56 57 beforeEach(() => { 58 presenter = new Presenter( 59 new Traces(), 60 new InMemoryStorage(), 61 (newData: UiData) => (uiData = newData), 62 ); 63 userNotifierChecker.reset(); 64 element = document.createElement('div'); 65 presenter.addEventListeners(element); 66 emitEventSpy = jasmine.createSpy(); 67 presenter.setEmitEvent(emitEventSpy); 68 }); 69 70 it('adds event listeners', () => { 71 let spy: jasmine.Spy = spyOn(presenter, 'onGlobalSearchSectionClick'); 72 element.dispatchEvent( 73 new CustomEvent(ViewerEvents.GlobalSearchSectionClick), 74 ); 75 expect(spy).toHaveBeenCalled(); 76 77 spy = spyOn(presenter, 'onSearchQueryClick'); 78 const testQuery = 'search query'; 79 element.dispatchEvent( 80 new CustomEvent(ViewerEvents.SearchQueryClick, { 81 detail: new QueryClickDetail(testQuery), 82 }), 83 ); 84 expect(spy).toHaveBeenCalledWith(testQuery); 85 86 spy = spyOn(presenter, 'onSaveQueryClick'); 87 const saveQueryDetail = new SaveQueryClickDetail('save query', 'foo'); 88 element.dispatchEvent( 89 new CustomEvent(ViewerEvents.SaveQueryClick, { 90 detail: saveQueryDetail, 91 }), 92 ); 93 expect(spy).toHaveBeenCalledWith( 94 saveQueryDetail.query, 95 saveQueryDetail.name, 96 ); 97 98 spy = spyOn(presenter, 'onDeleteSavedQueryClick'); 99 const deleteQueryDetail = new DeleteSavedQueryClickDetail( 100 new Search('delete query', 'bar'), 101 ); 102 element.dispatchEvent( 103 new CustomEvent(ViewerEvents.DeleteSavedQueryClick, { 104 detail: deleteQueryDetail, 105 }), 106 ); 107 expect(spy).toHaveBeenCalledWith(deleteQueryDetail.search); 108 }); 109 110 it('handles trace search initialization', async () => { 111 await presenter.onGlobalSearchSectionClick(); 112 expect(emitEventSpy).toHaveBeenCalledOnceWith( 113 new InitializeTraceSearchRequest(), 114 ); 115 expect(uiData.initialized).toBeFalse(); 116 117 await presenter.onAppEvent(new TraceSearchInitialized(['test_view'])); 118 expect(uiData.initialized).toBeTrue(); 119 expect(uiData.searchViews).toEqual(['test_view']); 120 121 emitEventSpy.calls.reset(); 122 await presenter.onGlobalSearchSectionClick(); 123 expect(emitEventSpy).not.toHaveBeenCalled(); 124 }); 125 126 it('handles search for successful query with zero rows', async () => { 127 const query = 'successful empty query'; 128 await runSearchWithNoRowsAndCheckUiData( 129 query, 130 UnitTestUtils.makeEmptyTrace(TraceType.SEARCH, [query]), 131 ); 132 }); 133 134 it('handles search for successful query with non-zero rows', async () => { 135 const testQuery = 'successful non-empty query'; 136 presenter.onSearchQueryClick(testQuery); 137 expect(emitEventSpy).toHaveBeenCalledOnceWith( 138 new TraceSearchRequest(testQuery), 139 ); 140 141 const time100 = TimestampConverterUtils.makeRealTimestamp(100n); 142 const [spyQueryResult, spyIter] = 143 UnitTestUtils.makeSearchTraceSpies(time100); 144 const trace = new TraceBuilder<QueryResult>() 145 .setEntries([spyQueryResult]) 146 .setTimestamps([time100]) 147 .setDescriptors([testQuery]) 148 .setType(TraceType.SEARCH) 149 .build(); 150 151 await presenter.onAppEvent(new TraceAddRequest(trace)); 152 const expectedSearchResult = new SearchResult(testQuery, [], []); 153 expect(uiData.currentSearches).toEqual([expectedSearchResult]); 154 expect(uiData.lastTraceFailed).toEqual(false); 155 156 await presenter.onAppEvent( 157 TracePositionUpdate.fromTraceEntry(trace.getEntry(0)), 158 ); 159 expect(uiData.currentSearches.length).toEqual(1); 160 expect(uiData.currentSearches[0].currentIndex).toEqual(0); 161 expect(uiData.currentSearches[0].headers.length).toEqual(2); 162 expect(uiData.currentSearches[0].entries.length).toEqual(1); 163 expect(uiData.lastTraceFailed).toEqual(false); 164 expect(uiData.recentSearches).toEqual([new Search(testQuery)]); 165 166 // adds event listeners and emit event for search presenter 167 emitEventSpy.calls.reset(); 168 element.dispatchEvent( 169 new CustomEvent(ViewerEvents.TimestampClick, { 170 detail: new TimestampClickDetail(undefined, time100), 171 }), 172 ); 173 expect(emitEventSpy).toHaveBeenCalledOnceWith( 174 TracePositionUpdate.fromTimestamp(time100, true), 175 ); 176 }); 177 178 it('handles non-search trace added event', async () => { 179 const currData = uiData; 180 const trace = UnitTestUtils.makeEmptyTrace(TraceType.SURFACE_FLINGER); 181 await presenter.onAppEvent(new TraceAddRequest(trace)); 182 expect(uiData).toEqual(currData); 183 }); 184 185 it('handles search for unsuccessful query', async () => { 186 const testQuery = 'unsuccessful query'; 187 presenter.onSearchQueryClick(testQuery); 188 await presenter.onAppEvent(new TraceSearchFailed()); 189 expect(uiData.lastTraceFailed).toEqual(true); 190 expect(uiData.currentSearches).toEqual([]); 191 expect(uiData.recentSearches).toEqual([]); 192 }); 193 194 it('clears current search result when query run again, keeping both in recent searches', async () => { 195 const testQuery = 'query to be overwritten'; 196 const trace = UnitTestUtils.makeEmptyTrace(TraceType.SEARCH, [testQuery]); 197 await runSearchWithNoRowsAndCheckUiData(testQuery, trace); 198 emitEventSpy.calls.reset(); 199 200 await presenter.onSearchQueryClick(testQuery); 201 expect(emitEventSpy).toHaveBeenCalledWith(new TraceRemoveRequest(trace)); 202 expect(emitEventSpy).toHaveBeenCalledWith( 203 new TraceSearchRequest(testQuery), 204 ); 205 expect(uiData.currentSearches).toEqual([]); 206 expect(uiData.recentSearches).toEqual([new Search(testQuery)]); 207 emitEventSpy.calls.reset(); 208 209 const newQuery = 'new query'; 210 const newTrace = UnitTestUtils.makeEmptyTrace(TraceType.SEARCH, [newQuery]); 211 await runSearchWithNoRowsAndCheckUiData(newQuery, newTrace); 212 emitEventSpy.calls.reset(); 213 214 // check removed presenter cannot still affect ui data 215 element.dispatchEvent(new CustomEvent(ViewerEvents.ArrowDownPress)); 216 expect(uiData.currentSearches.length).toEqual(1); 217 218 await presenter.onSearchQueryClick(newQuery); 219 expect(emitEventSpy).toHaveBeenCalledWith(new TraceRemoveRequest(newTrace)); 220 expect(emitEventSpy).toHaveBeenCalledWith(new TraceSearchRequest(newQuery)); 221 expect(uiData.currentSearches).toEqual([]); 222 expect(uiData.recentSearches).toEqual([ 223 new Search(newQuery), 224 new Search(testQuery), 225 ]); 226 }); 227 228 it('handles save query click', () => { 229 const testQuery = 'save query'; 230 const testName = 'save name'; 231 presenter.onSaveQueryClick(testQuery, testName); 232 const testSearch = new Search(testQuery, testName); 233 expect(uiData.savedSearches).toEqual([testSearch]); 234 235 const newQuery = 'new save query'; 236 const newName = 'new save name'; 237 presenter.onSaveQueryClick(newQuery, newName); 238 const newSearch = new Search(newQuery, newName); 239 expect(uiData.savedSearches).toEqual([newSearch, testSearch]); 240 }); 241 242 it('handles delete saved query click', () => { 243 const testQuery = 'delete query'; 244 const testName = 'delete name'; 245 const testSearch = new Search(testQuery, testName); 246 presenter.onDeleteSavedQueryClick(testSearch); 247 expect(uiData.savedSearches).toEqual([]); 248 249 presenter.onSaveQueryClick(testQuery, testName); 250 expect(uiData.savedSearches).toEqual([testSearch]); 251 252 presenter.onDeleteSavedQueryClick(uiData.savedSearches[0]); 253 expect(uiData.savedSearches).toEqual([]); 254 }); 255 256 function searchEqualityTester(first: any, second: any): boolean | undefined { 257 if (first instanceof Search && second instanceof Search) { 258 return first.query === second.query && first.name === second.name; 259 } 260 return undefined; 261 } 262 263 async function runSearchWithNoRowsAndCheckUiData( 264 testQuery: string, 265 trace: Trace<QueryResult>, 266 ) { 267 presenter.onSearchQueryClick(testQuery); 268 expect(emitEventSpy).toHaveBeenCalledOnceWith( 269 new TraceSearchRequest(testQuery), 270 ); 271 await presenter.onAppEvent(new TraceAddRequest(trace)); 272 expect(uiData.currentSearches).toEqual([ 273 new SearchResult(testQuery, [], []), 274 ]); 275 expect(uiData.lastTraceFailed).toEqual(false); 276 expect(uiData.recentSearches[0]).toEqual(new Search(testQuery)); 277 } 278}); 279