-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWidgetSyncEventHandlerTests.swift
More file actions
372 lines (320 loc) · 11.2 KB
/
Copy pathWidgetSyncEventHandlerTests.swift
File metadata and controls
372 lines (320 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//
// WidgetSyncEventHandlerTests.swift
// DevLogDataTests
//
// Created by opfic on 4/30/26.
//
import Foundation
import Testing
import DevLogCore
import DevLogDomain
@testable import DevLogData
struct WidgetSyncEventHandlerTests {
@Test("위젯 동기화 요청 이벤트는 Today와 Heatmap 스냅샷을 갱신한다")
func 위젯_동기화_요청_이벤트는_today와_heatmap_스냅샷을_갱신한다() async throws {
let calendar = Calendar.current
let now = Date()
let quarterStart = calendar.startOfQuarter(for: now)
let fixture = makeFixture(calendar: calendar)
await fixture.todoRepository.setTodos(
todayTodosWithDueDate: [
makeTodo(id: "today", createdAt: now, dueDate: now)
],
createdTodos: [
makeTodo(id: "created", createdAt: now)
],
completedTodos: [
makeTodo(id: "completed", createdAt: quarterStart, completedAt: now)
],
deletedTodos: [
makeTodo(id: "deleted", createdAt: quarterStart, deletedAt: now)
]
)
fixture.bus.publish(.syncRequested)
try await waitUntil {
fixture.snapshotUpdater.hasTodayUpdate && fixture.snapshotUpdater.hasHeatmapUpdate
}
let todayUpdates = fixture.snapshotUpdater.todayUpdates
let heatmapUpdates = fixture.snapshotUpdater.heatmapUpdates
let queries = await fixture.todoRepository.calledQueries()
#expect(todayUpdates.first?.todos.map(\.id) == ["today"])
#expect(heatmapUpdates.first?.createdTodos.map(\.id) == ["created"])
#expect(heatmapUpdates.first?.completedTodos.map(\.id) == ["completed"])
#expect(heatmapUpdates.first?.deletedTodos.map(\.id) == ["deleted"])
#expect(todayUpdates.first?.now == heatmapUpdates.first?.now)
#expect(queries.count == 5)
#expect(Set(queries.map(\.sortTarget)) == Set([
.dueDate,
.updatedAt,
.createdAt,
.completedAt,
.deletedAt
]))
_ = fixture.handler
}
@Test("Today 스냅샷 조회 실패는 Heatmap 스냅샷 갱신을 막지 않는다")
func today_스냅샷_조회_실패는_heatmap_스냅샷_갱신을_막지_않는다() async throws {
let calendar = Calendar.current
let now = Date()
let quarterStart = calendar.startOfQuarter(for: now)
let fixture = makeFixture(calendar: calendar)
await fixture.todoRepository.setTodos(
createdTodos: [
makeTodo(id: "created", createdAt: now)
],
completedTodos: [
makeTodo(id: "completed", createdAt: quarterStart, completedAt: now)
],
deletedTodos: [
makeTodo(id: "deleted", createdAt: quarterStart, deletedAt: now)
]
)
await fixture.todoRepository.setFailingSortTargets([.dueDate])
fixture.bus.publish(.syncRequested)
try await waitUntil {
fixture.snapshotUpdater.hasHeatmapUpdate
}
#expect(fixture.snapshotUpdater.todayUpdates.isEmpty)
#expect(fixture.snapshotUpdater.heatmapUpdates.first?.createdTodos.map(\.id) == ["created"])
#expect(fixture.snapshotUpdater.heatmapUpdates.first?.completedTodos.map(\.id) == ["completed"])
#expect(fixture.snapshotUpdater.heatmapUpdates.first?.deletedTodos.map(\.id) == ["deleted"])
_ = fixture.handler
}
@Test("Heatmap 스냅샷 조회 실패는 Today 스냅샷 갱신을 막지 않는다")
func heatmap_스냅샷_조회_실패는_today_스냅샷_갱신을_막지_않는다() async throws {
let calendar = Calendar.current
let now = Date()
let fixture = makeFixture(calendar: calendar)
await fixture.todoRepository.setTodos(
todayTodosWithDueDate: [
makeTodo(id: "today", createdAt: now, dueDate: now)
]
)
await fixture.todoRepository.setFailingSortTargets([.createdAt])
fixture.bus.publish(.syncRequested)
try await waitUntil {
fixture.snapshotUpdater.hasTodayUpdate
}
#expect(fixture.snapshotUpdater.todayUpdates.first?.todos.map(\.id) == ["today"])
#expect(fixture.snapshotUpdater.heatmapUpdates.isEmpty)
_ = fixture.handler
}
private func makeFixture(calendar: Calendar) -> Fixture {
let bus = WidgetSyncEventBusImpl()
let todoRepository = WidgetSyncTodoRepositorySpy()
let snapshotUpdater = WidgetSnapshotUpdaterSpy()
let handler = WidgetSyncEventHandler(
eventBus: bus,
repository: todoRepository,
snapshotUpdater: snapshotUpdater
)
return Fixture(
bus: bus,
todoRepository: todoRepository,
snapshotUpdater: snapshotUpdater,
handler: handler
)
}
private func makeTodo(
id: String,
createdAt: Date,
completedAt: Date? = nil,
deletedAt: Date? = nil,
dueDate: Date? = nil
) -> Todo {
Todo(
id: id,
isPinned: false,
isCompleted: completedAt != nil,
isChecked: false,
number: 1,
title: id,
content: "",
createdAt: createdAt,
updatedAt: createdAt,
completedAt: completedAt,
deletedAt: deletedAt,
dueDate: dueDate,
tags: [],
category: .system(.feature)
)
}
}
private struct Fixture {
let bus: WidgetSyncEventBusImpl
let todoRepository: WidgetSyncTodoRepositorySpy
let snapshotUpdater: WidgetSnapshotUpdaterSpy
let handler: WidgetSyncEventHandler
}
private actor WidgetSyncTodoRepositorySpy: TodoRepository {
private var queries = [TodoQuery]()
private var failingSortTargets = Set<TodoQuery.SortTarget>()
private var todayTodosWithDueDate = [Todo]()
private var todayTodosWithoutDueDate = [Todo]()
private var createdTodos = [Todo]()
private var completedTodos = [Todo]()
private var deletedTodos = [Todo]()
func setTodos(
todayTodosWithDueDate: [Todo] = [],
todayTodosWithoutDueDate: [Todo] = [],
createdTodos: [Todo] = [],
completedTodos: [Todo] = [],
deletedTodos: [Todo] = []
) {
self.todayTodosWithDueDate = todayTodosWithDueDate
self.todayTodosWithoutDueDate = todayTodosWithoutDueDate
self.createdTodos = createdTodos
self.completedTodos = completedTodos
self.deletedTodos = deletedTodos
}
func setFailingSortTargets(_ failingSortTargets: Set<TodoQuery.SortTarget>) {
self.failingSortTargets = failingSortTargets
}
func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
queries.append(query)
if failingSortTargets.contains(query.sortTarget) {
throw WidgetSyncTodoRepositorySpyError.fetchTodosFailed
}
let items: [Todo]
switch query.sortTarget {
case .dueDate:
items = todayTodosWithDueDate
case .updatedAt:
items = todayTodosWithoutDueDate
case .createdAt:
items = createdTodos
case .completedAt:
items = completedTodos
case .deletedAt:
items = deletedTodos
}
return TodoPage(items: items, nextCursor: nil)
}
func fetchTodo(_ todoId: String) async throws -> Todo {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func upsertTodo(_ todo: Todo) async throws {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func upsertTodo(_ todoDraft: TodoDraft) async throws {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func deleteTodo(_ todoId: String) async throws {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func undoDeleteTodo(_ todoId: String) async throws {
throw WidgetSyncTodoRepositorySpyError.unexpectedCall
}
func calledQueries() -> [TodoQuery] {
queries
}
}
private final class WidgetSnapshotUpdaterSpy: WidgetSnapshotUpdater {
struct TodayUpdate {
let todos: [WidgetTodoSnapshot]
let displayOptions: TodayDisplayOptions?
let now: Date
}
struct HeatmapUpdate {
let createdTodos: [WidgetTodoSnapshot]
let completedTodos: [WidgetTodoSnapshot]
let deletedTodos: [WidgetTodoSnapshot]
let quarterStart: Date
let now: Date
}
private let lock = NSRecursiveLock()
private var storedTodayUpdates = [TodayUpdate]()
private var storedHeatmapUpdates = [HeatmapUpdate]()
private(set) var clearCallCount = 0
var todayUpdates: [TodayUpdate] {
lock.lock()
defer { lock.unlock() }
return storedTodayUpdates
}
var heatmapUpdates: [HeatmapUpdate] {
lock.lock()
defer { lock.unlock() }
return storedHeatmapUpdates
}
var hasTodayUpdate: Bool {
!todayUpdates.isEmpty
}
var hasHeatmapUpdate: Bool {
!heatmapUpdates.isEmpty
}
func updateTodaySnapshot(
todos: [WidgetTodoSnapshot],
now: Date
) {
appendTodayUpdate(
TodayUpdate(
todos: todos,
displayOptions: nil,
now: now
)
)
}
func updateTodaySnapshot(
todos: [WidgetTodoSnapshot],
displayOptions: TodayDisplayOptions,
now: Date
) {
appendTodayUpdate(
TodayUpdate(
todos: todos,
displayOptions: displayOptions,
now: now
)
)
}
func updateHeatmapSnapshot(
createdTodos: [WidgetTodoSnapshot],
completedTodos: [WidgetTodoSnapshot],
deletedTodos: [WidgetTodoSnapshot],
quarterStart: Date,
now: Date
) {
appendHeatmapUpdate(
HeatmapUpdate(
createdTodos: createdTodos,
completedTodos: completedTodos,
deletedTodos: deletedTodos,
quarterStart: quarterStart,
now: now
)
)
}
func clear() {
lock.lock()
defer { lock.unlock() }
clearCallCount += 1
}
private func appendTodayUpdate(_ update: TodayUpdate) {
lock.lock()
defer { lock.unlock() }
storedTodayUpdates.append(update)
}
private func appendHeatmapUpdate(_ update: HeatmapUpdate) {
lock.lock()
defer { lock.unlock() }
storedHeatmapUpdates.append(update)
}
}
private enum WidgetSyncTodoRepositorySpyError: Error {
case fetchTodosFailed
case unexpectedCall
}
private func waitUntil(
timeout: Duration = .seconds(1),
pollInterval: Duration = .milliseconds(20),
_ condition: @escaping () -> Bool
) async throws {
let continuousClock = ContinuousClock()
let deadline = continuousClock.now + timeout
while !condition() && continuousClock.now < deadline {
try await Task.sleep(for: pollInterval)
}
}