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