202 lines
6.8 KiB
Kotlin
202 lines
6.8 KiB
Kotlin
package solutions.tretter.mindmachine.session
|
|
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Test
|
|
import kotlin.math.abs
|
|
|
|
/**
|
|
* Unit tests for TimelineViewport gesture handling logic.
|
|
* These tests verify zoom/pan transformations without requiring a device or emulator.
|
|
*/
|
|
class TimelineViewportGestureTest {
|
|
|
|
@Test
|
|
fun `applyZoomPan zooms in around centroid correctly`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
val centroidPx = widthPx / 2f // Center of screen
|
|
|
|
// Zoom in 2x around center
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 2.0f,
|
|
panPx = 0f,
|
|
widthPx = widthPx,
|
|
centroidPx = centroidPx
|
|
)
|
|
|
|
// Seconds per screen should halve (zoom in)
|
|
assertEquals(300f, result.secondsPerScreen, 1f)
|
|
// Center time should stay the same (300 seconds into timeline)
|
|
// Time at x position: startSec + (x / widthPx) * secondsPerScreen
|
|
val centerTime = viewport.startSec + (widthPx / 2f) / widthPx * viewport.secondsPerScreen
|
|
val resultCenterTime = result.startSec + (widthPx / 2f) / widthPx * result.secondsPerScreen
|
|
assertEquals(centerTime, resultCenterTime, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan zooms out around centroid correctly`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 300f)
|
|
val widthPx = 400f
|
|
val centroidPx = widthPx / 2f // Center of screen
|
|
|
|
// Zoom out 2x around center
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 0.5f,
|
|
panPx = 0f,
|
|
widthPx = widthPx,
|
|
centroidPx = centroidPx
|
|
)
|
|
|
|
// Seconds per screen should double (zoom out)
|
|
assertEquals(600f, result.secondsPerScreen, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan respects minimum zoom level`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 10f)
|
|
val widthPx = 400f
|
|
|
|
// Try to zoom in beyond minimum (30 seconds per screen)
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 1000f, // Extreme zoom
|
|
panPx = 0f,
|
|
widthPx = widthPx,
|
|
centroidPx = widthPx / 2f
|
|
)
|
|
|
|
// Should clamp to minimum
|
|
assertEquals(30f, result.secondsPerScreen, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan respects maximum zoom level`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 3600f) // 1 hour
|
|
val widthPx = 400f
|
|
|
|
// Try to zoom out beyond maximum (24 hours)
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 0.001f, // Extreme zoom out
|
|
panPx = 0f,
|
|
widthPx = widthPx,
|
|
centroidPx = widthPx / 2f
|
|
)
|
|
|
|
// Should clamp to maximum (86400 seconds = 24 hours)
|
|
assertEquals(86400f, result.secondsPerScreen, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan pans horizontally correctly`() {
|
|
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
|
|
// Pan right by 100 pixels
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 1f,
|
|
panPx = -100f, // Negative pan moves viewport right
|
|
widthPx = widthPx,
|
|
centroidPx = widthPx / 2f
|
|
)
|
|
|
|
// Calculate expected start time after pan
|
|
// Pan amount in seconds: -100px * (600s / 400px) = -150s
|
|
// Should move 150 seconds earlier
|
|
assertEquals(250f, result.startSec, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan prevents negative start time`() {
|
|
val viewport = TimelineViewport(startSec = 10f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
|
|
// Pan left (past start of timeline)
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 1f,
|
|
panPx = 200f, // Pan left
|
|
widthPx = widthPx,
|
|
centroidPx = widthPx / 2f
|
|
)
|
|
|
|
// Should clamp to 0
|
|
assertEquals(0f, result.startSec, 0.1f)
|
|
}
|
|
|
|
@Test
|
|
fun `timeToX converts time to X coordinate correctly`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
|
|
// 300 seconds should be at 50% of width
|
|
val x = viewport.timeToX(300, widthPx)
|
|
assertEquals(200f, x, 0.5f)
|
|
}
|
|
|
|
@Test
|
|
fun `xToTimeSec converts X coordinate to time correctly`() {
|
|
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
|
|
// 200px (center) should map to start + 300 seconds
|
|
val time = viewport.xToTimeSec(200f, widthPx)
|
|
assertEquals(400f, time, 0.5f)
|
|
}
|
|
|
|
@Test
|
|
fun `xToTimeSec and timeToX are inverses`() {
|
|
val viewport = TimelineViewport(startSec = 50f, secondsPerScreen = 300f)
|
|
val widthPx = 400f
|
|
|
|
val originalTime = 200f
|
|
val x = viewport.timeToX(originalTime.toInt(), widthPx)
|
|
val resultTime = viewport.xToTimeSec(x, widthPx)
|
|
|
|
assertEquals(originalTime, resultTime, 0.5f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan maintains view on centroid when zooming without panning`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
val centroidPx = 200f // Center of screen
|
|
|
|
// Get the time at centroid before zoom
|
|
val timeAtCentroidBefore = viewport.xToTimeSec(centroidPx, widthPx)
|
|
|
|
// Zoom without panning
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 2f,
|
|
panPx = 0f, // No panning
|
|
widthPx = widthPx,
|
|
centroidPx = centroidPx
|
|
)
|
|
|
|
// The time at centroid should remain the same when only zooming
|
|
val timeAtCentroidAfter = result.xToTimeSec(centroidPx, widthPx)
|
|
assertEquals(timeAtCentroidBefore, timeAtCentroidAfter, 1f)
|
|
}
|
|
|
|
@Test
|
|
fun `applyZoomPan with panning changes time at centroid appropriately`() {
|
|
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
|
|
val widthPx = 400f
|
|
val centroidPx = 200f // Center of screen
|
|
|
|
// Get the time at centroid before panning
|
|
val timeAtCentroidBefore = viewport.xToTimeSec(centroidPx, widthPx)
|
|
|
|
// Pan right (negative pan moves view to later times)
|
|
val result = viewport.applyZoomPan(
|
|
zoomChange = 1f, // No zoom
|
|
panPx = -50f, // Pan right (negative = moves to later times)
|
|
widthPx = widthPx,
|
|
centroidPx = centroidPx
|
|
)
|
|
|
|
// The time at centroid should change when panning
|
|
val timeAtCentroidAfter = result.xToTimeSec(centroidPx, widthPx)
|
|
// Pan right should move to later times, so time should increase
|
|
assertTrue("Panning right should increase time at centroid", timeAtCentroidAfter > timeAtCentroidBefore)
|
|
}
|
|
}
|