package com.aryan.reader.epubreader import android.webkit.WebView import com.aryan.reader.RenderMode import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.json.JSONArray import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class EpubReaderBridgeAndControlsTest { @Test fun `sanitizePlaceholders keeps one header per toolbar section and inserts empty placeholders`() { val input = listOf( FlatToolItem("old_header", FlatItemType.SECTION_HEADER, section = ToolbarSection.BOTTOM), FlatToolItem("format", FlatItemType.TOOL, tool = ReaderTool.FORMAT, section = ToolbarSection.BOTTOM), FlatToolItem("more_header", FlatItemType.MORE_HEADER, title = "More"), FlatToolItem("reading_mode", FlatItemType.MORE_TOOL, tool = ReaderTool.READING_MODE) ) val sanitized = sanitizePlaceholders(input) assertEquals( listOf( FlatItemType.SECTION_HEADER, FlatItemType.EMPTY_PLACEHOLDER, FlatItemType.SECTION_HEADER, FlatItemType.TOOL, FlatItemType.SECTION_HEADER, FlatItemType.EMPTY_PLACEHOLDER, FlatItemType.MORE_HEADER, FlatItemType.MORE_TOOL ), sanitized.map { it.type } ) assertEquals(listOf(ToolbarSection.TOP, ToolbarSection.BOTTOM, ToolbarSection.HIDDEN), sanitized.filter { it.type == FlatItemType.SECTION_HEADER }.map { it.section }) assertEquals(ReaderTool.FORMAT, sanitized.single { it.type == FlatItemType.TOOL }.tool) } @Test fun `auto scroll bridge invokes chapter end callback`() { var calls = 0 AutoScrollJsBridge { calls++ }.onChapterEnd() assertEquals(1, calls) } @Test fun `tts bridge relays nonblank structured text and normalizes blank payloads`() = runTest { val received = CompletableDeferred() val bridge = TtsJsBridge(scope = this, ttsStructuredTextHandler = { received.complete(it) }) bridge.onStructuredTextExtracted("[{\"text\":\"Hello\"}]") assertEquals("[{\"text\":\"Hello\"}]", received.await()) val blankReceived = CompletableDeferred() TtsJsBridge(scope = this, ttsStructuredTextHandler = { blankReceived.complete(it) }).onStructuredTextExtracted(" ") assertEquals("[]", blankReceived.await()) } @Test fun `highlight bridge forwards create and click events`() { var created: Triple? = null var clicked: List? = null val bridge = HighlightJsBridge( onCreateCallback = { cfi, text, color -> created = Triple(cfi, text, color) }, onClickCallback = { cfi, text, left, top, right, bottom -> clicked = listOf(cfi, text, left, top, right, bottom) } ) bridge.onHighlightCreated("/4", "Text", "yellow") bridge.onHighlightClicked("/4", "Text", 1, 2, 3, 4) assertEquals(Triple("/4", "Text", "yellow"), created) assertEquals(listOf("/4", "Text", 1, 2, 3, 4), clicked) } @Test fun `content snippet progress footnote and ai bridges forward callbacks`() = runTest { var requestedChunk = -1 var snippet = "" to "" var progressCalls = 0 var lastChunk = -1 var footnote = "" val aiContent = CompletableDeferred() ContentBridge { requestedChunk = it }.requestChunk(7) SnippetJsBridge { cfi, text -> snippet = cfi to text }.onSnippetExtracted("/6", "Snippet") val progress = ProgressJsBridge { progressCalls++ lastChunk = it } progress.updateTopChunk(2) progress.updateTopChunk(2) progress.updateTopChunk(3) FootnoteJsBridge { footnote = it }.onFootnoteRequested("

Note

") AiJsBridge(scope = this, onContentReady = { aiContent.complete(it) }).onContentExtractedForSummarization("Chapter text") assertEquals(7, requestedChunk) assertEquals("/6" to "Snippet", snippet) assertEquals(2, progressCalls) assertEquals(3, lastChunk) assertEquals("

Note

", footnote) assertEquals("Chapter text", aiContent.await()) } @Test fun `ai bridge ignores blank content`() = runTest { var called = false AiJsBridge(scope = this, onContentReady = { called = true }).onContentExtractedForSummarization(" ") assertFalse(called) } @Test fun `cfi bridge parses save bookmark and scroll callbacks with fallback for invalid save json`() { val saved = mutableListOf() val bookmark = mutableListOf() val scrollResults = mutableListOf() val bridge = CfiJsBridge( onCfiReady = { saved.add(it) }, onCfiForBookmarkReady = { bookmark.add(it) }, onScrollFinishedCallback = { scrollResults.add(it) } ) bridge.onCfiExtracted(JSONObject().put("cfi", "/4/2:8").put("log", JSONArray()).toString()) bridge.onCfiExtracted(JSONObject().put("cfi", "").toString()) bridge.onCfiExtracted("broken") bridge.onCfiForBookmarkExtracted(JSONObject().put("cfi", "/6/4:1").toString()) bridge.onCfiForBookmarkExtracted("broken") bridge.onScrollFinished(true) bridge.onScrollFinished(false) assertEquals(listOf("/4/2:8", "/4"), saved) assertEquals(listOf("/6/4:1"), bookmark) assertEquals(listOf(true, false), scrollResults) } @Test fun `cfi bridge preserves full reading position cfi payloads for save and bookmark callbacks`() { val saved = mutableListOf() val bookmark = mutableListOf() val bridge = CfiJsBridge( onCfiReady = { saved.add(it) }, onCfiForBookmarkReady = { bookmark.add(it) }, onScrollFinishedCallback = {} ) val cfi = "/6/4[chapter]!/4/2/8:137" bridge.onCfiExtracted(JSONObject().put("cfi", cfi).put("log", JSONArray().put("exact")).toString()) bridge.onCfiForBookmarkExtracted(JSONObject().put("cfi", cfi).put("log", JSONArray()).toString()) assertEquals(listOf(cfi), saved) assertEquals(listOf(cfi), bookmark) } @Test fun `updateAutoScrollJs emits start and stop commands`() { val webView = mockk(relaxed = true) updateAutoScrollJs(webView, playing = true, speed = 1.25f) updateAutoScrollJs(webView, playing = false, speed = 9f) verify { webView.evaluateJavascript("javascript:window.autoScroll.start(1.25);", null) } verify { webView.evaluateJavascript("javascript:window.autoScroll.stop();", null) } } @Test fun `initiateTtsPlayback chooses web extraction for vertical mode and callback for paginated mode`() { val webView = mockk(relaxed = true) var paginatedStarts = 0 initiateTtsPlayback(RenderMode.VERTICAL_SCROLL, webView) { paginatedStarts++ } initiateTtsPlayback(RenderMode.PAGINATED, webView) { paginatedStarts++ } verify { webView.evaluateJavascript("javascript:TtsBridgeHelper.extractAndRelayText();", null) } assertEquals(1, paginatedStarts) } @Test fun `reader tool metadata has stable unique names and categories`() { assertEquals(ReaderTool.entries.size, ReaderTool.entries.map { it.name }.toSet().size) assertTrue(ReaderTool.entries.any { it.category == "Top Bar" }) assertTrue(ReaderTool.entries.any { it.category == "Bottom Bar" }) assertTrue(ReaderTool.entries.any { it.category == "Overflow Menu" }) assertEquals("Top Bar", ReaderTool.SCREEN_ORIENTATION.category) } }