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

1
cypress/.gitignore vendored
View File

@ -1 +0,0 @@
videos

View File

@ -1,107 +0,0 @@
/// <reference types="cypress-wait-for-stable-dom" />
import QLib from "./lib/qLib";
describe("table query screen", () =>
{
before(() =>
{
QLib.init(cy);
cy.intercept("GET", "/metaData/authentication", {fixture: "metaData/authentication.json"}).as("authenticationMetaData");
cy.intercept("GET", "/metaData", {fixture: "metaData/index.json"}).as("metaData");
cy.intercept("GET", "/metaData/table/person", {fixture: "metaData/table/person.json"}).as("personMetaData");
cy.intercept("POST", "/data/person/query?*", {fixture: "data/person/index.json"}).as("personQuery");
cy.intercept("POST", "/data/person/count", {fixture: "data/person/count.json"}).as("personCount");
cy.intercept("POST", "/data/city/count", {fixture: "data/city/count.json"}).as("cityCount");
cy.intercept("GET", "/metaData/process/person.bulkEdit", {fixture: "metaData/process/person.bulkEdit.json"}).as("personBulkEditMetaData");
cy.intercept("POST", "/processes/person.bulkEdit/init?recordsParam=recordIds&recordIds=1,2,3,4,5", {fixture: "processes/person.bulkEdit/init.json"}).as("personBulkEditInit");
cy.intercept("POST", "/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/step/edit", {fixture: "processes/person.bulkEdit/step/edit.json"}).as("personBulkEditStepEdit");
cy.intercept("GET", "/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records?skip=0&limit=10", {fixture: "processes/person.bulkEdit/records.json"}).as("personBulkEditRecords");
cy.intercept("GET", "/widget/* ", {fixture: "widget/empty.json"}).as("emptyWidget");
});
it.skip("can be loaded from app screen", () =>
{
cy.visit("https://localhost:3000/peopleApp/greetingsApp/");
cy.contains("Person").click();
cy.location().should((loc) =>
{
expect(loc.pathname).to.eq("/peopleApp/greetingsApp/person");
});
});
it.skip("can add query filters", () =>
{
/////////////////////////////////////////////////////////////
// go to table, wait for filter to run, and rows to appear //
/////////////////////////////////////////////////////////////
cy.visit("https://localhost:3000/peopleApp/greetingsApp/person");
QLib.waitForQueryScreen();
/////////////////////////////////////////////////////////////////////
// open the filter window, enter a value, wait for query to re-run //
/////////////////////////////////////////////////////////////////////
cy.contains("Filters").click();
cy.get(".MuiDataGrid-filterForm input.MuiInput-input").should("be.focused").type("1");
///////////////////////////////////////////////////////////////////
// assert that query & count both have the expected filter value //
///////////////////////////////////////////////////////////////////
let expectedFilterContents = JSON.stringify({fieldName: "id", operator: "EQUALS", values: ["1"]});
cy.wait("@personQuery").its("request.body").should((body) => expect(body).to.contain(expectedFilterContents));
cy.wait("@personCount").its("request.body").should((body) => expect(body).to.contain(expectedFilterContents));
///////////////////////////////////////
// click away from the filter window //
///////////////////////////////////////
cy.get("#root").click("topLeft", {force: true});
cy.contains(".MuiBadge-root", "1").should("be.visible");
///////////////////////////////////////////////////////////////////
// click the 'x' clear icon, then yes, then expect another query //
///////////////////////////////////////////////////////////////////
cy.waitForStableDOM();
cy.get("#clearFiltersButton").should("be.visible").click();
cy.contains("button", "Yes").click();
////////////////////////////////////////////////////////////////////
// assert that query & count both no longer have the filter value //
////////////////////////////////////////////////////////////////////
cy.wait("@personQuery").its("request.body").should((body) => expect(body).not.to.contain(expectedFilterContents));
cy.wait("@personCount").its("request.body").should((body) => expect(body).not.to.contain(expectedFilterContents));
cy.contains(".MuiDataGrid-toolbarContainer .MuiBadge-root", "1").should("not.exist");
});
it.skip("can do a boolean or query", () => // :( failed in CI
{
/////////////////////////////////////////
// go to table, wait for filter to run //
/////////////////////////////////////////
cy.visit("https://localhost:3000/peopleApp/greetingsApp/person");
QLib.waitForQueryScreen();
QLib.buildEntityListQueryFilter([
{fieldLabel: "First Name", operator: "contains", textValue: "Dar"},
{fieldLabel: "First Name", operator: "contains", textValue: "Jam"}
], "or");
let expectedFilterContents0 = JSON.stringify({fieldName: "firstName", operator: "CONTAINS", values: ["Dar"]});
let expectedFilterContents1 = JSON.stringify({fieldName: "firstName", operator: "CONTAINS", values: ["Jam"]});
cy.wait("@personQuery").its("request.body").should((body) =>
{
expect(body).to.contain(expectedFilterContents0);
expect(body).to.contain(expectedFilterContents1);
expect(body).to.contain("asdf");
});
});
// tests to add:
// - sort column
// - all field types and operators
// - pagination, page size
// - check marks, select all
// - column chooser
});

View File

@ -1,86 +0,0 @@
/*
* 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/>.
*/
export default class QLib
{
// @ts-ignore
private static cy: Cypress.cy;
// @ts-ignore
public static init(cy: Cypress.cy)
{
QLib.cy = cy;
}
/*******************************************************************************
** Wait for a query to finish on the entity-list screen. specifically, wait for
** personQuery & personCount requests, and wait for the data grid to have rows.
*******************************************************************************/
public static waitForQueryScreen()
{
QLib.cy.wait(["@personQuery", "@personCount"]);
QLib.cy.get(".MuiDataGrid-virtualScrollerRenderZone").children().should("have.length.greaterThan", 3);
}
/*******************************************************************************
** Open the Filters drop down, and build a query
*******************************************************************************/
public static buildEntityListQueryFilter(input: QueryFilterInput | QueryFilterInput[], booleanOperator: ("and" | "or") = "and")
{
QLib.cy.contains("Filters").click();
if ((input as QueryFilterInput).fieldLabel)
{
const queryFilterInput = input as QueryFilterInput;
QLib.addSingleQueryFilterInput(queryFilterInput, 0, booleanOperator);
}
else
{
const inputArray = input as QueryFilterInput[];
inputArray.forEach((qfi, index) => QLib.addSingleQueryFilterInput(qfi, index, booleanOperator));
}
}
/*******************************************************************************
** private helper - adds 1 query filter input.
*******************************************************************************/
private static addSingleQueryFilterInput(queryFilterInput: QueryFilterInput, index: number, booleanOperator: ("and" | "or"))
{
if (index > 0)
{
QLib.cy.contains("Add filter").click();
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormLinkOperatorInput SELECT").select(booleanOperator);
}
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormColumnInput SELECT").select(queryFilterInput.fieldLabel);
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormOperatorInput SELECT").select(queryFilterInput.operator);
QLib.cy.get(".MuiDataGrid-filterForm").eq(index).find(".MuiDataGrid-filterFormValueInput INPUT").type(queryFilterInput.textValue);
}
}
interface QueryFilterInput
{
fieldLabel?: string;
fieldName?: string;
operator?: string;
textValue?: string;
}

View File

@ -1,41 +0,0 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
import {registerCommand} from "cypress-wait-for-stable-dom";
registerCommand({pollInterval: 100, timeout: 3000});

View File

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -2,7 +2,6 @@
"name": "qqq-frontend-material-dashboard",
"version": "1.0.0",
"description": "QQQ Default Dashboard",
"proxy": "http://localhost:8000",
"dependencies": {
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",

123
pom.xml Executable file
View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-material-dashboard</artifactId>
<version>0.0.3-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>5.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220924</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</distributionManagement>
</project>

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

@ -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);
}
}

View File

@ -5,6 +5,10 @@
"label": "Carrier",
"isHidden": false,
"iconName": "local_shipping",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
@ -19,6 +23,10 @@
"label": "Person",
"isHidden": false,
"iconName": "person",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
@ -33,6 +41,10 @@
"label": "Cities",
"isHidden": true,
"iconName": "location_city",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
@ -546,5 +558,8 @@
"label": "Quick Sight",
"type": "quickSightChart"
}
},
"environmentValues": {
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
}
}

View File

@ -5,6 +5,10 @@
"isHidden": false,
"primaryKeyField": "id",
"iconName": "person",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"fields": {
"firstName": {
"name": "firstName",