diff --git a/ios/App/AppUITests/MoveDirectoryUITests.swift b/ios/App/AppUITests/MoveDirectoryUITests.swift index afe9dd1..0154e66 100644 --- a/ios/App/AppUITests/MoveDirectoryUITests.swift +++ b/ios/App/AppUITests/MoveDirectoryUITests.swift @@ -35,21 +35,22 @@ final class MoveDirectoryUITests: XCTestCase { try createFolder(named: source, in: app) try createFolder(named: destination, in: app) - // 1. Long-press the source row: the sheet has Move to…, and the core's - // prompt takes an inbox-relative destination path. + // 1. Long-press the source row: the sheet has Move to…, and since core + // 2.55.0 (zennotes#844) the core's prompt spells destinations the + // way the sidebar does: a folder of the notes area by its own path, + // with no inbox/ in front, so the same text works on an Inbox vault + // and on one whose notes live at the vault root. let sourceRow = try row(labelPrefix: source, in: app) sourceRow.press(forDuration: 0.8) let move = try button(label: "Move to…", in: app, failure: "the folder sheet has no Move to… row") attachScreenshot("folder sheet", of: app) move.tap() - let target = promptField(placeholder: "inbox/Work", in: app) - // Touch prompts with suggestions are tap-first (no autofocus), so the - // field needs a tap before typing; the typed path filters the list and - // Move submits the typed value. + let target = promptField(placeholderSuffix: "(type a folder to change)", in: app) + // The typed path filters the list and Move submits the typed value. waitUntilHittable(target, failure: "the Move prompt did not open") - target.tap() - app.typeText("inbox/\(destination)") + focusPromptField(target) + app.typeText(destination) attachScreenshot("move prompt", of: app) try button(label: "Move", in: app, failure: "the Move prompt has no Move button").tap() @@ -82,14 +83,12 @@ final class MoveDirectoryUITests: XCTestCase { try hittable(label: "Browse", in: app, failure: "the ensō menu did not open").tap() } - /// "New folder" at the drawer's current directory. A tap on the field is - /// harmless when the prompt already focused it, and needed on a simulator - /// whose hardware keyboard kept the software one down. + /// "New folder" at the drawer's current directory. private func createFolder(named name: String, in app: XCUIApplication) throws { try row(labelPrefix: "New folder", in: app, failure: "no New folder row in Browse").tap() let field = promptField(placeholder: "Folder name", in: app) waitUntilHittable(field, failure: "the New folder prompt did not open") - field.tap() + focusPromptField(field) app.typeText(name) try button(label: "Create", in: app, failure: "the New folder prompt has no Create button").tap() _ = try row(labelPrefix: name, in: app, failure: "created folder \(name) did not appear in Browse") @@ -103,6 +102,38 @@ final class MoveDirectoryUITests: XCTestCase { .firstMatch } + /// The Move prompt's placeholder opens with the notes root's name (the + /// Inbox folder's, or "Vault root"), which depends on the vault, so only + /// its fixed ending identifies the field. + private func promptField(placeholderSuffix suffix: String, in app: XCUIApplication) -> XCUIElement { + app.textFields + .matching(NSPredicate(format: "placeholderValue ENDSWITH %@", suffix)) + .firstMatch + } + + /// A prompt's field may arrive focused or not. PromptModal keeps touch + /// pickers tap-first, but with core 2.52.0 to 2.60.0 the dialog shell + /// (useDialogFocus) focused a panel's first control on open, so the Move + /// prompt raised the keyboard itself and its sheet rose with it. A + /// tap aimed at the field's frame from before that rise blurred the field + /// again, and typing then had no target. So the field gets time to take + /// focus on its own, and a tap only when it did not (a tap-first prompt, + /// or a hardware keyboard keeping the software one down). + private func focusPromptField(_ field: XCUIElement, timeout: TimeInterval = 2) { + if waitForKeyboardFocus(field, timeout: timeout) { return } + field.tap() + XCTAssertTrue(waitForKeyboardFocus(field, timeout: 5), "the prompt field did not take keyboard focus") + } + + private func waitForKeyboardFocus(_ element: XCUIElement, timeout: TimeInterval) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + repeat { + if (element.value(forKey: "hasKeyboardFocus") as? Bool) == true { return true } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } while Date() < deadline + return false + } + /// Menus, sheets and dialogs animate in, and an element reports its final /// frame before it can take a tap there. private func waitUntilHittable(_ element: XCUIElement, timeout: TimeInterval = 10, failure: String) {