diff --git a/src/org/labkey/test/components/ui/search/FilterExpressionPanel.java b/src/org/labkey/test/components/ui/search/FilterExpressionPanel.java index b669ba328e..1ab0794b14 100644 --- a/src/org/labkey/test/components/ui/search/FilterExpressionPanel.java +++ b/src/org/labkey/test/components/ui/search/FilterExpressionPanel.java @@ -92,11 +92,19 @@ public void setFilter(int index, Object operator, Object value1, Object value2) } else if (value1 instanceof Date dateVal1) { - final String dateStr1 = DATE_FORMAT.format(dateVal1); + String dateStr1; + if (value1 instanceof DateString ds1) + dateStr1 = ds1.getDateStr(); // use the test-supplied date string + else + dateStr1 = DATE_FORMAT.format(dateVal1); // format the supplied date value to string elementCache().dateValues.get(index).set(dateStr1); if (value2 instanceof Date dateVal2) { - final String dateStr2 = DATE_FORMAT.format(dateVal2); + String dateStr2; + if (value2 instanceof DateString ds2) + dateStr2 = ds2.getDateStr(); + else + dateStr2 = DATE_FORMAT.format(dateVal2); elementCache().dateValuesSecond.get(index).set(dateStr2); } else if (value2 != null) @@ -176,6 +184,20 @@ protected class ElementCache extends Component.ElementCache RadioButton.RadioButton(Locator.radioButtonByNameAndValue("field-value-bool-1", "false")).refindWhenNeeded(this)); } + public static class DateString extends Date + { + private final String _dateStr; + public DateString(String dateStr) + { + _dateStr = dateStr; + } + + public String getDateStr() + { + return _dateStr; + } + } + public static class FilterExpressionPanelFinder extends WebDriverComponentFinder { private final Locator.XPathLocator _baseLocator = Locator.byClass("filter-expression__input-wrapper").parent(); diff --git a/src/org/labkey/test/tests/AdminConsoleTest.java b/src/org/labkey/test/tests/AdminConsoleTest.java index c885b39ee5..756af7106d 100644 --- a/src/org/labkey/test/tests/AdminConsoleTest.java +++ b/src/org/labkey/test/tests/AdminConsoleTest.java @@ -20,8 +20,10 @@ import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.SimpleGetCommand; +import org.labkey.remoteapi.SimplePostCommand; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; +import org.labkey.test.WebDriverWrapper; import org.labkey.test.WebTestHelper; import org.labkey.test.categories.Daily; import org.labkey.test.pages.core.admin.CustomizeSitePage; @@ -33,11 +35,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; @Category({Daily.class}) @BaseWebDriverTest.ClassTimeout(minutes = 3) @@ -151,6 +155,77 @@ public void testRibbonBar() assertElementNotPresent(ribbonLink); } + // Issue 51843 allow site banner configuration via api + @Test + public void testSiteBannerAPIConfiguration() throws Exception + { + goToAdminConsole(); + + String bannerMessage = "test banner message" + TRICKY_CHARACTERS; + Locator bannerLoc = Locator.tagWithClass("div", "lk-dismissable-warn") + .containing("test banner message" + TRICKY_CHARACTERS); + + //As site admin + // set the message and show it + var showBannerCmd = new SimplePostCommand("admin", "setRibbonMessage.api"); + showBannerCmd.setParameters(Map.of("message", bannerMessage, + "show", true)); + showBannerCmd.execute(createDefaultConnection(), "/"); + refresh(); + // verify it is shown + WebDriverWrapper.waitFor(()-> bannerLoc.isDisplayed(getDriver()), 1000); + if (checker().withScreenshot("banner not shown or not as expected") + .verifyTrue("expect banner to be shown", bannerLoc.isDisplayed(getDriver()))) + { + // hide the banner + var hideBannerCmd = new SimplePostCommand("admin", "setRibbonMessage.api"); + hideBannerCmd.setParameters(Map.of("show", false)); + hideBannerCmd.execute(createDefaultConnection(), "/"); + refresh(); + WebDriverWrapper.waitFor(()-> !bannerLoc.isDisplayed(getDriver()), 1000); + // verify it is hidden + checker().withScreenshot("banner is shown when not expected") + .verifyFalse("expect banner not to be shown", bannerLoc.isDisplayed(getDriver())); + } + + // restore it with the previous banner value + var restoreBannerCmd = new SimplePostCommand("admin", "setRibbonMessage.api"); + restoreBannerCmd.setParameters(Map.of("show", true)); + restoreBannerCmd.execute(createDefaultConnection(), "/"); + refresh(); + // verify it is restored with the previous value + checker().withScreenshot("banner not shown or not as expected") + .verifyTrue("expect banner to be shown", bannerLoc.isDisplayed(getDriver())); + + // as app admin + impersonateRole("Application Admin"); + var reHideBannerCmd = new SimplePostCommand("admin", "setRibbonMessage.api"); + reHideBannerCmd.setParameters(Map.of("show", false)); + try + { + reHideBannerCmd.execute(createDefaultConnection(), "/"); + fail("expect exception trying to call this API as app admin"); + } + catch (CommandException e) + { + // success, caller with app admin failed to hit this api + } + refresh(); + // verify it remains shown + checker().withScreenshot("banner is hidden when not expected") + .verifyTrue("expect banner to be shown", bannerLoc.isDisplayed(getDriver())); + + stopImpersonating(); + + // clean up after ourselves as site admin + var clearAndHideBannerCmd = new SimplePostCommand("admin", "setRibbonMessage.api"); + clearAndHideBannerCmd.setParameters(Map.of("show", false, "message", "")); + clearAndHideBannerCmd.execute(createDefaultConnection(), "/"); + refresh(); + checker().withScreenshot("banner is shown when not expected") + .verifyFalse("expect banner not to be shown", bannerLoc.isDisplayed(getDriver())); + } + @Test public void testAppAdminRole() { diff --git a/src/org/labkey/test/tests/MenuBarTest.java b/src/org/labkey/test/tests/MenuBarTest.java index 6c178e57c5..d5ed412701 100644 --- a/src/org/labkey/test/tests/MenuBarTest.java +++ b/src/org/labkey/test/tests/MenuBarTest.java @@ -15,12 +15,15 @@ */ package org.labkey.test.tests; +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.TestFileUtils; import org.labkey.test.TestTimeoutException; +import org.labkey.test.WebTestHelper; import org.labkey.test.categories.Daily; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.UIContainerHelper; @@ -44,6 +47,19 @@ public class MenuBarTest extends BaseWebDriverTest private static final String DEM_STUDY_FOLDER = "DemStudyFolder"; private static final String STUDY_FOLDER = "StudyFolder"; + @BeforeClass + public static void setupProject() throws Exception + { + MenuBarTest initTest = getCurrentTest(); + initTest.doSetup(); + } + + protected void doSetup() + { + log("Open new project"); + _containerHelper.createProject(PROJECT_NAME, "Collaboration"); + } + @Override public List getAssociatedModules() { @@ -67,8 +83,7 @@ public void testSteps() PortalHelper portalHelper = new PortalHelper(this); WikiHelper wikiHelper = new WikiHelper(this); - log("Open new project"); - _containerHelper.createProject(PROJECT_NAME, "Collaboration"); + goToProjectHome(); goToProjectSettings(); clickAndWait(Locator.linkWithText("Menu Bar")); @@ -211,6 +226,49 @@ public void testSteps() openMenu("Folders"); } + // Issue 52171: All menus that link to absolute URLs are broken + @Test + public void testDocumentationMenuLinks() + { + goToProjectHome(); + clickUserMenuItem(false,"LabKey Documentation"); // expect documentation to pop in another window + switchToWindow(1); + checker().withScreenshot("unexpected_destination") + .wrapAssertion(()-> Assertions.assertThat(getURL().toString()) + .as("expect navigation to labkey.org /documentation") + .contains("labkey.org/Documentation")); + switchToMainWindow(); + closeExtraWindows(); + + goToProjectHome(); + clickUserMenuItem(true,"Support"); + checker().withScreenshot("unexpected_destination") + .verifyEquals("Support menu destination", + WebTestHelper.buildURL("project", "home/support", "begin"), + getDriver().getCurrentUrl()); + + goToProjectHome(); + clickAdminMenuItem("Developer Links", "JavaScript API Reference"); + checker().withScreenshot("unexpected_destination") + .wrapAssertion(()-> Assertions.assertThat(getURL().toString()) + .as("expect navigation to labkey.org /download/clientapi_docs") + .endsWith("labkey.org/download/clientapi_docs/javascript-api/")); + + goToProjectHome(); + clickAdminMenuItem("Developer Links", "SQL Reference"); + checker().withScreenshot("unexpected_destination") + .wrapAssertion(()-> Assertions.assertThat(getURL().toString()) + .as("expect navigation to labkey.org /documentation") + .contains("labkey.org/Documentation")); + + goToProjectHome(); + clickAdminMenuItem("Developer Links", "XML Schema Reference"); + checker().withScreenshot("unexpected_destination") + .wrapAssertion(()-> Assertions.assertThat(getURL().toString()) + .as("expect navigation to labkey.org schema docs") + .endsWith("labkey.org/download/schema-docs/xml-schemas/")); + } + protected WebElement openMenu(String menuText) { WebElement menu = menuBarItem(menuText).findElement(getDriver()); diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index c0dd03695a..f96827d043 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -42,6 +42,7 @@ import org.labkey.test.components.html.OptionSelect; import org.labkey.test.pages.ImportDataPage; import org.labkey.test.pages.ReactAssayDesignerPage; +import org.labkey.test.pages.core.admin.BaseSettingsPage; import org.labkey.test.pages.experiment.CreateSampleTypePage; import org.labkey.test.pages.experiment.UpdateSampleTypePage; import org.labkey.test.params.FieldDefinition; @@ -137,6 +138,75 @@ protected void doCleanup(boolean afterTest) _userHelper.deleteUsers(false, USER_FOR_FILTERTEST.getEmail()); } + // Issue 52390: milliseconds are truncated from time fields on update or reshow + @Test + public void testDateAndTimeValueUpdates() throws Exception + { + var projectSettingsPage = goToProjectSettings(); + projectSettingsPage.setDefaultDateDisplayInherited(false); + projectSettingsPage.setDefaultDateDisplay(BaseSettingsPage.DATE_FORMAT.MMMM_dd_yyyy); + projectSettingsPage.setDefaultDateTimeDisplayInherited(false); + projectSettingsPage.setDefaultDateTimeDisplay(BaseSettingsPage.DATE_FORMAT.dd_MMM_yyyy, + BaseSettingsPage.TIME_FORMAT.HH_mm_ss_SSS); + projectSettingsPage.setDefaultTimeDisplayInherited(false); + projectSettingsPage.setDefaultTimeDisplay(BaseSettingsPage.TIME_FORMAT.HH_mm_ss_SSS); + projectSettingsPage.save(); + + final String sampleTypeName = "dateTimeEditSamples"; + final FieldDefinition txtField = new FieldDefinition( + TestDataGenerator.randomFieldName("text"), ColumnType.String).setRequired(true); + final FieldDefinition dateField = new FieldDefinition( + TestDataGenerator.randomFieldName("date", ":"), ColumnType.Date); + final FieldDefinition timeField = new FieldDefinition( + TestDataGenerator.randomFieldName("time", ":"), ColumnType.Time); + final FieldDefinition dateTimeField = new FieldDefinition( + TestDataGenerator.randomFieldName("dateTime", ":"), ColumnType.DateAndTime); + final List fields = List.of(txtField, dateField, timeField, dateTimeField); + + SampleTypeDefinition sampleTypeDefinition = new SampleTypeDefinition(sampleTypeName).setFields(fields); + sampleTypeDefinition.create(createDefaultConnection(), getProjectName()); + + goToProjectHome(); + waitAndClickAndWait(Locator.linkWithText(sampleTypeName)); + var dataRegion = DataRegionTable.DataRegion(getDriver()).withName("Material").waitFor(); + var updatePage = dataRegion.clickInsertNewRow(); + String date = "January 01 2025"; + String time = "23:56:54.123"; + String dateTime = "06-May-1986 23:58:34.123"; + updatePage.setField("Name", "sample01"); + updatePage.setField(dateField.getName(), date); + updatePage.setField(timeField.getName(), time); + updatePage.setField(dateTimeField.getName(), dateTime); + updatePage.submitExpectingError(); + + checker().wrapAssertion(()-> Assertions.assertThat(updatePage.getTextInputValue(dateField.getName())) + .isEqualTo("2025-01-01")); // expect reformat of date + checker().wrapAssertion(()-> Assertions.assertThat(updatePage.getTextInputValue(timeField.getName())) + .as("expect time to retain milliseconds") + .isEqualTo(time)); + checker().wrapAssertion(()-> Assertions.assertThat(updatePage.getTextInputValue(dateTimeField.getName())) + .as("expect dateTime to post back as entered") + .isEqualTo("1986-05-06 23:58:34.123")); + checker().screenShotIfNewError("unexpected data update"); + + // fill in the required text field and submit + updatePage.setField(txtField.getName(), "sample01"); + updatePage.submit(); + + var afterSubmit = DataRegionTable.DataRegion(getDriver()).withName("Material").waitFor(); + var rowData = afterSubmit.getRowDataAsText(0); + checker().withScreenshot("unexpected data persisted") + .wrapAssertion(()-> Assertions.assertThat(rowData) + .as("Issue 52390 expect date, time, dateTime to be shown as entered") + .contains(date, time, dateTime)); + + var cleanupDateFormatsPage = goToProjectSettings(); + cleanupDateFormatsPage.setDefaultDateDisplayInherited(true); + cleanupDateFormatsPage.setDefaultDateTimeDisplayInherited(true); + cleanupDateFormatsPage.setDefaultTimeDisplayInherited(true); + cleanupDateFormatsPage.save(); + } + @Test public void testCreateSampleTypeNoExpression() { diff --git a/src/org/labkey/test/tests/assay/AssayTransformImportUpdateTest.java b/src/org/labkey/test/tests/assay/AssayTransformImportUpdateTest.java index 0d99290cb2..91272d4508 100644 --- a/src/org/labkey/test/tests/assay/AssayTransformImportUpdateTest.java +++ b/src/org/labkey/test/tests/assay/AssayTransformImportUpdateTest.java @@ -13,6 +13,7 @@ import org.labkey.test.pages.admin.UsageStatisticsPage; import org.labkey.test.pages.assay.AssayImportPage; import org.labkey.test.pages.assay.AssayRunsPage; +import org.labkey.test.pages.pipeline.PipelineStatusDetailsPage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.assay.GeneralAssayDesign; import org.labkey.test.util.RReportHelper; @@ -219,6 +220,74 @@ public void testEnableTransformForUpdate() throws Exception assaysWithTransformScripts > 0); } + // Issue 50774 + @Test + public void testCancelAsyncAssayTransformJob() throws Exception + { + String transformCancelFile = "importCancelTransform.R"; + String importCancelTransformAssay = "importCancelTransformAssay"; + String transformContent = """ + library(Rlabkey); + + run.props = labkey.transform.readRunPropertiesFile("${runInfo}"); + + run.data.file = labkey.transform.getRunPropertyValue(run.props, "runDataFile"); + run.output.file = run.props$val3[run.props$name == "runDataFile"]; + error.file = labkey.transform.getRunPropertyValue(run.props, "errorsFile"); + + # sleep a bit before writing the table, give the test time to cancel the job before it is complete + labkey.setDebugMode(TRUE); + print("before"); + Sys.sleep(30); + print("after"); + labkey.setDebugMode(FALSE); + + if (file.exists(run.data.file)) { + run.data = read.delim(run.data.file, header=TRUE, sep="\\t", check.names = FALSE); + run.data$M2 = 111; + run.data$TransformType = "${transformOperation} testing"; + write.table(run.data, file=run.output.file, sep="\\t", na="", row.names=FALSE, quote=FALSE); + } + + """; + File transformFile = TestFileUtils.writeTempFile(transformCancelFile, transformContent); + var protocolResponse = new GeneralAssayDesign(importCancelTransformAssay) + .setDataFields(List.of(new FieldDefinition("M2", FieldDefinition.ColumnType.Decimal), + new FieldDefinition("TransformType", FieldDefinition.ColumnType.String), + new FieldDefinition("Comment", FieldDefinition.ColumnType.String)), true) + .createAssay(getProjectName(), createDefaultConnection()); + goToProjectHome(); + + var assayDesignerPage = ReactAssayDesignerPage.beginAt(this, getProjectName(), protocolResponse.getProtocolId(), + "general", getURL().toString()); + assayDesignerPage.addTransformScript(transformFile, true); + assayDesignerPage.setBackgroundImport(true); + assayDesignerPage.clickSave(); + + StringBuilder importDataBuilder = new StringBuilder("VisitID\tParticipantID\tComment\n"); + for (int i=1; i<=10; i++) + importDataBuilder.append(String.format("%d\t%d\tComment-%d\n", i, i, i)); + + clickAndWait(Locator.linkWithText(importCancelTransformAssay)); + new AssayRunsPage(getDriver()).getTable().clickHeaderButton("Import Data"); + clickButton("Next"); + var importPage = new AssayImportPage(getDriver()); + importPage.setNamedInputText("name", "cancelTransformTestImport"); + importPage.setNamedTextAreaValue("TextAreaDataCollector.textArea", importDataBuilder.toString()); + importPage.clickSaveAndFinish(); + + waitAndClickAndWait(Locator.linkWithText("Assay upload RUNNING")); + PipelineStatusDetailsPage pipelineStatusDetailsPage = new PipelineStatusDetailsPage(getDriver()); + pipelineStatusDetailsPage.clickCancel(); + + pipelineStatusDetailsPage.showLogDetails(); + pipelineStatusDetailsPage.assertLogTextContains("INFO : Attempting to cancel as requested", + "INFO : Interrupting job by sending interrupt request.", + "ERROR: The following error was generated by the assay upload", + "INFO : Failed to complete task 'org.labkey.api.assay.pipeline.AssayUploadPipelineTask'"); + resetErrors(); + } + @Override protected String getProjectName() { diff --git a/src/org/labkey/test/tests/component/GridPanelViewTest.java b/src/org/labkey/test/tests/component/GridPanelViewTest.java index c7452ef834..bb466c9bec 100644 --- a/src/org/labkey/test/tests/component/GridPanelViewTest.java +++ b/src/org/labkey/test/tests/component/GridPanelViewTest.java @@ -1,5 +1,6 @@ package org.labkey.test.tests.component; +import org.assertj.core.api.Assertions; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -28,16 +29,20 @@ import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.exp.SampleTypeAPIHelper; +import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.junit.Assert.fail; + @Category({Daily.class}) public class GridPanelViewTest extends GridPanelBaseTest { @@ -1261,6 +1266,58 @@ public void testRemoveAllFields() } + // Issue 52661 Saved grid view with an invalid date filter can't be edited or deleted + @Test + public void testWarningOnInvalidDateFilter() + { + String viewName = "broken view"; + goToProjectHome(); + + resetDefaultView(VIEW_DIALOG_ST, DEFAULT_COLUMNS); + + QueryGrid grid = beginAtQueryGrid(VIEW_DIALOG_ST); + + log("Save a view, call it 'broken view'"); + grid.saveView() + .setViewName(viewName) + .setMakeShared(false) + .saveView(); + + log("add created column so we can filter on it"); + grid.customizeView() + .selectAvailableField("Created") + .clickUpdateGrid(); + + log("filter on a valid date"); + grid.filterColumn("Created", Filter.Operator.GT, new FilterExpressionPanel.DateString("May 27, 2024")); + var filterStatuses = grid.getFilterStatusValues(); + checker().withScreenshot("Filter_Texts_Error") + .wrapAssertion(()-> Assertions.assertThat(filterStatuses) + .hasSize(1) + .filteredOn(a-> a.getText().equals("Created > 2024-05-27")) + .isNotEmpty()); + grid.saveView().saveView(); + + log("try to filter on an invalid date"); + grid.filterColumn("Created", Filter.Operator.GT, new FilterExpressionPanel.DateString("05/37/2024")); + // don't expect the parser to get the invalid date right; current behavior won't do that + // also add a bogus filter on int column, verify it refuses to do it + var err = grid.filterColumnExpectingError("Int", Filter.Operator.GT, "XYZ"); + checker().verifyEquals("expect error when trying to configure invalid filter", + "Missing filter values for: Int.", err); + grid.saveView().saveView(); + + log("ensure the view can be edited after"); + grid.filterColumn("Created", Filter.Operator.LTE, new Date()); // filter on right now + grid.saveView().saveView(); + + grid.manageViews() // ensure the view can be deleted now + .revertDefaultView() + .deleteView(viewName) + .confirmDelete() + .dismiss("Done"); + } + /** * Helper to validate the 'Views' menu. * diff --git a/src/org/labkey/test/tests/query/QueryLookupTest.java b/src/org/labkey/test/tests/query/QueryLookupTest.java new file mode 100644 index 0000000000..980acf70c8 --- /dev/null +++ b/src/org/labkey/test/tests/query/QueryLookupTest.java @@ -0,0 +1,149 @@ +package org.labkey.test.tests.query; + +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.Locator; +import org.labkey.test.categories.Daily; +import org.labkey.test.pages.list.EditListDefinitionPage; +import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.FieldInfo; +import org.labkey.test.params.list.IntListDefinition; + +import org.labkey.test.params.list.VarListDefinition; +import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.EscapeUtil; +import org.labkey.test.util.data.TestDataUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + + +@Category({Daily.class}) +public class QueryLookupTest extends BaseWebDriverTest +{ + private static final String PROJECT_NAME = "QueryLookupTest" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES; + private static final String LIST_NAME = "l&ist q"; + + private static final FieldInfo NAME_COLUMN = + new FieldInfo("Name", FieldDefinition.ColumnType.String); + private static final FieldInfo TSHIRT_COLUMN = + new FieldInfo("TShirt", FieldDefinition.ColumnType.String); + + @Override + protected void doCleanup(boolean afterTest) + { + _containerHelper.deleteProject(getProjectName(), afterTest); + } + + @BeforeClass + public static void setupProject() throws Exception + { + QueryLookupTest init = getCurrentTest(); + + init.doSetup(); + } + + private void doSetup() throws Exception + { + _containerHelper.createProject(PROJECT_NAME, null); + + // create a list + var dgen = new VarListDefinition(LIST_NAME) + .setFields(List.of(NAME_COLUMN.getFieldDefinition(), TSHIRT_COLUMN.getFieldDefinition())) + .create(createDefaultConnection(), PROJECT_NAME); + dgen.withGeneratedRows(10) + .insertRows(); + } + + // Issue 49511 Setting lookup to custom query shows "Error: Lookup target table does not exist." + @Test + public void testLookupToQueryColumn() throws Exception + { + var insertedRows = executeSelectRowCommand("lists", LIST_NAME).getRows(); + var itemNames = insertedRows.stream().map(a-> a.get("name").toString()).toList(); + String secondList = "secondList"; + + // create a query from LIST_NAME list, with a key defined in the query xml + String queryName = "query from list"; + String querySql = """ + SELECT [list_name].Name, + [list_name].TShirt + FROM [list_name] + """.replace("[list_name]", EscapeUtil.getSqlQuotedValue(LIST_NAME)); + String queryXml = """ + + + + + true + + +
+
+ """.replace("[query_name]", EscapeUtil.getMarkupEscapedValue(queryName)) + .replace("[list_key]", EscapeUtil.getMarkupEscapedValue(NAME_COLUMN.getName())); + // create a query on the list + goToSchemaBrowser(); + createQuery(getProjectName(), queryName, "lists", querySql, queryXml, false); + + // now create another list, with a lookup to the custom query + new IntListDefinition(secondList, "Key") + .setFields(List.of(NAME_COLUMN.getFieldDefinition(), + new FieldDefinition("lookup", new FieldDefinition.StringLookup(getProjectName(), "lists", queryName)))) + .create(createDefaultConnection(), PROJECT_NAME); + + // insert data into the list + goToManageLists(); + waitAndClickAndWait(Locator.linkWithText(secondList)); + var importPage = DataRegionTable.DataRegion(getDriver()).withName("query").waitFor() + .clickImportBulkData(); + List> importData = new ArrayList<>(); + int textIndex = 0; + for (String lookupValue : itemNames) + { + String nameVal = String.format("text-%d", textIndex); + importData.add(Map.of("Name", nameVal, "lookup", lookupValue)); + textIndex++; + } + importPage.setText(TestDataUtils.tsvStringFromRowMaps(importData, List.of("Name", "lookup"), true)); + importPage.submit(); + + // verify the query-list-items resolve here + var secondListDataRegion = DataRegionTable.DataRegion(getDriver()).withName("query").waitFor(); + var resolvedLookupsInSecondList = secondListDataRegion.getColumnDataAsText("lookup"); + checker().withScreenshot("query lookup values not resolved") + .wrapAssertion(()-> Assertions.assertThat(resolvedLookupsInSecondList) + .as("the expected sample items were not resolved via query lookup") + .containsAll(itemNames)); + + // navigate to the secondlist edit page and verify the details on the lookup field row are correct + secondListDataRegion.clickHeaderButtonAndWait("Design"); + var listEditPage = new EditListDefinitionPage(getDriver()); + var lookupFieldRow = listEditPage.getFieldsPanel().getField("Lookup"); + checker().withScreenshot("unexpected lookup details") + .wrapAssertion(()-> Assertions.assertThat(lookupFieldRow.detailsMessage()) + .as("query lookup table not resolved") + .isEqualTo(String.format("/%s > lists > %s", PROJECT_NAME, queryName))); + // click the link in the details message, expect to navigate to a view of the query + clickAndWait(Locator.linkWithText(queryName).findElement(lookupFieldRow)); + assertTextPresent("lists Schema", queryName, PROJECT_NAME); + + } + + @Override + protected String getProjectName() + { + return PROJECT_NAME; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList(); + } +} diff --git a/src/org/labkey/test/tests/wiki/WikiTest.java b/src/org/labkey/test/tests/wiki/WikiTest.java index 7dc01635fd..b095259c5e 100644 --- a/src/org/labkey/test/tests/wiki/WikiTest.java +++ b/src/org/labkey/test/tests/wiki/WikiTest.java @@ -28,6 +28,7 @@ import org.labkey.test.Locator; import org.labkey.test.categories.Daily; import org.labkey.test.categories.Wiki; +import org.labkey.test.components.WebPartPanel; import org.labkey.test.pages.admin.ExternalSourcesPage; import org.labkey.test.pages.admin.ExternalSourcesPage.Directive; import org.labkey.test.pages.search.SearchResultsPage; @@ -38,6 +39,7 @@ import org.labkey.test.util.search.SearchAdminAPIHelper; import java.io.File; +import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -46,6 +48,8 @@ public class WikiTest extends BaseWebDriverTest { private static final String PROJECT_NAME = TRICKY_CHARACTERS_FOR_PROJECT_NAMES + "WikiVerifyProject"; + private static final String SUBFOLDER_NAME = TRICKY_CHARACTERS_FOR_PROJECT_NAMES + "WikiVerifySubfolder"; + private static final String SUBFOLDER_PATH = String.format("%s/%s", PROJECT_NAME, SUBFOLDER_NAME); private static final String WIKI_PAGE_ALTTITLE = "PageBBB has HTML"; private static final String WIKI_PAGE_WEBPART_ID = "qwp999"; private static final String WIKI_PAGE_TITLE = "_Test Wiki " + BaseWebDriverTest.INJECT_CHARS_1; @@ -68,6 +72,8 @@ private void doSetup() { _containerHelper.createProject(PROJECT_NAME, null); _containerHelper.enableModules(Arrays.asList("Wiki")); + _containerHelper.createSubfolder(PROJECT_NAME, SUBFOLDER_NAME); + _containerHelper.enableModules(Arrays.asList("Wiki")); SearchAdminAPIHelper.pauseCrawler(getDriver()); @@ -344,7 +350,65 @@ public void testUpdateWikiWithHostileNameAndTitle() throws Exception .as("expect title field to be the source of the error") .isEqualTo("title")); } -} + } + + // Issue 52729 Wiki webpart doesn't resolve after wiki rename with alias + @Test + public void testRenameWebPartWiki() throws Exception + { + var newLine = '\u0081'; + var stringTerminator = '\u009c'; + String wikiContent = "

This is my content " + stringTerminator + TRICKY_CHARACTERS + newLine + "

"; + String wikiName = "webPartWiki"; + String wikiTitle = "wikiRenameWebPart"; + var cn = createDefaultConnection(); + + // first, create a straightforward wiki + var createCmd = new SimplePostCommand("wiki", "saveWiki"); + JSONObject createJson = new JSONObject(); + createJson.put("name", wikiName); + createJson.put("title", wikiTitle); + createJson.put("rendererType", "HTML"); + createJson.put("body", "

content for wiki webpart rename

"); + createJson.put("pageVersionId", -1); + createCmd.setJsonObject(createJson); + createCmd.execute(cn, SUBFOLDER_PATH); + SearchAdminAPIHelper.waitForIndexer(); + + // give the folder a wikiWebPart + goToProjectFolder(PROJECT_NAME, SUBFOLDER_NAME); + var wikiHelper = new WikiHelper(this); + new PortalHelper(this).addWebPart("Wiki"); + Locator wikiWebPartLoc = Locator.tagWithClass("div", "panel-portal") + .withDescendant(Locator.tagWithAttribute("h3", "title", wikiTitle)) + .descendant(Locator.tagWithText("p", "content for wiki webpart rename")); + + // configure the webPart to use the wiki created above + waitAndClickAndWait(Locator.linkWithText("Choose an existing page to display")); + var selectedPageOption = getSelectedOptionText(Locator.name("name")); + checker().withScreenshot("unexpected_selected_page") + .wrapAssertion(()-> Assertions.assertThat(selectedPageOption) + .as("expect our wiki to be selected") + .startsWith(wikiName)); + waitAndClickAndWait(Locator.id("btnSubmit")); + + // verify the webpart's content is our expected content + checker().withScreenshot("unexpected_wiki_content") + .awaiting(Duration.ofSeconds(1), ()-> Assertions.assertThat(wikiWebPartLoc.existsIn(getDriver())) + .as("expect our wiki content to be present") + .isTrue()); + + // Now edit the wiki, give it a new name, with an alias + var wikiConfigPage = wikiHelper.manageWikiConfiguration(); + wikiConfigPage.rename("webPartNewWikiName", true) + .save(); + + // verify the expected content is still present + checker().withScreenshot("unexpected_wiki_content_after_rename") + .awaiting(Duration.ofSeconds(1), ()-> Assertions.assertThat(wikiWebPartLoc.existsIn(getDriver())) + .as("expect our wiki content to be present") + .isTrue()); + } protected void verifyWikiPagePresent() {