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 {DragDropModule} from '@angular/cdk/drag-drop'; 18import {ChangeDetectionStrategy} from '@angular/core'; 19import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; 20import {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 {MatSelectModule} from '@angular/material/select'; 26import {MatTooltipModule} from '@angular/material/tooltip'; 27import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 28import {assertDefined} from 'common/assert_utils'; 29import {TimeRange} from 'common/time'; 30import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 31import {dragElement} from 'test/utils'; 32import {TracePosition} from 'trace/trace_position'; 33import {MIN_SLIDER_WIDTH, SliderComponent} from './slider_component'; 34 35describe('SliderComponent', () => { 36 let fixture: ComponentFixture<SliderComponent>; 37 let component: SliderComponent; 38 let htmlElement: HTMLElement; 39 const time100 = TimestampConverterUtils.makeRealTimestamp(100n); 40 const time125 = TimestampConverterUtils.makeRealTimestamp(125n); 41 const time126 = TimestampConverterUtils.makeRealTimestamp(126n); 42 const time150 = TimestampConverterUtils.makeRealTimestamp(150n); 43 const time175 = TimestampConverterUtils.makeRealTimestamp(175n); 44 const time200 = TimestampConverterUtils.makeRealTimestamp(200n); 45 46 beforeEach(async () => { 47 await TestBed.configureTestingModule({ 48 imports: [ 49 FormsModule, 50 MatButtonModule, 51 MatFormFieldModule, 52 MatInputModule, 53 MatIconModule, 54 MatSelectModule, 55 MatTooltipModule, 56 ReactiveFormsModule, 57 BrowserAnimationsModule, 58 DragDropModule, 59 ], 60 declarations: [SliderComponent], 61 }) 62 .overrideComponent(SliderComponent, { 63 set: {changeDetection: ChangeDetectionStrategy.Default}, 64 }) 65 .compileComponents(); 66 fixture = TestBed.createComponent(SliderComponent); 67 component = fixture.componentInstance; 68 htmlElement = fixture.nativeElement; 69 70 component.fullRange = new TimeRange(time100, time200); 71 component.zoomRange = new TimeRange(time125, time175); 72 component.currentPosition = TracePosition.fromTimestamp(time150); 73 component.timestampConverter = TimestampConverterUtils.TIMESTAMP_CONVERTER; 74 75 fixture.detectChanges(); 76 }); 77 78 it('can be created', () => { 79 expect(component).toBeTruthy(); 80 }); 81 82 it('reposition properly on zoom', () => { 83 fixture.detectChanges(); 84 component.ngOnChanges({ 85 zoomRange: { 86 firstChange: true, 87 isFirstChange: () => true, 88 previousValue: undefined, 89 currentValue: component.zoomRange, 90 }, 91 }); 92 fixture.detectChanges(); 93 94 const sliderWitdth = component.sliderBox.nativeElement.offsetWidth; 95 expect(component.sliderWidth).toBe(sliderWitdth / 2); 96 expect(component.dragPosition.x).toBe(sliderWitdth / 4); 97 }); 98 99 it('has min width', () => { 100 component.fullRange = new TimeRange(time100, time200); 101 component.zoomRange = new TimeRange(time125, time126); 102 103 fixture.detectChanges(); 104 component.ngOnChanges({ 105 zoomRange: { 106 firstChange: true, 107 isFirstChange: () => true, 108 previousValue: undefined, 109 currentValue: component.zoomRange, 110 }, 111 }); 112 fixture.detectChanges(); 113 114 const sliderWidth = component.sliderBox.nativeElement.offsetWidth; 115 expect(component.sliderWidth).toBe(MIN_SLIDER_WIDTH); 116 expect(component.dragPosition.x).toBe( 117 sliderWidth / 4 - MIN_SLIDER_WIDTH / 2, 118 ); 119 }); 120 121 it('repositions slider on resize', () => { 122 const slider = assertDefined(htmlElement.querySelector('.slider')); 123 const cursor = assertDefined(htmlElement.querySelector('.cursor')); 124 125 fixture.detectChanges(); 126 127 const initialSliderXPos = slider.getBoundingClientRect().left; 128 const initialCursorXPos = cursor.getBoundingClientRect().left; 129 130 spyOnProperty( 131 component.sliderBox.nativeElement, 132 'offsetWidth', 133 'get', 134 ).and.returnValue(100); 135 expect(component.sliderBox.nativeElement.offsetWidth).toBe(100); 136 137 htmlElement.style.width = '587px'; 138 component.onResize({} as Event); 139 fixture.detectChanges(); 140 141 expect(initialSliderXPos).not.toBe(slider.getBoundingClientRect().left); 142 expect(initialCursorXPos).not.toBe(cursor.getBoundingClientRect().left); 143 }); 144 145 it('draws current position cursor', () => { 146 fixture.detectChanges(); 147 component.ngOnChanges({ 148 currentPosition: { 149 firstChange: true, 150 isFirstChange: () => true, 151 previousValue: undefined, 152 currentValue: component.currentPosition, 153 }, 154 }); 155 fixture.detectChanges(); 156 157 const sliderBox = assertDefined( 158 htmlElement.querySelector('#timeline-slider-box'), 159 ); 160 const cursor = assertDefined(htmlElement.querySelector('.cursor')); 161 const sliderBoxRect = sliderBox.getBoundingClientRect(); 162 expect(cursor.getBoundingClientRect().left).toBeCloseTo( 163 (sliderBoxRect.left + sliderBoxRect.right) / 2, 164 0, 165 ); 166 }); 167 168 it('moving slider around updates zoom', fakeAsync(async () => { 169 fixture.detectChanges(); 170 171 const initialZoom = assertDefined(component.zoomRange); 172 173 let lastZoomUpdate: TimeRange | undefined = undefined; 174 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 175 (zoom) => { 176 lastZoomUpdate = zoom; 177 }, 178 ); 179 180 const slider = htmlElement.querySelector('.slider .handle'); 181 expect(slider).toBeTruthy(); 182 expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe( 183 'visible', 184 ); 185 186 dragElement(fixture, assertDefined(slider), 100, 8); 187 188 expect(zoomChangedSpy).toHaveBeenCalled(); 189 190 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 191 expect(finalZoom.from).not.toBe(initialZoom.from); 192 expect(finalZoom.to).not.toBe(initialZoom.to); 193 expect(finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs()).toBe( 194 initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(), 195 ); 196 })); 197 198 it('moving slider left pointer around updates zoom', fakeAsync(async () => { 199 fixture.detectChanges(); 200 201 const initialZoom = assertDefined(component.zoomRange); 202 203 let lastZoomUpdate: TimeRange | undefined = undefined; 204 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 205 (zoom) => { 206 lastZoomUpdate = zoom; 207 }, 208 ); 209 210 const leftCropper = htmlElement.querySelector('.slider .cropper.left'); 211 expect(leftCropper).toBeTruthy(); 212 expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe( 213 'visible', 214 ); 215 216 dragElement(fixture, assertDefined(leftCropper), 5, 0); 217 218 expect(zoomChangedSpy).toHaveBeenCalled(); 219 220 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 221 expect(finalZoom.from).not.toBe(initialZoom.from); 222 expect(finalZoom.to).toBe(initialZoom.to); 223 })); 224 225 it('moving slider right pointer around updates zoom', fakeAsync(async () => { 226 fixture.detectChanges(); 227 228 const initialZoom = assertDefined(component.zoomRange); 229 230 let lastZoomUpdate: TimeRange | undefined = undefined; 231 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 232 (zoom) => { 233 lastZoomUpdate = zoom; 234 }, 235 ); 236 237 const rightCropper = htmlElement.querySelector('.slider .cropper.right'); 238 expect(rightCropper).toBeTruthy(); 239 expect( 240 window.getComputedStyle(assertDefined(rightCropper)).visibility, 241 ).toBe('visible'); 242 243 dragElement(fixture, assertDefined(rightCropper), 5, 0); 244 245 expect(zoomChangedSpy).toHaveBeenCalled(); 246 247 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 248 expect(finalZoom.from).toBe(initialZoom.from); 249 expect(finalZoom.to).not.toBe(initialZoom.to); 250 })); 251 252 it('cannot slide left cropper past edges', fakeAsync(() => { 253 component.zoomRange = component.fullRange; 254 fixture.detectChanges(); 255 256 const initialZoom = assertDefined(component.zoomRange); 257 258 let lastZoomUpdate: TimeRange | undefined = undefined; 259 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 260 (zoom) => { 261 lastZoomUpdate = zoom; 262 }, 263 ); 264 265 const leftCropper = htmlElement.querySelector('.slider .cropper.left'); 266 expect(leftCropper).toBeTruthy(); 267 expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe( 268 'visible', 269 ); 270 271 dragElement(fixture, assertDefined(leftCropper), -5, 0); 272 273 expect(zoomChangedSpy).toHaveBeenCalled(); 274 275 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 276 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 277 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 278 })); 279 280 it('cannot slide right cropper past edges', fakeAsync(() => { 281 component.zoomRange = component.fullRange; 282 fixture.detectChanges(); 283 284 const initialZoom = assertDefined(component.zoomRange); 285 286 let lastZoomUpdate: TimeRange | undefined = undefined; 287 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 288 (zoom) => { 289 lastZoomUpdate = zoom; 290 }, 291 ); 292 293 const rightCropper = htmlElement.querySelector('.slider .cropper.right'); 294 expect(rightCropper).toBeTruthy(); 295 expect( 296 window.getComputedStyle(assertDefined(rightCropper)).visibility, 297 ).toBe('visible'); 298 299 dragElement(fixture, assertDefined(rightCropper), 5, 0); 300 301 expect(zoomChangedSpy).toHaveBeenCalled(); 302 303 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 304 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 305 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 306 })); 307 308 it('cannot slide left cropper past right cropper', fakeAsync(() => { 309 component.zoomRange = new TimeRange(time125, time125); 310 fixture.detectChanges(); 311 312 const initialZoom = assertDefined(component.zoomRange); 313 314 let lastZoomUpdate: TimeRange | undefined = undefined; 315 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 316 (zoom) => { 317 lastZoomUpdate = zoom; 318 }, 319 ); 320 321 const leftCropper = htmlElement.querySelector('.slider .cropper.left'); 322 expect(leftCropper).toBeTruthy(); 323 expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe( 324 'visible', 325 ); 326 327 dragElement(fixture, assertDefined(leftCropper), 100, 0); 328 329 expect(zoomChangedSpy).toHaveBeenCalled(); 330 331 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 332 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 333 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 334 })); 335 336 it('cannot slide right cropper past left cropper', fakeAsync(() => { 337 component.zoomRange = new TimeRange(time125, time125); 338 fixture.detectChanges(); 339 340 const initialZoom = assertDefined(component.zoomRange); 341 342 let lastZoomUpdate: TimeRange | undefined = undefined; 343 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 344 (zoom) => { 345 lastZoomUpdate = zoom; 346 }, 347 ); 348 349 const rightCropper = htmlElement.querySelector('.slider .cropper.right'); 350 expect(rightCropper).toBeTruthy(); 351 expect( 352 window.getComputedStyle(assertDefined(rightCropper)).visibility, 353 ).toBe('visible'); 354 355 dragElement(fixture, assertDefined(rightCropper), -100, 0); 356 357 expect(zoomChangedSpy).toHaveBeenCalled(); 358 359 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 360 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 361 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 362 })); 363 364 it('cannot move slider past edges', fakeAsync(() => { 365 component.zoomRange = component.fullRange; 366 fixture.detectChanges(); 367 368 const initialZoom = assertDefined(component.zoomRange); 369 370 let lastZoomUpdate: TimeRange | undefined = undefined; 371 const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake( 372 (zoom) => { 373 lastZoomUpdate = zoom; 374 }, 375 ); 376 377 const slider = htmlElement.querySelector('.slider .handle'); 378 expect(slider).toBeTruthy(); 379 expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe( 380 'visible', 381 ); 382 383 dragElement(fixture, assertDefined(slider), 100, 8); 384 385 expect(zoomChangedSpy).toHaveBeenCalled(); 386 387 const finalZoom = assertDefined<TimeRange>(lastZoomUpdate); 388 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 389 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 390 })); 391}); 392