1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@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)
}
}