Unit testing for IBAction connection with good form - swift

I've created a successful unit test that checks for an IBAction in Swift. However, the form isn't great:
func testAddButtonIBActionShouldExist() {
let actions=viewController.addButton.actionsForTarget(viewController, forControlEvent: UIControlEvents.TouchUpInside)
XCTAssertNotNil(actions, "Add button should have conections")
if actions != nil{
let actionsAsNSArray:NSArray=actions! as NSArray
XCTAssertTrue(actionsAsNSArray.containsObject("addNumbers:"), "Add button not connected to correct IBAction")
}
}
It separates the test for nil and the containsObject into two separate parts, and also includes an if… both of which I believe are bad unit testing form. Is there a way to somehow combine these in Swift 1.2 (or Swift 2)? Thanks for reading.

Related

How can I insert a textfield when running tests in swift?

I have this test:
func testLogin() throws {
viewController.email.insertText("email#outlook.com")
viewController.password.insertText("secret")
viewController.btnLogin?.sendActions(for: .touchDown)
XCTAssert(viewController.loggedIn == true)
}
But it gives me this error on the first line when I try to insert the email
Unexpectedly found nil while implicitly unwrapping an Optional value
How can I get this test to work?
This code block looks incomplete...
You can't directly access the UIElements for XCTests.
As a first step need to create application object
var app = XCUIApplication()
then you need to launch the app with
app.launch()
then access respective UI element with accessibilityIdentifier as
let email = app.textFields["identifier_of_emil_textfield"]
UI elements needs to have accessibilityIdentifier to work with XCTest framework. If you haven't assign this then you can as emailTxt.accessibilityIdentifier = "identifier_of_emil_textfield" or directly from identity inspector of respective XIB element.
and at last you can use typeText property to add your text like
email.typeText("email#outlook.com")

Text label not updating on screen, even though it shows up on `print()`

I'm writing an application which has an NSSplitViewController as the main View-Controller. I have it linked-up so that clicking a button in the menubar will trigger an #IBAction which then calls a function in one of the sub-View-Controllers.
if let board = storyboard {
let imageController = board.instantiateController(withIdentifier: "VC_image_ID") as! VC_image
imageController.viewDidAppear() // necessary or else I'll get an "Unexpectedly found nil" later on
DispatchQueue.global().async{imageController.processImage(path)} // path variable was set earlier in the code, but not shown here
}
Inside the sub-View-Controller (named VC_image), I'm trying to change the stringValue of a label by using the following code:
public func processImage(_ path: String) {
DispatchQueue.main.async {
self.imageText.stringValue = path
print(self.imageText.stringValue)
}
}
Although the imageText.stringValue actually seems to have changed based on the fact that it prints the updated text (through the console), the text in the window never changes. Why is this happening? The reason is probably really obvious to professionals, but I'm still an amateur and can't figure it out. Thanks.

compactMap on sequence() not lazy?

Every once in a while I have to walk up the responder chain to reach an instance of a known class. (Just accept this for purposes of the question.) I've been doing this with a while loop, but it occurred to me that it would be cooler to use sequence(), which can express the responder chain itself neatly like this:
let chain = sequence(first: someView as UIResponder) {$0.next}
That's brilliant because so far we haven't actually done any walking; the sequence is lazy and the anonymous function won't be executed until we start asking for elements. To prove that, let me instrument that code with a print statement:
let chain = sequence(first: someView as UIResponder) {r in print(r); return r.next}
Okay, so let's say I'm looking for the first ViewController instance in the chain. I can find it like this:
if let vc = (chain.first {$0 is ViewController}) as? ViewController {
print(vc)
}
The printout shows that laziness is maintained: we walked up the responder chain until we got to the ViewController and stopped. Perfect! Inside the curly braces, vc is typed as ViewController and we're off to the races.
It will not have escaped your attention, however, that that's ugly. I'm both testing and casting. Is there a way I can just cast without testing and still end up with a ViewController?
This is elegant and it works fine:
for case let vc as ViewController in chain {
print(vc)
break
}
That's lovely and laziness is maintained — but I have to remember to say break at the end, which sort of ruins everything.
Okay, so I was really hopeful when I thought of this:
if let vc = (chain.compactMap{ $0 as? ViewController }.first) {
print(vc)
}
It works in the sense that it compiles and gets the right answer and looks nice, but I've lost laziness. The entire chain is being traversed. Does compactMap lose laziness? Is there a way to get it back? (Or is there some other elegant way that has completely escaped me?)
The issue is not compactMap, per se. There are two issues:
If you want the sequence to call compactMap lazily, you need to use lazy.
It would appear that first is preventing the lazy behavior. If you use first(where:), though, you do enjoy the lazy behavior.
Thus, while somewhat inelegant, the following achieves what you’re looking for:
if let vc = (chain.lazy.compactMap { $0 as? ViewController }.first { _ in true } ) {
...
}
Or, as you say, you can implement first (or lazyFirst) on Sequence:
extension Sequence {
var first: Element? {
return first { _ in true }
}
}
And then this more simplified rendition now is still lazy:
if let vc = chain.lazy.compactMap({ $0 as? ViewController }).first {
...
}

scnScene.write not writing the scene to .scn file

I am working on a project in SceneKit that the user could change the object in the scene and want to be able to save the state of the scene when the user hit the submit button. In the documentation for SceneKit, there is a function call write: https://developer.apple.com/documentation/scenekit/scnscene/1523577-write. However, when I used it following the instruction, it doesn't write to the file. I also used the file scheme like they said in the documentation to create the URL: https://developer.apple.com/documentation/foundation/url.
Here is my code. Thank you for all your help. This is very new to me so any more advice would be much appreciated.
#IBAction func submitButton(_ sender: UIButton) {
let scnurl = NSURL.fileURL(withPath: "State.scn")
if(scnScene.write(to: scnurl, options: nil, delegate: nil, progressHandler: nil)){
print("tis success")
}
print("This button work")
}
The button does work because it prints out "This button work" but it doesn't print out "tis success" because the function return false.
I'd make use of the last parameter of the SCNScene.write() method, there's a progress handler there can will potentially give you an error with information if it fails.
I expect it's failing because of the URL you're giving it. Take a look at getting a URL from FileManager and append your filename to that.

How to obtain the selected text from the frontmost application in macOS?

I will soon be working on an application which needs to get the currently selected text in the frontmost application window, be it Safari, Pages, TextEdit, Word, etc., and do something with that text.
My goal is to find a solution that works with as much applications as possible. So far I thought about using AppleScript, but that would limit the amount of applications which could be used with my service. At least these common applications must be supported: Safari, Firefox (no AppleScript?), Word, Pages, Excel, TextEdit, ...
I also thought about keeping the clipboard's content in a temporary variable then simulating a text copy operation (Cmd-C), getting the text and then put the original content back in. This would probably highlight the Edit menu item when the copy operation is simulated and seems a bit hacky to me. IMO this solution doesn't seem good enough for a commercial product.
I am also looking to get more than the selection (i.e: the complete contents of the page in Safari or Word, etc.) to add some additional features in the future.
Any ideas/details on how to implement this behavior?
Thanks in advance for any hints!
N.B: I need to support at least 10.4 and up, but ideally older than 10.4 too.
UPDATE:
The solution I've opted for: Using the "Chain of Responsibility" design pattern (GOF) to combine 3 different input methods (Pasteboard, AppleScript and Accessibility), using the best available input source automatically.
Note that when using NSAppleScript's executeAndReturnError: method which returns an NSAppleEventDescriptor (let's say a "descriptor" instance), for the [descriptor stringValue] method to return something, in your AppleScript you must use "return someString" OUTSIDE of a "tell" block else nothing will be returned.
Here's the Swift 5.5 implementation of what is described in the accepted answer.
extension AXUIElement {
static var focusedElement: AXUIElement? {
systemWide.element(for: kAXFocusedUIElementAttribute)
}
var selectedText: String? {
rawValue(for: kAXSelectedTextAttribute) as? String
}
private static var systemWide = AXUIElementCreateSystemWide()
private func element(for attribute: String) -> AXUIElement? {
guard let rawValue = rawValue(for: attribute), CFGetTypeID(rawValue) == AXUIElementGetTypeID() else { return nil }
return (rawValue as! AXUIElement)
}
private func rawValue(for attribute: String) -> AnyObject? {
var rawValue: AnyObject?
let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &rawValue)
return error == .success ? rawValue : nil
}
}
Now, wherever you need to get the selected text from the frontmost application, you can just use AXUIElement.focusedElement?.selectedText.
As mentioned in the answer, this is not 100% reliable. So we're also implementing the other answer which simulates Command + C and copies from the clipboard. Also, ensure to remove the new item from the Clipboard if not required.
If you don't need selected text very frequently, you can programmatically press Command+C, then get the selected text from clipboard. But during my test, this is only works if you turn off App Sandbox (can't submit to Mac App Store).
Here is the Swift 3 code:
func performGlobalCopyShortcut() {
func keyEvents(forPressAndReleaseVirtualKey virtualKey: Int) -> [CGEvent] {
let eventSource = CGEventSource(stateID: .hidSystemState)
return [
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: true)!,
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: false)!,
]
}
let tapLocation = CGEventTapLocation.cghidEventTap
let events = keyEvents(forPressAndReleaseVirtualKey: kVK_ANSI_C)
events.forEach {
$0.flags = .maskCommand
$0.post(tap: tapLocation)
}
}
performGlobalCopyShortcut()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // wait 0.05s for copy.
let clipboardText = NSPasteboard.general().readObjects(forClasses: [NSString.self], options: nil)?.first as? String ?? ""
print(clipboardText)
}
Accessibility will work, but only if access for assistive devices is on.
You'll need to get the current application, then get its focused UI element, then get its selected text ranges and its value (whole text) and selected text ranges. You could just get its selected text, but that would either concatenate or ignore multiple selections.
Be prepared for any of those steps to fail: The app may not have any windows up, there may be no UI element with focus, the focused UI element may have no text, and the focused UI element may have only an empty selected text range.