Merge pull request #13 from Kingsrook/feature/selenium

Feature/selenium
This commit is contained in:
tim-chamberlain
2023-02-03 12:34:39 -06:00
committed by GitHub
52 changed files with 1559 additions and 269 deletions

10
src/main/java/Placeholder.java Executable file
View File

@ -0,0 +1,10 @@
/*******************************************************************************
** Placeholder class, because maven really wants some source under src/main?
*******************************************************************************/
public class Placeholder
{
public void f()
{
}
}

View File

@ -29,19 +29,26 @@ const {createProxyMiddleware} = require("http-proxy-middleware");
module.exports = function (app)
{
app.use(
"/data/*/export/*",
createProxyMiddleware({
target: "http://localhost:8000",
changeOrigin: true,
}),
);
let port = 8000;
if(process.env.REACT_APP_PROXY_LOCALHOST_PORT)
{
port = process.env.REACT_APP_PROXY_LOCALHOST_PORT;
}
app.use(
"/download/*",
createProxyMiddleware({
target: "http://localhost:8000",
function getRequestHandler()
{
return createProxyMiddleware({
target: `http://localhost:${port}`,
changeOrigin: true,
}),
);
});
}
app.use("/data/*/export/*", getRequestHandler());
app.use("/download/*", getRequestHandler());
app.use("/metaData/*", getRequestHandler());
app.use("/data/*", getRequestHandler());
app.use("/widget/*", getRequestHandler());
app.use("/serverInfo", getRequestHandler());
app.use("/processes", getRequestHandler());
app.use("/reports", getRequestHandler());
};

View File

@ -0,0 +1,97 @@
package com.kingsrook.qqq.materialdashbaord.lib;
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
/*******************************************************************************
** Base class for Selenium tests
*******************************************************************************/
public class QBaseSeleniumTest
{
private static ChromeOptions chromeOptions;
private WebDriver driver;
protected QSeleniumJavalin qSeleniumJavalin;
protected QSeleniumLib qSeleniumLib;
/*******************************************************************************
**
*******************************************************************************/
@BeforeAll
static void beforeAll()
{
chromeOptions = new ChromeOptions();
chromeOptions.setAcceptInsecureCerts(true);
chromeOptions.addArguments("--ignore-certificate-errors");
String headless = System.getenv("QQQ_SELENIUM_HEADLESS");
if("true".equals(headless))
{
chromeOptions.setHeadless(true);
}
WebDriverManager.chromiumdriver().setup();
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
driver = new ChromeDriver(chromeOptions);
driver.manage().window().setSize(new Dimension(1600, 1200));
qSeleniumLib = new QSeleniumLib(driver);
qSeleniumJavalin = new QSeleniumJavalin();
addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin.start();
}
/*******************************************************************************
** meant for sub-classes to define their own javalin routes, if they need to
*******************************************************************************/
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
{
qSeleniumJavalin
.withRouteToFile("/metaData", "metaData/index.json")
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
.withRouteToFile("/metaData/table/city", "metaData/table/person.json");
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
if(driver != null)
{
driver.quit();
}
if(qSeleniumJavalin != null)
{
qSeleniumJavalin.stop();
}
}
}

View File

@ -0,0 +1,14 @@
package com.kingsrook.qqq.materialdashbaord.lib;
/*******************************************************************************
** constants to define css selectors for common QQQ material dashboard elements.
*******************************************************************************/
public interface QQQMaterialDashboardSelectors
{
String SIDEBAR_ITEM = ".MuiDrawer-paperAnchorDockedLeft li.MuiListItem-root";
String BREADCRUMB_HEADER = ".MuiToolbar-root h5";
String QUERY_GRID_CELL = ".MuiDataGrid-root .MuiDataGrid-cellContent";
String QUERY_FILTER_INPUT = ".MuiDataGrid-filterForm input.MuiInput-input";
}

View File

@ -0,0 +1,404 @@
package com.kingsrook.qqq.materialdashbaord.lib;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
** Library working with Selenium!
*******************************************************************************/
public class QSeleniumLib
{
public final WebDriver driver;
private long WAIT_SECONDS = 10;
private String BASE_URL = "https://localhost:3001";
private boolean SCREENSHOTS_ENABLED = true;
private String SCREENSHOTS_PATH = "/tmp/";
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QSeleniumLib(WebDriver webDriver)
{
this.driver = webDriver;
}
/*******************************************************************************
** Fluent setter for waitSeconds
**
*******************************************************************************/
public QSeleniumLib withWaitSeconds(int waitSeconds)
{
WAIT_SECONDS = waitSeconds;
return (this);
}
/*******************************************************************************
** Fluent setter for screenshotsEnabled
**
*******************************************************************************/
public QSeleniumLib withScreenshotsEnabled(boolean screenshotsEnabled)
{
SCREENSHOTS_ENABLED = screenshotsEnabled;
return (this);
}
/*******************************************************************************
** Fluent setter for screenshotsPath
**
*******************************************************************************/
public QSeleniumLib withScreenshotsPath(String screenshotsPath)
{
SCREENSHOTS_PATH = screenshotsPath;
return (this);
}
/*******************************************************************************
** Fluent setter for baseUrl
**
*******************************************************************************/
public QSeleniumLib withBaseUrl(String baseUrl)
{
BASE_URL = baseUrl;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void waitForSeconds(int n)
{
try
{
new WebDriverWait(driver, Duration.ofSeconds(n))
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".wontEverBePresent")));
}
catch(Exception e)
{
///////////////////
// okay, resume. //
///////////////////
}
}
/*******************************************************************************
**
*******************************************************************************/
public void waitForever()
{
// todo - if env says we're in CIRCLECI, then... just do a hard fail (or just not wait forever?)
System.out.println("Going into a waitForever...");
new WebDriverWait(driver, Duration.ofHours(1))
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".wontEverBePresent")));
}
/*******************************************************************************
**
*******************************************************************************/
public void gotoAndWaitForBreadcrumbHeader(String path, String headerText)
{
driver.get(BASE_URL + path);
String title = driver.getTitle();
System.out.println("Page Title: " + title);
WebElement header = new WebDriverWait(driver, Duration.ofSeconds(WAIT_SECONDS))
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER)));
System.out.println("Breadcrumb Header: " + header.getText());
assertEquals(headerText, header.getText());
}
/*******************************************************************************
**
*******************************************************************************/
public WebElement waitForSelector(String cssSelector)
{
return (waitForSelectorAll(cssSelector, 1).get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public List<WebElement> waitForSelectorAll(String cssSelector, int minCount)
{
System.out.println("Waiting for element matching selector [" + cssSelector + "]");
long start = System.currentTimeMillis();
do
{
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
if(elements.size() >= minCount)
{
System.out.println("Found [" + elements.size() + "] element(s) matching selector [" + cssSelector + "]");
return (elements);
}
sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
fail("Failed to find element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds.");
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public static void sleepABit()
{
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
// resume
}
}
@FunctionalInterface
public interface Code<T>
{
public T run();
}
/*******************************************************************************
**
*******************************************************************************/
public <T> T waitLoop(String message, Code<T> c)
{
System.out.println("Waiting for: " + message);
long start = System.currentTimeMillis();
do
{
T t = c.run();
if(t != null)
{
System.out.println("Found: " + message);
return (t);
}
sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
System.out.println("Failed to match while waiting for: " + message);
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
{
System.out.println("Waiting for element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
long start = System.currentTimeMillis();
do
{
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
for(WebElement element : elements)
{
try
{
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
{
System.out.println("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
Actions actions = new Actions(driver);
actions.moveToElement(element);
return (element);
}
}
catch(StaleElementReferenceException sere)
{
System.err.println("Caught a StaleElementReferenceException - will retry.");
}
}
sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
fail("Failed to find element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds.");
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public WebElement waitForSelectorContainingV2(String cssSelector, String textContains)
{
return (waitLoop("element matching selector [" + cssSelector + "] containing text [" + textContains + "].", () ->
{
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
for(WebElement element : elements)
{
try
{
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
{
return (element);
}
}
catch(StaleElementReferenceException sere)
{
System.err.println("Caught a StaleElementReferenceException - will retry.");
}
}
return (null);
}));
}
/*******************************************************************************
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
** for the test class simple name, filename = methodName.png.
*******************************************************************************/
public void takeScreenshotToFile()
{
StackTraceElement[] stackTrace = new Exception().getStackTrace();
StackTraceElement caller = stackTrace[1];
String filePathSuffix = caller.getClassName().substring(caller.getClassName().lastIndexOf(".") + 1) + "/" + caller.getMethodName() + ".png";
takeScreenshotToFile(filePathSuffix);
}
/*******************************************************************************
** Take a screenshot, and give it a path/name of your choosing (under SCREENSHOTS_PATH)
*******************************************************************************/
public void takeScreenshotToFile(String filePathSuffix)
{
if(SCREENSHOTS_ENABLED)
{
try
{
File outputFile = driver.findElement(By.cssSelector("html")).getScreenshotAs(OutputType.FILE);
File destFile = new File(SCREENSHOTS_PATH + filePathSuffix);
destFile.mkdirs();
if(destFile.exists())
{
destFile.delete();
}
FileUtils.moveFile(outputFile, destFile);
System.out.println("Made screenshot at: " + destFile);
}
catch(Exception e)
{
System.err.println("Error taking screenshot to file: " + e.getMessage());
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public void assertElementHasFocus(WebElement element)
{
long start = System.currentTimeMillis();
do
{
if(Objects.equals(driver.switchTo().activeElement(), element))
{
return;
}
sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
fail("Failed to see that element [" + element + "] has focus.");
}
/*******************************************************************************
**
*******************************************************************************/
@FunctionalInterface
public interface VoidVoidFunction
{
/*******************************************************************************
**
*******************************************************************************/
void run();
}
/*******************************************************************************
**
*******************************************************************************/
public void tryMultiple(int noOfTries, VoidVoidFunction f)
{
for(int i = 0; i < noOfTries; i++)
{
try
{
f.run();
return;
}
catch(Exception e)
{
if(i < noOfTries - 1)
{
System.out.println("On try [" + i + " of " + noOfTries + "] caught: " + e.getMessage());
}
else
{
throw (e);
}
}
}
}
}

View File

@ -0,0 +1,93 @@
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
import io.javalin.http.Context;
/*******************************************************************************
** data copied from a javalin context, e.g., for inspection by a test
*******************************************************************************/
public class CapturedContext
{
private String method;
private String path;
private String body;
/*******************************************************************************
**
*******************************************************************************/
public CapturedContext(Context context)
{
path = context.path();
method = context.method().name();
body = context.body();
}
/*******************************************************************************
** Getter for method
**
*******************************************************************************/
public String getMethod()
{
return method;
}
/*******************************************************************************
** Setter for method
**
*******************************************************************************/
public void setMethod(String method)
{
this.method = method;
}
/*******************************************************************************
** Getter for path
**
*******************************************************************************/
public String getPath()
{
return path;
}
/*******************************************************************************
** Setter for path
**
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Getter for body
**
*******************************************************************************/
public String getBody()
{
return body;
}
/*******************************************************************************
** Setter for body
**
*******************************************************************************/
public void setBody(String body)
{
this.body = body;
}
}

View File

@ -0,0 +1,45 @@
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
import io.javalin.http.Context;
import io.javalin.http.Handler;
/*******************************************************************************
** javalin handler that captures the context, for later review, e.g., of the
** query string or posted body
*******************************************************************************/
public class CapturingHandler implements Handler
{
private final QSeleniumJavalin qSeleniumJavalin;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public CapturingHandler(QSeleniumJavalin qSeleniumJavalin)
{
this.qSeleniumJavalin = qSeleniumJavalin;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void handle(Context context) throws Exception
{
if(qSeleniumJavalin.capturing)
{
System.out.println("Capturing request for path [" + context.path() + "]");
qSeleniumJavalin.captured.add(new CapturedContext(context));
}
else
{
System.out.println("Not capturing request for path [" + context.path() + "]");
}
}
}

View File

@ -0,0 +1,258 @@
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.materialdashbaord.lib.QSeleniumLib;
import io.javalin.Javalin;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConnectionFactory;
import static io.javalin.apibuilder.ApiBuilder.get;
import static io.javalin.apibuilder.ApiBuilder.post;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
** Javalin server manager for use with by Selenium tests!!
*******************************************************************************/
public class QSeleniumJavalin
{
private long WAIT_SECONDS = 10;
private List<Pair<String, String>> routesToFiles;
private List<Pair<String, String>> routesToStrings;
private Javalin javalin;
////////////////////////////////////////////////////////////////////////////////////////
// multiple javalin threads will be running and hitting these structures in parallel, //
// so it's critical to wrap collections in synchronized versions!! //
////////////////////////////////////////////////////////////////////////////////////////
List<String> routeFilesServed = Collections.synchronizedList(new ArrayList<>());
List<String> pathsThat404ed = Collections.synchronizedList(new ArrayList<>());
boolean capturing = false;
List<CapturedContext> captured = Collections.synchronizedList(new ArrayList<>());
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QSeleniumJavalin()
{
}
/*******************************************************************************
** Fluent setter for routeToFile
**
*******************************************************************************/
public QSeleniumJavalin withRouteToFile(String path, String file)
{
if(this.routesToFiles == null)
{
this.routesToFiles = new ArrayList<>();
}
this.routesToFiles.add(Pair.of(path, file));
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QSeleniumJavalin withRouteToString(String path, String responseString)
{
if(this.routesToStrings == null)
{
this.routesToStrings = new ArrayList<>();
}
this.routesToStrings.add(Pair.of(path, responseString));
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QSeleniumJavalin start()
{
javalin = Javalin.create().start(8001);
if(routesToFiles != null)
{
javalin.routes(() ->
{
for(Pair<String, String> routeToFile : routesToFiles)
{
System.out.println("Setting up route for [" + routeToFile.getKey() + "] => [" + routeToFile.getValue() + "]");
get(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
post(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
}
});
}
if(routesToStrings != null)
{
javalin.routes(() ->
{
for(Pair<String, String> routeToString : routesToStrings)
{
System.out.println("Setting up route for [" + routeToString.getKey() + "] => [" + routeToString.getValue() + "]");
get(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
post(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
}
});
}
javalin.before(new CapturingHandler(this));
javalin.error(404, context -> {
System.out.println("Returning 404 for [" + context.method() + " " + context.path() + "]");
pathsThat404ed.add(context.path());
});
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to accept "large" access tokens in Authorization: Bearer <token> headers (e.g., with 100s of permissions), //
// we need a larger size allowed for HTTP headers (javalin/jetty default is 8K) //
// making this too large can waste resources and open one up to various DOS attacks, supposedly. //
// (Note, this must happen after the javalin service.start call) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(Connector connector : javalin.jettyServer().server().getConnectors())
{
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setRequestHeaderSize(65535);
}
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void stop()
{
if(javalin != null)
{
javalin.stop();
}
}
/*******************************************************************************
**
*******************************************************************************/
public void report()
{
System.out.println("Paths that 404'ed:");
pathsThat404ed.forEach(s -> System.out.println(" - " + s));
System.out.println("Routes served as static files:");
routeFilesServed.forEach(s -> System.out.println(" - " + s));
}
/*******************************************************************************
**
*******************************************************************************/
public void beginCapture()
{
System.out.println("Beginning to capture requests now");
capturing = true;
captured.clear();
}
/*******************************************************************************
**
*******************************************************************************/
public void endCapture()
{
System.out.println("Ending capturing of requests now");
capturing = false;
}
/*******************************************************************************
**
*******************************************************************************/
public List<CapturedContext> getCaptured()
{
return (captured);
}
/*******************************************************************************
**
*******************************************************************************/
public CapturedContext waitForCapturedPath(String path)
{
System.out.println("Waiting for captured request for path [" + path + "]");
long start = System.currentTimeMillis();
do
{
// System.out.println(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
for(CapturedContext context : captured)
{
if(context.getPath().equals(path))
{
System.out.println("Found captured request for path [" + path + "]");
return (context);
}
}
QSeleniumLib.sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
fail("Failed to capture a request for path [" + path + "] after [" + WAIT_SECONDS + "] seconds.");
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public CapturedContext waitForCapturedPathWithBodyContaining(String path, String bodyContaining)
{
System.out.println("Waiting for captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
long start = System.currentTimeMillis();
do
{
// System.out.println(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
for(CapturedContext context : captured)
{
if(context.getPath().equals(path))
{
if(context.getBody() != null && context.getBody().contains(bodyContaining))
{
System.out.println("Found captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
return (context);
}
}
}
QSeleniumLib.sleepABit();
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
fail("Failed to capture a request for path [" + path + "] with body containing [" + bodyContaining + "] after [" + WAIT_SECONDS + "] seconds.");
return (null);
}
}

View File

@ -0,0 +1,54 @@
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
import java.nio.charset.StandardCharsets;
import java.util.List;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
/*******************************************************************************
** javalin handler for returning content from a "fixtures" file
*******************************************************************************/
public class RouteFromFileHandler implements Handler
{
private final String route;
private final String filePath;
private final QSeleniumJavalin qSeleniumJavalin;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RouteFromFileHandler(QSeleniumJavalin qSeleniumJavalin, Pair<String, String> routeToFilePath)
{
this.qSeleniumJavalin = qSeleniumJavalin;
this.route = routeToFilePath.getKey();
this.filePath = routeToFilePath.getValue();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void handle(Context context) throws Exception
{
try
{
qSeleniumJavalin.routeFilesServed.add(this.route);
System.out.println("Serving route [" + this.route + "] via file [" + this.filePath + "]");
List<String> lines = IOUtils.readLines(getClass().getResourceAsStream("/fixtures/" + this.filePath), StandardCharsets.UTF_8);
context.result(String.join("\n", lines));
}
catch(Exception e)
{
throw new IllegalStateException("Error reading file [" + this.filePath + "]");
}
}
}

View File

@ -0,0 +1,43 @@
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.apache.commons.lang3.tuple.Pair;
/*******************************************************************************
** javalin handler for returning a static string for a route
*******************************************************************************/
public class RouteFromStringHandler implements Handler
{
private final String route;
private final String responseString;
private final QSeleniumJavalin qSeleniumJavalin;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RouteFromStringHandler(QSeleniumJavalin qSeleniumJavalin, Pair<String, String> routeToStringPath)
{
this.qSeleniumJavalin = qSeleniumJavalin;
this.route = routeToStringPath.getKey();
this.responseString = routeToStringPath.getValue();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void handle(Context context)
{
qSeleniumJavalin.routeFilesServed.add(this.route);
System.out.println("Serving route [" + this.route + "] via static String");
context.result(this.responseString);
}
}

View File

@ -0,0 +1,80 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.materialdashbaord.tests;
import com.kingsrook.qqq.materialdashbaord.lib.QBaseSeleniumTest;
import com.kingsrook.qqq.materialdashbaord.lib.QQQMaterialDashboardSelectors;
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** Tests for app pages and high-level navigation in material dashboard
*******************************************************************************/
public class AppPageNavTest extends QBaseSeleniumTest
{
/*******************************************************************************
**
*******************************************************************************/
@Override
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
{
super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin
.withRouteToString("/widget/PersonsByCreateDateBarChart", "{}")
.withRouteToString("/widget/QuickSightChartRenderer", """
{"url": "http://www.google.com"}""")
.withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/city/count", "data/city/count.json");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testHomeToAppPageViaLeftNav()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App");
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "People App").click();
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "Greetings App").click();
qSeleniumLib.takeScreenshotToFile();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAppPageToTablePage()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp", "Greetings App");
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelectorContaining("a", "Person").click());
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER, "Person");
qSeleniumLib.takeScreenshotToFile();
}
}

View File

@ -0,0 +1,177 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.materialdashbaord.tests;
import com.kingsrook.qqq.materialdashbaord.lib.QBaseSeleniumTest;
import com.kingsrook.qqq.materialdashbaord.lib.QQQMaterialDashboardSelectors;
import com.kingsrook.qqq.materialdashbaord.lib.javalin.CapturedContext;
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.Select;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Test for the record query screen
*******************************************************************************/
public class QueryScreenTest extends QBaseSeleniumTest
{
/*******************************************************************************
**
*******************************************************************************/
@Override
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
{
super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/person/query", "data/person/index.json");
}
/*******************************************************************************
**
*******************************************************************************/
// @RepeatedTest(10)
@Test
void testBasicQueryAndClearFilters()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
/////////////////////////////////////////////////////////////////////
// open the filter window, enter a value, wait for query to re-run //
/////////////////////////////////////////////////////////////////////
WebElement filterInput = qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_FILTER_INPUT);
qSeleniumLib.assertElementHasFocus(filterInput);
qSeleniumJavalin.beginCapture();
filterInput.sendKeys("1");
///////////////////////////////////////////////////////////////////
// assert that query & count both have the expected filter value //
///////////////////////////////////////////////////////////////////
String idEquals1FilterSubstring = """
{"fieldName":"id","operator":"EQUALS","values":["1"]}""";
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
assertThat(capturedCount).extracting("body").asString().contains(idEquals1FilterSubstring);
assertThat(capturedQuery).extracting("body").asString().contains(idEquals1FilterSubstring);
qSeleniumJavalin.endCapture();
///////////////////////////////////////
// click away from the filter window //
///////////////////////////////////////
qSeleniumLib.waitForSeconds(1); // todo grr.
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER).click();
qSeleniumLib.waitForSelectorContaining(".MuiBadge-root", "1");
///////////////////////////////////////////////////////////////////
// click the 'x' clear icon, then yes, then expect another query //
///////////////////////////////////////////////////////////////////
qSeleniumJavalin.beginCapture();
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelector("#clearFiltersButton").click());
qSeleniumLib.waitForSelectorContaining("BUTTON", "Yes").click();
////////////////////////////////////////////////////////////////////
// assert that query & count both no longer have the filter value //
////////////////////////////////////////////////////////////////////
capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
qSeleniumJavalin.endCapture();
qSeleniumLib.takeScreenshotToFile();
// qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
}
/*******************************************************************************
**
*******************************************************************************/
// @RepeatedTest(10)
@Test
void testMultiCriteriaQueryWithOr()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
addQueryFilterInput(0, "First Name", "contains", "Dar", "Or");
qSeleniumJavalin.beginCapture();
addQueryFilterInput(1, "First Name", "contains", "Jam", "Or");
String expectedFilterContents0 = """
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
String expectedFilterContents1 = """
{"fieldName":"firstName","operator":"CONTAINS","values":["Jam"]}""";
String expectedFilterContents2 = """
"booleanOperator":"OR"}""";
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents0);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
qSeleniumJavalin.endCapture();
qSeleniumLib.takeScreenshotToFile();
// qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
}
/*******************************************************************************
**
*******************************************************************************/
private void addQueryFilterInput(int index, String fieldlabel, String operator, String value, String booleanOperator)
{
if(index > 0)
{
qSeleniumLib.waitForSelectorContaining("BUTTON", "Add filter").click();
}
WebElement subFormForField = qSeleniumLib.waitForSelectorAll(".MuiDataGrid-filterForm", index + 1).get(index);
if(index == 1)
{
Select linkOperatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormLinkOperatorInput SELECT")));
linkOperatorSelect.selectByVisibleText(booleanOperator);
}
Select fieldSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormColumnInput SELECT")));
fieldSelect.selectByVisibleText(fieldlabel);
Select operatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormOperatorInput SELECT")));
operatorSelect.selectByVisibleText(operator);
WebElement valueInput = subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormValueInput INPUT"));
valueInput.click();
valueInput.sendKeys(value);
}
}

View File

@ -0,0 +1,3 @@
{
"count": 101406
}

View File

@ -0,0 +1,3 @@
{
"count": 101406
}

View File

@ -0,0 +1,64 @@
{
"records": [
{
"tableName": "person",
"values": {
"id": 1,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-22T19:17:06",
"firstName": "Jonny",
"lastName": "Doe",
"birthDate": "1980-05-31",
"email": "jdoe@kingsrook.com"
}
},
{
"tableName": "person",
"values": {
"id": 2,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "James",
"lastName": "Maes",
"birthDate": "1980-05-15",
"email": "jmaes@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 3,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Tim",
"lastName": "Chamberlain",
"birthDate": "1976-05-28",
"email": "tchamberlain@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 4,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Tyler",
"lastName": "Samples",
"birthDate": "1986-05-28",
"email": "tsamples@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 5,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Garret",
"lastName": "Richardson",
"birthDate": "1981-01-01",
"email": "grichardson@mmltholdings.com"
}
}
]
}

View File

@ -0,0 +1,4 @@
{
"name": "mock",
"type": "MOCK"
}

View File

@ -0,0 +1,565 @@
{
"tables": {
"carrier": {
"name": "carrier",
"label": "Carrier",
"isHidden": false,
"iconName": "local_shipping",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_UPDATE",
"TABLE_INSERT",
"TABLE_DELETE"
]
},
"person": {
"name": "person",
"label": "Person",
"isHidden": false,
"iconName": "person",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_UPDATE",
"TABLE_INSERT",
"TABLE_DELETE"
]
},
"city": {
"name": "city",
"label": "Cities",
"isHidden": true,
"iconName": "location_city",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_UPDATE",
"TABLE_INSERT",
"TABLE_DELETE"
]
}
},
"processes": {
"greet": {
"name": "greet",
"label": "Greet People",
"tableName": "person",
"isHidden": true,
"iconName": "emoji_people"
},
"greetInteractive": {
"name": "greetInteractive",
"label": "Greet Interactive",
"tableName": "person",
"isHidden": false,
"iconName": "waving_hand"
},
"clonePeople": {
"name": "clonePeople",
"label": "Clone People",
"tableName": "person",
"isHidden": false,
"iconName": "content_copy"
},
"simpleSleep": {
"name": "simpleSleep",
"label": "Simple Sleep",
"isHidden": true
},
"sleepInteractive": {
"name": "sleepInteractive",
"label": "Sleep Interactive",
"isHidden": false
},
"simpleThrow": {
"name": "simpleThrow",
"label": "Simple Throw",
"isHidden": false
},
"carrier.bulkInsert": {
"name": "carrier.bulkInsert",
"label": "Carrier Bulk Insert",
"tableName": "carrier",
"isHidden": true
},
"carrier.bulkEdit": {
"name": "carrier.bulkEdit",
"label": "Carrier Bulk Edit",
"tableName": "carrier",
"isHidden": true
},
"carrier.bulkDelete": {
"name": "carrier.bulkDelete",
"label": "Carrier Bulk Delete",
"tableName": "carrier",
"isHidden": true
},
"person.bulkInsert": {
"name": "person.bulkInsert",
"label": "Person Bulk Insert",
"tableName": "person",
"isHidden": true
},
"person.bulkEdit": {
"name": "person.bulkEdit",
"label": "Person Bulk Edit",
"tableName": "person",
"isHidden": true
},
"person.bulkDelete": {
"name": "person.bulkDelete",
"label": "Person Bulk Delete",
"tableName": "person",
"isHidden": true
},
"city.bulkInsert": {
"name": "city.bulkInsert",
"label": "Cities Bulk Insert",
"tableName": "city",
"isHidden": true
},
"city.bulkEdit": {
"name": "city.bulkEdit",
"label": "Cities Bulk Edit",
"tableName": "city",
"isHidden": true
},
"city.bulkDelete": {
"name": "city.bulkDelete",
"label": "Cities Bulk Delete",
"tableName": "city",
"isHidden": true
}
},
"apps": {
"greetingsApp": {
"name": "greetingsApp",
"label": "Greetings App",
"iconName": "emoji_people",
"widgets": [
"PersonsByCreateDateBarChart",
"QuickSightChartRenderer"
],
"children": [
{
"type": "PROCESS",
"name": "greet",
"label": "Greet People",
"iconName": "emoji_people"
},
{
"type": "TABLE",
"name": "person",
"label": "Person",
"iconName": "person"
},
{
"type": "TABLE",
"name": "city",
"label": "Cities",
"iconName": "location_city"
},
{
"type": "PROCESS",
"name": "greetInteractive",
"label": "Greet Interactive",
"iconName": "waving_hand"
}
],
"childMap": {
"greetInteractive": {
"type": "PROCESS",
"name": "greetInteractive",
"label": "Greet Interactive",
"iconName": "waving_hand"
},
"city": {
"type": "TABLE",
"name": "city",
"label": "Cities",
"iconName": "location_city"
},
"person": {
"type": "TABLE",
"name": "person",
"label": "Person",
"iconName": "person"
},
"greet": {
"type": "PROCESS",
"name": "greet",
"label": "Greet People",
"iconName": "emoji_people"
}
},
"sections": [
{
"name": "greetingsApp",
"label": "Greetings App",
"icon": {
"name": "badge"
},
"tables": [
"person",
"city"
],
"processes": [
"greet",
"greetInteractive"
]
}
]
},
"peopleApp": {
"name": "peopleApp",
"label": "People App",
"iconName": "person",
"widgets": [],
"children": [
{
"type": "APP",
"name": "greetingsApp",
"label": "Greetings App",
"iconName": "emoji_people"
},
{
"type": "PROCESS",
"name": "clonePeople",
"label": "Clone People",
"iconName": "content_copy"
}
],
"childMap": {
"greetingsApp": {
"type": "APP",
"name": "greetingsApp",
"label": "Greetings App",
"iconName": "emoji_people"
},
"clonePeople": {
"type": "PROCESS",
"name": "clonePeople",
"label": "Clone People",
"iconName": "content_copy"
}
},
"sections": [
{
"name": "peopleApp",
"label": "People App",
"icon": {
"name": "badge"
},
"processes": [
"clonePeople"
]
}
]
},
"miscellaneous": {
"name": "miscellaneous",
"label": "Miscellaneous",
"iconName": "stars",
"widgets": [],
"children": [
{
"type": "TABLE",
"name": "carrier",
"label": "Carrier",
"iconName": "local_shipping"
},
{
"type": "PROCESS",
"name": "simpleSleep",
"label": "Simple Sleep"
},
{
"type": "PROCESS",
"name": "sleepInteractive",
"label": "Sleep Interactive"
},
{
"type": "PROCESS",
"name": "simpleThrow",
"label": "Simple Throw"
}
],
"childMap": {
"carrier": {
"type": "TABLE",
"name": "carrier",
"label": "Carrier",
"iconName": "local_shipping"
},
"simpleSleep": {
"type": "PROCESS",
"name": "simpleSleep",
"label": "Simple Sleep"
},
"simpleThrow": {
"type": "PROCESS",
"name": "simpleThrow",
"label": "Simple Throw"
},
"sleepInteractive": {
"type": "PROCESS",
"name": "sleepInteractive",
"label": "Sleep Interactive"
}
},
"sections": [
{
"name": "miscellaneous",
"label": "Miscellaneous",
"icon": {
"name": "badge"
},
"tables": [
"carrier"
],
"processes": [
"simpleSleep",
"sleepInteractive",
"simpleThrow"
]
}
]
}
},
"appTree": [
{
"type": "APP",
"name": "peopleApp",
"label": "People App",
"children": [
{
"type": "APP",
"name": "greetingsApp",
"label": "Greetings App",
"children": [
{
"type": "PROCESS",
"name": "greet",
"label": "Greet People",
"iconName": "emoji_people"
},
{
"type": "TABLE",
"name": "person",
"label": "Person",
"iconName": "person"
},
{
"type": "TABLE",
"name": "city",
"label": "Cities",
"iconName": "location_city"
},
{
"type": "PROCESS",
"name": "greetInteractive",
"label": "Greet Interactive",
"iconName": "waving_hand"
}
],
"iconName": "emoji_people"
},
{
"type": "PROCESS",
"name": "clonePeople",
"label": "Clone People",
"iconName": "content_copy"
}
],
"iconName": "person"
},
{
"type": "APP",
"name": "miscellaneous",
"label": "Miscellaneous",
"children": [
{
"type": "TABLE",
"name": "carrier",
"label": "Carrier",
"iconName": "local_shipping"
},
{
"type": "PROCESS",
"name": "simpleSleep",
"label": "Simple Sleep"
},
{
"type": "PROCESS",
"name": "sleepInteractive",
"label": "Sleep Interactive"
},
{
"type": "PROCESS",
"name": "simpleThrow",
"label": "Simple Throw"
}
],
"iconName": "stars"
}
],
"branding": {
"logo": "/kr-logo.png",
"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",
"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"
}
},
"environmentValues": {
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
}
}

View File

@ -0,0 +1,176 @@
{
"process": {
"name": "person.bulkEdit",
"label": "Person Bulk Edit",
"tableName": "person",
"isHidden": true,
"frontendSteps": [
{
"name": "edit",
"label": "Edit Values",
"stepType": "frontend",
"components": [
{
"type": "HELP_TEXT",
"values": {
"text": "Flip the switches next to the fields that you want to edit.\nThe values you supply here will be updated in all of the records you are bulk editing.\nYou can clear out the value in a field by flipping the switch on for that field and leaving the input field blank.\nFields whose switches are off will not be updated."
}
},
{
"type": "BULK_EDIT_FORM"
}
],
"formFields": [
{
"name": "firstName",
"label": "First Name",
"backendName": "first_name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "lastName",
"label": "Last Name",
"backendName": "last_name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "birthDate",
"label": "Birth Date",
"backendName": "birth_date",
"type": "DATE",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "email",
"label": "Email",
"backendName": "email",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "isEmployed",
"label": "Is Employed",
"backendName": "is_employed",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "annualSalary",
"label": "Annual Salary",
"backendName": "annual_salary",
"type": "DECIMAL",
"isRequired": false,
"isEditable": true,
"displayFormat": "$%,.2f"
},
{
"name": "daysWorked",
"label": "Days Worked",
"backendName": "days_worked",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"displayFormat": "%,d"
}
]
},
{
"name": "review",
"label": "Review",
"stepType": "frontend",
"components": [
{
"type": "VALIDATION_REVIEW_SCREEN"
}
],
"recordListFields": [
{
"name": "firstName",
"label": "First Name",
"backendName": "first_name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "lastName",
"label": "Last Name",
"backendName": "last_name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "birthDate",
"label": "Birth Date",
"backendName": "birth_date",
"type": "DATE",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "email",
"label": "Email",
"backendName": "email",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "isEmployed",
"label": "Is Employed",
"backendName": "is_employed",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
{
"name": "annualSalary",
"label": "Annual Salary",
"backendName": "annual_salary",
"type": "DECIMAL",
"isRequired": false,
"isEditable": true,
"displayFormat": "$%,.2f"
},
{
"name": "daysWorked",
"label": "Days Worked",
"backendName": "days_worked",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"displayFormat": "%,d"
}
]
},
{
"name": "result",
"label": "Result",
"stepType": "frontend",
"components": [
{
"type": "PROCESS_SUMMARY_RESULTS"
}
]
}
]
}
}

View File

@ -0,0 +1,159 @@
{
"table": {
"name": "person",
"label": "Person",
"isHidden": false,
"primaryKeyField": "id",
"iconName": "person",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"fields": {
"firstName": {
"name": "firstName",
"label": "First Name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
"lastName": {
"name": "lastName",
"label": "Last Name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
"annualSalary": {
"name": "annualSalary",
"label": "Annual Salary",
"type": "DECIMAL",
"isRequired": false,
"isEditable": true,
"displayFormat": "$%,.2f"
},
"modifyDate": {
"name": "modifyDate",
"label": "Modify Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"daysWorked": {
"name": "daysWorked",
"label": "Days Worked",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"displayFormat": "%,d"
},
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"birthDate": {
"name": "birthDate",
"label": "Birth Date",
"type": "DATE",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"isEmployed": {
"name": "isEmployed",
"label": "Is Employed",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"email": {
"name": "email",
"label": "Email",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"createDate": {
"name": "createDate",
"label": "Create Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
}
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": [
"id",
"firstName",
"lastName"
],
"icon": {
"name": "badge"
},
"isHidden": false
},
{
"name": "basicInfo",
"label": "Basic Info",
"tier": "T2",
"fieldNames": [
"email",
"birthDate"
],
"icon": {
"name": "dataset"
},
"isHidden": false
},
{
"name": "employmentInfo",
"label": "Employment Info",
"tier": "T2",
"fieldNames": [
"isEmployed",
"annualSalary",
"daysWorked"
],
"icon": {
"name": "work"
},
"isHidden": false
},
{
"name": "dates",
"label": "Dates",
"tier": "T3",
"fieldNames": [
"createDate",
"modifyDate"
],
"icon": {
"name": "calendar_month"
},
"isHidden": false
}
],
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_DELETE",
"TABLE_INSERT",
"TABLE_UPDATE"
]
}
}

View File

@ -0,0 +1,9 @@
{
"values": {
"recordsParam": "recordIds",
"recordIds": "1,2,3,4,5",
"queryFilterJSON": "{\"criteria\":[{\"fieldName\":\"id\",\"operator\":\"IN\",\"values\":[\"1\",\"2\",\"3\",\"4\",\"5\"]}]}"
},
"processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43",
"nextStep": "edit"
}

View File

@ -0,0 +1,3 @@
[
{}
]

View File

@ -0,0 +1,12 @@
{
"values": {
"firstName": "Kahhhhn",
"valuesBeingUpdated": "First Name will be set to: Kahhhhn",
"bulkEditEnabledFields": "firstName",
"recordsParam": "recordIds",
"recordIds": "1,2,3,4,5",
"queryFilterJSON": "{\"criteria\":[{\"fieldName\":\"id\",\"operator\":\"IN\",\"values\":[\"1\",\"2\",\"3\",\"4\",\"5\"]}]}"
},
"processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43",
"nextStep": "review"
}

View File

@ -0,0 +1 @@
{}