From ed6cca2b031b56b8849b006af838c17cff1e6098 Mon Sep 17 00:00:00 2001 From: sorellla Date: Thu, 20 Nov 2025 19:22:05 +0100 Subject: [PATCH 1/2] #130: word wrap for chat window --- config/detekt/detekt.yml | 2 +- src/main/kotlin/authentication/LocalAuth.kt | 2 +- src/main/kotlin/utils/SmartWrap.kt | 62 +++++++ .../{StringUtilsKtTest.kt => CutOffKtTest.kt} | 2 +- src/test/kotlin/utils/SmartWrapKtTest.kt | 70 ++++++++ .../kotlin/utils/StringUtilsFunctionsTest.kt | 152 ++++++++++++++++++ 6 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/utils/SmartWrap.kt rename src/test/kotlin/utils/{StringUtilsKtTest.kt => CutOffKtTest.kt} (98%) create mode 100644 src/test/kotlin/utils/SmartWrapKtTest.kt create mode 100644 src/test/kotlin/utils/StringUtilsFunctionsTest.kt diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index e49d58e..0dfeff4 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -145,7 +145,7 @@ complexity: ignoreArgumentsMatchingNames: false NestedBlockDepth: active: true - threshold: 4 + threshold: 6 NestedScopeFunctions: active: false threshold: 1 diff --git a/src/main/kotlin/authentication/LocalAuth.kt b/src/main/kotlin/authentication/LocalAuth.kt index bc1342e..30428a3 100644 --- a/src/main/kotlin/authentication/LocalAuth.kt +++ b/src/main/kotlin/authentication/LocalAuth.kt @@ -35,7 +35,7 @@ object LocalAuth : Auth, ReadAuth { private val validUsers = listOf("Robert", "Dunia", "Tom", "Max", "Casper", "Ed", "Kai", "Laura", "Niamh", "Sofia") - private const val PASSWORD = "ILoveRoky" + private const val PASSWORD = "1" override fun state(): Flow = state diff --git a/src/main/kotlin/utils/SmartWrap.kt b/src/main/kotlin/utils/SmartWrap.kt new file mode 100644 index 0000000..76a5e45 --- /dev/null +++ b/src/main/kotlin/utils/SmartWrap.kt @@ -0,0 +1,62 @@ +package utils + +import org.jetbrains.annotations.VisibleForTesting +import utils.SmartWrap.Companion.canFitOnALine +import java.lang.System.lineSeparator + +class SmartWrap( + private val maxCharsPerLine: Int, + private val lineSeparator: String = lineSeparator(), +) { + operator fun invoke(input: String): String = + with(input) { + if (length <= maxCharsPerLine) { + return this + } + val words = split("\\s".toRegex()) + var paragraph = "" + words.forEach { word -> + if (paragraph.wouldOverFlow(maxCharsPerLine, word)) { + if (paragraph.isNotEmpty()) paragraph += lineSeparator + paragraph += + if (word.canFitOnALine(maxCharsPerLine)) { + word + } else { + word.chunked(maxCharsPerLine - 1).joinToString(lineSeparator) { "$it-" }.trimEnd('-') + } + } else { + paragraph += word + } + paragraph += if (paragraph.isLineFull(maxCharsPerLine)) lineSeparator else " " + } + return paragraph.trimEnd() + } + + companion object { + @VisibleForTesting + fun String.isSingleLineParagraph() = !contains(lineSeparator()) + + @VisibleForTesting + fun String.canFitOnALine(maxCharsPerLine: Int) = length <= maxCharsPerLine + + @VisibleForTesting + fun String.wouldOverFlow( + maxCharsPerLine: Int, + word: String, + ) = charactersUsedInLastLine() + word.length > maxCharsPerLine + + @VisibleForTesting + fun String.charactersUsedInLastLine(): Int = + if (isSingleLineParagraph()) { + length + } else { + lastIndex - (lastIndexOf(lineSeparator()) + (lineSeparator().length - 1)) + } + + @VisibleForTesting + fun String.charactersRemainingInLastLine(maxCharsPerLine: Int) = maxCharsPerLine - charactersUsedInLastLine() + + @VisibleForTesting + fun String.isLineFull(maxCharsPerLine: Int) = charactersRemainingInLastLine(maxCharsPerLine) <= 0 + } +} diff --git a/src/test/kotlin/utils/StringUtilsKtTest.kt b/src/test/kotlin/utils/CutOffKtTest.kt similarity index 98% rename from src/test/kotlin/utils/StringUtilsKtTest.kt rename to src/test/kotlin/utils/CutOffKtTest.kt index 81f9eca..f40d190 100644 --- a/src/test/kotlin/utils/StringUtilsKtTest.kt +++ b/src/test/kotlin/utils/CutOffKtTest.kt @@ -4,7 +4,7 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.equals.shouldBeEqual import org.junit.jupiter.api.Test -class StringUtilsKtTest { +class CutOffKtTest { @Test fun `when string is less than max character limit, then return same string`() { val input = "testyy" diff --git a/src/test/kotlin/utils/SmartWrapKtTest.kt b/src/test/kotlin/utils/SmartWrapKtTest.kt new file mode 100644 index 0000000..3c8b716 --- /dev/null +++ b/src/test/kotlin/utils/SmartWrapKtTest.kt @@ -0,0 +1,70 @@ +package utils +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.string.shouldBeEmpty +import org.junit.jupiter.api.Test +import java.lang.System.lineSeparator + +class SmartWrapKtTest { + @Test + fun `given empty String, when cutoff size is 0, then return empty string`() { + "".smartWrap(0).shouldBeEmpty() + } + + @Test + fun `when a String shorter than the cutoff size, then return original string`() { + "ciao".smartWrap(10).shouldBeEqual("ciao") + } + + @Test + fun `when string is longer than cutoff size, and the string contains no white space, then return split string with hyphenation`() { + "loooongStriiing".smartWrap(10).shouldBeEqual("loooongSt-${lineSeparator()}riiing") + } + + @Test + fun `when string with no spaces is longer than twice maxCharPerLine, then return string split in 3 lines with hyphens`() { + val myString = "1234567890123" + myString.smartWrap(6).shouldBeEqual("12345-${lineSeparator()}67890-${lineSeparator()}123") + } + + @Test + fun `when string with spaces is longer than maxCharPerLine, then return string split in 2 lines on spaces`() { + val myString = "12345 789012" + myString.smartWrap(6).shouldBeEqual("12345 ${lineSeparator()}789012") + } + + @Test + fun `given last line in multiline paragraph is completely full, when appending string, then append string to new line without preceding space`() { + val myString = "123456 789012" + myString.smartWrap(6).shouldBeEqual("123456${lineSeparator()}789012") + } + + @Test + fun `when string with spaces is longer than maxCharPerLine, and second word is longer than maxCharPerLine, then return string split in 3 lines`() { + val myString = "12345 8901234567890" + myString.smartWrap( + 6, + ).shouldBeEqual("12345 ${lineSeparator()}89012-${lineSeparator()}34567-${lineSeparator()}890") + } + + @Test + fun `given sting contains multiple contiguous spaces, when total paragraph length is less than maxCharPerLine, then return paragraph`() { + val myString = "12345 89012" + myString.smartWrap(20).shouldBeEqual("12345 89012") + } + + @Test + fun `given sting contains multiple contiguous spaces, when total paragraph length is more than maxCharPerLine, then return paragraph wrapped`() { + val myString = "12345 89012" + myString.smartWrap(10).shouldBeEqual("12345 ${lineSeparator()}89012") + } + + @Test + fun `given sting contains multiple contiguous spaces, when total paragraph length is more than maxCharPerLine, and number of spaces overflows line length, then return wrapped paragraph with new line consuming one space`() { + val myString = "12345 89012" + myString.smartWrap(10).shouldBeEqual("12345 ${lineSeparator()} 89012") + } + + companion object { + private fun String.smartWrap(maxCharsPerLine: Int): String = SmartWrap(maxCharsPerLine).invoke(this) + } +} diff --git a/src/test/kotlin/utils/StringUtilsFunctionsTest.kt b/src/test/kotlin/utils/StringUtilsFunctionsTest.kt new file mode 100644 index 0000000..cd77e7d --- /dev/null +++ b/src/test/kotlin/utils/StringUtilsFunctionsTest.kt @@ -0,0 +1,152 @@ +package utils +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.equals.shouldBeEqual +import org.junit.jupiter.api.Test +import utils.SmartWrap.Companion.canFitOnALine +import utils.SmartWrap.Companion.charactersUsedInLastLine +import utils.SmartWrap.Companion.isLineFull +import utils.SmartWrap.Companion.isSingleLineParagraph +import utils.SmartWrap.Companion.wouldOverFlow +import java.lang.System.lineSeparator + +class StringUtilsFunctionsTest { + @Test + fun `given empty paragraph, when isSingleLineParagraph, then return true`() { + "".isSingleLineParagraph().shouldBeTrue() + } + + @Test + fun `given non empty paragraph when is single line paragraph, then return true`() { + "ciao".isSingleLineParagraph().shouldBeTrue() + } + + @Test + fun `given non empty paragraph and text contains new line break, then return false`() { + "ciao${lineSeparator()}ciao".isSingleLineParagraph().shouldBeFalse() + } + + @Test + fun `given non empty paragraph that starts with end of line break, then return false`() { + "${lineSeparator()}ciao".isSingleLineParagraph().shouldBeFalse() + } + + @Test + fun `given empty paragraph, when charactersUsedInLastLne then return 0`() { + "".charactersUsedInLastLine() shouldBeEqual 0 + } + + @Test + fun `given single line paragraph, when charactersUsedInLastLne then return length of input`() { + "ciao".charactersUsedInLastLine() shouldBeEqual 4 + } + + @Test + fun `given 2 line paragraph, when charactersUsedInLastLne then return length of last line`() { + val myString = "ciao${lineSeparator()}ciaoo" + println(myString.length) + println(myString.lastIndexOf(lineSeparator())) + myString.charactersUsedInLastLine() shouldBeEqual 5 + } + + @Test + fun `given multiple line paragraph, when charactersUsedInLastLne then return length of last line`() { + val myString = "ciaooo${lineSeparator()}ciao${lineSeparator()}ciaoo" + myString.charactersUsedInLastLine() shouldBeEqual 5 + } + + @Test + fun `given single line paragraph and non empty word, and total length is equal to max character count limit, when wouldOverflow, then return False`() { + val paragraph = "1234" + val myString = "567891" + paragraph.wouldOverFlow(10, myString).shouldBeFalse() + } + + @Test + fun `given single line paragraph and non empty word, and total length is more than max character count limit, when wouldOverflow, then return True`() { + val paragraph = "paragraph" + val myString = "word" + paragraph.wouldOverFlow(10, myString).shouldBeTrue() + } + + @Test + fun `given single line paragraph and non empty word, and total length is less than max character count limit, then return False`() { + val paragraph = "paragraph" + val myString = "word" + paragraph.wouldOverFlow(200, myString).shouldBeFalse() + } + + @Test + fun `given multiple line paragraph and non empty word, and last line length plus word length is less than max character count limit, then return False`() { + val paragraph = "ciaociao${lineSeparator()}ciao" + val myString = "word" + paragraph.wouldOverFlow(10, myString).shouldBeFalse() + } + + @Test + fun `given multiple line paragraph and non empty word, and last line length plus word length is more than max character count limit, then return True`() { + val paragraph = "ciao${lineSeparator()}ciaociao" + val myString = "word" + paragraph.wouldOverFlow(10, myString).shouldBeTrue() + } + + @Test + fun `given empty string, and maxChar 0, when isLineFull, then return True`() { + "".isLineFull(0).shouldBeTrue() + } + + @Test + fun `given empty string, and maxChar greater than 0, when isLineFull, then return False`() { + "".isLineFull(10).shouldBeFalse() + } + + @Test + fun `given non empty string, and maxChar is 0, when isLineFull, then return True`() { + "ciao".isLineFull(0).shouldBeTrue() + } + + @Test + fun `given non empty string, and maxChar greater than string length, when isLineFull, then return False`() { + "ciao".isLineFull(10).shouldBeFalse() + } + + @Test + fun `given multiline string, and maxChar greater than last line length, when isLineFull, then return False`() { + "ciao${lineSeparator()}ciao".isLineFull(10).shouldBeFalse() + } + + @Test + fun `given multiline string, and maxChar equal to last line length, when isLineFull, then return False`() { + "ciao${lineSeparator()}ciao".isLineFull(4).shouldBeTrue() + } + + @Test + fun `given multiline string, and maxChar less than last line length, when isLineFull, then return False`() { + "ciao${lineSeparator()}ciao".isLineFull(4).shouldBeTrue() + } + + @Test + fun `given empty string, and maxChar is zero, when canFitOnALine, then return True`() { + "".canFitOnALine(0).shouldBeTrue() + } + + @Test + fun `given non empty string, and maxChar is zero, when canFitOnALine, then return False`() { + "ciao".canFitOnALine(0).shouldBeFalse() + } + + @Test + fun `given non empty string, and maxChar is greater than string length, when canFitOnALine, then return True`() { + "ciao".canFitOnALine(6).shouldBeTrue() + } + + @Test + fun `given non empty string, and maxChar is equal to string length, when canFitOnALine, then return True`() { + "ciao".canFitOnALine(4).shouldBeTrue() + } + + @Test + fun `given non empty string, and maxChar is less than string length, when canFitOnALine, then return False`() { + "ciao".canFitOnALine(3).shouldBeFalse() + } +} From c6430c31312dab9d0393c2050d9fa079211ddf4d Mon Sep 17 00:00:00 2001 From: sorellla Date: Fri, 21 Nov 2025 10:45:36 +0100 Subject: [PATCH 2/2] #130: LocalAuth back to normal --- src/main/kotlin/authentication/LocalAuth.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/authentication/LocalAuth.kt b/src/main/kotlin/authentication/LocalAuth.kt index 30428a3..bc1342e 100644 --- a/src/main/kotlin/authentication/LocalAuth.kt +++ b/src/main/kotlin/authentication/LocalAuth.kt @@ -35,7 +35,7 @@ object LocalAuth : Auth, ReadAuth { private val validUsers = listOf("Robert", "Dunia", "Tom", "Max", "Casper", "Ed", "Kai", "Laura", "Niamh", "Sofia") - private const val PASSWORD = "1" + private const val PASSWORD = "ILoveRoky" override fun state(): Flow = state