field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraAccessibilityUITests.swift (10366B)


      1 import XCTest
      2 
      3 @MainActor
      4 final class TeraAccessibilityUITests: XCTestCase {
      5   private var auditFindings: [String] = []
      6 
      7   override func tearDown() async throws {
      8     continueAfterFailure = true
      9     XCTAssertTrue(auditFindings.isEmpty, "Unresolved accessibility findings:\n" + auditFindings.joined(separator: "\n"))
     10     auditFindings.removeAll()
     11     try await super.tearDown()
     12   }
     13 
     14   func testFormUpdateAtLargestTextWithReduceMotionAndLocalSave() throws {
     15     try qualifyForm("Update")
     16   }
     17 
     18   func testFormPhotoUpdateAtLargestTextWithReduceMotionAndLocalSave() throws {
     19     try qualifyForm("Photo update")
     20   }
     21 
     22   func testFormAskAtLargestTextWithReduceMotionAndLocalSave() throws {
     23     try qualifyForm("Ask")
     24   }
     25 
     26   func testFormEventAtLargestTextWithReduceMotionAndLocalSave() throws {
     27     try qualifyForm("Event")
     28   }
     29 
     30   func testFormFoodAvailabilityAtLargestTextWithReduceMotionAndLocalSave() throws {
     31     try qualifyForm("Food availability")
     32   }
     33 
     34   func testUnconfirmedSaveStatusAtLargestText() throws {
     35     let app = launch(scenario: "save-error", form: "Photo update")
     36     XCTAssertTrue(app.tabBars.buttons["Add"].waitForExistence(timeout: 15))
     37     app.tabBars.buttons["Add"].tap()
     38     let prepare = app.buttons["Prepare unconfirmed Save"]
     39     XCTAssertTrue(prepare.waitForExistence(timeout: 5))
     40     prepare.tap()
     41     XCTAssertTrue(prepare.waitForNonExistence(timeout: 5))
     42     let error = app.staticTexts["The Add operation could not be completed."]
     43     XCTAssertEqual(error.label, "The Add operation could not be completed.")
     44     let state = app.descendants(matching: .any)["tera.add.save-state"]
     45     XCTAssertTrue(state.waitForExistence(timeout: 5))
     46     XCTAssertEqual(state.label, "Changes are not confirmed saved. Use Save draft to retry.")
     47     scroll(app, to: state)
     48     try audit(app)
     49   }
     50 
     51   func testActualPhotoDescriptionRemainsEditableAcrossSaveFailureAndRecovery() throws {
     52     let app = launch(form: "Photo update")
     53     XCTAssertTrue(app.tabBars.buttons["Add"].waitForExistence(timeout: 15))
     54     app.tabBars.buttons["Add"].tap()
     55     let library = app.buttons["Photo Library"]
     56     scroll(app, to: library)
     57     XCTAssertTrue(library.isHittable)
     58     library.tap()
     59     let description = app.descendants(matching: .any)["Describe this photo"]
     60     scroll(app, to: description, up: false)
     61     XCTAssertTrue(description.isHittable)
     62     description.tap()
     63     description.typeText("Carrots arranged in a wooden crate")
     64     app.buttons["Done"].tap()
     65     XCTAssertTrue(app.keyboards.firstMatch.waitForNonExistence(timeout: 5))
     66     XCTAssertEqual(description.value as? String, "Carrots arranged in a wooden crate")
     67     try audit(app)
     68     let save = app.buttons["Save draft"]
     69     scroll(app, to: save)
     70     save.tap()
     71     let failed = app.descendants(matching: .any)["tera.add.save-state"]
     72     XCTAssertTrue(failed.waitForExistence(timeout: 5))
     73     XCTAssertTrue(failed.isHittable, "Explicit Save must reveal its acknowledgment")
     74     scroll(app, to: failed, up: false)
     75     XCTAssertEqual(failed.label, "Changes are not confirmed saved. Use Save draft to retry.")
     76     try audit(app)
     77     scroll(app, to: description)
     78     XCTAssertEqual(description.value as? String, "Carrots arranged in a wooden crate")
     79     let remove = app.buttons["Remove photo"]
     80     scroll(app, to: remove)
     81     remove.tap()
     82     scroll(app, to: save)
     83     save.tap()
     84     let saved = app.descendants(matching: .any)["tera.add.save-state"]
     85     XCTAssertTrue(saved.waitForExistence(timeout: 5))
     86     XCTAssertTrue(saved.isHittable, "Explicit Save must reveal its acknowledgment")
     87     scroll(app, to: saved, up: false)
     88     XCTAssertEqual(saved.label, "Changes saved on this device.")
     89   }
     90 
     91   func testRuntimeFailureActionIsReachableAtLargestText() throws {
     92     let app = launch(scenario: "failure")
     93     let retry = app.buttons["Retry"]
     94     XCTAssertTrue(retry.waitForExistence(timeout: 5))
     95     try traverse(app, until: retry, auditing: true)
     96     XCTAssertGreaterThanOrEqual(retry.frame.height, 44)
     97     retry.tap()
     98     XCTAssertTrue(app.staticTexts["Local state checked"].exists)
     99   }
    100 
    101   func testSupportSettingsAndDiagnosticsActionRemainReachableAtLargestText() throws {
    102     let app = launch()
    103     XCTAssertTrue(app.tabBars.buttons["Today"].waitForExistence(timeout: 15))
    104     app.tabBars.buttons["Today"].tap()
    105     app.buttons["Account"].tap()
    106     let settings = app.buttons["Settings"]
    107     scroll(app, to: settings)
    108     settings.tap()
    109     let diagnostics = app.buttons["Prepare diagnostics export"]
    110     scroll(app, to: diagnostics)
    111     XCTAssertGreaterThanOrEqual(diagnostics.frame.height, 44)
    112     try audit(app)
    113     auditFindings += try TeraAccessibilityNavigation.qualifySupportSettings(app, test: self)
    114     let back = app.navigationBars.buttons["Me"]
    115     XCTAssertTrue(back.waitForExistence(timeout: 5))
    116     back.tap()
    117     XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 5))
    118     app.buttons["Done"].tap()
    119     XCTAssertTrue(app.tabBars.buttons["Today"].waitForExistence(timeout: 5))
    120   }
    121 
    122   func testSecureIdentityImportScalesAndCanBeDismissedWithoutSubmitting() throws {
    123     let app = launch(scenario: "identity")
    124     let open = app.buttons["Import identity"]
    125     XCTAssertTrue(open.waitForExistence(timeout: 5))
    126     try traverse(app, until: open, auditing: true)
    127     open.tap()
    128     for instruction in ["Enter an nsec or 64-character secret key.",
    129                         "It is transferred directly to Apple custody.",
    130                         "Secret input is never stored in view state."]
    131     {
    132       let text = app.staticTexts.matching(NSPredicate(format: "label == %@", instruction)).firstMatch
    133       scroll(app, to: text)
    134       try audit(app)
    135     }
    136     let field = app.secureTextFields["Secret identity key"]
    137     scroll(app, to: field)
    138     XCTAssertTrue(field.exists)
    139     XCTAssertTrue(field.isHittable)
    140     XCTAssertGreaterThanOrEqual(field.frame.height, 44)
    141     try audit(app)
    142     let submit = app.buttons["Import securely"]
    143     scroll(app, to: submit)
    144     XCTAssertTrue(submit.isHittable)
    145     XCTAssertGreaterThanOrEqual(submit.frame.height, 44)
    146     scroll(app, to: field, up: false)
    147     field.tap()
    148     field.typeText("invalid-key\n")
    149     XCTAssertTrue(app.keyboards.firstMatch.waitForExistence(timeout: 5))
    150     XCTAssertTrue(app.staticTexts["Enter a valid Nostr secret key."].waitForExistence(timeout: 5))
    151     app.buttons["Cancel"].tap()
    152     XCTAssertTrue(open.waitForExistence(timeout: 5))
    153   }
    154 }
    155 
    156 private extension TeraAccessibilityUITests {
    157   func qualifyForm(_ type: String) throws {
    158     let app = launch(form: type)
    159     XCTAssertTrue(app.tabBars.buttons["Add"].waitForExistence(timeout: 15), app.debugDescription)
    160     if type == "Update" {
    161       app.tabBars.buttons["Today"].tap()
    162       try audit(app, dynamicType: true)
    163     }
    164     app.tabBars.buttons["Add"].tap()
    165     verifyEditingFocus(app, type: type)
    166     try audit(app)
    167     let save = app.buttons["Save draft"]
    168     let disclosure = app.staticTexts["Submit saves an immutable local snapshot first."]
    169     scroll(app, to: disclosure)
    170     try audit(app)
    171     // Audits may move a lazy form back to its earlier rows. Seek the action
    172     // forward first; the bounded navigator reverses at an observed edge.
    173     scroll(app, to: save)
    174     XCTAssertTrue(save.isEnabled)
    175     XCTAssertGreaterThanOrEqual(save.frame.height, 44)
    176     save.tap()
    177     let saved = app.descendants(matching: .any)["tera.add.save-state"]
    178     XCTAssertTrue(saved.waitForExistence(timeout: 5))
    179     XCTAssertTrue(saved.isHittable, "Explicit Save must reveal its acknowledgment")
    180     scroll(app, to: saved, up: false)
    181     XCTAssertEqual(saved.label, "Changes saved on this device.")
    182     XCTAssertTrue(saved.isHittable)
    183     try audit(app)
    184     app.terminate()
    185   }
    186 
    187   func launch(scenario: String = "forms", form: String = "Update") -> XCUIApplication {
    188     continueAfterFailure = false
    189     _ = TeraAccessibilitySettings.setSystemReduceMotion(true, restoring: self)
    190     let app = XCUIApplication()
    191     app.launchEnvironment["TERA_IOS_UI_TEST_SHELL"] = "1"
    192     app.launchEnvironment["TERA_IOS_UI_TEST_ACCESSIBILITY"] = scenario
    193     app.launchEnvironment["TERA_IOS_UI_TEST_FORM"] = form
    194     app.launchArguments = ["-AppleLanguages", "(en)", "-AppleLocale", "en_US",
    195                            "-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL"]
    196     app.launch()
    197     let root = app.otherElements["tera.test.accessibility.root"]
    198     XCTAssertTrue(root.waitForExistence(timeout: 5))
    199     let environment = XCTAttachment(string: "Observed presentation environment: \(root.value ?? "missing")")
    200     environment.lifetime = .keepAlways
    201     add(environment)
    202     XCTAssertEqual(root.value as? String, "text=accessibility5; reduceMotion=true")
    203     return app
    204   }
    205 
    206   func verifyEditingFocus(_ app: XCUIApplication, type: String) {
    207     let labels = ["Update": "What’s happening locally?", "Photo update": "What should neighbors know?",
    208                   "Ask": "What do you need or want to know?", "Event": "Event details (optional)",
    209                   "Food availability": "Details"]
    210     let content = app.textViews[labels[type]!]
    211     scroll(app, to: content)
    212     XCTAssertEqual(content.label, labels[type])
    213     XCTAssertGreaterThanOrEqual(content.frame.height, 44)
    214     content.tap()
    215     XCTAssertTrue(app.keyboards.firstMatch.waitForExistence(timeout: 5))
    216     content.typeText("Seedlings available nearby.")
    217     XCTAssertEqual(content.value as? String, "Seedlings available nearby.")
    218     app.buttons["Done"].tap()
    219     XCTAssertTrue(app.keyboards.firstMatch.waitForNonExistence(timeout: 5))
    220     let traversal = XCTAttachment(string: app.debugDescription)
    221     traversal.name = "\(type) editing accessibility tree"
    222     traversal.lifetime = .keepAlways
    223     add(traversal)
    224   }
    225 
    226   func audit(_ app: XCUIApplication, dynamicType: Bool = false) throws {
    227     auditFindings += try TeraAccessibilityAudit(test: self).run(app, dynamicType: dynamicType)
    228   }
    229 
    230   func traverse(_ app: XCUIApplication, until element: XCUIElement, auditing: Bool) throws {
    231     scroll(app, to: element)
    232     if auditing {
    233       try audit(app)
    234       // Audits can move accessibility focus and scroll position. Restore the
    235       // requested action before interacting with it.
    236       scroll(app, to: element)
    237     }
    238   }
    239 
    240   func scroll(_ app: XCUIApplication, to element: XCUIElement, up: Bool = true) {
    241     TeraAccessibilityNavigation.scroll(app, to: element, up: up, test: self)
    242   }
    243 }