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