248 lines
8.6 KiB
TypeScript
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
|
|
})
|
|
})
|
|
})
|