1package com.peachpasswords.ui.vault23import androidx.compose.ui.text.font.FontStyle4import androidx.compose.ui.text.font.FontWeight5import androidx.compose.ui.text.style.TextDecoration6import com.peachpasswords.vault.VaultEntry7import org.junit.Assert.assertEquals8import org.junit.Assert.assertFalse9import org.junit.Assert.assertTrue10import org.junit.Test11121314151617class NoteHtmlTest {1819 private fun note(title: String, body: String?) =20 VaultEntry(id = title, title = title, type = "note", notes = body, createdAt = 0, modifiedAt = 0)2122 2324 @Test fun every_allowed_tag_renders_without_markup() {25 val cases = listOf(26 "<b>bold</b>" to "bold",27 "<strong>bold</strong>" to "bold",28 "<i>it</i>" to "it",29 "<em>it</em>" to "it",30 "<u>under</u>" to "under",31 "<s>struck</s>" to "struck",32 "<h2>Title</h2>" to "Title",33 "<h3>Sub</h3>" to "Sub",34 "<ul><li>one</li></ul>" to "• one",35 "<ol><li>one</li></ol>" to "1. one",36 "<p>para</p>" to "para",37 "<div>div text</div>" to "div text",38 "a<br>b" to "a\nb",39 )40 for ((html, expected) in cases) {41 assertEquals(html, expected, notePlainText(html))42 assertFalse(html, notePlainText(html).contains("<"))43 }44 45 assertFalse(notePlainText("a<hr>b").contains("hr"))46 }4748 @Test fun inline_tags_produce_the_matching_span_styles() {49 assertTrue(noteRendered("<b>x</b>").spanStyles.any { it.item.fontWeight == FontWeight.Bold })50 assertTrue(noteRendered("<strong>x</strong>").spanStyles.any { it.item.fontWeight == FontWeight.Bold })51 assertTrue(noteRendered("<i>x</i>").spanStyles.any { it.item.fontStyle == FontStyle.Italic })52 assertTrue(noteRendered("<em>x</em>").spanStyles.any { it.item.fontStyle == FontStyle.Italic })53 assertTrue(noteRendered("<u>x</u>").spanStyles.any { it.item.textDecoration == TextDecoration.Underline })54 assertTrue(noteRendered("<s>x</s>").spanStyles.any { it.item.textDecoration == TextDecoration.LineThrough })55 56 assertTrue(noteRendered("<h2>x</h2>").spanStyles.any { it.item.fontSize.value > 16f })57 }5859 @Test fun nested_inline_tags_keep_both_styles() {60 val styles = noteRendered("<b>a<i>b</i></b>").spanStyles61 assertTrue(styles.any { it.item.fontWeight == FontWeight.Bold && it.item.fontStyle == FontStyle.Italic })62 }6364 @Test fun entities_decode_and_re_encode() {65 assertEquals("a & b <tag>", notePlainText("<p>a & b <tag></p>"))66 67 val html = "<div>a & b</div>"68 assertEquals("a & b", noteStorageText(noteEditorText(html), isHtml = true))69 }7071 7273 @Test fun plain_text_note_is_passed_through_unchanged() {74 val plain = "buy milk\n- eggs\n1. bread\n**not bold**"75 assertFalse(isNoteHtml(plain))76 assertEquals(plain, noteEditorText(plain))77 assertEquals(plain, notePlainText(plain))78 assertEquals(plain, noteStorageText(plain, isHtml = false))79 assertTrue("plain text carries no styling", noteRendered(plain).spanStyles.isEmpty())80 }8182 @Test fun plain_text_note_survives_an_edit_without_becoming_html() {83 val stored = "line one\nline two"84 val edited = noteEditorText(stored) + "\nline three"85 assertEquals("line one\nline two\nline three", noteStorageText(edited, isNoteHtml(stored)))86 }8788 8990 @Test fun formatted_note_round_trips_through_the_editor() {91 val html = "<h2>Trip</h2><b>packing</b><br>list" +92 "<ul><li>socks</li><li>tent</li></ul>" +93 "<ol><li>book</li><li>map</li></ol>" +94 "<hr><i>later</i>"9596 val editorText = noteEditorText(html)97 assertEquals(98 "## Trip\n**packing**\nlist\n- socks\n- tent\n1. book\n2. map\n---\n*later*",99 editorText,100 )101 assertEquals(html, noteStorageText(editorText, isHtml = true))102 }103104 @Test fun round_trip_is_stable_over_repeated_edits() {105 val html = "<h2>Notes</h2><u>read</u> and <s>done</s><br><div>tail</div>"106 val once = noteStorageText(noteEditorText(html), isHtml = true)107 val twice = noteStorageText(noteEditorText(once), isHtml = true)108 assertEquals(once, twice)109 110 assertTrue(once.contains("<h2>"))111 assertTrue(once.contains("<u>read</u>"))112 assertTrue(once.contains("<s>done</s>"))113 }114115 @Test fun appending_text_to_a_formatted_note_keeps_the_formatting() {116 val html = "<h2>Trip</h2><ul><li>socks</li></ul>"117 val edited = noteEditorText(html) + "\n- tent"118 val saved = noteStorageText(edited, isHtml = true)119 assertEquals("<h2>Trip</h2><ul><li>socks</li><li>tent</li></ul>", saved)120 }121122 123124 @Test fun toc_search_does_not_match_markup() {125 val list = listOf(note("Trip", "<div><b>hotel</b> ideas</div>"))126 assertTrue("markup must not be searchable", filterNotesForToc(list, "div").isEmpty())127 assertTrue(filterNotesForToc(list, "strong").isEmpty())128 assertEquals(listOf("Trip"), filterNotesForToc(list, "hotel").map { it.title })129 }130131 @Test fun vault_search_text_for_a_note_carries_no_markup() {132 val text = vaultEntrySearchText(note("Trip", "<ul><li>socks</li></ul>"))133 assertFalse(text.contains("<"))134 assertTrue(text.contains("socks"))135 }136137 @Test fun preview_line_is_the_first_rendered_line() {138 assertEquals("Trip", notePreviewLine("<h2>Trip</h2><div>details</div>"))139 assertEquals("details", notePreviewLine("<div></div><div>details</div>"))140 assertEquals("plain first line", notePreviewLine("plain first line\nsecond"))141 assertEquals("", notePreviewLine(""))142 assertFalse(notePreviewLine("<div><b>hotel</b> ideas</div>").contains("<"))143 }144145 146147 @Test fun toolbar_wrap_marks_the_selection_and_serialises_to_a_tag() {148 val value = androidx.compose.ui.text.input.TextFieldValue(149 "hello world",150 selection = androidx.compose.ui.text.TextRange(0, 5),151 )152 val bolded = wrapSelection(value, "**")153 assertEquals("**hello** world", bolded.text)154 assertEquals("<b>hello</b> world", noteStorageText(bolded.text, isHtml = true))155 }156}