From 20053a75907b7daabc46d3b56339f8bb9ac1aa8e Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Tue, 2 Jun 2026 13:56:55 -0700 Subject: [PATCH 1/4] Fix intermittent R report timeouts (#3027) --- src/org/labkey/test/pages/reports/ScriptReportPage.java | 2 +- src/org/labkey/test/tests/AbstractKnitrReportTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/pages/reports/ScriptReportPage.java b/src/org/labkey/test/pages/reports/ScriptReportPage.java index a037747028..10679cd9ef 100644 --- a/src/org/labkey/test/pages/reports/ScriptReportPage.java +++ b/src/org/labkey/test/pages/reports/ScriptReportPage.java @@ -214,7 +214,7 @@ private void _clickReportTab() scrollToTop(); // Clicking report tab can scroll such that the cursor hovers over and opens the project menu waitAndClick(Ext4Helper.Locators.tab("Report")); // Report view should appear quickly - shortWait().until(ExpectedConditions.visibilityOfElementLocated(Locator.tagWithClass("div", "reportView"))); + longWait().until(ExpectedConditions.visibilityOfElementLocated(Locator.tagWithClass("div", "reportView"))); // Actual report might take a while to load _ext4Helper.waitForMaskToDisappear(BaseWebDriverTest.WAIT_FOR_PAGE); } diff --git a/src/org/labkey/test/tests/AbstractKnitrReportTest.java b/src/org/labkey/test/tests/AbstractKnitrReportTest.java index 518707fd5f..6a0d46acc5 100644 --- a/src/org/labkey/test/tests/AbstractKnitrReportTest.java +++ b/src/org/labkey/test/tests/AbstractKnitrReportTest.java @@ -234,7 +234,7 @@ protected void moduleReportDependencies() clickProject(getProjectName()); _ext4Helper.waitForMaskToDisappear(); waitAndClickAndWait(Locator.linkWithText("kable")); - _ext4Helper.waitForMaskToDisappear(3 * BaseWebDriverTest.WAIT_FOR_JAVASCRIPT); + _ext4Helper.waitForMaskToDisappear(60_000); waitForElement(Locator.id("mtcars_table")); } From fbf47ff0bc079c21a65396c25c41797963f11c23 Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Thu, 4 Jun 2026 08:24:38 -0700 Subject: [PATCH 2/4] 26.3 fb increase time out (#3032) --- src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java index ea2f3b964c..a9c7daacd2 100644 --- a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java +++ b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java @@ -103,7 +103,7 @@ public void testUpload200kRows() throws Exception // wait for import complete var assayJobsPage1 = new AssayUploadJobsPage(getDriver()); - var pipelineDetailsPage1 = assayJobsPage1.clickJobStatus("200k", 3 * WebDriverWrapper.WAIT_FOR_PAGE); + var pipelineDetailsPage1 = assayJobsPage1.clickJobStatus("200k", 6 * WebDriverWrapper.WAIT_FOR_PAGE); pipelineDetailsPage1.waitForComplete(12 * WebDriverWrapper.WAIT_FOR_PAGE); // export assay1 data to excel From 06216048baa12118005a68a8d568a4fa63439774 Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Thu, 4 Jun 2026 17:46:38 -0700 Subject: [PATCH 3/4] Change locator to look for a span and not an icon. (#3033) --- src/org/labkey/test/pages/ReactAssayDesignerPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/pages/ReactAssayDesignerPage.java b/src/org/labkey/test/pages/ReactAssayDesignerPage.java index 824ed87e27..ca79c67760 100644 --- a/src/org/labkey/test/pages/ReactAssayDesignerPage.java +++ b/src/org/labkey/test/pages/ReactAssayDesignerPage.java @@ -298,7 +298,7 @@ private ReactAssayDesignerPage setTransformScript(File transformScript, boolean { getWrapper().waitFor(()-> Locator.tagWithClass("div", "alert-danger").withText(expectedError).isDisplayed(this), "Transform script expected error not found", WAIT_FOR_JAVASCRIPT); - getWrapper().click(Locator.tagWithClass("i", "container--removal-icon")); + getWrapper().click(Locator.tagWithClass("span", "container--removal-icon")); } return this; From 0e88cbb151af61e53780dd7ca19745e8f4063b25 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:23:17 -0700 Subject: [PATCH 4/4] Calculated Column Expression Assistant - Test Automation (#3028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Rationale [Calculated Column Expression Assistant - Test Automation](https://github.com/LabKey/kanban/issues/1847) #### Related Pull Requests - https://github.com/LabKey/premiumModules/pull/589 - https://github.com/LabKey/limsModules/pull/2233 #### Changes - created CalculatedColumnAssistantDialog component --------- Co-authored-by: Trey Chadick --- .../CalculatedColumnAssistantDialog.java | 245 ++++++++++++++++++ .../components/domain/DomainFieldRow.java | 23 ++ 2 files changed, 268 insertions(+) create mode 100644 src/org/labkey/test/components/domain/CalculatedColumnAssistantDialog.java diff --git a/src/org/labkey/test/components/domain/CalculatedColumnAssistantDialog.java b/src/org/labkey/test/components/domain/CalculatedColumnAssistantDialog.java new file mode 100644 index 0000000000..cc8a6c1ab6 --- /dev/null +++ b/src/org/labkey/test/components/domain/CalculatedColumnAssistantDialog.java @@ -0,0 +1,245 @@ +package org.labkey.test.components.domain; + +import org.labkey.test.Locator; +import org.labkey.test.WebDriverWrapper; +import org.labkey.test.components.bootstrap.ModalDialog; +import org.openqa.selenium.WebElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Modal that opens when the user clicks the "AI Assistant" button inside the Calculation field options. + */ +public class CalculatedColumnAssistantDialog extends ModalDialog +{ + public static final String TITLE = "Expression AI Assistant"; + + private final DomainFieldRow _row; + + public CalculatedColumnAssistantDialog(DomainFieldRow row, ModalDialogFinder finder) + { + super(finder); + _row = row; + } + + public CalculatedColumnAssistantDialog(DomainFieldRow row) + { + this(row, new ModalDialogFinder(row.getDriver()).withTitle(TITLE)); + } + + /** + * Type the prompt into the textarea. The submit button stays disabled until non-empty text is present. + */ + public CalculatedColumnAssistantDialog setPrompt(String prompt) + { + getWrapper().setFormElement(elementCache().promptInput, prompt); + WebDriverWrapper.waitFor(() -> elementCache().promptSubmitButton.isEnabled(), + "Prompt submit button did not become enabled.", 2_000); + return this; + } + + public String getPrompt() + { + return getWrapper().getFormElement(elementCache().promptInput); + } + + /** + * Click the submit (arrow) button. First waits for the "Thinking..." spinner to disappear (up to 60s) + * and then for a new assistant response to render (up to 10s). + */ + public CalculatedColumnAssistantDialog submitPrompt() + { + int previousCount = getAssistantResponses().size(); + elementCache().promptSubmitButton.click(); + waitForThinkingSpinnerToDisappear(); + WebDriverWrapper.waitFor(() -> getAssistantResponses().size() > previousCount, + "No new assistant response appeared in chat history.", 10_000); + return this; + } + + private void waitForThinkingSpinnerToDisappear() + { + WebDriverWrapper.waitFor(() -> !Locators.thinkingSpinner.existsIn(this), 60_000); + } + + /** + * Convenience: type the prompt and submit it. + */ + public CalculatedColumnAssistantDialog sendPrompt(String prompt) + { + return setPrompt(prompt).submitPrompt(); + } + + /** + * @return one entry per assistant response bubble (concatenated text of all its {@code .assistant-text} blocks), + * in chat order. Suggested-expression SQL is not included here — see {@link #getSuggestedExpressions()}. + */ + public List getAssistantResponses() + { + return Locators.assistantResponse.findElements(this).stream() + .map(WebElement::getText) + .collect(Collectors.toList()); + } + + /** + * @return text of the most recent assistant response, or empty string if there are none. + */ + public String getLastAssistantResponse() + { + List responses = getAssistantResponses(); + return responses.isEmpty() ? "" : responses.get(responses.size() - 1); + } + + /** + * @return every applicable SQL expression suggested in the most recent assistant response, in display + * order. Only counts {@code .assistant-expression} blocks that include an "Apply Expression" button — read-only + * SQL the assistant shows for illustration (e.g. an alternative custom-query example) is excluded, since the user + * can't accept it as the field's calculation. + */ + public List getSuggestedExpressions() + { + WebElement lastResponse = lastAssistantResponseElement(); + if (lastResponse == null) + return List.of(); + return Locators.applicableSqlCode.findElements(lastResponse).stream() + .map(WebElement::getText) + .collect(Collectors.toList()); + } + + /** + * @return the first SQL expression in the most recent assistant response, or empty string if none. + */ + public String getFirstSuggestedExpression() + { + List expressions = getSuggestedExpressions(); + return expressions.isEmpty() ? "" : expressions.get(0); + } + + /** + * Click "Apply Expression" on the first suggestion in the most recent assistant response. + * Returns the underlying field row (the dialog stays open; call {@link #clickEndChat()} to close it). + */ + public DomainFieldRow applyFirstSuggestedExpression() + { + return applySuggestedExpression(0); + } + + /** + * Click "Apply Expression" on the suggestion at the given index in the most recent assistant response. Waits + * up to 5 seconds for at least one applicable expression to render — the spinner disappears as soon as the + * bubble exists, but the inner {@code assistant-expression} block sometimes finishes rendering a moment later. + */ + public DomainFieldRow applySuggestedExpression(int index) + { + List buttons = new ArrayList<>(); + WebDriverWrapper.waitFor(() -> { + buttons.clear(); + WebElement last = lastAssistantResponseElement(); + if (last != null) + buttons.addAll(Locators.applyButton.findElements(last)); + return !buttons.isEmpty(); + }, + "No applicable expression rendered in the assistant response.", + 5_000); + + if (index >= buttons.size()) + throw new IndexOutOfBoundsException( + "Requested expression index " + index + " but only " + buttons.size() + " expression(s) available."); + buttons.get(index).click(); + return _row; + } + + /** + * @return text of the first assistant response in the chat history, or empty string if there are none. Useful + * for asserting the intro message in NEW / CHANGE / VALIDATE entry modes. + */ + public String getFirstAssistantResponse() + { + List responses = getAssistantResponses(); + return responses.isEmpty() ? "" : responses.get(0); + } + + /** + * @return true while the dialog is waiting for an AI response (the "Thinking..." pending bubble is shown). + */ + public boolean isPending() + { + return Locators.pendingBubble.existsIn(this); + } + + /** + * Click the stop button to abort an in-flight AI request. The submit button toggles to a stop button (fa-stop) + * while the dialog is in the pending state; calling this method when no request is pending will fail. + */ + public void clickStop() + { + Locators.stopButton.findElement(this).click(); + } + + /** + * Click submit without waiting for the response. Useful for tests that need to interrupt or otherwise observe + * the pending state before the response arrives. Prefer {@link #submitPrompt()} when the caller wants to wait. + */ + public void clickSubmitWithoutWaiting() + { + elementCache().promptSubmitButton.click(); + } + + private WebElement lastAssistantResponseElement() + { + List responses = Locators.assistantResponse.findElements(this); + return responses.isEmpty() ? null : responses.get(responses.size() - 1); + } + + /** + * Click "End Chat" to close the dialog. + */ + public DomainFieldRow clickEndChat() + { + elementCache().endChatButton.click(); + waitForClose(); + return _row; + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + @Override + protected ElementCache elementCache() + { + return (ElementCache) super.elementCache(); + } + + public static class Locators + { + public static final Locator.XPathLocator assistantResponse = Locator.tagWithClass("div", "chat-item").withClass("assistant-response"); + + public static final Locator.XPathLocator pendingBubble = Locator.tagWithClass("div", "chat-item").withClass("pending"); + + public static final Locator.XPathLocator thinkingSpinner = Locator.tagWithClass("i", "fa-spinner"); + + public static final Locator.XPathLocator applyButton = Locator.tagWithClass("div", "assistant-expression") + .descendant(Locator.tagWithClass("button", "clickable-text")); + + public static final Locator.XPathLocator applicableSqlCode = Locator.tagWithClass("div", "assistant-expression") + .withDescendant(Locator.tagWithClass("button", "clickable-text")) + .descendant(Locator.tag("code")); + + public static final Locator.XPathLocator stopButton = Locator.tagWithClass("button", "prompt-button") + .withDescendant(Locator.tagWithClass("i", "fa-stop")); + } + + protected class ElementCache extends ModalDialog.ElementCache + { + final WebElement endChatButton = Locator.tagWithClass("button", "btn").withText("End Chat").findWhenNeeded(this); + + final WebElement promptInput = Locator.tagWithClass("textarea", "prompt-input").findWhenNeeded(this); + + final WebElement promptSubmitButton = Locator.tagWithClass("button", "prompt-button").refindWhenNeeded(this); + } +} diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index bf3487677a..be6ed3774d 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -1085,6 +1085,28 @@ public String getValueExpression() return getWrapper().getFormElement(elementCache().expressionInput); } + /** + * Click the "AI Assistant" button in the expanded Calculation field options and return the resulting dialog. + */ + public CalculatedColumnAssistantDialog openAIAssistant() + { + expand(); + elementCache().aiAssistantButton.click(); + return new CalculatedColumnAssistantDialog(this); + } + + /** + * @return true if the "AI Assistant" button is present in the expanded Calculation field options. + * The button is only available when the {@code professional} module is enabled. + */ + public boolean hasAIAssistantButton() + { + expand(); + return Locator.tagWithClass("button", "btn") + .withText("AI Assistant") + .findElementOrNull(this) != null; + } + // advanced settings public DomainFieldRow showFieldOnDefaultView(boolean checked) @@ -1778,6 +1800,7 @@ protected class ElementCache extends WebDriverComponent.ElementCache public final WebElement expressionStatusError = expressionStatusMsgLoc.descendant(Locator.tagWithClass("span", "error")).refindWhenNeeded(this); public final WebElement expressionStatusMsg = expressionStatusMsgLoc.childTag("div").refindWhenNeeded(this); public final WebElement expressionValidateLink = expressionStatusMsgLoc.child(Locator.tagWithClass("div", "validate-link")).refindWhenNeeded(this); + public final WebElement aiAssistantButton = Locator.tagWithClass("button", "btn").withText("AI Assistant").refindWhenNeeded(this); Locator.XPathLocator aliquotWarningAlert = Locator.tagWithClassContaining("div", "aliquot-alert-warning");