From b968705a010107e7ae23079eb29605b9f4bfe95c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 17 Oct 2023 19:16:04 -0500 Subject: [PATCH 1/6] Update class header comment --- .../tests/ClickLinkOnRecordThenEditShortcutTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 { From 791b50b89367590b5af8441786860a02bc40dbb4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 17 Oct 2023 19:23:02 -0500 Subject: [PATCH 2/6] Redo export buttons as JSX Elements that get passed into Widget.tsx, rather than "Component" objects. something to do with when things are getting bound, was making the export buttons never have data. This was done with labelAdditionalElementsLeft, similar to labelAdditionalComponentsLeft. In theory, maybe, this is better, and we should remove all the additionalComponents left & right... --- src/qqq/components/widgets/Widget.tsx | 85 ++++--------------- .../widgets/misc/RecordGridWidget.tsx | 81 ++++++++++++------ .../components/widgets/tables/TableWidget.tsx | 43 +++++++--- 3 files changed, 105 insertions(+), 104 deletions(-) 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} > Date: Tue, 17 Oct 2023 19:25:20 -0500 Subject: [PATCH 3/6] Add option to autoHighlight elements. Add getLatestChromeDownloadedFileInfo; maybe fix collisions when writing screenshots --- .../materialdashboard/lib/QSeleniumLib.java | 126 +++++++++++++++++- 1 file changed, 121 insertions(+), 5 deletions(-) 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); + } + } From 6bd6b0370b04cb150dd1a7f491fce16c64cc49f3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 17 Oct 2023 19:26:14 -0500 Subject: [PATCH 4/6] Initial checkin --- .../tests/DashboardTableWidgetExportTest.java | 104 +++++++++++++ .../resources/fixtures/metaData/index.json | 146 +----------------- 2 files changed, 108 insertions(+), 142 deletions(-) create mode 100755 src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java 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..03b9b47 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java @@ -0,0 +1,104 @@ +/* + * 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 com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest; +import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** 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() + { + 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(); + + ///////////////////////////////////////////////////////////////////////////// + // assert about the file that was downloaded - its name and some contents. // + ///////////////////////////////////////////////////////////////////////////// + String latestFile = qSeleniumLib.getLatestChromeDownloadedFileInfo(); + assertThat(latestFile).contains("Sample Table Widget"); + assertThat(latestFile).contains(".csv"); + assertThat(latestFile).contains(""" + "Id"%2C"Name"%0A"1"%2C"Homer S."%0A"""); + + qSeleniumLib.waitForever(); + } + +} diff --git a/src/test/resources/fixtures/metaData/index.json b/src/test/resources/fixtures/metaData/index.json index af09f14..9f31409 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,147 +737,10 @@ "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", + "SampleTableWidget": { + "name": "SampleTableWidget", + "label": "Sample Table Widget", "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", - "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" - }, - "scriptViewer": { - "name": "scriptViewer", - "label": "Contents", - "type": "scriptViewer", - "isCard": true, - "storeDropdownSelections": false, - "hasPermission": true } }, "environmentValues": { From e144cf3ec76e3dea6491626cc0dec1a3cd0dfbf5 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 17 Oct 2023 20:36:23 -0500 Subject: [PATCH 5/6] Added scriptViewer widget back in metaData to fix tests that needed it; turned on showExportButton in sample table widget; --- src/test/resources/fixtures/metaData/index.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/resources/fixtures/metaData/index.json b/src/test/resources/fixtures/metaData/index.json index 9f31409..4c076f8 100644 --- a/src/test/resources/fixtures/metaData/index.json +++ b/src/test/resources/fixtures/metaData/index.json @@ -740,7 +740,16 @@ "SampleTableWidget": { "name": "SampleTableWidget", "label": "Sample Table Widget", - "type": "table" + "type": "table", + "showExportButton": true + }, + "scriptViewer": { + "name": "scriptViewer", + "label": "Contents", + "type": "scriptViewer", + "isCard": true, + "storeDropdownSelections": false, + "hasPermission": true } }, "environmentValues": { From e993fcb949531569300e3165a31d5fdb962b0a83 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 18 Oct 2023 08:53:41 -0500 Subject: [PATCH 6/6] Add better support (hopefully that works in CI) for downloads; update this test to use that. --- .../lib/QBaseSeleniumTest.java | 65 +++++++++++++++++++ .../tests/DashboardTableWidgetExportTest.java | 27 +++++--- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QBaseSeleniumTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QBaseSeleniumTest.java index 649d82b..4d98e70 100755 --- a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QBaseSeleniumTest.java +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QBaseSeleniumTest.java @@ -1,6 +1,11 @@ package com.kingsrook.qqq.materialdashboard.lib; +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin; import io.github.bonigarcia.wdm.WebDriverManager; import org.junit.jupiter.api.AfterEach; @@ -11,6 +16,7 @@ import org.openqa.selenium.Dimension; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; +import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* @@ -54,7 +60,15 @@ public class QBaseSeleniumTest @BeforeEach public void beforeEach() { + manageDownloadsDirectory(); + + HashMap 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/tests/DashboardTableWidgetExportTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java index 03b9b47..bda8473 100755 --- a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java @@ -22,11 +22,16 @@ 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; /******************************************************************************* @@ -70,7 +75,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest ** *******************************************************************************/ @Test - void testDashboardTableWidgetExport() + void testDashboardTableWidgetExport() throws IOException { qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App"); @@ -89,16 +94,18 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest .findElement(By.cssSelector("button")) .click(); - ///////////////////////////////////////////////////////////////////////////// - // assert about the file that was downloaded - its name and some contents. // - ///////////////////////////////////////////////////////////////////////////// - String latestFile = qSeleniumLib.getLatestChromeDownloadedFileInfo(); - assertThat(latestFile).contains("Sample Table Widget"); - assertThat(latestFile).contains(".csv"); - assertThat(latestFile).contains(""" - "Id"%2C"Name"%0A"1"%2C"Homer S."%0A"""); + 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(); + // qSeleniumLib.waitForever(); } }