From 3ec02aec60ca7ecaac26c2edd099d1500dde4e95 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 8 Oct 2026 18:54:07 -0700 Subject: [PATCH 1/2] Retry notification menu waits across list refreshes --- .../notifications/ServerNotificationItem.java | 27 ++++++++++--- .../notifications/ServerNotificationMenu.java | 39 +++++++++++-------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java index 3077b12e21..6ff34556d8 100644 --- a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java +++ b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java @@ -15,11 +15,14 @@ */ package org.labkey.test.components.ui.notifications; +import org.apache.commons.lang3.mutable.MutableObject; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.Component; import org.labkey.test.components.WebDriverComponent; import org.labkey.test.util.TestLogger; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -191,15 +194,29 @@ public String getViewLinkText() public void clickViewLink() { String currentUrl = getDriver().getCurrentUrl().toLowerCase(); - String targetUrl = elementCache().link.getAttribute("href").toLowerCase(); - TestLogger.log(String.format("ServerNotificationItem.clickViewLink: currentUrl: %s", currentUrl)); - TestLogger.log(String.format("ServerNotificationItem.clickViewLink: targetUrl: %s", targetUrl)); - elementCache().link.click(); + // A notification refresh can re-render or briefly remove the list; retry until the link is back. + MutableObject targetUrl = new MutableObject<>(); + WebDriverWrapper.waitFor(() -> { + try + { + targetUrl.setValue(elementCache().link.getAttribute("href").toLowerCase()); + elementCache().link.click(); + return true; + } + catch (StaleElementReferenceException | NoSuchElementException retry) + { + return false; + } + }, "The 'View' link in the notification did not become clickable.", WebDriverWrapper.WAIT_FOR_JAVASCRIPT); + + + String value = targetUrl.get(); + TestLogger.log(String.format("ServerNotificationItem.clickViewLink: targetUrl: %s", value)); // If the url before clicking the link is the same as the target then don't wait for a navigation. - if(!targetUrl.equalsIgnoreCase(currentUrl)) + if(value == null || !value.equalsIgnoreCase(currentUrl)) { WebDriverWrapper.waitFor(()->!getDriver().getCurrentUrl().equalsIgnoreCase(currentUrl), "Clicking the 'View' link in the notification did not navigate in time.", 5_000); diff --git a/src/org/labkey/test/components/ui/notifications/ServerNotificationMenu.java b/src/org/labkey/test/components/ui/notifications/ServerNotificationMenu.java index fd35d3a3d5..4e0dd78a5a 100644 --- a/src/org/labkey/test/components/ui/notifications/ServerNotificationMenu.java +++ b/src/org/labkey/test/components/ui/notifications/ServerNotificationMenu.java @@ -196,20 +196,7 @@ public ImportsPage clickViewAll() */ private WebElement waitForNotificationList() { - - expand(); - - // Wait for the listing container to show up. The listing container is in the open menu, scope the search to that. - Locator notificationsContainerLocator = Locator.tagWithClass("div", "server-notifications-listing-container"); - WebDriverWrapper.waitFor(()-> notificationsContainerLocator.areAnyVisible(elementCache().menuContent()), - "List container did not render.", 500); - - // Find again (lambda requires a final reference to the component). - WebElement listContainer = notificationsContainerLocator.refindWhenNeeded(elementCache().menuContent()); - - // It may be a moment before any notifications show up. - WebDriverWrapper.waitFor(()-> Locator.tagWithClass("ul", "server-notifications-listing").areAnyVisible(listContainer), - "There are no notifications in the drop down.", 1_000); + waitForListingVisible(); // Just wait for a moment in case the list is slow to update with the most recent notification. WebDriverWrapper.sleep(500); @@ -218,10 +205,28 @@ private WebElement waitForNotificationList() // is no guarantee that it contains the most recent message. For example if an import is in progress a message // will appear in the list saying as much. When the import is done the 'in progress' message will go away and // be replaced with a new 'complete' message. + waitForListingVisible(); - // Find the container again, don't return listContainer WebElement previously found. If the list was slow to - // update with the most recent notification the old reference will be stale. - return notificationsContainerLocator.refindWhenNeeded((elementCache().menuContent())); + return LISTING_CONTAINER.refindWhenNeeded(elementCache().menuContent()); + } + + private static final Locator.XPathLocator LISTING_CONTAINER = Locator.tagWithClass("div", "server-notifications-listing-container"); + + private void waitForListingVisible() + { + // Every notification refresh swaps the listing for a spinner until the reload finishes, so re-find on each poll. + WebDriverWrapper.waitFor(() -> { + try + { + expand(); + return LISTING_CONTAINER.child(Locator.tagWithClass("ul", "server-notifications-listing")) + .areAnyVisible(elementCache().menuContent()); + } + catch (StaleElementReferenceException | NoSuchElementException retry) + { + return false; + } + }, "There are no notifications in the drop down.", WebDriverWrapper.WAIT_FOR_JAVASCRIPT); } /** From 14bb25426459f501466da8fb02c6334651d12b27 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 8 Oct 2026 22:19:49 -0700 Subject: [PATCH 2/2] Claude review --- .../notifications/ServerNotificationItem.java | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java index 6ff34556d8..f94b5380b4 100644 --- a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java +++ b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java @@ -15,7 +15,6 @@ */ package org.labkey.test.components.ui.notifications; -import org.apache.commons.lang3.mutable.MutableObject; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.Component; @@ -196,32 +195,42 @@ public void clickViewLink() String currentUrl = getDriver().getCurrentUrl().toLowerCase(); TestLogger.log(String.format("ServerNotificationItem.clickViewLink: currentUrl: %s", currentUrl)); - // A notification refresh can re-render or briefly remove the list; retry until the link is back. - MutableObject targetUrl = new MutableObject<>(); - WebDriverWrapper.waitFor(() -> { + // A notification refresh can re-render, reorder, or briefly remove the list, so pin the link by href and retry. + String href = WebDriverWrapper.waitFor(() -> { try { - targetUrl.setValue(elementCache().link.getAttribute("href").toLowerCase()); - elementCache().link.click(); - return true; + return elementCache().link.getDomAttribute("href"); } catch (StaleElementReferenceException | NoSuchElementException retry) { - return false; + return null; } - }, "The 'View' link in the notification did not become clickable.", WebDriverWrapper.WAIT_FOR_JAVASCRIPT); + }, "The notification has no 'View' link.", WebDriverWrapper.WAIT_FOR_JAVASCRIPT); + Locator.XPathLocator linkLocator = Locator.tagWithClass("ul", "server-notifications-listing") + .descendant(Locator.tagWithAttribute("a", "href", href)); + String targetUrl = WebDriverWrapper.waitFor(() -> { + try + { + WebElement link = linkLocator.findElement(getDriver()); + String url = link.getAttribute("href").toLowerCase(); + link.click(); + return url; + } + catch (StaleElementReferenceException | NoSuchElementException retry) + { + return null; + } + }, "The 'View' link in the notification did not become clickable.", WebDriverWrapper.WAIT_FOR_JAVASCRIPT); - String value = targetUrl.get(); - TestLogger.log(String.format("ServerNotificationItem.clickViewLink: targetUrl: %s", value)); + TestLogger.log(String.format("ServerNotificationItem.clickViewLink: targetUrl: %s", targetUrl)); // If the url before clicking the link is the same as the target then don't wait for a navigation. - if(value == null || !value.equalsIgnoreCase(currentUrl)) + if (!targetUrl.equalsIgnoreCase(currentUrl)) { WebDriverWrapper.waitFor(()->!getDriver().getCurrentUrl().equalsIgnoreCase(currentUrl), "Clicking the 'View' link in the notification did not navigate in time.", 5_000); } - } @Override