diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..bcb3c64
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,113 @@
+version: 2.1
+
+orbs:
+ node: circleci/node@5.1.0
+ browser-tools: circleci/browser-tools@1.4.1
+
+executors:
+ java17:
+ docker:
+ - image: 'cimg/openjdk:17.0'
+
+commands:
+ install_java17:
+ steps:
+ - run:
+ name: Install Java 17
+ command: |
+ sudo apt-get update
+ sudo apt install -y openjdk-17-jdk
+ sudo rm /etc/alternatives/java
+ sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
+
+ install_npm:
+ steps:
+ - checkout
+ - node/install:
+ node-version: '16.13'
+ - node/install-packages
+
+ mvn_verify:
+ steps:
+ - browser-tools/install-chrome
+ - browser-tools/install-chromedriver
+ - run:
+ name: install dockerize
+ command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
+ environment:
+ DOCKERIZE_VERSION: v0.3.0
+ - run:
+ name: Install Browser dependencies
+ command: |
+ sudo apt update
+ sudo apt install -y libnss3-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxss-dev
+
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: Run react app and mvn verify
+ command: |
+ echo "HTTPS=true" >> ./.env
+ export REACT_APP_PROXY_LOCALHOST_PORT=8001; export PORT=3001; npm run start &
+ dockerize -wait tcp://localhost:3001 -timeout 3m
+ export QQQ_SELENIUM_HEADLESS=true; mvn verify
+ - run:
+ name: Save test results
+ command: |
+ mkdir -p ~/test-results/junit/
+ find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/test-results/junit/ \;
+ when: always
+ - store_test_results:
+ path: ~/test-results
+ - save_cache:
+ paths:
+ - ~/.m2
+ key: v1-dependencies-{{ checksum "pom.xml" }}
+
+ mvn_deploy:
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: Run NPM Build
+ command: |
+ npm run build
+ - run:
+ name: Build Maven Jar and Deploy
+ command: |
+ rm -rf src/main/resources/material-dashboard
+ mkdir -p src/main/resources/material-dashboard
+ cp -r build/* src/main/resources/material-dashboard
+ mvn -s .circleci/mvn-settings.xml deploy -DskipTests
+ - save_cache:
+ paths:
+ - ~/.m2
+ key: v1-dependencies-{{ checksum "pom.xml" }}
+
+jobs:
+ mvn_test:
+ executor: java17
+ steps:
+## - install_java17
+ - install_npm
+ - mvn_verify
+
+ mvn_deploy:
+ executor: java17
+ steps:
+ - install_npm
+ - mvn_deploy
+
+workflows:
+ test_only:
+ jobs:
+ - mvn_test:
+ context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
+ - mvn_deploy:
+ context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
+ requires:
+ - mvn_test
+
diff --git a/.circleci/mvn-settings.xml b/.circleci/mvn-settings.xml
new file mode 100644
index 0000000..b2a345f
--- /dev/null
+++ b/.circleci/mvn-settings.xml
@@ -0,0 +1,9 @@
+
+
+
+ github-qqq-maven-registry
+ ${env.QQQ_MAVEN_REGISTRY_USERNAME}
+ ${env.QQQ_MAVEN_REGISTRY_PASSWORD}
+
+
+
diff --git a/cypress/.gitignore b/cypress/.gitignore
deleted file mode 100644
index 4094344..0000000
--- a/cypress/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-videos
diff --git a/cypress/e2e/entity-list.spec.cy.ts b/cypress/e2e/entity-list.spec.cy.ts
deleted file mode 100644
index ea9ef76..0000000
--- a/cypress/e2e/entity-list.spec.cy.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-///
-
-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
-
-});
diff --git a/cypress/e2e/lib/qLib.ts b/cypress/e2e/lib/qLib.ts
deleted file mode 100644
index ccac102..0000000
--- a/cypress/e2e/lib/qLib.ts
+++ /dev/null
@@ -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 .
- */
-
-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;
-}
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
deleted file mode 100644
index 41cc226..0000000
--- a/cypress/support/commands.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-///
-// ***********************************************
-// 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
-// drag(subject: string, options?: Partial): Chainable
-// dismiss(subject: string, options?: Partial): Chainable
-// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
-// }
-// }
-// }
-
-import {registerCommand} from "cypress-wait-for-stable-dom";
-
-registerCommand({pollInterval: 100, timeout: 3000});
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
deleted file mode 100644
index f80f74f..0000000
--- a/cypress/support/e2e.ts
+++ /dev/null
@@ -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')
\ No newline at end of file
diff --git a/package.json b/package.json
index f6cfa6f..29d45f7 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pom.xml b/pom.xml
new file mode 100755
index 0000000..41e527c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,123 @@
+
+
+
+
+ 4.0.0
+
+ com.kingsrook.qqq
+ qqq-frontend-material-dashboard
+ 0.0.3-SNAPSHOT
+ jar
+
+
+ UTF-8
+ UTF-8
+ 17
+ 17
+ true
+ true
+
+
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.3
+ test
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ 4.7.1
+ test
+
+
+ io.github.bonigarcia
+ webdrivermanager
+ 5.3.1
+ test
+
+
+ commons-io
+ commons-io
+ 2.11.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.1
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.8.1
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.23.1
+ test
+
+
+ io.javalin
+ javalin
+ 5.1.4
+ test
+
+
+ org.json
+ json
+ 20220924
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+
+
+
+
+ github-qqq-maven-registry
+ GitHub QQQ Maven Registry
+ https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
+
+
+
+
+
+ github-qqq-maven-registry
+ GitHub QQQ Maven Registry
+ https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
+
+
+
+
diff --git a/public/apple-icon.png b/public/apple-icon.png
index c0fd815..06ade65 100644
Binary files a/public/apple-icon.png and b/public/apple-icon.png differ
diff --git a/public/carrier-logos/axlehire.png b/public/carrier-logos/axlehire.png
deleted file mode 100644
index 95035d4..0000000
Binary files a/public/carrier-logos/axlehire.png and /dev/null differ
diff --git a/public/carrier-logos/cdl.png b/public/carrier-logos/cdl.png
deleted file mode 100644
index 35d81b1..0000000
Binary files a/public/carrier-logos/cdl.png and /dev/null differ
diff --git a/public/carrier-logos/dhl.png b/public/carrier-logos/dhl.png
deleted file mode 100644
index b79e5ea..0000000
Binary files a/public/carrier-logos/dhl.png and /dev/null differ
diff --git a/public/carrier-logos/fedex.png b/public/carrier-logos/fedex.png
deleted file mode 100644
index 80a062d..0000000
Binary files a/public/carrier-logos/fedex.png and /dev/null differ
diff --git a/public/carrier-logos/lso.png b/public/carrier-logos/lso.png
deleted file mode 100644
index 4599837..0000000
Binary files a/public/carrier-logos/lso.png and /dev/null differ
diff --git a/public/carrier-logos/ontrac.png b/public/carrier-logos/ontrac.png
deleted file mode 100644
index 4a99c73..0000000
Binary files a/public/carrier-logos/ontrac.png and /dev/null differ
diff --git a/public/carrier-logos/ups.png b/public/carrier-logos/ups.png
deleted file mode 100644
index c70a64e..0000000
Binary files a/public/carrier-logos/ups.png and /dev/null differ
diff --git a/public/favicon.png b/public/favicon.png
index c0fd815..06ade65 100644
Binary files a/public/favicon.png and b/public/favicon.png differ
diff --git a/public/icon-blue.png b/public/icon-blue.png
deleted file mode 100644
index c0fd815..0000000
Binary files a/public/icon-blue.png and /dev/null differ
diff --git a/public/integration-logos/deposco.png b/public/integration-logos/deposco.png
deleted file mode 100644
index dfacdc6..0000000
Binary files a/public/integration-logos/deposco.png and /dev/null differ
diff --git a/public/integration-logos/easypost.png b/public/integration-logos/easypost.png
deleted file mode 100644
index 577a0c3..0000000
Binary files a/public/integration-logos/easypost.png and /dev/null differ
diff --git a/public/integration-logos/infoplus.png b/public/integration-logos/infoplus.png
deleted file mode 100644
index 72380d0..0000000
Binary files a/public/integration-logos/infoplus.png and /dev/null differ
diff --git a/public/integration-logos/shipstation.png b/public/integration-logos/shipstation.png
deleted file mode 100644
index 7eebd59..0000000
Binary files a/public/integration-logos/shipstation.png and /dev/null differ
diff --git a/public/nf-icon-blue.png b/public/nf-icon-blue.png
deleted file mode 100644
index c0fd815..0000000
Binary files a/public/nf-icon-blue.png and /dev/null differ
diff --git a/public/nf-logo-blue.png b/public/nf-logo-blue.png
deleted file mode 100644
index ad772a5..0000000
Binary files a/public/nf-logo-blue.png and /dev/null differ
diff --git a/public/ups.png b/public/ups.png
deleted file mode 100644
index 0d8046c..0000000
Binary files a/public/ups.png and /dev/null differ
diff --git a/public/warehouses/edison.jpg b/public/warehouses/edison.jpg
deleted file mode 100644
index c0030e3..0000000
Binary files a/public/warehouses/edison.jpg and /dev/null differ
diff --git a/public/warehouses/patterson.jpg b/public/warehouses/patterson.jpg
deleted file mode 100644
index e950c28..0000000
Binary files a/public/warehouses/patterson.jpg and /dev/null differ
diff --git a/public/warehouses/stockton.jpg b/public/warehouses/stockton.jpg
deleted file mode 100644
index a43da46..0000000
Binary files a/public/warehouses/stockton.jpg and /dev/null differ
diff --git a/src/main/java/Placeholder.java b/src/main/java/Placeholder.java
new file mode 100755
index 0000000..e3ca68b
--- /dev/null
+++ b/src/main/java/Placeholder.java
@@ -0,0 +1,10 @@
+/*******************************************************************************
+ ** Placeholder class, because maven really wants some source under src/main?
+ *******************************************************************************/
+public class Placeholder
+{
+ public void f()
+ {
+
+ }
+}
diff --git a/src/setupProxy.js b/src/setupProxy.js
index d6d896c..eaba430 100644
--- a/src/setupProxy.js
+++ b/src/setupProxy.js
@@ -29,19 +29,26 @@ const {createProxyMiddleware} = require("http-proxy-middleware");
module.exports = function (app)
{
- app.use(
- "/data/*/export/*",
- createProxyMiddleware({
- target: "http://localhost:8000",
- changeOrigin: true,
- }),
- );
+ let port = 8000;
+ if(process.env.REACT_APP_PROXY_LOCALHOST_PORT)
+ {
+ port = process.env.REACT_APP_PROXY_LOCALHOST_PORT;
+ }
- app.use(
- "/download/*",
- createProxyMiddleware({
- target: "http://localhost:8000",
+ function getRequestHandler()
+ {
+ return createProxyMiddleware({
+ target: `http://localhost:${port}`,
changeOrigin: true,
- }),
- );
+ });
+ }
+
+ app.use("/data/*/export/*", getRequestHandler());
+ app.use("/download/*", getRequestHandler());
+ app.use("/metaData/*", getRequestHandler());
+ app.use("/data/*", getRequestHandler());
+ app.use("/widget/*", getRequestHandler());
+ app.use("/serverInfo", getRequestHandler());
+ app.use("/processes", getRequestHandler());
+ app.use("/reports", getRequestHandler());
};
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java
new file mode 100755
index 0000000..45b0068
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java
@@ -0,0 +1,97 @@
+package com.kingsrook.qqq.materialdashbaord.lib;
+
+
+import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
+import io.github.bonigarcia.wdm.WebDriverManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+
+
+/*******************************************************************************
+ ** Base class for Selenium tests
+ *******************************************************************************/
+public class QBaseSeleniumTest
+{
+ private static ChromeOptions chromeOptions;
+
+ private WebDriver driver;
+ protected QSeleniumJavalin qSeleniumJavalin;
+ protected QSeleniumLib qSeleniumLib;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeAll
+ static void beforeAll()
+ {
+ chromeOptions = new ChromeOptions();
+ chromeOptions.setAcceptInsecureCerts(true);
+ chromeOptions.addArguments("--ignore-certificate-errors");
+
+ String headless = System.getenv("QQQ_SELENIUM_HEADLESS");
+ if("true".equals(headless))
+ {
+ chromeOptions.setHeadless(true);
+ }
+
+ WebDriverManager.chromiumdriver().setup();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ void beforeEach()
+ {
+ driver = new ChromeDriver(chromeOptions);
+ driver.manage().window().setSize(new Dimension(1600, 1200));
+ qSeleniumLib = new QSeleniumLib(driver);
+
+ qSeleniumJavalin = new QSeleniumJavalin();
+ addJavalinRoutes(qSeleniumJavalin);
+ qSeleniumJavalin.start();
+ }
+
+
+
+ /*******************************************************************************
+ ** meant for sub-classes to define their own javalin routes, if they need to
+ *******************************************************************************/
+ protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
+ {
+ qSeleniumJavalin
+ .withRouteToFile("/metaData", "metaData/index.json")
+ .withRouteToFile("/metaData/authentication", "metaData/authentication.json")
+ .withRouteToFile("/metaData/table/person", "metaData/table/person.json")
+ .withRouteToFile("/metaData/table/city", "metaData/table/person.json");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterEach
+ void afterEach()
+ {
+ if(driver != null)
+ {
+ driver.quit();
+ }
+
+ if(qSeleniumJavalin != null)
+ {
+ qSeleniumJavalin.stop();
+ }
+ }
+
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QQQMaterialDashboardSelectors.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QQQMaterialDashboardSelectors.java
new file mode 100755
index 0000000..8a10812
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QQQMaterialDashboardSelectors.java
@@ -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";
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java
new file mode 100755
index 0000000..6958a01
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java
@@ -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 waitForSelectorAll(String cssSelector, int minCount)
+ {
+ System.out.println("Waiting for element matching selector [" + cssSelector + "]");
+ long start = System.currentTimeMillis();
+
+ do
+ {
+ List 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
+ {
+ public T run();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public T waitLoop(String message, Code 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 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 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);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturedContext.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturedContext.java
new file mode 100755
index 0000000..1270c95
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturedContext.java
@@ -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;
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturingHandler.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturingHandler.java
new file mode 100755
index 0000000..210fff7
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturingHandler.java
@@ -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() + "]");
+ }
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/QSeleniumJavalin.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/QSeleniumJavalin.java
new file mode 100755
index 0000000..6ce9d91
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/QSeleniumJavalin.java
@@ -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> routesToFiles;
+ private List> 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 routeFilesServed = Collections.synchronizedList(new ArrayList<>());
+ List pathsThat404ed = Collections.synchronizedList(new ArrayList<>());
+
+ boolean capturing = false;
+ List 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 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 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 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 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);
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromFileHandler.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromFileHandler.java
new file mode 100755
index 0000000..3ebe24d
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromFileHandler.java
@@ -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 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 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 + "]");
+ }
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromStringHandler.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromStringHandler.java
new file mode 100755
index 0000000..30f8a01
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromStringHandler.java
@@ -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 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);
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java
new file mode 100755
index 0000000..be6fb2d
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java
@@ -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 .
+ */
+
+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();
+ }
+
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java b/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java
new file mode 100755
index 0000000..fb64dde
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java
@@ -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 .
+ */
+
+package com.kingsrook.qqq.materialdashbaord.tests;
+
+
+import com.kingsrook.qqq.materialdashbaord.lib.QBaseSeleniumTest;
+import com.kingsrook.qqq.materialdashbaord.lib.QQQMaterialDashboardSelectors;
+import com.kingsrook.qqq.materialdashbaord.lib.javalin.CapturedContext;
+import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.Select;
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/*******************************************************************************
+ ** Test for the record query screen
+ *******************************************************************************/
+public class QueryScreenTest extends QBaseSeleniumTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
+ {
+ super.addJavalinRoutes(qSeleniumJavalin);
+ qSeleniumJavalin
+ .withRouteToFile("/data/person/count", "data/person/count.json")
+ .withRouteToFile("/data/person/query", "data/person/index.json");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ // @RepeatedTest(10)
+ @Test
+ void testBasicQueryAndClearFilters()
+ {
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
+ qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
+ qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
+
+ /////////////////////////////////////////////////////////////////////
+ // open the filter window, enter a value, wait for query to re-run //
+ /////////////////////////////////////////////////////////////////////
+ WebElement filterInput = qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_FILTER_INPUT);
+ qSeleniumLib.assertElementHasFocus(filterInput);
+ qSeleniumJavalin.beginCapture();
+ filterInput.sendKeys("1");
+
+ ///////////////////////////////////////////////////////////////////
+ // assert that query & count both have the expected filter value //
+ ///////////////////////////////////////////////////////////////////
+ String idEquals1FilterSubstring = """
+ {"fieldName":"id","operator":"EQUALS","values":["1"]}""";
+ CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
+ CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
+ assertThat(capturedCount).extracting("body").asString().contains(idEquals1FilterSubstring);
+ assertThat(capturedQuery).extracting("body").asString().contains(idEquals1FilterSubstring);
+ qSeleniumJavalin.endCapture();
+
+ ///////////////////////////////////////
+ // click away from the filter window //
+ ///////////////////////////////////////
+ qSeleniumLib.waitForSeconds(1); // todo grr.
+ qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER).click();
+ qSeleniumLib.waitForSelectorContaining(".MuiBadge-root", "1");
+
+ ///////////////////////////////////////////////////////////////////
+ // click the 'x' clear icon, then yes, then expect another query //
+ ///////////////////////////////////////////////////////////////////
+ qSeleniumJavalin.beginCapture();
+ qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelector("#clearFiltersButton").click());
+ qSeleniumLib.waitForSelectorContaining("BUTTON", "Yes").click();
+
+ ////////////////////////////////////////////////////////////////////
+ // assert that query & count both no longer have the filter value //
+ ////////////////////////////////////////////////////////////////////
+ capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
+ capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
+ assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
+ assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
+ qSeleniumJavalin.endCapture();
+
+ qSeleniumLib.takeScreenshotToFile();
+ // qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ // @RepeatedTest(10)
+ @Test
+ void testMultiCriteriaQueryWithOr()
+ {
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
+ qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
+ qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
+
+ addQueryFilterInput(0, "First Name", "contains", "Dar", "Or");
+ qSeleniumJavalin.beginCapture();
+ addQueryFilterInput(1, "First Name", "contains", "Jam", "Or");
+
+ String expectedFilterContents0 = """
+ {"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
+ String expectedFilterContents1 = """
+ {"fieldName":"firstName","operator":"CONTAINS","values":["Jam"]}""";
+ String expectedFilterContents2 = """
+ "booleanOperator":"OR"}""";
+
+ qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents0);
+ qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
+ qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
+ qSeleniumJavalin.endCapture();
+
+ qSeleniumLib.takeScreenshotToFile();
+ // qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void addQueryFilterInput(int index, String fieldlabel, String operator, String value, String booleanOperator)
+ {
+ if(index > 0)
+ {
+ qSeleniumLib.waitForSelectorContaining("BUTTON", "Add filter").click();
+ }
+
+ WebElement subFormForField = qSeleniumLib.waitForSelectorAll(".MuiDataGrid-filterForm", index + 1).get(index);
+
+ if(index == 1)
+ {
+ Select linkOperatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormLinkOperatorInput SELECT")));
+ linkOperatorSelect.selectByVisibleText(booleanOperator);
+ }
+
+ Select fieldSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormColumnInput SELECT")));
+ fieldSelect.selectByVisibleText(fieldlabel);
+
+ Select operatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormOperatorInput SELECT")));
+ operatorSelect.selectByVisibleText(operator);
+
+ WebElement valueInput = subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormValueInput INPUT"));
+ valueInput.click();
+ valueInput.sendKeys(value);
+ }
+
+}
diff --git a/cypress/fixtures/data/city/count.json b/src/test/resources/fixtures/data/city/count.json
similarity index 100%
rename from cypress/fixtures/data/city/count.json
rename to src/test/resources/fixtures/data/city/count.json
diff --git a/cypress/fixtures/data/person/count.json b/src/test/resources/fixtures/data/person/count.json
similarity index 100%
rename from cypress/fixtures/data/person/count.json
rename to src/test/resources/fixtures/data/person/count.json
diff --git a/cypress/fixtures/data/person/index.json b/src/test/resources/fixtures/data/person/index.json
similarity index 100%
rename from cypress/fixtures/data/person/index.json
rename to src/test/resources/fixtures/data/person/index.json
diff --git a/cypress/fixtures/metaData/authentication.json b/src/test/resources/fixtures/metaData/authentication.json
similarity index 100%
rename from cypress/fixtures/metaData/authentication.json
rename to src/test/resources/fixtures/metaData/authentication.json
diff --git a/cypress/fixtures/metaData/index.json b/src/test/resources/fixtures/metaData/index.json
similarity index 97%
rename from cypress/fixtures/metaData/index.json
rename to src/test/resources/fixtures/metaData/index.json
index 0f8133d..947343b 100644
--- a/cypress/fixtures/metaData/index.json
+++ b/src/test/resources/fixtures/metaData/index.json
@@ -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"
}
}
diff --git a/cypress/fixtures/metaData/process/person.bulkEdit.json b/src/test/resources/fixtures/metaData/process/person.bulkEdit.json
similarity index 100%
rename from cypress/fixtures/metaData/process/person.bulkEdit.json
rename to src/test/resources/fixtures/metaData/process/person.bulkEdit.json
diff --git a/cypress/fixtures/metaData/table/person.json b/src/test/resources/fixtures/metaData/table/person.json
similarity index 97%
rename from cypress/fixtures/metaData/table/person.json
rename to src/test/resources/fixtures/metaData/table/person.json
index 5f69a3b..b72f440 100644
--- a/cypress/fixtures/metaData/table/person.json
+++ b/src/test/resources/fixtures/metaData/table/person.json
@@ -5,6 +5,10 @@
"isHidden": false,
"primaryKeyField": "id",
"iconName": "person",
+ "deletePermission": true,
+ "editPermission": true,
+ "insertPermission": true,
+ "readPermission": true,
"fields": {
"firstName": {
"name": "firstName",
diff --git a/cypress/fixtures/processes/person.bulkEdit/init.json b/src/test/resources/fixtures/processes/person.bulkEdit/init.json
similarity index 100%
rename from cypress/fixtures/processes/person.bulkEdit/init.json
rename to src/test/resources/fixtures/processes/person.bulkEdit/init.json
diff --git a/cypress/fixtures/processes/person.bulkEdit/records.json b/src/test/resources/fixtures/processes/person.bulkEdit/records.json
similarity index 100%
rename from cypress/fixtures/processes/person.bulkEdit/records.json
rename to src/test/resources/fixtures/processes/person.bulkEdit/records.json
diff --git a/cypress/fixtures/processes/person.bulkEdit/step/edit.json b/src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json
similarity index 100%
rename from cypress/fixtures/processes/person.bulkEdit/step/edit.json
rename to src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json
diff --git a/cypress/fixtures/widget/empty.json b/src/test/resources/fixtures/widget/empty.json
similarity index 100%
rename from cypress/fixtures/widget/empty.json
rename to src/test/resources/fixtures/widget/empty.json