Skip to content

Commit 1998151

Browse files
authored
[#400] 위젯 예외 케이스 테스트를 보강한다 (#405)
* ui: 실제 히트맵과 동일한 형태의 플레이드홀더 형태로 수정 * test: 위젯 placeholder 날짜 배치 검증 추가 * test: 위젯 설정 테스트 중복 제거
1 parent 41ff16a commit 1998151

9 files changed

Lines changed: 464 additions & 12 deletions

DevLogWidget/Heatmap/HeatmapWidgetEntryView.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,22 @@ struct HeatmapWidgetEntryView: View {
5353

5454
@ViewBuilder
5555
private var emptyState: some View {
56+
let shape = WidgetHeatmapPlaceholderShape(date: entry.date)
57+
5658
switch widgetFamily {
5759
case .systemSmall:
5860
VStack(alignment: .leading, spacing: 8) {
59-
Text("이번 달 히트맵")
60-
.font(.headline)
61+
header(title: "이번 달 히트맵")
6162
WidgetHeatmapPlaceholderGrid(
62-
weekCounts: [5],
63+
months: shape.currentMonths,
6364
showsMonthTitles: false
6465
)
6566
}
6667
case .systemMedium:
6768
VStack(alignment: .leading, spacing: 8) {
6869
header(title: "이번 분기 히트맵")
6970
WidgetHeatmapPlaceholderGrid(
70-
weekCounts: [5, 5, 5],
71+
months: shape.quarterMonths,
7172
showsMonthTitles: true
7273
)
7374
}

DevLogWidget/Heatmap/WidgetHeatmapGrid.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ struct WidgetHeatmapGrid: View {
4343
}
4444

4545
struct WidgetHeatmapPlaceholderGrid: View {
46-
let weekCounts: [Int]
46+
let months: [WidgetHeatmapPlaceholderMonthShape]
4747
let showsMonthTitles: Bool
4848

4949
var body: some View {
50+
let weekCounts = months.map(\.weeks.count)
51+
5052
GeometryReader { proxy in
5153
let layout = WidgetHeatmapLayout(
5254
availableWidth: proxy.size.width,
@@ -56,9 +58,9 @@ struct WidgetHeatmapPlaceholderGrid: View {
5658
)
5759

5860
HStack(alignment: .top, spacing: layout.monthSpacing) {
59-
ForEach(Array(weekCounts.enumerated()), id: \.offset) { _, weekCount in
61+
ForEach(months) { month in
6062
WidgetHeatmapPlaceholderMonthGrid(
61-
weekCount: weekCount,
63+
month: month,
6264
layout: layout,
6365
showsMonthTitle: showsMonthTitles
6466
)
@@ -187,7 +189,7 @@ private enum WidgetHeatmapActivityKind: String {
187189
}
188190

189191
private struct WidgetHeatmapPlaceholderMonthGrid: View {
190-
let weekCount: Int
192+
let month: WidgetHeatmapPlaceholderMonthShape
191193
let layout: WidgetHeatmapLayout
192194
let showsMonthTitle: Bool
193195
private let orderedWeekdays = Array(1...7)
@@ -204,9 +206,13 @@ private struct WidgetHeatmapPlaceholderMonthGrid: View {
204206
VStack(alignment: .leading, spacing: layout.cellSpacing) {
205207
ForEach(orderedWeekdays, id: \.self) { weekday in
206208
HStack(spacing: layout.cellSpacing) {
207-
ForEach(0..<weekCount, id: \.self) { weekIndex in
209+
ForEach(month.weeks) { week in
210+
let day = week.days.first {
211+
Calendar.current.component(.weekday, from: $0.date) == weekday
212+
}
213+
208214
RoundedRectangle(cornerRadius: layout.cellCornerRadius)
209-
.fill(Color.secondary.opacity(opacity(weekday: weekday, weekIndex: weekIndex)))
215+
.fill(fillColor(for: day))
210216
.frame(width: layout.cellSize, height: layout.cellSize)
211217
}
212218
}
@@ -215,8 +221,13 @@ private struct WidgetHeatmapPlaceholderMonthGrid: View {
215221
}
216222
}
217223

218-
private func opacity(weekday: Int, weekIndex: Int) -> Double {
219-
switch (weekday + weekIndex) % 4 {
224+
private func fillColor(for day: WidgetHeatmapPlaceholderDayShape?) -> Color {
225+
guard let day, day.isVisible else { return .clear }
226+
return Color.secondary.opacity(opacity(for: day))
227+
}
228+
229+
private func opacity(for day: WidgetHeatmapPlaceholderDayShape) -> Double {
230+
switch Calendar.current.component(.day, from: day.date) % 4 {
220231
case 0:
221232
return 1 / 8
222233
case 1:

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 fixture = makeFixture()
16+
17+
#expect(fixture.widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .completed, .deleted]))
18+
}
19+
20+
@Test("Heatmap activity kind 설정에 유효하지 않은 값만 있으면 전체 kind를 사용한다")
21+
func heatmap_activity_kind_설정에_유효하지_않은_값만_있으면_전체_kind를_사용한다() {
22+
let fixture = makeFixture()
23+
24+
fixture.widgetSnapshotPreferenceStore.setHeatmapActivityTypes(["unknown"])
25+
26+
#expect(fixture.widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .completed, .deleted]))
27+
}
28+
29+
@Test("Heatmap activity kind 설정은 유효한 값만 유지한다")
30+
func heatmap_activity_kind_설정은_유효한_값만_유지한다() {
31+
let fixture = makeFixture()
32+
33+
fixture.widgetSnapshotPreferenceStore.setHeatmapActivityTypes(["created", "unknown", "deleted", "created"])
34+
35+
#expect(fixture.widgetSnapshotPreferenceStore.selectedActivityKinds() == Set([.created, .deleted]))
36+
}
37+
38+
@Test("Today display option 설정이 깨져 있으면 기본 옵션을 사용한다")
39+
func today_display_option_설정이_깨져_있으면_기본_옵션을_사용한다() {
40+
let fixture = makeFixture()
41+
42+
fixture.userDefaults.set("invalid", forKey: "Today.dueDateVisibility")
43+
fixture.userDefaults.set("invalid", forKey: "Today.focusVisibility")
44+
45+
#expect(fixture.widgetSnapshotPreferenceStore.todayDisplayOptions() == .default)
46+
}
47+
48+
private func makeFixture() -> (
49+
widgetSnapshotPreferenceStore: WidgetSnapshotPreferenceStore,
50+
userDefaults: UserDefaults
51+
) {
52+
let suiteName = "WidgetSnapshotPreferenceStoreTests.\(UUID().uuidString)"
53+
let userDefaults = UserDefaults(suiteName: suiteName) ?? .standard
54+
userDefaults.removePersistentDomain(forName: suiteName)
55+
let widgetSnapshotPreferenceStore = WidgetSnapshotPreferenceStore(userDefaults: userDefaults)
56+
return (widgetSnapshotPreferenceStore, userDefaults)
57+
}
58+
}

0 commit comments

Comments
 (0)