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 "<blockquote>quoted</blockquote>" to "quoted",35 "<ul><li>one</li></ul>" to "• one",36 "<ol><li>one</li></ol>" to "1. one",37 "<p>para</p>" to "para",38 "<div>div text</div>" to "div text",39 "a<br>b" to "a\nb",40 )41 for ((html, expected) in cases) {42 assertEquals(html, expected, notePlainText(html))43 assertFalse(html, notePlainText(html).contains("<"))44 }45 46 assertFalse(notePlainText("a<hr>b").contains("hr"))47 }4849 @Test fun inline_tags_produce_the_matching_span_styles() {50 assertTrue(noteRendered("<b>x</b>").spanStyles.any { it.item.fontWeight == FontWeight.Bold })51 assertTrue(noteRendered("<strong>x</strong>").spanStyles.any { it.item.fontWeight == FontWeight.Bold })52 assertTrue(noteRendered("<i>x</i>").spanStyles.any { it.item.fontStyle == FontStyle.Italic })53 assertTrue(noteRendered("<em>x</em>").spanStyles.any { it.item.fontStyle == FontStyle.Italic })54 assertTrue(noteRendered("<u>x</u>").spanStyles.any { it.item.textDecoration == TextDecoration.Underline })55 assertTrue(noteRendered("<s>x</s>").spanStyles.any { it.item.textDecoration == TextDecoration.LineThrough })56 57 assertTrue(noteRendered("<h2>x</h2>").spanStyles.any { it.item.fontSize.value > 16f })58 }5960 @Test fun nested_inline_tags_keep_both_styles() {61 val styles = noteRendered("<b>a<i>b</i></b>").spanStyles62 assertTrue(styles.any { it.item.fontWeight == FontWeight.Bold && it.item.fontStyle == FontStyle.Italic })63 }6465 @Test fun blockquote_round_trips_and_renders_italic() {66 val html = "<blockquote>quoted text</blockquote>"67 assertEquals("> quoted text", noteEditorText(html))68 assertEquals(html, noteStorageText(noteEditorText(html), isHtml = true))69 assertTrue(noteRendered(html).spanStyles.any { it.item.fontStyle == FontStyle.Italic })70 }7172 @Test fun entities_decode_and_re_encode() {73 assertEquals("a & b <tag>", notePlainText("<p>a & b <tag></p>"))74 75 val html = "<div>a & b</div>"76 assertEquals("a & b", noteStorageText(noteEditorText(html), isHtml = true))77 }7879 8081 @Test fun plain_text_note_is_passed_through_unchanged() {82 val plain = "buy milk\n- eggs\n1. bread\n**not bold**"83 assertFalse(isNoteHtml(plain))84 assertEquals(plain, noteEditorText(plain))85 assertEquals(plain, notePlainText(plain))86 assertEquals(plain, noteStorageText(plain, isHtml = false))87 assertTrue("plain text carries no styling", noteRendered(plain).spanStyles.isEmpty())88 }8990 @Test fun plain_text_note_survives_an_edit_without_becoming_html() {91 val stored = "line one\nline two"92 val edited = noteEditorText(stored) + "\nline three"93 assertEquals("line one\nline two\nline three", noteStorageText(edited, isNoteHtml(stored)))94 }9596 9798 @Test fun formatted_note_round_trips_through_the_editor() {99 val html = "<h2>Trip</h2><b>packing</b><br>list" +100 "<ul><li>socks</li><li>tent</li></ul>" +101 "<ol><li>book</li><li>map</li></ol>" +102 "<hr><i>later</i>"103104 val editorText = noteEditorText(html)105 assertEquals(106 "## Trip\n**packing**\nlist\n- socks\n- tent\n1. book\n2. map\n---\n*later*",107 editorText,108 )109 assertEquals(html, noteStorageText(editorText, isHtml = true))110 }111112 @Test fun round_trip_is_stable_over_repeated_edits() {113 val html = "<h2>Notes</h2><u>read</u> and <s>done</s><br><div>tail</div>"114 val once = noteStorageText(noteEditorText(html), isHtml = true)115 val twice = noteStorageText(noteEditorText(once), isHtml = true)116 assertEquals(once, twice)117 118 assertTrue(once.contains("<h2>"))119 assertTrue(once.contains("<u>read</u>"))120 assertTrue(once.contains("<s>done</s>"))121 }122123 @Test fun appending_text_to_a_formatted_note_keeps_the_formatting() {124 val html = "<h2>Trip</h2><ul><li>socks</li></ul>"125 val edited = noteEditorText(html) + "\n- tent"126 val saved = noteStorageText(edited, isHtml = true)127 assertEquals("<h2>Trip</h2><ul><li>socks</li><li>tent</li></ul>", saved)128 }129130 131132 @Test fun toc_search_does_not_match_markup() {133 val list = listOf(note("Trip", "<div><b>hotel</b> ideas</div>"))134 assertTrue("markup must not be searchable", filterNotesForToc(list, "div").isEmpty())135 assertTrue(filterNotesForToc(list, "strong").isEmpty())136 assertEquals(listOf("Trip"), filterNotesForToc(list, "hotel").map { it.title })137 }138139 @Test fun vault_search_text_for_a_note_carries_no_markup() {140 val text = vaultEntrySearchText(note("Trip", "<ul><li>socks</li></ul>"))141 assertFalse(text.contains("<"))142 assertTrue(text.contains("socks"))143 }144145 @Test fun preview_line_is_the_first_rendered_line() {146 assertEquals("Trip", notePreviewLine("<h2>Trip</h2><div>details</div>"))147 assertEquals("details", notePreviewLine("<div></div><div>details</div>"))148 assertEquals("plain first line", notePreviewLine("plain first line\nsecond"))149 assertEquals("", notePreviewLine(""))150 assertFalse(notePreviewLine("<div><b>hotel</b> ideas</div>").contains("<"))151 }152153 154155 @Test fun toolbar_wrap_marks_the_selection_and_serialises_to_a_tag() {156 val value = androidx.compose.ui.text.input.TextFieldValue(157 "hello world",158 selection = androidx.compose.ui.text.TextRange(0, 5),159 )160 val bolded = wrapSelection(value, "**")161 assertEquals("**hello** world", bolded.text)162 assertEquals("<b>hello</b> world", noteStorageText(bolded.text, isHtml = true))163 }164}