I have a UI test that checks the value of static text element, waits a few seconds and checks again to confirm a change. At first it wasn't working because the hierarchy was not updating. I noticed this in the log;
Use cached accessibility hierarchy for
I've put in a workaround for this by simply adding a tap to a menu and opening/closing it so that an event is synthesized and the hierarchy is updated.
It would be better, however, if there was a way to clear the cache directly or force and update. I haven't found one in the API. Am I missing something?
Any ideas?
this is what I am doing;
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
menu.tap()
sleep(1)
menu.tap()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
What I'd like to be able to do it
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
app.elements.refresh()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
In order to force an update of the accessibility hierarchy, request the count property for any XCUIElementQuery:
// refresh
_ = XCUIApplication().navigationBars.count
// examine
print(XCUIApplication().debugDescription)
The above results in: "Get number of matches for: Descendants matching type NavigationBar" and "Snapshot accessibility hierarchy for com.myapp".
The following works for me in Xcode 10.2 (10E125):
import XCTest
extension XCUIApplication {
// WORKAROUND:
// Force XCTest to update its accessibility cache. When accessibility data
// like NSObject.accessibility{Label|Identifier} changes, it takes a while
// for XCTest to catch up. Calling this method causes XCTest to update its
// accessibility cache immediately.
func updateAccessibilityCache() {
_ = try? snapshot()
}
}
You should use expectationForPredicate, along the lines of...
let myText = app.staticTexts["myText"]
let waitFor = NSPredicate(format: "label = 'Expected 2'")
label.tap()
self.expectationForPredicate(waitFor, evaluatedWithObject: myText, handler: nil)
self.waitForExpectationsWithTimeout(2.0, handler: nil)
This will wait until either myText's label is 'Expected 2', or the timeout of 2 seconds is reached.
In my case, it is a problem because I'm trying to test for Facebook login, which uses Safari controller. It looks like Facebook has updated the UI after cache.
So you need to wait a bit, use the wait function here https://stackoverflow.com/a/42222302/1418457
wait(for: 2)
let _ = app.staticTexts.count
But the above is just workaround and very flaky. A more correct approach would be to wait for a certain element to appear, see https://stackoverflow.com/a/44279203/1418457
Related
Writing a Finder extension / Quick Action (see https://developer.apple.com/documentation/appkit/app_extensions/add_functionality_to_finder_with_action_extensions) I happen to have a long-running operation showing "Estimate time remaining...".
I am able to provide an NSProgress object as result of registerFileRepresentation, but no progress is shown and cancelling is also not working:
itemProvider.registerFileRepresentation(
forTypeIdentifier: kUTTypePDF as String,
fileOptions: [.openInPlace],
visibility: .all,
loadHandler: {
completionHandler -> Progress? in
let op = self.newOperation(url: sourceUrl, completionHandler: completionHandler)
self.queue.addOperation(op)
return op.progress
}
)
What may I need to do, to get progress being calculated for my NSExtension?
New to Go and Fyne, and stumbling trying to get what I need out of Fyne. Sorry, this will be long.
My problem is this. I’m writing a application that gets a list of commands from a server, telling it to create a series of widgets and display them. They are of various types – Label, Button, Entry, Select, etc.
But these aren’t standard widgets; I need to extend their behavior a bit. For one thing, when operated by the user, they each need access to some per-widget information. Button click, for example, has to reference some data specific to that button so it knows what to do. We’ll call this additional information the About struct.
Secondly, every widget needs to be able to take a right click and drop down a widget specific menu. Yes, even buttons and labels need to be able to provide drop down menus. In the case of a widget like Entry, I know this design is going to condemn me to having to write my own menu choices for Paste, Copy and the other operations Entry normally offers on a right click, but I’m ok with that.
I have all this working, but in the process I broke Select (and probably there will be breakage for other widgets.) and I can’t see how to fix it.
Problem: trying to send the Select widget causes a panic: interface conversion: fyne.Canvas is nil, not *glfw.glCanvas
Approach:
type GenericWidget struct {
fyne.Widget //I’m some kind of widget
about *About //here’s my personal “About” data
//function pointers
OnRightClickp func(gw *GenericWidget, pe *fyne.PointEvent)
OnLeftClickp func(gw *GenericWidget, pe *fyne.PointEvent)
…other “function pointers” for OnRunep and so on…
}
And now I have to catch all “events” so GenericWidget will see them:
func (gw *GenericWidget) TappedSecondary(pe *fyne.PointEvent) {
if (gw.OnRightClickp != nil) {gw.OnRightClickp(gw, pe)}
}
func (gw *GenericWidget) Tapped(pe *fyne.PointEvent) {
if (gw.OnLeftClickp != nil){gw.OnLeftClickp(gw, pe)}
}
//type Focusable interface
//etc….
This should represent any single widget, regardless of type. It’s not complicated: When Tapped is invoked by the driver, GenericWidget.Tapped gets called. If this widget has a function pointer for “OnLeftClickp” set, call it. Crucially, we pass a pointer to the widget itself when that happens, because all the event handlers I write will need access to the *About and maybe anything else I add to GenericWidget.
Creation is simple enough:
func NewGenericWidget(w fyne.Widget, about *About) *GenericWidget {
gw := GenericWidget{w, about, nil, nil, nil, nil, nil, nil, nil, nil}
return &gw
}
And when it’s time to create a Label, I do that and fold it into a GenericWidget
func NewExtendedLabelWithStyle( //Label with catachable left and right click
text string,
about *About,
alignment fyne.TextAlign,
style fyne.TextStyle,
tappedLeft func(*GenericWidget, *fyne.PointEvent),
tappedRight func(*GenericWidget, *fyne.PointEvent)) *GenericWidget {
e := NewGenericWidget(widget.NewLabelWithStyle(text, alignment, style), about)
e.OnLeftClickp = tappedLeft
e.OnRightClickp = tappedRight
return e
}
All these GenericWidgets work fine – I add them to the appropriate Box and the window paints as I’d expect. I can right click on a label and if OnRightClickp is set, which is generally is, code gets called and is given access to *GenericWidget, which leads to *About, which means the menu that gets put up can offer all the right stuff for what’s in this label.
Of course, Labels don’t normally care about clicking, so the fact that I’ve stolen all the calls in Tappable doesn’t matter.
But a Select widget does care about left clicks, so the fact that GenericWidget is intercepting the call to Tapped() means I’d never see the dropdown appear.
No problem, I thought. When I create the Select widget and the surrounding GenericWidget, I’ll just specify I want to call Select’s Tapped myself:
func NewExtendedSelect(about *About, sel func(*GenericWidget, string)) *GenericWidget {
//build the options. NewSelect demands a slice of strings, so...
st := make([]string, 64)
for e := about.theList.Front(); e != nil; e = e.Next() {
st = append(st, e.Value.(string))
}
s := widget.NewSelect(st, func(c string){})
//make sure it selects the text it should, initially
s.SetSelected(about.value)
//wrap it
e := NewGenericWidget(s, about)
//e.OnChangedp = sel //not set up yet (and don’t know how)
//HERE BE DRAGONS --------------
//But we don't want to break left click. So when we intercept it,
// pass it down to the actual Selection widget:
e.OnLeftClickp = func(me* GenericWidget, pe *fyne.PointEvent) {
fmt.Println("select tapped")
//call the Tapped that Select knows about:
s.Tapped(pe) //PANIC!
}
// -----------------------------
//Handle right click with our usual menu magic
e.OnRightClickp =func(me* GenericWidget, pe *fyne.PointEvent) {
//defer used because not sure what to do with the return value, so make it Go's problem
defer widget.NewPopUpMenuAtPosition(me.GetRightClickMenu(),
me.about.sheet.window.Canvas(), pe.AbsolutePosition)
}
return e //here's your GenericWidget, ready to drop into a Fyne container.
}
Compiles fine, Selects get displayed, but when I left click it prints the expected “select tapped” and then immediately panics:
select tapped
panic: interface conversion: fyne.Canvas is nil, not *glfw.glCanvas
I’m lost. My GenericWidget is just a widget; I thought that’s what composition did, and all the GenericWidgets I create are put in a Box which is in a Box which is SetContent into the window. But the error is suggesting to me that somehow this Select object wasn’t set up right and when it goes to draw the options, something is missing. What did I do wrong?
It’s possible my whole approach is wrong (I do a lot of C++ and Python and I take an OO view of things). In that case, how do I do all this?
This turns out to have been a Fyne bug, which has since been fixed.
It does not look like the select is being composed - your extended select is just creating a new one, tapping it later.
The crash is coming from it trying to display a pop up next to a select that has not been found on the canvas.
If you are extending builtin widgets you also need to call ExtendBaseWidget so that the driver can lookup your widget instead of the default one.
That said a single widget that can extend any other type of widget kind of goes against the strongly types design of Fyne APIs and you may run in to trouble.
I want to verify if an element is visible or not depending on its .hidden property but I don't find a valid way to do that using the new Xcode 7 UI test stuff.
I've tried with myelement.exists and myelement.hittable but they doesn't seems to work as I expected. I suppose they work on conjunction with the hidden property. An hidden element shouldn't exists and is not hittable... but this is not the current behaviour (I can understand the exists behaviour... but a hidden element should be not hittable IMO).
Is there another way to verify the "hidden" property value?
As of Xcode 7.1 Beta 3, UI Testing does not currently support validating the visibility of an element. I suggest filing a radar to bring the necessary attention to Apple.
Xcode 7.1 has fixed this issue. hittable now checks to see if the element is correct.
1) I am testing the UI with swift in Xcode 7.3 and I using both .hittable and .exists for testing whether a label is hidden or not and they both work. I test for 'true' and 'false' to make sure either way agree with the result.
I have a label whose static text is "Track Info" and set to be hidden when app is first loaded, then later on I press a button to show the label, and here is the result after the label is shown.
// test fails
let trackInfoLabel = app.staticTexts["Track info"]
XCTAssertEqual(trackInfoLabel.exists, true)
XCTAssertEqual(trackInfoLabel.hittable, true)
// test passes
XCTAssertEqual(trackInfoLabel.exists, false)
XCTAssertEqual(trackInfoLabel.hittable, false)
// test passes
let trackInfoLabel = app.staticTexts["Track Info"]
XCTAssertEqual(trackInfoLabel.exists, true)
XCTAssertEqual(trackInfoLabel.hittable, true)
// test fails
XCTAssertEqual(trackInfoLabel.exists, false)
XCTAssertEqual(trackInfoLabel.hittable, false)
Leter on when I press the button to hide the label, all the results turned opposite. This confirms that both properties (hittable and exists) works for label.hidden setting.
2) Another way to find out if an element is hidden, you can do is element.frame.size.width == 0 || element.frame.size.height == 0
XCUIElement.hittable works for me (in my particular test case where I am checking several UIButton elements for visibility) - quite sure it is not a right way to do it though
Next code worked for me.
At the end of the test you can past code as follow:
while ([app.staticTexts matchingIdentifier:#"accesibilityId"].count > 0) {
sleep(1);
}
I agree hittable doesn't always work for buttons (Swift 2.0, XCode 7.2)
I just discovered that if button is visible, you can find it ONLY among buttons, while if button is hidden, you can find it's tag in staticTexts as well!
XCTAssertFalse(app.buttons["Log out"].exists && app.staticTexts["Log out"].exists) // This button is visible (hidden = false)
XCTAssert(app.buttons["Log in"].exists && app.staticTexts["Log in"].exists) // This one is hidden
Hacky, but works for buttons. Apple should just introduce .hidden or .visible along .hittable and .exists
The syntax is now .isHittable:
isHittable only returns true if the element is already visible and
hittable onscreen. It returns false for an offscreen element in a
scrollable view, even if the element would be scrolled into a hittable
position by calling click(), tap(), or another hit-point-related
interaction method.
Using the .isHittable property will work because hidden elements are not visible or hittable on screen.
My solution is to add accessibilityIdentifier dynamicly
func someMethod() {
label.isHidden = true
label. accessibilityIdentifier = "isHidden"
}
func someOtherMethod {
label.isHidden = false
label. accessibilityIdentifier = "isVisible"
}
and the in UITest you can use it as
if app.staticTexts["MyLabel"].identifier == "isHidden" {
dosomething()
}
I was looking at adding an option for sorting available fonts by user-defined font collections (I wish Pages and Keynote did this!), but it looks like the old ways of accessing these collections are being deprecated in 10.11:
https://developer.apple.com/library/prerelease/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSFontManager_Class/index.html#//apple_ref/occ/clm/NSFontManager/
Is there a new way of accessing and using those font collections?
I've recently been working with font collections, so I may have some info for you.
The NSFontCollection API is kind of strange. It offers access to "Named Font Collections," but the names associated with said collections aren't attached to them. If that makes no sense, it's because it doesn't. However, let me try to break it down:
To add a system-wide font collection:
// Create a font descriptor (or descriptors) with whatever attributes you want
let descriptor = NSFontDescriptor(fontAttributes: nil)
// Create a font collection with the above descriptor(s)
let collection = NSFontCollection(descriptors: [descriptor])
// In Objective-C, the `+ showFontCollection:withName:visibility:error:`
// method returns `YES` if the collection was shown or `NO` if an error occurred.
// The error is passed via pointer supplied to the `error:` parameter.
//
// In Swift, the method returns `()` (aka nil literal), and instead of passing
// the error in a pointer, it `throws`, so you have to wrap the call in a `do`
// statement and catch the error (or suppress error propagation via `try!`):
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible to All Users", visibility: .Computer)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
To add a user-visible font collection:
Repeat above steps, substituting .Computer with .User:
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible to the Current User", visibility: .User)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
To add a non-persistent font collection:
Repeat above steps, substituting .Computer with .Process:
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible for Current Process", visibility: .Process)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
Next steps…
Once you have a collection, you can change it all you want using the NSMutableFontCollection class. Continuing with the example above, you would do something like this:
let mutableCollection = collection.mutableCopy() as! NSMutableFontCollection
let boldTrait = [NSFontWeightTrait: NSFontWeightBold]
let boldAttributes = [NSFontTraitsAttribute: boldTrait]
let boldDescriptor = NSFontDescriptor(fontAttributes: newAttributes)
mutableCollection.addQueryForDescriptors([boldDescriptor])
At this point, the API gets weird again. We've added descriptors to our "named collection," but nothing in the UI is going to show up until you "show" the font collection again. In other words, after making any changes, you have to call showFontCollection(_:withName:visibility:) again.
Likewise, if you want to remove/delete a collection, you have to call hideFontCollectionWithName(_:visibility:). Despite its innocuous name, this method completely removes a persistent collection from disk, so be careful.
Next next steps…
In subsequent launches of your app, you can retrieve any persistent collection using the NSFontCollection(name:visibility:) method, like so:
// Retrieve collection created at an earlier time
let collectionOnDisk = NSFontCollection(name: "Visible to All Users", visibility: .Computer)
I think I've covered most of it, but if I've missed something, or if you have questions, just let me know. Good luck!
There are classes NSFontCollection and NSFontDescriptor.
Look into the NSFontManager header file of Xcode 7 (via ⇧⌘O) to get more information about the deprecated methods and their replacement.
So I'm experiencing an odd error - "fatal error: unexpectedly found nil while unwrapping an Optional value"...
It's strange because it only ever fails the FIRST time I run the simulator. As long as I don't quit the simulator and just re-run the app, it will not fail, and the user location will be found successfully. If I do quit the simulator and then try to re-run the app, it will fail with the same error.
Why is this happening? Is swift somehow checking for a location before it's actually found?
Thanks in advance.
override func queryForTable() -> PFQuery! {
let query = PFQuery(className: "Yak")
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self // ? delegate
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
var userLocation:CLLocation = manager.location
currLocation = userLocation.coordinate // error occurs here
println(" my location is \([manager.location])")
if let queryLoc = currLocation {
println("ahahahaha")
query.whereKey("location", nearGeoPoint:PFGeoPoint(latitude: queryLoc.latitude as CLLocationDegrees!, longitude: queryLoc.longitude as CLLocationDegrees!), withinMiles: 10)
query.limit = 200
query.orderByDescending("createdAt")
}
My guess is that you are trying to retrieve data coming from an asynchronous service using a linear procedure.
As this line implies:
manager.startUpdatingLocation()
the manager is starting to update the location, but it doesn't mean when it returns a location has already been obtained. It can take some time time, and it can also fail.
The right place to read the location is in the didUpdateLocations method of theCLLocationManagerDelegate, which I presume you have set in the view controller.
The reason why it works the 2nd time you use it is because in the previous run the manager did have the time to detect the location, so what you're getting is the old one, and not an up to date location.
Update
One possible way of solving the problem is:
in viewDidLoad check if userLocation.coordinate is not nil (it's an implicitly unwrapped optional, so you can do that check)
if it's nil, start updating the location, add a UIActivityIndicator, maybe disable user input, and set a flag (instance property) to true, to keep track you're waiting for a location
wait for a location to be obtained in locationManager(_:didUpdateLocations:)
in that method, check if the flag is true, if it is, unset it, remove the progress view and re-enable user input, then call queryForTable