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 {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; 18import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 19import {TraceBuilder} from 'test/unit/trace_builder'; 20import {TraceType} from 'trace/trace_type'; 21import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 22import {TextFilter} from 'viewers/common/text_filter'; 23import {LogSelectFilter, LogTextFilter} from './log_filters'; 24import {LogPresenter} from './log_presenter'; 25import {LogEntry, LogHeader} from './ui_data_log'; 26 27describe('LogPresenter', () => { 28 let presenter: LogPresenter<LogEntry>; 29 const timestamp1 = TimestampConverterUtils.makeElapsedTimestamp(1n); 30 const timestamp2 = TimestampConverterUtils.makeElapsedTimestamp(2n); 31 const timestamp3 = TimestampConverterUtils.makeElapsedTimestamp(3n); 32 const timestamp4 = TimestampConverterUtils.makeElapsedTimestamp(4n); 33 const trace = new TraceBuilder<PropertyTreeNode>() 34 .setType(TraceType.TRANSACTIONS) 35 .setEntries([ 36 new PropertyTreeBuilder() 37 .setRootId('Test Trace') 38 .setName('entry 1') 39 .build(), 40 new PropertyTreeBuilder() 41 .setRootId('Test Trace') 42 .setName('entry 2') 43 .build(), 44 new PropertyTreeBuilder() 45 .setRootId('Test Trace') 46 .setName('entry 3') 47 .build(), 48 new PropertyTreeBuilder() 49 .setRootId('Test Trace') 50 .setName('entry 4') 51 .build(), 52 ]) 53 .setTimestamps([timestamp1, timestamp2, timestamp3, timestamp4]) 54 .build(); 55 56 const STRING_COLUMN = { 57 name: 'String Column', 58 cssClass: 'string-column', 59 }; 60 const NUMBER_COLUMN = { 61 name: 'Number Column', 62 cssClass: 'number-column', 63 }; 64 const TIMESTAMP_COLUMN = { 65 name: 'Timestamp Column', 66 cssClass: 'timestamp-column', 67 }; 68 let stringFilter: LogTextFilter; 69 let numberFilter: LogSelectFilter; 70 let headers: LogHeader[]; 71 let testEntries: LogEntry[]; 72 73 describe('time-ordered entries', () => { 74 beforeEach(async () => { 75 presenter = new LogPresenter(); 76 testEntries = await buildTestEntries(); 77 presenter.setAllEntries(testEntries); 78 expectAllIndicesUndefined(); 79 80 stringFilter = new LogTextFilter(new TextFilter('stringValue')); 81 numberFilter = new LogSelectFilter(['0', '1', '2', '3']); 82 headers = [ 83 new LogHeader(STRING_COLUMN, stringFilter), 84 new LogHeader(NUMBER_COLUMN, numberFilter), 85 new LogHeader(TIMESTAMP_COLUMN), 86 ]; 87 }); 88 89 it('applies filters to existing entries when headers updated', async () => { 90 presenter.setHeaders(headers); 91 expect(presenter.getHeaders()).toEqual(headers); 92 expect(presenter.getFilteredEntries()).toEqual([ 93 testEntries[0], 94 testEntries[2], 95 ]); 96 expectAllIndicesUndefined(); 97 }); 98 99 it('applies existing filters when entries updated', async () => { 100 presenter.setAllEntries([]); 101 expectAllIndicesUndefined(); 102 103 presenter.setHeaders(headers); 104 expect(presenter.getHeaders()).toEqual(headers); 105 expectAllIndicesUndefined(); 106 107 presenter.setAllEntries(testEntries); 108 expect(presenter.getFilteredEntries()).toEqual([ 109 testEntries[0], 110 testEntries[2], 111 ]); 112 expectAllIndicesUndefined(); 113 }); 114 115 it('applies log entry click', () => { 116 // selects index 117 presenter.applyLogEntryClick(1); 118 expect(presenter.getSelectedIndex()).toEqual(1); 119 expect(presenter.getCurrentIndex()).toBeUndefined(); 120 expect(presenter.getScrollToIndex()).toBeUndefined(); 121 122 // on same index, clears scroll but leaves current and selected unchanged 123 presenter.applyTracePositionUpdate(trace.getEntry(0)); 124 presenter.applyLogEntryClick(1); 125 expect(presenter.getSelectedIndex()).toEqual(1); 126 expect(presenter.getCurrentIndex()).toEqual(0); 127 expect(presenter.getScrollToIndex()).toBeUndefined(); 128 }); 129 130 it('applies arrow down press', () => { 131 // selects and scrolls to first index if no selected or current index 132 presenter.applyArrowDownPress(); 133 expect(presenter.getSelectedIndex()).toEqual(0); 134 expect(presenter.getScrollToIndex()).toEqual(0); 135 expect(presenter.getCurrentIndex()).toBeUndefined(); 136 137 // selects next index after selected index 138 presenter.applyArrowDownPress(); 139 expect(presenter.getSelectedIndex()).toEqual(1); 140 expect(presenter.getScrollToIndex()).toEqual(1); 141 expect(presenter.getCurrentIndex()).toBeUndefined(); 142 143 // handles index out of range 144 presenter.applyArrowDownPress(); 145 presenter.applyArrowDownPress(); 146 presenter.applyArrowDownPress(); 147 expect(presenter.getSelectedIndex()).toEqual(3); 148 expect(presenter.getScrollToIndex()).toEqual(3); 149 expect(presenter.getCurrentIndex()).toBeUndefined(); 150 151 // selects next index after current index 152 presenter.applyTracePositionUpdate(trace.getEntry(0)); 153 presenter.applyArrowDownPress(); 154 expect(presenter.getSelectedIndex()).toEqual(1); 155 expect(presenter.getScrollToIndex()).toEqual(1); 156 expect(presenter.getCurrentIndex()).toEqual(0); 157 158 // handles no entries 159 presenter.setAllEntries([]); 160 presenter.applyArrowDownPress(); 161 expectAllIndicesUndefined(); 162 }); 163 164 it('applies arrow up press', () => { 165 // selects first index if no selected or current index 166 presenter.applyArrowUpPress(); 167 expect(presenter.getSelectedIndex()).toEqual(0); 168 expect(presenter.getScrollToIndex()).toEqual(0); 169 expect(presenter.getCurrentIndex()).toBeUndefined(); 170 171 // selects index before selected index 172 presenter.applyLogEntryClick(2); 173 presenter.applyArrowUpPress(); 174 expect(presenter.getSelectedIndex()).toEqual(1); 175 expect(presenter.getScrollToIndex()).toEqual(1); 176 expect(presenter.getCurrentIndex()).toBeUndefined(); 177 178 // handles index out of range 179 presenter.applyArrowUpPress(); 180 presenter.applyArrowUpPress(); 181 presenter.applyArrowUpPress(); 182 expect(presenter.getSelectedIndex()).toEqual(0); 183 expect(presenter.getScrollToIndex()).toEqual(0); 184 expect(presenter.getCurrentIndex()).toBeUndefined(); 185 186 // selects index before current index 187 presenter.applyTracePositionUpdate(trace.getEntry(1)); 188 presenter.applyArrowUpPress(); 189 expect(presenter.getSelectedIndex()).toEqual(0); 190 expect(presenter.getScrollToIndex()).toEqual(0); 191 expect(presenter.getCurrentIndex()).toEqual(1); 192 193 // handles no entries 194 presenter.setAllEntries([]); 195 presenter.applyArrowDownPress(); 196 expectAllIndicesUndefined(); 197 }); 198 199 it('applies trace position update', () => { 200 // updates current index, clears selected index, scrolls to current index 201 presenter.applyTracePositionUpdate(trace.getEntry(1)); 202 expect(presenter.getCurrentIndex()).toEqual(1); 203 expect(presenter.getSelectedIndex()).toBeUndefined(); 204 expect(presenter.getScrollToIndex()).toEqual(1); 205 206 // if no current entry, current index undefined 207 presenter.applyTracePositionUpdate(undefined); 208 expect(presenter.getCurrentIndex()).toBeUndefined(); 209 expect(presenter.getSelectedIndex()).toBeUndefined(); 210 expect(presenter.getScrollToIndex()).toBeUndefined(); 211 212 // if current entry filtered out, returns next entry by time 213 updateStringFilterAndCheckEntries('stringValue', [ 214 testEntries[0], 215 testEntries[2], 216 ]); 217 presenter.applyTracePositionUpdate(trace.getEntry(1)); 218 expect(presenter.getCurrentIndex()).toEqual(1); 219 expect(presenter.getSelectedIndex()).toBeUndefined(); 220 expect(presenter.getScrollToIndex()).toEqual(1); 221 222 // handles no filtered entries 223 updateStringFilterAndCheckEntries('no matches', []); 224 presenter.applyTracePositionUpdate(trace.getEntry(1)); 225 expectAllIndicesUndefined(); 226 updateStringFilterAndCheckEntries('', testEntries); 227 228 // handles no entries 229 presenter.setAllEntries([]); 230 presenter.applyTracePositionUpdate(trace.getEntry(1)); 231 presenter.applyArrowDownPress(); 232 expectAllIndicesUndefined(); 233 }); 234 235 it('applies trace position update for non time-ordered entries', () => { 236 const presenter = new LogPresenter(false); 237 presenter.setAllEntries([testEntries[3]].concat(testEntries.slice(0, 3))); 238 expectAllIndicesUndefined(); 239 240 presenter.applyTracePositionUpdate(trace.getEntry(1)); 241 expect(presenter.getCurrentIndex()).toEqual(2); 242 expect(presenter.getSelectedIndex()).toBeUndefined(); 243 expect(presenter.getScrollToIndex()).toEqual(2); 244 245 presenter.applyTracePositionUpdate(trace.getEntry(3)); 246 expect(presenter.getCurrentIndex()).toEqual(0); 247 expect(presenter.getSelectedIndex()).toBeUndefined(); 248 expect(presenter.getScrollToIndex()).toEqual(0); 249 }); 250 251 it('applies text filter change', () => { 252 updateStringFilterAndCheckEntries('stringValue', [ 253 testEntries[0], 254 testEntries[2], 255 ]); 256 updateStringFilterAndCheckEntries('no matches', []); 257 updateStringFilterAndCheckEntries('', testEntries); 258 }); 259 260 it('applies select filter change', () => { 261 updateNumberFilterAndCheckEntries(['0'], [testEntries[0]]); 262 updateNumberFilterAndCheckEntries( 263 ['0', '3'], 264 [testEntries[0], testEntries[3]], 265 ); 266 updateNumberFilterAndCheckEntries([], testEntries); 267 }); 268 }); 269 270 async function buildTestEntries(): Promise<LogEntry[]> { 271 return [ 272 { 273 traceEntry: trace.getEntry(0), 274 fields: [ 275 { 276 spec: STRING_COLUMN, 277 value: 'stringValue', 278 }, 279 { 280 spec: NUMBER_COLUMN, 281 value: 0, 282 }, 283 { 284 spec: TIMESTAMP_COLUMN, 285 value: timestamp1, 286 }, 287 ], 288 propertiesTree: await trace.getEntry(0).getValue(), 289 }, 290 { 291 traceEntry: trace.getEntry(1), 292 fields: [ 293 { 294 spec: STRING_COLUMN, 295 value: 'differentValue', 296 }, 297 { 298 spec: NUMBER_COLUMN, 299 value: 1, 300 }, 301 { 302 spec: TIMESTAMP_COLUMN, 303 value: timestamp2, 304 }, 305 ], 306 propertiesTree: await trace.getEntry(1).getValue(), 307 }, 308 { 309 traceEntry: trace.getEntry(2), 310 fields: [ 311 { 312 spec: STRING_COLUMN, 313 value: 'stringValue', 314 }, 315 { 316 spec: NUMBER_COLUMN, 317 value: 2, 318 }, 319 { 320 spec: TIMESTAMP_COLUMN, 321 value: timestamp3, 322 }, 323 ], 324 propertiesTree: await trace.getEntry(2).getValue(), 325 }, 326 { 327 traceEntry: trace.getEntry(3), 328 fields: [ 329 { 330 spec: STRING_COLUMN, 331 value: 'differentValue', 332 }, 333 { 334 spec: NUMBER_COLUMN, 335 value: 3, 336 }, 337 { 338 spec: TIMESTAMP_COLUMN, 339 value: timestamp4, 340 }, 341 ], 342 propertiesTree: await trace.getEntry(3).getValue(), 343 }, 344 ]; 345 } 346 347 function expectAllIndicesUndefined() { 348 expect(presenter.getCurrentIndex()).toBeUndefined(); 349 expect(presenter.getSelectedIndex()).toBeUndefined(); 350 expect(presenter.getScrollToIndex()).toBeUndefined(); 351 } 352 353 function updateStringFilterAndCheckEntries( 354 value: string, 355 expectedEntries: LogEntry[], 356 ) { 357 stringFilter.updateFilterValue([value]); 358 presenter.applyTextFilterChange(headers[0], stringFilter.textFilter); 359 expect(presenter.getFilteredEntries()).toEqual(expectedEntries); 360 } 361 362 function updateNumberFilterAndCheckEntries( 363 value: string[], 364 expectedEntries: LogEntry[], 365 ) { 366 presenter.applySelectFilterChange(headers[1], value); 367 expect(presenter.getFilteredEntries()).toEqual(expectedEntries); 368 } 369}); 370