1919import static androidx .test .espresso .matcher .ViewMatchers .withId ;
2020import static androidx .test .espresso .matcher .ViewMatchers .withText ;
2121import static org .hamcrest .Matchers .anything ;
22- import static org .hamcrest .Matchers .containsString ;
22+ import static org .hamcrest .Matchers .endsWith ;
2323import static org .hamcrest .Matchers .greaterThan ;
2424import static org .hamcrest .Matchers .hasToString ;
2525import static org .hamcrest .Matchers .not ;
@@ -118,12 +118,12 @@ public void testModelLoadingWorkflow() throws Exception {
118118 // Step 3: Click model selection button and select the model file
119119 onView (withId (R .id .modelImageButton )).perform (click ());
120120 // Select the model file matching the configured filename
121- onData (hasToString (containsString (modelFile ))).inRoot (isDialog ()).perform (click ());
121+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
122122
123123 // Step 4: Click tokenizer selection button and select the tokenizer file
124124 onView (withId (R .id .tokenizerImageButton )).perform (click ());
125125 // Select the tokenizer file matching the configured filename
126- onData (hasToString (containsString (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
126+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
127127
128128 // Step 5: Click load model button
129129 onView (withId (R .id .loadModelButton )).perform (click ());
@@ -165,13 +165,13 @@ public void testSendMessageAndReceiveResponse() throws Exception {
165165 // Select model - choose the configured model file
166166 onView (withId (R .id .modelImageButton )).perform (click ());
167167 Thread .sleep (300 ); // Wait for dialog to appear
168- onData (hasToString (containsString (modelFile ))).inRoot (isDialog ()).perform (click ());
168+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
169169 Thread .sleep (300 ); // Wait for dialog to dismiss and UI to update
170170
171171 // Select tokenizer - choose the configured tokenizer file
172172 onView (withId (R .id .tokenizerImageButton )).perform (click ());
173173 Thread .sleep (300 ); // Wait for dialog to appear
174- onData (hasToString (containsString (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
174+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
175175 Thread .sleep (300 ); // Wait for dialog to dismiss and UI to update
176176
177177 // Verify load button is now enabled
@@ -198,9 +198,10 @@ public void testSendMessageAndReceiveResponse() throws Exception {
198198 // Click send button
199199 onView (withId (R .id .sendButton )).perform (click ());
200200
201- // --- Wait for response and validate ---
202- // Wait 50 seconds for model to generate response
203- Thread .sleep (50000 );
201+ // --- Wait for response ---
202+ // Poll until we have some response text (at least 50 characters)
203+ boolean hasResponse = waitForResponseLength (scenario , 50 , 60000 );
204+ assertTrue ("Model should generate a response" , hasResponse );
204205
205206 // Extract all messages from the list
206207 AtomicInteger messageCount = new AtomicInteger (0 );
@@ -265,6 +266,143 @@ private boolean waitForModelLoaded(ActivityScenario<MainActivity> scenario, long
265266 return false ;
266267 }
267268
269+ /**
270+ * Tests stopping generation mid-way:
271+ * 1. Load model
272+ * 2. Send a message to start generation
273+ * 3. Wait for generation to start (button changes to stop mode)
274+ * 4. Click stop button
275+ * 5. Verify generation stops (button returns to send mode)
276+ * 6. Verify partial response was received
277+ */
278+ @ Test
279+ public void testStopGeneration () throws Exception {
280+ try (ActivityScenario <MainActivity > scenario = ActivityScenario .launch (MainActivity .class )) {
281+ // Wait for activity to fully load
282+ Thread .sleep (1000 );
283+
284+ // Dismiss the "Please Select a Model" dialog
285+ onView (withText (android .R .string .ok )).inRoot (isDialog ()).perform (click ());
286+
287+ // --- Load model ---
288+ onView (withId (R .id .settings )).perform (click ());
289+ Thread .sleep (500 );
290+
291+ // Select model
292+ onView (withId (R .id .modelImageButton )).perform (click ());
293+ Thread .sleep (300 );
294+ onData (hasToString (endsWith (modelFile ))).inRoot (isDialog ()).perform (click ());
295+ Thread .sleep (300 );
296+
297+ // Select tokenizer
298+ onView (withId (R .id .tokenizerImageButton )).perform (click ());
299+ Thread .sleep (300 );
300+ onData (hasToString (endsWith (tokenizerFile ))).inRoot (isDialog ()).perform (click ());
301+ Thread .sleep (300 );
302+
303+ // Load model
304+ onView (withId (R .id .loadModelButton )).perform (click ());
305+ onView (withText (android .R .string .yes )).inRoot (isDialog ()).perform (click ());
306+
307+ // Wait for model to load
308+ boolean modelLoaded = waitForModelLoaded (scenario , 60000 );
309+ assertTrue ("Model should be loaded successfully" , modelLoaded );
310+
311+ // --- Send a message to start generation ---
312+ onView (withId (R .id .editTextMessage )).perform (typeText ("Write a very long story about a brave knight" ), ViewActions .closeSoftKeyboard ());
313+ onView (withId (R .id .sendButton )).perform (click ());
314+
315+ // --- Wait for generation to start (some response text appears) ---
316+ boolean generationStarted = waitForResponseStarted (scenario , 30000 );
317+ assertTrue ("Generation should start (some response text should appear)" , generationStarted );
318+
319+ // --- Wait for some text to generate (at least 20 characters) ---
320+ boolean hasEnoughText = waitForResponseLength (scenario , 20 , 30000 );
321+ assertTrue ("Should generate some text before stopping" , hasEnoughText );
322+
323+ // --- Click stop button ---
324+ onView (withId (R .id .sendButton )).perform (click ());
325+
326+ // --- Wait for generation to stop ---
327+ // Give it a moment to process the stop
328+ Thread .sleep (1000 );
329+
330+ // --- Verify we got a partial response ---
331+ AtomicReference <String > responseText = new AtomicReference <>("" );
332+ scenario .onActivity (activity -> {
333+ ListView messagesView = activity .findViewById (R .id .messages_view );
334+ if (messagesView != null && messagesView .getAdapter () != null ) {
335+ for (int i = 0 ; i < messagesView .getAdapter ().getCount (); i ++) {
336+ Object item = messagesView .getAdapter ().getItem (i );
337+ if (item instanceof Message ) {
338+ Message message = (Message ) item ;
339+ // Find the model response (not sent by user, not system message)
340+ if (!message .getIsSent () && !message .getText ().contains ("Successfully loaded" )) {
341+ responseText .set (message .getText ());
342+ }
343+ }
344+ }
345+ }
346+ });
347+
348+ // Log the partial response
349+ android .util .Log .i ("STOP_TEST" , "Partial response after stop: " + responseText .get ());
350+
351+ // We should have received some tokens before stopping
352+ assertTrue ("Should have received some response before stopping" ,
353+ responseText .get () != null && !responseText .get ().isEmpty ());
354+ }
355+ }
356+
357+ /**
358+ * Waits for generation to start by checking for model response text.
359+ *
360+ * @param scenario the activity scenario
361+ * @param timeoutMs maximum time to wait in milliseconds
362+ * @return true if response text appeared, false if timeout
363+ */
364+ private boolean waitForResponseStarted (ActivityScenario <MainActivity > scenario , long timeoutMs ) throws InterruptedException {
365+ return waitForResponseLength (scenario , 1 , timeoutMs );
366+ }
367+
368+ /**
369+ * Waits for the model response to reach a minimum length.
370+ *
371+ * @param scenario the activity scenario
372+ * @param minLength minimum response length in characters
373+ * @param timeoutMs maximum time to wait in milliseconds
374+ * @return true if response reached minimum length, false if timeout
375+ */
376+ private boolean waitForResponseLength (ActivityScenario <MainActivity > scenario , int minLength , long timeoutMs ) throws InterruptedException {
377+ long startTime = System .currentTimeMillis ();
378+ while (System .currentTimeMillis () - startTime < timeoutMs ) {
379+ AtomicInteger responseLength = new AtomicInteger (0 );
380+ scenario .onActivity (activity -> {
381+ ListView messagesView = activity .findViewById (R .id .messages_view );
382+ if (messagesView != null && messagesView .getAdapter () != null ) {
383+ for (int i = 0 ; i < messagesView .getAdapter ().getCount (); i ++) {
384+ Object item = messagesView .getAdapter ().getItem (i );
385+ if (item instanceof Message ) {
386+ Message message = (Message ) item ;
387+ // Look for a model response (not sent, not system message)
388+ if (!message .getIsSent ()
389+ && !message .getText ().contains ("Successfully loaded" )
390+ && !message .getText ().contains ("Loading model" )
391+ && !message .getText ().contains ("To get started" )) {
392+ responseLength .set (message .getText ().length ());
393+ }
394+ }
395+ }
396+ }
397+ });
398+ if (responseLength .get () >= minLength ) {
399+ return true ;
400+ }
401+ Thread .sleep (200 ); // Poll every 200ms
402+ }
403+ return false ;
404+ }
405+
268406 /**
269407 * Writes the model response to logcat with a special tag for extraction.
270408 * The response can be extracted from logcat using: grep "LLAMA_RESPONSE"
0 commit comments