Files
MindMachine/app/src/test/java/com/mindmachine/mvp/TimelineViewportGestureTest.kt

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)
}
}