From f6de20bbbd1b29cdca8b48acd14b99465fcf3015 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Thu, 1 Oct 2026 15:14:20 -0500 Subject: [PATCH] fix(mobile): the folder Move UI test follows the 2.55 prompt and waits for focus MoveDirectoryUITests failed on every run since 1.13.0 with "the Move prompt did not open". It found the core's Move prompt by the placeholder "inbox/Work", but core 2.55.0 (ZenNotes/zennotes#844) made the prompt speak the sidebar's language: the placeholder became " (type a folder to change)" and destinations are typed as the sidebar shows them, with no inbox/ in front. The test now matches the placeholder's fixed ending (the root's name depends on the vault) and types the destination's own path, which works on an Inbox vault and on one whose notes live at the root. With the lookup fixed, the test still failed now and then while typing ("Neither element nor any descendant has keyboard focus"). From core 2.52.0 to 2.60.0 the dialog shell focused the prompt's input on open, so the keyboard came up by itself and lifted the sheet; the test's tap, aimed at the field's frame from before the lift, blurred it again (seen frame by frame in the run's recording). The desktop fix that restores tap-first is ZenNotes/zennotes 0c3071a6. The test no longer depends on either behavior: it gives the field a moment to take focus and taps only when it did not, which also covers a hardware keyboard keeping the software one down. The New folder step uses the same helper. iPhone 17 Pro Max (iOS 27) simulator: three iterations in a row passed on a clean vault (35.3 s, 34.0 s, 37.5 s), each removing its own fixture, and the test passed again on a build with the tap-first core packed in. --- ios/App/AppUITests/MoveDirectoryUITests.swift | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) 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) {