Auto commit Fri Mar 20 11:01:01 PM CDT 2026
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package com.mindmachine.mvp.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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user