Skip to content

Commit e3e2326

Browse files
authored
LlamaDemo more instrumentation tests (#159)
1 parent a4ed327 commit e3e2326

3 files changed

Lines changed: 331 additions & 19 deletions

File tree

llm/android/LlamaDemo/app/src/androidTest/java/com/example/executorchllamademo/UIWorkflowTest.java

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,89 @@ public void testStopGeneration() throws Exception {
356356
}
357357
}
358358

359+
/**
360+
* Tests that trying to send a new message during generation is not possible:
361+
* 1. Load model
362+
* 2. Send a message to start generation
363+
* 3. Wait for generation to start
364+
* 4. Verify input field is cleared
365+
* 5. Type new text in input field
366+
* 6. Verify send button shows stop icon (not send) - button is enabled for stopping
367+
* 7. Stop generation and verify button returns to send mode
368+
*/
369+
@Test
370+
public void testSendDuringGeneration() throws Exception {
371+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
372+
// Wait for activity to fully load
373+
Thread.sleep(1000);
374+
375+
// Dismiss the "Please Select a Model" dialog
376+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
377+
378+
// --- Load model ---
379+
onView(withId(R.id.settings)).perform(click());
380+
Thread.sleep(500);
381+
382+
// Select model
383+
onView(withId(R.id.modelImageButton)).perform(click());
384+
Thread.sleep(300);
385+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
386+
Thread.sleep(300);
387+
388+
// Select tokenizer
389+
onView(withId(R.id.tokenizerImageButton)).perform(click());
390+
Thread.sleep(300);
391+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
392+
Thread.sleep(300);
393+
394+
// Load model
395+
onView(withId(R.id.loadModelButton)).perform(click());
396+
onView(withText(android.R.string.yes)).inRoot(isDialog()).perform(click());
397+
398+
// Wait for model to load
399+
boolean modelLoaded = waitForModelLoaded(scenario, 60000);
400+
assertTrue("Model should be loaded successfully", modelLoaded);
401+
402+
// --- Send a message to start generation (use a longer prompt for slower generation) ---
403+
onView(withId(R.id.editTextMessage)).perform(typeText("Write a very long detailed story about a brave knight who goes on an adventure"), ViewActions.closeSoftKeyboard());
404+
onView(withId(R.id.sendButton)).perform(click());
405+
406+
// --- Wait for generation to start ---
407+
boolean generationStarted = waitForResponseStarted(scenario, 30000);
408+
assertTrue("Generation should start", generationStarted);
409+
410+
// --- Verify input field is cleared after sending ---
411+
onView(withId(R.id.editTextMessage)).check(matches(withText("")));
412+
413+
// --- Check if still generating (button enabled means stop mode) ---
414+
AtomicBoolean isStillGenerating = new AtomicBoolean(false);
415+
scenario.onActivity(activity -> {
416+
ImageButton sendButton = activity.findViewById(R.id.sendButton);
417+
if (sendButton != null) {
418+
isStillGenerating.set(sendButton.isEnabled());
419+
}
420+
});
421+
422+
// If generation already completed, skip the during-generation checks
423+
if (!isStillGenerating.get()) {
424+
android.util.Log.i("UIWorkflowTest", "Generation completed quickly, skipping during-generation checks");
425+
return;
426+
}
427+
428+
// --- Type new text during generation ---
429+
onView(withId(R.id.editTextMessage)).perform(typeText("Another message"), ViewActions.closeSoftKeyboard());
430+
431+
// --- Stop generation ---
432+
onView(withId(R.id.sendButton)).perform(click());
433+
Thread.sleep(1000);
434+
435+
// --- Verify generation stopped and we can now send ---
436+
// After stopping, the input still has text, so send button should be enabled
437+
onView(withId(R.id.editTextMessage)).check(matches(withText("Another message")));
438+
onView(withId(R.id.sendButton)).check(matches(isEnabled()));
439+
}
440+
}
441+
359442
/**
360443
* Waits for generation to start by checking for model response text.
361444
*
@@ -553,6 +636,242 @@ public void testNoFilesInDirectory() throws Exception {
553636
}
554637
}
555638

639+
/**
640+
* Tests that canceling file selection dialogs does not change the current selection state:
641+
* 1. Go to settings
642+
* 2. Verify initial state (no model/tokenizer selected)
643+
* 3. Select a model file
644+
* 4. Open model selection again and press back without selecting
645+
* 5. Verify original selection is preserved
646+
* 6. Same test for tokenizer
647+
*/
648+
@Test
649+
public void testCancelFileSelection() throws Exception {
650+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
651+
// Wait for activity to fully load
652+
Thread.sleep(1000);
653+
654+
// Dismiss the "Please Select a Model" dialog
655+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
656+
657+
// Go to settings
658+
onView(withId(R.id.settings)).perform(click());
659+
Thread.sleep(500);
660+
661+
// Verify we're in settings with no initial selections
662+
onView(withId(R.id.modelTextView)).check(matches(withText("no model selected")));
663+
onView(withId(R.id.tokenizerTextView)).check(matches(withText("no tokenizer selected")));
664+
665+
// --- Test model selection cancellation ---
666+
// First, select a model
667+
onView(withId(R.id.modelImageButton)).perform(click());
668+
Thread.sleep(300);
669+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
670+
Thread.sleep(300);
671+
672+
// Verify model is selected
673+
onView(withId(R.id.modelTextView)).check(matches(withText(endsWith(modelFile))));
674+
675+
// Open model selection again
676+
onView(withId(R.id.modelImageButton)).perform(click());
677+
Thread.sleep(300);
678+
679+
// Press back to cancel without selecting
680+
androidx.test.espresso.Espresso.pressBack();
681+
Thread.sleep(300);
682+
683+
// Verify original selection is preserved
684+
onView(withId(R.id.modelTextView)).check(matches(withText(endsWith(modelFile))));
685+
686+
// --- Test tokenizer selection cancellation ---
687+
// First, select a tokenizer
688+
onView(withId(R.id.tokenizerImageButton)).perform(click());
689+
Thread.sleep(300);
690+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
691+
Thread.sleep(300);
692+
693+
// Verify tokenizer is selected
694+
onView(withId(R.id.tokenizerTextView)).check(matches(withText(endsWith(tokenizerFile))));
695+
696+
// Open tokenizer selection again
697+
onView(withId(R.id.tokenizerImageButton)).perform(click());
698+
Thread.sleep(300);
699+
700+
// Press back to cancel without selecting
701+
androidx.test.espresso.Espresso.pressBack();
702+
Thread.sleep(300);
703+
704+
// Verify original selection is preserved
705+
onView(withId(R.id.tokenizerTextView)).check(matches(withText(endsWith(tokenizerFile))));
706+
}
707+
}
708+
709+
/**
710+
* Tests that the load button is disabled until both model and tokenizer are selected:
711+
* 1. Go to settings
712+
* 2. Verify load button is initially disabled (skip if cached values exist)
713+
* 3. Select only model, verify load button still disabled
714+
* 4. Select tokenizer, verify load button becomes enabled
715+
*/
716+
@Test
717+
public void testLoadButtonDisabledState() throws Exception {
718+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
719+
// Wait for activity to fully load
720+
Thread.sleep(1000);
721+
722+
// Dismiss the "Please Select a Model" dialog
723+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
724+
725+
// Go to settings
726+
onView(withId(R.id.settings)).perform(click());
727+
Thread.sleep(500);
728+
729+
// Check if there are cached model/tokenizer selections from previous sessions
730+
// If so, skip this test as the load button would already be enabled
731+
try {
732+
onView(withId(R.id.modelTextView)).check(matches(withText("no model selected")));
733+
onView(withId(R.id.tokenizerTextView)).check(matches(withText("no tokenizer selected")));
734+
} catch (AssertionError e) {
735+
// Cached selections exist, skip this test
736+
android.util.Log.i("UIWorkflowTest", "Skipping testLoadButtonDisabledState: cached selections exist");
737+
return;
738+
}
739+
740+
// Verify load button is initially disabled (no model/tokenizer selected)
741+
onView(withId(R.id.loadModelButton)).check(matches(not(isEnabled())));
742+
743+
// --- Select only model ---
744+
onView(withId(R.id.modelImageButton)).perform(click());
745+
Thread.sleep(300);
746+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
747+
Thread.sleep(300);
748+
749+
// Verify model is selected but load button still disabled (no tokenizer)
750+
onView(withId(R.id.modelTextView)).check(matches(withText(endsWith(modelFile))));
751+
onView(withId(R.id.loadModelButton)).check(matches(not(isEnabled())));
752+
753+
// --- Now select tokenizer ---
754+
onView(withId(R.id.tokenizerImageButton)).perform(click());
755+
Thread.sleep(300);
756+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
757+
Thread.sleep(300);
758+
759+
// Verify both selected and load button is now enabled
760+
onView(withId(R.id.tokenizerTextView)).check(matches(withText(endsWith(tokenizerFile))));
761+
onView(withId(R.id.loadModelButton)).check(matches(isEnabled()));
762+
}
763+
}
764+
765+
/**
766+
* Tests that the send button is disabled when input contains only whitespace:
767+
* 1. Load model
768+
* 2. Verify send button is disabled with empty input
769+
* 3. Type only spaces, verify send button remains disabled
770+
* 4. Type only tabs/newlines, verify send button remains disabled
771+
* 5. Type actual text, verify send button becomes enabled
772+
* 6. Clear and type whitespace + text + whitespace, verify enabled
773+
*/
774+
@Test
775+
public void testWhitespaceOnlyPrompt() throws Exception {
776+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
777+
// Wait for activity to fully load
778+
Thread.sleep(1000);
779+
780+
// Dismiss the "Please Select a Model" dialog
781+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
782+
783+
// --- Load model ---
784+
onView(withId(R.id.settings)).perform(click());
785+
Thread.sleep(500);
786+
787+
// Select model
788+
onView(withId(R.id.modelImageButton)).perform(click());
789+
Thread.sleep(300);
790+
onData(hasToString(endsWith(modelFile))).inRoot(isDialog()).perform(click());
791+
Thread.sleep(300);
792+
793+
// Select tokenizer
794+
onView(withId(R.id.tokenizerImageButton)).perform(click());
795+
Thread.sleep(300);
796+
onData(hasToString(endsWith(tokenizerFile))).inRoot(isDialog()).perform(click());
797+
Thread.sleep(300);
798+
799+
// Load model
800+
onView(withId(R.id.loadModelButton)).perform(click());
801+
onView(withText(android.R.string.yes)).inRoot(isDialog()).perform(click());
802+
803+
// Wait for model to load
804+
boolean modelLoaded = waitForModelLoaded(scenario, 60000);
805+
assertTrue("Model should be loaded successfully", modelLoaded);
806+
807+
// --- Test whitespace-only input behavior ---
808+
// Verify send button is disabled when input is empty
809+
onView(withId(R.id.sendButton)).check(matches(not(isEnabled())));
810+
811+
// Type only spaces
812+
onView(withId(R.id.editTextMessage)).perform(typeText(" "), ViewActions.closeSoftKeyboard());
813+
814+
// Verify send button is still disabled (whitespace only)
815+
onView(withId(R.id.sendButton)).check(matches(not(isEnabled())));
816+
817+
// Clear and type actual text
818+
onView(withId(R.id.editTextMessage)).perform(ViewActions.clearText());
819+
onView(withId(R.id.editTextMessage)).perform(typeText("hello"), ViewActions.closeSoftKeyboard());
820+
821+
// Verify send button is now enabled
822+
onView(withId(R.id.sendButton)).check(matches(isEnabled()));
823+
824+
// Clear and type text with surrounding whitespace
825+
onView(withId(R.id.editTextMessage)).perform(ViewActions.clearText());
826+
onView(withId(R.id.editTextMessage)).perform(typeText(" hello world "), ViewActions.closeSoftKeyboard());
827+
828+
// Verify send button is still enabled (has non-whitespace content)
829+
onView(withId(R.id.sendButton)).check(matches(isEnabled()));
830+
831+
// Clear and verify disabled again
832+
onView(withId(R.id.editTextMessage)).perform(ViewActions.clearText());
833+
onView(withId(R.id.sendButton)).check(matches(not(isEnabled())));
834+
}
835+
}
836+
837+
/**
838+
* Tests the add media button toggle functionality:
839+
* 1. Launch MainActivity
840+
* 2. Dismiss the "Please Select a Model" dialog
841+
* 3. Verify add media layout is initially hidden
842+
* 4. Click add media button (+) to show the attachment options
843+
* 5. Verify add media layout is now visible
844+
* 6. Click add media button again (now shows collapse icon) to hide the attachment options
845+
* 7. Verify add media layout is hidden again
846+
*/
847+
@Test
848+
public void testCollapseMediaButton() throws Exception {
849+
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
850+
// Wait for activity to fully load
851+
Thread.sleep(1000);
852+
853+
// Dismiss the "Please Select a Model" dialog
854+
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click());
855+
856+
// Verify add media layout is initially hidden (GONE)
857+
onView(withId(R.id.addMediaLayout)).check(matches(not(isDisplayed())));
858+
859+
// Click add media button (+) to show attachment options
860+
onView(withId(R.id.addMediaButton)).perform(click());
861+
Thread.sleep(300);
862+
863+
// Verify add media layout is now visible
864+
onView(withId(R.id.addMediaLayout)).check(matches(isDisplayed()));
865+
866+
// Click add media button again (now shows collapse icon) to hide attachment options
867+
onView(withId(R.id.addMediaButton)).perform(click());
868+
Thread.sleep(300);
869+
870+
// Verify add media layout is hidden again
871+
onView(withId(R.id.addMediaLayout)).check(matches(not(isDisplayed())));
872+
}
873+
}
874+
556875
/**
557876
* Writes the model response to logcat with a special tag for extraction.
558877
* The response can be extracted from logcat using: grep "LLAMA_RESPONSE"

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/MainActivity.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,15 @@ private void setupMediaButton() {
512512
ImageButton addMediaButton = requireViewById(R.id.addMediaButton);
513513
addMediaButton.setOnClickListener(
514514
view -> {
515-
mAddMediaLayout.setVisibility(View.VISIBLE);
515+
if (mAddMediaLayout.getVisibility() == View.VISIBLE) {
516+
// Collapse: hide the media layout and change icon back to +
517+
mAddMediaLayout.setVisibility(View.GONE);
518+
addMediaButton.setImageResource(R.drawable.baseline_add_24);
519+
} else {
520+
// Expand: show the media layout and change icon to collapse (down arrow)
521+
mAddMediaLayout.setVisibility(View.VISIBLE);
522+
addMediaButton.setImageResource(R.drawable.expand_circle_down);
523+
}
516524
});
517525

518526
mGalleryButton = requireViewById(R.id.galleryButton);
@@ -798,7 +806,7 @@ private void addSelectedImagesToChatThread(List<Uri> selectedImageUri) {
798806
}
799807

800808
private void updateSendButtonState() {
801-
boolean hasText = mEditTextMessage.getText().length() > 0;
809+
boolean hasText = mEditTextMessage.getText().toString().trim().length() > 0;
802810
boolean enabled = mIsModelReady && !mIsGenerating && hasText;
803811
mSendButton.setEnabled(enabled);
804812
mSendButton.setAlpha(enabled ? 1.0f : 0.3f);
@@ -808,7 +816,6 @@ private void onModelRunStarted() {
808816
mIsGenerating = true;
809817
mSendButton.setEnabled(true);
810818
mSendButton.setAlpha(1.0f);
811-
mSendButton.setClickable(true);
812819
mSendButton.setImageResource(R.drawable.baseline_stop_24);
813820
mSendButton.setOnClickListener(
814821
view -> {

0 commit comments

Comments
 (0)