Watch: waiting-count complication and multi-select questions
A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows GET /v1/summary. The device token moves to the App Group's Keychain access group on launch so the extension can read it without re-pairing; the app reloads the timeline when its feed counts change and on notifications. Multi-select questions toggle options and send an array of values. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d4d67295c6
commit
0994fd7d5a
18 files changed
+800
-23
No files matched your search
@@ -69,6 +69,33 @@ private let feedItemJSON = """
|
||||
#expect(request.quickOptions == nil)
|
||||
#expect(request.summary == "Hash the codes?")
|
||||
#expect(request.questions.first?.allowCustomAnswer == true)
|
||||
// A gateway older than multi-select leaves the flag out, which means single-select.
|
||||
#expect(request.questions.first?.multiSelect == false)
|
||||
}
|
||||
|
||||
@Test func multiSelectQuestion() throws {
|
||||
let question = try decode(PendingRequest.Question.self, """
|
||||
{ "id": "q1", "header": "Checks", "question": "Which checks?",
|
||||
"options": [{ "label": "Unit", "value": "unit" }, { "label": "Lint", "value": "lint" }],
|
||||
"allowCustomAnswer": false, "multiSelect": true }
|
||||
""")
|
||||
#expect(question.multiSelect)
|
||||
#expect(question.options.map(\.value) == ["unit", "lint"])
|
||||
}
|
||||
|
||||
@Test func summaryFromDocs() throws {
|
||||
let summary = try decode(Summary.self, """
|
||||
{ "needsYou": 1, "working": 3, "hostsOffline": 0,
|
||||
"next": { "hostId": "h", "threadId": "t", "title": "Receipt OCR", "hostLabel": "Mac mini" } }
|
||||
""")
|
||||
#expect(summary.needsYou == 1)
|
||||
#expect(summary.working == 3)
|
||||
#expect(summary.next?.title == "Receipt OCR")
|
||||
#expect(summary.next?.route == .thread(hostId: "h", threadId: "t"))
|
||||
|
||||
let idle = try decode(Summary.self, #"{ "needsYou": 0, "working": 0, "hostsOffline": 2, "next": null }"#)
|
||||
#expect(idle.next == nil)
|
||||
#expect(idle.hostsOffline == 2)
|
||||
}
|
||||
|
||||
@Test func unknownEnumValuesStillDecode() throws {
|
||||
@@ -127,11 +154,19 @@ private let feedItemJSON = """
|
||||
}
|
||||
|
||||
@Test func respondWithAnswers() throws {
|
||||
let object = try encodeObject(RespondRequest.answers(requestId: "r1", answers: ["q1": "yes"]))
|
||||
let object = try encodeObject(RespondRequest.answers(requestId: "r1", answers: ["q1": .text("yes")]))
|
||||
#expect(object["answers"] as? [String: String] == ["q1": "yes"])
|
||||
#expect(object["decision"] == nil)
|
||||
}
|
||||
|
||||
@Test func respondWithMultiSelectAnswers() throws {
|
||||
let object = try encodeObject(RespondRequest.answers(
|
||||
requestId: "r1", answers: ["checks": .values(["unit", "lint"]), "note": .text("after lunch")]))
|
||||
let answers = try #require(object["answers"] as? [String: Any])
|
||||
#expect(answers["checks"] as? [String] == ["unit", "lint"])
|
||||
#expect(answers["note"] as? String == "after lunch")
|
||||
}
|
||||
|
||||
@Test func pushRegistration() throws {
|
||||
let object = try encodeObject(PushRegistration(apnsToken: "ab", apnsEnvironment: .sandbox))
|
||||
#expect(object["apnsEnvironment"] as? String == "sandbox")
|
||||
@@ -185,6 +220,25 @@ private let feedItemJSON = """
|
||||
== ProjectRef(hostId: "on", projectId: "a"))
|
||||
}
|
||||
|
||||
@Test func summaryFromFeed() throws {
|
||||
let feed = DemoFixtures.previewFeed()
|
||||
let summary = Summary(feed)
|
||||
#expect(summary.needsYou == feed.needsYou.count)
|
||||
#expect(summary.working == feed.working.count)
|
||||
#expect(summary.hostsOffline == 1)
|
||||
#expect(summary.next?.threadId == feed.needsYou.first?.threadId)
|
||||
#expect(Summary(Feed(needsYou: [], working: [], recent: [], hosts: [])).next == nil)
|
||||
}
|
||||
|
||||
@Test func routeURLs() {
|
||||
let thread = Route.thread(hostId: "mac mini", threadId: "t/1")
|
||||
#expect(thread.url.absoluteString == "wristcode://thread/mac%20mini/t%2F1")
|
||||
#expect(Route(url: thread.url) == thread)
|
||||
#expect(Route(url: Route.newThread.url) == .newThread)
|
||||
#expect(Route(url: URL(string: "wristcode://thread/only-host")!) == nil)
|
||||
#expect(Route(url: URL(string: "https://thread/h/t")!) == nil)
|
||||
}
|
||||
|
||||
@Test func notificationCommands() {
|
||||
let info: [AnyHashable: Any] = ["hostId": "h", "threadId": "t", "requestId": "r"]
|
||||
#expect(NotificationCommand(actionIdentifier: "allow", userInfo: info, text: nil)
|
||||
@@ -223,4 +277,16 @@ private let feedItemJSON = """
|
||||
request: .decision(requestId: item.request!.id, decision: .accept))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func multiSelectAnswerMovesThreadToWorking() async throws {
|
||||
let gateway = DemoGateway()
|
||||
let item = try #require(try await gateway.feed().needsYou.first { $0.request?.questions.first?.multiSelect == true })
|
||||
let request = try #require(item.request)
|
||||
let question = try #require(request.questions.first)
|
||||
try await gateway.respond(
|
||||
hostId: item.hostId, threadId: item.threadId,
|
||||
request: .answers(requestId: request.id, answers: [question.id: .values(["unit", "lint"])]))
|
||||
#expect(try await gateway.summary().working == 3)
|
||||
#expect(try await gateway.feed().working.contains { $0.id == item.id })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user