~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
161 lines · 5.1 KB Raw History
@testable import Grain
import XCTest

/// The @ detection runs on every keystroke in a comment box, and getting it
/// wrong either pops a suggestion strip over an email address or fails to offer
/// one at all. Detection and completion are synchronous; only the lookup they
/// trigger is not, so these stay off the network by clearing before it fires.
@MainActor
final class MentionAutocompleteTests: GrainTestCase {
    private var state: MentionAutocompleteState!

    override func setUp() async throws {
        try await super.setUp()
        state = MentionAutocompleteState()
    }

    override func tearDown() async throws {
        // Cancels the debounced lookup so it can't outlive the test.
        state.clear()
        state = nil
        try await super.tearDown()
    }

    /// `update` sets the query synchronously and only then schedules the search,
    /// so the query can be read without waiting for the network.
    private func query(after text: String) -> String? {
        state.update(text: text)
        let result = state.activeQuery
        state.clear()
        return result
    }

    // MARK: - Detection

    func testAnAtAtTheStartOfTheTextStartsAMention() {
        XCTAssertEqual(query(after: "@ali"), "ali")
    }

    func testAnAtAfterASpaceStartsAMention() {
        XCTAssertEqual(query(after: "nice shot @ali"), "ali")
    }

    func testAnAtAfterANewlineStartsAMention() {
        XCTAssertEqual(query(after: "nice shot\n@ali"), "ali")
    }

    /// The classic false positive: an email address is not a mention.
    func testAnAtInTheMiddleOfAWordIsNotAMention() {
        XCTAssertNil(query(after: "mail me at chad@example.com"))
    }

    func testABareAtIsNotYetAMention() {
        XCTAssertNil(query(after: "@"))
        XCTAssertNil(query(after: "hello @"))
    }

    /// Once a space follows the handle the user has moved on, so the strip has
    /// to go away rather than keep matching the finished handle.
    func testASpaceAfterTheHandleEndsTheMention() {
        XCTAssertNil(query(after: "@alice.grain.social nice"))
    }

    func testTextWithNoAtIsNotAMention() {
        XCTAssertNil(query(after: "just a caption"))
        XCTAssertNil(query(after: ""))
    }

    /// Only the mention being typed matters, not one already finished earlier
    /// in the sentence.
    func testTheLastAtWins() {
        XCTAssertEqual(query(after: "@alice.test and @bo"), "bo")
    }

    func testAPartialHandleWithDotsIsCarriedThrough() {
        XCTAssertEqual(query(after: "@alice.grain."), "alice.grain.")
    }

    // MARK: - isActive

    func testTheStripIsOnlyActiveWhileAMentionIsBeingTyped() {
        XCTAssertFalse(state.isActive)

        state.update(text: "@ali")
        XCTAssertTrue(state.isActive)

        state.update(text: "@alice.test done")
        XCTAssertFalse(state.isActive)
    }

    func testClearingTakesTheStripDownAndDropsTheSuggestions() {
        state.update(text: "@ali")
        state.suggestions = [MentionSuggestion(handle: "alice.test", displayName: "Alice", avatar: nil)]

        state.clear()

        XCTAssertFalse(state.isActive)
        XCTAssertNil(state.activeQuery)
        XCTAssertTrue(state.suggestions.isEmpty)
    }

    // MARK: - complete

    func testCompletingReplacesThePartialHandleAndAddsATrailingSpace() {
        var text = "nice shot @ali"
        state.update(text: text)

        state.complete(handle: "alice.grain.social", in: &text)

        XCTAssertEqual(text, "nice shot @alice.grain.social ")
    }

    func testCompletingAtTheStartOfTheText() {
        var text = "@ali"
        state.update(text: text)

        state.complete(handle: "alice.grain.social", in: &text)

        XCTAssertEqual(text, "@alice.grain.social ")
    }

    /// Only the mention under the cursor should be rewritten — an earlier,
    /// already-complete mention has to survive untouched.
    func testCompletingLeavesAnEarlierMentionAlone() {
        var text = "@bob.test hi @ali"
        state.update(text: text)

        state.complete(handle: "alice.test", in: &text)

        XCTAssertEqual(text, "@bob.test hi @alice.test ")
    }

    func testCompletingTakesTheStripDown() {
        var text = "@ali"
        state.update(text: text)

        state.complete(handle: "alice.test", in: &text)

        XCTAssertFalse(state.isActive)
        XCTAssertTrue(state.suggestions.isEmpty)
    }

    /// Nothing is being typed, so there is nothing to replace — the text must
    /// come back unchanged rather than gaining a stray handle.
    func testCompletingWithNoActiveMentionLeavesTheTextAlone() {
        var text = "just a caption"

        state.complete(handle: "alice.test", in: &text)

        XCTAssertEqual(text, "just a caption")
    }

    // MARK: - MentionSuggestion

    func testSuggestionsAreIdentifiedByHandle() {
        let suggestion = MentionSuggestion(handle: "alice.test", displayName: "Alice", avatar: nil)

        XCTAssertEqual(suggestion.id, "alice.test")
        XCTAssertEqual(suggestion, MentionSuggestion(handle: "alice.test", displayName: "Alice", avatar: nil))
        XCTAssertNotEqual(suggestion, MentionSuggestion(handle: "bob.test", displayName: "Alice", avatar: nil))
    }
}