- 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>
185 lines
7.7 KiB
Swift
185 lines
7.7 KiB
Swift
import XCTest
|
|
|
|
/// Integration tests for document CRUD against the real local backend.
|
|
///
|
|
/// Test Plan IDs: DOC-002, DOC-004, DOC-005
|
|
/// Data is seeded via API and cleaned up in tearDown.
|
|
final class DocumentIntegrationTests: AuthenticatedTestCase {
|
|
|
|
override var useSeededAccount: Bool { true }
|
|
|
|
// MARK: - DOC-002: Create Document
|
|
|
|
func testDOC002_CreateDocumentWithRequiredFields() {
|
|
// Seed a residence so the document form has a valid residence picker
|
|
cleaner.seedResidence()
|
|
|
|
navigateToDocuments()
|
|
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Document.addButton]
|
|
let emptyState = app.otherElements[AccessibilityIdentifiers.Document.emptyStateView]
|
|
let documentList = app.otherElements[AccessibilityIdentifiers.Document.documentsList]
|
|
|
|
let loaded = addButton.waitForExistence(timeout: defaultTimeout)
|
|
|| emptyState.waitForExistence(timeout: 3)
|
|
|| documentList.waitForExistence(timeout: 3)
|
|
XCTAssertTrue(loaded, "Documents screen should load")
|
|
|
|
if addButton.exists && addButton.isHittable {
|
|
addButton.forceTap()
|
|
} else {
|
|
let emptyAddButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")
|
|
).firstMatch
|
|
emptyAddButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
emptyAddButton.forceTap()
|
|
}
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Document.titleField]
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
let uniqueTitle = "IntTest Doc \(Int(Date().timeIntervalSince1970))"
|
|
titleField.forceTap()
|
|
titleField.typeText(uniqueTitle)
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Document.saveButton]
|
|
saveButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
saveButton.forceTap()
|
|
|
|
let newDoc = app.staticTexts[uniqueTitle]
|
|
XCTAssertTrue(
|
|
newDoc.waitForExistence(timeout: longTimeout),
|
|
"Newly created document should appear in list"
|
|
)
|
|
}
|
|
|
|
// MARK: - DOC-004: Edit Document
|
|
|
|
func testDOC004_EditDocument() {
|
|
// Seed a residence and document via API
|
|
let residence = cleaner.seedResidence()
|
|
let doc = cleaner.seedDocument(residenceId: residence.id, title: "Edit Target Doc \(Int(Date().timeIntervalSince1970))")
|
|
|
|
navigateToDocuments()
|
|
|
|
// Find and tap the seeded document
|
|
let card = app.staticTexts[doc.title]
|
|
card.waitForExistenceOrFail(timeout: longTimeout)
|
|
card.forceTap()
|
|
|
|
// Tap edit
|
|
let editButton = app.buttons[AccessibilityIdentifiers.Document.editButton]
|
|
editButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
editButton.forceTap()
|
|
|
|
// Update title
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Document.titleField]
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
titleField.forceTap()
|
|
titleField.press(forDuration: 1.0)
|
|
let selectAll = app.menuItems["Select All"]
|
|
if selectAll.waitForExistence(timeout: 2) {
|
|
selectAll.tap()
|
|
}
|
|
|
|
let updatedTitle = "Updated Doc \(Int(Date().timeIntervalSince1970))"
|
|
titleField.typeText(updatedTitle)
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Document.saveButton]
|
|
saveButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
|
saveButton.forceTap()
|
|
|
|
let updatedText = app.staticTexts[updatedTitle]
|
|
XCTAssertTrue(
|
|
updatedText.waitForExistence(timeout: longTimeout),
|
|
"Updated document title should appear after edit"
|
|
)
|
|
}
|
|
|
|
// MARK: - DOC-007: Document Image Section Exists
|
|
// NOTE: Full image-deletion testing (the original DOC-007 scenario) requires a
|
|
// document with at least one uploaded image. Image upload cannot be triggered
|
|
// via API alone — it requires user interaction with the photo picker inside the
|
|
// app (or a multipart upload endpoint). This stub seeds a document, opens its
|
|
// detail view, and verifies the images section is present so that a human tester
|
|
// or future automation (with photo injection) can extend it.
|
|
|
|
func test22_documentImageSectionExists() throws {
|
|
// Seed a residence and a document via API
|
|
let residence = cleaner.seedResidence()
|
|
let document = cleaner.seedDocument(
|
|
residenceId: residence.id,
|
|
title: "Image Section Doc \(Int(Date().timeIntervalSince1970))"
|
|
)
|
|
|
|
navigateToDocuments()
|
|
|
|
// Open the seeded document's detail
|
|
let docText = app.staticTexts[document.title]
|
|
docText.waitForExistenceOrFail(timeout: longTimeout)
|
|
docText.forceTap()
|
|
|
|
// Verify the detail view loaded
|
|
let detailView = app.otherElements[AccessibilityIdentifiers.Document.detailView]
|
|
let detailLoaded = detailView.waitForExistence(timeout: defaultTimeout)
|
|
|| app.navigationBars.staticTexts[document.title].waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(detailLoaded, "Document detail view should load after tapping the document")
|
|
|
|
// Look for an images / photos section header or add-image button.
|
|
// The exact identifier or label will depend on the document detail implementation.
|
|
let imagesSection = app.staticTexts.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Image' OR label CONTAINS[c] 'Photo' OR label CONTAINS[c] 'Attachment'")
|
|
).firstMatch
|
|
|
|
let addImageButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Image' OR label CONTAINS[c] 'Photo' OR label CONTAINS[c] 'Add'")
|
|
).firstMatch
|
|
|
|
let sectionVisible = imagesSection.waitForExistence(timeout: defaultTimeout)
|
|
|| addImageButton.waitForExistence(timeout: 3)
|
|
|
|
// This assertion will fail gracefully if the images section is not yet implemented.
|
|
// When it does fail, it surfaces the missing UI element for the developer.
|
|
XCTAssertTrue(
|
|
sectionVisible,
|
|
"Document detail should show an images/photos section or an add-image button. " +
|
|
"Full deletion of a specific image requires manual upload first — see DOC-007 in test plan."
|
|
)
|
|
}
|
|
|
|
// MARK: - DOC-005: Delete Document
|
|
|
|
func testDOC005_DeleteDocument() {
|
|
// Seed a document via API — don't track since we'll delete through UI
|
|
let residence = cleaner.seedResidence()
|
|
let deleteTitle = "Delete Doc \(Int(Date().timeIntervalSince1970))"
|
|
TestDataSeeder.createDocument(token: session.token, residenceId: residence.id, title: deleteTitle)
|
|
|
|
navigateToDocuments()
|
|
|
|
let target = app.staticTexts[deleteTitle]
|
|
target.waitForExistenceOrFail(timeout: longTimeout)
|
|
target.forceTap()
|
|
|
|
let deleteButton = app.buttons[AccessibilityIdentifiers.Document.deleteButton]
|
|
deleteButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
deleteButton.forceTap()
|
|
|
|
let confirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton]
|
|
let alertDelete = app.alerts.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'")
|
|
).firstMatch
|
|
|
|
if confirmButton.waitForExistence(timeout: shortTimeout) {
|
|
confirmButton.tap()
|
|
} else if alertDelete.waitForExistence(timeout: shortTimeout) {
|
|
alertDelete.tap()
|
|
}
|
|
|
|
let deletedDoc = app.staticTexts[deleteTitle]
|
|
XCTAssertTrue(
|
|
deletedDoc.waitForNonExistence(timeout: longTimeout),
|
|
"Deleted document should no longer appear"
|
|
)
|
|
}
|
|
}
|