diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index da59312..66e749d 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -30,7 +30,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip"; import Typography from "@mui/material/Typography"; import parse from "html-react-parser"; import React, {useEffect, useState} from "react"; -import {Link, NavigateFunction, useNavigate} from "react-router-dom"; +import {NavigateFunction, useNavigate} from "react-router-dom"; import colors from "qqq/components/legacy/colors"; import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu"; @@ -46,6 +46,7 @@ export interface WidgetData dropdownNeedsSelectedText?: string; hasPermission?: boolean; errorLoading?: boolean; + [other: string]: any; } @@ -53,6 +54,7 @@ export interface WidgetData interface Props { labelAdditionalComponentsLeft: LabelComponent[]; + labelAdditionalElementsLeft: JSX.Element[]; labelAdditionalComponentsRight: LabelComponent[]; widgetMetaData?: QWidgetMetaData; widgetData?: WidgetData; @@ -70,6 +72,7 @@ Widget.defaultProps = { widgetMetaData: {}, widgetData: {}, labelAdditionalComponentsLeft: [], + labelAdditionalElementsLeft: [], labelAdditionalComponentsRight: [], }; @@ -88,34 +91,8 @@ export class LabelComponent { render = (args: LabelComponentRenderArgs): JSX.Element => { - return (
Unsupported component type
) - } -} - - -/******************************************************************************* - ** - *******************************************************************************/ -export class HeaderLink extends LabelComponent -{ - label: string; - to: string - - constructor(label: string, to: string) - { - super(); - this.label = label; - this.to = to; - } - - render = (args: LabelComponentRenderArgs): JSX.Element => - { - return ( - - {this.to ? {this.label} : null} - - ); - } + return (
Unsupported component type
); + }; } @@ -141,8 +118,8 @@ export class AddNewRecordButton extends LabelComponent openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) => { - navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`) - } + navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`); + }; render = (args: LabelComponentRenderArgs): JSX.Element => { @@ -151,35 +128,7 @@ export class AddNewRecordButton extends LabelComponent ); - } -} - - -/******************************************************************************* - ** - *******************************************************************************/ -export class ExportDataButton extends LabelComponent -{ - callbackToExport: any; - tooltipTitle: string; - isDisabled: boolean; - - constructor(callbackToExport: any, isDisabled = false, tooltipTitle: string = "Export") - { - super(); - this.callbackToExport = callbackToExport; - this.isDisabled = isDisabled; - this.tooltipTitle = tooltipTitle; - } - - render = (args: LabelComponentRenderArgs): JSX.Element => - { - return ( - - - - ); - } + }; } @@ -227,7 +176,7 @@ export class Dropdown extends LabelComponent /> ); - } + }; } @@ -251,7 +200,7 @@ export class ReloadControl extends LabelComponent ); - } + }; } @@ -372,7 +321,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element if (index < 0) { - throw(`Could not find table name for label ${tableName}`); + throw (`Could not find table name for label ${tableName}`); } dropdownData[index] = (changedData) ? changedData.id : null; @@ -394,7 +343,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element } } - reloadWidget(dropdownData) + reloadWidget(dropdownData); } } @@ -422,7 +371,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element { console.log(`No reload widget callback in ${props.widgetMetaData.label}`); } - } + }; const toggleFullScreenWidget = () => { @@ -434,14 +383,14 @@ function Widget(props: React.PropsWithChildren): JSX.Element { setFullScreenWidgetClassName("fullScreenWidget"); } - } + }; const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true; const isSet = (v: any): boolean => { return (v !== null && v !== undefined); - } + }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components // @@ -450,6 +399,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element if (hasPermission) { needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0); + needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0); needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0); needLabelBox ||= isSet(props.widgetMetaData?.icon); needLabelBox ||= isSet(props.widgetData?.label); @@ -530,6 +480,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element }) ) } + {props.labelAdditionalElementsLeft} { diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 6f02ba8..1f97113 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -22,10 +22,14 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import Button from "@mui/material/Button"; +import Icon from "@mui/material/Icon"; +import Tooltip from "@mui/material/Tooltip/Tooltip"; +import Typography from "@mui/material/Typography"; import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro"; import React, {useEffect, useState} from "react"; -import {useNavigate} from "react-router-dom"; -import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget"; +import {useNavigate, Link} from "react-router-dom"; +import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget"; import DataGridUtils from "qqq/utils/DataGridUtils"; import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; @@ -47,6 +51,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element const [records, setRecords] = useState([] as QRecord[]) const [columns, setColumns] = useState([]); const [allColumns, setAllColumns] = useState([]) + const [csv, setCsv] = useState(null as string); + const [fileName, setFileName] = useState(null as string); const navigate = useNavigate(); useEffect(() => @@ -75,6 +81,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // capture all-columns to use for the export (before we might splice some away from the on-screen display) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + const allColumns = [... columns]; setAllColumns(JSON.parse(JSON.stringify(columns))); //////////////////////////////////////////////////////////////// @@ -95,39 +102,42 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element setRows(rows); setRecords(records) setColumns(columns); - } - }, [data]); - const exportCallback = () => - { - let csv = ""; - for (let i = 0; i < allColumns.length; i++) - { - csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"` - } - csv += "\n"; - - for (let i = 0; i < records.length; i++) - { - for (let j = 0; j < allColumns.length; j++) + let csv = ""; + for (let i = 0; i < allColumns.length; i++) { - const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field) - csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"` + csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"` } csv += "\n"; - } - const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; - HtmlUtils.download(fileName, csv); - } + for (let i = 0; i < records.length; i++) + { + for (let j = 0; j < allColumns.length; j++) + { + const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field) + csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"` + } + csv += "\n"; + } + + const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; + + setCsv(csv); + setFileName(fileName); + } + }, [data]); /////////////////// // view all link // /////////////////// - const labelAdditionalComponentsLeft: LabelComponent[] = [] + const labelAdditionalElementsLeft: JSX.Element[] = []; if(data && data.viewAllLink) { - labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink)); + labelAdditionalElementsLeft.push( + + View All + + ) } /////////////////// @@ -149,7 +159,26 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } } - labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle)) + const onExportClick = () => + { + if(csv) + { + HtmlUtils.download(fileName, csv); + } + else + { + alert("There is no data available to export.") + } + } + + if(widgetMetaData?.showExportButton) + { + labelAdditionalElementsLeft.push( + + + + ); + } //////////////////// // add new button // @@ -184,7 +213,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element - { if (props.widgetData && rows && columns) { - console.log(props.widgetData); - let csv = ""; for (let j = 0; j < columns.length; j++) { @@ -98,16 +98,37 @@ function TableWidget(props: Props): JSX.Element csv += "\n"; } - console.log(csv); + setCsv(csv); const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; + setFileName(fileName) + + console.log(`useEffect, setting fileName ${fileName}`); + } + + }, [props.widgetMetaData, props.widgetData]); + + const onExportClick = () => + { + if(csv) + { HtmlUtils.download(fileName, csv); } else { - alert("There is no data available to export."); + alert("There is no data available to export.") } - }; + } + + const labelAdditionalElementsLeft: JSX.Element[] = []; + if(props.widgetMetaData?.showExportButton) + { + labelAdditionalElementsLeft.push( + + + + ); + } return ( props.reloadWidgetCallback(data)} footerHTML={props.widgetData?.footerHTML} isChild={props.isChild} - labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []} + labelAdditionalElementsLeft={labelAdditionalElementsLeft} > chromePrefs = new HashMap<>(); + chromePrefs.put("profile.default_content_settings.popups", 0); + chromePrefs.put("download.default_directory", getDownloadsDirectory()); + chromeOptions.setExperimentalOption("prefs", chromePrefs); + driver = new ChromeDriver(chromeOptions); + driver.manage().window().setSize(new Dimension(1700, 1300)); qSeleniumLib = new QSeleniumLib(driver); @@ -68,6 +82,57 @@ public class QBaseSeleniumTest + /******************************************************************************* + ** + *******************************************************************************/ + private void manageDownloadsDirectory() + { + File downloadsDirectory = new File(getDownloadsDirectory()); + if(!downloadsDirectory.exists()) + { + if(!downloadsDirectory.mkdir()) + { + fail("Could not create downloads directory: " + downloadsDirectory); + } + } + + if(!downloadsDirectory.isDirectory()) + { + fail("Downloads directory: " + downloadsDirectory + " is not a directory."); + } + + for(File file : CollectionUtils.nonNullArray(downloadsDirectory.listFiles())) + { + if(!file.delete()) + { + fail("Could not remove a file from the downloads directory: " + file.getAbsolutePath()); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String getDownloadsDirectory() + { + return ("/tmp/selenium-downloads"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected List getDownloadedFiles() + { + File[] downloadedFiles = CollectionUtils.nonNullArray((new File(getDownloadsDirectory())).listFiles()); + return (Arrays.stream(downloadedFiles).toList()); + } + + + /******************************************************************************* ** control if the test needs to start its own javalin server, or if we're running ** in an environment where an external web server is being used. diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java index 2b28423..ebd239f 100755 --- a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java @@ -1,3 +1,24 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + package com.kingsrook.qqq.materialdashboard.lib; @@ -6,11 +27,15 @@ import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.OutputType; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; @@ -36,6 +61,8 @@ public class QSeleniumLib private boolean SCREENSHOTS_ENABLED = true; private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/"; + private boolean autoHighlight = false; + /******************************************************************************* @@ -187,7 +214,13 @@ public class QSeleniumLib *******************************************************************************/ public WebElement waitForSelector(String cssSelector) { - return (waitForSelectorAll(cssSelector, 1).get(0)); + WebElement element = waitForSelectorAll(cssSelector, 1).get(0); + + Actions actions = new Actions(driver); + actions.moveToElement(element); + + conditionallyAutoHighlight(element); + return element; } @@ -230,7 +263,7 @@ public class QSeleniumLib do { List elements = driver.findElements(By.cssSelector(cssSelector)); - if(elements.size() == 0) + if(elements.isEmpty()) { LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]"); return; @@ -256,7 +289,7 @@ public class QSeleniumLib do { List elements = driver.findElements(By.cssSelector(cssSelector)); - if(elements.size() == 0) + if(elements.isEmpty()) { LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]"); return; @@ -330,6 +363,22 @@ public class QSeleniumLib + /******************************************************************************* + ** + *******************************************************************************/ + private void soonUnhighlightElement(WebElement element) + { + CompletableFuture.supplyAsync(() -> + { + SleepUtils.sleep(2, TimeUnit.SECONDS); + JavascriptExecutor js = (JavascriptExecutor) driver; + js.executeScript("arguments[0].setAttribute('style', 'background: unset; border: unset;');", element); + return (true); + }); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -380,7 +429,10 @@ public class QSeleniumLib @FunctionalInterface public interface Code { - public T run(); + /******************************************************************************* + ** + *******************************************************************************/ + T run(); } @@ -430,6 +482,7 @@ public class QSeleniumLib LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "]."); Actions actions = new Actions(driver); actions.moveToElement(element); + conditionallyAutoHighlight(element); return (element); } } @@ -437,6 +490,10 @@ public class QSeleniumLib { LOG.debug("Caught a StaleElementReferenceException - will retry."); } + catch(NoSuchElementException nsee) + { + LOG.debug("Caught a NoSuchElementException - will retry."); + } } sleepABit(); @@ -449,6 +506,20 @@ public class QSeleniumLib + /******************************************************************************* + ** + *******************************************************************************/ + private void conditionallyAutoHighlight(WebElement element) + { + if(autoHighlight && System.getenv("CIRCLECI") == null) + { + highlightElement(element); + soonUnhighlightElement(element); + } + } + + + /******************************************************************************* ** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory ** for the test class simple name, filename = methodName.png. @@ -478,7 +549,8 @@ public class QSeleniumLib destFile.mkdirs(); if(destFile.exists()) { - destFile.delete(); + String newFileName = destFile.getAbsolutePath().replaceFirst("\\.png", "-" + System.currentTimeMillis() + ".png"); + destFile.renameTo(new File(newFileName)); } FileUtils.moveFile(outputFile, destFile); LOG.info("Made screenshot at: " + destFile); @@ -555,4 +627,48 @@ public class QSeleniumLib } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getLatestChromeDownloadedFileInfo() + { + driver.get("chrome://downloads/"); + JavascriptExecutor js = (JavascriptExecutor) driver; + WebElement element = (WebElement) js.executeScript("return document.querySelector('downloads-manager').shadowRoot.querySelector('#mainContainer > iron-list > downloads-item').shadowRoot.querySelector('#content')"); + return (element.getText()); + } + + + + /******************************************************************************* + ** Getter for autoHighlight + *******************************************************************************/ + public boolean getAutoHighlight() + { + return (this.autoHighlight); + } + + + + /******************************************************************************* + ** Setter for autoHighlight + *******************************************************************************/ + public void setAutoHighlight(boolean autoHighlight) + { + this.autoHighlight = autoHighlight; + } + + + + /******************************************************************************* + ** Fluent setter for autoHighlight + *******************************************************************************/ + public QSeleniumLib withAutoHighlight(boolean autoHighlight) + { + this.autoHighlight = autoHighlight; + return (this); + } + } diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java index 25ca3c9..741b6ef 100755 --- a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java @@ -29,7 +29,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* - ** Test for Associated Record Scripts functionality. + ** Test that goes to a record, clicks a link for another record, then + ** hits 'e' on keyboard to edit the second record - and confirms that we're + ** on the edit url for the second record, not the first (a former bug). *******************************************************************************/ public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest { diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java new file mode 100755 index 0000000..bda8473 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java @@ -0,0 +1,111 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.materialdashboard.tests; + + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest; +import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Tests for dashboard table widget with export button + *******************************************************************************/ +public class DashboardTableWidgetExportTest extends QBaseSeleniumTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin) + { + super.addJavalinRoutes(qSeleniumJavalin); + qSeleniumJavalin + .withRouteToFile("/data/person/count", "data/person/count.json") + .withRouteToFile("/data/city/count", "data/city/count.json"); + + qSeleniumJavalin.withRouteToString("/widget/SampleTableWidget", """ + { + "label": "Sample Table Widget", + "footerHTML": "Updated at 2023-10-17 09:11:38 AM CDT", + "columns": [ + { "type": "html", "header": "Id", "accessor": "id", "width": "1%" }, + { "type": "html", "header": "Name", "accessor": "name", "width": "99%" } + ], + "rows": [ + { "id": "1", "name": "Homer S." }, + { "id": "2", "name": "Marge B." }, + { "id": "3", "name": "Bart J." } + ], + "type": "table" + } + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDashboardTableWidgetExport() throws IOException + { + qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App"); + + //////////////////////////////////////////////////////////////////////// + // assert that the table widget rendered its header and some contents // + //////////////////////////////////////////////////////////////////////// + qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget"); + qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S."); + qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT"); + + ///////////////////////////// + // click the export button // + ///////////////////////////// + qSeleniumLib.waitForSelector("#SampleTableWidget h6") + .findElement(By.xpath("./..")) + .findElement(By.cssSelector("button")) + .click(); + + qSeleniumLib.waitForCondition("Should have downloaded 1 file", () -> getDownloadedFiles().size() == 1); + File csvFile = getDownloadedFiles().get(0); + assertThat(csvFile.getName()).matches("Sample Table Widget.*.csv"); + String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8); + assertEquals(""" + "Id","Name" + "1","Homer S." + "2","Marge B." + "3","Bart J." + """, fileContents); + + // qSeleniumLib.waitForever(); + } + +} diff --git a/src/test/resources/fixtures/metaData/index.json b/src/test/resources/fixtures/metaData/index.json index af09f14..4c076f8 100644 --- a/src/test/resources/fixtures/metaData/index.json +++ b/src/test/resources/fixtures/metaData/index.json @@ -302,8 +302,7 @@ "label": "Greetings App", "iconName": "emoji_people", "widgets": [ - "PersonsByCreateDateBarChart", - "QuickSightChartRenderer" + "SampleTableWidget" ], "children": [ { @@ -738,139 +737,11 @@ "icon": "/kr-icon.png" }, "widgets": { - "parcelTrackingDetails": { - "name": "parcelTrackingDetails", - "label": "Tracking Details", - "type": "childRecordList" - }, - "deposcoSalesOrderLineItems": { - "name": "deposcoSalesOrderLineItems", - "label": "Line Items", - "type": "childRecordList" - }, - "TotalShipmentsByDayBarChart": { - "name": "TotalShipmentsByDayBarChart", - "label": "Total Shipments By Day", - "type": "chart" - }, - "TotalShipmentsByMonthLineChart": { - "name": "TotalShipmentsByMonthLineChart", - "label": "Total Shipments By Month", - "type": "chart" - }, - "YTDShipmentsByCarrierPieChart": { - "name": "YTDShipmentsByCarrierPieChart", - "label": "Shipments By Carrier Year To Date", - "type": "chart" - }, - "TodaysShipmentsStatisticsCard": { - "name": "TodaysShipmentsStatisticsCard", - "label": "Today's Shipments", - "type": "statistics" - }, - "ShipmentsInTransitStatisticsCard": { - "name": "ShipmentsInTransitStatisticsCard", - "label": "Shipments In Transit", - "type": "statistics" - }, - "OpenOrdersStatisticsCard": { - "name": "OpenOrdersStatisticsCard", - "label": "Open Orders", - "type": "statistics" - }, - "ShippingExceptionsStatisticsCard": { - "name": "ShippingExceptionsStatisticsCard", - "label": "Shipping Exceptions", - "type": "statistics" - }, - "WarehouseLocationCards": { - "name": "WarehouseLocationCards", - "type": "location" - }, - "TotalShipmentsStatisticsCard": { - "name": "TotalShipmentsStatisticsCard", - "label": "Total Shipments", - "type": "statistics" - }, - "SuccessfulDeliveriesStatisticsCard": { - "name": "SuccessfulDeliveriesStatisticsCard", - "label": "Successful Deliveries", - "type": "statistics" - }, - "ServiceFailuresStatisticsCard": { - "name": "ServiceFailuresStatisticsCard", - "label": "Service Failures", - "type": "statistics" - }, - "CarrierVolumeLineChart": { - "name": "CarrierVolumeLineChart", - "label": "Carrier Volume By Month", - "type": "lineChart" - }, - "YTDSpendByCarrierTable": { - "name": "YTDSpendByCarrierTable", - "label": "Spend By Carrier Year To Date", - "type": "table" - }, - "TimeInTransitBarChart": { - "name": "TimeInTransitBarChart", - "label": "Time In Transit Last 30 Days", - "type": "chart" - }, - "OpenBillingWorksheetsTable": { - "name": "OpenBillingWorksheetsTable", - "label": "Open Billing Worksheets", - "type": "table" - }, - "AssociatedParcelInvoicesTable": { - "name": "AssociatedParcelInvoicesTable", - "label": "Associated Parcel Invoices", + "SampleTableWidget": { + "name": "SampleTableWidget", + "label": "Sample Table Widget", "type": "table", - "icon": "receipt" - }, - "BillingWorksheetLinesTable": { - "name": "BillingWorksheetLinesTable", - "label": "Billing Worksheet Lines", - "type": "table" - }, - "RatingIssuesWidget": { - "name": "RatingIssuesWidget", - "label": "Rating Issues", - "type": "html", - "icon": "warning", - "gridColumns": 6 - }, - "UnassignedParcelInvoicesTable": { - "name": "UnassignedParcelInvoicesTable", - "label": "Unassigned Parcel Invoices", - "type": "table" - }, - "ParcelInvoiceSummaryWidget": { - "name": "ParcelInvoiceSummaryWidget", - "label": "Parcel Invoice Summary", - "type": "multiStatistics" - }, - "ParcelInvoiceLineExceptionsSummaryWidget": { - "name": "ParcelInvoiceLineExceptionsSummaryWidget", - "label": "Parcel Invoice Line Exceptions", - "type": "multiStatistics" - }, - "BillingWorksheetStatusStepper": { - "name": "BillingWorksheetStatusStepper", - "label": "Billing Worksheet Progress", - "type": "stepper", - "icon": "refresh", - "gridColumns": 6 - }, - "PersonsByCreateDateBarChart": { - "name": "PersonsByCreateDateBarChart", - "label": "Persons By Create Date", - "type": "barChart" - }, - "QuickSightChartRenderer": { - "name": "QuickSightChartRenderer", - "label": "Quick Sight", - "type": "quickSightChart" + "showExportButton": true }, "scriptViewer": { "name": "scriptViewer",