Checkpoint - wip - selenium tests, pom project

This commit is contained in:
2023-01-27 19:00:38 -06:00
parent a343f76a3a
commit 010eb98d2f
29 changed files with 1416 additions and 256 deletions

View File

@ -0,0 +1,96 @@
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);
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.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);
}
}