1119 lines
43 KiB
Swift
1119 lines
43 KiB
Swift
//
|
||
// Copyright 2023 Signal Messenger, LLC
|
||
// SPDX-License-Identifier: AGPL-3.0-only
|
||
//
|
||
|
||
import LibSignalClient
|
||
import XCTest
|
||
|
||
@testable import SignalServiceKit
|
||
|
||
final class MessageBodyTests: XCTestCase {
|
||
|
||
typealias Style = MessageBodyRanges.Style
|
||
typealias SingleStyle = MessageBodyRanges.SingleStyle
|
||
typealias CollapsedStyle = MessageBodyRanges.CollapsedStyle
|
||
|
||
// MARK: - Hydration
|
||
|
||
let acis = (0...5).map { _ in Aci.randomForTesting() }
|
||
|
||
func testHydration_noMentions() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Hello",
|
||
ranges: .init(
|
||
mentions: [:],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [:],
|
||
output: .init(
|
||
hydratedText: "Hello",
|
||
mentionAttributes: [],
|
||
styleAttributes: []
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_singleMention() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Hello @",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 6, length: 1): acis[0]
|
||
],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [acis[0]: "Luke"],
|
||
output: .init(
|
||
hydratedText: "Hello @Luke",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 6, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 6, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: []
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_multipleMentions() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Hello @ and @, how is @?",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 6, length: 1): acis[0],
|
||
NSRange(location: 12, length: 1): acis[1],
|
||
NSRange(location: 22, length: 1): acis[2]
|
||
],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "Luke",
|
||
acis[1]: "Leia",
|
||
acis[2]: "Han"
|
||
],
|
||
output: .init(
|
||
hydratedText: "Hello @Luke and @Leia, how is @Han?",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 6, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 6, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 12, length: 1),
|
||
mentionAci: acis[1],
|
||
displayName: "Leia"
|
||
),
|
||
range: NSRange(location: 16, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 22, length: 1),
|
||
mentionAci: acis[2],
|
||
displayName: "Han"
|
||
),
|
||
range: NSRange(location: 30, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: []
|
||
)
|
||
)
|
||
}
|
||
|
||
/// Strictly speaking, mentions should always have length 1 when sent
|
||
/// in messages. But best not to crash due to an antagonistic sender.
|
||
func testHydration_nonSingularLengthMentions() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Hello @wasd and @1, how is ?",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 6, length: 5): acis[0],
|
||
NSRange(location: 16, length: 2): acis[1],
|
||
NSRange(location: 27, length: 0): acis[2]
|
||
],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "Luke",
|
||
acis[1]: "Leia",
|
||
acis[2]: "Han"
|
||
],
|
||
output: .init(
|
||
hydratedText: "Hello @Luke and @Leia, how is @Han?",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 6, length: 5),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 6, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 16, length: 2),
|
||
mentionAci: acis[1],
|
||
displayName: "Leia"
|
||
),
|
||
range: NSRange(location: 16, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 27, length: 0),
|
||
mentionAci: acis[2],
|
||
displayName: "Han"
|
||
),
|
||
range: NSRange(location: 30, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: []
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_notAllHydrated() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Hello @ and @, how is @?",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 6, length: 1): acis[0],
|
||
NSRange(location: 12, length: 1): acis[1],
|
||
NSRange(location: 22, length: 1): acis[2]
|
||
],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "Luke",
|
||
acis[2]: "Han"
|
||
],
|
||
output: .init(
|
||
hydratedText: "Hello @Luke and @, how is @Han?",
|
||
unhydratedMentions: [
|
||
.init(
|
||
.fromOriginalRange(NSRange(location: 12, length: 1), mentionAci: acis[1]),
|
||
range: NSRange(location: 16, length: 1)
|
||
)
|
||
],
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 6, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 6, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 22, length: 1),
|
||
mentionAci: acis[2],
|
||
displayName: "Han"
|
||
),
|
||
range: NSRange(location: 26, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: []
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_justStyles() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "This is bold, italic, and mono",
|
||
ranges: .init(
|
||
mentions: [:],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 8, length: 4)),
|
||
.init(.italic, range: NSRange(location: 14, length: 6)),
|
||
.init(.monospace, range: NSRange(location: 26, length: 4))
|
||
]
|
||
)
|
||
),
|
||
names: [:],
|
||
output: .init(
|
||
hydratedText: "This is bold, italic, and mono",
|
||
mentionAttributes: [],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 8, length: 4))
|
||
),
|
||
range: NSRange(location: 8, length: 4)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 14, length: 6))
|
||
),
|
||
range: NSRange(location: 14, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.monospace, mergedRange: NSRange(location: 26, length: 4))
|
||
),
|
||
range: NSRange(location: 26, length: 4)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_stylesAndTrailingMention() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "This is bold, italic, and mono, @.",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 32, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 8, length: 4)),
|
||
.init(.italic, range: NSRange(location: 14, length: 6)),
|
||
.init(.monospace, range: NSRange(location: 26, length: 4))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "Luke"],
|
||
output: .init(
|
||
hydratedText: "This is bold, italic, and mono, @Luke.",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 32, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 32, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 8, length: 4))
|
||
),
|
||
range: NSRange(location: 8, length: 4)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 14, length: 6))
|
||
),
|
||
range: NSRange(location: 14, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.monospace, mergedRange: NSRange(location: 26, length: 4))
|
||
),
|
||
range: NSRange(location: 26, length: 4)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_stylesAndLeadingMention() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@, this is bold, italic, and mono",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 11, length: 4)),
|
||
.init(.italic, range: NSRange(location: 17, length: 6)),
|
||
.init(.monospace, range: NSRange(location: 29, length: 4))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "Luke"],
|
||
output: .init(
|
||
hydratedText: "@Luke, this is bold, italic, and mono",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 0, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 11, length: 4))
|
||
),
|
||
range: NSRange(location: 15, length: 4)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 17, length: 6))
|
||
),
|
||
range: NSRange(location: 21, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.monospace, mergedRange: NSRange(location: 29, length: 4))
|
||
),
|
||
range: NSRange(location: 33, length: 4)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_overlappingStyleAndMention() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "Use the force, @",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 15, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 0, length: 16))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "Luke"],
|
||
output: .init(
|
||
hydratedText: "Use the force, @Luke",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 15, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 15, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 0, length: 16))
|
||
),
|
||
range: NSRange(location: 0, length: 20)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_overlappingStylesAndMentions() {
|
||
// The styles are flattened out into this before hydration applies:
|
||
// .init(.bold, range: NSRange(location: 0, length: 3)),
|
||
// .init(.bold.union(.italic), range: NSRange(location: 3, length: 3)),
|
||
// .init(.bold, range: NSRange(location: 6, length: 2)),
|
||
// .init(.bold.union(.monospace), range: NSRange(location: 8, length: 16)),
|
||
// .init(.bold.union(.monospace).union(.spoiler), range: NSRange(location: 24, length: 3)),
|
||
// .init(.bold.union(.spoiler), range: NSRange(location: 27, length: 4)),
|
||
// .init(.bold, range: NSRange(location: 31, length: 20)),
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@, @@@, @@@@@@@@@@@@@@@ and @@@ are stylish people.",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 1): acis[0],
|
||
NSRange(location: 3, length: 3): acis[1],
|
||
NSRange(location: 8, length: 15): acis[2],
|
||
NSRange(location: 28, length: 3): acis[3]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 0, length: 51)),
|
||
.init(.italic, range: NSRange(location: 4, length: 1)),
|
||
.init(.monospace, range: NSRange(location: 12, length: 15)),
|
||
.init(.spoiler, range: NSRange(location: 24, length: 5))
|
||
]
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "BoldGuy",
|
||
acis[1]: "BoldItalicGuy",
|
||
acis[2]: "BoldMonoGuy",
|
||
acis[3]: "BoldSpoilerGuy"
|
||
],
|
||
output: .init(
|
||
hydratedText: "@BoldGuy, @BoldItalicGuy, @BoldMonoGuy and @BoldSpoilerGuy are stylish people.",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "BoldGuy"
|
||
),
|
||
range: NSRange(location: 0, length: 8)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 3, length: 3),
|
||
mentionAci: acis[1],
|
||
displayName: "BoldItalicGuy"
|
||
),
|
||
range: NSRange(location: 10, length: 14)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 8, length: 15),
|
||
mentionAci: acis[2],
|
||
displayName: "BoldMonoGuy"
|
||
),
|
||
range: NSRange(location: 26, length: 12)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 28, length: 3),
|
||
mentionAci: acis[3],
|
||
displayName: "BoldSpoilerGuy"
|
||
),
|
||
range: NSRange(location: 43, length: 15)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 0, length: 10)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.italic: NSRange(location: 3, length: 3)
|
||
])
|
||
),
|
||
range: NSRange(location: 10, length: 14)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 24, length: 2)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.monospace: NSRange(location: 8, length: 19)
|
||
])
|
||
),
|
||
range: NSRange(location: 26, length: 13)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.monospace: NSRange(location: 8, length: 19),
|
||
.spoiler: NSRange(location: 24, length: 7)
|
||
])
|
||
),
|
||
range: NSRange(location: 39, length: 3)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.spoiler: NSRange(location: 24, length: 7)
|
||
])
|
||
),
|
||
range: NSRange(location: 42, length: 16)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 58, length: 20)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_overlappingStylesAndSomeUnhydratedMentions() {
|
||
// The styles are flattened out into this before hydration applies:
|
||
// .init(, range: NSRange(location: 0, length: 3), .bold),
|
||
// .init(, range: NSRange(location: 3, length: 3), .bold.union(.italic)),
|
||
// .init(, range: NSRange(location: 6, length: 2), .bold),
|
||
// .init(, range: NSRange(location: 8, length: 16), .bold.union(.monospace)),
|
||
// .init(, range: NSRange(location: 24, length: 3), .bold.union(.monospace).union(.spoiler)),
|
||
// .init(, range: NSRange(location: 27, length: 4), .bold.union(.spoiler)),
|
||
// .init(, range: NSRange(location: 31, length: 20), .bold),
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@, @@@, @@@@@@@@@@@@@@@ and @@@ are stylish people.",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 1): acis[0],
|
||
NSRange(location: 3, length: 3): acis[1],
|
||
NSRange(location: 8, length: 15): acis[2],
|
||
NSRange(location: 28, length: 3): acis[3]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 0, length: 51)),
|
||
.init(.italic, range: NSRange(location: 4, length: 1)),
|
||
.init(.monospace, range: NSRange(location: 12, length: 15)),
|
||
.init(.spoiler, range: NSRange(location: 24, length: 5))
|
||
]
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "BoldGuy",
|
||
acis[3]: "BoldSpoilerGuy"
|
||
],
|
||
output: .init(
|
||
hydratedText: "@BoldGuy, @@@, @@@@@@@@@@@@@@@ and @BoldSpoilerGuy are stylish people.",
|
||
unhydratedMentions: [
|
||
.init(.fromOriginalRange(NSRange(location: 3, length: 3), mentionAci: acis[1]), range: NSRange(location: 10, length: 3)),
|
||
.init(.fromOriginalRange(NSRange(location: 8, length: 15), mentionAci: acis[2]), range: NSRange(location: 15, length: 15))
|
||
],
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "BoldGuy"
|
||
),
|
||
range: NSRange(location: 0, length: 8)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 28, length: 3),
|
||
mentionAci: acis[3],
|
||
displayName: "BoldSpoilerGuy"
|
||
),
|
||
range: NSRange(location: 35, length: 15)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 0, length: 10)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.italic: NSRange(location: 3, length: 3)
|
||
])
|
||
),
|
||
range: NSRange(location: 10, length: 3)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 13, length: 2)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.monospace: NSRange(location: 8, length: 19)
|
||
])
|
||
),
|
||
range: NSRange(location: 15, length: 16)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.monospace: NSRange(location: 8, length: 19),
|
||
.spoiler: NSRange(location: 24, length: 7)
|
||
])
|
||
),
|
||
range: NSRange(location: 31, length: 3)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 51),
|
||
.spoiler: NSRange(location: 24, length: 7)
|
||
])
|
||
),
|
||
range: NSRange(location: 34, length: 16)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 51))
|
||
),
|
||
range: NSRange(location: 50, length: 20)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_multipleMentions_RTL() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "שלום @. שלום @.",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 5, length: 1): acis[0],
|
||
NSRange(location: 13, length: 1): acis[1]
|
||
],
|
||
styles: []
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "לוק",
|
||
acis[1]: "ליאה"
|
||
],
|
||
output: .init(
|
||
hydratedText: "שלום לוק@. שלום ליאה@.",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 5, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 5, length: 4)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 13, length: 1),
|
||
mentionAci: acis[1],
|
||
displayName: "ליאה"
|
||
),
|
||
range: NSRange(location: 16, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: []
|
||
),
|
||
isRTL: true
|
||
)
|
||
}
|
||
|
||
func testHydration_styleAndMention_RTL() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "השתמש בכוח, @",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 12, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 5, length: 3))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "השתמש בכוח, לוק@",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 12, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 12, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 5, length: 3))
|
||
),
|
||
range: NSRange(location: 5, length: 3)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@, השתמש בכוח",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 5, length: 3))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "לוק@, השתמש בכוח",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 0, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 5, length: 3))
|
||
),
|
||
range: NSRange(location: 8, length: 3)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
}
|
||
|
||
func testHydration_overlappingStyleAndMention_RTL() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "השתמש בכוח, @",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 12, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 0, length: 13))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "השתמש בכוח, לוק@",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 12, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 12, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 0, length: 13))
|
||
),
|
||
range: NSRange(location: 0, length: 16)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@, השתמש בכוח",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 1): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 0, length: 13))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "לוק@, השתמש בכוח",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 1),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 0, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 0, length: 13))
|
||
),
|
||
range: NSRange(location: 0, length: 16)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
}
|
||
|
||
func testHydration_partlyOverlappingStyleAndMention_RTL() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "השתמש בכוח, @@@",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 12, length: 3): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 5, length: 8))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "השתמש בכוח, לוק@",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 12, length: 3),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 12, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 5, length: 10))
|
||
),
|
||
range: NSRange(location: 5, length: 11)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@@@, השתמש בכוח",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 3): acis[0]
|
||
],
|
||
styles: [
|
||
.init(.italic, range: NSRange(location: 1, length: 8))
|
||
]
|
||
)
|
||
),
|
||
names: [acis[0]: "לוק"],
|
||
output: .init(
|
||
hydratedText: "לוק@, השתמש בכוח",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 3),
|
||
mentionAci: acis[0],
|
||
displayName: "לוק"
|
||
),
|
||
range: NSRange(location: 0, length: 4)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.italic, mergedRange: NSRange(location: 0, length: 9))
|
||
),
|
||
range: NSRange(location: 0, length: 10)
|
||
)
|
||
]
|
||
),
|
||
isRTL: true
|
||
)
|
||
}
|
||
|
||
func testHydration_multipleMentions_accents() {
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: "@@@ engaña a @@@",
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 3): acis[0],
|
||
NSRange(location: 13, length: 3): acis[1]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 1, length: 9)),
|
||
.init(.italic, range: NSRange(location: 4, length: 6)),
|
||
.init(.monospace, range: NSRange(location: 11, length: 3))
|
||
]
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "José",
|
||
acis[1]: "María"
|
||
],
|
||
output: .init(
|
||
hydratedText: "@José engaña a @María",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 3),
|
||
mentionAci: acis[0],
|
||
displayName: "José"
|
||
),
|
||
range: NSRange(location: 0, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 13, length: 3),
|
||
mentionAci: acis[1],
|
||
displayName: "María"
|
||
),
|
||
range: NSRange(location: 15, length: 6)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 10))
|
||
),
|
||
range: NSRange(location: 0, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 10),
|
||
.italic: NSRange(location: 4, length: 6)
|
||
])
|
||
),
|
||
range: NSRange(location: 6, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.monospace, mergedRange: NSRange(location: 11, length: 5))
|
||
),
|
||
range: NSRange(location: 13, length: 8)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
func testHydration_multipleMentions_emoji() {
|
||
let firstMention = "@@@ "
|
||
let firstMentionHydrated = "@Luke "
|
||
|
||
let firstEmojiLocation = (firstMention as NSString).length
|
||
let firstEmojiLocationHydrated = (firstMentionHydrated as NSString).length
|
||
let firstEmojis = "🤗👨👨👧👦"
|
||
let firstEmojiLength = (firstEmojis as NSString).length
|
||
|
||
let middleWordLocation = firstEmojiLocation + firstEmojiLength
|
||
let middleWordLocationHydrated = firstEmojiLocationHydrated + firstEmojiLength
|
||
let middleWord = "hello"
|
||
|
||
let secondEmojiLocation = middleWordLocation + (middleWord as NSString).length
|
||
let secondEmojiLocationHydrated = middleWordLocationHydrated + (middleWord as NSString).length
|
||
let secondEmojis = "👩❤️👨🌗"
|
||
let secondEmojiLength = (secondEmojis as NSString).length
|
||
|
||
let secondMentionLocation = secondEmojiLocation + secondEmojiLength
|
||
let secondMentionLocationHydrated = secondEmojiLocationHydrated + secondEmojiLength
|
||
let secondMention = " @@@"
|
||
|
||
runHydrationTest(
|
||
input: .init(
|
||
text: firstMention + firstEmojis + middleWord + secondEmojis + secondMention,
|
||
ranges: .init(
|
||
mentions: [
|
||
NSRange(location: 0, length: 3): acis[0],
|
||
NSRange(location: secondMentionLocation + 1, length: 3): acis[1]
|
||
],
|
||
styles: [
|
||
.init(.bold, range: NSRange(location: 1, length: 3 + firstEmojiLength + 5)),
|
||
.init(.italic, range: NSRange(location: firstEmojiLocation, length: firstEmojiLength + 5 + secondEmojiLength)),
|
||
.init(.monospace, range: NSRange(location: middleWordLocation, length: 5 + secondEmojiLength + 2))
|
||
]
|
||
)
|
||
),
|
||
names: [
|
||
acis[0]: "Luke",
|
||
acis[1]: "Leia"
|
||
],
|
||
output: .init(
|
||
hydratedText: "@Luke 🤗👨👨👧👦hello👩❤️👨🌗 @Leia",
|
||
mentionAttributes: [
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: 0, length: 3),
|
||
mentionAci: acis[0],
|
||
displayName: "Luke"
|
||
),
|
||
range: NSRange(location: 0, length: 5)
|
||
),
|
||
.init(
|
||
.fromOriginalRange(
|
||
NSRange(location: secondMentionLocation + 1, length: 3),
|
||
mentionAci: acis[1],
|
||
displayName: "Leia"
|
||
),
|
||
range: NSRange(location: secondMentionLocation + 3, length: 5)
|
||
)
|
||
],
|
||
styleAttributes: [
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.bold, mergedRange: NSRange(location: 0, length: 4 + firstEmojiLength + 5))
|
||
),
|
||
range: NSRange(location: 0, length: 6)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 4 + firstEmojiLength + 5),
|
||
.italic: NSRange(location: firstEmojiLocation, length: firstEmojiLength + 5 + secondEmojiLength)
|
||
])
|
||
),
|
||
range: NSRange(location: firstEmojiLocationHydrated, length: firstEmojiLength)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.bold: NSRange(location: 0, length: 4 + firstEmojiLength + 5),
|
||
.italic: NSRange(location: firstEmojiLocation, length: firstEmojiLength + 5 + secondEmojiLength),
|
||
.monospace: NSRange(location: middleWordLocation, length: 5 + secondEmojiLength + 4)
|
||
])
|
||
),
|
||
range: NSRange(location: middleWordLocationHydrated, length: 5)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init([
|
||
.italic: NSRange(location: firstEmojiLocation, length: firstEmojiLength + 5 + secondEmojiLength),
|
||
.monospace: NSRange(location: middleWordLocation, length: 5 + secondEmojiLength + 4)
|
||
])
|
||
),
|
||
range: NSRange(location: secondEmojiLocationHydrated, length: secondEmojiLength)
|
||
),
|
||
.init(
|
||
.fromCollapsedStyle(
|
||
.init(.monospace, mergedRange: NSRange(location: middleWordLocation, length: 5 + secondEmojiLength + 4))
|
||
),
|
||
range: NSRange(location: secondMentionLocationHydrated, length: 6)
|
||
)
|
||
]
|
||
)
|
||
)
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
private func runHydrationTest(
|
||
input: MessageBody,
|
||
names: [Aci: String],
|
||
output: HydratedMessageBody,
|
||
isRTL: Bool = false,
|
||
file: StaticString = #filePath,
|
||
line: UInt = #line
|
||
) {
|
||
let hydrated = input.hydrating(
|
||
mentionHydrator: { aci in
|
||
if let displayName = names[aci] {
|
||
return .hydrate(displayName)
|
||
} else {
|
||
return .preserveMention
|
||
}
|
||
},
|
||
isRTL: isRTL
|
||
)
|
||
XCTAssertEqual(
|
||
output,
|
||
hydrated,
|
||
file: file,
|
||
line: line
|
||
)
|
||
}
|
||
}
|