Files
honeyDueKMP/iosApp/CaseraTests/TaskMetricsTests.swift
Trey t 5e3596db77 Complete re-validation remediation: KMP architecture, iOS platform, XCUITest rewrite
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>
2026-02-18 18:50:13 -06:00

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)
}
}