I was writing pretty complicated UI tests using XCTest, and recently switched to EarlGrey, because it's so much faster and way more reliable - Tests aren't randomly failing on a build server, and the test suite could take up to a half hour to run!
One thing that I haven't been able to do in EarlGrey, that I could do in XCTest, was randomly select an element.
For example, on a calendar collectionView, I could query for all collectionViewCells with 'identifier' using an NSPredicate, and then randomly select a day using [XCUIElementQuery count] to get an index, and then tap.
For now, I'm going to hard code it, but I would love to randomize date selection so that I don't have to rewrite tests if we change app code.
Please let me know if I can elaborate, looking forward to solving this!
Step 1 write a matcher that can count elements matching a given matcher using GREYElementMatcherBlock:
- (NSUInteger)elementCountMatchingMatcher:(id<GREYMatcher>)matcher {
__block NSUInteger count = 0;
GREYElementMatcherBlock *countMatcher = [GREYElementMatcherBlock matcherWithMatchesBlock:^BOOL(id element) {
if ([matcher matches:element]) {
count += 1;
}
return NO; // return NO so EarlGrey continues to search.
} descriptionBlock:^(id<GREYDescription> description) {
// Pass
}];
NSError *unused;
[[EarlGrey selectElementWithMatcher:countMatcher] assertWithMatcher:grey_notNil() error:&unused];
return count;
}
Step 2 Use % to select a random index
NSUInteger randomIndex = arc4random() % count;
Step 3 Finally use atIndex: to select that random element and perform action/assertion on it.
// Count all UIView's
NSUInteger count = [self elementCountMatchingMatcher:grey_kindOfClass([UIView class])];
// Find a random index.
NSUInteger randIndex = arc4random() % count;
// Tap the random UIView
[[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UIView class])]
atIndex:randIndex]
performAction:grey_tap()];
Related
I had a .js file for my screenshot automation with Instruments.app where I was looking up a cell with the following predicate:
var classScheduleCell = classScheduleTableView.cells().firstWithPredicate("isEnabled == 1 && NONE staticTexts.value BEGINSWITH 'No classes'").withValueForKey(1, "isVisible");
I want to translate that predicate to an objective C UI test, as the ruby scripts I was using for the screenshots now uses UI testing instead of Instruments. Using the same predicate fails
XCUIElement *firstCell = [classScheduleTableView.cells elementMatchingPredicate:[NSPredicate predicateWithFormat:#"isEnabled == 1 && NONE staticTexts.value BEGINSWITH 'No classes'"]];
Looks like I can make the first part of the predicate work changing
isEnabled == 1
for
enabled == true
Any ideas on how to make the other part work?
I found a solution for this, though is not the most elegant. I couldn't find a way to make a predicate to work as the one I had in UI Automation, so I used a couple of for loops to check the value of the cell labels.
NSPredicate *enabledCellsPredicate = [NSPredicate predicateWithFormat:#"enabled == true "];
XCUIElementQuery *enabledCellsQuery = [classScheduleTableView.cells matchingPredicate:enabledCellsPredicate];
int cellCount = enabledCellsQuery.count;
for (int i = 0; i < cellCount; i++) {
XCUIElement *cellElement = [enabledCellsQuery elementBoundByIndex:i];
XCUIElementQuery *cellStaticTextsQuery = cellElement.staticTexts;
int textCount = cellStaticTextsQuery.count;
BOOL foundNoClasses = NO;
for (int j = 0; j < textCount; j++) {
XCUIElement *textElement = [cellStaticTextsQuery elementBoundByIndex:j];
if (textElement.value && [textElement.value rangeOfString:NSLocalizedString(#"No classes", nil) options:NSCaseInsensitiveSearch].location != NSNotFound) {
foundNoClasses = YES;
break;
}
}
if (foundNoClasses == NO) {
[cellElement tap];
break;
}
}
Thanks #joe-masilotti for your help anyway.
I don't believe your exact predicate is possible with UI Testing.
UI Testings vs UI Automation
Matching predicates with UI Testing (UIT) behaves a little differently than UI Automation (UIA) did. UIA had more access to the actual UIKit elements. UIT is a more black-box approach, only being able to interact with elements via the accessibility APIs.
NOT Predicate
Breaking down your query I'm assuming the second part is trying to find the first cell not titled 'No classes'. First, let's just match staticTexts.
let predicate = NSPredicate(format: "NOT label BEGINSWITH 'No classes'")
let firstCell = app.staticTexts.elementMatchingPredicate(predicate)
XCTAssert(firstCell.exists)
firstCell.tap() // UI Testing Failure: Multiple matches found
As noted in the comment, trying to tap the "first" cell raises an exception. This is because there is more than one match for a text label that starts with "No classes".
NONE Predicate
Using the NONE operator of NSPredicate leads us down a different path.
let predicate = NSPredicate(format: "NONE label BEGINSWITH 'No classes'")
let firstCell = app.staticTexts.elementMatchingPredicate(predicate)
XCTAssert(firstCell.exists)// XCTAssertTrue failed: throwing "The left hand side for an ALL or ANY operator must be either an NSArray or an NSSet." -
This approach can't even find the cell. This is because the elementMatchingPredicate() expects the predict to return a single instance, not an array or set. I couldn't figure out how to make the query select the first element. And once you get an XCUIElement there are no ways to farther restrict it.
Different Approach
That said I suggest you take a slightly different approach for your test. If your tests are deterministic, and they should be, just tap the first known cell. This means you don't have to worry about ambiguous matchers nor chaining predicates.
let firstCell = app.staticTexts["Biology"]
XCTAssert(firstCell.exists)
firstCell.tap()
I have a question regarding integers outside of methods in Objective-C/Xcode. I'm trying to create a simple guessing game, however my randomizer randomize number every time when the method is called, here is the code snipped:
- (IBAction)guessButton:(id)sender {
int tempUserGuess = [self.textField.text integerValue];
int randomNumber = (arc4random() % 11);
if(tempUserGuess == randomNumber){
self.guessAns.text = #"you won!";
}
if (tempUserGuess < randomNumber){
self.guessAns.text = #"no! too low!";
}
if (tempUserGuess > randomNumber){
self.guessAns.text = #"no! too high!";
}
}
The reason why I'm trying to put an int outside of the method is of that once randomized integer should not be randomized every single time (of course). By the way, everything works fine, the app compiles and works but every single time when I hit return, it randomizes the number.
I know how to do this in Java, but Objective-C seems to be more complex.
Your guessButton method is probably a member of some class. You need to add property to that class holding that randomized number.
You only have to create/store a random number once per game play. There is no need to call the randomize method each time the guess button is pressed. The guessButton method is basically like an ActionListener in java. Each time the button is pressed, whatever's inside the curly braces will be executed. If you add a play again button to the game, then you might want to call the randomize method inside of it's action method.
if(tempUserGuess == num){
self.guessAns.text = #"you won!";
}
This will be a good way because it will be using less memory in the sytem when trying to divert the random number. Good work
Okay, I got it.
Here is the answer:
in ViewController.h I had to include:
NSInteger num;
and then in ViewController.m a method that simply returns a number:
-(int)giveRandom {
int randomNumber = (arc4random() % 10);
return randomNumber;
}
And then refer to 'num' in the method with if statements such as:
...
if(tempUserGuess == num){
self.guessAns.text = #"you won!";
}
...
For some reason it returns 0, but I will try to solve it.
Thanks!
I would like to delete multiple spaces from self.textDocumentProxy when working with my extension's KeyboardViewController, and was wondering if there is a Apple-supported method that specifically performs this action?
So far, I have been using a pretty "hacky" way of doing the following (here it deletes all the previous characters found on textDocumentProxy):
for (int i = 0; i < self.textDocumentProxy.documentContextBeforeInput.length; i++){
[self.textDocumentProxy deleteBackward];
}
The issue with this lies in the method deleteBackward, which, depending on what prompt is given, always deletes around half (its quite reliable, especially when documentContextBeforeInput is longer than 20 characters) of the total number of times its told to delete. Since this is rather unreliable, I was wondering if there is a way to easily delete multiple spaces, or all of the texted in textDocumentProxy.documentContextBeforeInput
Thanks!
There is a fundamental issue in the loop you're using:
for (int i = 0; i < self.textDocumentProxy.documentContextBeforeInput.length; i++){
[self.textDocumentProxy deleteBackward];
}
The i < self.textDocumentProxy.documentContextBeforeInput.length check is performed multiple times. And the .length attribute is actually decreasing by 1 for every deleteBackward you do. i however is merrily increasing by 1 for each iteration.
As a result only half will be deleted.
You can flip the order around to fix the issue.
for (int i = self.textDocumentProxy.documentContextBeforeInput.length; i > 0; i--){
[self.textDocumentProxy deleteBackward];
}
You can also cache the original length of the textDocument before you start changing it.
Maybe try this solution that will erase everything in the input text, not only the text before the cursor ;)
func deleteInputText() {
if let afterInput = self.textDocumentProxy.documentContextAfterInput {
self.textDocumentProxy.adjustTextPositionByCharacterOffset(afterInput.characters.count)
}
while let _=self.textDocumentProxy.documentContextBeforeInput {
self.textDocumentProxy.deleteBackward()
}
}
while (self.textDocumentProxy.hasText==YES)
{
[self.textDocumentProxy deleteBackward];
}
should remove all the text.
[self.textDocumentProxy deleteBackward]; deletes only 1 character.
This question already has answers here:
Getting a random object from NSArray without duplication
(3 answers)
Closed 7 years ago.
I have an array of random properties I would like to assign to equipment within the game I'm developing.
The code that I use below is returning an NSArray. I'm interested if there's way to get item indices from that array without getting duplicate values. The obvious solution is to create a mutable array with the returned array, do random, remove item that was returned and loop until the number of items is received.
But is there a different way of getting X random items from NSArray without getting duplicates?
//get possible enchantments
NSPredicate *p = [NSPredicate predicateWithFormat:#"type = %i AND grade >= %i", kEnchantmentArmor,armor.grade];
NSArray* possibleEnchantments = [[EquipmentGenerator allEnchantmentDictionary] objectForKey:#"enchantments"];
//get only applicable enchantments
NSArray *validEnchantments = [possibleEnchantments filteredArrayUsingPredicate:p];
NSMutableArray* mutableArray = [NSMutableArray arrayWithArray:validEnchantments];
NSDictionary* enchantment = nil;
if(mutableArray.count>0)
{
//got enchantments, assign number and intensity based on grade
for (int i = 0; i<3;i++)
{
enchantment = mutableArray[arc4random()%mutableArray.count];
[mutableArray removeObject:enchantment];
//create enchantment from dictionary and assign to item.
}
}
You can shuffle the array using one of the following techniques:
What's the Best Way to Shuffle an NSMutableArray?
Non repeating random numbers
Then, take the first X elements from the array.
Many years ago, I was working on card game and I realized that shuffling the deck was an inefficient way to get random cards. What I would do in your shoes is pick a random element, and then replace it with the element at the end of the array, like so:
#interface NSMutableArray (pickAndShrink)
- (id) pullElementFromIndex:(int) index // pass in your random value here
{
id pickedItem = [self elementAtIndex:index];
[self replaceObjectAtIndex:index withObject:[self lastObject]];
[self removeLastObject];
return pickedItem;
}
#end
The array will shrink by one every time you pull an element this way.
You could use a random number generator to pick a starting index, and then pick the subsequent indices based on some kind of math function. You would still need to loop depending on how many properties you want.
Eg:
-(NSMutableArray*)getRandomPropertiesFromArray:(NSArray*)myArray
{
int lengthOfMyArray = myArray.count;
int startingIndex = arc4random()%lengthOfMyArray;
NSMutableArray *finalArray = [[NSMutableArray alloc]init]autorelease];
for(int i=0; i<numberOfPropertiesRequired; i++)
{
int index = [self computeIndex:i usingStartingIndex:startingIndex origninalArray:myArray];
[finalArray addObject:[myArray objectAtIndex:index]];
}
return finalArray;
}
-(int)computeIndex:(int)index usingStartingIndex:(int)startingIndex
{
//You write your custom function here. This is just an example.
//You will have to write some code to make use you don't pick an Index greater than the length of your array.
int computedIndex = startingIndex + index*2;
return startingIndex;
}
EDIT: Even your computeIndex function could use randomness in picking the subsequent indices. Since you have a startingIndex, and another index, you could use that to offset your function so that you never pick a duplicate.
EDIT: If your array is very large, and the subset you need to pick is small, then rather than shuffle the entire array (maybe more expensive), you could use this method to pick the number of items you need. But if your array is small, or if the number of items you need to pick are almost the size of the array, then the godel9's solution is better.
You can use a mutable array and then remove them as the are selected, use something like random()%array.count to get a random index. If you don't want to modify the array then copy it with [array mutableCopy].
I have an app with a relatively large SQLite database, containing around 15,000 rows. At the start of the app, I run a SELECT * FROM TABLE query and populate a NSMutableArray *data containing elements of a class ProverbRow. Each ProverbRow object corresponds to one row of data from the database and contains NSStrings and NSIntegers corresponding to each cell of the database. So, effectively the NSMutableArray *data object is a copy of the SQLite database.
Now, to the question...
I want to add a search functionality in the app like this example :
When the user types in "abc" and taps search, I want to get all the elements of data where the NSString *proverb inside the ProverbRow object contains the string "abc" as the substring. So, the strings like "abcde", "qqqabcqqq", etc. should be obtained.
Currently, to accomplish this, I am firing a query like this : SELECT * FROM PROVERB WHERE PRONUNCIATION LIKE abc. The objects which I get back from the query are stored in another NSArray for further use.
Now, if there are only a small number of objects getting returned, then this query completes fast enough, but with larger number of rows, it takes a lot of time.
I was wondering whether there is a quicker way to accomplish this apart from firing the query. Is it possible to use the already populated data object and run it through a loop and equating the substring or something like that? My main concern is to reduce the time search takes.
Thanks!
15 k is tiny. Just do a linear scan of the in-memory objects.
The following test finds the xyz at the end of the alphabet 15000 times. Repeating this process 100 times took 4.7 s on my iPhone 5. That's 47 ms for a full scan that returns every element.
NSMutableArray * data = [[NSMutableArray alloc] initWithCapacity:15000];
for (int i = 0; i < 15000; ++i)
[data addObject:#[#"abcdefghijklmnopqrstuvwxyz", #123]];
NSLog(#"Starting test");
int count = 0;
for (int i = 0; i < 100; ++i) {
NSIndexSet * s = [data indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
NSString * s = obj[0];
return [s rangeOfString:#"xyz"].location != NSNotFound;
}];
count += s.count;
}
NSLog(#"Finished test: %d", count); // Outputs 1500000
Let me reiterate: 15 k is tiny.