Reusable XCTests for several products - xcode

I need UI tests for XCode project that is a platfrom for several products.
It means that some elements are custom for some products. For instance, they can have different colors, text styles, etc. from product to product, or the same element could be visible for one project, but hided for another one.
How can I configure my XCode UI tests to make them reusable for various products? I understand I need different schemas. But what about visability elements, for example? It seems I need to check it inside the UI test code? But I think it would be better to use any config file. Am I right? Does anyone have any ideas? I'd be grateful for all advices.

I recommend building out a suite of test helpers that you use in each of your products. These are generic, parameterized functions, such as logging a user in or adding an item to the shopping cart. Instead of hard coding your UI element representations in these helpers, parameterize the inputs. Then you can use them over and over.
func addItemToCart(named: String, saveButtonName: String)
func login(username: String, password: String, submitButtonText: String)
func tapTableCell(imageNamed: String)
Once you create a basic scaffolding for navigation you can move on to assertion helpers. Leaving the complex logic in the helpers enables you to reuse them and keep your product-specific tests lean and readable.
func assertCurrentScreen(named: String)
func assertHighlightedCell(colorNamed: String)
func assertCartTotal(cents: String, containerIdentifier: String)
For all of these functions, I recommend adding two default parameters at the end to note the callers file and line number. If you make any custom assertions you can then pass these references in to show your failure at the callers line, not the helpers.
func assertScreen(titled: String, file: StaticString = #file, line: UInt = #line) {
if !XCUIApplication().navigationBars[titled].exists {
XCTFail("Item was not added to cart.", file: file, line: line)
}
}

I am using a function at the beginning of each test.
class SomeTestsClass: XCTestCase {
func testSomeTest() {
var externalConfigVariable_1 = "value for defult test"
var externalConfigVariable_2 = "value for defult test"
// here You use the external config to override
// the default test logc
if let testConfig = getConfig(for: self.name) {
// read config parameters here
externalConfigVariable_1 = testConfig["test_var_1"]
externalConfigVariable_2 = testConfig["test_var_2"]
// ..........
}
// use the variables as You like
// ......
}
}
extension XCTestCase {
public func getConfig(for name: String?) -> [String: Any]? {
let nameComponents = name?.replacingOccurrences(of: "-", with: "").replacingOccurrences(of: "[", with: "").replacingOccurrences(of: "]", with: "").components(separatedBy: " ")
if let fileName = nameComponents?.last {
let testBundle = Bundle(for: type(of: self))
guard let path = testBundle.url(forResource: fileName, withExtension: "JSON") else {
return nil
}
if let data = try? Data(contentsOf: path) {
if let testConfig = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: Any] {
return testConfig
}
}
}
return nil
}
}
Here is an example JSON:
{
"test_var_1": "Some var",
"test_var_2": "Some other var"
}

Related

is anyone able to restrict the type of the objects dropped on the mac in SwiftUI 3?

as per the documentation, it should be pretty straightforward. example for a List: https://developer.apple.com/documentation/swiftui/list/ondrop(of:istargeted:perform:)-75hvy#
the UTType should be the parameter restricting what a SwiftUI object can receive. in my case i want to accept only Apps. the UTType is .applicationBundle: https://developer.apple.com/documentation/uniformtypeidentifiers/uttype/3551459-applicationbundle
but it doesn't work. the SwiftUI object never changes status and never accepts the drop. the closure is never run. whether on Lists, H/VStacks, Buttons, whatever. the pdf type don't seem to work either, as well as many others. the only type that i'm able to use if fileURL, which is mainly like no restriction.
i'm not sure if i'm doing something wrong or if SwiftUI is half working for the mac.
here's the code:
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.applicationBundle, .application], isTargeted: isTargeted) { providers in
print("hehe")
return true
}
replacing or just adding .fileURL in the UTType array makes the drop work but without any type restriction.
i've also tried to use .onInsert on a ForEach instead (https://developer.apple.com/documentation/swiftui/foreach/oninsert(of:perform:)-2whxl#), and to go through a proper DropDelegate (https://developer.apple.com/documentation/swiftui/dropdelegate#) but keep getting the same results. it would seem the SwiftUI drop for macOS is not yet working, but i can't find any official information about this. in the docs it is written macOS 11.0+ so i would expect it to work?
any info appreciated! thanks.
You need to validate manually, using DropDelegate of what kind of file is dragged over.
Here is a simplified demo of possible approach. Tested with Xcode 13 / macOS 11.6
let delegate = MyDelegate()
...
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.fileURL], delegate: delegate) // << accept file URLs
and verification part like
class MyDelegate: DropDelegate {
func validateDrop(info: DropInfo) -> Bool {
// find provider with file URL
guard info.hasItemsConforming(to: [.fileURL]) else { return false }
guard let provider = info.itemProviders(for: [.fileURL]).first else { return false }
var result = false
if provider.canLoadObject(ofClass: String.self) {
let group = DispatchGroup()
group.enter() // << make decoding sync
// decode URL from item provider
_ = provider.loadObject(ofClass: String.self) { value, _ in
defer { group.leave() }
guard let fileURL = value, let url = URL(string: fileURL) else { return }
// verify type of content by URL
let flag = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle
result = flag ?? false
}
// wait a bit for verification result
_ = group.wait(timeout: .now() + 0.5)
}
return result
}
func performDrop(info: DropInfo) -> Bool {
// handling code is here
return true
}
}

ORSSerialPort doesn't send data

I am junior Swift user but on this stage of life I need use serial connection in my project.
when I looking for availeble port ,I see them without problem but when try to send something I have problem why?
I have problem with ORSSerial this my code:
func applicationDidFinishLaunching(_ aNotification: Notification) {
let portEvString = ORSSerialPortManager.shared().availablePorts
let pathString = portEvString[0].path
let portClass = SerialController(path:pathString)
portClass.open()
portClass.SendString(data: "hello")
// Insert code here to initialize your application
RunLoop.current.run()
}
class SerialController : NSObject, ORSSerialPortDelegate {
var port : ORSSerialPort?
init(path: String){
port = ORSSerialPort(path: path)
port?.close()
}
func open(){
port?.baudRate=9600
port?.delegate=self
port?.open()
}
func close(){
port?.delegate=nil
port?.close()
}
func SendString(data: String){
let dataa = Data(data.utf8)
port?.send(dataa)
}
func serialPortWasOpened(serialPort: ORSSerialPort!) {
print("PORT IS OPEN....")
}
func serialPortWasClosed(serialPort: ORSSerialPort!) {
print("PORT IS CLOSE")
}
func serialPort(serialPort: ORSSerialPort!, didReceiveData data: NSData!) {
print(NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue))
}
func serialPortWasRemovedFromSystem(_ serialPort: ORSSerialPort!) {
print("PORT REMOVED")
}
func serialPort(serialPort: ORSSerialPort!, didEncounterError error: NSError!) {
print("PORT ERR \(error)")
}
}
I believe the primary problem is that you don't store the instance of SerialController in a strong reference, so it gets deallocated immediately after you call SendString() on it. You need to store it in a property like so:
var serialController: SerialController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let availablePorts = ORSSerialPortManager.shared().availablePorts
let pathString = availablePorts[0].path
serialController = SerialController(path:pathString)
serialController?.open()
serialController?.SendString(data: "hello")
}
Beyond that problem, there are a number of things about this code that you should fix. The things that stick out most:
Don't call RunLoop.current.run() in applicationDidFinishLaunching(_:). You rarely need to manually run the run loop, and you definitely don't need to here.
As documented in ORSSerialPort, you must release any strong references to the port in your implementation of serialPortWasRemovedFromSystem(_:). That means in that method, you need to do port = nil.
The force unwrap operators (!) scattered through this code make me wonder if you're using an old version of ORSSerialPort, from before I added nullability annotations. Please make sure you're using the latest version (2.0.2 as of this writing).
Use more descriptive names for your variables. For example, portEvString is not a good name, because it's difficult to understand (what does "Ev" mean?), and its type is not a string, it's an array of ORSSerialPort objects (ie. [ORSSerialPort].
Your SendString method shouldn't start with a capital letter. Function (and method) names in Swift should always start with a lowercase letter.
After the disable SandBox, everything is OK.

didChangeAutomaticCapitalizationNotification not triggered

What am I doing wrong? I don't get this notification. I have this function:
#objc func onAutocorrection (_ notification: Foundation.Notification) {
Swift.print("\(notification)")
}
later in the same class I do use it as follows:
NotificationCenter.default.addObserver(
self,
selector: #selector(onAutocorrection(_:)),
name: NSSpellChecker.didChangeAutomaticCapitalizationNotification,
object: nil)
The addObserver is executed, but the function is never called even when the application is capitalising in an NSTextView.
Why? Many thanks in advance!
It looks like I misunderstood the notification. It is not meant to be triggered when automatic capitalisation happens but when the systems preference of your Mac is changing.
See the comment of ever helpful Willeke and see Notification of autocorrect
In order to get to the intended result of reacting to autocapitalisation did I implement this function in the NSTextViewDelegate:
public func textView(_ view: NSTextView, didCheckTextIn range: NSRange, types checkingTypes: NSTextCheckingTypes, options: [NSSpellChecker.OptionKey : Any] = [:], results: [NSTextCheckingResult], orthography: NSOrthography, wordCount: Int) -> [NSTextCheckingResult] {
if !range.contains(0){
return results
}
var newResult = [NSTextCheckingResult]()
for result in results {
if let textToChange = view.string[range].components(separatedBy: " ").first, let replacement = result.replacementString?.components(separatedBy: " ").first {
let firstLetterCap = textToChange.capitalizingFirstLetter()
if replacement == firstLetterCap {
continue //don't add to results
}
}
newResult.append(result)
}
return newResult
}
This function will prevent that the first character will be capitalised.
Ultimately, I check whether the capitalised version of the first word of the range that must include position "0" is equal to the first word of the replacement string. And if it is then I remove that result/suggestion from the result list.

Passing NSOpenPanel options to NSDocument

I know how to add an AccessoryView to an NSOpenPanel (and that works correctly).
Now I would like to make the options that the user selects in the AccessoryView available to the document that is opened.
Any suggestions how that can be doen (if at all?)
I have not found a standard solution, so I created my own:
Introduced a dictionary in the NSDocumentController that associates file URLs with option sets
Override the runModalOpenPanel and wrap the runModalOpenPanel of super with first the setup of the accessory view, and afterwards the evaluation of the options and adding of the options to the dictionary for the associated urls.
When a document is opened, the document can -through the shared NSDocumentController- access the dictionary and retrieve the options.
I am not blown away by this solution, but I also do not see an easier path.
Example code:
struct OptionsAtFileOpen {
let alsoLoadFormat: Bool
}
class DocumentController: NSDocumentController {
var fileOptions: Dictionary<URL, OptionsAtFileOpen> = [:]
var accessoryViewController: OpenPanelAccessoryViewController!
override func runModalOpenPanel(_ openPanel: NSOpenPanel, forTypes types: [String]?) -> Int {
// Load accessory view
let accessoryViewController = OpenPanelAccessoryViewController(nibName: NSNib.Name(rawValue: "OpenPanelAccessoryView"), bundle: nil)
// Add accessory view and make sure it is shown
openPanel.accessoryView = accessoryViewController.view
openPanel.isAccessoryViewDisclosed = true
// Run the dialog
let result = super.runModalOpenPanel(openPanel, forTypes: types)
// If not cancelled, add the files to open to the fileOptions dictionary
if result == 1 {
// Return the state of the checkbox that selects the loading of the formatting file
let alsoLoadFormat = accessoryViewController.alsoLoadFormatFile.state == NSControl.StateValue.on
for url in openPanel.urls {
fileOptions[url] = OptionsAtFileOpen(alsoLoadFormat: alsoLoadFormat)
}
}
return result
}
}
And then in Document
override func read(from data: Data, ofType typeName: String) throws {
...
if let fileUrl = fileURL {
if let dc = (NSDocumentController.shared as? DocumentController) {
if let loadFormat = dc.fileOptions[fileUrl]?.alsoLoadFormat {
...
}
}
}
}

Can I mix UIKit and TVMLKit within one app?

I'm exploring tvOS and I found that Apple offers nice set of templates written using TVML. I'd like to know if a tvOS app that utilises TVML templates can also use UIKit.
Can I mix UIKit and TVMLKit within one app?
I found a thread on Apple Developer Forum but it does not fully answer this question and I am going through documentation to find an answer.
Yes, you can. Displaying TVML templates requires you to use an object that controls the JavaScript Context: TVApplicationController.
var appController: TVApplicationController?
This object has a UINavigationController property associated with it. So whenever you see fit, you can call:
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
This allows you to push a Custom UIKit viewcontroller onto the navigation stack. If you want to go back to TVML Templates, just pop the viewController off of the navigation stack.
If what you would like to know is how to communicate between JavaScript and Swift, here is a method that creates a javascript function called pushMyView()
func createPushMyView(){
//allows us to access the javascript context
appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
//this is the block that will be called when javascript calls pushMyView()
let pushMyViewBlock : #convention(block) () -> Void = {
() -> Void in
//pushes a UIKit view controller onto the navigation stack
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
}
//this creates a function in the javascript context called "pushMyView".
//calling pushMyView() in javascript will call the block we created above.
evaluation.setObject(unsafeBitCast(pushMyViewBlock, AnyObject.self), forKeyedSubscript: "pushMyView")
}, completion: {(Bool) -> Void in
//done running the script
})
}
Once you call createPushMyView() in Swift, you are free to call pushMyView() in your javascript code and it will push a view controller onto the stack.
SWIFT 4.1 UPDATE
Just a few simple changes to method names and casting:
appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in
and
evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)
As mentioned in the accepted answer, you can call pretty much any Swift function from within the JavaScript context. Note that, as the name implies, setObject:forKeyedSubscript: will also accept objects (if they conform to a protocol that inherits from JSExport) in addition to blocks, allowing you to access methods and properties on that object. Here's an example
import Foundation
import TVMLKit
// Just an example, use sessionStorage/localStorage JS object to actually accomplish something like this
#objc protocol JSBridgeProtocol : JSExport {
func setValue(value: AnyObject?, forKey key: String)
func valueForKey(key: String) -> AnyObject?
}
class JSBridge: NSObject, JSBridgeProtocol {
var storage: Dictionary<String, String> = [:]
override func setValue(value: AnyObject?, forKey key: String) {
storage[key] = String(value)
}
override func valueForKey(key: String) -> AnyObject? {
return storage[key]
}
}
Then in your app controller:
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
let bridge:JSBridge = JSBridge();
jsContext.setObject(bridge, forKeyedSubscript:"bridge");
}
Then you can do this in your JS: bridge.setValue(['foo', 'bar'], "baz")
Not only that, but you can override views for existing elements, or define custom elements to use in your markup, and back them with native views:
// Call lines like these before you instantiate your TVApplicationController
TVInterfaceFactory.sharedInterfaceFactory().extendedInterfaceCreator = CustomInterfaceFactory()
// optionally register a custom element. You could use this in your markup as <loadingIndicator></loadingIndicator> or <loadingIndicator /> with optional attributes. LoadingIndicatorElement needs to be a TVViewElement subclass, and there are three functions you can optionally override to trigger JS events or DOM updates
TVElementFactory.registerViewElementClass(LoadingIndicatorElement.self, forElementName: "loadingIndicator")
Quick custom element example:
import Foundation
import TVMLKit
class LoadingIndicatorElement: TVViewElement {
override var elementName: String {
return "loadingIndicator"
}
internal override func resetProperty(resettableProperty: TVElementResettableProperty) {
super.resetProperty(resettableProperty)
}
// API's to dispatch events to JavaScript
internal override func dispatchEventOfType(type: TVElementEventType, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//super.dispatchEventOfType(type, canBubble: canBubble, cancellable: isCancellable, extraInfo: extraInfo, completion: completion)
}
internal override func dispatchEventWithName(eventName: String, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//...
}
}
And here's how to set up a custom interface factory:
class CustomInterfaceFactory: TVInterfaceFactory {
let kCustomViewTag = 97142 // unlikely to collide
override func viewForElement(element: TVViewElement, existingView: UIView?) -> UIView? {
if (element.elementName == "title") {
if (existingView != nil) {
return existingView
}
let textElement = (element as! TVTextElement)
if (textElement.attributedText!.length > 0) {
let label = UILabel()
// Configure your label here (this is a good way to set a custom font, for example)...
// You can examine textElement.style or textElement.textStyle to get the element's style properties
label.backgroundColor = UIColor.redColor()
let existingText = NSMutableAttributedString(attributedString: textElement.attributedText!)
label.text = existingText.string
return label
}
} else if element.elementName == "loadingIndicator" {
if (existingView != nil && existingView!.tag == kCustomViewTag) {
return existingView
}
let view = UIImageView(image: UIImage(named: "loading.png"))
return view // Simple example. You could easily use your own UIView subclass
}
return nil // Don't call super, return nil when you don't want to override anything...
}
// Use either this or viewForElement for a given element, not both
override func viewControllerForElement(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {
if (element.elementName == "whatever") {
let whateverStoryboard = UIStoryboard(name: "Whatever", bundle: nil)
let viewController = whateverStoryboard.instantiateInitialViewController()
return viewController
}
return nil
}
// Use this to return a valid asset URL for resource:// links for badge/img src (not necessary if the referenced file is included in your bundle)
// I believe you could use this to cache online resources (by replacing resource:// with http(s):// if a corresponding file doesn't exist (then starting an async download/save of the resource before returning the modified URL). Just return a file url for the version on disk if you've already cached it.
override func URLForResource(resourceName: String) -> NSURL? {
return nil
}
}
Unfortunately, view/viewControllerForElement: will not be called for all elements. Some of the existing elements (like collection views) will handle the rendering of their child elements themselves, without involving your interface factory, which means you'll have to override a higher level element, or maybe use a category/swizzling or UIAppearance to get the effect you want.
Finally, as I just implied, you can use UIAppearance to change the way certain built-in views look. Here's the easiest way to change the appearance of your TVML app's tab bar, for example:
// in didFinishLaunching...
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)
If you already have a native UIKit app for tvOS, but would like to extend it by using TVMLKit for some part of it, You can.
Use the TVMLKit as a sub app in your native tvOS app. The following app shows how to do this, by retaining the TVApplicationController and present the navigationController from the TVApplicationController. The TVApplicationControllerContext is used to transfer data to the JavaScript app, as the url is transferred here :
class ViewController: UIViewController, TVApplicationControllerDelegate {
// Retain the applicationController
var appController:TVApplicationController?
static let tvBaseURL = "http://localhost:9001/"
static let tvBootURL = "\(ViewController.tvBaseURL)/application.js"
#IBAction func buttonPressed(_ sender: UIButton) {
print("button")
// Use TVMLKit to handle interface
// Get the JS context and send it the url to use in the JS app
let hostedContContext = TVApplicationControllerContext()
if let url = URL(string: ViewController.tvBootURL) {
hostedContContext.javaScriptApplicationURL = url
}
// Save an instance to a new Sub application, the controller already knows what window we are running so pass nil
appController = TVApplicationController(context: hostedContContext, window: nil, delegate: self)
// Get the navigationController of the Sub App and present it
let navc = appController!.navigationController
present(navc, animated: true, completion: nil)
}
Yes. See the TVMLKit Framework, whose docs start with:
The TVMLKit framework enables you to incorporate JavaScript and TVML files in your binary apps to create client-server apps.
From a quick skim of those docs, it looks like you use the various TVWhateverFactory classes to create UIKit views or view controllers from TVML, after which you can insert them into a UIKit app.

Resources