From 010eb98d2ff3cbd7c58a6fa3860d023e3f277726 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 27 Jan 2023 19:00:38 -0600 Subject: [PATCH] Checkpoint - wip - selenium tests, pom project --- cypress/.gitignore | 1 - cypress/e2e/entity-list.spec.cy.ts | 107 ----- cypress/e2e/lib/qLib.ts | 86 ---- cypress/support/commands.ts | 41 -- cypress/support/e2e.ts | 20 - package.json | 1 - pom.xml | 123 ++++++ src/main/java/Placeholder.java | 10 + .../lib/QBaseSeleniumTest.java | 96 +++++ .../lib/QQQMaterialDashboardSelectors.java | 14 + .../materialdashbaord/lib/QSeleniumLib.java | 404 ++++++++++++++++++ .../lib/javalin/CapturedContext.java | 93 ++++ .../lib/javalin/CapturingHandler.java | 45 ++ .../lib/javalin/QSeleniumJavalin.java | 258 +++++++++++ .../lib/javalin/RouteFromFileHandler.java | 54 +++ .../lib/javalin/RouteFromStringHandler.java | 43 ++ .../tests/AppPageNavTest.java | 80 ++++ .../tests/QueryScreenTest.java | 177 ++++++++ .../resources}/fixtures/data/city/count.json | 0 .../fixtures/data/person/count.json | 0 .../fixtures/data/person/index.json | 0 .../fixtures/metaData/authentication.json | 0 .../resources}/fixtures/metaData/index.json | 15 + .../metaData/process/person.bulkEdit.json | 0 .../fixtures/metaData/table/person.json | 4 + .../processes/person.bulkEdit/init.json | 0 .../processes/person.bulkEdit/records.json | 0 .../processes/person.bulkEdit/step/edit.json | 0 .../resources}/fixtures/widget/empty.json | 0 29 files changed, 1416 insertions(+), 256 deletions(-) delete mode 100644 cypress/.gitignore delete mode 100644 cypress/e2e/entity-list.spec.cy.ts delete mode 100644 cypress/e2e/lib/qLib.ts delete mode 100644 cypress/support/commands.ts delete mode 100644 cypress/support/e2e.ts create mode 100755 pom.xml create mode 100755 src/main/java/Placeholder.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QQQMaterialDashboardSelectors.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QSeleniumLib.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturedContext.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/CapturingHandler.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/QSeleniumJavalin.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromFileHandler.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/lib/javalin/RouteFromStringHandler.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/tests/AppPageNavTest.java create mode 100755 src/test/java/com/kingsrook/qqq/materialdashbaord/tests/QueryScreenTest.java rename {cypress => src/test/resources}/fixtures/data/city/count.json (100%) rename {cypress => src/test/resources}/fixtures/data/person/count.json (100%) rename {cypress => src/test/resources}/fixtures/data/person/index.json (100%) rename {cypress => src/test/resources}/fixtures/metaData/authentication.json (100%) rename {cypress => src/test/resources}/fixtures/metaData/index.json (97%) rename {cypress => src/test/resources}/fixtures/metaData/process/person.bulkEdit.json (100%) rename {cypress => src/test/resources}/fixtures/metaData/table/person.json (97%) rename {cypress => src/test/resources}/fixtures/processes/person.bulkEdit/init.json (100%) rename {cypress => src/test/resources}/fixtures/processes/person.bulkEdit/records.json (100%) rename {cypress => src/test/resources}/fixtures/processes/person.bulkEdit/step/edit.json (100%) rename {cypress => src/test/resources}/fixtures/widget/empty.json (100%) 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 1f1ae80..2482177 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/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/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..bd949b4 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/materialdashbaord/lib/QBaseSeleniumTest.java @@ -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(); + } + } + +} 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..e430ad0 --- /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.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