book-reader/app/src/androidTest/java/com/aryan/reader/paginatedreader/CssParserTest.kt
Aryan 83dcafa4b6
Windows ga (#358)
* Enhance Cloud TTS with navigation controls and shared UI overlay in desktop app

* Refactor Cloud TTS voice settings and remove standalone settings overlay on desktop app

* Persist reader window state and improve slider interaction in desktop app

* Improve PDF page transitions and refine focus management in desktop app

* Refactor scrollbar interaction and adjust desktop modal focus handling

* Improve PDF sidecar synchronization and cross-platform metadata compatibility

* Refactor PDF annotation comment logic to shared module and implement Desktop UI

* Refactor reader screen to use tap-to-toggle and full-width styling in desktop app

* Refactor reader workspace layout and chrome-panel interactions

* Implement global search keyboard shortcuts and focusable chrome layers

* Implement flavor-specific legal links and update the About UI

* Refactor reader UI controls on desktop app

* Enhance desktop folder sync with background metadata extraction and improved error handling

* Refactor Library UI and remove redundant Home tab in desktop app

* Add custom tooltips to reader icon buttons in desktop app

* Integrate app theme controls into reader interfaces on desktop

* Add right-to-left pagination support and improve focus restoration on desktop app

* Improve EPUB pagination geometry and diagnostic logging for layout cutoffs on desktop app

* Update desktop reader defaults and implement settings migration

* Implement block-based position tracking in ReaderLocator

* Enhance EPUB highlighting reliability in desktop app

* Add support for custom reader themes and update highlight palette logic in desktop app

* Replace the Tools panel with a "More" dropdown menu and refactor account UI

* Implement account profile header in desktop sidebar

* Implement cloud sync reliability improvements and sidebar toggle on desktop app

* Improve EPUB annotation synchronization and highlight mapping accuracy in desktop app

* Integrate WebView2 for EPUB vertical rendering on Windows

* Refactor reader layout logic and enhance WebView2 diagnostics

* Improve vertical reading layout and WebView2 resizing on Desktop

* Refine vertical reading mode layout and margin handling

* Enhance reader locator precision and Desktop mode-switching reliability

* Implement chapter-level caching and warm-start pagination in desktop app

* Replace bundled KCEF with native system webviews via SWT

* Refactor EPUB page info bar visibility and layout logic

* Improve PDF toolbar persistence and fix tab reactivation logic

* Enable multi-selection and bulk operations for custom fonts

* Refactor instrumentation tests

* Add EPUB UI test fixture and initial instrumentation tests

* Expand EpubReader UI tests and improve accessibility

* Add instrumentation tests and test tags for library and reader screens

* Enhance OPDS parser logic and catalog integration

* Add support for toggling local synchronization on a per-folder basis.

* Implement tri-state sizing for the TTS overlay

* Persist TTS overlay size across sessions

* Refactor reader brightness control and add incremental step buttons

* Improve CSS support, pagination control, and style-aware semantic caching

* Improve link handling, interaction, and diagnostics in the paginated reader

* crash fixes

* Implement persistent pending removal for external files

* Implement book-specific word replacements

* Add native vertical reading mode with custom renderer

* Implement text selection and navigation improvements for the native vertical reader

* Implement locator-based navigation and improved vertical scrolling in native vertical mode in epub

* Implement lazy loading and chapter prefetching for native vertical reader

* Improve window lifecycle and disposal handling on Desktop

* Optimize vertical reading performance in desktop app

* Enhance TTS start accuracy and diagnostic logging on desktop

* Refactor AI settings visibility on desktop

* Improve pagination height measurement and enhance cutoff diagnostics

* Implement lifecycle management and improve justified text splitting for pagination

* Refine AI usage tracking and force AI feature visibility on Desktop

* Add descriptive context comments and usage examples to string and plural resources.

* Optimize performance and memory usage in search and state mapping

* Replace reader page sliders with minimal slider and navigation controls

* Add support for CBT comic archives

* Harden file path validation and XML parsing to prevent security vulnerabilities

* Implement local account profile caching and optimize desktop performance

* Improve desktop persistence reliability and add Linux secure storage support

* Improved PDF zoom stability and layout prediction during zoom commits

* Improved PDF spread layout prediction, reader focus restoration, and account profile caching

* Enhance highlight precision and scoping using block-local offsets and CFIs

* Enhance cloud book content synchronization and background downloads

* Implement granular timestamp tracking for reading positions and PDF annotations

* Restrict diagnostic logging and stack traces to debug builds

* Refine PDF page gaps and reader chrome interaction logic

* Refactor PDF highlight rendering and overhaul Desktop sidebar UI

* Implement a new interaction dock and undo/redo history for PDF annotations in desktop

* Enhance PDF color picker and improve navigation scroll restoration

* Add highlight palette customization and improve selection menu UI in desktop app epub reader

* Enhance desktop shelf management and library organization
2026-06-02 00:51:42 +05:30

415 lines
17 KiB
Kotlin

// CssParserTest.kt
package com.aryan.reader.paginatedreader
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import com.google.common.truth.Truth.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CssParserTest {
private val dummyConstraints = androidx.compose.ui.unit.Constraints()
private val baseFontSize = 16f
private val density = 1f
private fun parseTextColor(value: String): Color? {
val result = CssParser.parse(
cssContent = "p { color: $value; }",
cssPath = null,
baseFontSizeSp = baseFontSize,
density = density,
constraints = dummyConstraints,
isDarkTheme = false
)
return result.rules.byTag["p"]
?.firstOrNull()
?.style
?.spanStyle
?.color
?.takeIf { it.isSpecified }
}
@Test
fun parseColor_handlesNamedColorsCorrectly() {
assertThat(parseTextColor("red")).isEqualTo(Color.Red)
assertThat(parseTextColor("black")).isEqualTo(Color.Black)
assertThat(parseTextColor("transparent")).isEqualTo(Color.Transparent)
}
@Test
fun parseColor_handles3DigitHexCodes() {
assertThat(parseTextColor("#F0C")).isEqualTo(Color(0xFFFF00CC))
}
@Test
fun parseColor_handles6DigitHexCodes() {
assertThat(parseTextColor("#FF00CC")).isEqualTo(Color(0xFFFF00CC))
}
@Test
fun parseColor_handles8DigitHexCodes() {
assertThat(parseTextColor("#80FF00CC")).isEqualTo(Color(128, 255, 0, 204))
}
@Test
fun parseColor_handlesRgbFunction() {
assertThat(parseTextColor("rgb(255, 0, 204)")).isEqualTo(Color(255, 0, 204))
}
@Test
fun parseColor_handlesRgbaFunction() {
assertThat(parseTextColor("rgba(255, 0, 204, 0.5)")).isEqualTo(Color(255, 0, 204, 128))
}
@Test
fun parseColor_returnsNullForInvalidInput() {
assertThat(parseTextColor("not a color")).isNull()
assertThat(parseTextColor("#12345")).isNull()
assertThat(parseTextColor("rgb(1,2)")).isNull()
}
@Test
fun parse_handlesSimpleRule() {
val css = "p { color: red; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val rules = result.rules.byTag["p"]
assertThat(rules).hasSize(1)
assertThat(rules?.first()?.style?.spanStyle?.color).isEqualTo(Color.Red)
}
@Test
fun parse_handlesMultipleSelectors() {
val css = "h1, h2, h3 { font-weight: bold; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.rules.byTag["h1"]).hasSize(1)
assertThat(result.rules.byTag["h2"]).hasSize(1)
assertThat(result.rules.byTag["h3"]).hasSize(1)
assertThat(result.rules.byTag["h1"]?.first()?.style?.spanStyle?.fontWeight).isEqualTo(FontWeight.Bold)
assertThat(result.rules.byTag["h2"]?.first()?.style?.spanStyle?.fontWeight).isEqualTo(FontWeight.Bold)
assertThat(result.rules.byTag["h3"]?.first()?.style?.spanStyle?.fontWeight).isEqualTo(FontWeight.Bold)
}
@Test
fun parse_handlesImportantRules() {
val css = "p { color: red !important; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val importantRule = result.rules.byTag["p"]?.find { it.selector.specificity >= 10000 }
assertThat(importantRule).isNotNull()
assertThat(importantRule!!.style.spanStyle.color).isEqualTo(Color.Red)
val normalRule = result.rules.byTag["p"]?.find { it.selector.specificity < 10000 }
assertThat(normalRule).isNull()
}
@Test
fun parse_createsBothNormalAndImportantRulesWhenMixed() {
val css = "p { color: blue; background-color: white !important; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val rules = result.rules.byTag["p"]
assertThat(rules).hasSize(2)
val importantRule = rules?.find { it.selector.specificity >= 10000 }
assertThat(importantRule).isNotNull()
assertThat(importantRule!!.style.blockStyle.backgroundColor).isEqualTo(Color.White)
assertThat(importantRule.style.spanStyle.color.isSpecified).isFalse()
val normalRule = rules.find { it.selector.specificity < 10000 }
assertThat(normalRule).isNotNull()
assertThat(normalRule!!.style.spanStyle.color).isEqualTo(Color.Blue)
assertThat(normalRule.style.blockStyle.backgroundColor.isSpecified).isFalse()
}
@Test
fun parse_extractsFontFaceRulesAndResolvesPath() {
val css = """
@font-face {
font-family: "MyCustomFont";
src: url("../fonts/myfont.ttf");
font-weight: bold;
}
p { color: black; }
""".trimIndent()
val result = CssParser.parse(css, "OEBPS/styles/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.rules.byTag).containsKey("p")
assertThat(result.fontFaces).hasSize(1)
val fontFace = result.fontFaces.first()
assertThat(fontFace.fontFamily).isEqualTo("mycustomfont")
assertThat(fontFace.src).isEqualTo("OEBPS/fonts/myfont.ttf")
assertThat(fontFace.fontWeight).isEqualTo(FontWeight.Bold)
assertThat(fontFace.fontStyle).isEqualTo(FontStyle.Normal)
}
@Test
fun parse_handlesFontFaceWithDataUri() {
val dataUri = "data:font/truetype;base64,AAEAAA..."
val css = """
@font-face {
font-family: 'MyDataFont';
src: url('$dataUri');
}
""".trimIndent()
val result = CssParser.parse(css, "/css/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.fontFaces).hasSize(1)
assertThat(result.fontFaces.first().src).isEqualTo(dataUri)
}
@Test
fun parse_sanitizesPseudoClassesFromSelectors() {
val css = "a:hover, p::first-line, button:focus { color: red; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.rules.byTag.keys).containsExactly("a", "p", "button")
}
@Test
fun parse_calculatesSpecificityCorrectly() {
val css = """
#myId { color: red; } /* 100 */
p.myClass { color: green; } /* 11 */
p { color: blue; } /* 1 */
div p { color: yellow; } /* 2 */
""".trimIndent()
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val idRule = result.rules.byId["myId"]?.first()
val classRule = result.rules.otherComplex.find { it.selector.selector == "p.myClass" }
val elementRule = result.rules.byTag["p"]?.first()
val descendantRule = result.rules.otherComplex.find { it.selector.selector == "div p" }
assertThat(idRule?.selector?.specificity).isEqualTo(100)
assertThat(classRule?.selector?.specificity).isEqualTo(11)
assertThat(elementRule?.selector?.specificity).isEqualTo(1)
assertThat(descendantRule?.selector?.specificity).isEqualTo(2)
}
@Test
fun parse_ignoresComments() {
val css = """
/* This is a comment */
p {
color: /* another comment */ blue; /* block comment */
}
""".trimIndent()
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val rules = result.rules.byTag["p"]
assertThat(rules).hasSize(1)
assertThat(rules?.first()?.style?.spanStyle?.color).isEqualTo(Color.Blue)
}
@Test
fun parse_handlesBorderShorthand() {
val css = "div { border: 2px solid red; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["div"]?.first()?.style?.blockStyle
val expectedBorder = BorderStyle(width = 2.dp, color = Color.Red, style = "solid")
assertThat(style?.borderTop).isEqualTo(expectedBorder)
assertThat(style?.borderRight).isEqualTo(expectedBorder)
assertThat(style?.borderBottom).isEqualTo(expectedBorder)
assertThat(style?.borderLeft).isEqualTo(expectedBorder)
}
@Test
fun parse_handlesMarginAndPaddingShorthand() {
val css = "p { margin: 10px 20px; padding: 1em 2em 3em 4em; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["p"]?.first()?.style?.blockStyle
assertThat(style?.margin?.top).isEqualTo(10.dp)
assertThat(style?.margin?.right).isEqualTo(20.dp)
assertThat(style?.margin?.bottom).isEqualTo(10.dp)
assertThat(style?.margin?.left).isEqualTo(20.dp)
assertThat(style?.padding?.top).isEqualTo(16.dp) // 1em
assertThat(style?.padding?.right).isEqualTo(32.dp) // 2em
assertThat(style?.padding?.bottom).isEqualTo(48.dp) // 3em
assertThat(style?.padding?.left).isEqualTo(64.dp) // 4em
}
@Test
fun parse_handlesFontSizeWithEmUnits() {
val css = "p { font-size: 1.2em; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["p"]?.first()?.style
assertThat(style?.fontSize?.isEm).isTrue()
assertThat(style?.fontSize?.value).isEqualTo(1.2f)
}
@Test
fun parse_optimizationCategorizesRulesCorrectly() {
val css = """
p { color: blue; }
.myClass { color: green; }
#myId { color: red; }
div > p { color: yellow; }
""".trimIndent()
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.rules.byTag).containsKey("p")
assertThat(result.rules.byClass).containsKey("myClass")
assertThat(result.rules.byId).containsKey("myId")
assertThat(result.rules.otherComplex).hasSize(1)
assertThat(result.rules.otherComplex.first().selector.selector).isEqualTo("div > p")
}
@Test
fun parse_mediaQueryAppliesDarkThemeRules() {
val css = """
p { color: black; }
@media (prefers-color-scheme: dark) {
p { color: white; }
}
""".trimIndent()
val lightResult = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(lightResult.rules.byTag["p"]?.first()?.style?.spanStyle?.color).isEqualTo(Color.Black)
val darkResult = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = true)
assertThat(darkResult.rules.byTag["p"]?.last()?.style?.spanStyle?.color).isEqualTo(Color.White)
}
@Test
fun parse_fontFaceSelectsPreferredSourceFormat() {
val css = """
@font-face {
font-family: "MyFont";
src: url("font.woff2") format("woff2"),
url("font.otf") format("opentype"),
url("font.ttf") format("truetype");
}
""".trimIndent()
val result = CssParser.parse(css, "OEBPS/css/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.fontFaces).hasSize(1)
assertThat(result.fontFaces.first().src).isEqualTo("OEBPS/css/font.otf")
}
@Test
fun parse_handlesNestedMediaAndCalcVariables() {
val css = """
:root { --gap: 12px; }
@media screen and (min-width: 300px) {
p { margin-left: calc(var(--gap) + 8px); color: hsl(120 100% 25%); }
}
""".trimIndent()
val result = CssParser.parse(
css,
null,
baseFontSize,
density,
androidx.compose.ui.unit.Constraints(maxWidth = 500),
isDarkTheme = false
)
val style = result.rules.byTag["p"]!!.first().style
assertThat(style.blockStyle.margin.left).isEqualTo(20.dp)
assertThat(style.spanStyle.color).isEqualTo(Color(0, 128, 0))
}
@Test
fun parse_preservesBeforeAfterPseudoElementRules() {
val css = "p::before { content: 'Note: '; color: red; } p { color: blue; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val pseudoRule = result.rules.otherComplex.single { it.pseudoElement == "before" }
assertThat(pseudoRule.selector.selector).isEqualTo("p")
assertThat(pseudoRule.style.content).isEqualTo("'Note: '")
assertThat(pseudoRule.style.spanStyle.color).isEqualTo(Color.Red)
assertThat(result.rules.byTag["p"]!!.single().style.spanStyle.color).isEqualTo(Color.Blue)
}
@Test
fun parse_handlesModernRgbSlashAlphaAndCssHexAlpha() {
assertThat(parseTextColor("rgb(255 0 204 / 50%)")).isEqualTo(Color(255, 0, 204, 128))
assertThat(parseTextColor("#ff00cc80")).isEqualTo(Color(255, 0, 204, 128))
}
@Test
fun parse_backgroundShorthandExtractsColorAndImage() {
val css = "section { background: #ffeecc url('../images/paper.png') repeat; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["section"]!!.first().style.blockStyle
assertThat(style.backgroundColor).isEqualTo(Color(255, 238, 204))
assertThat(style.backgroundImage).isEqualTo("../images/paper.png")
}
@Test
fun parse_listStyleShorthandExtractsMarkerTypeAndImage() {
val css = "ul { list-style: square url('../images/bullet.png') outside; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["ul"]!!.first().style.blockStyle
assertThat(style.listStyleType).isEqualTo("square")
assertThat(style.listStyleImage).isEqualTo("../images/bullet.png")
}
@Test
fun parse_propertiesHandlesVariousUnitsAndValues() {
val css = """
p {
font-size: 150%;
text-transform: uppercase;
text-decoration: underline;
text-align: center;
page-break-inside: avoid;
margin: 0 auto;
}
""".trimIndent()
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["p"]?.first()?.style
assertThat(style?.fontSize).isEqualTo(1.5.em)
assertThat(style?.textTransform).isEqualTo("uppercase")
assertThat(style?.spanStyle?.textDecoration).isEqualTo(TextDecoration.Underline)
assertThat(style?.paragraphStyle?.textAlign).isEqualTo(TextAlign.Center)
assertThat(style?.blockStyle?.pageBreakInsideAvoid).isTrue()
assertThat(style?.blockStyle?.horizontalAlign).isEqualTo("center")
}
@Test
fun parse_themeAdaptationAdaptsColorsCorrectlyForDarkTheme() {
val css = "p { color: #111; background-color: #EEE; }" // very dark text, very light bg
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = true)
val style = result.rules.byTag["p"]?.first()?.style
assertThat(style?.spanStyle?.color).isEqualTo(Color.White.copy(alpha = 0.87f))
assertThat(style?.blockStyle?.backgroundColor).isEqualTo(Color.Transparent)
}
@Test
fun parse_dataUriWithSemicolonParsesCorrectly() {
val dataUri = "data:font/opentype;base64,d09GMgABAAAAAAPs...;something=else"
val css = """
@font-face {
font-family: 'MyDataFont';
src: url('$dataUri');
}
p { color: red; }
""".trimIndent()
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.fontFaces).hasSize(1)
assertThat(result.fontFaces.first().src).isEqualTo(dataUri)
assertThat(result.rules.byTag).containsKey("p")
}
@Test
fun parse_textEmphasisParsesCorrectly() {
val css = "p { -epub-text-emphasis-style: filled dot; -epub-text-emphasis-color: red; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val emphasis = result.rules.byTag["p"]?.first()?.style?.textEmphasis
assertThat(emphasis).isNotNull()
assertThat(emphasis?.style).isEqualTo("dot")
assertThat(emphasis?.fill).isEqualTo("filled")
assertThat(emphasis?.color).isEqualTo(Color.Red)
}
@Test
fun parse_lineHeightPreservesUnitlessMultiplier() {
val css = "p { line-height: 1.1; }" // This is treated as 1.1em
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["p"]?.first()?.style
assertThat(style?.paragraphStyle?.lineHeight).isEqualTo(1.1.em)
}
}