498e6b8064
Android UI Tests / ui-tests (pull_request) Has been cancelled
Users with multiple residences can now pick which one a given home- screen widget shows tasks for. Pinning two widgets — one per house — lets each surface tasks for only that residence; users who keep the configuration untouched continue to see all residences (the previous default), so single-home users see no behavioural change. Implementation (iOS only — Android Glance follow-up is scoped in the issue): * `ConfigurationAppIntent` (HoneyDue widget extension) gains an optional `@Parameter` of type `WidgetResidenceEntity`. `AppIntents` renders it as a residence picker in the widget edit sheet. * `WidgetResidenceEntity` + `WidgetResidenceEntityQuery` resolve the user's residences from a new `widget_residences.json` sidecar in the App Group container (avoids a network call at config time). * `WidgetDataManager.saveResidences(from:)` writes that sidecar from the main app whenever `DataManagerObservable.myResidences` updates. Logout clears it along with the rest of the widget cache. * `WidgetDataManager.WidgetTask` + the widget extension's `CacheManager.CustomTask` both gain an optional `residence_id` field. Optional so older app builds that wrote pre-#6 widget cache continue to decode — those tasks pass through the filter for unscoped widgets and are hidden from scoped ones (safer than guessing). * `CacheManager.getUpcomingTasks(forResidenceId:)` and the pure helper `WidgetDataManager.filterTasks(_:forResidenceId:)` apply the filter. `Provider.timeline` / `snapshot` read `configuration.residence?.intId` and pass it through. Tests: new `WidgetResidenceFilterTests` (HoneyDueTests target, 5 cases) cover nil-passthrough, matching-id, no-match, missing-residence on a task, and order preservation. All five green. No Android changes in this commit — Glance widgets need a separate configuration activity and an actionStartActivity wiring that's non-trivial; tracking as a follow-up in the same issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3.3 KiB
Swift
91 lines
3.3 KiB
Swift
import XCTest
|
|
@testable import honeyDue
|
|
|
|
/// Tests for the per-residence widget filter added in gitea#6.
|
|
///
|
|
/// `WidgetDataManager.filterTasks(_:forResidenceId:)` is the pure
|
|
/// function the widget timeline provider calls when a configuration
|
|
/// intent has a residence selected. These tests guarantee the contract
|
|
/// stays stable: nil → pass-through, matching id → only matching tasks,
|
|
/// no match → empty, missing residenceId on a task → never leaks into
|
|
/// a residence-scoped widget.
|
|
final class WidgetResidenceFilterTests: XCTestCase {
|
|
|
|
private func makeTask(
|
|
id: Int,
|
|
residenceId: Int? = nil
|
|
) -> WidgetDataManager.WidgetTask {
|
|
WidgetDataManager.WidgetTask(
|
|
id: id,
|
|
title: "Task \(id)",
|
|
description: nil,
|
|
priority: nil,
|
|
inProgress: false,
|
|
dueDate: nil,
|
|
category: nil,
|
|
residenceId: residenceId,
|
|
residenceName: nil,
|
|
isOverdue: false,
|
|
isDueWithin7Days: false,
|
|
isDue8To30Days: false
|
|
)
|
|
}
|
|
|
|
func testNilResidenceReturnsAllTasks() {
|
|
// "All residences" config — widget passes nil, gets every task.
|
|
let tasks = [
|
|
makeTask(id: 1, residenceId: 10),
|
|
makeTask(id: 2, residenceId: 20),
|
|
makeTask(id: 3, residenceId: nil),
|
|
]
|
|
let result = WidgetDataManager.filterTasks(tasks, forResidenceId: nil)
|
|
XCTAssertEqual(result.map(\.id), [1, 2, 3])
|
|
}
|
|
|
|
func testMatchingResidenceKeepsOnlyMatchingTasks() {
|
|
let tasks = [
|
|
makeTask(id: 1, residenceId: 10),
|
|
makeTask(id: 2, residenceId: 20),
|
|
makeTask(id: 3, residenceId: 10),
|
|
makeTask(id: 4, residenceId: 30),
|
|
]
|
|
let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 10)
|
|
XCTAssertEqual(result.map(\.id), [1, 3])
|
|
}
|
|
|
|
func testUnknownResidenceReturnsEmpty() {
|
|
let tasks = [
|
|
makeTask(id: 1, residenceId: 10),
|
|
makeTask(id: 2, residenceId: 20),
|
|
]
|
|
let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 999)
|
|
XCTAssertTrue(result.isEmpty)
|
|
}
|
|
|
|
func testNilResidenceIdOnTaskDoesNotMatchScopedConfiguration() {
|
|
// A task written by an older app build (no `residence_id` in JSON)
|
|
// must NOT leak into a residence-scoped widget — we'd rather hide
|
|
// it than misattribute it to the wrong home.
|
|
let tasks = [
|
|
makeTask(id: 1, residenceId: 10),
|
|
makeTask(id: 2, residenceId: nil),
|
|
]
|
|
let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 10)
|
|
XCTAssertEqual(result.map(\.id), [1])
|
|
}
|
|
|
|
func testFilterPreservesInputOrder() {
|
|
// The filter is a pure subset op — no sorting side effects.
|
|
// Timeline provider relies on this so its sort step (overdue
|
|
// first, then by due date) operates on already-filtered tasks.
|
|
let tasks = [
|
|
makeTask(id: 5, residenceId: 1),
|
|
makeTask(id: 3, residenceId: 1),
|
|
makeTask(id: 7, residenceId: 1),
|
|
makeTask(id: 1, residenceId: 2),
|
|
]
|
|
let result = WidgetDataManager.filterTasks(tasks, forResidenceId: 1)
|
|
XCTAssertEqual(result.map(\.id), [5, 3, 7])
|
|
}
|
|
}
|