1 /* <lambda>null2 * 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 17 @file:OptIn(ExperimentalCoroutinesApi::class) 18 19 package com.android.mechanics 20 21 import android.util.Log 22 import android.util.Log.TerribleFailureHandler 23 import androidx.compose.runtime.LaunchedEffect 24 import androidx.compose.runtime.mutableFloatStateOf 25 import androidx.compose.runtime.mutableStateOf 26 import androidx.compose.runtime.snapshotFlow 27 import androidx.compose.ui.test.ExperimentalTestApi 28 import androidx.compose.ui.test.TestMonotonicFrameClock 29 import androidx.compose.ui.test.junit4.createComposeRule 30 import androidx.test.ext.junit.runners.AndroidJUnit4 31 import com.android.mechanics.spec.BreakpointKey 32 import com.android.mechanics.spec.DirectionalMotionSpec 33 import com.android.mechanics.spec.Guarantee 34 import com.android.mechanics.spec.InputDirection 35 import com.android.mechanics.spec.Mapping 36 import com.android.mechanics.spec.MotionSpec 37 import com.android.mechanics.spec.builder 38 import com.android.mechanics.spec.reverseBuilder 39 import com.android.mechanics.testing.DefaultSprings.matStandardDefault 40 import com.android.mechanics.testing.DefaultSprings.matStandardFast 41 import com.android.mechanics.testing.MotionValueToolkit 42 import com.android.mechanics.testing.MotionValueToolkit.Companion.dataPoints 43 import com.android.mechanics.testing.MotionValueToolkit.Companion.input 44 import com.android.mechanics.testing.MotionValueToolkit.Companion.isStable 45 import com.android.mechanics.testing.MotionValueToolkit.Companion.output 46 import com.android.mechanics.testing.VerifyTimeSeriesResult.AssertTimeSeriesMatchesGolden 47 import com.android.mechanics.testing.VerifyTimeSeriesResult.SkipGoldenVerification 48 import com.android.mechanics.testing.goldenTest 49 import com.google.common.truth.Truth.assertThat 50 import com.google.common.truth.Truth.assertWithMessage 51 import kotlinx.coroutines.CoroutineScope 52 import kotlinx.coroutines.ExperimentalCoroutinesApi 53 import kotlinx.coroutines.launch 54 import kotlinx.coroutines.test.TestCoroutineScheduler 55 import kotlinx.coroutines.test.TestScope 56 import kotlinx.coroutines.test.runTest 57 import kotlinx.coroutines.withContext 58 import org.junit.Rule 59 import org.junit.Test 60 import org.junit.rules.ExternalResource 61 import org.junit.runner.RunWith 62 import platform.test.motion.MotionTestRule 63 import platform.test.motion.testing.createGoldenPathManager 64 65 @RunWith(AndroidJUnit4::class) 66 class MotionValueTest { 67 private val goldenPathManager = 68 createGoldenPathManager("frameworks/libs/systemui/mechanics/tests/goldens") 69 70 @get:Rule(order = 0) val rule = createComposeRule() 71 @get:Rule(order = 1) val motion = MotionTestRule(MotionValueToolkit(rule), goldenPathManager) 72 @get:Rule(order = 2) val wtfLog = WtfLogRule() 73 74 @Test 75 fun emptySpec_outputMatchesInput_withoutAnimation() = 76 motion.goldenTest( 77 spec = MotionSpec.Empty, 78 verifyTimeSeries = { 79 // Output always matches the input 80 assertThat(output).containsExactlyElementsIn(input).inOrder() 81 // There must never be an ongoing animation. 82 assertThat(isStable).doesNotContain(false) 83 84 AssertTimeSeriesMatchesGolden 85 }, 86 ) { 87 animateValueTo(100f) 88 } 89 90 // TODO the tests should describe the expected values not only in terms of goldens, but 91 // also explicitly in verifyTimeSeries 92 93 @Test 94 fun changingInput_addsAnimationToMapping_becomesStable() = 95 motion.goldenTest( 96 spec = 97 specBuilder(Mapping.Zero) 98 .toBreakpoint(1f) 99 .completeWith(Mapping.Linear(factor = 0.5f)) 100 ) { 101 animateValueTo(1.1f, changePerFrame = 0.5f) 102 while (underTest.isStable) { 103 updateValue(input + 0.5f) 104 awaitFrames() 105 } 106 } 107 108 @Test 109 fun segmentChange_inMaxDirection_animatedWhenReachingBreakpoint() = 110 motion.goldenTest( 111 spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One) 112 ) { 113 animateValueTo(1f, changePerFrame = 0.5f) 114 awaitStable() 115 } 116 117 @Test 118 fun segmentChange_inMinDirection_animatedWhenReachingBreakpoint() = 119 motion.goldenTest( 120 initialValue = 2f, 121 initialDirection = InputDirection.Min, 122 spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One), 123 ) { 124 animateValueTo(1f, changePerFrame = 0.5f) 125 awaitStable() 126 } 127 128 @Test 129 fun segmentChange_inMaxDirection_springAnimationStartedRetroactively() = 130 motion.goldenTest( 131 spec = specBuilder(Mapping.Zero).toBreakpoint(.75f).completeWith(Mapping.One) 132 ) { 133 animateValueTo(1f, changePerFrame = 0.5f) 134 awaitStable() 135 } 136 137 @Test 138 fun segmentChange_inMinDirection_springAnimationStartedRetroactively() = 139 motion.goldenTest( 140 initialValue = 2f, 141 initialDirection = InputDirection.Min, 142 spec = specBuilder(Mapping.Zero).toBreakpoint(1.25f).completeWith(Mapping.One), 143 ) { 144 animateValueTo(1f, changePerFrame = 0.5f) 145 awaitStable() 146 } 147 148 @Test 149 fun segmentChange_guaranteeNone_springAnimatesIndependentOfInput() = 150 motion.goldenTest( 151 spec = 152 specBuilder(Mapping.Zero) 153 .toBreakpoint(1f) 154 .completeWith(Mapping.One, guarantee = Guarantee.None) 155 ) { 156 animateValueTo(5f, changePerFrame = 0.5f) 157 awaitStable() 158 } 159 160 @Test 161 fun segmentChange_guaranteeInputDelta_springCompletesWithinDistance() = 162 motion.goldenTest( 163 spec = 164 specBuilder(Mapping.Zero) 165 .toBreakpoint(1f) 166 .completeWith(Mapping.One, guarantee = Guarantee.InputDelta(3f)) 167 ) { 168 animateValueTo(4f, changePerFrame = 0.5f) 169 } 170 171 @Test 172 fun segmentChange_guaranteeGestureDragDelta_springCompletesWithinDistance() = 173 motion.goldenTest( 174 spec = 175 specBuilder(Mapping.Zero) 176 .toBreakpoint(1f) 177 .completeWith(Mapping.One, guarantee = Guarantee.GestureDragDelta(3f)) 178 ) { 179 animateValueTo(1f, changePerFrame = 0.5f) 180 while (!underTest.isStable) { 181 gestureContext.dragOffset += 0.5f 182 awaitFrames() 183 } 184 } 185 186 @Test 187 fun segmentChange_appliesOutputVelocity_atSpringStart() = 188 motion.goldenTest(spec = specBuilder().toBreakpoint(10f).completeWith(Mapping.Fixed(20f))) { 189 animateValueTo(11f, changePerFrame = 3f) 190 awaitStable() 191 } 192 193 @Test 194 fun segmentChange_appliesOutputVelocity_springVelocityIsNotAppliedTwice() = 195 motion.goldenTest( 196 spec = 197 specBuilder() 198 .toBreakpoint(10f) 199 .continueWith(Mapping.Linear(factor = 1f, offset = 20f)) 200 .toBreakpoint(20f) 201 .completeWith(Mapping.Fixed(40f)) 202 ) { 203 animateValueTo(21f, changePerFrame = 3f) 204 awaitStable() 205 } 206 207 @Test 208 fun specChange_shiftSegmentBackwards_doesNotAnimateWithinSegment_animatesSegmentChange() { 209 fun generateSpec(offset: Float) = 210 specBuilder(Mapping.Zero) 211 .toBreakpoint(offset, B1) 212 .jumpTo(1f) 213 .continueWithTargetValue(2f) 214 .toBreakpoint(offset + 1f, B2) 215 .completeWith(Mapping.Zero) 216 217 motion.goldenTest(spec = generateSpec(0f), initialValue = .5f) { 218 var offset = 0f 219 repeat(4) { 220 offset -= .2f 221 underTest.spec = generateSpec(offset) 222 awaitFrames() 223 } 224 awaitStable() 225 } 226 } 227 228 @Test 229 fun specChange_shiftSegmentForward_doesNotAnimateWithinSegment_animatesSegmentChange() { 230 fun generateSpec(offset: Float) = 231 specBuilder(Mapping.Zero) 232 .toBreakpoint(offset, B1) 233 .jumpTo(1f) 234 .continueWithTargetValue(2f) 235 .toBreakpoint(offset + 1f, B2) 236 .completeWith(Mapping.Zero) 237 238 motion.goldenTest(spec = generateSpec(0f), initialValue = .5f) { 239 var offset = 0f 240 repeat(4) { 241 offset += .2f 242 underTest.spec = generateSpec(offset) 243 awaitFrames() 244 } 245 awaitStable() 246 } 247 } 248 249 @Test 250 fun directionChange_maxToMin_changesSegmentWithDirectionChange() = 251 motion.goldenTest( 252 spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One), 253 initialValue = 2f, 254 initialDirection = InputDirection.Max, 255 directionChangeSlop = 3f, 256 ) { 257 animateValueTo(-2f, changePerFrame = 0.5f) 258 awaitStable() 259 } 260 261 @Test 262 fun directionChange_minToMax_changesSegmentWithDirectionChange() = 263 motion.goldenTest( 264 spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One), 265 initialValue = 0f, 266 initialDirection = InputDirection.Min, 267 directionChangeSlop = 3f, 268 ) { 269 animateValueTo(4f, changePerFrame = 0.5f) 270 awaitStable() 271 } 272 273 @Test 274 fun directionChange_maxToMin_appliesGuarantee_afterDirectionChange() = 275 motion.goldenTest( 276 spec = 277 specBuilder(Mapping.Zero) 278 .toBreakpoint(1f) 279 .completeWith(Mapping.One, guarantee = Guarantee.InputDelta(1f)), 280 initialValue = 2f, 281 initialDirection = InputDirection.Max, 282 directionChangeSlop = 3f, 283 ) { 284 animateValueTo(-2f, changePerFrame = 0.5f) 285 awaitStable() 286 } 287 288 @Test 289 fun traverseSegments_maxDirection_noGuarantee_addsDiscontinuityToOngoingAnimation() = 290 motion.goldenTest( 291 spec = 292 specBuilder(Mapping.Zero) 293 .toBreakpoint(1f) 294 .continueWith(Mapping.One) 295 .toBreakpoint(2f) 296 .completeWith(Mapping.Two) 297 ) { 298 animateValueTo(3f, changePerFrame = 0.2f) 299 awaitStable() 300 } 301 302 @Test 303 fun traverseSegmentsInOneFrame_noGuarantee_combinesDiscontinuity() = 304 motion.goldenTest( 305 spec = 306 specBuilder(Mapping.Zero) 307 .toBreakpoint(1f) 308 .continueWith(Mapping.One) 309 .toBreakpoint(2f) 310 .completeWith(Mapping.Two) 311 ) { 312 updateValue(2.5f) 313 awaitStable() 314 } 315 316 @Test 317 fun traverseSegmentsInOneFrame_withGuarantee_appliesGuarantees() = 318 motion.goldenTest( 319 spec = 320 specBuilder(Mapping.Zero) 321 .toBreakpoint(1f) 322 .jumpBy(5f, guarantee = Guarantee.InputDelta(.9f)) 323 .continueWithConstantValue() 324 .toBreakpoint(2f) 325 .jumpBy(1f, guarantee = Guarantee.InputDelta(.9f)) 326 .continueWithConstantValue() 327 .complete() 328 ) { 329 updateValue(2.1f) 330 awaitStable() 331 } 332 333 @Test 334 fun traverseSegmentsInOneFrame_withDirectionChange_appliesGuarantees() = 335 motion.goldenTest( 336 spec = 337 specBuilder(Mapping.Zero) 338 .toBreakpoint(1f) 339 .continueWith(Mapping.One, guarantee = Guarantee.InputDelta(1f)) 340 .toBreakpoint(2f) 341 .completeWith(Mapping.Two), 342 initialValue = 2.5f, 343 initialDirection = InputDirection.Max, 344 directionChangeSlop = 1f, 345 ) { 346 updateValue(.5f) 347 animateValueTo(0f) 348 awaitStable() 349 } 350 351 @Test 352 fun changeDirection_flipsBetweenDirectionalSegments() { 353 val spec = 354 MotionSpec( 355 maxDirection = forwardSpecBuilder(Mapping.Zero).complete(), 356 minDirection = reverseSpecBuilder(Mapping.One).complete(), 357 ) 358 359 motion.goldenTest( 360 spec = spec, 361 initialValue = 2f, 362 initialDirection = InputDirection.Max, 363 directionChangeSlop = 1f, 364 ) { 365 animateValueTo(0f) 366 awaitStable() 367 } 368 } 369 370 @Test 371 fun derivedValue_reflectsInputChangeInSameFrame() { 372 motion.goldenTest( 373 spec = specBuilder(Mapping.Zero).toBreakpoint(0.5f).completeWith(Mapping.One), 374 createDerived = { primary -> 375 listOf(MotionValue.createDerived(primary, MotionSpec.Empty, label = "derived")) 376 }, 377 verifyTimeSeries = { 378 // the output of the derived value must match the primary value 379 assertThat(output) 380 .containsExactlyElementsIn(dataPoints<Float>("derived-output")) 381 .inOrder() 382 // and its never animated. 383 assertThat(dataPoints<Float>("derived-isStable")).doesNotContain(false) 384 385 AssertTimeSeriesMatchesGolden 386 }, 387 ) { 388 animateValueTo(1f, changePerFrame = 0.1f) 389 awaitStable() 390 } 391 } 392 393 @Test 394 fun derivedValue_hasAnimationLifecycleOnItsOwn() { 395 motion.goldenTest( 396 spec = specBuilder(Mapping.Zero).toBreakpoint(0.5f).completeWith(Mapping.One), 397 createDerived = { primary -> 398 listOf( 399 MotionValue.createDerived( 400 primary, 401 specBuilder(Mapping.One).toBreakpoint(0.5f).completeWith(Mapping.Zero), 402 label = "derived", 403 ) 404 ) 405 }, 406 ) { 407 animateValueTo(1f, changePerFrame = 0.1f) 408 awaitStable() 409 } 410 } 411 412 @Test 413 fun nonFiniteNumbers_producesNaN_recoversOnSubsequentFrames() { 414 motion.goldenTest( 415 spec = specBuilder(Mapping { if (it >= 1f) Float.NaN else 0f }).complete(), 416 verifyTimeSeries = { 417 assertThat(output.drop(1).take(5)) 418 .containsExactlyElementsIn(listOf(0f, Float.NaN, Float.NaN, 0f, 0f)) 419 .inOrder() 420 SkipGoldenVerification 421 }, 422 ) { 423 animatedInputSequence(0f, 1f, 1f, 0f, 0f) 424 } 425 426 assertThat(wtfLog.loggedFailures).isEmpty() 427 } 428 429 @Test 430 fun nonFiniteNumbers_segmentChange_skipsAnimation() { 431 motion.goldenTest( 432 spec = MotionSpec.Empty, 433 verifyTimeSeries = { 434 // The mappings produce a non-finite number during a segment change. 435 // The animation thereof is skipped to avoid poisoning the state with non-finite 436 // numbers 437 assertThat(output.drop(1).take(5)) 438 .containsExactlyElementsIn(listOf(0f, 1f, Float.NaN, 0f, 0f)) 439 .inOrder() 440 SkipGoldenVerification 441 }, 442 ) { 443 animatedInputSequence(0f, 1f) 444 underTest.spec = 445 specBuilder() 446 .toBreakpoint(0f) 447 .completeWith(Mapping { if (it >= 1f) Float.NaN else 0f }) 448 awaitFrames() 449 450 animatedInputSequence(0f, 0f) 451 } 452 453 assertThat(wtfLog.loggedFailures).hasSize(1) 454 assertThat(wtfLog.loggedFailures.first()).startsWith("Delta between mappings is undefined") 455 } 456 457 @Test 458 fun nonFiniteNumbers_segmentTraverse_skipsAnimation() { 459 motion.goldenTest( 460 spec = 461 specBuilder(Mapping.Zero) 462 .toBreakpoint(1f) 463 .completeWith(Mapping { if (it < 2f) Float.NaN else 2f }), 464 verifyTimeSeries = { 465 // The mappings produce a non-finite number during a breakpoint traversal. 466 // The animation thereof is skipped to avoid poisoning the state with non-finite 467 // numbers 468 assertThat(output.drop(1).take(6)) 469 .containsExactlyElementsIn(listOf(0f, 0f, Float.NaN, Float.NaN, 2f, 2f)) 470 .inOrder() 471 SkipGoldenVerification 472 }, 473 ) { 474 animatedInputSequence(0f, 0.5f, 1f, 1.5f, 2f, 3f) 475 } 476 assertThat(wtfLog.loggedFailures).hasSize(1) 477 assertThat(wtfLog.loggedFailures.first()) 478 .startsWith("Delta between breakpoints is undefined") 479 } 480 481 @Test 482 fun keepRunning_concurrentInvocationThrows() = runTestWithFrameClock { testScheduler, _ -> 483 val underTest = MotionValue({ 1f }, FakeGestureContext, label = "Foo") 484 val realJob = launch { underTest.keepRunning() } 485 testScheduler.runCurrent() 486 487 assertThat(realJob.isActive).isTrue() 488 try { 489 underTest.keepRunning() 490 // keepRunning returns Nothing, will never get here 491 } catch (e: Throwable) { 492 assertThat(e).isInstanceOf(IllegalStateException::class.java) 493 assertThat(e).hasMessageThat().contains("MotionValue(Foo) is already running") 494 } 495 assertThat(realJob.isActive).isTrue() 496 realJob.cancel() 497 } 498 499 @Test 500 fun keepRunning_suspendsWithoutAnAnimation() = runTest { 501 val input = mutableFloatStateOf(0f) 502 val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One) 503 val underTest = MotionValue(input::value, FakeGestureContext, spec) 504 rule.setContent { LaunchedEffect(Unit) { underTest.keepRunning() } } 505 506 val inspector = underTest.debugInspector() 507 var framesCount = 0 508 backgroundScope.launch { snapshotFlow { inspector.frame }.collect { framesCount++ } } 509 510 rule.awaitIdle() 511 framesCount = 0 512 rule.mainClock.autoAdvance = false 513 514 assertThat(inspector.isActive).isTrue() 515 assertThat(inspector.isAnimating).isFalse() 516 517 // Update the value, but WITHOUT causing an animation 518 input.floatValue = 0.5f 519 rule.awaitIdle() 520 521 // Still on the old frame.. 522 assertThat(framesCount).isEqualTo(0) 523 // ... [underTest] is now waiting for an animation frame 524 assertThat(inspector.isAnimating).isTrue() 525 526 rule.mainClock.advanceTimeByFrame() 527 rule.awaitIdle() 528 529 // Produces the frame.. 530 assertThat(framesCount).isEqualTo(1) 531 // ... and is suspended again. 532 assertThat(inspector.isAnimating).isTrue() 533 534 rule.mainClock.advanceTimeByFrame() 535 rule.awaitIdle() 536 537 // Produces the frame.. 538 assertThat(framesCount).isEqualTo(2) 539 // ... and is suspended again. 540 assertThat(inspector.isAnimating).isFalse() 541 542 rule.mainClock.autoAdvance = true 543 rule.awaitIdle() 544 // Ensure that no more frames are produced 545 assertThat(framesCount).isEqualTo(2) 546 } 547 548 @Test 549 fun keepRunning_remainsActiveWhileAnimating() = runTest { 550 val input = mutableFloatStateOf(0f) 551 val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One) 552 val underTest = MotionValue(input::value, FakeGestureContext, spec) 553 rule.setContent { LaunchedEffect(Unit) { underTest.keepRunning() } } 554 555 val inspector = underTest.debugInspector() 556 var framesCount = 0 557 backgroundScope.launch { snapshotFlow { inspector.frame }.collect { framesCount++ } } 558 559 rule.awaitIdle() 560 framesCount = 0 561 rule.mainClock.autoAdvance = false 562 563 assertThat(inspector.isActive).isTrue() 564 assertThat(inspector.isAnimating).isFalse() 565 566 // Update the value, WITH triggering an animation 567 input.floatValue = 1.5f 568 rule.awaitIdle() 569 570 // Still on the old frame.. 571 assertThat(framesCount).isEqualTo(0) 572 // ... [underTest] is now waiting for an animation frame 573 assertThat(inspector.isAnimating).isTrue() 574 575 // A couple frames should be generated without pausing 576 repeat(5) { 577 rule.mainClock.advanceTimeByFrame() 578 rule.awaitIdle() 579 580 // The spring is still settling... 581 assertThat(inspector.frame.isStable).isFalse() 582 // ... animation keeps going ... 583 assertThat(inspector.isAnimating).isTrue() 584 // ... and frames are produces... 585 assertThat(framesCount).isEqualTo(it + 1) 586 } 587 588 val timeBeforeAutoAdvance = rule.mainClock.currentTime 589 590 // But this will stop as soon as the animation is finished. Skip forward. 591 rule.mainClock.autoAdvance = true 592 rule.awaitIdle() 593 594 // At which point the spring is stable again... 595 assertThat(inspector.frame.isStable).isTrue() 596 // ... and animations are suspended again. 597 assertThat(inspector.isAnimating).isFalse() 598 599 rule.awaitIdle() 600 601 // Stabilizing the spring during awaitIdle() took 160ms (obtained from looking at reference 602 // test runs). That time is expected to be 100% reproducible, given the starting 603 // state/configuration of the spring before awaitIdle(). 604 assertThat(rule.mainClock.currentTime).isEqualTo(timeBeforeAutoAdvance + 160) 605 } 606 607 @Test 608 fun keepRunningWhile_stopRunningWhileStable_endsImmediately() = runTest { 609 val input = mutableFloatStateOf(0f) 610 val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One) 611 val underTest = MotionValue(input::value, FakeGestureContext, spec) 612 613 val continueRunning = mutableStateOf(true) 614 615 rule.setContent { 616 LaunchedEffect(Unit) { underTest.keepRunningWhile { continueRunning.value } } 617 } 618 619 val inspector = underTest.debugInspector() 620 621 rule.awaitIdle() 622 623 assertWithMessage("isActive").that(inspector.isActive).isTrue() 624 assertWithMessage("isAnimating").that(inspector.isAnimating).isFalse() 625 626 val timeBeforeStopRunning = rule.mainClock.currentTime 627 continueRunning.value = false 628 rule.awaitIdle() 629 630 assertWithMessage("isActive").that(inspector.isActive).isFalse() 631 assertWithMessage("isAnimating").that(inspector.isAnimating).isFalse() 632 assertThat(rule.mainClock.currentTime).isEqualTo(timeBeforeStopRunning) 633 } 634 635 @Test 636 fun debugInspector_sameInstance_whileInUse() { 637 val underTest = MotionValue({ 1f }, FakeGestureContext) 638 639 val originalInspector = underTest.debugInspector() 640 assertThat(underTest.debugInspector()).isSameInstanceAs(originalInspector) 641 } 642 643 @Test 644 fun debugInspector_newInstance_afterUnused() { 645 val underTest = MotionValue({ 1f }, FakeGestureContext) 646 647 val originalInspector = underTest.debugInspector() 648 originalInspector.dispose() 649 assertThat(underTest.debugInspector()).isNotSameInstanceAs(originalInspector) 650 } 651 652 @OptIn(ExperimentalTestApi::class) 653 private fun runTestWithFrameClock( 654 testBody: 655 suspend CoroutineScope.( 656 testScheduler: TestCoroutineScheduler, backgroundScope: CoroutineScope, 657 ) -> Unit 658 ) = runTest { 659 val testScope: TestScope = this 660 withContext(TestMonotonicFrameClock(testScope, FrameDelayNanos)) { 661 testBody(testScope.testScheduler, testScope.backgroundScope) 662 } 663 } 664 665 class WtfLogRule : ExternalResource() { 666 val loggedFailures = mutableListOf<String>() 667 668 private lateinit var oldHandler: TerribleFailureHandler 669 670 override fun before() { 671 oldHandler = 672 Log.setWtfHandler { tag, what, _ -> 673 if (tag == MotionValue.TAG) { 674 loggedFailures.add(checkNotNull(what.message)) 675 } 676 } 677 } 678 679 override fun after() { 680 Log.setWtfHandler(oldHandler) 681 } 682 } 683 684 companion object { 685 val B1 = BreakpointKey("breakpoint1") 686 val B2 = BreakpointKey("breakpoint2") 687 val FakeGestureContext = 688 object : GestureContext { 689 override val direction: InputDirection 690 get() = InputDirection.Max 691 692 override val dragOffset: Float 693 get() = 0f 694 } 695 private val FrameDelayNanos: Long = 16_000_000L 696 697 fun specBuilder(firstSegment: Mapping = Mapping.Identity) = 698 MotionSpec.builder( 699 defaultSpring = matStandardDefault, 700 resetSpring = matStandardFast, 701 initialMapping = firstSegment, 702 ) 703 704 fun forwardSpecBuilder(firstSegment: Mapping = Mapping.Identity) = 705 DirectionalMotionSpec.builder( 706 defaultSpring = matStandardDefault, 707 initialMapping = firstSegment, 708 ) 709 710 fun reverseSpecBuilder(firstSegment: Mapping = Mapping.Identity) = 711 DirectionalMotionSpec.reverseBuilder( 712 defaultSpring = matStandardDefault, 713 initialMapping = firstSegment, 714 ) 715 } 716 } 717