minimask/test/getNextInputState.test.ts
2025-07-17 10:41:47 -07:00

248 lines
8.6 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FormatterToken } from "../dist/minimask"
import { getNextInputState, type InputState } from "../src/getNextInputState"
import type { Formatter } from "../src/minimask"
import { assert, expect, suite, test } from "vitest"
/**
* In order to make before/after input states easier to test, you can use a
* visual string representation (a "viz") of the input like:
*
* "ABC|DEF"
*
* Where the input is "ABCDEF" and the cursor is placed between "C" and "D"
*
* Or you can have a selection like:
*
* "AB|CD|EF"
*
* Where the selection starts between "B" and "C" and ends between "D" and "E"
*/
function vizToState(input: string): InputState {
let value = input.split("|").join("")
let start = input.indexOf("|")
assert(start !== -1, "must have at least one cursor")
let after = input.indexOf("|", start + 1)
assert(after !== start + 1, "cannot have separate start and end cursors in the same position")
let end = after === -1 ? start : after - 1
if (after !== -1) {
assert(input.indexOf("|", after + 1) === -1, "cannot have more than two cursors")
}
return { value, start, end }
}
function stateToViz(state: InputState): string {
let parts = state.value.split("")
parts = parts.toSpliced(state.start, 0, "|")
if (state.end !== state.start) {
parts = parts.toSpliced(state.end + 1, 0, "|")
}
return parts.join("")
}
suite("viz", () => {
function pass(input: string, value: string, start: number, end: number) {
test(input, () => {
let state = vizToState(input)
expect(state.value).toBe(value)
expect(state.start).toBe(start)
expect(state.end).toBe(end)
let output = stateToViz({ value, start, end })
expect(output).toBe(input)
})
}
function fail(input: string, message: string) {
test(input, () => {
expect(() => vizToState(input)).toThrow(message)
})
}
pass("|ABC", "ABC", 0, 0)
pass("A|BC", "ABC", 1, 1)
pass("AB|C", "ABC", 2, 2)
pass("ABC|", "ABC", 3, 3)
pass("|ABC|", "ABC", 0, 3)
pass("|AB|C", "ABC", 0, 2)
pass("|A|BC", "ABC", 0, 1)
pass("AB|C|", "ABC", 2, 3)
pass("A|BC|", "ABC", 1, 3)
fail("||ABC", "cannot have separate start and end cursors in the same position")
fail("A||BC", "cannot have separate start and end cursors in the same position")
fail("AB||C", "cannot have separate start and end cursors in the same position")
fail("ABC||", "cannot have separate start and end cursors in the same position")
fail("ABC", "must have at least one cursor")
fail("|A||BC", "cannot have more than two cursors")
fail("|A|B|C", "cannot have more than two cursors")
fail("|A|BC|", "cannot have more than two cursors")
fail("|A|B|C|", "cannot have more than two cursors")
})
function isLetter(char: string): boolean {
return /[A-Z]/.test(char)
}
let TEST_FORMATTER: Formatter = (input) => {
let tokens: FormatterToken[] = []
let chars = input.split("")
let index = 0
function letter() {
while (true) {
let char = chars[index]
if (char == null) return
if (isLetter(char)) {
tokens.push({ char, index, mask: false })
index++
break
} else {
index++
}
}
}
function dash() {
let char = chars[index]
if (char == null) return
if (!isLetter(char)) {
tokens.push({ char: "-", index, mask: true })
index++
} else if (index < chars.length) {
tokens.push({ char: "-", index: index - 1, mask: true })
}
}
letter()
letter()
letter()
dash()
letter()
letter()
letter()
dash()
dash()
letter()
letter()
letter()
return tokens
}
suite("getNextInputState", () => {
function check(input: string, expectedNormal: string, expectedDeleting: string) {
test(`${input} -> ${expectedNormal} (normal)`, () => {
let state = vizToState(input)
let result = getNextInputState(TEST_FORMATTER, state.value, state.start, state.end, false)
let actual = stateToViz(result)
expect(actual).toBe(expectedNormal)
})
test(`${input} -> ${expectedNormal} (deleting)`, () => {
let state = vizToState(input)
let result = getNextInputState(TEST_FORMATTER, state.value, state.start, state.end, true)
let actual = stateToViz(result)
expect(actual).toBe(expectedDeleting)
})
}
suite("formatted", () => {
suite("cursor", () => {
check("|ABC-DEF", "|ABC-DEF", "|ABC-DEF")
check("A|BC-DEF", "A|BC-DEF", "A|BC-DEF")
check("AB|C-DEF", "AB|C-DEF", "AB|C-DEF")
check("ABC|-DEF", "ABC|-DEF", "ABC|-DEF") // deleting moves before mask chars
check("ABC-|DEF", "ABC-|DEF", "ABC|-DEF") // deleting moves before mask chars
check("ABC-D|EF", "ABC-D|EF", "ABC-D|EF")
check("ABC-DE|F", "ABC-DE|F", "ABC-DE|F")
check("ABC-DEF|", "ABC-DEF|", "ABC-DEF|")
})
suite("selection", () => {
check("|A|BC-DEF", "|A|BC-DEF", "|A|BC-DEF")
check("|AB|C-DEF", "|AB|C-DEF", "|AB|C-DEF")
check("|ABC|-DEF", "|ABC|-DEF", "|ABC|-DEF")
check("|ABC-|DEF", "|ABC-|DEF", "|ABC|-DEF") // deleting moves before mask chars
check("|ABC-D|EF", "|ABC-D|EF", "|ABC-D|EF")
check("|ABC-DE|F", "|ABC-DE|F", "|ABC-DE|F")
check("|ABC-DEF|", "|ABC-DEF|", "|ABC-DEF|")
check("A|BC-DEF|", "A|BC-DEF|", "A|BC-DEF|")
check("AB|C-DEF|", "AB|C-DEF|", "AB|C-DEF|")
check("ABC|-DEF|", "ABC|-DEF|", "ABC|-DEF|")
check("ABC-|DEF|", "ABC-|DEF|", "ABC|-DEF|") // deleting moves before mask chars
check("ABC-D|EF|", "ABC-D|EF|", "ABC-D|EF|")
check("ABC-DE|F|", "ABC-DE|F|", "ABC-DE|F|")
})
suite("multiple dropped chars before", () => {
check("ABC-DEF--G|HI", "ABC-DEF--G|HI", "ABC-DEF--G|HI")
check("ABC-DEF--|GHI", "ABC-DEF--|GHI", "ABC-DEF|--GHI") // deleting moves before mask chars
check("ABC-DEF-|-GHI", "ABC-DEF-|-GHI", "ABC-DEF|--GHI") // deleting moves before mask chars
check("ABC-DEF|--GHI", "ABC-DEF|--GHI", "ABC-DEF|--GHI")
check("ABC-DE|F--GHI", "ABC-DE|F--GHI", "ABC-DE|F--GHI")
})
suite("poorly formatted", () => {
check("|*A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
check("*|A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
check("*A|*B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
check("*A*|B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
check("*A*B|*C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
check("*A*B*|C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
check("*A*B*C|*D*E*F*", "ABC|-DEF-", "ABC|-DEF-")
check("*A*B*C*|D*E*F*", "ABC-|DEF-", "ABC|-DEF-") // deleting moves before mask chars
check("*A*B*C*D|*E*F*", "ABC-D|EF-", "ABC-D|EF-")
check("*A*B*C*D*|E*F*", "ABC-D|EF-", "ABC-D|EF-")
check("*A*B*C*D*E|*F*", "ABC-DE|F-", "ABC-DE|F-")
check("*A*B*C*D*E*|F*", "ABC-DE|F-", "ABC-DE|F-")
check("*A*B*C*D*E*F|*", "ABC-DEF|-", "ABC-DEF|") // deleting trims trailing mask chars
check("*A*B*C*D*E*F*|", "ABC-DEF-|", "ABC-DEF|") // deleting trims trailing mask chars
})
})
suite("unformatted", () => {
suite("cursor", () => {
check("|ABCDEF", "|ABC-DEF", "|ABC-DEF")
check("A|BCDEF", "A|BC-DEF", "A|BC-DEF")
check("AB|CDEF", "AB|C-DEF", "AB|C-DEF")
check("ABC|DEF", "ABC-|DEF", "ABC|-DEF")
check("ABCD|EF", "ABC-D|EF", "ABC-D|EF")
check("ABCDE|F", "ABC-DE|F", "ABC-DE|F")
check("ABCDEF|", "ABC-DEF|", "ABC-DEF|")
})
suite("selection", () => {
check("|A|BCDEF", "|A|BC-DEF", "|A|BC-DEF")
check("|AB|CDEF", "|AB|C-DEF", "|AB|C-DEF")
check("|ABC|DEF", "|ABC-|DEF", "|ABC|-DEF") // deleting moves before mask chars
check("|ABCD|EF", "|ABC-D|EF", "|ABC-D|EF")
check("|ABCDE|F", "|ABC-DE|F", "|ABC-DE|F")
check("|ABCDEF|", "|ABC-DEF|", "|ABC-DEF|")
check("A|BCDEF|", "A|BC-DEF|", "A|BC-DEF|")
check("AB|CDEF|", "AB|C-DEF|", "AB|C-DEF|")
check("ABC|DEF|", "ABC-|DEF|", "ABC|-DEF|") // deleting moves before mask chars
check("ABCD|EF|", "ABC-D|EF|", "ABC-D|EF|")
check("ABCDE|F|", "ABC-DE|F|", "ABC-DE|F|")
})
suite("multiple dropped chars before", () => {
check("ABCDEFG|HI", "ABC-DEF--G|HI", "ABC-DEF--G|HI")
check("ABCDEF|GHI", "ABC-DEF--|GHI", "ABC-DEF|--GHI")
check("ABCDE|FGHI", "ABC-DE|F--GHI", "ABC-DE|F--GHI")
})
suite("poorly formatted", () => {
check("|*A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
check("*|A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
check("*A|*B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
check("*A*|B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
check("*A*B|*C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
check("*A*B*|C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
check("*A*B*C|*D*E*F*", "ABC|-DEF-", "ABC|-DEF-")
check("*A*B*C*D|*E*F*", "ABC-D|EF-", "ABC-D|EF-")
check("*A*B*C*D*|E*F*", "ABC-D|EF-", "ABC-D|EF-")
check("*A*B*C*D*E|*F*", "ABC-DE|F-", "ABC-DE|F-")
check("*A*B*C*D*E*|F*", "ABC-DE|F-", "ABC-DE|F-")
check("*A*B*C*D*E*F|*", "ABC-DEF|-", "ABC-DEF|") // deleting trims trailing mask chars
check("*A*B*C*D*E*F*|", "ABC-DEF-|", "ABC-DEF|") // deleting trims trailing mask chars
})
})
})