How to wait for Location Alert ( which is system alert) to show? - xcode8

I figured out how to dismiss System alert, but I am not able to wait for it to show , since app doesn't see System Alerts. I tried to debug with app.debugDescription and app.alerts.count but no luck.

You should use addUIInterruptionMonitor as #Oletha wrote.
The tricky part here is that system alert buttons don't use Accessibility Identifiers so you have to search for text to tap them. This text is translated to the language you're running your simulator/device, which can be hard if you want to run the test for several languages beside English.
You could use AutoMate framework to simplify this. Here you have an example how to deal with system alerts using AutoMate:
func locationWhenInUse() {
let token = addUIInterruptionMonitor(withDescription: "Location") { (alert) -> Bool in
guard let locationAlert = LocationWhenInUseAlert(element: alert) else {
XCTFail("Cannot create LocationWhenInUseAlert object")
return false
}
locationAlert.allowElement.tap()
return true
}
// Interruption won't happen without some kind of action.
app.tap()
// Wait for given element to appear
wait(forVisibilityOf: locationPage.requestLabel)
removeUIInterruptionMonitor(token)
}
In the example above the locationAlert.allowElement.tap() is possible because AutoMate can handle any language supported by iOS Simulator.
For more examples on how to deal with system alerts using AutoMate please look into: PermissionsTests.swift

Use addUIInterruptionMonitor:withDescription:handler: to register an interruption monitor. To 'wait' for the system alert to appear, use the handler to set a variable when it has been dealt with, and execute a benign interaction with the app when you want to wait for the alert.
You must continue to interact with the app while you are waiting, as interactions are what trigger interruption monitors.
class MyTests: XCTestCase {
let app = XCUIApplication()
func testLocationAlertAppears() {
var hasDismissedLocationAlert = false
let monitor = addUIInterruptionMonitor(withDescription: "LocationPermissions") { (alert) in
// Check this alert is the location alert
let location = NSPredicate(format: "label CONTAINS 'Location'")
if alert.staticTexts.element(matching: location).exists {
// Dismiss the alert
alert.buttons["Allow"].tap()
hasDismissedLocationAlert = true
return true
}
return false
}
// Wait for location alert to be dismissed
var i = 0
while !hasDismissedLocationAlert && i < 20 {
// Do some benign interaction
app.tap()
i += 1
}
// Clean up
removeUIInterruptionMonitor(monitor)
// Check location alert was dismissed
XCTAssertTrue(hasDismissedLocationAlert)
}
}

Related

macOS Swift: How to properly add application as Login Item

I have spent about one day (maybe a little more) on trying to add my application to Login Item in the order it starts up at macOS launch (user login).
The first approach was the newest one; I check this tutorial on youtube:
https://www.youtube.com/watch?v=2mmWEHUgEBo&t=660s
So following this steps, I have done:
Add new project inside my main project that I have named Launcher
I am using Automatic Signing (as version of my Xcode) is different
In Project Settings > Capabilities I toggled App Sandbox to ON.
In Build Phases I have added this:
My Launcher has Skip Install = YES
Code in my Launcher app looks like this (I have even previously use Swift to do the same)
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
NSArray *pathComponents = [[[NSBundle mainBundle] bundlePath] pathComponents];
pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 4)];
NSString *path = [NSString pathWithComponents:pathComponents];
[[NSWorkspace sharedWorkspace] launchApplication:path];
[NSApp terminate:nil];
}
Finally, I have magic code in Main App to enable app as Login Item
if(!SMLoginItemSetEnabled("click.remotely.Remotely-Click-Server-Launcher"
as CFString, Bool(checkboxButton.state as NSNumber) ) ) {
let alert: NSAlert = NSAlert()
alert.messageText = "Remotely.Click Server - Error";
alert.informativeText = "Application couldn't be added as
Login Item to macOS System Preferences > Users & Groups.";
alert.alertStyle = NSAlertStyle.warning;
alert.addButton(withTitle:"OK");
alert.runModal();
}
I have made Archive, and then have different options to Export:
I couldn't decide which one to choose, so I tried all of them.
"Save for Mac App Store Deployment" - made Installation package that has installed in /Applications/ directory but the app never runs.
"Developer-Id signed," "Development-signed" , "macOS App" all makes file in a directory that I exported to Applications directory, but no one works.
When I click the checkbox button, I could see some window blinking for a while on the screen (Launcher program). When I log out and log in the same window blinking effect appears but Launcher didn't start the Main application. When I click checkbox button again (and turn off Login Item) this blinking effect on user login (system startup) doesn't happen again. So it seems that this addition of Launcher program as Login Item works, but this Launcher couldn't start the Main app. Moreover when I go to /Applications/Main.app/Contents/Library/LoginItems/Launcher.app and click it manually then Launcher app launch Main application correctly (so the path was correct).
So what's going wrong?
Then I consider implementation of deprecated approach using
kLSSharedFileListSessionLoginItems
I have thought it must work it just add something in System Preferences this
window below.
But it also could go wrong!
I have chosen implementation in Swift (all examples/tutorials I have found was in Objective-C) So I have written something like this:
class LoginItemsList : NSObject {
let loginItemsList : LSSharedFileList = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue();
func addLoginItem(_ path: CFURL) -> Bool {
if(getLoginItem(path) != nil) {
print("Login Item has already been added to the list.");
return true;
}
var path : CFURL = CFURLCreateWithString(nil, "file:///Applications/Safari.app" as CFString, nil);
print("Path adding to Login Item list is: ", path);
// add new Login Item at the end of Login Items list
if let loginItem = LSSharedFileListInsertItemURL(loginItemsList,
getLastLoginItemInList(),
nil, nil,
path,
nil, nil) {
print("Added login item is: ", loginItem);
return true;
}
return false;
}
func removeLoginItem(_ path: CFURL) -> Bool {
// remove Login Item from the Login Items list
if let oldLoginItem = getLoginItem(path) {
print("Old login item is: ", oldLoginItem);
if(LSSharedFileListItemRemove(loginItemsList, oldLoginItem) == noErr) {
return true;
}
return false;
}
print("Login Item for given path not found in the list.");
return true;
}
func getLoginItem(_ path : CFURL) -> LSSharedFileListItem! {
var path : CFURL = CFURLCreateWithString(nil, "file:///Applications/Safari.app" as CFString, nil);
// Copy all login items in the list
let loginItems : NSArray = LSSharedFileListCopySnapshot(loginItemsList, nil).takeRetainedValue();
var foundLoginItem : LSSharedFileListItem?;
var nextItemUrl : Unmanaged<CFURL>?;
// Iterate through login items to find one for given path
print("App URL: ", path);
for var i in (0..<loginItems.count) // CFArrayGetCount(loginItems)
{
var nextLoginItem : LSSharedFileListItem = loginItems.object(at: i) as! LSSharedFileListItem; // CFArrayGetValueAtIndex(loginItems, i).;
if(LSSharedFileListItemResolve(nextLoginItem, 0, &nextItemUrl, nil) == noErr) {
print("Next login item URL: ", nextItemUrl!.takeUnretainedValue());
// compare searched item URL passed in argument with next item URL
if(nextItemUrl!.takeRetainedValue() == path) {
foundLoginItem = nextLoginItem;
}
}
}
return foundLoginItem;
}
func getLastLoginItemInList() -> LSSharedFileListItem! {
// Copy all login items in the list
let loginItems : NSArray = LSSharedFileListCopySnapshot(loginItemsList, nil).takeRetainedValue() as NSArray;
if(loginItems.count > 0) {
let lastLoginItem = loginItems.lastObject as! LSSharedFileListItem;
print("Last login item is: ", lastLoginItem);
return lastLoginItem
}
return kLSSharedFileListItemBeforeFirst.takeRetainedValue();
}
func isLoginItemInList(_ path : CFURL) -> Bool {
if(getLoginItem(path) != nil) {
return true;
}
return false;
}
static func appPath() -> CFURL {
return NSURL.fileURL(withPath: Bundle.main.bundlePath) as CFURL;
}
}
I have used this to turn on/off Login Item by clicking in the checkbox
let loginItemsList = LoginItemsList();
if( checkboxButton.state == 0) {
if(!loginItemsList.removeLoginItem(LoginItemsList.appPath())) {
print("Error while removing Login Item from the list.");
}
} else {
if(!loginItemsList.addLoginItem(LoginItemsList.appPath())) {
print("Error while adding Login Item to the list.");
}
}
I have run it in Debug mode (Xcode Play button) and try to archive it and export to /Applications folder if it matters, but this approach also doesn't work.
Console-printed messaged. Error means that the function Inserting Login Item returns nil.
So after that I even try to implement this (from some stackoverflow example) using Objective-C (as there is many Unmanaged<> in Swift)
So I added new .m and .h file and Bridging-Header.h and then a code like this:
- (void)enableLoginItemWithURL:(NSURL *)itemURL
{
LSSharedFileListRef loginListRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
if (loginListRef) {
// Insert the item at the bottom of Login Items list.
LSSharedFileListItemRef loginItemRef = LSSharedFileListInsertItemURL(loginListRef,
kLSSharedFileListItemLast,
NULL,
NULL,
(__bridge CFURLRef) itemURL,
NULL,
NULL);
if (loginItemRef) {
CFRelease(loginItemRef);
}
CFRelease(loginListRef);
}
}
Simple (just insertion) without any bells and whistles.
It also has the same issue that LSSharedFileListInsertItemURL returns nil and Login Item is not added to System Preferences > Users & Groups > Login Items.
So any idea why I cannot make this work?
UPDATE 1
I have tried to implement application using first approach (helper Launcher application inside Main application) on another computer iMac (MacOS Sierra and the newest XCode 8.3) and it seems to work there correctly so maybe there is something wrong with my OS or Xcode (provisioning profiles, signing of app or whatever) On MacBook Air where this approach doesn't work I am using OS X El Capitan 10.11.5 and Xcode 8.0.
Watch how it works here:
https://youtu.be/6fnLzkh5Rbs
and testing
https://www.youtube.com/watch?v=sUE7Estju0U
The second approach doesn't work also on my iMac returning the nil while doing LSSharedFileListInsertItemURL. So I don't know why that is happening.
Watch how it works here:
https://youtu.be/S_7ctQLkIuA
UPDATE 2
After upgrade to macOS Sierra 10.12.5 from El Capitan 10.11.5 and using Xcode 8.3.2 instead of Xcode 8.0.0 the second approach also happens to work correctly and is adding Login Items to System Preferences > Users & Groups > Login Items
IMPORTANT! To work this approach with LSSharedFileListInsertItemURL needs to disable App Sandboxing! Like in the video below:
https://youtu.be/UvDkby0t_WI
I also struggled with this a few years ago and ended up making a package for it that makes it much easier to add "launch at login" functionality for sandboxed apps.
Instead of lots of manual steps, you just need:
import LaunchAtLogin
LaunchAtLogin.isEnabled = true
Since macOS 13 Ventura, we can finally use a new SMAppService.
import ServiceManagement
try SMAppService.mainApp.register()
See SMAppService documentation for more details.
For the ServiceManagement approach, sometimes it doesn't work in your development machine because there is another copy of the app in your Xcode's DerivedData. The system don't know which app to launch. So go to ~/Library/Developer/Xcode/DerivedData and delete your development copy could help.
The above solution of Login Item programming problem works correctly both using modern approach with ServiceManagement.framework, and old (deprecated) approach with inserting Login Item into System Preferences > Users & Groups > Login Items. See my UPDATE 1 and UPDATE 2 remarks.
Dear Michal I have had the same problem about log in items. Log in items can be added in two ways; one from LSSharedFileListItemRef which will be shown in Login item of preferences but this approach will only work for non-sandboxing app and if you are making sandbox app then you should go for another way of using ServiceManagement framework.
You can have look over below link which specify everything - :
Launching your app on system start
I am adding another reference of adding app on log in - :
Approach for sandbox app with helper application
May be you are having trouble implementing app on log in item but follow the steps appropriately and you will succeed.

XCtest UI testing - running test stalls on wait for app to idle

I'm have just starting using XCTest for UI testing.
While running my tests, execution takes forever.
I get following output:
Wait for app to idle - waits 120 seconds to fail
App animations complete notification not received, will attempt to continue.
I have looked all over and found zero answer on how to either change the timeout or how to manipulate the app to move on.
There are no animations running at all in the app. The screen is fully loaded. My developers also have zero answers as well and I have tried disabling animations which fails the tests outright.
Any ideas on how I can address this?
func testHomeScreen() {
logIn()
let home = app.navigationBars["HOME"].staticTexts["HOME"]
waitForElementToAppear(element: home)
XCTAssert(home.exists)
let myFriendsButton = app.buttons["My Friends"]
XCTAssert(myFriendsButton.exists)
let collectionViewsQuery = app.collectionViews
collectionViewsQuery.buttons["compose"].tap()
app.navigationBars["CREATE POST"].buttons["CANCEL"].tap()
collectionViewsQuery.buttons["photo"].tap()
app.navigationBars["PHOTO BOOTH"].buttons["CANCEL"].tap()
let element = collectionViewsQuery.children(matching: .cell).element(boundBy: 1).children(matching: .other).element
element.children(matching: .other).element(boundBy: 0).children(matching: .image).element(boundBy: 0).tap()
let profileCloseButton = app.buttons["profile close"]
profileCloseButton.tap()
let image = element.children(matching: .image).element
image.swipeUp()
let button = app.navigationBars["DETAILS"].buttons[" "]
let cell = collectionViewsQuery.children(matching: .cell).element(boundBy: 0)
cell.buttons["actionsheet"].tap()
app.sheets.buttons["Cancel"].tap()
cell.buttons["comment"].tap()
button.tap()
collectionViewsQuery.buttons["like selected"].tap()
Here is my LogIn func and Setup.
func logIn() {
let app = XCUIApplication()
if app.tabBars.buttons["HOME"].exists {
logOut()
}
let logInButton = app.buttons["LOG IN"]
logInButton.tap()
let elementsQuery = app.scrollViews.otherElements
let emailField = elementsQuery.textFields["example#email.com"]
emailField.tap()
emailField.typeText("***************")
let passwordSecureTextField = elementsQuery.secureTextFields["Password"]
passwordSecureTextField.tap()
passwordSecureTextField.typeText("************")
app.navigationBars["LOG IN"].buttons["btn login"].tap()
if app.staticTexts["PLEASE TURN ON NOTIFICATIONS"].exists {
XCTAssert(app.staticTexts["This way you will be able to see when your friends post, message, like or comment on your stuff!"].exists)
app.buttons["NOT NOW"].tap()
}
if app.collectionViews.staticTexts["3D Rooms Now Available!"].exists {
XCTAssert(app.collectionViews.staticTexts["Chat with friends in 3D rooms wherever you go! Tap here to get started"].exists)
app.collectionViews.buttons["close"].tap()
}
if app.staticTexts["DAILY"].exists {
XCTAssert(app.staticTexts[" SPIN"].exists)
app.buttons["close"].tap()
}
}
Setup:
class HomeScreenTests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
app.launch()
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
Here is a chunk of the output from the test logs:
LogFile

Selenium: are there events like "New element inserted in DOM"

The site I am testing has a notification logic that brings up a message at the bottom of the screen, keeps it there for one second and then sends it away. When the notification is displayed it hides other elements and that makes my test unstable. I did my best to figure out when the notification is displayed (when the business logic displays it) and dismiss it but every now and then I detect new cases my code are not aware of when the notification is displayed.
Is there a way (using Selenium) to subscribe to an event like "New element inserted in DOM". Dismissing the notification on its callback would solve my problem once and for all.
Selenium doesn't support this use case out of the box but you can achieve that using MutationObserver in javascript. I don't know what language you are using to write selenium test but in C# you can create extensions method as follow
public static void StartWatchingForContentChange(this RemoteWebDriver driver, string containerId, int timeout = SearchElementDefaultTimeout)
{
driver.ExecuteScript(#"var $$expectedId = arguments[0];
__selenium_observers__ = window.__selenium_observers__ || {};
(function(){
var target = document.getElementById($$expectedId);
__selenium_observers__[$$expectedId] = {
observer: new MutationObserver(function(mutations) {
__selenium_observers__[$$expectedId].occured = true;
__selenium_observers__[$$expectedId].observer.disconnect();
}),
occured:false
};
var config = { attributes: true, childList: true, characterData: true, subtree: true };
__selenium_observers__[$$expectedId].observer.observe(target, config);
})();", containerId);
}
public static bool WasContentChanged(this RemoteWebDriver driver, string containerId)
{
return (bool) driver.ExecuteScript( "return window.__selenium_observers__ && window.__selenium_observers__[arguments[0]].occured;", containerId)
}
You can use some kind of timer to asynchronously invoke WasContentChanged method and react for content changes. Please read MutationObserver documentation for more details https://developer.mozilla.org/pl/docs/Web/API/MutationObserver

Handling Windows 8 lifecycle

i have run into a problem, that my app sometimes Activates and sometimes Launches when i open something via:
var options = new Windows.System.LauncherOptions();
options.DisplayApplicationPicker = false;
bool success = await Windows.System.Launcher.LaunchFileAsync(sampleFile, options);
When app re-activates it shows the same window - when i went to an external app using LaunchFileAsync - this is nice.
But sometimes the app launches, i see a SplashPage and app is beginning from the MainPage. - how can i make this also to return to the page, that i left when used LaunchFileAsync?
Example:
I have a MainPage and a BlankPage1
So here is my page on suspend+shutdown (terminate) 8 buttons:
On Restore 0 buttons, I WANT TO SAVE MY VIEW XAML CODE when app gets killed by system:
It depends entirely on the conditions of your application shutdown. Was it suspended and terminated automatically by the OS ? or did you close it yourself ? (ex : ALT-F4)
You can see here the application lifecyle : http://msdn.microsoft.com/en-us/library/windows/apps/hh464925.aspx
If you want your application to restore its previous state on a user shutdown, I think you can enable it on your OnLaunched method in you App.xaml.cs :
if (args.PreviousExecutionState == ApplicationExecutionState.Terminated
|| args.PreviousExecutionState == ApplicationExecutionState.ClosedByUser)
{
try
{
await SuspensionManager.RestoreAsync();
}
catch (SuspensionManagerException)
{
}
}
Then, if your Page extends LayoutAwarePage, you have two methods, SaveState and LoadState.
These methods are called automatically when navigating from or to the frame (including suspending/restoring/opening...).
If you save your data behind your buttons in your SaveState method, you can restore it in the LoadState method (and thus redraw your buttons). There is a detailled exemple here : http://msdn.microsoft.com/en-us/library/windows/apps/hh986968.aspx

React to every app activation in Windows Store app

I have a Windows Store app with Live Tile updates using Background Task. When I activate the app by any means (click on the live tile, switch back to the app, etc..) I want to clear the live tile (I have a number there that I want to change to zero).
To be more concerete, I run the app, I switch to another app or desktop, then I switch ti the star screen and I see a number on the Live Tile. I click the Live Tile, I am taken to the app and I want the Live Tile to clear. The same functionality as the Email app.
I tried the OnActivated method in App.xaml.cs but it does not seem to get called at any time (I put a throw new NotImplementeExeption there and the app never crashes).
You should put it in the OnLaunched method, you just need to determine where.
protected async override void OnLaunched(LaunchActivatedEventArgs args)
{
var rootFrame = new Frame();
// Do not repeat app initialization when already running, just ensure that
// the window is active
if (args.PreviousExecutionState == ApplicationExecutionState.Running)
{
//....
}
if (args.PreviousExecutionState == ApplicationExecutionState.ClosedByUser)
{
/....
}
if (!String.IsNullOrEmpty(args.Arguments))
{
//....
}
if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//....
}
if (args.PreviousExecutionState == ApplicationExecutionState.NotRunning)
{
//.....
}
TileUpdateManager.CreateTileUpdaterForApplication().Clear();
BadgeUpdateManager.CreateBadgeUpdaterForApplication().Clear();
SettingsPane.GetForCurrentView().CommandsRequested += OnCommandsRequested;
// Create a Frame to act navigation context and navigate to the first page
if (!rootFrame.Navigate(typeof(MainPage)))
{
throw new Exception("Failed to create initial page");
}
// Place the frame in the current Window and ensure that it is active
Window.Current.Content = rootFrame;
Window.Current.Activate();
}
If you look at the code, there are several reasons of why your App is closed/suspended. So, determine in which cases you want to run de code for updating the number in the Live Tile, put it inside that if, and it should work.
I guess that the better place for such actions is the OnLaunched method. It called every time you appication start.
update: Hmm, seems you should react on both OnActivated and OnLaunched methods:
OnLaunched - Invoked when the application is launched. Override this
method to perform application initialization and to display initial
content in the associated Window.
On the application start OnLaunched will be called. But when you switch to another app and then go back OnActivated should be called:
OnActivated - Invoked when the application is activated by some means other than normal launching.