Skip to content

Commit e3faaeb

Browse files
committed
test: 위젯 placeholder 날짜 배치 검증 추가
1 parent 000e4e0 commit e3faaeb

5 files changed

Lines changed: 313 additions & 0 deletions

DevLog_Unit/Widget/HeatmapWidgetSnapshotFactoryTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,73 @@ struct HeatmapWidgetSnapshotFactoryTests {
126126
#expect(targetDay.deletedCount == 3)
127127
}
128128

129+
@Test("Heatmap 위젯 스냅샷은 분기 시작일은 포함하고 다음 분기 시작일은 제외한다")
130+
func heatmap_위젯_스냅샷은_분기_시작일은_포함하고_다음_분기_시작일은_제외한다() throws {
131+
let calendar = Calendar(identifier: .gregorian)
132+
let quarterStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 1)))
133+
let nextQuarterStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 7, day: 1)))
134+
let quarterLastDate = try #require(calendar.date(byAdding: .second, value: -1, to: nextQuarterStart))
135+
let factory = HeatmapWidgetSnapshotFactory(calendar: calendar)
136+
137+
let snapshot = factory.makeSnapshot(
138+
createdTodos: [
139+
makeTodo(id: "created-quarter-start", createdAt: quarterStart),
140+
makeTodo(id: "created-next-quarter-start", createdAt: nextQuarterStart)
141+
],
142+
completedTodos: [
143+
makeTodo(
144+
id: "completed-quarter-last-date",
145+
createdAt: quarterStart,
146+
completedAt: quarterLastDate
147+
)
148+
],
149+
deletedTodos: [],
150+
selectedActivityKinds: [.created, .completed],
151+
quarterStart: quarterStart,
152+
now: quarterStart
153+
)
154+
155+
let aprilFirst = try #require(day(for: DateComponents(year: 2026, month: 4, day: 1), in: snapshot, calendar: calendar))
156+
let juneThirtieth = try #require(day(for: DateComponents(year: 2026, month: 6, day: 30), in: snapshot, calendar: calendar))
157+
158+
#expect(aprilFirst.createdCount == 1)
159+
#expect(juneThirtieth.completedCount == 1)
160+
#expect(day(for: DateComponents(year: 2026, month: 7, day: 1), in: snapshot, calendar: calendar) == nil)
161+
#expect(snapshot.maxCount == 1)
162+
}
163+
164+
@Test("Heatmap 위젯 스냅샷은 Q4 분기를 다음 해 1월 전까지 만든다")
165+
func heatmap_위젯_스냅샷은_q4_분기를_다음_해_1월_전까지_만든다() throws {
166+
let calendar = Calendar(identifier: .gregorian)
167+
let q4Date = try #require(calendar.date(from: DateComponents(year: 2026, month: 11, day: 10)))
168+
let octoberStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 10, day: 1)))
169+
let novemberStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 11, day: 1)))
170+
let decemberStart = try #require(calendar.date(from: DateComponents(year: 2026, month: 12, day: 1)))
171+
let decemberLastDate = try #require(calendar.date(from: DateComponents(year: 2026, month: 12, day: 31)))
172+
let nextYearStart = try #require(calendar.date(from: DateComponents(year: 2027, month: 1, day: 1)))
173+
let factory = HeatmapWidgetSnapshotFactory(calendar: calendar)
174+
175+
let snapshot = factory.makeSnapshot(
176+
createdTodos: [
177+
makeTodo(id: "created-december-last-date", createdAt: decemberLastDate),
178+
makeTodo(id: "created-next-year-start", createdAt: nextYearStart)
179+
],
180+
completedTodos: [],
181+
deletedTodos: [],
182+
selectedActivityKinds: [.created],
183+
quarterStart: q4Date,
184+
now: q4Date
185+
)
186+
187+
let decemberLastDay = try #require(day(for: DateComponents(year: 2026, month: 12, day: 31), in: snapshot, calendar: calendar))
188+
189+
#expect(snapshot.quarterStart == octoberStart)
190+
#expect(snapshot.months.map(\.monthStart) == [octoberStart, novemberStart, decemberStart])
191+
#expect(decemberLastDay.createdCount == 1)
192+
#expect(day(for: DateComponents(year: 2027, month: 1, day: 1), in: snapshot, calendar: calendar) == nil)
193+
#expect(snapshot.maxCount == 1)
194+
}
195+
129196
private func day(
130197
for components: DateComponents,
131198
in snapshot: HeatmapWidgetSnapshot,

DevLog_Unit/Widget/TodayWidgetSnapshotFactoryTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,67 @@ struct TodayWidgetSnapshotFactoryTests {
5757
#expect(snapshot.sections[0].items.map(\.title) == ["고정된 할 일"])
5858
}
5959

60+
@Test("Today 위젯 스냅샷은 날짜 경계에 따라 일정 섹션을 구분한다")
61+
func today_위젯_스냅샷은_날짜_경계에_따라_일정_섹션을_구분한다() throws {
62+
let calendar = Calendar(identifier: .gregorian)
63+
let now = try #require(calendar.date(from: DateComponents(year: 2026, month: 4, day: 17, hour: 12)))
64+
let yesterday = try #require(calendar.date(byAdding: .day, value: -1, to: now))
65+
let sevenDaysLater = try #require(calendar.date(byAdding: .day, value: 7, to: now))
66+
let eightDaysLater = try #require(calendar.date(byAdding: .day, value: 8, to: now))
67+
let factory = TodayWidgetSnapshotFactory(calendar: calendar)
68+
69+
let snapshot = factory.makeSnapshot(
70+
todos: try [
71+
makeTodayTodoItem(
72+
id: "todo-overdue",
73+
number: 1,
74+
title: "지난 일정",
75+
isPinned: false,
76+
dueDate: yesterday
77+
),
78+
makeTodayTodoItem(
79+
id: "todo-today",
80+
number: 2,
81+
title: "오늘 일정",
82+
isPinned: false,
83+
dueDate: now
84+
),
85+
makeTodayTodoItem(
86+
id: "todo-seven-days-later",
87+
number: 3,
88+
title: "7일 뒤 일정",
89+
isPinned: false,
90+
dueDate: sevenDaysLater
91+
),
92+
makeTodayTodoItem(
93+
id: "todo-eight-days-later",
94+
number: 4,
95+
title: "8일 뒤 일정",
96+
isPinned: false,
97+
dueDate: eightDaysLater
98+
),
99+
makeTodayTodoItem(
100+
id: "todo-unscheduled",
101+
number: 5,
102+
title: "미정 일정",
103+
isPinned: false,
104+
dueDate: nil
105+
)
106+
],
107+
displayOptions: .default,
108+
now: now
109+
)
110+
111+
#expect(snapshot.totalCount == 5)
112+
#expect(snapshot.overdueCount == 1)
113+
#expect(snapshot.dueSoonCount == 2)
114+
#expect(snapshot.sections.map(\.category) == ["overdue", "dueSoon", "later", "unscheduled"])
115+
#expect(snapshot.sections[0].items.map(\.title) == ["지난 일정"])
116+
#expect(snapshot.sections[1].items.map(\.title) == ["오늘 일정", "7일 뒤 일정"])
117+
#expect(snapshot.sections[2].items.map(\.title) == ["8일 뒤 일정"])
118+
#expect(snapshot.sections[3].items.map(\.title) == ["미정 일정"])
119+
}
120+
60121
private func makeTodayTodos(
61122
now: Date,
62123
calendar: Calendar
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// WidgetHeatmapPlaceholderShapeTests.swift
3+
// DevLog_Unit
4+
//
5+
// Created by opfic on 4/30/26.
6+
//
7+
8+
import Foundation
9+
import Testing
10+
@testable import DevLog
11+
12+
struct WidgetHeatmapPlaceholderShapeTests {
13+
@Test("Heatmap 위젯 placeholder는 현재 월과 분기의 실제 날짜 위치를 사용한다")
14+
func heatmap_위젯_placeholder는_현재_월과_분기의_실제_날짜_위치를_사용한다() throws {
15+
let calendar = Calendar(identifier: .gregorian)
16+
let date = try #require(calendar.date(from: DateComponents(year: 2026, month: 5, day: 15)))
17+
18+
let widgetHeatmapPlaceholderShape = WidgetHeatmapPlaceholderShape(
19+
date: date,
20+
calendar: calendar
21+
)
22+
23+
#expect(widgetHeatmapPlaceholderShape.currentMonthWeekCounts == [6])
24+
#expect(widgetHeatmapPlaceholderShape.quarterWeekCounts == [5, 6, 5])
25+
26+
let currentMonth = try #require(widgetHeatmapPlaceholderShape.currentMonths.first)
27+
#expect(currentMonth.weeks.count == 6)
28+
#expect(currentMonth.weeks[0].days.map(\.isVisible) == [
29+
false,
30+
false,
31+
false,
32+
false,
33+
false,
34+
true,
35+
true
36+
])
37+
#expect(currentMonth.weeks[5].days.map(\.isVisible) == [
38+
true,
39+
false,
40+
false,
41+
false,
42+
false,
43+
false,
44+
false
45+
])
46+
47+
let quarterMonths = widgetHeatmapPlaceholderShape.quarterMonths
48+
#expect(quarterMonths.map(\.weeks.count) == [5, 6, 5])
49+
#expect(quarterMonths[0].weeks[0].days.map(\.isVisible) == [
50+
false,
51+
false,
52+
false,
53+
true,
54+
true,
55+
true,
56+
true
57+
])
58+
#expect(quarterMonths[2].weeks[4].days.map(\.isVisible) == [
59+
true,
60+
true,
61+
true,
62+
false,
63+
false,
64+
false,
65+
false
66+
])
67+
}
68+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// WidgetSnapshotPreferenceStoreTests.swift
3+
// DevLog_Unit
4+
//
5+
// Created by opfic on 4/30/26.
6+
//
7+
8+
import Foundation
9+
import Testing
10+
@testable import DevLog
11+
12+
struct WidgetSnapshotPreferenceStoreTests {
13+
@Test("Heatmap activity kind 설정이 비어 있으면 전체 kind를 사용한다")
14+
func heatmap_activity_kind_설정이_비어_있으면_전체_kind를_사용한다() {
15+
let widgetSnapshotPreferenceStore = makeStore()
16+
17+
#expect(widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .completed, .deleted]))
18+
}
19+
20+
@Test("Heatmap activity kind 설정에 유효하지 않은 값만 있으면 전체 kind를 사용한다")
21+
func heatmap_activity_kind_설정에_유효하지_않은_값만_있으면_전체_kind를_사용한다() {
22+
let widgetSnapshotPreferenceStore = makeStore()
23+
24+
widgetSnapshotPreferenceStore.setHeatmapActivityTypes(["unknown"])
25+
26+
#expect(widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .completed, .deleted]))
27+
}
28+
29+
@Test("Heatmap activity kind 설정은 유효한 값만 유지한다")
30+
func heatmap_activity_kind_설정은_유효한_값만_유지한다() {
31+
let widgetSnapshotPreferenceStore = makeStore()
32+
33+
widgetSnapshotPreferenceStore.setHeatmapActivityTypes(["created", "unknown", "deleted", "created"])
34+
35+
#expect(widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .deleted]))
36+
}
37+
38+
@Test("Today display option 설정이 깨져 있으면 기본 옵션을 사용한다")
39+
func today_display_option_설정이_깨져_있으면_기본_옵션을_사용한다() {
40+
let suiteName = "WidgetSnapshotPreferenceStoreTests.\(UUID().uuidString)"
41+
let userDefaults = UserDefaults(suiteName: suiteName) ?? .standard
42+
userDefaults.removePersistentDomain(forName: suiteName)
43+
let widgetSnapshotPreferenceStore = WidgetSnapshotPreferenceStore(userDefaults: userDefaults)
44+
45+
userDefaults.set("invalid", forKey: "Today.dueDateVisibility")
46+
userDefaults.set("invalid", forKey: "Today.focusVisibility")
47+
48+
#expect(widgetSnapshotPreferenceStore.todayDisplayOptions() == .default)
49+
}
50+
51+
private func makeStore() -> WidgetSnapshotPreferenceStore {
52+
let suiteName = "WidgetSnapshotPreferenceStoreTests.\(UUID().uuidString)"
53+
let userDefaults = UserDefaults(suiteName: suiteName) ?? .standard
54+
userDefaults.removePersistentDomain(forName: suiteName)
55+
return WidgetSnapshotPreferenceStore(userDefaults: userDefaults)
56+
}
57+
}

DevLog_Unit/Widget/WidgetSyncEventHandlerTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,57 @@ struct WidgetSyncEventHandlerTests {
5151
_ = fixture.handler
5252
}
5353

54+
@Test("Today 스냅샷 조회 실패는 Heatmap 스냅샷 갱신을 막지 않는다")
55+
func today_스냅샷_조회_실패는_heatmap_스냅샷_갱신을_막지_않는다() async throws {
56+
let calendar = Calendar.current
57+
let now = Date()
58+
let quarterStart = calendar.startOfQuarter(for: now)
59+
let fixture = makeFixture(calendar: calendar)
60+
61+
await fixture.todoRepository.setTodos(
62+
createdTodos: [
63+
makeTodo(id: "created", createdAt: now)
64+
],
65+
completedTodos: [
66+
makeTodo(id: "completed", createdAt: quarterStart, completedAt: now)
67+
],
68+
deletedTodos: [
69+
makeTodo(id: "deleted", createdAt: quarterStart, deletedAt: now)
70+
]
71+
)
72+
await fixture.todoRepository.setFailingSortTargets([.dueDate])
73+
74+
fixture.bus.publish(.syncRequested)
75+
76+
let heatmapSnapshot = try await loadHeatmapSnapshot(from: fixture.snapshotStore)
77+
78+
#expect(heatmapSnapshot.maxCount == 3)
79+
#expect(try fixture.snapshotStore.loadTodaySnapshot() == nil)
80+
_ = fixture.handler
81+
}
82+
83+
@Test("Heatmap 스냅샷 조회 실패는 Today 스냅샷 갱신을 막지 않는다")
84+
func heatmap_스냅샷_조회_실패는_today_스냅샷_갱신을_막지_않는다() async throws {
85+
let calendar = Calendar.current
86+
let now = Date()
87+
let fixture = makeFixture(calendar: calendar)
88+
89+
await fixture.todoRepository.setTodos(
90+
todayTodosWithDueDate: [
91+
makeTodo(id: "today", createdAt: now, dueDate: now)
92+
]
93+
)
94+
await fixture.todoRepository.setFailingSortTargets([.createdAt])
95+
96+
fixture.bus.publish(.syncRequested)
97+
98+
let todaySnapshot = try await loadTodaySnapshot(from: fixture.snapshotStore)
99+
100+
#expect(todaySnapshot.totalCount == 1)
101+
#expect(try fixture.snapshotStore.loadHeatmapSnapshot() == nil)
102+
_ = fixture.handler
103+
}
104+
54105
private func makeFixture(
55106
calendar: Calendar
56107
) -> (
@@ -139,6 +190,7 @@ struct WidgetSyncEventHandlerTests {
139190

140191
private actor WidgetSyncTodoRepositorySpy: TodoRepository {
141192
private var queries = [TodoQuery]()
193+
private var failingSortTargets = Set<TodoQuery.SortTarget>()
142194
private var todayTodosWithDueDate = [Todo]()
143195
private var todayTodosWithoutDueDate = [Todo]()
144196
private var createdTodos = [Todo]()
@@ -159,9 +211,17 @@ private actor WidgetSyncTodoRepositorySpy: TodoRepository {
159211
self.deletedTodos = deletedTodos
160212
}
161213

214+
func setFailingSortTargets(_ failingSortTargets: Set<TodoQuery.SortTarget>) {
215+
self.failingSortTargets = failingSortTargets
216+
}
217+
162218
func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
163219
queries.append(query)
164220

221+
if failingSortTargets.contains(query.sortTarget) {
222+
throw DataError.invalidData("WidgetSyncTodoRepositorySpy.fetchTodos failed")
223+
}
224+
165225
let items: [Todo]
166226
switch query.sortTarget {
167227
case .dueDate:

0 commit comments

Comments
 (0)