@@ -202,7 +202,7 @@ public void testSendMessageAndReceiveResponse() throws Exception {
202202
203203 // --- Wait for generation to complete ---
204204 // Poll until the send button is enabled again (generation finished)
205- boolean generationComplete = waitForGenerationComplete (scenario , 120000 ); // 2 minute timeout
205+ boolean generationComplete = waitForGenerationComplete (scenario , 300000 ); // 5 minute timeout
206206 assertTrue ("Generation should complete" , generationComplete );
207207
208208 // Extract all messages from the list
@@ -553,6 +553,204 @@ public void testNoFilesInDirectory() throws Exception {
553553 }
554554 }
555555
556+ /**
557+ * Tests that canceling file selection dialogs does not change the current selection state:
558+ * 1. Go to settings
559+ * 2. Verify initial state (no model/tokenizer selected)
560+ * 3. Select a model file
561+ * 4. Open model selection again and press back without selecting
562+ * 5. Verify original selection is preserved
563+ * 6. Same test for tokenizer
564+ */
565+ @ Test
566+ public void testCancelFileSelection () throws Exception {
567+ try (ActivityScenario <MainActivity > scenario = ActivityScenario .launch (MainActivity .class )) {
568+ // Wait for activity to fully load
569+ Thread .sleep (1000 );
570+
571+ // Dismiss the "Please Select a Model" dialog
572+ onView (withText (android .R .string .ok )).inRoot (isDialog ()).perform (click ());
573+
574+ // Go to settings
575+ onView (withId (R .id .settings )).perform (click ());
576+ Thread .sleep (500 );
577+
578+ // Verify we're in settings with no initial selections
579+ onView (withId (R .id .modelTextView )).check (matches (withText ("no model selected" )));
580+ onView (withId (R .id .tokenizerTextView )).check (matches (withText ("no tokenizer selected" )));
581+
582+ // --- Test model selection cancellation ---
583+ // First, select a model
584+ onView (withId (R .id .modelImageButton )).perform (click ());
585+ Thread .sleep (300 );
586+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
587+ Thread .sleep (300 );
588+
589+ // Verify model is selected
590+ onView (withId (R .id .modelTextView )).check (matches (withText (endsWith (modelFile ))));
591+
592+ // Open model selection again
593+ onView (withId (R .id .modelImageButton )).perform (click ());
594+ Thread .sleep (300 );
595+
596+ // Press back to cancel without selecting
597+ androidx .test .espresso .Espresso .pressBack ();
598+ Thread .sleep (300 );
599+
600+ // Verify original selection is preserved
601+ onView (withId (R .id .modelTextView )).check (matches (withText (endsWith (modelFile ))));
602+
603+ // --- Test tokenizer selection cancellation ---
604+ // First, select a tokenizer
605+ onView (withId (R .id .tokenizerImageButton )).perform (click ());
606+ Thread .sleep (300 );
607+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
608+ Thread .sleep (300 );
609+
610+ // Verify tokenizer is selected
611+ onView (withId (R .id .tokenizerTextView )).check (matches (withText (endsWith (tokenizerFile ))));
612+
613+ // Open tokenizer selection again
614+ onView (withId (R .id .tokenizerImageButton )).perform (click ());
615+ Thread .sleep (300 );
616+
617+ // Press back to cancel without selecting
618+ androidx .test .espresso .Espresso .pressBack ();
619+ Thread .sleep (300 );
620+
621+ // Verify original selection is preserved
622+ onView (withId (R .id .tokenizerTextView )).check (matches (withText (endsWith (tokenizerFile ))));
623+ }
624+ }
625+
626+ /**
627+ * Tests that the load button is disabled until both model and tokenizer are selected:
628+ * 1. Go to settings
629+ * 2. Verify load button is initially disabled (skip if cached values exist)
630+ * 3. Select only model, verify load button still disabled
631+ * 4. Select tokenizer, verify load button becomes enabled
632+ */
633+ @ Test
634+ public void testLoadButtonDisabledState () throws Exception {
635+ try (ActivityScenario <MainActivity > scenario = ActivityScenario .launch (MainActivity .class )) {
636+ // Wait for activity to fully load
637+ Thread .sleep (1000 );
638+
639+ // Dismiss the "Please Select a Model" dialog
640+ onView (withText (android .R .string .ok )).inRoot (isDialog ()).perform (click ());
641+
642+ // Go to settings
643+ onView (withId (R .id .settings )).perform (click ());
644+ Thread .sleep (500 );
645+
646+ // Check if there are cached model/tokenizer selections from previous sessions
647+ // If so, skip this test as the load button would already be enabled
648+ try {
649+ onView (withId (R .id .modelTextView )).check (matches (withText ("no model selected" )));
650+ onView (withId (R .id .tokenizerTextView )).check (matches (withText ("no tokenizer selected" )));
651+ } catch (AssertionError e ) {
652+ // Cached selections exist, skip this test
653+ android .util .Log .i ("UIWorkflowTest" , "Skipping testLoadButtonDisabledState: cached selections exist" );
654+ return ;
655+ }
656+
657+ // Verify load button is initially disabled (no model/tokenizer selected)
658+ onView (withId (R .id .loadModelButton )).check (matches (not (isEnabled ())));
659+
660+ // --- Select only model ---
661+ onView (withId (R .id .modelImageButton )).perform (click ());
662+ Thread .sleep (300 );
663+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
664+ Thread .sleep (300 );
665+
666+ // Verify model is selected but load button still disabled (no tokenizer)
667+ onView (withId (R .id .modelTextView )).check (matches (withText (endsWith (modelFile ))));
668+ onView (withId (R .id .loadModelButton )).check (matches (not (isEnabled ())));
669+
670+ // --- Now select tokenizer ---
671+ onView (withId (R .id .tokenizerImageButton )).perform (click ());
672+ Thread .sleep (300 );
673+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
674+ Thread .sleep (300 );
675+
676+ // Verify both selected and load button is now enabled
677+ onView (withId (R .id .tokenizerTextView )).check (matches (withText (endsWith (tokenizerFile ))));
678+ onView (withId (R .id .loadModelButton )).check (matches (isEnabled ()));
679+ }
680+ }
681+
682+ /**
683+ * Tests that the send button is disabled when input contains only whitespace:
684+ * 1. Load model
685+ * 2. Verify send button is disabled with empty input
686+ * 3. Type only spaces, verify send button remains disabled
687+ * 4. Type only tabs/newlines, verify send button remains disabled
688+ * 5. Type actual text, verify send button becomes enabled
689+ * 6. Clear and type whitespace + text + whitespace, verify enabled
690+ */
691+ @ Test
692+ public void testWhitespaceOnlyPrompt () throws Exception {
693+ try (ActivityScenario <MainActivity > scenario = ActivityScenario .launch (MainActivity .class )) {
694+ // Wait for activity to fully load
695+ Thread .sleep (1000 );
696+
697+ // Dismiss the "Please Select a Model" dialog
698+ onView (withText (android .R .string .ok )).inRoot (isDialog ()).perform (click ());
699+
700+ // --- Load model ---
701+ onView (withId (R .id .settings )).perform (click ());
702+ Thread .sleep (500 );
703+
704+ // Select model
705+ onView (withId (R .id .modelImageButton )).perform (click ());
706+ Thread .sleep (300 );
707+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
708+ Thread .sleep (300 );
709+
710+ // Select tokenizer
711+ onView (withId (R .id .tokenizerImageButton )).perform (click ());
712+ Thread .sleep (300 );
713+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
714+ Thread .sleep (300 );
715+
716+ // Load model
717+ onView (withId (R .id .loadModelButton )).perform (click ());
718+ onView (withText (android .R .string .yes )).inRoot (isDialog ()).perform (click ());
719+
720+ // Wait for model to load
721+ boolean modelLoaded = waitForModelLoaded (scenario , 60000 );
722+ assertTrue ("Model should be loaded successfully" , modelLoaded );
723+
724+ // --- Test whitespace-only input behavior ---
725+ // Verify send button is disabled when input is empty
726+ onView (withId (R .id .sendButton )).check (matches (not (isEnabled ())));
727+
728+ // Type only spaces
729+ onView (withId (R .id .editTextMessage )).perform (typeText (" " ), ViewActions .closeSoftKeyboard ());
730+
731+ // Verify send button is still disabled (whitespace only)
732+ onView (withId (R .id .sendButton )).check (matches (not (isEnabled ())));
733+
734+ // Clear and type actual text
735+ onView (withId (R .id .editTextMessage )).perform (ViewActions .clearText ());
736+ onView (withId (R .id .editTextMessage )).perform (typeText ("hello" ), ViewActions .closeSoftKeyboard ());
737+
738+ // Verify send button is now enabled
739+ onView (withId (R .id .sendButton )).check (matches (isEnabled ()));
740+
741+ // Clear and type text with surrounding whitespace
742+ onView (withId (R .id .editTextMessage )).perform (ViewActions .clearText ());
743+ onView (withId (R .id .editTextMessage )).perform (typeText (" hello world " ), ViewActions .closeSoftKeyboard ());
744+
745+ // Verify send button is still enabled (has non-whitespace content)
746+ onView (withId (R .id .sendButton )).check (matches (isEnabled ()));
747+
748+ // Clear and verify disabled again
749+ onView (withId (R .id .editTextMessage )).perform (ViewActions .clearText ());
750+ onView (withId (R .id .sendButton )).check (matches (not (isEnabled ())));
751+ }
752+ }
753+
556754 /**
557755 * Writes the model response to logcat with a special tag for extraction.
558756 * The response can be extracted from logcat using: grep "LLAMA_RESPONSE"
0 commit comments