Giter VIP home page Giter VIP logo

hammer's Introduction

Hammer

If you can't touch this, it's Hammer time!

Demo

Table of Contents
  1. Introduction
  2. Installation
  3. Setup
  4. Usage
  5. Troubleshooting
  6. License

Introduction

Hammer is a touch, stylus and keyboard synthesis library for emulating user interaction events. It enables better ways of triggering UI actions in unit tests, replicating a real world environment as much as possible.

⚠️ IMPORTANT: This library makes extensive use of private APIs and should never be included in a production app.

Installation

Requirements

Hammer requires Swift 5.3 and iOS 11.0 or later.

With SwiftPM

.package(url: "https://github.com/lyft/Hammer.git", from: "0.13.0")
pod 'HammerTests', '~> 0.13.1'

Setup

Hammer unit tests need to run in a host application to be able to generate touches. To configure this select your project in the sidebar, select your test target, and choose a host application in the general tab. The host application can be your main application or an empty wrapper like TestHost.

SwiftPM does not currently support creating applications. To use Hammer with SwiftPM frameworks you need to create an xcodeproj and setup a host application.

Usage

Hammer allows you to simulate fingers, stylus and keyboard events. It also provides various convenience methods to simulate higher level user interactions.

To be able to send events to a view you must first create an EventGenerator:

// Initialize for an existing UIWindow, ensure that the window is key and visible.
let eventGenerator = EventGenerator(window: myWindow)

// Initialize for a UIView, automatically wrapping it in a temporary window.
let eventGenerator = EventGenerator(view: myView)

// Initialize for a UIViewController, automatically wrapping it in a temporary window.
let eventGenerator = EventGenerator(viewController: myViewController)

When simulating finger or stylus touches, there are multiple ways of specifying a touch location:

  1. Default: If you don't specify a location it will use the center of the screen.
  2. Point: A CGPoint in screen coordinates.
  3. View: A reference to a UIView or UIViewController, the location will be the center of the visible part of the view.
  4. Identifier: An accessibility identifier string of a view, the location will be the center of the visible part of the view.

By default, Hammer will display simulated touches over the view. You can change this behavior for your event generator.

eventGenerator.showTouches = false

Simulating Fingers

Fingers are the most common method of user interaction on iOS. Hammer supports handling multiple fingers on the screen simultaneously, up to the limit on the device. You can specify the specific finger index you would like to use, if unspecified it will choose the most appropriate one automatically.

Primitive events are the basic building blocks of user interactions, they can be combined together to create full gestures. Some methods will allow you to specify a duration and will interpolate the changes during that time.

try eventGenerator.fingerDown(at: CGPoint(x: 10, y: 10))
try eventGenerator.fingerMove(to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.fingerUp()

For convenience, Hammer provides many higher level gestures. If you don't specify a location it will automatically default to the center of the view.

try eventGenerator.fingerTap()
try eventGenerator.fingerDoubleTap()
try eventGenerator.fingerLongPress()
try eventGenerator.twoFingerTap()

Many advanced gestures are also available.

try eventGenerator.fingerDrag(from: CGPoint(x: 10, y: 10), to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.fingerPinch(fromDistance: 100, toDistance: 50, duration: 0.5)
try eventGenerator.fingerRotate(angle: .pi, duration: 0.5)

Simulating Stylus

Stylus is available when running on an iPad. It allows for additional properties like pressure, altitude and azimuth to be specified.

Similar to fingers, primitive events are the basic building blocks of stylus interactions.

try eventGenerator.stylusDown(at: CGPoint(x: 10, y: 10), azimuth: 0, altitude: 0, pressure: 0.5)
try eventGenerator.stylusMove(to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.stylusUp()

Hammer also provides many higher level gestures for Stylus. If you don't specify a location it will automatically default to the center of the view.

try eventGenerator.stylusTap()
try eventGenerator.stylusDoubleTap()
try eventGenerator.stylusLongPress()

Simulating Keyboard

Keyboard methods take an explicit KeyboardKey object or a Character. Characters will be mapped to their closest keyboard key, you must wrap them with a shift key modifier if needed. This means that specifying a lowercase "a" character is equivalent to specifying an uppercase "A", this is also true for keys with symbols.

// Explicit `KeyboardKey`
try eventGenerator.keyDown(.letterA)
try eventGenerator.keyUp(.letterA)

// Automatic `Character` mapping
try eventGenerator.keyDown("a")
try eventGenerator.keyUp("a")

// Convenience key down and up events
try eventGenerator.keyPress(.letterA)
try eventGenerator.keyPress("a")

To type characters or longer strings and get automatic shift wrapping you can use the keyType() methods.

try eventGenerator.keyType("This will type the string as specified, including symbols!")

Finding a subview

When running on a full screen app or testing navigation, specifying a CGPoint in screen coordinates can be difficult. For this, Hammer provides convenience methods to find views in the hierarchy by their accessibility identifier.

let myButton = try eventGenerator.viewWithIdentifier("my_button", ofType: UIButton.self)
try eventGenerator.fingerTap(at: myButton)

This method will throw an error if the view was not found in the hierarchy. If you're testing navigation or screen changes and you need to wait until the view appears, you can add a timeout. This will wait until the hierarchy has updated and return the view.

let myButton = try eventGenerator.viewWithIdentifier("my_button", ofType: UIButton.self, timeout: 1)
try eventGenerator.fingerTap(at: myButton)

You can also pass accessibility identifiers directly to the event methods.

try eventGenerator.fingerDown(at: "my_draggable_object")
try eventGenerator.fingerMove(to: "drop_target", duration: 0.5)
try eventGenerator.fingerUp()

Waiting

You will often need to wait for the simulator to finish displaying something on the screen or for an animation to end. Hammer provides multiple methods to wait until a view is visible on screen or if a control is hittable

try eventGenerator.waitUntilVisible("my_label", timeout: 1)
try eventGenerator.waitUntilHittable("my_button", timeout: 1)

Troubleshooting

  • The app or window is not ready for interaction

Make sure you are running your unit tests in a host application (setup instructions). To interact with a view, it must be visible on the screen and the application must have finished presenting. You can test this by adding a delay to your testing and verifying that your view is appearing on screen.

  • View is not in hirarchy / Unable to find view

Make sure the view you specified is in the same hierarchy as the view that was used to create the EventGenerator. If you used an accessibility identifier, check that it was spelled correctly.

  • View is not visible

This means that the view is in the hierarchy but is not currently visible on screen, so it's not possible to generate touches for it. Make sure that the view is within visible bounds, not covered by other views, not hidden, and with alpha greater than 0.01.

  • View is not hittable

This means that the view is in the hierarchy and visible on screen but is not currently able to receive touches. Make sure that the view reponds to hit test in its center coordinate and user interaction is enabled.

License

Hammer is released under the Apache License. See LICENSE

hammer's People

Contributors

darrenkong avatar gabriellanata avatar nsoojin avatar tunous avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hammer's Issues

having trouble interacting with the callout bar

Thank you for creating Hammer!

I’d like to tap on an item in the callout bar, which lives in the text effects window. This is what I have so far:

let textEffectsWindow = UIApplication.textEffectsWindow /// A helper I wrote.
textEffectsWindow.makeKey() /// Otherwise, initializing an EventGenerator on the next line will throw.
let eventGeneratorForTextEffectsWindow = try EventGenerator(window: textEffectsWindow)
let linkButton = try eventGeneratorForTextEffectsWindow.viewWithLabel("Link") /// A helper I wrote to get a view by its `accessibilityLabel`.
try eventGeneratorForTextEffectsWindow.fingerDown(at: linkButton) /// This line throws `HammerError.viewIsNotHittable`.
try eventGeneratorForTextEffectsWindow.fingerUp()
window.makeKey() /// Makes my main window key again so I can proceed with my test.

fingerDown(at:) throws on line 264 of Subviews.swift since self.window.hitTest(hitPoint, with: nil) returns the entire UICalloutBar, not the UICalloutBarButton linkButton. Do you have any experience with the callout bar?

Thank you!

Working with SwiftUI Lists?

I wonder if you ever got Hammer to work with SwiftUI Lists. We've played around with the .accessibiliy(identifier:) View modifier and that has worked for creating UI Tests within Xcode, however when we tried to locate the Lists with Hammer we seem to have found that no UIViews in the view hierarchy are given the accessibility identifiers we gave to the List Views.

Just to move forward, I've used the .accessibility(label:) view modifier as well and written an extension on UIView which recursively hunts all subviews for the first UIView matching a given test and tested the UIView.accessibilityLabel instead. At least once the UIView is identified we can then use it with Hammer.

Have you folks tried hunting SwiftUI-built objects much and had you found Lists don't play nicely?

Hammer fails to build with Xcode 13 beta 4

We are bringing in Hammer via Swift PM and including it under Build Phases > Link Binary With Libraries for a test target.

Our project compiles fine under Xcode 12.5.1 but with Xcode 13 beta 4 I get a compiler error about Hammer’s use of UIApplication.shared:

'shared' is unavailable in application extensions for iOS: Use view controller based solutions where appropriate instead.

I tried cloning Hammer and compiling it for an iOS 15 sim and hit the same error.

New console log warnings and suggestions on flaky tests?

�Hey there,

I started seeing new type of warning logs as I generate touch events. However it doesn't seem to affect the functionality. Do you know why this started happening?

2022-07-22 14:38:11.471364-0400 IntegrationTestHost[36563:3464107] [EventDispatcher] Found no UIEvent for backing event of type: 1; contextId: 0x9DE5CC17; Event will not be dispatched
2022-07-22 14:38:11.473324-0400 IntegrationTestHost[36563:3464107] [EventDispatcher] Found no UIEvent for backing event of type: 1; contextId: 0x9DE5CC17; Event will not be dispatched
2022-07-22 14:38:11.491951-0400 IntegrationTestHost[36563:3464107] [EventDispatcher] Found no UIEvent for backing event of type: 1; contextId: 0x9DE5CC17; Event will not be dispatched

Another question I have is how are you guys handling the flakiness for Hammer in testing. Although not very often, my tests which rely on using Hammer shows flakiness in CI build machines. Do you encounter similar problems? Do you have any tips on how to mitigate it?

Thanks in advance @gabriellanata

Question: Watch Support

Hi! Thanks for putting together this library, more ways to avoid scenario tests but still tap around are great! :D

Question here more than an issue: currently, this can only be used with UIKit views (it seems), but this could be extended to WatchKit, right? Is that something that you've attempted before? Thanks!

Missing support for `.stationary` touches

Describe the bug
After two finger downs it is not possible to up single finger – both touches are ending

To Reproduce
Code to reproduce:

    func testBug() throws {
        try eventGenerator.wait(1)
        try eventGenerator.fingerDown([.rightThumb, .rightIndex],
                                      at: [
                                        CGPoint(x: 200, y: 400),
                                        CGPoint(x: 400, y: 400)
                                      ])
        try eventGenerator.wait(1)
        try eventGenerator.fingerUp([.rightThumb])
        try eventGenerator.wait(3)
        try eventGenerator.fingerUp([.rightIndex])
        try eventGenerator.wait(3)
    }

Expected behavior
Single finger released allowing to manipulate the second one.
I have a scenario where I do zooming then release one finger and starting to pan the map view.

Screenshots
If applicable, add screenshots to help explain your problem.
There is an extra framework to visualise touches with handling phases and locations via UIApplication.sendEvent. Circle animation starts when touch in the Ended phase was received.

hammer_two_fingers_one_release_bug.mov

Environment (please complete the following information):

  • Devices: iPhone 14 Pro, iPad mini 5
    • Simulator: iPad mini 6
  • OS: iOS 16.3.1
  • Xcode: 14.2

Additional context
UIApplication.sendEvent receives two UITouch events with the Ended phase.
Add any other context about the problem here.

Full UIApplication.sendEvent events report from sample code
Test Case '-[DebugAppTests.DebugAppTests testBug]' started (Iteration 1 of 3).
<UITouchesEvent: 0x60000331a490> timestamp: 178537 touches: {(
    <UITouch: 0x1288461a0> phase: Began tap count: 1 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: (null) ,
    <UITouch: 0x128838560> phase: Began tap count: 1 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: <MTKView: 0x12981e400; frame = (0 0; 1133 744); layer = <CAMetalLayer: 0x600000666e80>> location in window: {200, 400} previous location in window: {200, 400} location in view: {200, 400} previous location in view: {200, 400}
)}

2023-02-15 13:41:11.608328+0200 DebugApp[69948:3352821] [Window] Manually adding the rootViewController's view to the view hierarchy is no longer supported. Please allow UIWindow to add the rootViewController's view to the view hierarchy itself.
<UITouchesEvent: 0x60000331a490> timestamp: 178537 touches: {(
    <UITouch: 0x1288461a0> phase: Began tap count: 1 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: (null) ,
    <UITouch: 0x128838560> phase: Began tap count: 1 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: <MTKView: 0x12981e400; frame = (0 0; 1133 744); layer = <CAMetalLayer: 0x600000666e80>> location in window: {200, 400} previous location in window: {200, 400} location in view: {200, 400} previous location in view: {200, 400}
)}

2023-02-15 13:41:11.707522+0200 DebugApp[69948:3352821] [EventDispatcher] Found no UIEvent for backing event of type: 1; contextId: 0x91D53B81
<UITouchesEvent: 0x60000331a490> timestamp: 178538 touches: {(
    <UITouch: 0x1288461a0> phase: Ended tap count: 0 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: (null) ,
    <UITouch: 0x128838560> phase: Ended tap count: 0 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: <MTKView: 0x12981e400; frame = (0 0; 1133 744); layer = <CAMetalLayer: 0x600000666e80>> location in window: {200, 400} previous location in window: {200, 400} location in view: {200, 400} previous location in view: {200, 400}
)}

<UITouchesEvent: 0x60000331a490> timestamp: 178538 touches: {(
    <UITouch: 0x1288461a0> phase: Ended tap count: 0 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: (null) ,
    <UITouch: 0x128838560> phase: Ended tap count: 0 force: 0.000 window: <MBXFingerTipWindow: 0x11fe07840; baseClass = UIWindow; frame = (0 0; 1133 744); gestureRecognizers = <NSArray: 0x60000066a6d0>; layer = <UIWindowLayer: 0x60000066a520>> responder: <MTKView: 0x12981e400; frame = (0 0; 1133 744); layer = <CAMetalLayer: 0x600000666e80>> location in window: {200, 400} previous location in window: {200, 400} location in view: {200, 400} previous location in view: {200, 400}
)}

2023-02-15 13:41:12.907412+0200 DebugApp[69948:3352821] [EventDispatcher] Found no UIEvent for backing event of type: 1; contextId: 0x91D53B81

viewIsVisible Implementation Incorrect

Describe the bug
A view is deemed "not visible" by the library upon finger tap, even if the view is visible but outside the superview's bounds.

To Reproduce
Steps to reproduce the behavior:

  1. Create a tappable view outside superview's bounds. (with superview.clipsToBounds = false)
  2. try to tap the view using fingerTap(at:)
  3. .viewIsNotVisible error is thrown

Expected behavior
View should be hittable.

Screenshots
If applicable, add screenshots to help explain your problem.

Environment (please complete the following information):

  • Device: iPhone 13 Pro Max
  • OS: iOS 15

Additional context
Bug is pretty clear and easy to reproduce.
Below is the problematic condition.

func isVisible(_ rect: CGRect, visibility: EventGenerator.Visibility = .partial) -> Bool {
        switch visibility {
        case .partial:
            return self.intersects(rect)
        case .center:
            return self.contains(rect.center)
        case .full:
            return self.contains(rect)
        }
}

cocoapods support?

is it possible to add and publish a podspec for this project? i still use cocoapods and cannot use SPM.

Xcode 13.3 can't compile Hammer 0.14.0

Describe the bug
Xcode 13.3 can't compile an App-project which contains Hammer 0.14.0

To Reproduce

  1. Create new project with Xcode 13.3
  2. Add Hammer as depedency
  3. Build this project
  4. see compiler error

Expected behavior
Project builds successfully with Hammer

Screenshots
Screenshot 2022-03-23 at 15 56 26

Environment (please complete the following information):
Screenshot 2022-03-23 at 16 03 47

  • Device: MacBook Pro M1 Pro
  • OS: macOS 12.3

Additional context
We have added a sample project in https://github.com/awBSH/lyft-hammer-xcode13.3

waitUntilHittable never waits?

Hi from Japan👋🏻
I see waitUntilHittable in testcases a lot, but it seems like self.defaultTouchLocation is never nil or throws error, so what is the point?

Tests are only failing in remote CI pipeline.

Describe the bug

  • Following hammers documentation we are generating an event and attempting to tap.

let eventGenerator = try EventGenerator(view: tabs) let cell = try eventGenerator.viewWithIdentifier( "image_tab_button_Tab 1", ofType: ImageTabs.Button.self, timeout: 1 ) try eventGenerator.fingerTap(at: cell)

Locally we can run these tests as often as we want. I have recently been running these tests 100 times in a row with & without building and I never see a failure. When I push to our remote CI system we are seeing failures (maybe 50% of the time) with

failed: caught error: "The app or window is not ready for interaction. Ensure that your tests are running in a host application and that you have given enough time for the view to present on screen. For more troubleshooting tips see: https://github.com/lyft/Hammer#troubleshooting." (#CharacterRangeLen=0)

I have attempted adding waits
try eventGenerator.waitUntilHittable(timeout: 2)

but no matter how long the timeout is I am seeing the above error thrown.

SFSafariViewController support

Cool tool. Seems like events don't work for this controller due to security. Perhaps you can/want to find a way to its heart.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.