Phases 1-6 of fixes.md — closes all 13 issues from codex_issues_2.md re-validation: KMP Architecture: - Fix subscription purchase/restore response contract (VerificationResponse aligned) - Add feature benefits auth token + APILayer init flow - Remove ResidenceFormScreen direct API bypass (use APILayer) - Wire paywall purchase/restore to real SubscriptionApi calls iOS Platform: - Add iOS Keychain token storage via Swift KeychainHelper - Implement Google Sign-In via ASWebAuthenticationSession (GoogleSignInManager) - DocumentViewModelWrapper observes DataManager for auto-updates - Add missing accessibility identifiers (document, task columns, Google Sign-In) XCUITest Rewrite: - Rewrite test infrastructure: zero sleep() calls, accessibility ID lookups - Create AuthCriticalPathTests and NavigationCriticalPathTests - Delete 14 legacy brittle test files (Suite0-10, templates) - Fix CaseraTests module import (@testable import Casera) All platforms build clean. TEST BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
396 lines
13 KiB
Swift
396 lines
13 KiB
Swift
//
|
|
// TaskMetricsTests.swift
|
|
// CaseraTests
|
|
//
|
|
// Unit tests for WidgetDataManager.TaskMetrics and task categorization logic.
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
@testable import Casera
|
|
|
|
// MARK: - Column Name Constants Tests
|
|
|
|
struct ColumnNameConstantsTests {
|
|
|
|
@Test func overdueColumnName() {
|
|
#expect(WidgetDataManager.overdueColumn == "overdue_tasks")
|
|
}
|
|
|
|
@Test func dueWithin7DaysColumnName() {
|
|
#expect(WidgetDataManager.dueWithin7DaysColumn == "due_soon_tasks")
|
|
}
|
|
|
|
@Test func due8To30DaysColumnName() {
|
|
#expect(WidgetDataManager.due8To30DaysColumn == "upcoming_tasks")
|
|
}
|
|
|
|
@Test func completedColumnName() {
|
|
#expect(WidgetDataManager.completedColumn == "completed_tasks")
|
|
}
|
|
|
|
@Test func cancelledColumnName() {
|
|
#expect(WidgetDataManager.cancelledColumn == "cancelled_tasks")
|
|
}
|
|
|
|
@Test func allColumnNamesUnique() {
|
|
let columns = [
|
|
WidgetDataManager.overdueColumn,
|
|
WidgetDataManager.dueWithin7DaysColumn,
|
|
WidgetDataManager.due8To30DaysColumn,
|
|
WidgetDataManager.completedColumn,
|
|
WidgetDataManager.cancelledColumn
|
|
]
|
|
#expect(Set(columns).count == columns.count)
|
|
}
|
|
}
|
|
|
|
// MARK: - TaskMetrics Calculation Tests
|
|
|
|
struct TaskMetricsCalculationTests {
|
|
|
|
private func makeTask(
|
|
id: Int,
|
|
isOverdue: Bool = false,
|
|
isDueWithin7Days: Bool = false,
|
|
isDue8To30Days: Bool = false
|
|
) -> WidgetDataManager.WidgetTask {
|
|
WidgetDataManager.WidgetTask(
|
|
id: id,
|
|
title: "Task \(id)",
|
|
description: nil,
|
|
priority: "medium",
|
|
inProgress: false,
|
|
dueDate: nil,
|
|
category: "maintenance",
|
|
residenceName: "Home",
|
|
isOverdue: isOverdue,
|
|
isDueWithin7Days: isDueWithin7Days,
|
|
isDue8To30Days: isDue8To30Days
|
|
)
|
|
}
|
|
|
|
@Test func emptyArrayReturnsZeroCounts() {
|
|
let metrics = WidgetDataManager.calculateMetrics(from: [])
|
|
#expect(metrics.totalCount == 0)
|
|
#expect(metrics.overdueCount == 0)
|
|
#expect(metrics.upcoming7Days == 0)
|
|
#expect(metrics.upcoming30Days == 0)
|
|
}
|
|
|
|
@Test func allOverdueTasks() {
|
|
let tasks = [
|
|
makeTask(id: 1, isOverdue: true),
|
|
makeTask(id: 2, isOverdue: true),
|
|
makeTask(id: 3, isOverdue: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 3)
|
|
#expect(metrics.overdueCount == 3)
|
|
#expect(metrics.upcoming7Days == 0)
|
|
#expect(metrics.upcoming30Days == 0)
|
|
}
|
|
|
|
@Test func allDueWithin7DaysTasks() {
|
|
let tasks = [
|
|
makeTask(id: 1, isDueWithin7Days: true),
|
|
makeTask(id: 2, isDueWithin7Days: true),
|
|
makeTask(id: 3, isDueWithin7Days: true),
|
|
makeTask(id: 4, isDueWithin7Days: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 4)
|
|
#expect(metrics.overdueCount == 0)
|
|
#expect(metrics.upcoming7Days == 4)
|
|
#expect(metrics.upcoming30Days == 0)
|
|
}
|
|
|
|
@Test func allDue8To30DaysTasks() {
|
|
let tasks = [
|
|
makeTask(id: 1, isDue8To30Days: true),
|
|
makeTask(id: 2, isDue8To30Days: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 2)
|
|
#expect(metrics.overdueCount == 0)
|
|
#expect(metrics.upcoming7Days == 0)
|
|
#expect(metrics.upcoming30Days == 2)
|
|
}
|
|
|
|
@Test func mixedTaskCategories() {
|
|
let tasks = [
|
|
makeTask(id: 1, isOverdue: true),
|
|
makeTask(id: 2, isOverdue: true),
|
|
makeTask(id: 3, isDueWithin7Days: true),
|
|
makeTask(id: 4, isDueWithin7Days: true),
|
|
makeTask(id: 5, isDueWithin7Days: true),
|
|
makeTask(id: 6, isDue8To30Days: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 6)
|
|
#expect(metrics.overdueCount == 2)
|
|
#expect(metrics.upcoming7Days == 3)
|
|
#expect(metrics.upcoming30Days == 1)
|
|
}
|
|
|
|
@Test func tasksWithNoFlagsCountedInTotalOnly() {
|
|
let tasks = [
|
|
makeTask(id: 1),
|
|
makeTask(id: 2),
|
|
makeTask(id: 3, isOverdue: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 3)
|
|
#expect(metrics.overdueCount == 1)
|
|
#expect(metrics.upcoming7Days == 0)
|
|
#expect(metrics.upcoming30Days == 0)
|
|
}
|
|
|
|
@Test func singleTask() {
|
|
let tasks = [makeTask(id: 1, isOverdue: true)]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 1)
|
|
#expect(metrics.overdueCount == 1)
|
|
}
|
|
|
|
@Test func largeDataset() {
|
|
var tasks: [WidgetDataManager.WidgetTask] = []
|
|
for i in 1...50 { tasks.append(makeTask(id: i, isOverdue: true)) }
|
|
for i in 51...150 { tasks.append(makeTask(id: i, isDueWithin7Days: true)) }
|
|
for i in 151...225 { tasks.append(makeTask(id: i, isDue8To30Days: true)) }
|
|
for i in 226...250 { tasks.append(makeTask(id: i)) }
|
|
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 250)
|
|
#expect(metrics.overdueCount == 50)
|
|
#expect(metrics.upcoming7Days == 100)
|
|
#expect(metrics.upcoming30Days == 75)
|
|
}
|
|
|
|
@Test func orderDoesNotAffectMetrics() {
|
|
let tasks1 = [
|
|
makeTask(id: 1, isOverdue: true),
|
|
makeTask(id: 2, isDueWithin7Days: true),
|
|
makeTask(id: 3, isDue8To30Days: true)
|
|
]
|
|
let tasks2 = [
|
|
makeTask(id: 3, isDue8To30Days: true),
|
|
makeTask(id: 1, isOverdue: true),
|
|
makeTask(id: 2, isDueWithin7Days: true)
|
|
]
|
|
let m1 = WidgetDataManager.calculateMetrics(from: tasks1)
|
|
let m2 = WidgetDataManager.calculateMetrics(from: tasks2)
|
|
#expect(m1.totalCount == m2.totalCount)
|
|
#expect(m1.overdueCount == m2.overdueCount)
|
|
#expect(m1.upcoming7Days == m2.upcoming7Days)
|
|
#expect(m1.upcoming30Days == m2.upcoming30Days)
|
|
}
|
|
}
|
|
|
|
// MARK: - TaskMetrics Struct Tests
|
|
|
|
struct TaskMetricsStructTests {
|
|
|
|
@Test func initializesWithCorrectValues() {
|
|
let metrics = WidgetDataManager.TaskMetrics(
|
|
totalCount: 10,
|
|
overdueCount: 2,
|
|
upcoming7Days: 3,
|
|
upcoming30Days: 5
|
|
)
|
|
#expect(metrics.totalCount == 10)
|
|
#expect(metrics.overdueCount == 2)
|
|
#expect(metrics.upcoming7Days == 3)
|
|
#expect(metrics.upcoming30Days == 5)
|
|
}
|
|
|
|
@Test func allowsZeroValues() {
|
|
let metrics = WidgetDataManager.TaskMetrics(
|
|
totalCount: 0,
|
|
overdueCount: 0,
|
|
upcoming7Days: 0,
|
|
upcoming30Days: 0
|
|
)
|
|
#expect(metrics.totalCount == 0)
|
|
#expect(metrics.overdueCount == 0)
|
|
#expect(metrics.upcoming7Days == 0)
|
|
#expect(metrics.upcoming30Days == 0)
|
|
}
|
|
}
|
|
|
|
// MARK: - WidgetTask Codable Tests
|
|
|
|
struct WidgetTaskCodableTests {
|
|
|
|
@Test func decodesFromJSON() throws {
|
|
let json = """
|
|
{
|
|
"id": 123,
|
|
"title": "Test Task",
|
|
"description": "Desc",
|
|
"priority": "high",
|
|
"in_progress": true,
|
|
"due_date": "2024-12-25",
|
|
"category": "plumbing",
|
|
"residence_name": "Home",
|
|
"is_overdue": false,
|
|
"is_due_within_7_days": true,
|
|
"is_due_8_to_30_days": false
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json)
|
|
#expect(task.id == 123)
|
|
#expect(task.title == "Test Task")
|
|
#expect(task.description == "Desc")
|
|
#expect(task.priority == "high")
|
|
#expect(task.inProgress == true)
|
|
#expect(task.dueDate == "2024-12-25")
|
|
#expect(task.category == "plumbing")
|
|
#expect(task.residenceName == "Home")
|
|
#expect(task.isOverdue == false)
|
|
#expect(task.isDueWithin7Days == true)
|
|
#expect(task.isDue8To30Days == false)
|
|
}
|
|
|
|
@Test func encodesToJSON() throws {
|
|
let task = WidgetDataManager.WidgetTask(
|
|
id: 456,
|
|
title: "Encode Test",
|
|
description: nil,
|
|
priority: "medium",
|
|
inProgress: false,
|
|
dueDate: "2024-06-01",
|
|
category: "hvac",
|
|
residenceName: nil,
|
|
isOverdue: true,
|
|
isDueWithin7Days: false,
|
|
isDue8To30Days: false
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(task)
|
|
let json = String(data: data, encoding: .utf8)!
|
|
#expect(json.contains("\"id\":456"))
|
|
#expect(json.contains("\"is_overdue\":true"))
|
|
#expect(json.contains("\"in_progress\":false"))
|
|
}
|
|
|
|
@Test func roundTripsJSON() throws {
|
|
let original = WidgetDataManager.WidgetTask(
|
|
id: 789,
|
|
title: "Round Trip",
|
|
description: "Testing",
|
|
priority: "low",
|
|
inProgress: true,
|
|
dueDate: "2024-03-15",
|
|
category: "landscaping",
|
|
residenceName: "Cabin",
|
|
isOverdue: false,
|
|
isDueWithin7Days: true,
|
|
isDue8To30Days: false
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(original)
|
|
let decoded = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: data)
|
|
|
|
#expect(decoded.id == original.id)
|
|
#expect(decoded.title == original.title)
|
|
#expect(decoded.description == original.description)
|
|
#expect(decoded.inProgress == original.inProgress)
|
|
#expect(decoded.isOverdue == original.isOverdue)
|
|
#expect(decoded.isDueWithin7Days == original.isDueWithin7Days)
|
|
#expect(decoded.isDue8To30Days == original.isDue8To30Days)
|
|
}
|
|
|
|
@Test func handlesNilOptionalFields() throws {
|
|
let json = """
|
|
{
|
|
"id": 1,
|
|
"title": "Minimal",
|
|
"description": null,
|
|
"priority": null,
|
|
"in_progress": false,
|
|
"due_date": null,
|
|
"category": null,
|
|
"residence_name": null,
|
|
"is_overdue": false,
|
|
"is_due_within_7_days": false,
|
|
"is_due_8_to_30_days": false
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json)
|
|
#expect(task.id == 1)
|
|
#expect(task.description == nil)
|
|
#expect(task.priority == nil)
|
|
#expect(task.dueDate == nil)
|
|
#expect(task.category == nil)
|
|
#expect(task.residenceName == nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Realistic Scenario Tests
|
|
|
|
struct RealisticScenarioTests {
|
|
|
|
private func makeTask(
|
|
id: Int,
|
|
title: String,
|
|
isOverdue: Bool = false,
|
|
isDueWithin7Days: Bool = false,
|
|
isDue8To30Days: Bool = false
|
|
) -> WidgetDataManager.WidgetTask {
|
|
WidgetDataManager.WidgetTask(
|
|
id: id,
|
|
title: title,
|
|
description: nil,
|
|
priority: "medium",
|
|
inProgress: false,
|
|
dueDate: nil,
|
|
category: "maintenance",
|
|
residenceName: "Home",
|
|
isOverdue: isOverdue,
|
|
isDueWithin7Days: isDueWithin7Days,
|
|
isDue8To30Days: isDue8To30Days
|
|
)
|
|
}
|
|
|
|
@Test func newUserWithNoTasks() {
|
|
let metrics = WidgetDataManager.calculateMetrics(from: [])
|
|
#expect(metrics.totalCount == 0)
|
|
#expect(metrics.overdueCount == 0)
|
|
}
|
|
|
|
@Test func userWithOverdueTasks() {
|
|
let tasks = [
|
|
makeTask(id: 1, title: "HVAC maintenance", isOverdue: true),
|
|
makeTask(id: 2, title: "Roof inspection", isOverdue: true),
|
|
makeTask(id: 3, title: "Weekly lawn care", isDueWithin7Days: true),
|
|
makeTask(id: 4, title: "Monthly pest control", isDue8To30Days: true)
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 4)
|
|
#expect(metrics.overdueCount == 2)
|
|
#expect(metrics.upcoming7Days == 1)
|
|
#expect(metrics.upcoming30Days == 1)
|
|
}
|
|
|
|
@Test func typicalHomeowner() {
|
|
let tasks = [
|
|
makeTask(id: 1, title: "Replace water heater anode", isOverdue: true),
|
|
makeTask(id: 2, title: "Mow lawn", isDueWithin7Days: true),
|
|
makeTask(id: 3, title: "Take out trash", isDueWithin7Days: true),
|
|
makeTask(id: 4, title: "Water plants", isDueWithin7Days: true),
|
|
makeTask(id: 5, title: "Change air filter", isDue8To30Days: true),
|
|
makeTask(id: 6, title: "Clean dryer vent", isDue8To30Days: true),
|
|
makeTask(id: 7, title: "Organize garage"),
|
|
makeTask(id: 8, title: "Paint fence")
|
|
]
|
|
let metrics = WidgetDataManager.calculateMetrics(from: tasks)
|
|
#expect(metrics.totalCount == 8)
|
|
#expect(metrics.overdueCount == 1)
|
|
#expect(metrics.upcoming7Days == 3)
|
|
#expect(metrics.upcoming30Days == 2)
|
|
}
|
|
}
|