Tests/OrgAppTests/AgendaSnapshotTests.swift
122 lines · 5948 bytes
9 symbols in this file
1import Foundation
2import OrgCore
3import OrgPresentation
4import Testing
5@testable import OrgApp
6
7struct AgendaSnapshotTests {
8 static let file = """
9 #+CATEGORY: life
10 * TODO [#A] Send forms
11 DEADLINE: <2026-10-02 Fri>
12 * TODO Water plants
13 SCHEDULED: <2026-10-05 Mon>
14 * DONE Took out bins
15 SCHEDULED: <2026-10-05 Mon>
16 * Standup
17 SCHEDULED: <2026-10-05 Mon 08:00>
18 * Dentist <2026-10-05 Mon 13:50-14:30>
19 * NEXT Call the bank
20 SCHEDULED: <2026-10-06 Tue 09:30>
21 * Yesterday's note <2026-10-04 Sun>
22 * Far off <2026-10-20 Tue>
23 """
24
25 static let today = Days.absolute(year: 2026, month: 10, day: 5)
26 static let utc: Calendar = {
27 var c = Calendar(identifier: .gregorian)
28 c.timeZone = TimeZone(identifier: "UTC")!
29 return c
30 }()
31
32 static func date(_ day: Int, _ hour: Int, _ minute: Int = 0) -> Date {
33 Date(timeIntervalSince1970: Double((day - 719163) * 86400 + hour * 3600 + minute * 60))
34 }
35
36 /// The agenda from three days before today for three weeks, as the app's view may be.
37 static func snapshot() -> AgendaSnapshot {
38 let source = AgendaSource(path: "/notes/life.org", text: file, defaults: OrgSettings.default.withTodoKeywords("TODO NEXT | DONE")!)
39 let days = Agenda.list([source], start: today - 3, days: 21, today: today, now: (10, 0))
40 let event = AgendaEvent(
41 id: "e1", title: "Lunch", calendarID: "c", calendarTitle: "Home", color: RGBA(hex: "#336699"),
42 start: date(today, 12), end: date(today, 13), isAllDay: false
43 )
44 let groups = AgendaRows.groups(days, events: [event], today: today, calendar: utc)
45 return AgendaSnapshot(groups: groups, today: today, theme: .default, generated: date(today, 10), calendar: utc)
46 }
47
48 @Test func holdsTodayAndTheNextDaysOnly() {
49 let snapshot = Self.snapshot()
50 #expect(snapshot.days.map(\.day) == [Self.today, Self.today + 1])
51 let today = snapshot.days[0]
52 #expect(today.date == Self.date(Self.today, 0))
53 #expect(today.overdue.map(\.title) == ["Send forms"])
54 #expect(today.rows.map(\.title) == ["Standup", "Lunch", "Dentist", "Water plants", "Took out bins"])
55 #expect(snapshot.days[1].rows.map(\.title) == ["Call the bank"])
56 }
57
58 @Test func rowsCarryTimesLinksAndColors() throws {
59 let snapshot = Self.snapshot()
60 let rows = snapshot.days[0].rows + snapshot.days[0].overdue
61 let dentist = try #require(rows.first { $0.title == "Dentist" })
62 #expect(dentist.gutter == "13:50–14:30")
63 #expect(dentist.start == Self.date(Self.today, 13, 50))
64 #expect(dentist.end == Self.date(Self.today, 14, 30))
65 #expect(dentist.path == "/notes/life.org")
66 #expect(dentist.link == .entry(path: "/notes/life.org", offset: dentist.offset!))
67 let forms = try #require(rows.first { $0.title == "Send forms" })
68 #expect(forms.status == "overdue")
69 #expect(forms.todo == "TODO")
70 #expect(forms.priority == "A")
71 #expect(forms.todoColor == .init(light: "#d70015", dark: "#ff6961"))
72 #expect(forms.priorityColor == .init(light: "#d70015", dark: "#ff6961"))
73 #expect(forms.statusColor == .init(light: "#d70015", dark: "#ff6961"))
74 let lunch = try #require(rows.first { $0.title == "Lunch" })
75 #expect(lunch.isEvent && lunch.link == nil)
76 #expect(lunch.categoryColor == .init(light: "#336699", dark: "#336699"))
77 #expect(snapshot.colors["agenda-today"] == .init(light: "#007aff", dark: "#0a84ff"))
78 #expect(snapshot.colors["agenda-background"] == nil)
79 }
80
81 @Test func encodesAndDecodes() throws {
82 let snapshot = Self.snapshot()
83 let data = try AgendaSnapshot.encoder.encode(snapshot)
84 #expect(try AgendaSnapshot.decoder.decode(AgendaSnapshot.self, from: data) == snapshot)
85 }
86
87 @Test func writesOnlyWhatChanged() throws {
88 let folder = try Folder()
89 let file = folder.url.appendingPathComponent("Widgets/agenda.json")
90 var snapshot = Self.snapshot()
91 #expect(try snapshot.write(to: file))
92 #expect(AgendaSnapshot.read(from: file) == snapshot)
93 snapshot.generated = snapshot.generated.addingTimeInterval(60)
94 #expect(try !snapshot.write(to: file))
95 snapshot.days.removeLast()
96 #expect(try snapshot.write(to: file))
97 #expect(AgendaSnapshot.read(from: file)?.days.count == 1)
98 }
99
100 @Test func whatIsLeftOfToday() {
101 let snapshot = Self.snapshot()
102 let morning = Self.date(Self.today, 7)
103 #expect(snapshot.upcoming(at: morning, calendar: Self.utc).map(\.title) == ["Standup", "Lunch", "Dentist", "Water plants", "Send forms"])
104 #expect(snapshot.upcoming(at: Self.date(Self.today, 14), calendar: Self.utc).map(\.title) == ["Dentist", "Water plants", "Send forms"])
105 #expect(snapshot.openCount(at: morning, calendar: Self.utc) == 4)
106 #expect(snapshot.overdueCount(at: morning, calendar: Self.utc) == 1)
107 #expect(snapshot.day(after: morning, calendar: Self.utc)?.day == Self.today + 1)
108 #expect(snapshot.upcoming(at: Self.date(Self.today + 3, 9), calendar: Self.utc) == [])
109 #expect(snapshot.changes(after: Self.date(Self.today, 12, 30), calendar: Self.utc) == [
110 Self.date(Self.today, 13, 1), Self.date(Self.today, 14, 31), Self.date(Self.today + 1, 0),
111 ])
112 }
113
114 @Test func linksRoundTrip() {
115 for link in [AppLink.agenda, .clock, .entry(path: "/notes/a b&c+d/ünï.org", offset: 42)] {
116 #expect(AppLink(url: link.url) == link)
117 }
118 #expect(AppLink.agenda.url.absoluteString == "orgstar://agenda")
119 #expect(AppLink(url: URL(string: "org-protocol://capture?body=x")!) == nil)
120 #expect(AppLink(url: URL(string: "orgstar://entry?path=/a.org")!) == nil)
121 }
122}