From 5bb98d8fcec5f88b77a4fd4dfa9308e65f695562 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Fri, 27 Feb 2026 16:05:06 -0600 Subject: [PATCH 1/7] GitHub Issue 875: Standard Assay Multi-File Transform Import Skips First Data Row (#2896) - GpatAssayTest to randomlyAddTransformScript for testing GitHub Issue 875 --- src/org/labkey/test/tests/GpatAssayTest.java | 13 ++++++++++++- src/org/labkey/test/util/TestDataGenerator.java | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/tests/GpatAssayTest.java b/src/org/labkey/test/tests/GpatAssayTest.java index 1770094f38..14b04e28cd 100644 --- a/src/org/labkey/test/tests/GpatAssayTest.java +++ b/src/org/labkey/test/tests/GpatAssayTest.java @@ -41,6 +41,7 @@ import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; +import org.labkey.test.util.RReportHelper; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.core.webdav.WebDavUploadHelper; import org.openqa.selenium.WebElement; @@ -72,11 +73,13 @@ public class GpatAssayTest extends BaseWebDriverTest private static final String ASSAY_NAME_FNA = "FASTA Assay"; private static final String ASSAY_NAME_FNA_MULTIPLE = "FASTA Assay - Multiple file upload"; private static final String ASSAY_NAME_FNA_MULTIPLE_SINGLE_INPUT = "FASTA Assay - Multiple file single input upload"; + private static final File RTRANSFORM_SCRIPT_FILE_NOOP = TestFileUtils.getSampleData("qc/noopTransform.R"); @BeforeClass public static void doSetup() { GpatAssayTest init = getCurrentTest(); + new RReportHelper(init).ensureRConfig(); init._containerHelper.createProject(init.getProjectName(), "Assay"); init.goToProjectHome(); } @@ -256,6 +259,14 @@ private void importFastaGpatAssay(File fnaFile, String assayName) clickButton("Save and Finish", defaultWaitForPage); } + // GitHub Issue #875: Optionally add transform scripts in GPAT assay design to test code path with and without transform script + private void randomlyAddTransformScript(ReactAssayDesignerPage assayDesignerPage) + { + boolean shouldAddTransformScript = TestDataGenerator.randomBoolean("whether to add transform script in assay design"); + if (shouldAddTransformScript) + assayDesignerPage.addTransformScript(RTRANSFORM_SCRIPT_FILE_NOOP); + } + @LogMethod private ReactAssayDesignerPage startCreateGpatAssay(File dataFile, @LoggedParam String assayName) { @@ -265,9 +276,9 @@ private ReactAssayDesignerPage startCreateGpatAssay(File dataFile, @LoggedParam _fileBrowserHelper.importFile(dataFile.getName(), "Create New Standard Assay Design"); ReactAssayDesignerPage assayDesignerPage = new ReactAssayDesignerPage(getDriver()); - if (assayName != null) assayDesignerPage.setName(assayName); + randomlyAddTransformScript(assayDesignerPage); return assayDesignerPage; } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 854b2d2952..bd0436eab5 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -799,7 +799,15 @@ public String randomDateString(String dateFormat, Date min, Date max) public static boolean randomBoolean() { - return ThreadLocalRandom.current().nextBoolean(); + return randomBoolean(null); + } + + public static boolean randomBoolean(@Nullable String message) + { + boolean value = ThreadLocalRandom.current().nextBoolean(); + if (message != null) + TestLogger.log("Generated random boolean value for %s: %s".formatted(message, value)); + return value; } private @NotNull List getFieldsForFile() From c7a5caeb6d3afdb28517d02e090134c4e4ba98b8 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 2 Mar 2026 11:44:21 -0800 Subject: [PATCH 2/7] Handle behavior change in `WebElement.isDisplayed` (#2895) - Centralize JavascriptExecutor customization - Upgrade Selenium to 4.41.0 --- gradle.properties | 2 +- src/org/labkey/test/WebDriverWrapper.java | 19 +---- .../components/ui/grids/ResponsiveGrid.java | 2 +- .../selenium/JavascriptExecutorWrapper.java | 85 +++++++++++++++++++ .../test/util/selenium/WebDriverUtils.java | 13 +++ .../test/util/selenium/WebElementUtils.java | 29 ++++++- 6 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java diff --git a/gradle.properties b/gradle.properties index de493ea18a..c1d3217e49 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ lookfirstSardineVersion=5.13 jettyVersion=12.1.5 -seleniumVersion=4.40.0 +seleniumVersion=4.41.0 mockserverNettyVersion=5.15.0 diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 4065e31c28..af3e2b6a67 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -63,6 +63,7 @@ import org.labkey.test.util.TextSearcher; import org.labkey.test.util.TextSearcher.TextTransformers; import org.labkey.test.util.Timer; +import org.labkey.test.util.selenium.JavascriptExecutorWrapper; import org.labkey.test.util.selenium.ScrollUtils; import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.Alert; @@ -539,11 +540,7 @@ public Object executeScript(@Language("JavaScript") String script, Object... arg */ public T executeScript(@Language("JavaScript") String script, Class expectedResultType, Object... arguments) { - Object o = executeScript(script, arguments); - if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) - Assert.fail("Script return wrong type. Expected '" + expectedResultType.getSimpleName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); - - return (T) o; + return new JavascriptExecutorWrapper(getDriver()).executeScript(script, expectedResultType, arguments); } /** @@ -552,20 +549,12 @@ public T executeScript(@Language("JavaScript") String script, Class expec */ public Object executeAsyncScript(@Language("JavaScript") String script, Object... arguments) { - script = "var callback = arguments[arguments.length - 1];\n" + // See WebDriver documentation for details on injected callback - "try {" + - script + - "} catch (error) { callback(error); }"; // ensure that the callback is invoked when an exception would otherwise prevent it - return ((JavascriptExecutor) getDriver()).executeAsyncScript(script, arguments); + return new JavascriptExecutorWrapper(getDriver()).executeAsyncScript(script, arguments); } public T executeAsyncScript(@Language("JavaScript") String script, Class expectedResultType, Object... arguments) { - Object o = executeAsyncScript(script, arguments); - if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) - Assert.fail("Script return wrong type. Expected '" + expectedResultType.getSimpleName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); - - return (T) o; + return new JavascriptExecutorWrapper(getDriver()).executeAsyncScript(script, expectedResultType, arguments); } @LogMethod(quiet = true) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 625e9d4909..f21859cec3 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -75,7 +75,7 @@ public WebElement getComponentElement() public Boolean isLoaded() { - return getComponentElement().isDisplayed() && + return WebElementUtils.checkVisibility(getComponentElement()) && !Locators.loadingGrid.existsIn(this) && !Locators.spinner.existsIn(this) && (Locator.tag("td").existsIn(this) || diff --git a/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java b/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java new file mode 100644 index 0000000000..ceb3928b8a --- /dev/null +++ b/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java @@ -0,0 +1,85 @@ +package org.labkey.test.util.selenium; + +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.ScriptKey; +import org.openqa.selenium.WebDriver; + +import java.util.Set; + +public class JavascriptExecutorWrapper implements JavascriptExecutor +{ + private final JavascriptExecutor _wrappedExecutor; + + public JavascriptExecutorWrapper(WebDriver driver) + { + _wrappedExecutor = (JavascriptExecutor) driver; + } + + @Override + public @Nullable Object executeScript(@Language("JavaScript") String script, @Nullable Object... args) + { + return _wrappedExecutor.executeScript(script, args); + } + + @Override + public @Nullable Object executeScript(ScriptKey key, @Nullable Object... args) + { + return _wrappedExecutor.executeScript(key, args); + } + + /** + * Wrapper for executing JavaScript through WebDriver and verifying return type. + * @param See {@link JavascriptExecutor#executeScript(java.lang.String, java.lang.Object...)} for valid return types + */ + public @Nullable T executeScript(@Language("JavaScript") String script, Class expectedResultType, @Nullable Object... arguments) + { + return verifyType(expectedResultType, executeScript(script, arguments)); + } + + /** + * Wrapper for synchronous execution of asynchronous JavaScript. This wrapper extracts the 'callback' from the argument list + * See {@link JavascriptExecutor#executeAsyncScript(java.lang.String, java.lang.Object...)} for details + */ + @Override + public @Nullable Object executeAsyncScript(@Language("JavaScript") String script, @Nullable Object... arguments) + { + script = "var callback = arguments[arguments.length - 1];\n" + // See WebDriver documentation for details on injected callback + "try {" + + script + + "} catch (error) { callback(error); }"; // ensure that the callback is invoked when an exception would otherwise prevent it + return _wrappedExecutor.executeAsyncScript(script, arguments); + } + + public @Nullable T executeAsyncScript(@Language("JavaScript") String script, Class expectedResultType, @Nullable Object... arguments) + { + return verifyType(expectedResultType, executeAsyncScript(script, arguments)); + } + + private @Nullable T verifyType(Class expectedResultType, @Nullable Object o) + { + if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) + throw new IllegalStateException("Script return wrong type. Expected '" + expectedResultType.getName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); + + return (T) o; + } + + @Override + public Set getPinnedScripts() + { + return _wrappedExecutor.getPinnedScripts(); + } + + @Override + public void unpin(ScriptKey key) + { + _wrappedExecutor.unpin(key); + } + + @Override + public ScriptKey pin(@Language("JavaScript") String script) + { + return _wrappedExecutor.pin(script); + } +} diff --git a/src/org/labkey/test/util/selenium/WebDriverUtils.java b/src/org/labkey/test/util/selenium/WebDriverUtils.java index eff8ec79bc..3e6b78dd41 100644 --- a/src/org/labkey/test/util/selenium/WebDriverUtils.java +++ b/src/org/labkey/test/util/selenium/WebDriverUtils.java @@ -25,6 +25,8 @@ import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.WrapsElement; +import java.util.Objects; + public abstract class WebDriverUtils { /** @@ -74,6 +76,17 @@ public static WebDriver extractWrappedDriver(Object peeling) return null; } + /** + * Extract a WebDriver instance from an arbitrarily wrapped object and the JavascriptExecutor tied to it. + * + * @param object Object that wraps a WebDriver. Typically, a Component, SearchContext, or WebElement + * @return JavascriptExecutor instance + */ + public static JavascriptExecutorWrapper getJavascriptExecutor(Object object) + { + return Objects.requireNonNull(new JavascriptExecutorWrapper(extractWrappedDriver(object)), () -> "No WebDriver found in " + object.getClass()); + } + /** * Attempts to get alert text from an {@link UnhandledAlertException}. If exception does not supply the alert text, * attempt to get it from the alert directly (requires {@link org.openqa.selenium.UnexpectedAlertBehaviour#IGNORE}). diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index d6866f73de..5f7c823561 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -1,13 +1,17 @@ package org.labkey.test.util.selenium; import org.intellij.lang.annotations.Language; +import org.labkey.test.selenium.LazyWebElement; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import static org.labkey.test.Locator.NBSP; @@ -93,6 +97,29 @@ public static String getTextNodeWithin(WebElement element) */ public static String getTextContent(WebElement element) { - return element.getDomProperty("textContent").replace(NBSP, " "); + return Optional.ofNullable(element.getDomProperty("textContent")).map(s -> s.replace(NBSP, " ")).orElse(null); + } + + /** + * Determines whether the specified element is visible. {@link WebElement#isDisplayed()} might return false if the + * element is out the viewport and scrolling is disabled due to a modal dialog.
+ * TODO: Consider moving to {@link LazyWebElement#isDisplayed()} + * + * @param element element to inspect + * @return true if the element is visible, false otherwise + */ + public static boolean checkVisibility(WebElement element) + { + try + { + return element.isDisplayed() || + Objects.requireNonNullElse(WebDriverUtils.getJavascriptExecutor(element) + .executeScript("return arguments[0].checkVisibility();", Boolean.class, element), + false); + } + catch (NoSuchElementException | StaleElementReferenceException e) + { + return false; + } } } From f4cd961b176942b51fbbd61d3badd79a3f01563c Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:11:37 -0800 Subject: [PATCH 3/7] 26.3 Fix for export tests (#2898) #### Rationale Fix for export tests, changed the " " symbol processing. #### Related Pull Requests - [https://github.com/LabKey/limsModules/pull/2017](https://github.com/LabKey/limsModules/pull/2017) --- src/org/labkey/test/components/domain/DomainFieldRow.java | 2 ++ src/org/labkey/test/util/data/TestDataUtils.java | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index dc1ea4f328..5261edb788 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -769,6 +769,8 @@ public DomainFieldRow clickRemoveOntologyConcept() public void setAllowMultipleSelections(Boolean allowMultipleSelections) { + WebDriverWrapper.waitFor(() -> elementCache().allowMultipleSelectionsCheckbox.isDisplayed(), + "Allow Multiple Selections checkbox did not become visible", 2000); elementCache().allowMultipleSelectionsCheckbox.set(allowMultipleSelections); } diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index b7ed252c1a..2795dd4916 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -581,14 +581,13 @@ public static List> readRowsFromFile(File file, CSVFormat format) t public static List parseMultiValueText(String multiValueString) throws IOException { CSVFormat format = CSVFormat.RFC4180.builder() - .setIgnoreSurroundingSpaces(true).get(); + .setIgnoreSurroundingSpaces(true).setTrim(true).get(); try (CSVParser parser = format.parse(new StringReader(multiValueString))) { List records = parser.getRecords(); - List> list = records.stream().map(CSVRecord::toList).toList(); - if (list.size() != 1) + if (records.size() != 1) throw new IllegalArgumentException("Invalid multi-value text string: " + multiValueString); - return list.getFirst(); + return records.getFirst().toList(); } } From b38a8e0e1858b7f57bf807ed9fb338aba34d98cc Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Thu, 5 Mar 2026 14:00:24 -0800 Subject: [PATCH 4/7] Expand "See Audit Log Events" test (#2905) --- .../labkey/test/tests/AbstractAssayTest.java | 5 +- src/org/labkey/test/tests/AuditLogTest.java | 54 ++++++++++++------- .../labkey/test/util/PermissionsHelper.java | 5 ++ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/org/labkey/test/tests/AbstractAssayTest.java b/src/org/labkey/test/tests/AbstractAssayTest.java index 58a7a0fcb0..9ac92692d0 100644 --- a/src/org/labkey/test/tests/AbstractAssayTest.java +++ b/src/org/labkey/test/tests/AbstractAssayTest.java @@ -36,6 +36,7 @@ import static org.labkey.test.params.FieldDefinition.DOMAIN_TRICKY_CHARACTERS; import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE; import static org.labkey.test.util.PermissionsHelper.READER_ROLE; +import static org.labkey.test.util.PermissionsHelper.SEE_AUDIT_LOG_SITE_ROLE; /** * @deprecated TODO: Move shared functionality to a Helper class @@ -209,8 +210,8 @@ protected void setupEnvironment() //add a PI user to that group permissionsHelper.addUserToProjGroup(TEST_ASSAY_USR_PI1, getProjectName(), TEST_ASSAY_GRP_PIS); - // give the PI user "CanSeeAuditLog" permission - permissionsHelper.setSiteRoleUserPermissions(TEST_ASSAY_USR_PI1, "See Audit Log Events"); + // give the PI user site "CanSeeAuditLog" permission + permissionsHelper.setSiteRoleUserPermissions(TEST_ASSAY_USR_PI1, SEE_AUDIT_LOG_SITE_ROLE); //add a lab tech user to the Users group permissionsHelper.addUserToProjGroup(TEST_ASSAY_USR_TECH1, getProjectName(), TEST_ASSAY_GRP_USERS); diff --git a/src/org/labkey/test/tests/AuditLogTest.java b/src/org/labkey/test/tests/AuditLogTest.java index 0ea179d032..00853b70ac 100644 --- a/src/org/labkey/test/tests/AuditLogTest.java +++ b/src/org/labkey/test/tests/AuditLogTest.java @@ -47,6 +47,7 @@ import org.labkey.test.util.Log4jUtils; import org.labkey.test.util.PermissionsHelper; import org.labkey.test.util.PortalHelper; +import org.labkey.test.util.SearchHelper; import org.labkey.test.util.UIUserHelper; import java.io.BufferedReader; @@ -64,11 +65,13 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.labkey.test.util.PasswordUtil.getUsername; import static org.labkey.test.util.PermissionsHelper.AUTHOR_ROLE; import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE; import static org.labkey.test.util.PermissionsHelper.FOLDER_ADMIN_ROLE; import static org.labkey.test.util.PermissionsHelper.PROJECT_ADMIN_ROLE; -import static org.labkey.test.util.PasswordUtil.getUsername; +import static org.labkey.test.util.PermissionsHelper.SEE_AUDIT_LOG_FOLDER_ROLE; +import static org.labkey.test.util.PermissionsHelper.SEE_AUDIT_LOG_SITE_ROLE; @Category({Daily.class, Hosting.class}) @BaseWebDriverTest.ClassTimeout(minutes = 9) @@ -79,21 +82,18 @@ public class AuditLogTest extends BaseWebDriverTest public static final String QUERY_UPDATE_EVENT = "Query update events"; public static final String PROJECT_AUDIT_EVENT = "Project and Folder events"; public static final String ASSAY_AUDIT_EVENT = "Link to Study events"; + public static final String COMMENT_COLUMN = "Comment"; private static final String AUDIT_TEST_USER = "audit_user1@auditlog.test"; private static final String AUDIT_TEST_USER2 = "audit_user2@auditlog.test"; private static final String AUDIT_TEST_USER3 = "audit_user3@auditlog.test"; - private static final String AUDIT_SECURITY_GROUP = "Testers"; - private static final String AUDIT_TEST_PROJECT = "AuditVerifyTest"; private static final String AUDIT_DETAILED_TEST_PROJECT = "AuditDetailedLogTest"; private static final String AUDIT_TEST_SUBFOLDER = "AuditVerifyTest_Subfolder"; private static final String AUDIT_PROPERTY_EVENTS_PROJECT = "AuditDomainPropertyEvents"; - - final String DOMAIN_PROPERTY_LOG_NAME = "Domain property events"; - - public static final String COMMENT_COLUMN = "Comment"; + private static final String DOMAIN_PROPERTY_LOG_NAME = "Domain property events"; + private static final String SEARCH_TERM = "doesn't matter"; private final ApiPermissionsHelper permissionsHelper = new ApiPermissionsHelper(this); private final AuditLogHelper _auditLogHelper = new AuditLogHelper(this); @@ -377,19 +377,37 @@ protected void canSeeAuditLogTest() createUserWithPermissions(AUDIT_TEST_USER, AUDIT_TEST_PROJECT, EDITOR_ROLE); createUserWithPermissions(AUDIT_TEST_USER2, AUDIT_TEST_PROJECT, PROJECT_ADMIN_ROLE); + // Do a search to ensure an audit entry in /home + clickProject("Home"); + new SearchHelper(this).searchFor(SEARCH_TERM); + goToProjectHome(); + // signed in as an admin so we should see rows here - verifyAuditQueries(true); + verifyAuditQueries(true, getProjectName()); // signed in as an editor should not show any rows for audit query links impersonate(AUDIT_TEST_USER); - verifyAuditQueries(false); + verifyAuditQueries(false, getProjectName()); + verifyAuditQueries(false, "Home"); + stopImpersonating(); + + // Grant the "See Audit Log Events" folder role to our audit user in the project and verify we see audit + // information in this project but not /Home. We pass the fully qualified classnames in the next few calls to + // disambiguate the root role from the folder role. + permissionsHelper.addMemberToRole(AUDIT_TEST_USER, SEE_AUDIT_LOG_FOLDER_ROLE, PermissionsHelper.MemberType.user, getProjectName()); + impersonate(AUDIT_TEST_USER); + verifyAuditQueries(true, getProjectName()); + verifyAuditQueries(false, "Home"); stopImpersonating(); + permissionsHelper.removeUserRoleAssignment(AUDIT_TEST_USER, SEE_AUDIT_LOG_FOLDER_ROLE, getProjectName()); - // now grant CanSeeAuditLog permission to our audit user and verify - // we see audit information - permissionsHelper.setSiteRoleUserPermissions(AUDIT_TEST_USER, "See Audit Log Events"); + // Grant the "See Audit Log Events" root role to our audit user and verify we see audit information in this + // project and in /Home + permissionsHelper.setSiteRoleUserPermissions(AUDIT_TEST_USER, SEE_AUDIT_LOG_SITE_ROLE); impersonate(AUDIT_TEST_USER); - verifyAuditQueries(true); + verifyAuditQueries(true, getProjectName()); + ExecuteQueryPage.beginAt(this, "Home", "auditLog", "SearchAuditEvent"); + verifyAuditQueryEvent(this, "Query", SEARCH_TERM, 1); // cleanup stopImpersonating(); @@ -482,7 +500,7 @@ public void testDetailedQueryUpdateAuditLog() throws IOException, CommandExcepti //then create model (which has detailed audit log level) InsertRowsCommand insertCmd2 = new InsertRowsCommand("vehicle", "models"); rowMap = new HashMap<>(); - rowMap.put("manufacturerId", resp1.getRows().get(0).get("rowid")); + rowMap.put("manufacturerId", resp1.getRows().getFirst().get("rowid")); rowMap.put("name", "Soul"); insertCmd2.addRow(rowMap); insertCmd2.execute(cn, AUDIT_DETAILED_TEST_PROJECT); @@ -535,17 +553,17 @@ protected void verifyListAuditLogQueries(Visibility v) verifyAuditQueryEvent(this, "List", "Child List", 1, canSeeChild(v)); } - protected void verifyAuditQueries(boolean canSeeAuditLog) + protected void verifyAuditQueries(boolean canSeeAuditLog, String containerPath) { - ExecuteQueryPage.beginAt(this, getProjectName(), "auditLog", "ContainerAuditEvent"); + ExecuteQueryPage.beginAt(this, containerPath, "auditLog", "ContainerAuditEvent"); if (canSeeAuditLog) verifyAuditQueryEvent(this, COMMENT_COLUMN, AUDIT_TEST_PROJECT + " was created", 1); else assertTextPresent("No data to show."); - ExecuteQueryPage.beginAt(this, getProjectName(), "auditLog", "GroupAuditEvent"); + ExecuteQueryPage.beginAt(this, containerPath, "auditLog", "GroupAuditEvent"); if (canSeeAuditLog) - verifyAuditQueryEvent(this, COMMENT_COLUMN, "The user " + AUDIT_TEST_USER + " was assigned to the security role Editor.", 1); + verifyAuditQueryEvent(this, COMMENT_COLUMN, "The user " + AUDIT_TEST_USER + " was assigned to the security role Editor.", 4); else assertTextPresent("No data to show."); } diff --git a/src/org/labkey/test/util/PermissionsHelper.java b/src/org/labkey/test/util/PermissionsHelper.java index cf1ba40fa0..8a4bc85741 100644 --- a/src/org/labkey/test/util/PermissionsHelper.java +++ b/src/org/labkey/test/util/PermissionsHelper.java @@ -47,6 +47,11 @@ public abstract class PermissionsHelper public static final String AUTHOR_ROLE = "Author"; public static final String SUBMITTER_ROLE = "Submitter"; + // These two roles have the same friendly name; disambiguate using their fully qualified class names + public static final String SEE_AUDIT_LOG_SITE_ROLE = "org.labkey.api.security.roles.CanSeeAuditLogRole"; + public static final String SEE_AUDIT_LOG_FOLDER_ROLE = "org.labkey.api.security.roles.CanSeeAuditLogFolderRole"; + + // TODO: Should "See Audit Log" roles be added here? public static final Set AUDIT_LOG_VIEWER_ROLES = Set.of(SITE_ADMIN_ROLE, APP_ADMIN_ROLE, PROJECT_ADMIN_ROLE, FOLDER_ADMIN_ROLE); public static boolean canSeeAuditLogs(String roleName) From 1afe6eb4f995c399afee782f89aa34464601c613 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 6 Mar 2026 14:01:01 -0800 Subject: [PATCH 5/7] Fix navigation to audit log (#2910) --- src/org/labkey/test/util/AuditLogHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/AuditLogHelper.java b/src/org/labkey/test/util/AuditLogHelper.java index 4213ac2472..74c8a45114 100644 --- a/src/org/labkey/test/util/AuditLogHelper.java +++ b/src/org/labkey/test/util/AuditLogHelper.java @@ -163,7 +163,7 @@ public DataRegionTable beginAtAuditEventView(String auditTable, Integer rowIdCut public DataRegionTable goToAuditEventView(String eventType) { - if (!_wrapper.isTextPresent("Audit Log")) + if (!StringUtils.trimToEmpty(_wrapper.getDriver().getCurrentUrl()).contains("audit-showAuditLog.view")) { ShowAdminPage.beginAt(_wrapper).clickAuditLog(); } From 93814835304dff778eade40844ff0a9fa9606cc9 Mon Sep 17 00:00:00 2001 From: Xing Yang <5168106+XingY@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:22:26 -0800 Subject: [PATCH 6/7] Making it harder to delete all rows from lists (#2904) --- src/org/labkey/test/BaseWebDriverTest.java | 10 + .../test/components/list/ManageListsGrid.java | 21 +- .../test/pages/list/ConfirmDeletePage.java | 10 +- .../test/tests/InlineImagesListTest.java | 8 +- .../labkey/test/tests/TriggerScriptTest.java | 11 +- .../test/tests/list/ListDeleteTest.java | 369 ++++++++++++++++++ 6 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 src/org/labkey/test/tests/list/ListDeleteTest.java diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index bd10151d43..10d9d5ad98 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.time.FastDateFormat; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -2734,6 +2735,15 @@ private File clickExportImageIcon(String chartParentCls, int chartIndex, Locator } } + public void verifyImagePopupInGrid(File imageFile) + { + mouseOver(Locator.xpath("//img[contains(@title, '" + imageFile.getName() + "')]")); + longWait().until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#helpDiv"))); + String src = Locator.xpath("//div[@id='helpDiv']//img[contains(@src, 'download')]").findElement(getDriver()).getAttribute("src"); + assertTrue("Wrong image in popup: " + src, src.contains(imageFile.getName())); + assertEquals("Bad response from image pop-up", HttpStatus.SC_OK, WebTestHelper.getHttpResponse(src).getResponseCode()); + } + public List> loadTsv(File tsv) { try (TabLoader loader = new TabLoader(tsv, true)) diff --git a/src/org/labkey/test/components/list/ManageListsGrid.java b/src/org/labkey/test/components/list/ManageListsGrid.java index cbf794da7c..c654aa62bb 100644 --- a/src/org/labkey/test/components/list/ManageListsGrid.java +++ b/src/org/labkey/test/components/list/ManageListsGrid.java @@ -53,11 +53,30 @@ public File exportSelectedLists() public BeginPage deleteSelectedLists() { - clickHeaderButtonAndWait("Delete"); + if (getHeaderButton("Delete").getAttribute("class").contains("labkey-down-arrow")) + clickHeaderMenu("Delete", true, "Delete List"); + else + clickHeaderButtonAndWait("Delete"); + ConfirmDeletePage confirmPage = new ConfirmDeletePage(getDriver()); return confirmPage.confirmDelete(); } + public BeginPage deleteAllDataFromSelectedLists() + { + clickHeaderMenu("Delete", true, "Delete All Data from List"); + ConfirmDeletePage confirmPage = new ConfirmDeletePage(getDriver(), "Confirm Delete All Data"); + return confirmPage.confirmDelete(); + } + + public ManageListsGrid selectLists(List listNames) + { + for (String listName : listNames) + checkCheckbox(getRowIndex("Name", listName)); + + return this; + } + public List getListNames() { return getColumnDataAsText("Name"); diff --git a/src/org/labkey/test/pages/list/ConfirmDeletePage.java b/src/org/labkey/test/pages/list/ConfirmDeletePage.java index 544b066aaf..a115d2c9bc 100644 --- a/src/org/labkey/test/pages/list/ConfirmDeletePage.java +++ b/src/org/labkey/test/pages/list/ConfirmDeletePage.java @@ -8,9 +8,17 @@ public class ConfirmDeletePage extends LabKeyPage { + private String _deleteBtnText; + public ConfirmDeletePage(WebDriver driver) + { + this(driver, "Confirm Delete"); + } + + public ConfirmDeletePage(WebDriver driver, String deleteBtnText) { super(driver); + _deleteBtnText = deleteBtnText; } public BeginPage confirmDelete() @@ -27,6 +35,6 @@ protected ElementCache newElementCache() protected class ElementCache extends LabKeyPage.ElementCache { - WebElement deleteButton = Locator.lkButton("Confirm Delete").findWhenNeeded(this); + WebElement deleteButton = Locator.lkButton(_deleteBtnText == null ? "Confirm Delete" : _deleteBtnText).findWhenNeeded(this); } } diff --git a/src/org/labkey/test/tests/InlineImagesListTest.java b/src/org/labkey/test/tests/InlineImagesListTest.java index 07c9d9ddcc..7a3ee99a4c 100644 --- a/src/org/labkey/test/tests/InlineImagesListTest.java +++ b/src/org/labkey/test/tests/InlineImagesListTest.java @@ -219,11 +219,7 @@ public final void testList() throws Exception // Mouse over the logo, migh help with the following mouse over the image. mouseOver(Locator.tagWithAttributeContaining("img", "src", "logo.image")); sleep(500); - mouseOver(Locator.xpath("//img[contains(@title, '" + LRG_PNG_FILE.getName() + "')]")); - longWait().until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#helpDiv"))); - String src = Locator.xpath("//div[@id='helpDiv']//img[contains(@src, 'download')]").findElement(getDriver()).getAttribute("src"); - assertTrue("Wrong image in popup: " + src, src.contains(LRG_PNG_FILE.getName())); - assertEquals("Bad response from image pop-up", HttpStatus.SC_OK, WebTestHelper.getHttpResponse(src).getResponseCode()); + verifyImagePopupInGrid(LRG_PNG_FILE); // Commenting out for now. There is a random behavior where sometimes the thumbnail image will not show up when you move from one cell to another. /* @@ -303,7 +299,7 @@ public final void testList() throws Exception sleep(500); mouseOver(Locator.xpath("//img[contains(@title, '" + LRG_PNG_FILE.getName() + "')]")); shortWait().until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#helpDiv"))); - src = Locator.xpath("//div[@id='helpDiv']//img[contains(@src, 'download')]").findElement(getDriver()).getAttribute("src"); + var src = Locator.xpath("//div[@id='helpDiv']//img[contains(@src, 'download')]").findElement(getDriver()).getAttribute("src"); assertTrue("Wrong image in popup: " + src, src.contains(LRG_PNG_FILE.getName())); assertEquals("Bad response from image pop-up", HttpStatus.SC_OK, WebTestHelper.getHttpResponse(src).getResponseCode()); diff --git a/src/org/labkey/test/tests/TriggerScriptTest.java b/src/org/labkey/test/tests/TriggerScriptTest.java index 41dd5ddd77..cc89fd6093 100644 --- a/src/org/labkey/test/tests/TriggerScriptTest.java +++ b/src/org/labkey/test/tests/TriggerScriptTest.java @@ -824,12 +824,11 @@ public void updateDataSetRow(int id, String tableName, Map data) */ private void cleanUpListRows() { - goToManagedList(LIST_NAME); - clickButton("Delete All Rows", 0); - waitForElement(Locator.xpath("//*[text()='Confirm Deletion']")); - clickButton("Yes", 0); - waitForText("Success"); - clickButton("OK"); + var listsPage = goToManageLists(); + var grid = listsPage.getGrid(); + grid.uncheckAllOnPage(); + grid.selectLists(List.of(LIST_NAME)); + grid.deleteAllDataFromSelectedLists(); } /** diff --git a/src/org/labkey/test/tests/list/ListDeleteTest.java b/src/org/labkey/test/tests/list/ListDeleteTest.java new file mode 100644 index 0000000000..862a0d3713 --- /dev/null +++ b/src/org/labkey/test/tests/list/ListDeleteTest.java @@ -0,0 +1,369 @@ +package org.labkey.test.tests.list; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.remoteapi.CommandException; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.Locator; +import org.labkey.test.TestFileUtils; +import org.labkey.test.categories.Daily; +import org.labkey.test.categories.Data; +import org.labkey.test.categories.Hosting; +import org.labkey.test.pages.list.BeginPage; +import org.labkey.test.util.DataRegionTable; +import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.list.IntListDefinition; +import org.labkey.test.params.list.VarListDefinition; +import org.labkey.test.util.DomainUtils; +import org.labkey.test.util.TestDataGenerator; +import org.labkey.test.util.TestUser; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE; + +@Category({Daily.class, Data.class, Hosting.class}) +@BaseWebDriverTest.ClassTimeout(minutes = 5) +public class ListDeleteTest extends BaseWebDriverTest +{ + private static final String PROJECT_NAME = "ListDeleteTest"; + private static final String PROJECT_PATH = "/" + PROJECT_NAME; + private static final String SUBFOLDER_A_NAME = "SubfolderA"; + private static final String SUBFOLDER_A_PATH = PROJECT_PATH + "/" + SUBFOLDER_A_NAME; + private static final String SUBFOLDER_B_NAME = "SubfolderB"; + private static final String SUBFOLDER_B_PATH = PROJECT_PATH + "/" + SUBFOLDER_B_NAME; + + private static final TestUser LIST_DESIGNER_USER = new TestUser("listdesigner@listdelete.test"); + + private static final String attachmentFieldName = TestDataGenerator.randomFieldName("Attachment", null, DomainUtils.DomainKind.IntList); + private static final String booleanFieldName = TestDataGenerator.randomFieldName("Boolean", null, DomainUtils.DomainKind.IntList); + private static final String integerFieldName = TestDataGenerator.randomFieldName("Integer", null, DomainUtils.DomainKind.IntList); + private static final String stringFieldName = TestDataGenerator.randomFieldName("String", null, DomainUtils.DomainKind.IntList); + + private static final String autoIncrementKeyFieldName1 = TestDataGenerator.randomFieldName("Key", null, DomainUtils.DomainKind.IntList); + + protected static final File IMG_FILE = TestFileUtils.getSampleData("InlineImages/help.jpg"); // use in Project + protected static final File PDF_FILE = TestFileUtils.getSampleData("InlineImages/agraph.pdf"); // use in Subfolder A + protected static final File TXT_FILE = TestFileUtils.getSampleData("InlineImages/test.txt"); // use in Subfolder B + + private static IntListDefinition LIST_1; // int list with attachment column + private static VarListDefinition LIST_2; // var list with attachment column, keyed by string + + @BeforeClass + public static void setupProject() throws Exception + { + ListDeleteTest init = getCurrentTest(); + init.doSetup(); + } + + private void doSetup() throws Exception + { + // Create project and subfolders + _containerHelper.createProject(getProjectName(), null); + _containerHelper.createSubfolder(getProjectName(), SUBFOLDER_A_NAME); + _containerHelper.createSubfolder(getProjectName(), SUBFOLDER_B_NAME); + + // Create user with Assay Designer + Editor permissions + LIST_DESIGNER_USER.create(this) + .setInitialPassword() + .addPermission(EDITOR_ROLE, PROJECT_PATH) + .addPermission("Assay Designer", PROJECT_PATH) + .addPermission(EDITOR_ROLE, SUBFOLDER_A_PATH) + .addPermission("Assay Designer", SUBFOLDER_A_PATH) + .addPermission(EDITOR_ROLE, SUBFOLDER_B_PATH); + + var conn = createDefaultConnection(); + + // Create list 1 with attachment column + var list1Name = DomainUtils.DomainKind.IntList.randomName("DEL1"); + LIST_1 = (IntListDefinition) new IntListDefinition(list1Name, autoIncrementKeyFieldName1) + .setFields(List.of( + new FieldDefinition(attachmentFieldName, FieldDefinition.ColumnType.Attachment), + new FieldDefinition(booleanFieldName, FieldDefinition.ColumnType.Boolean), + new FieldDefinition(integerFieldName, FieldDefinition.ColumnType.Integer), + new FieldDefinition(stringFieldName, FieldDefinition.ColumnType.String) + )); + LIST_1.getCreateCommand().execute(conn, PROJECT_PATH); + + // Create list 2 — var list with attachment column, keyed by string + var stringKeyField = new FieldDefinition(stringFieldName); + var list2Name = DomainUtils.DomainKind.VarList.randomName("DEL2"); + LIST_2 = (VarListDefinition) new VarListDefinition(list2Name) + .setKeyName(stringKeyField.getName()) + .setFields(List.of( + stringKeyField, + new FieldDefinition(attachmentFieldName, FieldDefinition.ColumnType.Attachment), + new FieldDefinition(booleanFieldName, FieldDefinition.ColumnType.Boolean), + new FieldDefinition(integerFieldName, FieldDefinition.ColumnType.Integer) + )); + LIST_2.getCreateCommand().execute(conn, PROJECT_PATH); + + // Populate data in project folder for both lists + populateList1(PROJECT_PATH, IMG_FILE); + populateList2(PROJECT_PATH, IMG_FILE); + + // Populate data in subfolder A for both lists + populateList1(SUBFOLDER_A_PATH, PDF_FILE); + populateList2(SUBFOLDER_A_PATH, PDF_FILE); + + // Populate data in subfolder B for both lists + populateList1(SUBFOLDER_B_PATH, TXT_FILE); + populateList2(SUBFOLDER_B_PATH, TXT_FILE); + } + + private void populateList1(String containerPath, File attachment) throws IOException, CommandException + { + var dataGenerator = LIST_1.getTestDataGenerator(containerPath) + .addDataSupplier(attachmentFieldName, () -> attachment); + + // Insert rows with attachment values via UI (only way to provide attachment values) + var attachmentRows = dataGenerator.withGeneratedRows(1) + .getRows(); + + _listHelper.beginAtList(containerPath, LIST_1.getName()); + var newRow = new CaseInsensitiveHashMap<>(); + newRow.putAll(attachmentRows.getFirst()); + _listHelper.insertNewRow(newRow, false); + } + + private void populateList2(String containerPath, File attachment) throws IOException, CommandException + { + var dataGenerator = LIST_2.getTestDataGenerator(containerPath) + .addDataSupplier(stringFieldName, () -> "String" + containerPath) + .addDataSupplier(attachmentFieldName, () -> attachment); + + // Insert rows without attachments via API + var attachmentRows = dataGenerator.withGeneratedRows(1) + .getRows(); + + _listHelper.beginAtList(containerPath, LIST_2.getName()); + var newRow = new CaseInsensitiveHashMap<>(); + newRow.putAll(attachmentRows.getFirst()); + _listHelper.insertNewRow(newRow, false); + } + + private void verifyConfirmationPage(String containerPath, List listNames) + { + // Navigate to manage lists page, clear all row selections, verify delete button is disabled + var listsPage = BeginPage.beginAt(this, containerPath); + var grid = listsPage.getGrid(); + grid.uncheckAllOnPage(); + var deleteButton = grid.getHeaderButton("Delete"); + checker().withScreenshot().verifyTrue("Delete button should be disabled when no rows selected", + deleteButton.getAttribute("class").contains("disabled")); + + // Select lists, verify delete menu is enabled and has 2 options + grid.selectLists(listNames); + var menuOptions = grid.getHeaderMenuOptions("Delete"); + checker().withScreenshot().verifyEquals("Expected 2 delete menu options", + List.of("Delete List", "Delete All Data from List"), menuOptions); + + // Click DELETE -> "Delete List", verify landing on confirmation page with expected text, cancel + listsPage = BeginPage.beginAt(this, containerPath); + grid = listsPage.getGrid(); + grid.selectLists(listNames); + grid.clickHeaderMenu("Delete", true, "Delete List"); + assertTextPresent("Are you sure you want to delete the following Lists?"); + for (String listName : listNames) + assertElementPresent(Locator.linkWithText(listName)); + clickButton("Cancel"); + + // Click DELETE -> "Delete All Data from List", verify landing on confirmation page with expected text, cancel + listsPage = BeginPage.beginAt(this, containerPath); + grid = listsPage.getGrid(); + grid.selectLists(listNames); + grid.clickHeaderMenu("Delete", true, "Delete All Data from List"); + assertTextPresent("Are you sure you want to delete all data"); + assertTextPresent("This action cannot be undone and will result in an empty list."); + for (String listName : listNames) + { + assertElementPresent(Locator.linkWithText(listName)); + } + assertTextPresent("1 row"); + clickButton("Cancel"); + } + + private void verifyListRowCount(String containerPath, String listName, int expectedCount) + { + _listHelper.beginAtList(containerPath, listName); + var table = new DataRegionTable("query", getDriver()); + checker().withScreenshot().verifyEquals("Expected " + expectedCount + " rows in " + listName + " at " + containerPath, + expectedCount, table.getDataRowCount()); + } + + private void verifyListDataWithAttachment(String containerPath, String listName, int expectedCount, String attachmentFileName) + { + _listHelper.beginAtList(containerPath, listName); + var table = new DataRegionTable("query", getDriver()); + checker().withScreenshot().verifyEquals("Expected " + expectedCount + " rows in " + listName + " at " + containerPath, + expectedCount, table.getDataRowCount()); + + if (attachmentFileName.contains(IMG_FILE.getName())) + { + log("Hover over the thumbnail for the image and make sure the pop-up is as expected."); + verifyImagePopupInGrid(IMG_FILE); + + } + else + { + File download = doAndWaitForDownload(() -> click(Locator.linkWithText(attachmentFileName))); + checker().withScreenshot().verifyTrue("Downloaded attachment should exist: " + attachmentFileName, download.exists()); + } + + } + + /** + * Verifies list deletion and data truncation across folders and permission levels. + * + *
    + *
  1. Verify "Delete List" and "Delete All Data from List" confirmation pages render correctly + * for single and multi-list selections across project and subfolders.
  2. + *
  3. Impersonate a non-admin designer user (Editor + Assay Designer) and verify they see a + * simple "Delete" button (no "Delete All Data" menu option) and can reach the delete + * confirmation page. Cancel without deleting.
  4. + *
  5. As admin, truncate all list data from Subfolder A. Verify both lists are empty in + * Subfolder A while data and attachments remain intact in the project and Subfolder B.
  6. + *
  7. As admin, truncate only LIST_2 data from the project. Verify LIST_2 is empty in the + * project while LIST_1 data in the project and all data in Subfolder B are unaffected.
  8. + *
  9. Impersonate the designer user again. Verify no Delete button appears in Subfolder B + * (where the user lacks Assay Designer permission). Delete LIST_1 from Subfolder A and + * LIST_2 from the project, verifying the list definitions are removed.
  10. + *
+ */ + @Test + public void testDeleteListData() throws IOException, CommandException + { + verifyConfirmationPage(getProjectName(), List.of(LIST_1.getName())); + verifyConfirmationPage(getProjectName(), List.of(LIST_1.getName(), LIST_2.getName())); + verifyConfirmationPage(SUBFOLDER_A_PATH, List.of(LIST_1.getName())); + verifyConfirmationPage(SUBFOLDER_B_PATH, List.of(LIST_1.getName(), LIST_2.getName())); + + LIST_DESIGNER_USER.impersonate(); + + // verify DESIGNER don't see the menu option to "Delete All Data from List", only "Delete" button + var listsPage = BeginPage.beginAt(this, getProjectName()); + var grid = listsPage.getGrid(); + grid.uncheckAllOnPage(); + var deleteButton = grid.getHeaderButton("Delete"); + checker().withScreenshot().verifyTrue("Delete button should be disabled when no rows selected", + deleteButton.getAttribute("class").contains("disabled")); + + // Select lists, verify delete button is enabled + grid.selectLists(List.of(LIST_1.getName(), LIST_2.getName())); + // verify DELETE button is enabled, verify click DELETE land on confirmation page, click cancel + checker().withScreenshot().verifyFalse("Delete button should be enabled when rows are selected", + deleteButton.getAttribute("class").contains("disabled")); + grid.clickHeaderButton("Delete"); + assertTextPresent("Are you sure you want to delete the following Lists?"); + for (String listName : List.of(LIST_1.getName(), LIST_2.getName())) + assertElementPresent(Locator.linkWithText(listName)); + clickButton("Cancel"); + stopImpersonating(); + + // Verify deleting data from Subfolder A doesn't impact lists or data in project folder or Subfolder B + listsPage = BeginPage.beginAt(this, SUBFOLDER_A_PATH); + grid = listsPage.getGrid(); + grid.selectLists(List.of(LIST_1.getName(), LIST_2.getName())); + grid.clickHeaderMenu("Delete", true, "Delete All Data from List"); + assertTextPresent("Are you sure you want to delete all data"); + assertTextPresent("This action cannot be undone and will result in an empty list."); + for (String listName : List.of(LIST_1.getName(), LIST_2.getName())) + { + assertElementPresent(Locator.linkWithText(listName)); + assertTextPresent("1 row"); + } + clickButton("Confirm Delete All Data"); + + // Verify data deleted in Subfolder A — both lists should be empty + verifyListRowCount(SUBFOLDER_A_PATH, LIST_1.getName(), 0); + verifyListRowCount(SUBFOLDER_A_PATH, LIST_2.getName(), 0); + + // Verify data still exists in project folder + // Go to LIST_1, verify grid is not empty, verify attachment can still be downloaded successfully + verifyListDataWithAttachment(PROJECT_PATH, LIST_1.getName(), 1, IMG_FILE.getName()); + + // Go to Subfolder B, go to LIST_2, verify data present, verify attachment can still be downloaded successfully + verifyListDataWithAttachment(SUBFOLDER_B_PATH, LIST_2.getName(), 1, TXT_FILE.getName()); + + // Now delete just LIST_2 data from project folder + listsPage = BeginPage.beginAt(this, PROJECT_PATH); + grid = listsPage.getGrid(); + grid.uncheckAllOnPage(); + grid.selectLists(List.of(LIST_2.getName())); + grid.clickHeaderMenu("Delete", true, "Delete All Data from List"); + + // Verify confirmation page + assertTextPresent("Are you sure you want to delete all data"); + assertElementPresent(Locator.linkWithText(LIST_2.getName())); + assertElementNotPresent(Locator.linkWithText(LIST_1.getName())); + assertTextPresent("1 row"); + clickButton("Confirm Delete All Data"); + + // Verify data deleted from LIST_2 in project + verifyListRowCount(PROJECT_PATH, LIST_2.getName(), 0); + + // Verify data still present in LIST_2 in Subfolder B and in LIST_1 in both folders + verifyListRowCount(SUBFOLDER_B_PATH, LIST_2.getName(), 1); + verifyListRowCount(PROJECT_PATH, LIST_1.getName(), 1); + verifyListRowCount(SUBFOLDER_B_PATH, LIST_1.getName(), 1); + + LIST_DESIGNER_USER.impersonate(); + + // From Subfolder B, verify LIST_DESIGNER_USER cannot delete LIST_1 or LIST_2 + // since they don't have designer permission in the sub folder + listsPage = BeginPage.beginAt(this, SUBFOLDER_B_PATH); + grid = listsPage.getGrid(); + checker().withScreenshot().verifyFalse("Delete button should not be present without designer permission", + grid.hasHeaderMenu("Delete")); + + // From Subfolder A, verify LIST_DESIGNER_USER can delete LIST_1 + listsPage = BeginPage.beginAt(this, SUBFOLDER_A_PATH); + grid = listsPage.getGrid(); + grid.uncheckAllOnPage(); + grid.selectLists(List.of(LIST_1.getName())); + grid.clickHeaderButtonAndWait("Delete"); + assertTextPresent("Are you sure you want to delete the following Lists?"); + assertElementPresent(Locator.linkWithText(LIST_1.getName())); + clickButton("Confirm Delete"); + + // Verify LIST_1 is deleted successfully + listsPage = BeginPage.beginAt(this, PROJECT_PATH); + grid = listsPage.getGrid(); + checker().withScreenshot().verifyFalse("LIST_1 should no longer exist", + grid.getListNames().contains(LIST_1.getName())); + checker().withScreenshot().verifyTrue("LIST_2 should still exist", + grid.getListNames().contains(LIST_2.getName())); + + // From project folder, verify LIST_DESIGNER_USER can delete LIST_2 + grid.selectLists(List.of(LIST_2.getName())); + grid.clickHeaderButtonAndWait("Delete"); + assertTextPresent("Are you sure you want to delete the following Lists?"); + assertElementPresent(Locator.linkWithText(LIST_2.getName())); + clickButton("Confirm Delete"); + + stopImpersonating(); + } + + @Override + protected void doCleanup(boolean afterTest) + { + super.doCleanup(afterTest); + _userHelper.deleteUsers(afterTest, LIST_DESIGNER_USER); + } + + @Override + protected String getProjectName() + { + return PROJECT_NAME; + } + + @Override + public List getAssociatedModules() + { + return List.of("list"); + } +} \ No newline at end of file From 6685eb90bf41daee68bb363fe5219b370546105f Mon Sep 17 00:00:00 2001 From: Karl Lum Date: Fri, 13 Mar 2026 08:35:36 -0700 Subject: [PATCH 7/7] Deprecate R report sharing (#2909) --- src/org/labkey/test/tests/ReportSharingTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/org/labkey/test/tests/ReportSharingTest.java b/src/org/labkey/test/tests/ReportSharingTest.java index 6821db03bf..0f061a30c5 100644 --- a/src/org/labkey/test/tests/ReportSharingTest.java +++ b/src/org/labkey/test/tests/ReportSharingTest.java @@ -27,6 +27,7 @@ import org.labkey.test.categories.Reports; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.OptionalFeatureHelper; import org.labkey.test.util.RReportHelper; import java.util.Arrays; @@ -50,6 +51,8 @@ public class ReportSharingTest extends BaseWebDriverTest @Override protected void doCleanup(boolean afterTest) throws TestTimeoutException { + OptionalFeatureHelper.resetOptionalFeature(createDefaultConnection(), "rReportCustomSharing"); + _containerHelper.deleteProject(getProjectName(),afterTest); _userHelper.deleteUsers(false, USER_DEV,USER_EDITOR,USER_NON_EDITOR); } @@ -63,6 +66,8 @@ public static void setupProject() private void doSetup() { + OptionalFeatureHelper.setOptionalFeature(createDefaultConnection(), "rReportCustomSharing", true); + _rReportHelper.ensureRConfig(); _containerHelper.createProject(getProjectName(), null);