I'm trying to write an integration test for an Android application entirely written in Compose that has a single Activity and uses the Compose Navigation to change the screen content.
I managed to properly interact and test the first screen that is shown by the navigation graph but, as soon as I navigate to a new destination, the test fails because it does not wait for the NavHost to load the new content.
#RunWith(AndroidJUnit4::class)
class MainActivityTest {
#get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
#Test
fun appStartsWithoutCrashing() {
composeTestRule.apply {
// Check Switch
onNodeWithTag(FirstScreen.CONSENT_SWITCH)
.assertIsDisplayed()
.assertIsOff()
.performClick()
.assertIsOn()
// Click accept button
onNodeWithTag(FirstScreen.ACCEPT_BUTTON)
.assertIsDisplayed()
.performClick()
// Check we are inside the second screen
onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD)
.assertIsDisplayed()
}
}
}
I'm sure that is a timing issue because if I add a Thread.sleep(500) before the onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD).assertIsDisplayed(), the test is successful. But I would like to avoid Thread.sleep()s in my code.
Is there a better way to tell the composeTestRule to wait for the NavHost to load the new content before executing the assertIsDisplayed()?
PS
I know that would be better to test the Composables in isolation, but I really want to simulate the user input on the App using Espresso and not only test the Composable behavior.
As suggested in this very informative blog article, waitUntil can be used to wait until the node with the right tag is shown:
// Waiting for the new destination to be shown
waitUntil {
composeTestRule
.onAllNodesWithTag(LogInTestTags.USERNAME_TEXT_FIELD)
.fetchSemanticsNodes().size == 1
}
Or, after adding some sugar:
#RunWith(AndroidJUnit4::class)
class MainActivityTest {
#get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
#Test
fun appStartsWithoutCrashing() {
composeTestRule.apply {
// Check Switch
onNodeWithTag(FirstScreen.CONSENT_SWITCH)
.assertIsDisplayed()
.assertIsOff()
.performClick()
.assertIsOn()
// Click accept button
onNodeWithTag(FirstScreen.ACCEPT_BUTTON)
.assertIsDisplayed()
.performClick()
// Waiting for the new destination to be shown
waitUntilExists(hasTestTag(SecondScreen.USERNAME_TEXT_FIELD))
// Check we are inside the second screen
onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD)
.assertIsDisplayed()
}
}
}
private const val WAIT_UNTIL_TIMEOUT = 1_000L
fun ComposeContentTestRule.waitUntilNodeCount(
matcher: SemanticsMatcher,
count: Int,
timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) {
waitUntil(timeoutMillis) {
onAllNodes(matcher).fetchSemanticsNodes().size == count
}
}
fun ComposeContentTestRule.waitUntilExists(
matcher: SemanticsMatcher,
timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) = waitUntilNodeCount(matcher, 1, timeoutMillis)
fun ComposeContentTestRule.waitUntilDoesNotExist(
matcher: SemanticsMatcher,
timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) = waitUntilNodeCount(matcher, 0, timeoutMillis)
Related
This seems to be so easy that I feel embarrassed to ask. However, I have been fighting it for hours and scratching my head.
The goal is to simply launch camera, if the button is pressed and camera permission has been granted, and then take a picture and display it in an Image composable with Coil. Each time I try this, the ActivityResultContract fails to save the image. I think something with the given URI is messy, but it's beyond my magical powers to solve it.
Long story short, here's my code. Hope someone has an idea of how to remedy this issue!
#Composable
fun MyPicker() {
val context = LocalContext.current
val permissionLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission().apply{
}){
}
val imgUri by remember{mutableStateOf("${context.filesDir}/temp.jpg".toUri())}
val captureLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicture()){
Toast.makeText(context, "Image capture: ${if(it) "Successful" else "Failed"}", Toast.LENGTH_SHORT)
.show()
}
Column {
Button(onClick = {
if(ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA) != PERMISSION_GRANTED)
permissionLauncher.launch(android.Manifest.permission.CAMERA)
else captureLauncher.launch(imgUri)
}) {
Text("Load")
}
Image(painter = rememberAsyncImagePainter(imgUri), null)
}
}
The error says different things for each Uri I provide. For example, for the above uri, it says the following:
E exception while saving result to URI: Optional.of(/data/user/0/com.mycompany.activityops/files/temp.jpg)
java.io.FileNotFoundException: No content provider: /data/user/0/com.mycompany.activityops/files/temp.jpg
And then I tried to, also, provide a Uri.fromFile("${context.filesDir}/temp.jpg"), which was also frowned upon by Android throwing a huge FileUriExposedException, and then I'm clueless, with my head wandering around storage, content resolvers, Uris, etc.
In a util file create a function which returns a temporary file
fun createImageFile(context: Context): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
"JPEG_${timeStamp}_", //prefix
".jpg", //suffix
storageDir //directory
)
}
Further get uri for the file
val file = Utils.createImageFile(context = context)
val imageUri = FileProvider.getUriForFile(
context,
"application_authority",
file
)
NOTE:
The path of the image will be
val path = file.absolutePath
When starting a result contract you need to send uri
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
onResult = {isSuccess: Boolean ->
// Handle Result
}
cameraLauncher.launch(uri)
On getting a success call back, you can get the image inside file by
File(path) // Where **path** is defined above.
I'm trying to find a simple and easy way to cancel all running sagas within a "page" when the user decides to navigate to another "page" within the app... We are not using routing, but instead each "page" is its own widget within a larger host application that is responsible for creating and loading each page when the user navigates...
Currently, we are using redux-saga and have setup logic like so (simplified for brevity) when a page widget is created and loaded...
// page-sagas
export function* rootSaga() {
const allSagas = [
// ... all sagas used by page (example) ...
// function* watchFoo() {
// yield takeEvery(FooAction, foo);
// }
];
yield all(allSagas.map((saga) => call(saga)));
}
// page-widget
onLoad = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, initState, applyMiddlware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
}
Ideally, I'd prefer to avoid having to add forking logic to every single saga in every single page-widget, and looking at the Redux-Saga Task API, it says you can cancel a task returned by the call to middleware.run, but I'm wondering if this propagates down to all nested / child sagas that are currently in progress, or if there are any issues / gotcha's I should be aware of:
Example:
// page-widget
onLoad = () => {
...
this.task = sagaMiddlware.run(rootSaga);
}
destroy = () => {
this.task.cancel();
}
The xpages contain SAVE button. The xpages also contain InternetAddres field.
When user click SAVE button, need to check first on names.nsf
- Save success if InternetAddress value NOT found in names.nsf view "($Users)"
- Save fail if InternetAddress value found in names.nsf view "($Users)"
How to write the script to do that?
This is the LotusScript version of script:
Set namesview = namesdb.GetView( "($Users)" )
Set namesdoc = namesview.GetDocumentByKey( Lcase(doc.CurrentInternetAddress( 0 ) ), True )
If ( namesdoc Is Nothing ) Then '-- Create New Doc
How to move on xpages?
The latest release of the OpenNTF Domino API adds a checkUnique() method to the View class. It takes two parameters, the first being a key to check against the view (e.g. a String or List of Strings), the second being the current document. After all, if you're checking for a pre-existing document, you don't want to fail just because it finds this document in the view.
So assuming CurrentInternetAddress is a single value field, the code would be:
function continueWithValidUser(namesDB, doc) {
var success = false;
try {
var view = namesDB.getView("($Users)");
success = view.checkUnique(doc.getItemValue("CurrentInternetAddress"),doc);
} catch (e) {
print(e.message);
}
return success;
}
OpenNTF Domino API recycles all handles to Domino objects, so the recycle() calls aren't needed.
In your datasource is a querySave event. You write JS there. It is almost the same code. Just with { } and ;
Remarks:
your app will break when there is more than one address book, so you you would want to use #NameLookup which is quite fast and checks all addressbooks.
unless you need the document getEntry is faster than getDocument
In SSJS your function would look like this:
function continueWithValidUser(namesDB, addressCandidate) {
var success = false;
try {
var view = namesDB.getView("($Users)");
var doc = view.getDocumentByKey(addressCandidate);
success = (doc != null);
doc.recycle();
view.recycle();
} catch (e) {
print(e.message);
}
return success;
}
That should do the trick
All,
Here is a unit test for checking the size of a collection
main() {
test("Resource Manager Image Load", () {
ResourceManager rm = new ResourceManager();
int WRONG_SIZE = 1000000;
rm.loadImageManifest("data/rm/test_images.yaml").then((_){
print("Length="+ rm.images.length.toString()); // PRINTS '6' - WHICH IS CORRECT
expect(rm.images, hasLength(WRONG_SIZE));
});
});
}
I am running this from a browser (client-side Dart libraries are in use) and it ALWAYS passes, no matter what the value of WRONG_SIZE.
Help appreciated.
In such simple cases you can just return the future. The unit test framework recognizes it and waits for the future to complete. This also works for setUp/tearDown.
main() {
test("Resource Manager Image Load", () {
ResourceManager rm = new ResourceManager();
int WRONG_SIZE = 1000000;
return rm.loadImageManifest("data/rm/test_images.yaml").then((_) {
//^^^^
print("Length="+ rm.images.length.toString()); // PRINTS '6' - WHICH IS CORRECT
expect(rm.images, hasLength(WRONG_SIZE));
});
});
}
The problem is that your code returns a Future, and your test completes before the code in the Future has finished, so there's nothing to cause it to fail.
Check out the Asynchronous Tests section on the Dart site. There are methods like expectAsync that allow the future to be passed to the test framework so that it can wait for them to complete and handle the result correctly.
Here's an example (note the expect call is now inside the function passed to expectAsync)
test('callback is executed once', () {
// wrap the callback of an asynchronous call with [expectAsync] if
// the callback takes 0 arguments...
var timer = Timer.run(expectAsync(() {
int x = 2 + 3;
expect(x, equals(5));
}));
});
I'm using KRL to inject elements into twitter timeline statuses similar to Jesse Stay's TwitterBook. The problem I have is that these elements are only associated with statuses that are currently visible when the bookmarklet is initiated. If a new status is added through the 'new tweet' updated via Ajax or through status updates via infinite scroll, these new statuses are untouched.
Is there a way to either poll for new statuses or sense a twitter status update event via KRL in order to inject elements only into those newly added statuses?
The example posted at
http://kynetxappaday.wordpress.com/2010/12/25/day-21-modifying-facebook-stream-with-kynetx/
works with the Facebook stream but the concept is the same
create setTimeout infinite loop to look for stream items
only select stream items not marked as processed
process stream items
rinse and repeat
Code example from post
ruleset a60x512 {
meta {
name "MikeGrace-status-update-translator"
description <<
MikeGrace-status-update-translator
>>
author "Mike Grace"
logging on
}
global {
datasource insult:HTML <- "http://www.pangloss.com/seidel/Shaker/index.html?" cachable for 1 second;
}
rule find_status_updates_by_mike_grace {
select when pageview ".*"
{
notify("Starting to look for status upates by Mike Grace","");
emit <|
// get app object to raise web events
app = KOBJ.get_application("a60x512");
// function that finds FB status updates by Mike Grace
function findMikeGrace() {
// loop through each stream item on the page that hasn't been processed already by the app
$K("li[id^=stream_story]:not(li[kfbt])").each(function() {
var currentStreamItem = this;
// grab the current stream item posters name
var name = $K(currentStreamItem).find(".actorName").text();
// mark the stream item as being processed to reduce future processing times
$K(currentStreamItem).attr("kfbt","y");
// is the stream item by the perpetrator?
if (name == "Michael Grace") {
// strikethrough the original update
$K(currentStreamItem).find(".messageBody").wrap("<strike />");
// get selector to return translation of status update
var returnSelector = $K(currentStreamItem).attr("id");
returnSelector = "li#"+returnSelector+" .messageBody";
// raise web event to get translation for non geeks
app.raise_event("get_insult", {"returnSelector":returnSelector});
} // end of checking name
}); // end of looping through unprocessed stream items
// call myself again later to process new items on the page
setTimeout(function() {
findMikeGrace();
}, 9000);
}
// start the process of finding the perpetrator
findMikeGrace();
|>;
}
}
rule get_insult {
select when web get_insult
pre {
selector = event:param("returnSelector");
insulter = datasource:insult("#{selector}");
foundInsult = insulter.query("font");
singleInsult = foundInsult[0];
}
{
emit <|
console.log(singleInsult);
$K(selector).parent().after("<br/>"+singleInsult);
|>;
}
}
}