krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgAppTests/AgendaSnapshotTests.swift

2fa201330f61c801f777baa3f2943d49d8758cda
orgstar/Tests/OrgAppTests/AgendaSnapshotTests.swift history · blame · raw

122 lines · 5948 bytes

  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}