Hammer is a touch, stylus and keyboard synthesis library for emulating user interaction events

Overview

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.10.0")

With CocoaPods

pod 'HammerTests', '~> 0.10.2'

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

Comments
  • having trouble interacting with the callout bar

    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!

    bug 
    opened by littlebobert 8
  • viewIsVisible Implementation Incorrect

    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)
            }
    }
    
    bug 
    opened by nsoojin 6
  • Xcode 13.3 can't compile Hammer 0.14.0

    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
    • https://raw.githubusercontent.com/awBSH/lyft-hammer-xcode13.3/main/EventGenerator%2BMarker.swift.txt
    • https://raw.githubusercontent.com/awBSH/lyft-hammer-xcode13.3/main/KeyboardKey.swift.txt

    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

    bug 
    opened by awBSH 3
  • Fix FingerUp causing UITouch to end in start location

    Fix FingerUp causing UITouch to end in start location

    @gabriellanata Hey buddy,

    There is a a bug in fingerUp where the location in touchesEnded callback is the start position of the drag(e.g. fingerDown). It is because FingerInfo only holds the initial location of the finger, so when I call fingerUp, it will cause the final touch on the initial point, not where the drag has ended.

    To reproduce, comment out this line and run the DragTests I put in.

    Once again, I greatly appreciate this library you put together. It has opened up fantastic automated testing suites for my app.

    bug 
    opened by nsoojin 3
  • Hammer fails to build with Xcode 13 beta 4

    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.

    enhancement help wanted 
    opened by littlebobert 3
  • New console log warnings and suggestions on flaky tests?

    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

    bug 
    opened by nsoojin 2
  • Bypass deprecation warning

    Bypass deprecation warning

    This method is deprecated but we still need to use it because iOS uses it itself and we need it to detect when the app is ignoring events. The fix is to add a protocol to bypass the warning.

    opened by gabriellanata 1
  • Improve view initialization

    Improve view initialization

    Currently, when you initialize an event generator from a UIView, it adds that view directly to a UIWindow. This can be a destructive change because the view might already be a subview of another view (possibly even already added to a window) and it will get removed.

    This change detects if the view already has a window and uses that directly. If not, it will get the top level superview by transversing the view hierarchy and use that instead. Because we set the mainView to the view that was passed in the initializer, everything else remains the same and it should not be a breaking change.

    NOTE: This is possibly a breaking change for some cases where the view was getting removed from its original superview. This should be very rare and should have been considered a bug in the first place.

    opened by gabriellanata 1
  • Improve visibility check

    Improve visibility check

    The visibility check was not taking into account the clipsToBounds property so it was often returning the wrong error (it should have passed the visibility check but not the hittable check).

    Fixes #38

    bug 
    opened by gabriellanata 1
  • Improve scrolling flaky test

    Improve scrolling flaky test

    ScrollViews seem to be a little inconsistent when they start scrolling. The gesture detection requires a few points of travel to begin detecting, but the exact time the detection begins seems to vary, meaning that the scroll amount is not precise. Fixing by making the check allow for a bigger variation

    opened by gabriellanata 1
  • Remove key window requirement for touches

    Remove key window requirement for touches

    Key window is an unnecessary requirement for touches so I'm moving the check only to keyboard events. In many cases, the window will become key by itself when tapped, but we don't need to enforce that.

    opened by gabriellanata 1
  • Working with SwiftUI Lists?

    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?

    enhancement help wanted 
    opened by andyj-at-aspin 2
  • Question: Watch Support

    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!

    enhancement help wanted 
    opened by coolbnjmn 3
Releases(0.14.3)
Owner
Lyft
Lyft
ResponderChain is a library that passes events using the responder chain.

ResponderChain ResponderChain is a library that passes events using the responder chain.

GodL 21 Aug 11, 2021
iOS Logs, Events, And Plist Parser

iLEAPP iOS Logs, Events, And Plists Parser Details in blog post here: https://abrignoni.blogspot.com/2019/12/ileapp-ios-logs-events-and-properties.htm

Brigs 421 Jan 5, 2023
Swift implementation of AWS Lambda Events

Swift AWS Lambda Events Overview Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an i

Swift on Server 29 Dec 19, 2022
What if you could give your wallpapers, a little touch? On the fly, of course

Amēlija On the fly preferences. Features Custom Blurs for your LockScreen. Custom Blurs for your HomeScreen. Blur Types Epic (Gaussian). Dark. Light.

null 9 Dec 2, 2022
Automatically set your keyboard's backlight based on your Mac's ambient light sensor.

QMK Ambient Backlight Automatically set your keyboard's backlight based on your Mac's ambient light sensor. Compatibility macOS Big Sur or later, a Ma

Karl Shea 29 Aug 6, 2022
Popping sounds for your keyboard!

KeyPopper Popping sounds for your keyboard! ?? ?? Have you ever wanted a nice popping sound whenever you typed something? But how about not just on a

Linus Skucas 12 Apr 14, 2022
Checks if there is a newer version of your app in the AppStore and alerts the user to update.

YiAppUpdater Checks if there is a newer version of your app in the AppStore and alerts the user to update. Installation YiAppUpdater is available thro

coderyi 4 Mar 17, 2022
This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to load remote images

iOS NOW ⭐ This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to loa

William Tristão de Paula 1 Dec 7, 2021
A basic iOS app that takes input from the user, displays it, allows changing both text color and background color.

Hello-iOSApp App Description A basic iOS app that takes input from the user, displays it, allows changing both text color and background color. App Wa

null 0 Jan 8, 2022
Truncate - An app that allows the user to talk with a chatbot about their mental health struggles

Project Description An app that allows the user to talk with a chatbot about the

Vincent Cloutier 0 Jul 15, 2022
SuggestionsBox helps you build better a product trough your user suggestions. Written in Swift. 🗳

SuggestionsBox An iOS library to aggregate users feedback about suggestions, features or comments in order to help you build a better product. Swift V

Manuel Escrig 100 Feb 6, 2022
Cross-Platform, Protocol-Oriented Programming base library to complement the Swift Standard Library. (Pure Swift, Supports Linux)

SwiftFoundation Cross-Platform, Protocol-Oriented Programming base library to complement the Swift Standard Library. Goals Provide a cross-platform in

null 620 Oct 11, 2022
ZIP Foundation is a library to create, read and modify ZIP archive files.

ZIP Foundation is a library to create, read and modify ZIP archive files. It is written in Swift and based on Apple's libcompression for high performa

Thomas Zoechling 1.9k Dec 27, 2022
Butterfly is a lightweight library for integrating bug-report and feedback features with shake-motion event.

Butterfly is a lightweight library for integrating bug-report and feedback features with shake-motion event. Goals of this project One of th

Zigii Wong 410 Sep 9, 2022
Focus is an Optics library for Swift (where Optics includes Lens, Prisms, and Isos)

Focus Focus is an Optics library for Swift (where Optics includes Lens, Prisms, and Isos) that is inspired by Haskell's Lens library. Introduction Foc

TypeLift 201 Dec 31, 2022
📘A library for isolated developing UI components and automatically taking snapshots of them.

A library for isolated developing UI components and automatically taking snapshots of them. Playbook Playbook is a library that provides a sandbox for

Playbook 969 Dec 27, 2022
A Swift micro library for generating Sunrise and Sunset times.

Solar A Swift helper for generating Sunrise and Sunset times. Solar performs its calculations locally using an algorithm from the United States Naval

Chris Howell 493 Dec 25, 2022
Plugin and runtime library for using protobuf with Swift

Swift Protobuf Welcome to Swift Protobuf! Apple's Swift programming language is a perfect complement to Google's Protocol Buffer ("protobuf") serializ

Apple 4.1k Dec 28, 2022
A Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and other native frameworks.

ZamzamKit ZamzamKit is a Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and othe

Zamzam Inc. 261 Dec 15, 2022