1515 */
1616package org .labkey .test .tests .testresults ;
1717
18+ import org .apache .hc .client5 .http .classic .methods .HttpGet ;
1819import org .apache .hc .client5 .http .classic .methods .HttpPost ;
1920import org .apache .hc .client5 .http .entity .mime .MultipartEntityBuilder ;
2021import org .apache .hc .client5 .http .impl .classic .CloseableHttpClient ;
4950import java .util .Map ;
5051
5152import static org .junit .Assert .assertEquals ;
53+ import static org .junit .Assert .assertFalse ;
5254import static org .junit .Assert .assertTrue ;
5355
5456@ Category ({External .class , MacCossLabModules .class })
@@ -59,6 +61,13 @@ public class TestResultsTest extends BaseWebDriverTest implements PostgresOnlyTe
5961 static final String COMPUTER_NAME_1 = "TEST-PC-1" ;
6062 static final String COMPUTER_NAME_2 = "TEST-PC-2" ;
6163
64+ private static final Locator SUBMIT_BUTTON = Locator .css ("input[type='submit'][value='Submit']" );
65+
66+ // XPath for the problems matrix table (header cell contains "Fail: | Leak: | Hang:")
67+ private static final String PROBLEMS_TABLE_XPATH =
68+ "//table[contains(@class,'decoratedtable')]" +
69+ "[.//td[contains(.,'Fail:') and contains(.,'Leak:') and contains(.,'Hang:')]]" ;
70+
6271 // Run IDs populated in @BeforeClass, used across test methods
6372 private static int _disposableRunId = -1 ;
6473 private static int _cleanRunId = -1 ;
@@ -164,12 +173,6 @@ public void navigateToProject()
164173 // Tests
165174 // -------------------------------------------------------------------------
166175
167- private static final Locator SUBMIT_BUTTON = Locator .css ("input[type='submit'][value='Submit']" );
168- // XPath for the problems matrix table (header cell contains "Fail: | Leak: | Hang:")
169- private static final String PROBLEMS_TABLE_XPATH =
170- "//table[contains(@class,'decoratedtable')]" +
171- "[.//td[contains(.,'Fail:') and contains(.,'Leak:') and contains(.,'Hang:')]]" ;
172-
173176 @ Test
174177 public void testBeginPage ()
175178 {
@@ -324,7 +327,7 @@ public void testShowFailuresPage()
324327 // 08:01 via setToEightAM, but the start wall-clock time can shift
325328 // across DST boundaries (e.g. start 07:01 PST, end 08:01 PDT when
326329 // the window crosses spring-forward), so asserting just the dates
327- // keeps the test stable year-round .
330+ // keeps the test stable.
328331
329332 // --- Path 1: navigate via runDetail.jsp ---
330333 // The "TestFailOne" link on runDetail.jsp does NOT set the `end`
@@ -356,17 +359,11 @@ public void testShowFailuresPage()
356359 // --- Path 2: navigate via the Fail/Leak/Hang table on rundown.jsp ---
357360 // The link in this table sets `end` to the begin page's selected
358361 // date but does not set `viewType`, so the controller defaults to
359- // ViewType.DAY → start = end - 1 day. (The Top Failures table
360- // link also passes `viewType`, which preserves rundown's current
361- // view and produces a 30-day window — not what we want here.)
362- // Anchoring to a fixed sample-data date (01/17/2026) keeps this
363- // assertion stable regardless of when the test runs. The link
362+ // ViewType.DAY → start = end - 1 day. The link
364363 // opens in a new browser tab via target="_blank".
365364 beginAt (WebTestHelper .buildRelativeUrl ("testresults" , PROJECT_NAME , "begin" ,
366365 Map .of ("end" , "01/17/2026" )));
367- Locator failLeakHangLink = Locator .xpath (
368- "//table[contains(@class,'decoratedtable')][.//td[contains(., 'Fail:') and contains(., 'Leak:') and contains(., 'Hang:')]]" +
369- "//a[text()='TestFailOne']" );
366+ Locator failLeakHangLink = Locator .xpath (PROBLEMS_TABLE_XPATH + "//a[text()='TestFailOne']" );
370367 click (failLeakHangLink );
371368 switchToWindow (1 ); // Link opens in a new tab
372369 try
@@ -396,8 +393,7 @@ public void testShowFlaggedPage()
396393 // Verify the Flags page now shows the flagged run. Each flagged row is
397394 // rendered (in flagged.jsp) as a link with text:
398395 // "id: <runId> / <userId> / <postTime>"
399- // Match by the runId prefix — userId and postTime are baked into the
400- // sample XML and aren't worth duplicating in the test.
396+ // Match by the runId prefix
401397 clickAndWait (Locator .linkWithText ("Flags" ));
402398 assertTextNotPresent ("There are currently no flagged runs." );
403399 assertTextPresent ("Flagged Runs" );
@@ -420,18 +416,14 @@ public void testTrainingDataPage()
420416 // rows with Date | Duration | Tests Run | Failure Count | Mean Memory
421417 // | Remove, and a `.stats-row` with "RunCount:N"). Users WITHOUT
422418 // training runs are listed in a separate <table> below, under a
423- // "No Training Data --" header — that header is always present, so
424- // it can't be used as an empty-state indicator.
419+ // "No Training Data --" header
425420 Locator .XPathLocator trainingTable = Locator .tagWithId ("table" , "trainingdata" );
426421 Locator removeLink = trainingTable .descendant (Locator .tagWithClass ("a" , "removedata" ));
427422 Locator statsRow = trainingTable .descendant (Locator .tagWithClass ("tr" , "stats-row" ));
428423
429424 // Initial state: no run has been added to the training set yet, so
430425 // the clean run's date should not appear in the training table and
431- // no Remove link should be present. (We can't assert the stats-row
432- // is absent — users may already have non-zero mean memory / mean
433- // tests-run from the imported sample data, which causes a stats-row
434- // to render before any run is explicitly added.)
426+ // no Remove link should be present.
435427 goToProjectHome (PROJECT_NAME );
436428 clickAndWait (Locator .linkWithText ("Training Data" ));
437429 assertElementNotPresent (removeLink );
@@ -445,8 +437,7 @@ public void testTrainingDataPage()
445437 assertTextPresent ("Remove from training set" );
446438
447439 // Verify the Training Data page now shows the run for COMPUTER_NAME_1.
448- // Scope assertions to <table id="trainingdata"> so we don't accidentally
449- // match the same text in the "No Training Data" section below.
440+ // Scope assertions to <table id="trainingdata">
450441 clickAndWait (Locator .linkWithText ("Training Data" ));
451442 assertElementPresent (trainingTable .descendant (
452443 Locator .tagWithId ("tr" , "user-anchor-" + COMPUTER_NAME_1 )));
@@ -464,12 +455,14 @@ public void testTrainingDataPage()
464455 toggleTrainingSet ();
465456 assertTextPresent ("Add to training set" );
466457
467- // After removal: the Remove link and the run's data row are gone, but
468- // TEST-PC-1's section stays in the training table because the user's
469- // meanmemory / meantestsrun are persisted on the User row (not derived
470- // from current training rows). The stats row now shows "RunCount:0".
471- // See trainingdata.jsp:158 — users only move to the "No Training Data"
472- // section when both meanmemory and meantestsrun are 0.
458+ // After removal: the Remove link and the run's data row are gone, and
459+ // the stats row now shows "RunCount:0". TEST-PC-1's section is still
460+ // rendered in the training table though, because of a known bug in
461+ // TrainRunAction: when the user's last training run is removed, the
462+ // the stale UserData row (non-zero meanmemory / meantestsrun) is
463+ // never cleared. trainingdata.jsp:158 only moves users to the
464+ // "No Training Data --" list when both fields are 0, so the section
465+ // stays. See TODO-LK-20260403_testresults-bugs.md.
473466 clickAndWait (Locator .linkWithText ("Training Data" ));
474467 assertElementNotPresent (removeLink );
475468 assertElementNotPresent (trainingTable .containing ("2026-01-16 06:00" ));
@@ -481,7 +474,7 @@ public void testTrainingDataPage()
481474 public void testViewLog ()
482475 {
483476 // The clean run has a <Log> element — ViewLogAction should return it
484- String logContent = getApiString ("testresults" , " viewLog" , _cleanRunId , "log" );
477+ String logContent = getApiString ("viewLog" , _cleanRunId , "log" );
485478 assertTrue ("ViewLog should return log content" , logContent != null && !logContent .isEmpty ());
486479 // Spot-check the nightly header and test entries from the beginning,
487480 // middle, and end of the log (see pc1-run-0115-clean.xml).
@@ -498,11 +491,11 @@ public void testViewLog()
498491 public void testViewXml ()
499492 {
500493 // ViewXmlAction should return the stored XML (without the <Log> element)
501- String xmlContent = getApiString ("testresults" , " viewXml" , _cleanRunId , "xml" );
494+ String xmlContent = getApiString ("viewXml" , _cleanRunId , "xml" );
502495 assertTrue ("ViewXml should return XML content" , xmlContent != null && !xmlContent .isEmpty ());
503496 assertTrue ("XML should contain nightly element" , xmlContent .contains ("nightly" ));
504497 assertTrue ("XML should contain test data" , xmlContent .contains ("TestAlpha" ));
505- assertTrue ("XML should not contain Log element (stripped before storage)" , ! xmlContent .contains ("<Log>" ));
498+ assertFalse ("XML should not contain Log element (stripped before storage)" , xmlContent .contains ("<Log>" ));
506499 }
507500
508501 @ Test
@@ -579,19 +572,19 @@ public void testApiErrorResponses()
579572 {
580573 // TrainRunAction: missing runId
581574 JSONObject noRunId = postApi ("trainRun" , Map .of ("train" , "true" ));
582- assertEquals ( false , noRunId .optBoolean ("Success" , true ));
575+ assertFalse ( noRunId .optBoolean ("Success" , true ));
583576 assertEquals ("runId is required" , noRunId .optString ("error" ));
584577
585578 // TrainRunAction: invalid train value
586579 JSONObject badTrain = postApi ("trainRun" ,
587580 Map .of ("runId" , String .valueOf (_cleanRunId ), "train" , "garbage" ));
588- assertEquals ( false , badTrain .optBoolean ("Success" , true ));
581+ assertFalse ( badTrain .optBoolean ("Success" , true ));
589582 assertEquals ("train must be one of: true, false, force" , badTrain .optString ("error" ));
590583
591584 // TrainRunAction: nonexistent runId
592585 JSONObject missingRun = postApi ("trainRun" ,
593586 Map .of ("runId" , "999999" , "train" , "true" ));
594- assertEquals ( false , missingRun .optBoolean ("Success" , true ));
587+ assertFalse ( missingRun .optBoolean ("Success" , true ));
595588 assertEquals ("run does not exist: 999999" , missingRun .optString ("error" ));
596589
597590 // SetUserActive: missing userId
@@ -605,12 +598,12 @@ public void testApiErrorResponses()
605598
606599 // DeleteRunAction: missing runId
607600 JSONObject noDeleteRunId = postApi ("deleteRun" , Map .of ());
608- assertEquals ( false , noDeleteRunId .optBoolean ("Success" , true ));
601+ assertFalse ( noDeleteRunId .optBoolean ("Success" , true ));
609602 assertEquals ("runId is required" , noDeleteRunId .optString ("error" ));
610603
611604 // FlagRunAction: missing runId
612605 JSONObject noFlagRunId = postApi ("flagRun" , Map .of ("flag" , "true" ));
613- assertEquals ( false , noFlagRunId .optBoolean ("Success" , true ));
606+ assertFalse ( noFlagRunId .optBoolean ("Success" , true ));
614607 assertEquals ("runId is required" , noFlagRunId .optString ("error" ));
615608 }
616609
@@ -735,7 +728,7 @@ private void assertProblemIconCount(String testName, String iconFile, int expect
735728 Locator icons = Locator .xpath (PROBLEMS_TABLE_XPATH +
736729 "//tr[.//a[text()='" + testName + "']]//img[contains(@src,'" + iconFile + "')]" );
737730 assertEquals ("Expected " + expectedCount + " " + iconFile + " icon(s) for " + testName ,
738- expectedCount , getElementCount ( icons ));
731+ expectedCount , icons . findElements ( getDriver ()). size ( ));
739732 }
740733
741734 /**
@@ -816,18 +809,18 @@ private void goToPrevDay(int count)
816809 }
817810
818811 /**
819- * Makes an API GET request and returns the value of the specified field from the JSON response.
812+ * Makes an API GET request to a testresults action and returns the value
813+ * of the specified field from the JSON response.
820814 */
821- private String getApiString (String controller , String action , int runId , String field )
815+ private String getApiString (String action , int runId , String field )
822816 {
823- String url = WebTestHelper .buildURL (controller , PROJECT_NAME , action ) + "?runId=" + runId ;
817+ String url = WebTestHelper .buildURL ("testresults" , PROJECT_NAME , action ) + "?runId=" + runId ;
824818 try (CloseableHttpClient httpClient = WebTestHelper .getHttpClient ())
825819 {
826- var request = new org . apache . hc . client5 . http . classic . methods . HttpGet (url );
820+ HttpGet request = new HttpGet (url );
827821 APITestHelper .injectCookies (request );
828822 return httpClient .execute (request , response -> {
829- String body = EntityUtils .toString (response .getEntity ());
830- org .json .JSONObject json = new org .json .JSONObject (body );
823+ JSONObject json = new JSONObject (EntityUtils .toString (response .getEntity ()));
831824 return json .optString (field , null );
832825 });
833826 }
0 commit comments