diff --git a/pom.xml b/pom.xml index b87f70e..a4dfcc3 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,8 @@ UTF-8 11 11 + 2.35.0 + 1.15.0 @@ -46,6 +48,27 @@ + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + NONE + + + + + + + ${google-java-style.version} + + false + + + + maven-clean-plugin diff --git a/scripts/cldrHarStats.mjs b/scripts/cldrHarStats.mjs new file mode 100644 index 0000000..1b355aa --- /dev/null +++ b/scripts/cldrHarStats.mjs @@ -0,0 +1,130 @@ +import { readFileSync } from "fs"; + +const URL_PREFIX = "http://localhost:9080/cldr-apps/"; +const FILTER_REGEX = "api/voting/[a-zA-Z_]+/row"; + +/* + * Run with three args: the HAR file name, the start time, and the end time. For example: + * node scripts/cldrHarStats.mjs ../HAR/2024-01-12-a.har 2024-01-12T17:15:55Z 2024-01-12T17:18:55Z > ../HAR/2024-01-12-a.html + * + * Summary is written to stderr (console.warn); detailed HTML is written to stdout + */ +const harFileName = process.argv[2]; +const startTimeStamp = process.argv[3]; +const endTimeStamp = process.argv[4]; +/* + * Read the input HAR file into an array and filter it to include only entries + * matching FILTER_REGEX and within the given start/end times + */ +const obj = JSON.parse(readFileSync(harFileName, "utf8")); +const allEntries = obj.log.entries; +const filteredEntries = allEntries.filter(function (entry) { + return ( + entry.startedDateTime.localeCompare(startTimeStamp) >= 0 && + entry.startedDateTime.localeCompare(endTimeStamp) < 0 && + entry.request.url.match(FILTER_REGEX) + ); +}); +console.warn("allEntries.length = " + allEntries.length); +console.warn("filteredEntries.length = " + filteredEntries.length); + +writeHtmlThruTableStart(); +writeHtmlTableHeader(); +writeHtmlTableBody(filteredEntries); +writeHtmlFromTableEnd(); + +/** + * Write the HTML up to and including the opening table tag + */ +function writeHtmlThruTableStart() { + write(""); + write(""); + write( + '' + ); + write(""); + write(""); + write(""); + write(""); +} + +function writeHtmlFromTableEnd() { + write("
"); + write(""); + write(""); +} + +function writeHtmlTableHeader() { + const info = { + isHeader: true, + requestNumber: "request", + startedDateTime: "start", + time: "ms", + size: "size", + method: "method", + url: "URL", + postData: "POST data (if applicable)", + }; + putEntryInfo(info); +} + +function writeHtmlTableBody(entries) { + let postTime = 0, + getTime = 0, + postCount = 0, + getCount = 0; + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + let url = entry.request.url; + if (url.indexOf(URL_PREFIX) === 0) { + url = url.slice(URL_PREFIX.length); + } + if (entry.request.method === "POST") { + postCount++; + postTime += entry.time; + } else { + getCount++; + getTime += entry.time; + } + const postData = + entry.request.method === "POST" && entry.request?.postData?.text + ? entry.request.postData.text + : ""; + const info = { + isHeader: false, + requestNumber: i + 1, + startedDateTime: entry.startedDateTime, + time: Math.round(parseFloat(entry.time)), + size: Math.round(entry.response.content.size / 1000) + "k", + method: entry.request.method, + url: url, + postData: postData, + }; + putEntryInfo(info); + } + console.warn("Average POST time = " + postTime / postCount); + console.warn("Average GET time = " + getTime / getCount); +} + +function putEntryInfo(info) { + const cellStart = info.isHeader ? "" : ""; + const cellEnd = info.isHeader ? "" : ""; + write(""); + write(cellStart + info.requestNumber + cellEnd); + write(cellStart + info.startedDateTime + cellEnd); + write(cellStart + info.time + cellEnd); + write(cellStart + info.size + cellEnd); + write(cellStart + info.method + cellEnd); + write(cellStart + info.url + cellEnd); + write(cellStart + info.postData + cellEnd); + write(""); +} + +function write(string) { + process.stdout.write(string + "\n"); +} diff --git a/scripts/selenium-server-4.16.1.jar b/scripts/selenium-server-4.16.1.jar new file mode 100644 index 0000000..8f18c72 Binary files /dev/null and b/scripts/selenium-server-4.16.1.jar differ diff --git a/src/test/java/org/unicode/cldr/surveydriver/SurveyDriver.java b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriver.java index eea67db..118bb96 100644 --- a/src/test/java/org/unicode/cldr/surveydriver/SurveyDriver.java +++ b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriver.java @@ -30,28 +30,27 @@ /** * Perform automated testing of the CLDR Survey Tool using Selenium WebDriver. - *

- * This test has been used with the cldr-apps-webdriver project running in IntelliJ. At the same time, - * cldr-apps can be running either on localhost or on SmokeTest. - *

- * This code requires installing an implementation of WebDriver, such as chromedriver for Chrome. - * On macOS, chromedriver can be installed from Terminal with brew as follows: - * brew install chromedriver - * Then, right-click chromedriver, choose Open, and authorize to avoid the macOS error, - * "“chromedriver” cannot be opened because the developer cannot be verified". - * Press Ctrl+C to stop this instance of chromedriver. - *

- * (Testing with geckodriver for Firefox was unsuccessful, but has not been tried recently.) - *

- * Go to selenium.dev/downloads and scroll down - * to "Selenium Server (Grid)" and follow the link to download a file like selenium-server-4.16.1.jar - * and save it in the parent directory of cldr-apps-webdriver. - *

- * Start selenium grid: - *

- * sh cldr-apps-webdriver/scripts/selenium-grid-start.sh & - *

- * Open this file (SurveyDriver.java) in IntelliJ, right-click cldr-apps-webdriver in the Project + * + *

This test has been used with the cldr-apps-webdriver project running in IntelliJ. At the same + * time, cldr-apps can be running either on localhost or on SmokeTest. + * + *

This code requires installing an implementation of WebDriver, such as chromedriver for Chrome. + * On macOS, chromedriver can be installed from Terminal with brew as follows: brew install + * chromedriver Then, right-click chromedriver, choose Open, and authorize to avoid the macOS error, + * "“chromedriver” cannot be opened because the developer cannot be verified". Press Ctrl+C to stop + * this instance of chromedriver. + * + *

(Testing with geckodriver for Firefox was unsuccessful, but has not been tried recently.) + * + *

Go to selenium.dev/downloads and scroll down + * to "Selenium Server (Grid)" and follow the link to download a file like + * selenium-server-4.16.1.jar and save it in the parent directory of cldr-apps-webdriver. + * + *

Start selenium grid: + * + *

cd cldr-apps-webdriver/scripts; sh selenium-grid-start.sh + * + *

Open this file (SurveyDriver.java) in IntelliJ, right-click cldr-apps-webdriver in the Project * panel, and choose "Debug All Tests". You can do this repeatedly to start multiple browsers with * simulated vetters vetting at the same time. */ @@ -61,10 +60,11 @@ public class SurveyDriver { * Enable/disable specific tests using these booleans */ static final boolean TEST_VETTING_TABLE = false; - static final boolean TEST_FAST_VOTING = true; + static final boolean TEST_FAST_VOTING = false; static final boolean TEST_LOCALES_AND_PAGES = false; static final boolean TEST_ANNOTATION_VOTING = false; static final boolean TEST_XML_UPLOADER = false; + static final boolean TEST_DASHBOARD = true; /* * Configure for Survey Tool server, which can be localhost, cldr-smoke, cldr-staging, ... @@ -81,9 +81,7 @@ public class SurveyDriver { * the WebDriver interface). Otherwise, the driver could be a ChromeDriver, or FirefoxDriver, EdgeDriver, * or SafariDriver (all subclasses of RemoteWebDriver) if we add options for those. * While much of the code in this class works either way, Selenium Grid needs the driver to be a - * RemoteWebDriver and requires installation of a hub and one or more "slots", which can be done by - * - * sh scripts/selenium-grid-start.sh + * RemoteWebDriver and requires installation, see above comment about selenium-grid-start.sh */ static final boolean USE_REMOTE_WEBDRIVER = true; static final String REMOTE_WEBDRIVER_URL = "http://localhost:4444"; @@ -93,10 +91,9 @@ public class SurveyDriver { private SessionId sessionId = null; /** - * A nonnegative integer associated with a particular simulated user of Survey Tool, - * and with a particular instance of a browser in the selenium grid. The user's - * email address will be like "driver-123@cldr-apps-webdriver.org", where 123 - * would be the userIndex. + * A nonnegative integer associated with a particular simulated user of Survey Tool, and with a + * particular instance of a browser in the selenium grid. The user's email address will be like + * "driver-123@cldr-apps-webdriver.org", where 123 would be the userIndex. */ private int userIndex = 0; // possibly changed below, see getUserIndexFromGrid @@ -120,12 +117,13 @@ public static void runTests() { if (TEST_XML_UPLOADER) { assertTrue(new SurveyDriverXMLUploader(s).testXMLUploader()); } + if (TEST_DASHBOARD) { + assertTrue(new SurveyDriverDashboard(s).test()); + } s.tearDown(); } - /** - * Set up the driver and its "wait" object. - */ + /** Set up the driver and its "wait" object. */ private void setUp() { LoggingPreferences logPrefs = new LoggingPreferences(); logPrefs.enable(LogType.BROWSER, Level.ALL); @@ -151,9 +149,7 @@ private void setUp() { } } - /** - * Clean up when finished testing. - */ + /** Clean up when finished testing. */ private void tearDown() { SurveyDriverLog.println("cldr-apps-webdriver is quitting, goodbye from sessionId " + sessionId); if (driver != null) { @@ -171,14 +167,12 @@ private void tearDown() { } /** - * Test "fast" voting, that is, voting for several items on a page, and measuring - * the time of response. - *

- * Purposes: - * (1) study the sequence of events, especially client-server traffic, - * when multiple voting events (maybe by a single user) are being handled; - * (2) simulate simultaneous input from multiple vetters, for integration and - * performance testing under high load. + * Test "fast" voting, that is, voting for several items on a page, and measuring the time of + * response. + * + *

Purposes: (1) study the sequence of events, especially client-server traffic, when + * multiple voting events (maybe by a single user) are being handled; (2) simulate simultaneous + * input from multiple vetters, for integration and performance testing under high load. */ private boolean testFastVoting() { if (!login()) { @@ -435,9 +429,7 @@ private boolean testFastVotingInner(String page, String url) { return true; } - /** - * Log into Survey Tool. - */ + /** Log into Survey Tool. */ public boolean login() { final String url = BASE_URL; SurveyDriverLog.println("Logging in to " + url); @@ -484,7 +476,7 @@ private boolean loginWithButton(String url) { return clickButtonByXpath(loginXpath, url); } - private boolean clickButtonByXpath(String xpath, String url) { + public boolean clickButtonByXpath(String xpath, String url) { WebElement el = getClickableElementByXpath(xpath, url); if (el == null) { return false; @@ -519,12 +511,12 @@ private boolean inputTextByXpath(String xpath, String text, String url) { private WebElement getClickableElementByXpath(String xpath, String url) { try { - wait.until( - (ExpectedCondition) webDriver -> driver.findElement(By.xpath(xpath)) != null - ); + wait.until((ExpectedCondition) webDriver -> driver.findElement(By.xpath(xpath)) != null); } catch (Exception e) { SurveyDriverLog.println(e); - SurveyDriverLog.println("❌ Test failed, timed out waiting for element to be found by xpath " + xpath + " in url " + url); + SurveyDriverLog.println( + "❌ Test failed, timed out waiting for element to be found by xpath " + xpath + " in url " + url + ); return null; } WebElement el; @@ -534,7 +526,8 @@ private WebElement getClickableElementByXpath(String xpath, String url) { } catch (Exception e) { SurveyDriverLog.println(e); SurveyDriverLog.println( - "❌ Test failed, timed out waiting for " + xpath + " button to be clickable in " + url); + "❌ Test failed, timed out waiting for " + xpath + " button to be clickable in " + url + ); return null; } return el; @@ -576,9 +569,7 @@ private boolean chooseComprehensiveCoverage(String url) { return true; } - /** - * Test all the locales and pages we're interested in. - */ + /** Test all the locales and pages we're interested in. */ private boolean testAllLocalesAndPages() { String[] locales = SurveyDriverData.getLocales(); String[] pages = SurveyDriverData.getPages(); @@ -586,7 +577,8 @@ private boolean testAllLocalesAndPages() { * Reference: https://unicode.org/cldr/trac/ticket/11238 "browser console shows error message, * there is INHERITANCE_MARKER without inheritedValue" */ - String searchString = "INHERITANCE_MARKER without inheritedValue"; // formerly, "there is no Bailey Target item" + String searchString = "INHERITANCE_MARKER without inheritedValue"; // formerly, "there is no Bailey Target + // item" for (String loc : locales) { // for (PathHeader.PageId page : PathHeader.PageId.values()) { @@ -602,10 +594,8 @@ private boolean testAllLocalesAndPages() { /** * Test the given locale and page. * - * @param loc - * the locale string, like "pt_PT" - * @param page - * the page name, like "Alphabetic_Information" + * @param loc the locale string, like "pt_PT" + * @param page the page name, like "Alphabetic_Information" * @return true if all parts of the test pass, else false */ private boolean testOneLocationAndPage(String loc, String page, String searchString) { @@ -686,8 +676,7 @@ private boolean testAnnotationVoting() { /** * Count how many log entries contain the given string. * - * @param searchString - * the string for which to search + * @param searchString the string for which to search * @return the number of occurrences */ private int countLogEntriesContainingString(String searchString) { @@ -738,7 +727,12 @@ public boolean waitUntilLoadingMessageDone(String url) { String loadingId = "LoadingMessageSection"; try { wait.until( - (ExpectedCondition) webDriver -> Objects.requireNonNull(webDriver).findElement(By.id(loadingId)).getCssValue("display").contains("none") + (ExpectedCondition) webDriver -> + Objects + .requireNonNull(webDriver) + .findElement(By.id(loadingId)) + .getCssValue("display") + .contains("none") ); } catch (Exception e) { SurveyDriverLog.println(e); @@ -749,8 +743,8 @@ public boolean waitUntilLoadingMessageDone(String url) { } /** - * Hide the element whose id is "left-sidebar", by simulating the appropriate mouse action if it's - * visible. + * Hide the element whose id is "left-sidebar", by simulating the appropriate mouse action if + * it's visible. * * @param url the url we're loading * @return true for success, false for failure @@ -872,6 +866,29 @@ public WebElement waitInputBoxAppears(WebElement rowEl, String url) { return inputEl; } + /** + * Wait until the element with the given class name is clickable, then click it. + * + * @param className the class name + * @param url the url we're loading + * @return true for success, false for failure + */ public boolean clickButtonByClassName(String className, String url) { + WebElement button = driver.findElement(By.className(className)); + if (!waitUntilElementClickable(button, url)) { + return false; + } + try { + button.click(); + } catch (Exception e) { + SurveyDriverLog.println(e); + SurveyDriverLog.println( + "❌ Test failed, maybe timed out, waiting button with class " + className + " to be clickable in " + url + ); + return false; + } + return true; + } + /** * Wait until the element is clickable. * @@ -1013,7 +1030,8 @@ public void clickOnRowCellTagElement( * Wait until an element with class with the given name exists, or wait until one doesn't. * * @param className the class name - * @param checking true to wait until such an element exists, or false to wait until no such element exists + * @param checking true to wait until such an element exists, or false to wait until no such + * element exists * @param url the url we're loading * @return true for success, false for failure */ @@ -1043,7 +1061,8 @@ public boolean waitUntilClassExists(String className, boolean checking, String u * Wait until an element with id with the given name exists, or wait until one doesn't. * * @param idName the id name - * @param checking true to wait until such an element exists, or false to wait until no such element exists + * @param checking true to wait until such an element exists, or false to wait until no such + * element exists * @param url the url we're loading * @return true for success, false for failure */ @@ -1070,34 +1089,34 @@ public boolean waitUntilIdExists(String idName, boolean checking, String url) { } /** - * Supposing there are n slots in the selenium grid, get a number - * in the range [0, ..., n - 1], representing the particular - * slot we are using. This number will be used as the "user index" - * identifying a unique Survey Tool simulated user, with a fictitious - * email address like "driver-123@cldr-apps-webdriver.org", where - * 123 would be the user index. + * Supposing there are n slots in the selenium grid, get a number in the range [0, ..., n - 1], + * representing the particular slot we are using. This number will be used as the "user index" + * identifying a unique Survey Tool simulated user, with a fictitious email address like + * "driver-123@cldr-apps-webdriver.org", where 123 would be the user index. * * @param sessionId the session id associated with our slot - * * @return the user index, a nonnegative integer */ private int getUserIndexFromGrid(SessionId sessionId) { String url = REMOTE_WEBDRIVER_URL + "/status"; // http://localhost:4444/status driver.get(url); String jsonString = driver.findElement(By.tagName("body")).getText(); - SurveyDriverLog.println("jsonString = " + jsonString); + // SurveyDriverLog.println("jsonString = " + jsonString); // Ideally, at this point we could convert the json into a NodeStatus object // NodeStatus nodeStatus = NodeStatus.fromJson(jsonString); - // https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/grid/data/NodeStatus.html + // + // https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/grid/data/NodeStatus.html // Unfortunately it's not clear how to do that... // Another way would be with gson: - // SurveyDriverTestSession obj = new Gson().fromJson(jsonString, SurveyDriverNodeStatus.class); + // SurveyDriverTestSession obj = new Gson().fromJson(jsonString, + // SurveyDriverNodeStatus.class); // -- with our own imitation of NodeStatus, but that seems too difficult and error-prone. // Another way would be to forget about NodeStatus, and simply assign a different user index // each time we launch SurveyDriver, using our own mechanism independent of the grid... - // Crude work-around: each slot has "lastStarted" (1970 if never), sometimes followed by sessionId. + // Crude work-around: each slot has "lastStarted" (1970 if never), sometimes followed by + // sessionId. // For example: // "lastStarted": "2023-12-21T03:24:37.017561Z", // "sessionId": "19049a348716f4dd825e330c47787573", diff --git a/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverDashboard.java b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverDashboard.java new file mode 100644 index 0000000..5deeabc --- /dev/null +++ b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverDashboard.java @@ -0,0 +1,66 @@ +package org.unicode.cldr.surveydriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +public class SurveyDriverDashboard { + + private final SurveyDriver s; + private final WebDriver driver; + + public SurveyDriverDashboard(SurveyDriver s) { + this.s = s; + this.driver = s.driver; + } + + private static final String[] locales = { + // Czech, German, Spanish, French, Hindi, Japanese, Russian, Chinese + "cs", "de", "es", "fr", "hi", "ja", "ru", "zh" + }; + + /** Test the Dashboard interface */ + public boolean test() { + if (!s.login()) { + return false; + } + final int REPETITION_COUNT = 10000; + for (int i = 0; i < REPETITION_COUNT; i++) { + SurveyDriverLog.println("SurveyDriverDashboard.test i = " + i); + if (!testOne(i)) { + return false; + } + } + SurveyDriverLog.println("✅ Dashboard test passed"); + return true; + } + + private boolean testOne(int i) { + String loc = locales[i % locales.length]; + String url = SurveyDriver.BASE_URL + "v#/" + loc + "//"; + driver.get(url); + if (!s.hideLeftSidebar(url)) { + return false; + } + if (!s.waitUntilElementInactive("left-sidebar", url)) { + return false; + } + if (!s.waitUntilElementInactive("overlay", url)) { + return false; + } + // If we're on a locale's "General Info" page (rather than a specific + // page such as "Alphabetic_Information"), then the "Open Dashboard" + // button is from GeneralInfo.vue, and its class includes "general-open-dash". + // There are other "Open Dashboard" buttons produced in cldrGui.mjs, and a + // "Dashboard" item in the left-sidebar. + // For unknown reasons, clickButtonByXpath with "//button[contains(., 'Open Dashboard')]" fails here. + if (!s.clickButtonByClassName("general-open-dash", url)) { + return false; + } + if (!s.waitUntilIdExists("DashboardScroller", true, url)) { + return false; + } + SurveyDriverLog.println("✅ Dashboard: tested locale " + loc); + return true; + } +} diff --git a/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverXMLUploader.java b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverXMLUploader.java index 7042db0..8da6177 100644 --- a/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverXMLUploader.java +++ b/src/test/java/org/unicode/cldr/surveydriver/SurveyDriverXMLUploader.java @@ -14,9 +14,7 @@ public SurveyDriverXMLUploader(SurveyDriver s) { this.s = s; } - /** - * Test the XMLUploader interface ("Upload XML" in the gear menu). - */ + /** Test the XMLUploader interface ("Upload XML" in the gear menu). */ public boolean testXMLUploader() { if (!s.login()) { return false; @@ -39,6 +37,14 @@ public boolean testXMLUploader() { return false; } } while (!clickOnUploadXMLElement(url)); + + // The interface has changed. + // There is still a separate tab for bulk upload, but now there's a new step, choosing + // between these options: + // 1. Convert XLSX to XML + // 2. Upload XML as your vote (Bulk Upload) + SurveyDriverLog.println("❌ XML-Upload test needs revision for changed interface in Survey Tool!"); + switchToNewTabOrWindow(); if (!specifyXmlFileToUpload(url)) { @@ -50,8 +56,8 @@ public boolean testXMLUploader() { } /** - * After new tab or window is created, switch WebDriver to it. - * Otherwise, our actions would still operate on the old window. + * After new tab or window is created, switch WebDriver to it. Otherwise, our actions would + * still operate on the old window. */ private void switchToNewTabOrWindow() { WebDriver driver = s.driver; @@ -81,7 +87,7 @@ private void switchToNewTabOrWindow() { } /** - * Click on the gear menu. + * Click on the main menu. * * @param url the url we're loading * @return true for success, false for failure @@ -120,13 +126,13 @@ private boolean clickOnMainMenu(String url) { } /** - * Click on the "Upload XML" item in the gear menu. + * Click on the "Upload (Bulk Import)" item in the gear menu. * * @param url the url we're loading * @return true for success, false for failure */ private boolean clickOnUploadXMLElement(String url) { - String linkText = "Upload XML"; + String linkText = "Upload (Bulk Import)"; WebElement clickEl = null; try { clickEl = s.driver.findElement(By.partialLinkText(linkText));