From 692d85d422b7c06e98a3f05734504347069e6802 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 9 Jun 2026 13:55:09 -0700 Subject: [PATCH 1/6] Page header components --- .../test/components/ui/AppPageHeader.java | 155 ++++++++++++++++++ .../test/components/ui/PageDetailHeader.java | 107 ++++++++++++ .../test/util/selenium/WebElementUtils.java | 31 ++++ 3 files changed, 293 insertions(+) create mode 100644 src/org/labkey/test/components/ui/AppPageHeader.java create mode 100644 src/org/labkey/test/components/ui/PageDetailHeader.java diff --git a/src/org/labkey/test/components/ui/AppPageHeader.java b/src/org/labkey/test/components/ui/AppPageHeader.java new file mode 100644 index 0000000000..83a059fac9 --- /dev/null +++ b/src/org/labkey/test/components/ui/AppPageHeader.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2018-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.components.ui; + +import org.labkey.test.Locator; +import org.labkey.test.components.Component; +import org.labkey.test.components.WebDriverComponent; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import static org.labkey.test.util.selenium.WebElementUtils.tryMapElement; + +/** + * Wraps component + */ +public class AppPageHeader extends WebDriverComponent +{ + private final WebElement _el; + private final WebDriver _driver; + + protected AppPageHeader(WebElement element, WebDriver driver) + { + _el = element; + _driver = driver; + } + + @Override + public WebElement getComponentElement() + { + return _el; + } + + @Override + public WebDriver getDriver() + { + return _driver; + } + + /** + * Gets the text of the page header. If there is no header returns an empty string. + * + * @return Text from the page title, empty string if element is not there. + */ + public String getTitle() + { + return tryMapElement(elementCache().title, WebElement::getText); + } + + /** + * Get the text of the subtitle of the page. If there is no subtitle, return an empty string. + * + * @return Text from the page subtitle, empty string if element is not there. + */ + public String getSubtitle() + { + return tryMapElement(elementCache().subtitle, WebElement::getText); + } + + /** + * Get the text of the description of the page. If there is no description, returns an empty string. + * + * @return Text from the page description, empty string if element is not there. + */ + public String getDescription() + { + return tryMapElement(elementCache().description, WebElement::getText); + } + + /** + * @throws UnsupportedOperationException Label color is not supported by AppPageHeader. + */ + public String getLabelColor() + { + throw new UnsupportedOperationException("Label color is not supported by AppPageHeader."); + } + + /** + * Get the source file of the page icon. If there is no icon returns an empty string. + * + * @return The 'src' attribute header icon, empty string if element is not there. + */ + public String getIconSource() + { + return tryMapElement(elementCache().icon, el -> el.getDomAttribute("src")); + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends Component.ElementCache + { + public final WebElement icon = getIconLocator().findWhenNeeded(this); + public final WebElement title = getTitleLocator().findWhenNeeded(this); + public final WebElement subtitle = getSubtitleLocator().findWhenNeeded(this); + public final WebElement description = getDescriptionLocator().findWhenNeeded(this); + + protected Locator.XPathLocator getIconLocator() + { + return Locator.byClass("app-page-header__icon"); + } + + protected Locator.XPathLocator getTitleLocator() + { + return Locator.byClass("app-page-header__title"); + } + + protected Locator.XPathLocator getSubtitleLocator() + { + return Locator.byClass("app-page-header__subtitle"); + } + + protected Locator.XPathLocator getDescriptionLocator() + { + return Locator.byClass("app-page-header__description"); + } + } + + public static class AppPageHeaderFinder extends WebDriverComponentFinder + { + private final Locator.XPathLocator _baseLocator = Locator.byClass("app-page-header"); + + public AppPageHeaderFinder(WebDriver driver) + { + super(driver); + } + + @Override + protected AppPageHeader construct(WebElement el, WebDriver driver) + { + return new AppPageHeader(el, driver); + } + + @Override + protected Locator locator() + { + return _baseLocator; + } + } +} diff --git a/src/org/labkey/test/components/ui/PageDetailHeader.java b/src/org/labkey/test/components/ui/PageDetailHeader.java new file mode 100644 index 0000000000..8c73c4ea62 --- /dev/null +++ b/src/org/labkey/test/components/ui/PageDetailHeader.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.components.ui; + +import org.labkey.test.Locator; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import static org.labkey.test.util.selenium.WebElementUtils.tryMapElement; + +/** + * Wraps component + */ +public class PageDetailHeader extends AppPageHeader +{ + protected PageDetailHeader(WebElement element, WebDriver driver) + { + super(element, driver); + } + + /** + * Get the rgb style value for the label color in the header + * + * @return A string such as "rgb(104, 204, 202)" as used in the "color-icon__circle-small" "i" element in the detail header or the empty string if element is not there. + */ + @Override + public String getLabelColor() + { + return tryMapElement(elementCache().colorIcon, el -> el.getCssValue("background-color")); + } + + @Override + protected ElementCache elementCache() + { + return (ElementCache) super.elementCache(); + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends AppPageHeader.ElementCache + { + @Override + protected Locator.XPathLocator getTitleLocator() + { + return Locator.byClass("detail__header--name"); + } + + @Override + protected Locator.XPathLocator getDescriptionLocator() + { + return Locator.byClass("detail__header--desc"); + } + + @Override + protected Locator.XPathLocator getSubtitleLocator() + { + return Locator.byClass("detail-subtitle"); + } + + @Override + protected Locator.XPathLocator getIconLocator() + { + return Locator.byClass("detail__header-icon"); + } + + final WebElement colorIcon = Locator.byClass("color-icon__circle-small").findWhenNeeded(subtitle); + } + + public static class PageDetailHeaderFinder extends WebDriverComponentFinder + { + private final Locator.XPathLocator _baseLocator = Locator.byClass("page-header"); + + public PageDetailHeaderFinder(WebDriver driver) + { + super(driver); + } + + @Override + protected PageDetailHeader construct(WebElement el, WebDriver driver) + { + return new PageDetailHeader(el, driver); + } + + @Override + protected Locator locator() + { + return _baseLocator; + } + } +} diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index a62edd0fbd..5aee1264cb 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import static org.labkey.test.Locator.NBSP; @@ -137,4 +138,34 @@ public static boolean checkVisibility(WebElement element) return false; } } + + /** + * Convenience method to extract some attribute from a WebElement. Stale or missing elements will return a default. + * @param element WebElement to inspect; may be stale or backed by a missing DOM node + * @param mapper function applied to {@code element} to extract the desired value + * @param defaultValue value returned when {@code element} is stale or missing + * @return the mapped value, or {@code defaultValue} if the element is unavailable + * @param type of the extracted value + */ + public static T tryMapElement(WebElement element, Function mapper, T defaultValue) + { + try + { + return mapper.apply(element); + } + catch (NoSuchElementException | StaleElementReferenceException _) { } + + return defaultValue; + } + + /** + * Convenience overload of {@link #tryMapElement(WebElement, Function, Object)} that defaults to an empty string. + * @param element WebElement to inspect; may be stale or backed by a missing DOM node + * @param mapper function applied to {@code element} to extract a String value + * @return the mapped value, or {@code ""} if the element is unavailable + */ + public static String tryMapElement(WebElement element, Function mapper) + { + return tryMapElement(element, mapper, ""); + } } From fe8e58eeb9c62a20c655c39ab6f8c411aedcc06d Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 11 Jun 2026 13:32:05 -0700 Subject: [PATCH 2/6] Update how header type is determined. --- .../test/components/ui/AppPageHeader.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/org/labkey/test/components/ui/AppPageHeader.java b/src/org/labkey/test/components/ui/AppPageHeader.java index 83a059fac9..35778e4144 100644 --- a/src/org/labkey/test/components/ui/AppPageHeader.java +++ b/src/org/labkey/test/components/ui/AppPageHeader.java @@ -20,6 +20,7 @@ import org.labkey.test.components.WebDriverComponent; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; import static org.labkey.test.util.selenium.WebElementUtils.tryMapElement; @@ -49,6 +50,22 @@ public WebDriver getDriver() return _driver; } + @Override + protected void waitForReady() + { + getWrapper().quickWait().withMessage(getClass().getSimpleName() + " is not present.").until(ExpectedConditions.visibilityOf(getComponentElement())); + } + + /** + * Get the source file of the page icon. If there is no icon returns an empty string. + * + * @return The 'src' attribute header icon, empty string if element is not there. + */ + public String getIconSource() + { + return tryMapElement(elementCache().icon, el -> el.getDomAttribute("src")); + } + /** * Gets the text of the page header. If there is no header returns an empty string. * @@ -87,16 +104,6 @@ public String getLabelColor() throw new UnsupportedOperationException("Label color is not supported by AppPageHeader."); } - /** - * Get the source file of the page icon. If there is no icon returns an empty string. - * - * @return The 'src' attribute header icon, empty string if element is not there. - */ - public String getIconSource() - { - return tryMapElement(elementCache().icon, el -> el.getDomAttribute("src")); - } - @Override protected ElementCache newElementCache() { From e7a07bd435292766775f2d63fe9d2746aa374669 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 11 Jun 2026 15:17:10 -0700 Subject: [PATCH 3/6] Longer wait --- src/org/labkey/test/components/ui/AppPageHeader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/AppPageHeader.java b/src/org/labkey/test/components/ui/AppPageHeader.java index 35778e4144..2f218ff0a1 100644 --- a/src/org/labkey/test/components/ui/AppPageHeader.java +++ b/src/org/labkey/test/components/ui/AppPageHeader.java @@ -53,7 +53,7 @@ public WebDriver getDriver() @Override protected void waitForReady() { - getWrapper().quickWait().withMessage(getClass().getSimpleName() + " is not present.").until(ExpectedConditions.visibilityOf(getComponentElement())); + getWrapper().shortWait().withMessage(getClass().getSimpleName() + " is not present.").until(ExpectedConditions.visibilityOf(getComponentElement())); } /** From e3bd42741f5ffb39864357642082235f58b264bb Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 12 Jun 2026 09:53:56 -0700 Subject: [PATCH 4/6] Add children element --- src/org/labkey/test/components/ui/AppPageHeader.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/labkey/test/components/ui/AppPageHeader.java b/src/org/labkey/test/components/ui/AppPageHeader.java index 2f218ff0a1..2949732f1b 100644 --- a/src/org/labkey/test/components/ui/AppPageHeader.java +++ b/src/org/labkey/test/components/ui/AppPageHeader.java @@ -116,6 +116,7 @@ protected class ElementCache extends Component.ElementCache public final WebElement title = getTitleLocator().findWhenNeeded(this); public final WebElement subtitle = getSubtitleLocator().findWhenNeeded(this); public final WebElement description = getDescriptionLocator().findWhenNeeded(this); + public final WebElement children = Locator.byClass("app-page-header__children").findWhenNeeded(this); protected Locator.XPathLocator getIconLocator() { From 82a605567187faa6c2c8cee6748a93497ca59a9e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 25 Jun 2026 14:54:39 -0700 Subject: [PATCH 5/6] Add method to fetch menu section headers --- .../test/components/react/MultiMenu.java | 32 +++++++++++-------- .../test/util/selenium/WebElementUtils.java | 12 +++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/org/labkey/test/components/react/MultiMenu.java b/src/org/labkey/test/components/react/MultiMenu.java index 5334b0c1eb..cbc4fbc987 100644 --- a/src/org/labkey/test/components/react/MultiMenu.java +++ b/src/org/labkey/test/components/react/MultiMenu.java @@ -15,12 +15,14 @@ */ package org.labkey.test.components.react; +import org.apache.commons.lang3.StringUtils; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.WebDriverComponent; import org.labkey.test.components.html.BootstrapMenu; import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; @@ -285,8 +287,8 @@ protected List getMenuItemsUnderHeading(String heading) for (WebElement item : listItems) { - String className = item.getAttribute("class"); - String role = item.getAttribute("role"); + String className = StringUtils.trimToEmpty(item.getAttribute("class")); + String role = StringUtils.trimToEmpty(item.getAttribute("role")); String text = item.getText().trim(); if (className.contains("dropdown-header") && text.equalsIgnoreCase(heading)) @@ -329,6 +331,17 @@ public void clickMenuItemUnderHeading(String heading, String menuItem) item.click(); } + /** + * Get the text of all the menu section headers + * + * @return text from the menu section headers + */ + public List getSectionHeadersText() + { + expand(); + return WebElementUtils.getTexts(Locators.dropdownHeader.findElements(this)); + } + /** * gets the names of dropdown-section__menu-items between a toggle and the next toggle or divider * @@ -425,24 +438,17 @@ static public Locator.XPathLocator menuContainer() static public Locator.XPathLocator menuContainer(String text) { - return menuContainer().withChild(dropdownToggle().withText(text)); + return menuContainer().withChild(dropdownToggle.withText(text)); } static public Locator.XPathLocator menuContainerContains(String text) { - return menuContainer().withChild(dropdownToggle().containing(text)); + return menuContainer().withChild(dropdownToggle.containing(text)); } // finds the toggle to expand/collapse the root menu - public static Locator.XPathLocator dropdownToggle() - { - return Locator.byClass("dropdown-toggle"); - } - - public static Locator.XPathLocator dropdownHeader() - { - return Locator.tagWithClass("li", "dropdown-header"); - } + public static final Locator.XPathLocator dropdownToggle = Locator.byClass("dropdown-toggle"); + public static final Locator.XPathLocator dropdownHeader = Locator.tagWithClass("li", "dropdown-header"); // finds a menu-item public static Locator.XPathLocator menuItem() diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index 5aee1264cb..32703aa19c 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -168,4 +168,16 @@ public static String tryMapElement(WebElement element, Function getTexts(List elements) + { + return elements.stream().map(el -> tryMapElement(el, WebElement::getText, null)).toList(); + } } From 8678e2f0806da1eb2aa8f890b7aff85dc470e808 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 26 Jun 2026 17:12:25 -0700 Subject: [PATCH 6/6] Some fixes --- src/org/labkey/test/components/ui/AppPageHeader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/AppPageHeader.java b/src/org/labkey/test/components/ui/AppPageHeader.java index 2949732f1b..998dad6741 100644 --- a/src/org/labkey/test/components/ui/AppPageHeader.java +++ b/src/org/labkey/test/components/ui/AppPageHeader.java @@ -53,7 +53,8 @@ public WebDriver getDriver() @Override protected void waitForReady() { - getWrapper().shortWait().withMessage(getClass().getSimpleName() + " is not present.").until(ExpectedConditions.visibilityOf(getComponentElement())); + getWrapper().shortWait().withMessage(getClass().getSimpleName() + " is not present.") + .until(ExpectedConditions.elementToBeClickable(getComponentElement())); } /**