I have an App with Today Widget. So I would like to perform some UI testing on it.
I found a way to open Today/Notifications panel. It seems easy:
let statusBar = XCUIApplication().statusBars.elementBoundByIndex(0)
statusBar.swipeDown()
But then I can't find a way to do something useful. It is possible to record UI interactions in Today/Notifications panel, but such code can't reproduce my actions.
First you need to open Today View, you can use this way:
let app = XCUIApplication()
// Open Notification Center
let bottomPoint = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 2))
app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)).press(forDuration: 0.1, thenDragTo: bottomPoint)
// Open Today View
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
springboard.scrollViews.firstMatch.swipeRight()
Then, to access everything you need, just use springboard, for example:
let editButton = springboard.buttons["Edit"]
There's a similar problem testing extensions. I've found that what you must do is tap the element at where it is on the screen rather than the element itself in order to drive the interaction. I haven't tested this with your scenario, but I haven't found anything un-tappable via this method yet.
Here is a Swift example of tapping the "X" button on the Springboard for an app icon, which similarly cannot be tapped via typical interaction:
let iconFrame = icon.frame // App icon on the springboard
let springboardFrame = springboard.frame // The springboard (homescreen)
icon.pressForDuration(1.3) // tap and hold
// Tap the little "X" button at approximately where it is. The X is not exposed directly
springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()
By getting the frame of the superview and the subview, you can calculate where on the screen the element should be. Note that coordinateWithNormalizedOffset takes a vector in the range [0,1], not a frame or pixel offset. Tapping the element itself at a coordinate doesn't work, either, so you must tap at the superview / XCUIApplication() layer.
More generalized example:
let myElementFrame = myElement.frame
let appFrame = XCUIApplication().frame
let middleOfElementVector = CGVectorMake(iconFrame.midX / appFrame.maxX, iconFrame.midY / appFrame.maxY)
// Tap element from the app-level at the given coordinate
XCUIApplication().coordinateWithNormalizedOffset(middleOfElementVector).tap()
If you need to access the Springboard layer and go outside your application, you can do so with:
let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
springboard.resolve()
But you'll need to expose some private XCUITest methods with Objective-C:
#interface XCUIApplication (Private) {
- (id)initPrivateWithPath:(id)arg1 bundleID:(id)arg2;
}
#interface XCUIElement (Private) {
- (void) resolve;
}
Related
I started out with the template project which you get when you choose ARKit project. As you run the app you can see the ship and view it from any angle.
However, once I allow camera control and tap on the screen or zoom into the ship through panning the ship gets stuck to camera. Now wherever I go with the camera the ship is stuck to the screen.
I went through the Apple Guide and seems like the don't really consider this as unexpected behavior as there is nothing about this behavior.
How to keep the position of the ship fixed after I zoom it or touch the screen?
Well, looks like allowsCameraControl is not the answer at all. It's good for SceneKit but not for ARKit(maybe it's good for something in AR but I'm not aware of it yet).
In order to zoom into the view a UIPinchGestureRecognizer is required.
// 1. Find the touch location
// 2. Perform a hit test
// 3. From the results take the first result
// 4. Take the node from that first result and change the scale
#objc private func handlePan(recognizer: UIPinchGestureRecognizer) {
if recognizer.state == .changed {
// 1.
let location = recognizer.location(in: sceneView)
// 2.
let hitTestResults = sceneView.hitTest(location, options: nil)
// 3.
if let hitTest = hitTestResults.first {
let shipNode = hitTest.node
let newScaleX = Float(recognizer.scale) * shipNode.scale.x
let newScaleY = Float(recognizer.scale) * shipNode.scale.y
let newScaleZ = Float(recognizer.scale) * shipNode.scale.z
// 4.
shipNode.scale = SCNVector3(newScaleX, newScaleY, newScaleZ)
recognizer.scale = 1
}
}
Regarding #2. I got confused a little with another hitTest method called hitTest(_:types:)
Note from documentation
This method searches for AR anchors and real-world objects detected by
the AR session, not SceneKit content displayed in the view. To search
for SceneKit objects, use the view's hitTest(_:options:) method
instead.
So that method cannot be used if you want to scale a node which is a SceneKit content
I managed to open the control center of the device, but i cannot identify the buttons, I need the Wi-Fi one more exactly. I tried with the recorder and it's identified as
app.scrollViews.otherElements.scrollViews.otherElements.switches["Wi-Fi"]
but when I try to run the test again, it fails as it does not find the element.
I also tried to find it as other kind of element(buttons or all kinds of bars elements), but nothing works. Also tried to identify it by its label simply using app.buttons["Wi-Fi"] and still no results.
Does anyone know a solution for this?
With Xcode 9 the Control Center is now accessible (the Springboard controls it). Right now it is only possible on a physical device because the Xcode 9 beta simulators don't have a control center. Maybe that will be fixed when Xcode is officially released. For now you have to use a real device.
This test opens the control center and taps on the WiFi Button:
func testSwitchOffWiFi() {
let app = XCUIApplication()
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
app.launch()
// open control center
let coord1 = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
let coord2 = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
coord1.press(forDuration: 0.1, thenDragTo: coord2)
let wifiButton = springboard.switches["wifi-button"]
wifiButton.tap()
}
That's rather easy done using the setting app...
First you know if the airplane mode icon is present if you query the taskbar.
XCUIElement* airplaneModeIcon = app.windows.otherElements[#"Airplane mode on"];
const bool isAirplaneModeEnabled = airplaneModeIcon.exists;
If after that you realize that you really need to set airplane mode to on or off, you need to launch the settings app.
XCUIApplication* settings = [[XCUIApplication alloc] initWithBundleIdentifier:#"com.apple.Preferences"];
[settings launch];
XCUIElement* airplaneModeCell = settings.tables.cells[#"Airplane Mode"];
// Do what you have to do with the Cell...
Here's an adaptation of joern's answer for iPhones with notch. Also, we don't need to refer to the app under test at all - springboard is enough.
func toggleWiFi() {
let springboard = XCUIApplication(bundleIdentifier:"com.apple.springboard")
// expand control center
let start = springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.01))
let end = springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.2))
start.press(forDuration: 0.1, thenDragTo: end)
// perform the action
let wifiButton = springboard.switches["wifi-button"]
wifiButton.tap()
// hide control center
let empty = springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.1))
empty.tap()
}
}
The control centre is outside the scope of your application under test and therefore cannot be accessed by your UI tests.
To disable wifi, you need to physically disconnect the device from the Internet, as it's not possible to disconnect from wifi programmatically.
I am curious if it is possible to fire images from the middle of the screen, to anywhere on the screen with a random speed.
It is important that it will be done without files containing .sks, since Xcode is crashing when I try opening that kind of files. It has to be done in a UIViewController.
I already tried subclassing a view to a SKView but without success. Looking at existing projects always uses SKViews and files containing .sks which I can not open. I hope somebody can help me out!
Each of the images you need to "fire" should be an SKSpriteNode. They should be children of a SKScene. If you don't know how to create a SKScene or how to make the view controller present it, try creating a sample game project in Xcode - File > New Project > Game. It would create a nice sample project that would display some nice rectangles whenever you touch the screen.
Once you have your scene up and running, as well as your sprite nodes ready to fire, simply run these few lines for every node you need to fire from the middle of the screen:
let viewSize = UIScreen.main.bounds.size
let randomX = RandomInt(min: 0, max: viewSize.width)
let randomY = RandomInt(min: 0, max: viewSize.height)
let randomPosition = CGPoint(x: randomX, y: randomY)
let minDuration = 1 // Put here whatever value you feel fit for the minimum duration of flying
let maxDuration = 5 // Put here whatever value you feel fit for the maximum duration of flying
let randomDuration = TimeInterval(RandomInt(min: minDuration, max: maxDuration))
let fireAtWill = SKAction.move(to: randomPosition, duration: randomSpeed)
yourSpriteNode.runAction(fireAtWill)
Don't forget to replace "yourSpriteNode" with the name of the sprite node you need to fire.
Here is the RandomInt helper function:
func RandomInt(min: Int, max: Int) -> Int {
return Int(arc4random_uniform(UInt32(max-(min-1)))) + min
}
Your problem with sks files is an odd one - you may want to ask a separate question on that with more details.
You can use SpriteKit without sks files though - they are just a shortcut but everything can be done in code if you need/want to. See this SO answer for a discussion, an example and some useful links.
I am getting a very strange crash while trying to change the position of an SKSpriteNode. This crash happens in iOS8 and not in iOS9.
// if the powerup is full, spawn the icon on screen
if orangeBallsCollected == numberOfBallsRequiredToActivatePowerup {
// add the powerup icon to the array of powerupiconsonscreen
powerupIconsOnScreen.append(fewerColorsPowerupIcon)
// set the lighting mask of the powerup icon so that we know what positon it is onscreen for alter
fewerColorsPowerupIcon.lightingBitMask = UInt32(powerupIconsOnScreen.count - 1)
// remove the ball from its current parent
fewerColorsPowerupIcon.removeFromParent()
print(fewerColorsPowerupIcon, fewerColorsPowerupIcon.parent, fewerColorsPowerupIcon.physicsBody, fewerColorsPowerupIcon.superclass)
// place the ball of the screen so that we can bring it on later
fewerColorsPowerupIcon.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
fewerColorsPowerupIcon.xScale = scaleFactor
fewerColorsPowerupIcon.yScale = scaleFactor
// add it the scene
self.addChild(fewerColorsPowerupIcon)
// animate it moving down to the first avaliable position
let animation = SKAction.moveTo(CGPoint(x: width * 0.1, y: height * 0.1), duration: 0.5)
// run the animation!
fewerColorsPowerupIcon.runAction(animation)
// activate the poweurp
activateFewerColors()
}
The crash happens when I try to set the position (fewerColorsPowerupIcon.position) and this is the crash message:
Thread 1: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)
This crash still happens if I put the .removeFromParent() piece of code after I set the position of the node.
I think that if you remove the sprite, you can't do anithing.
so try to do this:
let spriteCopy = fewerColorsPowerupIcon.copy()
// place the ball of the screen so that we can bring it on later
spriteCopy.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
spriteCopy.xScale = scaleFactor
spriteCopy.yScale = scaleFactor
// add it the scene
self.addChild(spriteCopy)
or you can first add in the scene and after change property:
// remove the ball from its current parent
fewerColorsPowerupIcon.removeFromParent()
// add it the scene
self.addChild(fewerColorsPowerupIcon)
// place the ball of the screen so that we can bring it on later
fewerColorsPowerupIcon.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
fewerColorsPowerupIcon.xScale = scaleFactor
fewerColorsPowerupIcon.yScale = scaleFactor
If fewerColorsPowerupIcon were declared something like:
weak var fewerColorsPowerupIcon: Type!
Then as soon as you call removeFromParent(), there might be no strong references to it any more. If that were so, you are not guaranteed that fewerColorsPowerupIcon will be set to nil or that it would be detected. The next time you use it, you might get EXC_BAD_ACCESS.
One way to find something like this is to use the zombie instrument. Under it, no objects are really freed when they have no references, but they complain if you try to use them once all strong references are released.
Another option is to change the ! to ? (and update the code that uses the var). It might be a pain, but there are more guarantees about how ? acts.
Is there any way to automate SFSafariViewController? I like the Xcode 7 UI test feature, but it seems it does not support SFSafariViewController automation. Some of the UI flows I am testing require a web browser so the app uses SFSafariViewController to make it safer vs a web view.
If it's similar to launching extensions (currently broken with direct interactions), try tapping the screen at the point where the element you're looking for is:
Example of tapping an action sheet which launches an extension:
func tapElementInActionSheetByPosition(element: XCUIElement!) {
let tableSize = app.tables.elementBoundByIndex(0).frame.size
let elementFrame = element.frame
// get the frame of the cancel button, because it has a real origin point
let CancelY = app.buttons["Cancel"].frame.origin.y
// 8 is the standard apple margin between views
let yCoordinate = CancelY - 8.0 - tableSize.height + elementFrame.midY
// tap the button at its screen position since tapping a button in the extension picker directly is currently broken
app.coordinateWithNormalizedOffset(CGVectorMake(elementFrame.midX / tableSize.width, yCoordinate / app.frame.size.height)).tap()
}
Note: You must tap at the XCUIApplication query layer. Tapping the element by position doesn't work.
For now Xcode 9.3 has support for that, but it doesn't work properly because of annoying Xcode bug.
In test you can print app.webViews.buttons.debugDescription or app.webViews.textFields.debugDescription, and it prints correct information, but after tap or typeText you have crash.
To workaround you can parse debugDescription, extract coordinates and tap by coordinate. For text field you can insert text via "Paste" menu.
private func coordinate(forWebViewElement element: XCUIElement) -> XCUICoordinate? {
// parse description to find its frame
let descr = element.firstMatch.debugDescription
guard let rangeOpen = descr.range(of: "{{", options: [.backwards]),
let rangeClose = descr.range(of: "}}", options: [.backwards]) else {
return nil
}
let frameStr = String(descr[rangeOpen.lowerBound..<rangeClose.upperBound])
let rect = CGRectFromString(frameStr)
// get the center of rect
let center = CGVector(dx: rect.midX, dy: rect.midY)
let coordinate = XCUIApplication().coordinate(withNormalizedOffset: .zero).withOffset(center)
return coordinate
}
func tap(onWebViewElement element: XCUIElement) {
// xcode has bug, so we cannot directly access webViews XCUIElements
// as workaround we can check debugDesciption, find frame and tap by coordinate
let coord = coordinate(forWebViewElement: element)
coord?.tap()
}
Article about that: https://medium.com/#pilot34/work-with-sfsafariviewcontroller-or-wkwebview-in-xcode-ui-tests-8b14fd281a1f
Full code is here: https://gist.github.com/pilot34/09d692f74d4052670f3bae77dd745889