import XCTest import UIKit final class NativeUITests: XCTestCase { @MainActor func testInputRuleReversalAndMultilinePersistence() async throws { continueAfterFailure = false let app = XCUIApplication() app.launch() defer { app.terminate() } let create = app.buttons["docs.create"] XCTAssertTrue(create.waitForExistence(timeout: 15)) create.tap() let title = app.textFields["Title"] XCTAssertTrue(title.waitForExistence(timeout: 5)) title.tap(); title.typeText("Native input rule verification") app.buttons["docs.create.submit"].tap() let editor = app.textViews["docs.editor"] XCTAssertTrue(editor.waitForExistence(timeout: 15)) editor.tap(); editor.typeText("- ") let transformed = editor.value as? String ?? "" XCTAssertFalse(transformed.contains("-"), "The native input rule must remove its typed prefix") editor.typeText(XCUIKeyboardKey.delete.rawValue) let reverted = editor.value as? String ?? "" print("Synthetic rule transformed: \(String(reflecting: transformed)); reverted: \(String(reflecting: reverted))") let reversal = XCTAttachment(string: "Synthetic transformed value: \(String(reflecting: transformed))\nSynthetic reverted value: \(String(reflecting: reverted))") reversal.name = "Native input rule reversal"; reversal.lifetime = .keepAlways; add(reversal) XCTAssertTrue(reverted.contains("- "), "Backspace must restore the complete input-rule prefix") editor.typeText("Alpha\nBeta") let displayed = editor.value as? String ?? "" XCTAssertTrue(displayed.contains("Alpha")) XCTAssertTrue(displayed.contains("Beta")) struct Config: Decodable { struct Credential: Decodable { let name: String; let value: String }; let origin: String; let credential: Credential } let config = try JSONDecoder().decode(Config.self, from: Data(contentsOf: URL.documentsDirectory.appendingPathComponent("review.json"))) func fetch(_ path: String) async throws -> Data { var request = URLRequest(url: URL(string: config.origin + path)!) request.setValue("\(config.credential.name)=\(config.credential.value)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await URLSession.shared.data(for: request) XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200) return data } struct Listed: Decodable { struct Item: Decodable { let sqid: String; let title: String }; let documents: [Item] } let list = try JSONDecoder().decode(Listed.self, from: await fetch("/")) let item = try XCTUnwrap(list.documents.first { $0.title == "Native input rule verification" }) var saved = "" for _ in 0..<50 { saved = String(decoding: try await fetch("/d/\(item.sqid)/export.md"), as: UTF8.self) if saved.contains("Alpha") && saved.contains("Beta") { break } try await Task.sleep(for: .milliseconds(100)) } let attachment = XCTAttachment(string: "Synthetic Markdown: \(String(reflecting: saved))\nSynthetic editor value: \(String(reflecting: displayed))") attachment.name = "Native multiline persistence"; attachment.lifetime = .keepAlways; add(attachment) let prefix = "# Native input rule verification\n\n" XCTAssertTrue(saved.hasPrefix(prefix)) let body = String(saved.dropFirst(prefix.count)).replacingOccurrences(of: "\\-", with: "-") print("Synthetic rule persisted Markdown: \(String(reflecting: saved)); displayed: \(String(reflecting: displayed))") XCTAssertEqual(body.split(whereSeparator: { $0.isWhitespace }).joined(separator: " "), "- Alpha Beta", "Reversal and multiline typing must preserve every character in the real CRDT store") let screenshot = XCTAttachment(screenshot: app.screenshot()) screenshot.name = "Docs native input rule and multiline editor"; screenshot.lifetime = .keepAlways; add(screenshot) } @MainActor func testKeyboardJumpSearchOpensDocument() throws { // Apple's XCUIElement overview supports keyboard injection on iPadOS/macOS, // while iPhone automation supports gestures. On iPhone this inserts literal K. try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "XCTest hardware keyboard injection requires iPadOS; use the jump-button case on iPhone") try exerciseJump(keyboard: true) } @MainActor func testJumpButtonSearchOpensDocument() throws { try exerciseJump(keyboard: false) } @MainActor func testJumpButtonAndHardwareReturnOpensDocument() throws { try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "XCTest hardware keyboard injection requires iPadOS") try exerciseJump(keyboard: false, hardwareReturn: true) } @MainActor func testRegisteredCommandAndSoftwareSubmitOpenDocument() throws { try exerciseJump(keyboard: false, responderProbe: true) } @MainActor private func exerciseJump(keyboard: Bool, hardwareReturn: Bool = false, responderProbe: Bool = false) throws { continueAfterFailure = false let app = XCUIApplication() if responderProbe { app.launchEnvironment["DOCS_REVIEW_COMMAND_PROBE"] = "1" } app.launch() defer { app.terminate() } let first = app.buttons["Native review document"].firstMatch XCTAssertTrue(first.waitForExistence(timeout: 15)) first.tap() let editor = app.textViews["docs.editor"] XCTAssertTrue(editor.waitForExistence(timeout: 15)) editor.tap() if responderProbe { /* Review target dispatches the registered action after focus. */ } else if keyboard { editor.typeKey("k", modifierFlags: .command) } else { app.buttons["docs.jump"].firstMatch.tap() } print("Synthetic jump shortcut hierarchy: \(app.debugDescription)") try app.screenshot().pngRepresentation.write(to: URL.documentsDirectory.appendingPathComponent("jump-shortcut.review.png"), options: .atomic) let query = app.textFields["docs.jump.query"] XCTAssertTrue(query.waitForExistence(timeout: 5), "Command-K must open native document search from the editor") query.typeText("Second review") let result = app.buttons.containing(.staticText, identifier: "Second review document").firstMatch XCTAssertTrue(result.waitForExistence(timeout: 10), "Native search must show the existing server-ranked result") if responderProbe { query.typeText("\n") } else if keyboard || hardwareReturn { query.typeKey(XCUIKeyboardKey.return, modifierFlags: []) } else { result.tap() } print("Synthetic jump return hierarchy: \(app.debugDescription)") try app.screenshot().pngRepresentation.write(to: URL.documentsDirectory.appendingPathComponent("jump-return.review.png"), options: .atomic) let opened = app.navigationBars["Second review document"].waitForExistence(timeout: 10) if !opened { print("Synthetic jump result hierarchy: \(app.debugDescription)") let diagnostic = XCTAttachment(screenshot: app.screenshot()) diagnostic.name = "Docs native jump failure"; diagnostic.lifetime = .keepAlways; add(diagnostic) } XCTAssertTrue(opened, "Enter must open the selected native search result") XCTAssertTrue(editor.waitForExistence(timeout: 10)) let screenshot = XCTAttachment(screenshot: app.screenshot()) screenshot.name = "Docs native keyboard jump result"; screenshot.lifetime = .keepAlways; add(screenshot) } @MainActor func testExportPDFAndMarkdownDestinationsAndCancellation() throws { continueAfterFailure = false let app = XCUIApplication() app.launch() defer { app.terminate() } let document = app.buttons["Native review document"].firstMatch XCTAssertTrue(document.waitForExistence(timeout: 15)) document.tap() XCTAssertTrue(app.textViews["docs.editor"].waitForExistence(timeout: 15)) let export = app.buttons["Export and print"] for format in ["PDF", "Markdown"] { XCTAssertTrue(export.waitForExistence(timeout: 5)) export.tap() app.buttons["Download as " + format].tap() let cancel = app.buttons["Cancel"] XCTAssertTrue(cancel.waitForExistence(timeout: 20), "The native destination picker must open for " + format) let screenshot = XCTAttachment(screenshot: app.screenshot()) screenshot.name = "Docs native " + format + " export destination"; screenshot.lifetime = .keepAlways; add(screenshot) let hierarchy = XCTAttachment(string: app.debugDescription) hierarchy.name = "Synthetic " + format + " export picker hierarchy"; hierarchy.lifetime = .keepAlways; add(hierarchy) cancel.tap() XCTAssertTrue(export.waitForExistence(timeout: 10)) } } @MainActor func testCreateTypeFormatAndPersistDocument() async throws { continueAfterFailure = false let app = XCUIApplication() app.launch() defer { app.terminate() } XCTAssertFalse(app.staticTexts["fixture.missing"].exists) let create = app.buttons["docs.create"] guard create.waitForExistence(timeout: 10) else { XCTFail("Fixture document list did not load"); throw NSError(domain: "NativeReview", code: 1) } create.tap() let title = app.textFields["Title"] XCTAssertTrue(title.waitForExistence(timeout: 3)) title.tap(); title.typeText("Native editing verification") app.buttons["docs.create.submit"].tap() let editor = app.textViews["docs.editor"] guard editor.waitForExistence(timeout: 10) else { XCTFail("Native editor did not open"); throw NSError(domain: "NativeReview", code: 2) } editor.tap(); editor.typeText("Native text") XCTAssertTrue((editor.value as? String)?.contains("Native text") == true) let bold = app.buttons["Bold"].firstMatch XCTAssertTrue(bold.exists) bold.tap() // The toolbar retains the editor's first responder. Tapping the centre // of a whole-page text view here would deliberately reposition the caret. editor.typeText(" bold") let displayed = editor.value as? String ?? "" if !displayed.contains("bold") { let diagnostic = XCTAttachment(string: "Synthetic editor value: \(String(reflecting: displayed))\n\(app.debugDescription)") diagnostic.name = "Synthetic native input failure"; diagnostic.lifetime = .keepAlways; add(diagnostic) let failedScreenshot = XCTAttachment(screenshot: app.screenshot()) failedScreenshot.name = "Docs native input failure"; failedScreenshot.lifetime = .keepAlways; add(failedScreenshot) } XCTAssertTrue(displayed.contains("bold"), "Formatted input must remain visible: \(String(reflecting: displayed))") struct Config: Decodable { struct Credential: Decodable { let name: String; let value: String }; let origin: String; let credential: Credential } let config = try JSONDecoder().decode(Config.self, from: Data(contentsOf: URL.documentsDirectory.appendingPathComponent("review.json"))) func fetch(_ path: String) async throws -> Data { var request = URLRequest(url: URL(string: config.origin + path)!) request.setValue("\(config.credential.name)=\(config.credential.value)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await URLSession.shared.data(for: request) XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200) return data } struct Listed: Decodable { struct Item: Decodable { let sqid: String; let title: String }; let documents: [Item] } let list = try JSONDecoder().decode(Listed.self, from: await fetch("/")) let item = try XCTUnwrap(list.documents.first { $0.title == "Native editing verification" }) var saved = "" for _ in 0..<40 { saved = String(decoding: try await fetch("/d/\(item.sqid)/export.md"), as: UTF8.self) if saved.contains("Native text") && saved.contains("**") && saved.contains("bold") { break } try await Task.sleep(for: .milliseconds(100)) } let persistedDiagnostic = XCTAttachment(string: "Synthetic persisted Markdown: \(String(reflecting: saved))\nSynthetic displayed value: \(String(reflecting: editor.value as? String ?? ""))") persistedDiagnostic.name = "Synthetic native persistence"; persistedDiagnostic.lifetime = .keepAlways; add(persistedDiagnostic) XCTAssertTrue(saved.contains("Native text"), "Native edits must reach the real CRDT store") XCTAssertTrue(saved.contains("**"), "Bold must persist as Markdown formatting, not only visible text") XCTAssertTrue(saved.contains("bold"), "Formatted typing must reach the real CRDT store") // Canonical Markdown export includes the document title as its heading. let titlePrefix = "# Native editing verification\n\n" XCTAssertTrue(saved.hasPrefix(titlePrefix)) let body = String(saved.dropFirst(titlePrefix.count)) XCTAssertEqual(body.replacingOccurrences(of: "**", with: "").trimmingCharacters(in: .whitespacesAndNewlines), "Native text bold", "Formatting the insertion point must preserve every character and append at the retained caret") let screenshot = XCTAttachment(screenshot: app.screenshot()) screenshot.name = "Docs native editor"; screenshot.lifetime = .keepAlways; add(screenshot) } }