Objective C - "Reset Content and Settings" programmatically in test files - objective-c

I am playing around with the new UI testing introduced in Xcode 7 beta. In one of my UI testing scenarios, I need to add some code that does the same thing as clicking Simulator -> Reset Content and Settings in the setup() of my test file, which is a XCTestCase. Can the reset be done programmatically? Or, can we mimic the effect of a factory reset on an app in test code?

Not entirely programmatically, but you can always write a bash file to delete:
${user.home}/Library/Application Support/iPhone Simulator/${simulator.version}
That will clear the settings on the simulator.
My understanding is that you won't be able to that from within your app, as apps are sandboxed.

Usually people were using shell scripts or apple scripts. However, using hard reset is absolutely not necessary.
You shouldn't care about data in other apps, you should care only about the data in your app. You can always delete your app data (files, user defaults) in the beginning of your tests. So, why should you do a hard reset?
A better solution is mocking. If your test supposes that, for example, some variable in NSUserDefaults is not set, you don't have to care about the actual value stored there, just mock the method your implementation is using (e.g. objectForKey: and let it return nil.

Related

Fetch preferences / permissions for NSUserNotificationCenter

I'm currently trying to set up a notification service using the old NSUserNotification API on macOSX (not iOS) in objective c++.
The catch is that I would need to know the system preferences associated with notifications that the user set (and preferably during the rest of runtime too) to determine if my notifications are actually seen. The application is meant to get the users attention in some way to guarantee things like reminders don't go unnoticed, so the thing I want to avoid is a notification delivered that silently doesn't produce any sound nor image on the screen whatsoever without the application knowing. If the notification is not allowed the application should handle it in some other way.
I am working on a macOS 10.14 Mojave machine. My overall code with NSUserNotificationCenter works fine with a custom Delegate that implements shouldDeliver, didActivate etc. Its just that I am currently assuming the user didn't press the "frick off" button on my notifications.
Unfortunately, NSUserNotificationCenter and related classes don't appear to feature a function that determines the permissions the way the new API (UNUserNotificationCenter) does. I have been trying to resolve this for a few hours so I was wondering if anyone else has found a solution to this. I can't really use the new API since this is strictly for back-compatibility in favour of the possibly rather prevalent amount of people who don't use mojave yet.
Things I have tried to make it work so far:
Try and see if there is a .plist somewhere where these settings are stored, in the User-specific Library folder as well as the general Library folder.
Try and find said .plist using CFPreferencesCopyKeyList and related methods. Apparently I don't really know the right domain name.
This answer from 2012 which appears to be outdated since I cannot locate the database on my machine anymore. Might be due to me using a mojave machine, or the file has since moved somewhere else.
Try and determine whether we can retro-actively check our permissions by checking a NSUserNotification's "presented" property on didDeliverNotification, shouldPresentNotification in the delegate. Unfortunately as stated in NSUserNotificationCenter itself it still behaves the same whether the preferences allow notifications or not.
Some code I tried includes the following:
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.systemPreferences.plist", kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.systemPreferences.plist", kCFPreferencesAnyUser, kCFPreferencesAnyHost);
//Unfortunately returns null with any combination of com.apple.notificationcenter.plist, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.notificationcenter", kCFPreferencesAnyUser, kCFPreferencesAnyHost);
On my system (only macOS High Sierra), it looks like the file you're after is:
~/Library/Preferences/com.apple.ncprefs.plist
That would correspond to a combination of kCFPreferencesCurrentUser, and kCFPreferencesAnyHost. (For the record, kCFPreferencesCurrentUser + kCFPreferencesCurrentHost equates to pref files in the ~/Library/Preferences/ByHost/ folder).
I don't think any settings would be stored in the local domain (by that I mean in the root /Library/Preferences/ folder).
I was able to get a list of apps and settings using the following code:
[[NSUserDefaults standardUserDefaults] addSuiteNamed:#"com.apple.ncprefs"];
NSArray *apps = [[NSUserDefaults standardUserDefaults] objectForKey:#"apps"];
NSLog(#"[%# %#] apps == %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), apps);
NOTE: this will not work if your app is sanboxed, as access to other pref files like that will be denied.

How can I automatically run an AppleScript when Safari.app starts?

Basically I want make sure I execute a custom written .applescript every time Safari is opened.
I tried adding the script to the "~/Library/Scripts/Applications/Safari" folder but that doesn't seem to automatically do it.
As a workaround I can probably just write an AppleScript that both starts Safari and runs my custom stuff. I just wanted to check whether there's a cleaner way of doing this.
Putting stuff in the ~/LibraryScripts/Applications folder just makes them available when the particular application is active.
In addition to your alias that runs the script and Safari, you could use some AppleScriptObj-C in a background application (maybe run via login items) that registers for NSWorkspaceDidLaunchApplicationNotification notifications to get the names of applications as they are launched. This would also have the advantage of seeing when Safari gets launched other than from your alias, such as via various document links (Mail, PDFs, etc):
use framework "Foundation"
my addObserver:me selector:"appLaunchNotification:" |name|:(current application's NSWorkspaceDidLaunchApplicationNotification) object:(missing value)
on appLaunchNotification:notification -- an application was launched
# notification's userInfo contains the application that was launched
set applicationInfo to NSWorkspaceApplicationKey of notification's userInfo -- NSRunningApplication
set appName to (localizedName of applicationInfo) as text
-- do your thing depending on appName
end appLaunchNotification:

Xcode 7+ Is there a way to Preserve App State after Recompiling

My application has deep navigational chains of 8+ screens deep (Wizard-style). When i'm editing a View Controller, and i want to make a quick visual change and re-test, As a tester, I have to go through the full flow to end up where I was before recompiling, in the same Data state. Is there a way that Xcode can somehow preserve the application state, and re-run the same view controller?
If there was an automated way to detect the last launched screen, and re-display it after the recompile, that would save a lot of developers a lot of time.
I realize something like this can be built custom. Something like:
IF A Certain Debug Flag is ON:
- Retrieve from NSUserDefaults the Class Name of the last controller used, and redisplay it.
The problem with this is: Data State and Navigation State will not be preserved. Also, all other object state which invokes and depends on your controller will not be included. That's why I need a more universal solution.
IF A Certain Debug Flag is ON: - Retrieve from NSUserDefaults the Class Name of the last controller used, and redisplay it.
The problem with this is: Data State and Navigation State will not be preserved.
But this is exactly the problem that the built-in state saving and restoration mechanism is intended to solve, is it not?
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/PreservingandRestoringState.html
I recommend you use a User Interface target and then create an XCTestCase that works on your view controller and exercise it. Use the new record button in Xcode 7 to get you started with the test case. Familiarize yourself with some of the new queries it uses for some of the dynamic information you may be creating.
Another option would be to inject your test case's UI state into the application delegate in the setup function of an XCTestCase. Then it will need custom logic to navigate to the correct view controller. Then your test case could focus on just that ViewController once the Application delegate has navigated to it.
Of course you can use the debugger and breakpoints in the Test Case and your View Controller while UI Testing. By "scripting" this you do significantly less manual clicking on a device to test a ViewCOntroller
Also you've now got a Test! Hopefully worth something to you down the road when you change something

Values not always persisted in App group between companion app & app extension

From time to time, but not always (I have had this working for a bit), the app/extension gets in a state where I can't read a flag set in my App Group between my companion app and my app extension. Don't know how it gets in this state or why the values differ, but it's critical to my application these always be in sync.
Companion app viewDidLoad:
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
.....
[myAppSettings setBool:true forKey:#"myBool"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"Companion app - bool TRUE" : #"Companion app - bool FALSE");
App extension viewDidLoad
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"App extension app - bool TRUE" : #"App extension - bool FALSE");
Console output
Companion app - bool TRUE
App extension - bool FALSE
I also synchronize before my companion app will enter background. I have my app group set up in the portal etc.
What am I doing wrong?
EDIT
Apparently others having this problem too:
https://devforums.apple.com/message/977151#977151
"I think that this is currently very glitchy.
Sometimes the data sharing works, then a change and all of a sudden the widget can't see the shared data anymore (both on Simulator and device).
Annoying and hope it's a bit more reliable in next beta!"
EDIT 2
Looks like another person has reported this exact issue as well:
"I also noticed the same thing too.This not only happen to the
NSUserDefaults, but also all the files in the container folder. The
keyboard extension suddenly will lose read/write pemission to the
container folder after using the keyboard for a while."
EDIT 3
More evidence: https://devforums.apple.com/message/1028078#1028078
After I upgrade to beta 3, I noticed that sometimes the keyboard
failed to open the database because it failed to access to the DB
file. The keyboard has been able to access to the file before.
EDIT 4
Seems like this could be because the keyboard loses the RequestsOpenAccess flag. But I can't reproduce it, and there's no way for me to tell for sure.
EDIT 5
Seems like others are reporting this in the iOS8 GM build:
This issue still persists for me in the GM. It seems related to a
keyboard crash.. but also there seems to be some contention between
keyboard and containing app in terms of who creates the suite in what
order. I think this problem is on Apple's end. Trust me, I WANT it to
be my fault but I've spent countless hours with trial and error. No
matter what I do in code and verify with NSLog, it will end up in this
state eventually. Hoping someone finds a magic pill. :S
Has anyone solved this yet?
You must request open access in order to access shared NSUserDefaults. It's stated directly in the App Extension Programming guide:
By default, a keyboard has no network access and cannot share a container with its containing app. To enable these things, set the value of the RequestsOpenAccess Boolean key in the Info.plist file to YES.
Be sure you change the RequestsOpenAccess field to YES. You'll find it in keyboard's Info.plist > NSExtension > NSExtensionAttributes > RequestOpenAccess. Then remove the keyboard in Settings, delete the app, run it again, and add the keyboard again. After you add it, tap on the keyboard name and then flip the switch to enable Allow Full Access. You'll need to instruct the users to follow those same steps to grant access (and reassure them you're not evil), otherwise it simply will not work and you'll never get the data that's stored in your shared container. Note that in iOS 8.3+, if the user hasn't enabled full access the keyboard will be able to access the shared container, but writing to it will not save the data, for security and privacy purposes. In 8.2- you can't access that data without open access granted.
I can confirm that the problem is related to RequestsOpenAccess flag.
Assuming that everything done right (NSUserDefaults use initWithSuiteName, all Capabilities for main application and custom keyboard were set, etc.) I have the next steps:
1) Install the main application and a custom keyboard on device
2) Set 'Allow full access' for the custom keyboard to YES
3) Add some items (in my case this is a simple text templates) in the main app
4) Go to keyboard and check that all items, that were added from the main app,
appeared in custom keyboard
5) Go to main app and add a few more items
6) Go to keyboard and now you will see that nothing changed
7) Go to settings and switch 'Allow full access' to NO and then to YES
8) Go to custom keyboard again and check that item which were added in step 5 appeared.

Couple service related "bugs"

I've written an application that can be invoked as a service (by right-clicking a file in Finder and selecting to open it with my application), but there are a couple of unwanted side-effects when doing this.
Example of service target method:
- (void)doSomething:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
NSLog(#"Selected file(s): %#", files);
[self.anotherWindow makeKeyAndOrderFront:self];
}
1) When the application is launched this way (even if already open in debug mode), I seem unable to access other windows/controls from the doSomething function. The above attempt to show "anotherWindow", for example, produces no errors, but doesn't "do" anything. According to the stack trace, when inspected from -doSomething, all gui components have values 0x0000000000000000 - yet the application is displayed and fully functional. It's only from -doSomething that I cannot reach them. "self" also has a different value when inspected from -doSomething versus -applicationDidFinishLaunching. I'm not sure how or why -doSomething is acquiring a different self/AppDelegate with uninitialized components. Seemingly fixed by [NSApp setServicesProvider:self];
2) I am not clear on how the system decides which copy of the application to launch when the service is invoked, but it usually doesn't pick the one I want. I have a copy in /Debug, a copy in /Release, a copy on my desktop... and if I delete one, it opens the file with another one instead (some sort of fallback-chain?). How do I configure the service (in code or thru .plist) to open a specific version/location of this app? But this is a dev machine. If I release a distributable which installs to /Applications, do I ever really need to worry about this?
1) Double-check your XIB to makes sure that you've got everything hooked up correctly and then try launching the app with a breakpoint set at the NSLog above and verify that self.anotherWindow points at what you want. If, for some reason, the breakpoint isn't firing, trying adding an:
NSLog( #"Window: %#", self.anotherWindow);
To make sure everything is initialized and hooked up
2) The system uses Launch Services to determine which version of the application to launch. Often it is the version most recently added to the system (which will cause the Launch Services database to be modified), but it is possible, depending on how your system is configured, that it won't be the version you expect.
You can manually inquire and modify the launch services database using:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister
(yeah, really long path). If you use the -dump option, that'll give you all of the data in the system (pipe into a file and search through it to get a better idea what's going on). If you search of the bundle id, you'll see all of the entries for the app. Generally, most recent wins, but you can force a reload (instructions below).
If you just want to force a reload based on a a particular binary, use the -f flag and the path to the application:
..../lsregister -f /Applications/Foo.app
You can also use -u to explicitly unregister something.
Hopefully this will give you an idea what's going on here.