// // 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 { let formattedString: String let selectionStart: Int let selectionEnd: Int var debugDescription: String { if selectionStart == selectionEnd { return formattedString.inserted("|", at: selectionStart) } else { return formattedString.inserted("]", at: selectionEnd).inserted("[", at: selectionStart) } } init(formattedString: String, selectionStart: Int, selectionEnd: Int) { self.formattedString = formattedString self.selectionStart = selectionStart self.selectionEnd = selectionEnd } 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)") } } } private 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) } } private extension FormattedNumberField.OperationResult { var asTestState: FormattedNumberFieldTest.TestState { .init( formattedString: formattedString, selectionStart: cursorPosition, selectionEnd: cursorPosition, ) } }