* Implement build profiles and feature policy for offline desktop builds * Introduce unified cross-platform Settings Hub * Refactor main settings into a hierarchical page-based navigation model * Refactor library projection to use shared multiplatform logic * Refactor UI state consumption by removing intermediate screen models * Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic * Refactor state management for tabs, selection, and pinning to use shared bridge logic * Refactor file type management and validation into a centralized shared module * Centralize file type resolution and improve handling of unknown types * Centralize book import logic with SharedImportPlanner * Refactor magnifier geometry logic and coordinate mapping * Properly handle orientation changes in scroll-locked PDF reader * Add screen orientation controls to EPUB and PDF readers * Implement right-to-left (RTL) pagination support and refactor reader menus * Separate right-to-left pagination settings for PDF and EPUB * Ensure PDF page data is scoped by document key for multi tab support * Implement theme-aware link styling for the epub reader * Implement jump history for back and forward navigation in the epub reader * Improve locator handling and navigation logic in paginated reader mode * Implement stable pagination navigation and location tracking * Centralize banner message management and auto-dismiss logic in MainViewModel * Implement zoom and pan state preservation for PDF pan lock mode * Enhance reader navigation UI and workspace layout management in desktop app * Refactor reader navigation sidebar and relocate search controls in desktop app * Enhance reader UI with redesigned selection menus and bottom sheet overlays * Implement custom highlight palettes and reader theme customization in desktop app * Implement cross-platform modal layer and refine reader UI styling * Improve highlight accuracy and implement metadata enrichment on book open in desktop app * Implement two-page spread layout for paginated reader on desktop * Implement persistent caching for book loading and pagination in desktop app * Implement persistent caching for book loading and pagination in desktop app * Optimize reader settings updates by separating layout and appearance changes in desktop app * Improve desktop window branding and native Windows styling * Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app * Refine selection handle positioning and interaction logic * Implement EPUB selection debug logging and improve handle targeting * Optimize desktop book loading performance and UI responsiveness * Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer. * Implement smooth zoom preview for the PDF reader in desktop app * Optimize PDF rendering performance and responsiveness in the desktop reader * Implement conditional diagnostic logging and update desktop build configuration * Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling * Added management options for annotations and highlights in the sidebar in desktop app * Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability. * Refined library filters and enhanced OPDS functionality in desktop app * Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app * Added PPTX support including document parsing, rendering, and indexing * Improved PPTX rendering and layout accuracy * Implemented text autofit support for PPTX rendering * Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity * Improved EPUB pagination accuracy and added layout telemetry in desktop app * Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app * Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling * Implemented billing account obfuscation * Implemented hierarchical folder navigation and improved library selection functionality in desktop app * Implemented platform-aware directory resolution and multi-platform native library support for desktop * Added full-screen mode for the reader workspace * Added PDF zoom indicator and interactive vertical scrollbar with page tooltips * Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management * Updated PDF indexing to replace existing page text and removed search result item keys * Implemented "preparing" foreground notification for TTS service * Optimized PDF rendering performance by pre-calculating page-specific annotations * Refactored desktop packaging tasks and improved distribution configuration * Optimized EPUB parser memory usage and added path traversal protection * Refactored WorkManager monitoring logic and added work pruning * Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks * Implemented bitmap size limits and scaling to prevent canvas rendering errors * Split long text paragraphs into multiple semantic blocks during HTML parsing * Implemented local ActionMode for text selection to prevent platform crashes * Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic * Added desktop startup splash screen and deferred WebView initialization * Reorganized settings hub and added separate PDF reader defaults * Implemented embedded cover extraction and metadata support for MOBI and FB2 formats * Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance. * Implemented procedurally generated book covers and replaced static placeholders * Redesigned search UI with a top bar and results overlay in desktop app * Added PDF page gap and overlay visibility options and implemented DesktopBookImporter * Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop * Implemented PDF viewport persistence for zoom and scroll positions in desktop app * Improved desktop fullscreen implementation and state restoration * Implemented desktop window state persistence * Implemented flavor-based branding and ProGuard configuration for desktop builds * Implemented precise reader positioning and improved highlight rendering logic in desktop app * Added support for user-editable book metadata * Enhanced book metadata support and integrated info/edit dialogs * Implemented embedded EPUB metadata editing * Improved highlight mapping and added custom scrollbar styling for the reader. * Reduced desktop WebView bundle size by excluding unused locales and runtime files * Added neutral pan mode as the default PDF interaction state. * Refactored library empty states and updated primary navigation tabs in desktop app * Implemented native paginated reader and unified content rendering architecture in desktop epub reader * Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing. * Improved pagination overflow detection in desktop * Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
201 lines
7.9 KiB
Kotlin
201 lines
7.9 KiB
Kotlin
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<String>()
|
|
val bridge = TtsJsBridge(scope = this, ttsStructuredTextHandler = { received.complete(it) })
|
|
|
|
bridge.onStructuredTextExtracted("[{\"text\":\"Hello\"}]")
|
|
|
|
assertEquals("[{\"text\":\"Hello\"}]", received.await())
|
|
|
|
val blankReceived = CompletableDeferred<String>()
|
|
TtsJsBridge(scope = this, ttsStructuredTextHandler = { blankReceived.complete(it) }).onStructuredTextExtracted(" ")
|
|
assertEquals("[]", blankReceived.await())
|
|
}
|
|
|
|
@Test
|
|
fun `highlight bridge forwards create and click events`() {
|
|
var created: Triple<String, String, String>? = null
|
|
var clicked: List<Any>? = 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<String>()
|
|
|
|
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("<p>Note</p>")
|
|
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("<p>Note</p>", 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<String>()
|
|
val bookmark = mutableListOf<String>()
|
|
val scrollResults = mutableListOf<Boolean>()
|
|
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<String>()
|
|
val bookmark = mutableListOf<String>()
|
|
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<WebView>(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<WebView>(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)
|
|
}
|
|
}
|