Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ android {
lint {
// AGP stays within the installed Android Studio support range; keep updates visible in lint.
informational += "AndroidGradlePluginVersion"
// New library releases are maintenance advice, not a defect in the pinned build.
// Keep them visible without forcing unplanned runtime upgrades to pass CI.
informational += "GradleDependency"
warningsAsErrors = true
error += "AutoboxingStateCreation"
abortOnError = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kairo.reader.ui.rsvp

import android.os.SystemClock
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -34,6 +35,7 @@ internal class RsvpDeviceFixture(tokens: List<Token>, config: RsvpConfig, startI
),
)
val consumed = CopyOnWriteArrayList<RsvpFrame>()
val consumedAtMs = CopyOnWriteArrayList<Long>()

@Volatile var saved: RsvpResumePoint? = null

Expand Down Expand Up @@ -64,7 +66,10 @@ internal class RsvpDeviceFixture(tokens: List<Token>, config: RsvpConfig, startI
onPositionChanged = {},
onTempoChange = {},
onExit = { saved = it },
onFrameConsumed = { consumed += it },
onFrameConsumed = {
consumedAtMs += SystemClock.elapsedRealtime()
consumed += it
},
),
preferences = RsvpPreferenceCallbacks(
onExtremeSpeedUnlockedChange = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import com.kairo.reader.TestActivity
import com.kairo.reader.core.model.BlinkMode
import com.kairo.reader.core.model.RsvpConfig
import com.kairo.reader.core.model.RsvpContextAssistMode
import com.kairo.reader.core.model.RsvpProfile
import com.kairo.reader.core.model.RsvpResumeCursor
import com.kairo.reader.core.model.Token
import com.kairo.reader.core.model.TokenType
import com.kairo.reader.core.model.defaultConfig
import com.kairo.reader.core.model.withReaderPreferencesFrom
import com.kairo.reader.ui.theme.KairoTheme
import java.io.File
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -161,6 +164,40 @@ class RsvpDeviceSmokeTest {
capture("playback-completed")
}

@Test
fun changingPresetWhilePausedKeepsContextAndSourcePosition() {
val current = RsvpProfile.FOCUS.defaultConfig().copy(
contextAssistMode = RsvpContextAssistMode.FULL_CLAUSE,
blinkMode = BlinkMode.ADAPTIVE,
tempoMsPerWord = 250L,
)
val fixture = RsvpDeviceFixture(
listOf(word("Earlier"), word("measurements"), word("contain"), word("uncertainty")),
current,
startIndex = 1,
)
fixture.state = fixture.state.copy(initialIsPlaying = false)
show(fixture)
awaitLoaded(fixture)
for ((index, preset) in listOf(RsvpProfile.FLOW, RsvpProfile.STUDY, RsvpProfile.NARRATIVE).withIndex()) {
composeRule.runOnIdle {
fixture.state = fixture.state.copy(
profile = fixture.state.profile.copy(
config = preset.defaultConfig().withReaderPreferencesFrom(current),
)
)
}
awaitLoaded(fixture, minimumLoads = index + 2)
composeRule.onNodeWithText("measurements", useUnmergedTree = true).assertIsDisplayed()
capture("preset-${preset.name.lowercase()}")
}
click(R.string.content_desc_close)
composeRule.runOnIdle {
assertEquals(1, requireNotNull(fixture.saved).tokenIndex)
assertEquals(RsvpContextAssistMode.FULL_CLAUSE, fixture.state.profile.config.contextAssistMode)
}
}

private fun show(fixture: RsvpDeviceFixture) {
composeRule.setContent {
KairoTheme {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.kairo.reader.ui.rsvp

import android.os.SystemClock
import androidx.activity.compose.setContent
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kairo.reader.TestActivity
import com.kairo.reader.core.model.RsvpConfig
import com.kairo.reader.core.model.Token
import com.kairo.reader.core.model.TokenType
import com.kairo.reader.ui.theme.KairoTheme
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

/** Use the normal Android runtime, without Compose test-clock advancement, to measure exposure. */
@RunWith(AndroidJUnit4::class)
class RsvpPunctuationTimingDeviceTest {
@Test
fun fasterLiveTempoKeepsThePunctuationHoldOnScreen() {
val fixture = RsvpDeviceFixture(
listOf(word("Before"), word("home"), Token(".", TokenType.PUNCTUATION), word("Next")),
RsvpConfig(
enablePhraseChunking = false,
tempoMsPerWord = 200L,
periodPauseMs = 1000L,
minPauseScale = 0.5,
useAdaptiveTiming = false,
usePunctuationLandingHold = false,
startDelayMs = 0L,
endDelayMs = 0L,
rampUpFrames = 0,
rampDownFrames = 0,
),
)
fixture.state = fixture.state.copy(launchTempoMsPerWord = 80L)
ActivityScenario.launch(TestActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
activity.setContent {
KairoTheme {
RsvpScreen(fixture.state, fixture.callbacks, fixture.dependencies)
}
}
}
val deadline = SystemClock.elapsedRealtime() + 10_000L
while (!fixture.finished && SystemClock.elapsedRealtime() < deadline) SystemClock.sleep(20L)
assertTrue("Playback should complete", fixture.finished)
val index = fixture.consumed.indexOfFirst { frame -> frame.tokens.any { it.text == "home" } }
assertTrue(index > 0)
val hold = requireNotNull(fixture.consumed[index].punctuationHoldMs)
val displayedMs = fixture.consumedAtMs[index] - fixture.consumedAtMs[index - 1]
assertTrue("The test must exercise a full second of punctuation hold", hold >= 1000L)
assertTrue("Live tempo must preserve the hold: $displayedMs ms vs $hold ms", displayedMs >= hold)
}
}

private fun word(text: String) = Token(text, TokenType.WORD)
}
Loading