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

grain.social/grain-ios

1 collaborator
0
1
main
75 lines · 3.4 KB Raw History
@testable import Grain
import XCTest

/// Siri phrases and the Home Screen quick actions both end up posting one
/// notification that `MainTabView` listens for. The rawValues are the contract
/// between the two halves, and nothing else checks that they still line up.
@MainActor
final class AppShortcutsTests: GrainTestCase {
    private func actionPosted(by run: @MainActor () async throws -> Void) async rethrows -> String? {
        final class Box: @unchecked Sendable {
            var value: String?
        }
        let box = Box()
        let token = NotificationCenter.default.addObserver(
            forName: .grainShortcutAction, object: nil, queue: nil
        ) { notification in
            box.value = notification.object as? String
        }
        defer { NotificationCenter.default.removeObserver(token) }

        try await run()
        return box.value
    }

    func testEveryIntentPostsItsOwnAction() async throws {
        let cases: [(String, @MainActor () async throws -> Void)] = [
            (GrainShortcutAction.feed.rawValue, { _ = try await OpenFeedIntent().perform() }),
            (GrainShortcutAction.search.rawValue, { _ = try await OpenSearchIntent().perform() }),
            (GrainShortcutAction.notifications.rawValue, { _ = try await OpenNotificationsIntent().perform() }),
            (GrainShortcutAction.profile.rawValue, { _ = try await OpenProfileIntent().perform() }),
            (GrainShortcutAction.createStory.rawValue, { _ = try await CreateStoryIntent().perform() }),
            (GrainShortcutAction.createGallery.rawValue, { _ = try await CreateGalleryIntent().perform() }),
        ]

        for (expected, run) in cases {
            let posted = try await actionPosted(by: run)
            XCTAssertEqual(posted, expected)
        }
    }

    /// The Info.plist quick actions carry these strings, so renaming a case
    /// silently breaks the Home Screen long-press menu.
    func testTheActionRawValuesAreTheOnesTheInfoPlistUses() {
        XCTAssertEqual(GrainShortcutAction.createStory.rawValue, "createStory")
        XCTAssertEqual(GrainShortcutAction.createGallery.rawValue, "createGallery")
        XCTAssertEqual(GrainShortcutAction(rawValue: "feed"), .feed)
        XCTAssertNil(GrainShortcutAction(rawValue: "somethingElse"))
    }

    /// Every quick action declared in Info.plist has to map to a case the app
    /// can route, or long-pressing the icon does nothing.
    func testEveryInfoPlistQuickActionMapsToAKnownAction() throws {
        let items = try XCTUnwrap(
            Bundle.main.object(forInfoDictionaryKey: "UIApplicationShortcutItems") as? [[String: Any]],
            "The app bundle declares no quick actions"
        )
        XCTAssertFalse(items.isEmpty)

        for item in items {
            let type = try XCTUnwrap(item["UIApplicationShortcutItemType"] as? String)
            let suffix = String(type.split(separator: ".").last ?? "")
            XCTAssertNotNil(
                GrainShortcutAction(rawValue: suffix),
                "Quick action \(type) doesn't map to a GrainShortcutAction"
            )
            XCTAssertFalse((item["UIApplicationShortcutItemTitle"] as? String ?? "").isEmpty)
        }
    }

    /// The provider is resolved by the system at install time; a malformed one
    /// silently drops every phrase.
    func testTheShortcutsProviderDeclaresOneShortcutPerIntent() {
        XCTAssertEqual(GrainShortcuts.appShortcuts.count, 6)
    }
}