- Create unit tests: DataLayerTests (27 tests for DATA-001–007), DataManagerExtendedTests (20 tests for TASK-005, TASK-012, TCOMP-003, THEME-001, QA-002), plus ValidationHelpers, TaskMetrics, StringExtensions, DoubleExtensions, DateUtils, DocumentHelpers, ErrorMessageParser - Create UI tests: AuthenticationTests, PasswordResetTests, OnboardingTests, TaskIntegration, ContractorIntegration, ResidenceIntegration, DocumentIntegration, DataLayer, Stability - Add UI test framework: AuthenticatedTestCase, ScreenObjects, TestFlows, TestAccountManager, TestAccountAPIClient, TestDataCleaner, TestDataSeeder - Add accessibility identifiers to password reset views for UI test support - Add greenfield test plan CSVs and update automated column for 27 test IDs - All 297 unit tests pass across 60 suites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
5.8 KiB
Swift
160 lines
5.8 KiB
Swift
import XCTest
|
|
|
|
/// Base class for tests requiring a logged-in session against the real local backend.
|
|
///
|
|
/// By default, creates a fresh verified account via the API, launches the app
|
|
/// (without `--ui-test-mock-auth`), and drives the UI through login.
|
|
///
|
|
/// Override `useSeededAccount` to log in with a pre-existing database account instead.
|
|
/// Override `performUILogin` to skip the UI login step (if you only need the API session).
|
|
///
|
|
/// ## Data Seeding & Cleanup
|
|
/// Use the `cleaner` property to seed data that auto-cleans in tearDown:
|
|
/// ```
|
|
/// let residence = cleaner.seedResidence(name: "My Test Home")
|
|
/// let task = cleaner.seedTask(residenceId: residence.id)
|
|
/// ```
|
|
/// Or seed without tracking via `TestDataSeeder` and track manually:
|
|
/// ```
|
|
/// let res = TestDataSeeder.createResidence(token: session.token)
|
|
/// cleaner.trackResidence(res.id)
|
|
/// ```
|
|
class AuthenticatedTestCase: BaseUITestCase {
|
|
|
|
/// The active test session, populated during setUp.
|
|
var session: TestSession!
|
|
|
|
/// Tracks and cleans up resources created during the test.
|
|
/// Initialized in setUp after the session is established.
|
|
private(set) var cleaner: TestDataCleaner!
|
|
|
|
/// Override to `true` in subclasses that should use the pre-seeded admin account.
|
|
var useSeededAccount: Bool { false }
|
|
|
|
/// Seeded account credentials. Override in subclasses that use a different seeded user.
|
|
var seededUsername: String { "admin" }
|
|
var seededPassword: String { "test1234" }
|
|
|
|
/// Override to `false` to skip driving the app through the login UI.
|
|
var performUILogin: Bool { true }
|
|
|
|
/// No mock auth - we're testing against the real backend.
|
|
override var additionalLaunchArguments: [String] { [] }
|
|
|
|
// MARK: - Setup
|
|
|
|
override func setUpWithError() throws {
|
|
// Check backend reachability before anything else
|
|
guard TestAccountAPIClient.isBackendReachable() else {
|
|
throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)")
|
|
}
|
|
|
|
// Create or login account via API
|
|
if useSeededAccount {
|
|
guard let s = TestAccountManager.loginSeededAccount(
|
|
username: seededUsername,
|
|
password: seededPassword
|
|
) else {
|
|
throw XCTSkip("Could not login seeded account '\(seededUsername)'")
|
|
}
|
|
session = s
|
|
} else {
|
|
guard let s = TestAccountManager.createVerifiedAccount() else {
|
|
throw XCTSkip("Could not create verified test account")
|
|
}
|
|
session = s
|
|
}
|
|
|
|
// Initialize the cleaner with the session token
|
|
cleaner = TestDataCleaner(token: session.token)
|
|
|
|
// Launch the app (calls BaseUITestCase.setUpWithError which launches and waits for ready)
|
|
try super.setUpWithError()
|
|
|
|
// Drive the UI through login if needed
|
|
if performUILogin {
|
|
loginViaUI()
|
|
}
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
// Clean up all tracked test data
|
|
cleaner?.cleanAll()
|
|
try super.tearDownWithError()
|
|
}
|
|
|
|
// MARK: - UI Login
|
|
|
|
/// Navigate from onboarding welcome → login screen → type credentials → wait for main tabs.
|
|
func loginViaUI() {
|
|
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
|
login.enterUsername(session.username)
|
|
login.enterPassword(session.password)
|
|
|
|
// Tap the login button
|
|
let loginButton = app.buttons[UITestID.Auth.loginButton]
|
|
loginButton.waitUntilHittable(timeout: defaultTimeout).tap()
|
|
|
|
// Wait for either main tabs or verification screen
|
|
let mainTabs = app.otherElements[UITestID.Root.mainTabs]
|
|
let tabBar = app.tabBars.firstMatch
|
|
|
|
let deadline = Date().addingTimeInterval(longTimeout)
|
|
while Date() < deadline {
|
|
if mainTabs.exists || tabBar.exists {
|
|
return
|
|
}
|
|
// Check for email verification gate - if we hit it, enter the debug code
|
|
let verificationScreen = VerificationScreen(app: app)
|
|
if verificationScreen.codeField.exists {
|
|
verificationScreen.enterCode(TestAccountAPIClient.debugVerificationCode)
|
|
verificationScreen.submitCode()
|
|
// Wait for main tabs after verification
|
|
if mainTabs.waitForExistence(timeout: longTimeout) || tabBar.waitForExistence(timeout: 5) {
|
|
return
|
|
}
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
}
|
|
|
|
XCTFail("Failed to reach main app after login. Debug tree:\n\(app.debugDescription)")
|
|
}
|
|
|
|
// MARK: - Tab Navigation
|
|
|
|
func navigateToTab(_ tab: String) {
|
|
let tabButton = app.buttons[tab]
|
|
if tabButton.waitForExistence(timeout: defaultTimeout) {
|
|
tabButton.forceTap()
|
|
} else {
|
|
// Fallback: search tab bar buttons by label
|
|
let label = tab.replacingOccurrences(of: "TabBar.", with: "")
|
|
let byLabel = app.tabBars.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] %@", label)
|
|
).firstMatch
|
|
byLabel.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
byLabel.forceTap()
|
|
}
|
|
}
|
|
|
|
func navigateToResidences() {
|
|
navigateToTab(AccessibilityIdentifiers.Navigation.residencesTab)
|
|
}
|
|
|
|
func navigateToTasks() {
|
|
navigateToTab(AccessibilityIdentifiers.Navigation.tasksTab)
|
|
}
|
|
|
|
func navigateToContractors() {
|
|
navigateToTab(AccessibilityIdentifiers.Navigation.contractorsTab)
|
|
}
|
|
|
|
func navigateToDocuments() {
|
|
navigateToTab(AccessibilityIdentifiers.Navigation.documentsTab)
|
|
}
|
|
|
|
func navigateToProfile() {
|
|
navigateToTab(AccessibilityIdentifiers.Navigation.profileTab)
|
|
}
|
|
}
|