@@ -34,6 +34,35 @@ final class FormattableNotIconTests: XCTestCase {
3434 XCTAssertEqual ( subject? . value, Constants . icon)
3535 }
3636
37+ // MARK: - apply(_:to:withShift:)
38+
39+ func testApplyReturnsUTF16ShiftForMultiByteNoticon( ) {
40+ // π¨βπ©βπ§βπ¦ is 1 grapheme cluster but 11 UTF-16 code units.
41+ // The noticon string is value + " ", so 12 UTF-16 code units total.
42+ // With the old .count code, the shift would have been 2 (1 emoji + 1 space).
43+ let familyEmoji = " π¨βπ©βπ§βπ¦ "
44+ let noticonRange = FormattableNoticonRange ( value: familyEmoji, range: NSRange ( location: 0 , length: 0 ) )
45+
46+ let baseText = " Hello World "
47+ let string = NSMutableAttributedString ( string: baseText)
48+
49+ let styles = SnippetsContentStyles ( rangeStylesMap: [
50+ . noticon: [ . foregroundColor: UIColor . red]
51+ ] )
52+
53+ let shift = noticonRange. apply ( styles, to: string, withShift: 0 )
54+
55+ // The noticon is familyEmoji + " " = 12 UTF-16 code units
56+ let expectedShift = ( familyEmoji + " " ) . utf16. count
57+ XCTAssertEqual ( shift, expectedShift, " Shift should equal the UTF-16 count of the noticon, not the grapheme cluster count " )
58+
59+ // Verify styles were applied to the full noticon range
60+ var effectiveRange = NSRange ( )
61+ let color = string. attribute ( . foregroundColor, at: 0 , effectiveRange: & effectiveRange)
62+ XCTAssertNotNil ( color)
63+ XCTAssertEqual ( effectiveRange. length, expectedShift, " Style should cover the full UTF-16 range of the inserted noticon " )
64+ }
65+
3766 private func mockProperties( ) -> NotificationContentRange . Properties {
3867 return NotificationContentRange . Properties ( range: Constants . range)
3968 }
0 commit comments