How to write unit test cases (XCTest) in UIWebView / WebKit - Swift - xcode

I want to write unit test cases (XCTest) in UIWebView/ Webkit- Swift
Please post any helpful link, example, or tutorial.
Thank you.
Shriram

A good way is to create fake navigation actions to call manually the delegate.
In this question you have a good example to write test cases of this way. unit-testing-wknavigationdelegate-functions
Example to test loading in navigation:
// setup
let fakeNavigation = WKNavigation()
delegateObject.refresh() // Set loading to true and init the web view
XCTAssertTrue(delegateObject.loading)
delegateObject.webView(webView, didFinish: fakeNavigation)
XCTAssertFalse(delegateObject.loading)
Example to test the policy:
class FakeNavigationAction: WKNavigationAction {
let testRequest: URLRequest
override var request: URLRequest {
return testRequest
}
init(testRequest: URLRequest) {
self.testRequest = testRequest
super.init()
}
}
// setup
var receivedPolicy: WKNavigationActionPolicy?
let fakeAction = FakeNavigationAction(testRequest: ...)
// act
delegateObject.webView(webView, decidePolicyFor: fakeAction, decisionHandler: {
receivedPolicy = $0
})
XCTAssertEqual(receivedPolicy, theExpectedValue)

Related

SwiftUI - How to get access to "WindowScene"

In watching the WWDC 21 videos reference StoreKit 2, there are a few functions that they reference wherein they let a value = WindowScene as follows:
func manageSubscriptions() async {
if let windowScene = self.view.window?.windowScene {
do {
try await AppStore.showManageSubscriptions(in: windowScene)
} catch {
//error
}
}
}
The let line errors out with the message: Type of expression is ambiguous without more context
If I try and provide more context with something like:
if let windowScene = (self.view.window?.windowScene)! as UIWindowScene {
I am told: Value of type 'MyStruct' has no member 'view'
What am I missing, must be something simple, to gain access to this needed UI element?
Thank you
Added:
I'd like to add that I am using a SwiftUI app that was created using a SceneDelegate and AppDelegate, not a simple struct: App, type of structure. So I am guessing I need to access something in the SceneDelegate to get the right object..
Just to provide an answer for anyone interested, with all credit to #aheze for finding it and #Matteo Pacini for the solution, to get this specific method to work when using a SwiftUI app that has an AppDelegate/SceneDelegate structure, this will work:
#MainActor
func manageSubscriptions() async {
if let windowScene = UIApplication.shared.connectedScenes.first {
do {
try await AppStore.showManageSubscriptions(in: windowScene as! UIWindowScene)
} catch {
//error
}
}
}
You can conversely use the view modifier manageSubscriptionsSheet(isPresented:) instead. This is Apple's recommended approach when using SwiftUI and will mitigate the need for getting a reference to the window scene.
Source:
If you’re using SwiftUI, call the manageSubscriptionsSheet(isPresented:)view modifier.

Xamarin.iOS - Unable to load view controller from within another view controller

I recently started developing using Xamarin, so I'm by no means an expert and have been stuck on this problem for a day or so now.
First of all, I am not using storyboards. I am creating my own custom views (xib) and loading them from code
I'm building a new Xamarin.iOS app and am attempting to load a view controller from within another view controller. Initially, I am loading the first controller from the AppDelegate like so:
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
window = new UIWindow(UIScreen.MainScreen.Bounds);
appStartUpController = new AppStartUpController();
window.RootViewController = appStartUpController;
window.MakeKeyAndVisible();
return true;
}
This loads my AppStartUpController fine which is basically just a loading screen with a background image and loading animation while I make an API call in the background. Once the API call has completed, I want to load another view controller.
After the API call has completed, I attempt to load the next Controller like so:
var controller = new CityPickerViewController();
this.NavigationController.PushViewController(controller, false);
And here is my CityPickerViewController:
public partial class CityPickerViewController : UIViewController
{
CityPicker_View v;
public CityPickerViewController(IntPtr handle) : base(handle)
{
}
public CityPickerViewController ()
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
v = CityPicker_View.Create();
this.View = v;
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(false);
UIImage i = UIImage.FromFile("citypickbackground.jpg");
i = i.Scale(this.View.Frame.Size);
this.View.BackgroundColor = UIColor.FromPatternImage(i);
}
}
I'm probably missing something obvious here, but the CityPickerViewController will not load. If I put a break point within the code, the viewDidLoad / ViewWillAppear overrides never get hit.
I'm a rookie programmer and would definitely appreciate any tips on this. Thanks in advance!
Welcome to SO!
Try base.NavigationController.PushViewController(controller, true); instead since you don't have a local navigation controller.
There could also be an issue in your CityPickerViewController, so try a different ViewController if that doesn't work.

How to port a network extension NEPacketTunnelProvider class from Obj-C/Swift to Xamarin C#?

I'm trying to figure out how to make a network extension so that my iOS app can programmatically open an custom VPN tunnel in C#, but looking at some similar Obj-C projects I'm not sure if it's possible in Xamarin (as I don't see a network extension project in Visual Studio) and how to port a what I gather is a required PacketTunnelProvider class which I think must be present and listed as an extension in the plist.info first...I'm in particular having most trouble in how to port the parts of that class which appear at the end as an extension and some event handlers named like this func Adapter(adapter: Adapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings, completionHandler: #escaping (AdapterPacketFlow?) -> Void) and func Adapter(adapter: Adapter, handleEvent event: AdapterEvent, message: String?) as they both have a different signature than an event handler in C# which accepts sender and eventArgs (or something derived)…if anyone did this in C# I'd like to know at least if it's possible if not how to port such a class?
I've found this one project https://github.com/ss-abramchuk/OpenVPNAdapter (since it seems to do most of what I want) that I managed to translate into a Xamarin binding library but I'm unsure how and if to incorporate its PacketTunnelProvider class in Xamarin (as that is what the readme says you should use to incorporate something like that adapter)...I gather one should add to plist.info something like this first:
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
but where do you go from there to use the binding library? This is the Obj-C code that says and seemingly does what I want to do to after i.e. add that custom VPN protocol tunnel to an app using the library:
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider : NEPacketTunnelProvider
{
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}
()
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
override func startTunnel(options: [String: NSObject]?, completionHandler: #escaping (Error?) -> Void)
{
// There are many ways to provide OpenVPN settings to the tunnel provider. For instance,
// you can use `options` argument of `startTunnel(options:completionHandler:)` method or get
// settings from `protocolConfiguration.providerConfiguration` property of `NEPacketTunnelProvider`
// class. Also you may provide just content of a ovpn file or use key:value pairs
// that may be provided exclusively or in addition to file content.
// In our case we need providerConfiguration dictionary to retrieve content
// of the OpenVPN configuration file. Other options related to the tunnel
// provider also can be stored there.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else
{
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else
{
fatalError()
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
configuration.settings = [
// Additional parameters as key:value pairs may be provided here
]
// Apply OpenVPN configuration
let properties: OpenVPNProperties
do
{
properties = try vpnAdapter.apply(configuration: configuration)
}
catch
{
completionHandler(error)
return
}
// Provide credentials if needed
if !properties.autologin {
// If your VPN configuration requires user credentials you can provide them by
// `protocolConfiguration.username` and `protocolConfiguration.passwordReference`
// properties. It is recommended to use persistent keychain reference to a keychain
// item containing the password.
guard let username: String = protocolConfiguration.username else
{
fatalError()
}
// Retrieve a password from the keychain
guard let password: String = ... {
fatalError()
}
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do
{
try vpnAdapter.provide(credentials: credentials)
}
catch
{
completionHandler(error)
return
}
}
// Checking reachability. In some cases after switching from cellular to
// WiFi the adapter still uses cellular data. Changing reachability forces
// reconnection so the adapter will use actual connection.
vpnReachability.startTracking { [weak self] status in
guard status != .notReachable else { return }
self?.vpnAdapter.reconnect(interval: 5)
}
// Establish connection and wait for .connected event
startHandler = completionHandler
vpnAdapter.connect()
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: #escaping () -> Void)
{
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings, completionHandler: #escaping (OpenVPNAdapterPacketFlow?) -> Void)
{
setTunnelNetworkSettings(settings) {
(error) in
completionHandler(error == nil ? self.packetFlow : nil)
}
}
// Process events returned by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?)
{
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error)
{
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else
{
return
}
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else
{
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String)
{
// Handle log messages
}
}
// Extend NEPacketTunnelFlow to adopt OpenVPNAdapterPacketFlow protocol so that
// `self.packetFlow` could be sent to `completionHandler` callback of OpenVPNAdapterDelegate
// method openVPNAdapter(openVPNAdapter:configureTunnelWithNetworkSettings:completionHandler).
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
How do I port to C# then or maybe I'm doing it all wrong (because of the comment bellow - the binding dll is bigger than 15MB - or is that limit in regard to use of memory which isn't related to file size)? Should I actually be just referencing the custom VPN library to open up a VPN tunnel from code directly and go on from there like it's business as usual (as I also found a project/app https://github.com/passepartoutvpn which uses a TunnelKit cocoapod, but that app's lib won't work with sharpie to make the binding library, and if so would the app like that even be admissible to the AppStore)? Thank you for any help in advance.
Per #SushiHangover advice, I've tried binding TunnelKit, as that project seemed smaller, and succeeded, partially... I've managed to build ~3MB dll, which seems much smaller than 21MB OpenVPNAdapter, and I think I'm almost there with the NetworkExtension project...I've got just to figure out the did I do ok with #escaping completionHandler and how to get some group constants which I guess should be set within the Host app somehow?
public override void StartTunnel(NSDictionary<NSString, NSObject> options, Action<NSError> completionHandler)
{
//appVersion = "\(GroupConstants.App.name) \(GroupConstants.App.versionString)";
//dnsTimeout = GroupConstants.VPN.dnsTimeout;
//logSeparator = GroupConstants.VPN.sessionMarker;
base.StartTunnel(options, completionHandler);
}
I've commented out the groupcontants for now but at least I'm hoping that's good enough porting of Swift3's:
override func startTunnel(options: [String : NSObject]?, completionHandler: #escaping (Error?) -> Void) {
appVersion = "\(GroupConstants.App.name) \(GroupConstants.App.versionString)"
dnsTimeout = GroupConstants.VPN.dnsTimeout
logSeparator = GroupConstants.VPN.sessionMarker
super.startTunnel(options: options, completionHandler: completionHandler)
}
If anyone else knows about the group constants and how to get them I'd be grateful (but I should also note that sharpie pod didn't give/expose any of those fields I should be assigning. Maybe I did it wrong as that's TunnelKit is a completely Swift3 project unlike the OpenVPNAdapter :/
Should I actually be just using the a custom VPN library to open up a VPN tunnel and go from there, but would the app then be admissible to the AppStore?
For iOS 12+, you absolutely have to use the Network Extension framework to be Store eligible.
The Xamarin.iOS build task (ValidateAppBundle) correctly identifies com.apple.networkextension.packet-tunnel as a valid extension (.appex) so yes you can build an NEPacketTunnelProvider extension.
You are correct the VS does not have build-in templating for Network Provider .appex's for tunnels, dns proxy, filter control|data, proxy types, but that does not mean you can not just use another one of the templates (or create the project from scratch) and modify it (I create an Xcode iOS project and start adding extension targets and just mirror those changes in VS).
(FYI: That is Swift code in your example, not ObjC...)
Now due to limitations in .appex size (and performance issues in some cases), a lot of extensions are very difficult to do via Xamarin.iOS. Most devs that encounter this, go native using ObjC/Swift for at least the appex development...

Distinguishing first launch on UI Tests

I want to use Fastlane Snapshot in order to generate screenshots for my app. But the behavior of the app is different when launch for the first time and launching after that.
How do I manage to get a consistent behavior in order to grab the screenshots?
(this question is also relevant for any UI test desired consistency I presume)
You should be using UserDefaults class to preserve data in your app so that you can stub data in your tests.
If we assume that the Bool key you use to see if it's the first launch is isFirstTime, in order to stub it in your UI test, you should pass it to launchArguments following the value YES or NO (for Bool values). Note that I added - sign before key, this is the way it works:
class FirstTimeLaunchTest: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchArguments += ["-isFirstTime", "YES"]
app.launch()
}
func testWelcomeScreenShown() {
XCTAssert(app.navigationBars["Welcome"].exists)
}
}
For tests where you'd like to have first start skipped, use this class:
class LaterLaunchesTest: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchArguments += ["-isFirstTime", "NO"]
app.launch()
}
func testMainAppScreenShown() {
XCTAssert(app.navigationBars["My App"].exists)
}
}
One note though: If you are using SwiftyUserDefaults library, this solution wouldn't work. There is a problem in the current version of the library where converting YES and NO strings to true and false is not working as expected. There was a PR that solved this problem (that is rejected), but to solve this problem, you can look at the solutions #2 and #3 from this answer.

Call to swift method from JavaScript hangs xcode and application

I am writing an iOS App (using xcode 7.3 and swift 2.2) using JavascriptCode framework. Calling javascript methods from swift works perfect, but when I call the swift method from javascript, xcode simply shows a "loading" type of symbol and nothing happens. I need to "force quit" xcode to get out of this state.
I have followed https://www.raywenderlich.com/124075/javascriptcore-tutorial and http://nshipster.com/javascriptcore/ and I am trying pretty simple calls.
Has anyone faced this kind of issue?
My swift code is as follows:
#objc protocol WindowJSExports : JSExport {
var name: String { get set }
func getName() -> String
static func createWindowWithName(name: String) -> WindowJS
}
#objc class WindowJS : NSObject, WindowJSExports {
dynamic var name: String
init(name: String) {
self.name = name
}
class func createWindowWithName(name: String) -> WindowJS {
return WindowJS(name: name)
}
func getName() -> String {
NSLog("getName called from JS context")
return "\(name)"
}
}
I am initializing the context as follows:
runContext = JSContext()
runContext.name = "test_Context"
windowToJs = WindowJS(name: "test")
runContext.setObject(windowToJs.self, forKeyedSubscript: "WindowJS")
If I replace the last two lines in above code with below code without instantiating it, the code simply fails to load.
runContext.setObject(WindowJS.self, forKeyedSubscript: "WindowJS")
And the javascript code is as simple as
function check() {
return WindowJS.getName()
}
I do see the breakpoint being hit in the JS function check and when the WindowJS.getName gets called, xcode simply becomes unresponsive.
The setTimeout could be solved by adding following piece of code to my swift function.
let setTimeout: #convention(block) (JSValue, Int) -> () =
{ callback, timeout in
let timeVal = Int64(timeout)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeVal), dispatch_get_main_queue(), { callback.callWithArguments(nil)})
}
To expose this native code to the JS context, I also added following.
runContext.setObject(unsafeBitCast(setTimeout, AnyObject.self), forKeyedSubscript: "setTimeout")
Things then worked fine.
You're creating a deadlock since you are calling from Swift to JavaScript back to Swift. I'm not sure exactly why it is a deadlock but I had a similar issue with WKWebView on Mac recently.
You need to decouple this and make the communication asynchronous. This obviously means you cannot simply return a value from your JS function in this case.
To decouple, you can break the deadlock by deferring the work the JavaScript function needs to do out of the current runloop iteration using setTimeout:
function myFunction() {
setTimeout(function() {
// The actual work is done here.
// Call the Swift part here.
}, 0);
}
The whole native ↔︎ JavaScript communication is very, very tricky. Avoid it if you can. There's a project called XWebView that may be able to help you as it tries to ease bridging between the two worlds.

Resources