mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 12:50:43 +00:00
Checkpoint - wip - selenium tests, pom project
This commit is contained in:
1
cypress/.gitignore
vendored
1
cypress/.gitignore
vendored
@ -1 +0,0 @@
|
||||
videos
|
@ -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
|
||||
|
||||
});
|
@ -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;
|
||||
}
|
@ -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});
|
@ -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')
|
@ -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
123
pom.xml
Executable 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
10
src/main/java/Placeholder.java
Executable file
@ -0,0 +1,10 @@
|
||||
/*******************************************************************************
|
||||
** Placeholder class, because maven really wants some source under src/main?
|
||||
*******************************************************************************/
|
||||
public class Placeholder
|
||||
{
|
||||
public void f()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
96
src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java
Executable file
96
src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java
Executable 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
404
src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java
Executable file
404
src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
80
src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java
Executable file
80
src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java
Executable 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();
|
||||
}
|
||||
|
||||
}
|
177
src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java
Executable file
177
src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java
Executable 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);
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -5,6 +5,10 @@
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"fields": {
|
||||
"firstName": {
|
||||
"name": "firstName",
|
Reference in New Issue
Block a user