TM-SGNL-iOS/SignalUI/Utils/FormattedNumberFieldTest.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

260 lines
9.5 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
@testable import SignalUI
final class FormattedNumberFieldTest: XCTestCase {
/// A helper that makes it easy to express test cases with concise strings.
struct TestState: Equatable, ExpressibleByStringLiteral, CustomDebugStringConvertible {
public let formattedString: String
public let selectionStart: Int
public let selectionEnd: Int
public var debugDescription: String {
if selectionStart == selectionEnd {
return formattedString.inserted("|", at: selectionStart)
} else {
return formattedString.inserted("]", at: selectionEnd).inserted("[", at: selectionStart)
}
}
public init(formattedString: String, selectionStart: Int, selectionEnd: Int) {
self.formattedString = formattedString
self.selectionStart = selectionStart
self.selectionEnd = selectionEnd
}
public init(stringLiteral string: String) {
self.formattedString = string
.replacingOccurrences(of: "|", with: "")
.replacingOccurrences(of: "[", with: "")
.replacingOccurrences(of: "]", with: "")
if let pipeIndex = string.firstIndex(of: "|") {
let pipeDistance = string.distance(to: pipeIndex)
self.selectionStart = pipeDistance
self.selectionEnd = pipeDistance
} else if let startIndex = string.firstIndex(of: "["), let endIndex = string.lastIndex(of: "]") {
self.selectionStart = string.distance(to: startIndex)
self.selectionEnd = string.distance(to: endIndex) - 1
} else {
fatalError("Test was not set up correctly. String was badly formatted: \(string)")
}
}
}
private func testFormat(_ str: String) -> String {
var result = [Character]()
var charactersInGroup: UInt8 = 0
for character in str {
result.append(character)
charactersInGroup += 1
if charactersInGroup == 4 {
result.append(" ")
charactersInGroup = 0
}
}
return String(result)
}
func testSingleDelete() {
let noopCases: [(TestState, FormattedNumberField.SingleDeletionDirection)] = [
("|", .backward),
("|123", .backward),
("|1234 ", .backward),
("|", .forward),
("123|", .forward),
("1234| ", .forward),
("1234 |", .forward)
]
for (inputState, deletionDirection) in noopCases {
let result = FormattedNumberField.singleDelete(
formattedString: inputState.formattedString,
allowedCharacters: .numbers,
cursorPosition: inputState.selectionStart,
direction: deletionDirection,
format: testFormat
)
XCTAssertNil(result, "\(inputState) \(deletionDirection)")
}
let deletionCases: [(TestState, FormattedNumberField.SingleDeletionDirection, TestState)] = [
("1|", .backward, "|"),
("1234| ", .backward, "123|"),
("1234 |", .backward, "123|"),
("12|34 567", .backward, "1|345 67"),
("1234| 567", .backward, "123|5 67"),
("1234 |567", .backward, "123|5 67"),
("12|39 9999 9999 9999 9999 ", .backward, "1|399 9999 9999 9999 999"),
("|1", .forward, "|"),
("12|34 567", .forward, "12|45 67"),
("1234| 56", .forward, "1234| 6"),
("1234 |56", .forward, "1234| 6"),
("12|39 9999 9999 9999 9999 ", .forward, "12|99 9999 9999 9999 999")
]
for (inputState, deletionDirection, expectedOutputState) in deletionCases {
let result = FormattedNumberField.singleDelete(
formattedString: inputState.formattedString,
allowedCharacters: .numbers,
cursorPosition: inputState.selectionStart,
direction: deletionDirection,
format: testFormat
)
XCTAssertEqual(result?.asTestState, expectedOutputState, "\(inputState) \(deletionDirection)")
}
}
func testNumericInsert() {
let noopCases: [(TestState, String)] = [
("|", ""),
("|", "x"),
("123|", "x"),
("1234567890|", "1"),
("1234|567890", "1"),
("123456789|", "123")
]
for (inputState, insertion) in noopCases {
let result = FormattedNumberField.insertOrReplace(
formattedString: inputState.formattedString,
allowedCharacters: .numbers,
selectionStart: inputState.selectionStart,
selectionEnd: inputState.selectionEnd,
rawInsertion: insertion,
maxCharacters: 10,
format: testFormat
)
XCTAssertNil(result, "\(inputState) \(insertion)")
}
let testCases: [(TestState, String, TestState)] = [
("|", "1", "1|"),
("|", "123", "123|"),
("|", "123x", "123|"),
("|", "1234", "1234 |"),
("123|", "4", "1234 |"),
("12|3", "9", "129|3 "),
("12|34 ", "9", "129|3 4"),
("1234| ", "5", "1234 5|"),
("1234 |", "5", "1234 5|"),
("[123]", "9", "9|"),
("[1234] ", "9", "9|"),
("[1234] 5678 ", "9", "9|567 8"),
("[1234] 5678 ", "9", "9|567 8"),
("12[34 56]78 ", "9", "129|7 8"),
("12[34 56]78 ", "000", "1200 0|78"),
("12[34 56]78 ", "0000", "1200 00|78 "),
("12[34 56]78 ", "", "12|78 "),
("12[34 56]78 ", "x", "12|78 "),
("12[34 56]78 ", "0x9", "1209 |78"),
("[1234 5678 9012 34]", "", "|"),
("[1234 5678 9012 34]", "987", "987|"),
("1234 5678 9012 3[4]", "", "1234 5678 9012 3|")
]
for (inputState, insertion, expectedOutputState) in testCases {
let result = FormattedNumberField.insertOrReplace(
formattedString: inputState.formattedString,
allowedCharacters: .numbers,
selectionStart: inputState.selectionStart,
selectionEnd: inputState.selectionEnd,
rawInsertion: insertion,
maxCharacters: 10,
format: testFormat
)
XCTAssertEqual(result?.asTestState, expectedOutputState, "\(inputState) \(insertion)")
}
}
func testAlphanumericInsert() {
let noopCases: [(TestState, String)] = [
("|", ""),
("|", "."),
("AT123|", "."),
("BE1234567890|", "1"),
("HR1234|567890", "1"),
("EE123456789|", "123")
]
for (inputState, insertion) in noopCases {
let result = FormattedNumberField.insertOrReplace(
formattedString: inputState.formattedString,
allowedCharacters: .alphanumeric,
selectionStart: inputState.selectionStart,
selectionEnd: inputState.selectionEnd,
rawInsertion: insertion,
maxCharacters: 12,
format: testFormat
)
XCTAssertNil(result, "\(inputState) \(insertion)")
}
let testCases: [(TestState, String, TestState)] = [
("|", "A", "A|"),
("|", "ABC", "ABC|"),
("|", "ABC.", "ABC|"),
("|", "ABCD", "ABCD |"),
("FI2|", "1", "FI21 |"),
("AB|3", "9", "AB9|3 "),
("AB|34 ", "9", "AB9|3 4"),
("AB34| ", "E", "AB34 E|"),
("AB34 |", "E", "AB34 E|"),
("[AB3]", "9", "9|"),
("[AB34] ", "9", "9|"),
("[AB34] E678 ", "9", "9|E67 8"),
("[AB34] E678 ", "9", "9|E67 8"),
("AB[34 E6]78 ", "9", "AB9|7 8"),
("AB[34 E6]78 ", "000", "AB00 0|78"),
("AB[34 E6]78 ", "0000", "AB00 00|78 "),
("AB[34 E6]78 ", "", "AB|78 "),
("AB[34 E6]78 ", ".", "AB|78 "),
("[AB34 E678 90AB 34]", "", "|"),
("[AB34 E678 90AB 34]", "987", "987|"),
("AB34 E678 90AB 3[4]", "", "AB34 E678 90AB 3|")
]
for (inputState, insertion, expectedOutputState) in testCases {
let result = FormattedNumberField.insertOrReplace(
formattedString: inputState.formattedString,
allowedCharacters: .alphanumeric,
selectionStart: inputState.selectionStart,
selectionEnd: inputState.selectionEnd,
rawInsertion: insertion,
maxCharacters: 10,
format: testFormat
)
XCTAssertEqual(result?.asTestState, expectedOutputState, "\(inputState) \(insertion)")
}
}
}
fileprivate extension String {
func inserted(_ newElement: Character, at offset: Int) -> String {
var result = self
result.insert(newElement, at: index(result.startIndex, offsetBy: offset))
return result
}
func distance(to end: String.Index) -> Int {
distance(from: startIndex, to: end)
}
}
fileprivate extension FormattedNumberField.OperationResult {
var asTestState: FormattedNumberFieldTest.TestState {
.init(
formattedString: formattedString,
selectionStart: cursorPosition,
selectionEnd: cursorPosition
)
}
}