|
16 | 16 | package org.labkey.test.tests.ehr; |
17 | 17 |
|
18 | 18 | import org.json.JSONObject; |
19 | | -import org.junit.Assert; |
20 | 19 | import org.junit.Test; |
21 | 20 | import org.labkey.remoteapi.CommandResponse; |
22 | 21 | import org.labkey.remoteapi.SimplePostCommand; |
23 | 22 | import org.labkey.test.Locator; |
| 23 | +import org.labkey.test.Locators; |
24 | 24 | import org.labkey.test.pages.ehr.AnimalHistoryPage; |
| 25 | +import org.labkey.test.util.Crawler; |
25 | 26 | import org.labkey.test.util.DataRegionTable; |
| 27 | +import org.labkey.test.util.EscapeUtil; |
26 | 28 | import org.labkey.test.util.Ext4Helper; |
27 | 29 | import org.labkey.test.util.LoggedParam; |
28 | 30 | import org.labkey.test.util.PortalHelper; |
29 | 31 | import org.labkey.test.util.ext4cmp.Ext4ComboRef; |
30 | 32 | import org.labkey.test.util.external.labModules.LabModuleHelper; |
| 33 | +import org.labkey.test.util.selenium.WebDriverUtils; |
31 | 34 | import org.openqa.selenium.By; |
| 35 | +import org.openqa.selenium.Keys; |
| 36 | +import org.openqa.selenium.WebDriverException; |
32 | 37 | import org.openqa.selenium.WebElement; |
33 | 38 |
|
| 39 | +import java.net.MalformedURLException; |
| 40 | +import java.net.URL; |
34 | 41 | import java.util.ArrayList; |
35 | 42 | import java.util.Arrays; |
36 | 43 | import java.util.Collections; |
|
43 | 50 | import java.util.UUID; |
44 | 51 |
|
45 | 52 | import static org.junit.Assert.assertEquals; |
| 53 | +import static org.junit.Assert.assertFalse; |
| 54 | +import static org.junit.Assert.assertTrue; |
46 | 55 |
|
47 | 56 | //Inherit from this class instead of AbstractEHRTest when you want to run these tests, which should work across all ehr modules |
48 | 57 | public abstract class AbstractGenericEHRTest extends AbstractEHRTest |
@@ -221,7 +230,7 @@ public void testCustomButtons() |
221 | 230 | recallLocation(); |
222 | 231 | List<String> submenuItems = dr.getHeaderMenuOptions("More Actions"); |
223 | 232 | List<String> expectedSubmenu = Arrays.asList("Jump To History", "Return Distinct Values","Show Record History","Compare Weights","Edit Records"); |
224 | | - Assert.assertTrue("More actions menu did not contain expected options. Expected: " + expectedSubmenu + ", but found: " + submenuItems, submenuItems.containsAll(expectedSubmenu)); |
| 233 | + assertTrue("More actions menu did not contain expected options. Expected: " + expectedSubmenu + ", but found: " + submenuItems, submenuItems.containsAll(expectedSubmenu)); |
225 | 234 | } |
226 | 235 |
|
227 | 236 | private void testUserAgainstAllStates(@LoggedParam EHRUser user) |
@@ -300,6 +309,177 @@ private void testUserAgainstAllStates(@LoggedParam EHRUser user) |
300 | 309 | resetErrors(); //note: inserting records without permission will log errors by design. the UI should prevent this from happening, so we want to be aware if it does occur |
301 | 310 | } |
302 | 311 |
|
| 312 | + // Clicks the link and switches to the window if it's a viable link. Otherwise throws an exception. |
| 313 | + private void verifyLInk(WebElement link) |
| 314 | + { |
| 315 | + int winCount = getDriver().getWindowHandles().size(); |
| 316 | + link.sendKeys(Keys.chord(WebDriverUtils.MODIFIER_KEY, Keys.ENTER)); |
| 317 | + |
| 318 | + // Short wait for a new window to open. If not then throw exception |
| 319 | + boolean winOpen = waitFor(() -> getDriver().getWindowHandles().size() > winCount, 1000); |
| 320 | + if (!winOpen) |
| 321 | + throw new IllegalStateException("Link did not open new window in tab."); |
| 322 | + |
| 323 | + List<String> windows = new ArrayList<>(getDriver().getWindowHandles()); |
| 324 | + getDriver().switchTo().window(windows.get(1)); |
| 325 | + } |
| 326 | + |
| 327 | + protected List<String> skipLinksForValidation() |
| 328 | + { |
| 329 | + return List.of( |
| 330 | + "showAllErrors.view", |
| 331 | + "query-exportRowsExcel.view", |
| 332 | + "ldk-runNotification.view" // need to scope notifications to enabled modules then can remove this |
| 333 | + ); // Override if there are links to pages that are known to throw errors |
| 334 | + } |
| 335 | + |
| 336 | + protected List<String> skipLinksForCrawling() |
| 337 | + { |
| 338 | + return List.of( |
| 339 | + "project-begin.view", |
| 340 | + "query-begin.view", |
| 341 | + "query-searchPanel.view", |
| 342 | + "query-executeQuery.view", |
| 343 | + "study-manageStudy.view", |
| 344 | + "ehr-animalHistory.view", |
| 345 | + "ehr-updateQuery.view", |
| 346 | + "ehr-updateTable.view", |
| 347 | + "ehr-populateLookupData.view", |
| 348 | + "ehr-ensureQcStates.view", |
| 349 | + "ehr-ehrTemplates.view", |
| 350 | + "ehr-primeDataEntryCache.view", |
| 351 | + "ehr-cacheLivingAnimals.view", |
| 352 | + "core-modulePropertyAdmin.view", |
| 353 | + "dataintegration-begin.view", |
| 354 | + "ldk-updateQuery", |
| 355 | + "junit-begin.view", |
| 356 | + "admin-", |
| 357 | + "ehr-datasets.view" |
| 358 | + ); |
| 359 | + } |
| 360 | + |
| 361 | + private String validLink(WebElement anchor) |
| 362 | + { |
| 363 | + String href = anchor.getDomAttribute("href"); |
| 364 | + if (href != null && !href.startsWith("#") && !href.equalsIgnoreCase("undefined")) |
| 365 | + { |
| 366 | + String decodedHref = EscapeUtil.decodeUriPath(href); |
| 367 | + if (skipLinksForValidation().stream().anyMatch(s -> decodedHref.toLowerCase().contains(s.toLowerCase()))) |
| 368 | + { |
| 369 | + log(href + " is specified as an exception to link validation. Skipping validation."); |
| 370 | + return null; |
| 371 | + } |
| 372 | + |
| 373 | + // Ensure link is not external |
| 374 | + try |
| 375 | + { |
| 376 | + URL url = new URL(href); |
| 377 | + if (!url.getHost().equalsIgnoreCase(getURL().getHost())) |
| 378 | + { |
| 379 | + log(href + " is an external link. Skipping validation."); |
| 380 | + return null; |
| 381 | + } |
| 382 | + } |
| 383 | + catch (MalformedURLException e) |
| 384 | + { |
| 385 | + // not a full URL so not external. Carry on. |
| 386 | + } |
| 387 | + |
| 388 | + // scope this to admin, ehr folder and subfolders |
| 389 | + String controller = new Crawler.ControllerActionId(decodedHref).getController(); |
| 390 | + if (!decodedHref.contains(getContainerPath()) && !controller.equalsIgnoreCase("admin")) |
| 391 | + { |
| 392 | + log(href + " is in a different folder than the EHR folder, " + getContainerPath() + ". Skipping validation."); |
| 393 | + return null; |
| 394 | + } |
| 395 | + } |
| 396 | + |
| 397 | + if (!anchor.isDisplayed()) |
| 398 | + return null; |
| 399 | + |
| 400 | + boolean clickable = true; |
| 401 | + String validUrl = null; |
| 402 | + |
| 403 | + try |
| 404 | + { |
| 405 | + verifyLInk(anchor); |
| 406 | + } |
| 407 | + catch (WebDriverException | IllegalStateException e) |
| 408 | + { |
| 409 | + clickable = false; |
| 410 | + } |
| 411 | + |
| 412 | + if (clickable) |
| 413 | + { |
| 414 | + // Give page time to load |
| 415 | + boolean loaded = waitFor(() -> (getDriver().getCurrentUrl() != null && !getDriver().getCurrentUrl().equalsIgnoreCase("about:blank")), WAIT_FOR_PAGE); |
| 416 | + assertTrue("Link " + href + " did not load in " + WAIT_FOR_PAGE + "ms.", loaded); |
| 417 | + |
| 418 | + // Assert page is not empty and does not have errors |
| 419 | + URL url = getURL(); |
| 420 | + assertFalse("URL " + url + " is empty.", isPageEmpty()); |
| 421 | + assertNoLabKeyErrors(); |
| 422 | + |
| 423 | + // assertNoLabKeyErrors does not catch all types of errors |
| 424 | + assertElementNotPresent("LabKey error found for URL " + url, Locators.labkeyErrorHeading); |
| 425 | + |
| 426 | + // record link as valid and cleanup |
| 427 | + validUrl = url.toString(); |
| 428 | + getDriver().close(); |
| 429 | + switchToWindow(0); |
| 430 | + } |
| 431 | + |
| 432 | + return validUrl; |
| 433 | + } |
| 434 | + |
| 435 | + private void validatePageLinks(Set<String> crawledLinks) |
| 436 | + { |
| 437 | + log("Validating links on " + getURL()); |
| 438 | + |
| 439 | + // Find all anchors in the body content area, excluding buttons and those in data regions |
| 440 | + List<WebElement> anchors = getDriver().findElements(By.xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' lk-body-ct ')]//a[not(ancestor::form[@data-region-form]) and not(@role='button') and not(contains(@class, 'labkey-button'))]")); |
| 441 | + |
| 442 | + log(anchors.size() + " possible links found."); |
| 443 | + int validatedCount = 0; |
| 444 | + Set<String> validLinksOnPage = new HashSet<>(); |
| 445 | + for (WebElement anchor : anchors) |
| 446 | + { |
| 447 | + // Only validate links once |
| 448 | + String href = anchor.getDomAttribute("href"); |
| 449 | + if (href != null && (validLinksOnPage.contains(href) || crawledLinks.contains(href))) |
| 450 | + continue; |
| 451 | + |
| 452 | + // Validate and record valid links |
| 453 | + String validUrl = validLink(anchor); |
| 454 | + if (validUrl != null) |
| 455 | + { |
| 456 | + validatedCount++; |
| 457 | + validLinksOnPage.add(validUrl); |
| 458 | + } |
| 459 | + |
| 460 | + } |
| 461 | + log(validatedCount + " links validated."); |
| 462 | + |
| 463 | + // Recursively crawl valid links that have not yet been crawled and aren't listed as a skip. |
| 464 | + for (String s : validLinksOnPage) |
| 465 | + { |
| 466 | + if (!crawledLinks.contains(s) && skipLinksForCrawling().stream().noneMatch(link -> s.toLowerCase().contains(link.toLowerCase()))) |
| 467 | + { |
| 468 | + beginAt(s); |
| 469 | + crawledLinks.add(s); // mark page as crawled to avoid loops |
| 470 | + validatePageLinks(crawledLinks); |
| 471 | + } |
| 472 | + } |
| 473 | + } |
| 474 | + |
| 475 | + @Test |
| 476 | + public void testCrawlEhrLinks() |
| 477 | + { |
| 478 | + goToEHRFolder(); |
| 479 | + Set<String> crawledLinks = new HashSet<>(); |
| 480 | + validatePageLinks(crawledLinks); |
| 481 | + } |
| 482 | + |
303 | 483 | @Test |
304 | 484 | public void testCalculatedAgeColumns() |
305 | 485 | { |
|
0 commit comments