@@ -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"
0 commit comments