UI 测试失败 - 元素和任何后代都没有键盘焦点在 secureTextField 上

UI Testing Failure - Neither element nor any descendant has keyboard focus on secureTextField

这是我的案例:

let passwordSecureTextField = app.secureTextFields["password"]
passwordSecureTextField.tap()
passwordSecureTextField.typeText("wrong_password") //here is an error

UI Testing Failure - Neither element nor any descendant has keyboard focus. Element:

怎么了?这对正常的 textFields 很好用,但只有 secureTextFields 才会出现问题。有什么解决方法吗?

您的第一行只是一个 查询定义 ,这并不意味着 passwordSecureTextField 实际存在。

您的第二行将动态 执行查询并尝试(重新)将查询绑定到UI 元素。您应该在其上放置一个断点并验证是否找到了一个且仅一个元素。或者只使用断言:

XCTAssertFalse(passwordSecureTextField.exists);

否则它看起来没问题,tap 应该强制键盘可见,然后 typeText 应该可以正常工作。错误日志应该会告诉您更多信息。

这可能有帮助:我只是在错误之前添加了一个 "tap" 操作;就这些:)

[app.textFields[@"theTitle"] tap];
[app.textFields[@"theTitle"] typeText:@"kk"];
func pasteTextFieldText(app:XCUIApplication, element:XCUIElement, value:String, clearText:Bool) {
    // Get the password into the pasteboard buffer
    UIPasteboard.generalPasteboard().string = value

    // Bring up the popup menu on the password field
    element.tap()

    if clearText {
        element.buttons["Clear text"].tap()
    }

    element.doubleTap()

    // Tap the Paste button to input the password
    app.menuItems["Paste"].tap()
}

[重新发布 Bartłomiej Semańczyk 的评论作为答案,因为它解决了我的问题]

我需要在模拟器菜单栏中执行 Simulator > Reset Contents and Settings 才能开始为我工作。

这个问题让我很痛苦,但我已经设法找到了一个合适的解决方案。在模拟器中,确保 I/O -> Keyboard -> Connect hardware keyboard 已关闭。

斯坦尼斯拉夫的想法是正确的。

在团队环境中,您需要能够自动工作的东西。我在我的博客上提出了一个解决方案 here

基本上你只需要粘贴:

UIPasteboard.generalPasteboard().string = "Their password"
let passwordSecureTextField = app.secureTextFields["password"]
passwordSecureTextField.pressForDuration(1.1)
app.menuItems["Paste"].tap()

我写了一个非常适合我的小扩展 (Swift)。 这是代码:

extension XCTestCase {

    func tapElementAndWaitForKeyboardToAppear(element: XCUIElement) {
        let keyboard = XCUIApplication().keyboards.element
        while (true) {
            element.tap()
            if keyboard.exists {
                break;
            }
            NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.5))
        }
    }
}

主要思想是在出现键盘之前不断点击一个元素(文本字段)。

最近我们发现了一种黑客技术,可以使已接受答案的解决方案持久化。要禁用模拟器设置:I/O -> Keyboard -> Connect hardware keyboard 从命令行应该写:

defaults write com.apple.iphonesimulator ConnectHardwareKeyboard 0

它不会影响 运行 的模拟器 - 您需要重新启动模拟器或启动一个新的模拟器才能使该设置生效。

别搞砸了,出现问题的原因是你记录了你的测试时间,你的应用程序将连接硬件键盘,而你的自动测试时间模拟器只需要软件键盘。那么如何解决这个问题。只需在您的录音时间使用软件键盘。你可以看到魔法。

随心所欲地录制案例,键盘或不连接键盘。 但在进行测试之前请执行以下操作。

进行测试时应取消选中以下选项(连接硬件键盘)。

我的问题和 Ted 的一样。实际上,如果在登录字段和硬件 KB 开启后密码字段被点击,软件键盘将在第二次字段点击时自动关闭,并且它不是特定于 UI 测试。

经过一段时间使用 AppleScript,这是我想出的(欢迎改进):

tell application "Simulator"
    activate
    tell application "System Events"
        try
            tell process "Simulator"
                tell menu bar 1
                    tell menu bar item "Hardware"
                        tell menu "Hardware"
                            tell menu item "Keyboard"
                                tell menu "Keyboard"
                                    set menuItem to menu item "Connect Hardware Keyboard"
                                    tell menu item "Connect Hardware Keyboard"
                                        set checkboxStatus to value of attribute "AXMenuItemMarkChar" of menuItem
                                        if checkboxStatus is equal to "✓" then
                                            click
                                        end if
                                    end tell
                                end tell
                            end tell
                        end tell
                    end tell
                end tell
            end tell
        on error
            tell application "System Preferences"
                activate
                set securityPane to pane id "com.apple.preference.security"
                tell securityPane to reveal anchor "Privacy_Accessibility"
                    display dialog "Xcode needs Universal access to disable hardware keyboard during tests(otherwise tests may fail because of focus issues)"
                end tell
        end try
    end tell
end tell

用上面的代码创建一个脚本文件并将其添加到必要的目标(可能 UI 仅测试目标,您可能希望将类似的脚本添加到您的开发目标以在开发期间重新启用硬件键盘)。 您应该在构建阶段添加 Run Script 阶段并像这样使用它: osascript Path/To/Script/script_name.applescript

它发生在我身上很多次。 您必须在模拟器

中禁用键盘硬件和与 OSX 相同的布局

Hardware/Keyboard(全部禁用)

之后键盘软件将不会关闭,您的测试可以输入文本

在启动应用程序和在文本字段中输入数据之间使用睡眠,如下所示:

sleep(2)

在我的例子中,我每次都收到这个错误,只有这个解决方案帮助了我。

我们在为包含 UIControl 个子视图的自定义视图(UIStackView 子类)设置 accessibilityIdentifier 值时遇到了同样的错误。在那种情况下,XCTest 无法获得后代元素的键盘焦点。

我们的解决方案只是从父视图中删除 accessibilityIdentifier 并通过专用属性为子视图设置 accessibilityIdentifier

与 Securetextfields 有同样的问题。我的模拟器中的连接硬件选项是,但仍然 运行 成为问题。最后,这对我有用 (Swift 3):

 let enterPasswordSecureTextField = app.secureTextFields["Enter Password"]
    enterPasswordSecureTextField.tap()
    enterPasswordSecureTextField.typeText("12345678")

此错误的另一个原因是,如果您尝试在其中输入文本的文本字段的父视图被设置为辅助功能元素 (view.isAccessibilityElement = true)。在这种情况下,XCTest 无法获取子视图的句柄以输入文本,并且 returns 错误。

UI Testing Failure - Neither element nor any descendant has keyboard focus.

并不是没有元素有焦点(你经常可以在 UITextField 中看到键盘向上和光标闪烁),只是没有元素它可以到达有重点。当试图在 UISearchBar 中输入文本时,我 运行 进入了这个。搜索栏本身不是文本字段,将其设置为可访问性元素时,将阻止对底层 UITextField 的访问。为了解决这个问题,searchBar.accessibilityIdentifier = "My Identifier" 设置在 UISearchBar 上,但是 isAccessibilityElement 没有设置为 true。在此之后,测试代码的形式:

app.otherElements["My Identifier"].tap()
app.otherElements["My Identifier"].typeText("sample text")

有效

有时文本字段未实现为文本字段,或者它们被包装到另一个 UI 元素中并且不易访问。 这是一个解决方法:

//XCUIApplication().scrollViews.otherElements.staticTexts["Email"] the locator for the element
RegistrationScreenStep1of2.emailTextField.tap()
let keys = app.keys
 keys["p"].tap() //type the keys that you need
 
 //If you stored your data somewhere and need to access that string //you can cats your string to an array and then pass the index //number to key[]
 
 let newUserEmail = Array(newPatient.email())
 let password = Array(newPatient.password)
 
 //When you cast your string to an array the elements of the array //are Character so you would need to cast them into string otherwise //Xcode will compain. 
 
 let keys = app.keys
     keys[String(newUserEmail[0])].tap()
     keys[String(newUserEmail[1])].tap()
     keys[String(newUserEmail[2])].tap()
     keys[String(newUserEmail[3])].tap()
     keys[String(newUserEmail[4])].tap()
     keys[String(newUserEmail[5])].tap()       

另一个答案,但对我们来说,问题是视图与另一个视图太接近,无法在其上安装手势识别器。我们发现我们需要视图距离至少 20 像素(在我们下面的例子中)。从字面上看,15 个不起作用,而 20 个或更多起作用了。我承认这很奇怪,但我们有一些 UITextViews 正在工作,一些没有,并且都在同一个父级和相同的其他定位(当然还有变量名)下。键盘打开或关闭或其他任何东西都没有区别。可访问性显示了字段。我们重新启动了计算机。我们做了干净的构建。新鲜来源结帐。

为我解决这个问题的方法是增加 1 秒的睡眠时间:

let textField = app.textFields["identifier"]
textField.tap()
sleep(1)
textField.typeText(text)

在我的例子中,这个 Hardware -> Keyboard -> Connect Hardware Keyboard -> Disable 对我不起作用。

但是当我跟着

1) Hardware -> Keyboard -> Connect Hardware Keyboard -> 已启用 和 运行 应用
2) Hardware -> Keyboard -> Connect Hardware Keyboard -> 禁用.

对我有用

无需在 I/O 中转动 on/off 键盘。 不要将 .typeText 用于 secureTextField,只需使用

app.keys["p"].tap()
app.keys["a"].tap()
app.keys["s"].tap()
app.keys["s"].tap()

奖励:您会听到键盘发出的咔嗒声:)

我 运行 解决了这个问题,并且能够在我的场景中通过采用 发布的解决方案并将其添加到我的 运行 & 测试.

最后,我编写了一个脚本,用于编辑模拟器的 .plist 文件并将所选模拟器的 ConnectHardwareKeyboard 属性 设置为 false。您没听错,它会更改“DevicePreferences”字典中专门选择的模拟器的 属性,而不是编辑全局 属性。

首先,创建一个名为 disable-hardware-keyboard.sh 的 shell 脚本,其中包含以下内容。你可以把它放在 "YourProject/xyzUITests/Scripts/".:

echo "Script: Set ConnectHardwareKeyboard to false for given Simulator UDID"

if [[  != *-*-*-*-* ]]; then
    echo "Pass device udid as first argument."
    exit 1
else
    DEVICE_ID=
fi

DEVICE_PREFERENCES_VALUE='<dict><key>ConnectHardwareKeyboard</key><false/></dict>'
killall Simulator # kill restart the simulator to make the plist changes picked up
defaults write com.apple.iphonesimulator DevicePreferences -dict-add $DEVICE_ID $DEVICE_PREFERENCES_VALUE
open -a Simulator # IMPORTANT

现在按照以下步骤通过将所选模拟器的 udid 作为参数传递来调用它:

  1. 编辑您的 Xcode 方案(或 UI 测试特定方案,如果有的话)
  2. 转到:测试 > 预操作
  3. 通过点击“+”符号 >“新建 运行 脚本操作”添加新脚本。
  4. 重要提示:在“从以下位置提供构建设置”下拉菜单中选择您的主要应用程序目标,而不是 UI 测试目标。
  5. 现在在下面的文本区域添加以下脚本。

测试中的脚本>预操作:

#!/bin/sh
# $PROJECT_DIR is path to your source project. This is provided when we select "Provide build settings from" to "AppTarget"
# $TARGET_DEVICE_IDENTIFIER is the UDID of the selected simulator
sh $PROJECT_DIR/xyzUITests/Scripts/disable-hardware-keyboard.sh $TARGET_DEVICE_IDENTIFIER

# In order to see output of above script, append following with it:
#  | tee ~/Desktop/ui-test-scheme-prescript.txt

测试时间:

  1. 启动模拟器
  2. 为其启用硬件键盘
  3. 运行 任意 UI 键盘交互测试。 观察 模拟器重新启动,硬件键盘被禁用。并且测试的键盘交互工作正常。 :)

适合我:

extension XCUIElement {
    func typeTextAlt(_ text: String) {
        // Solution for `Neither element nor any descendant has keyboard focus.`
        if !(self.value(forKey: "hasKeyboardFocus") as? Bool ?? false) {
            XCUIDevice.shared.press(XCUIDevice.Button.home)
            XCUIApplication().activate()
        }
        self.typeText(text)
    }
}

长话短说:iOS 模拟器中的硬件键盘输入似乎坏了很长时间。

事情是 -[UIApplicationSupport _accessibilityHardwareKeyboardActive] 来自 com.apple.UIKit.axbundle 的私有方法有时 returns NO 即使它应该 return YES (当文本输入视图是 firstResponder,并且光标在闪烁,字面意思是硬件键盘处于活动状态。

将此代码放在应用程序中的任何位置,使 iOS 模拟器相信如果软件键盘处于非活动状态,则硬件键盘处于活动状态。

    
#if TARGET_OS_SIMULATOR
__attribute__((constructor))
static void Fix_UIApplicationAccessibility__accessibilityHardwareKeyboardActive(void) {
    {
        static id observer = nil;

        observer = [[NSNotificationCenter defaultCenter] addObserverForName:NSBundleDidLoadNotification
                                                          object:nil
                                                           queue:nil
                                                      usingBlock:^(NSNotification * _Nonnull note) {
            NSBundle *bundle = note.object;
            if ([bundle.bundleIdentifier isEqualToString:@"com.apple.UIKit.axbundle"]) {
                Class cls = objc_lookUpClass("UIApplicationAccessibility");
                SEL sel = sel_getUid("_accessibilityHardwareKeyboardActive");
                Method m = class_getInstanceMethod(cls, sel);
                method_setImplementation(m, imp_implementationWithBlock(^BOOL(UIApplication *app) {
                    return ![[app valueForKey:@"_accessibilitySoftwareKeyboardActive"] boolValue];
                }));
            }
        }];
    }
}
#endif

当模拟器 UI 在前台进行 运行 UI 测试时,上面的答案为我解决了(当你可以看到模拟器 window).但是,当 运行 它们在后台时它不起作用(例如,fastlane 运行 它们的方式)。

我必须去 Simulator > Device > Erase All Content and Settings...

为了清楚起见,我在模拟器设置中手动禁用-启用-禁用硬件键盘,设置defaults write com.apple.iphonesimulator ConnectHardwareKeyboard 0,然后然后擦除内容和设置。

以下来自 Swift 5.4,SwiftUI 应用程序场景,该场景会失败 UI 测试。

app.cells.firstMatch.tap()
// transition from list view to detail view for item
app.textFields
    .matching(identifier: "balance_textedit")
    .element.tap()
app.textFields
    .matching(identifier: "balance_textedit")
    .element.typeText("7620") // :FAIL:

Test: Failed to synthesize event: Neither element nor any descendant has keyboard focus.

解决方法:再次点击

第二个 tap() 被发现通过。

app.cells.firstMatch.tap()
// transition from list view to detail view for item
app.textFields
    .matching(identifier: "balance_textedit")
    .element.tap()
app.textFields
    .matching(identifier: "balance_textedit")
    .element.tap()
app.textFields
    .matching(identifier: "balance_textedit")
    .element.typeText("7620") // :PASS:

但是,.doubleTap()tap(withNumberOfTaps: 2, numberOfTouches: 1) 仍然会失败。

解决方案:等待 TextField 到达,然后点击

有时视图转换需要明确等待 TextEdit 的到来。下面的方法等待目标 TextEdit 字段的到来。

XCTestCase.swift 分机:

extension XCTestCase {
    func awaitArrival(element: XCUIElement, timeout: TimeInterval) {
        let predicate = NSPredicate(format: "exists == 1")
        expectation(for: predicate, evaluatedWith: element) 
        waitForExpectations(timeout: timeout)
    }

    func awaitDeparture(element: XCUIElement, timeout: TimeInterval) {
        let predicate = NSPredicate(format: "exists == 0")
        expectation(for: predicate, evaluatedWith: element) 
        waitForExpectations(timeout: timeout)
    }
}

使用示例:

app.cells.firstMatch.tap()
// transition from list view to detail view for item
awaitArrival(
    element: app.textFields.matching(identifier: "balance_textedit").element, 
    timeout: 5)
app.textFields
    .matching(identifier: "balance_textedit")
    .element.tap()
app.textFields
    .matching(identifier: "balance_textedit")
    .element.typeText("7620") // :PASS:

解决方法:waitForExistence,然后点击

使用<a href="https://developer.apple.com/documentation/xctest/xcuielement/2879412-waitforexistence/" rel="nofollow noreferrer">waitForExistence(timeout:)</a> method provided by <a href="https://developer.apple.com/documentation/xctest/xcuielement" rel="nofollow noreferrer">XCUIElement</a>.

app.cells.firstMatch.tap()
// transition from list view to detail view for item
app.textFields.matching(identifier: "balance_textedit")
    .element.waitForExistence(timeout: 5)
app.textFields
    .matching(identifier: "balance_textedit")
    .element.tap()
app.textFields
    .matching(identifier: "balance_textedit")
    .element.typeText("7620") // :PASS:
    emailTextField.tap() // add this line before typeText
    
    emailTextField.typeText("your text")