From 31840a9bd29d57149852df3d2a8fd5c3e844477c Mon Sep 17 00:00:00 2001
From: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com>
Date: Fri, 6 Dec 2024 16:37:11 -0500
Subject: [PATCH] HPCC-33066 add more ECL Watch v9 UI tests

add a few more ECL Watch v9 playwright tests, mostly on the WU, File
and Query list pages, as well as a few on the playground for creating
WUs, files, and queries.

Signed-off-by: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com>
---
 .github/workflows/build-test-eclwatch.yml     |   8 +-
 esp/src/eclwatch/ECLPlaygroundWidget.js       |   2 +-
 esp/src/playwright.config.ts                  |  27 +++-
 .../src-react/components/ECLPlayground.tsx    |   2 +-
 esp/src/src-react/components/forms/Fields.tsx |   1 +
 esp/src/tests/eclwatch-v9.spec.ts             | 147 ++++++++++++++++--
 esp/src/tests/global.setup.ts                 | 145 +++++++++++++++++
 esp/src/tests/global.teardown.ts              |  26 ++++
 8 files changed, 338 insertions(+), 20 deletions(-)
 create mode 100644 esp/src/tests/global.setup.ts
 create mode 100644 esp/src/tests/global.teardown.ts

diff --git a/.github/workflows/build-test-eclwatch.yml b/.github/workflows/build-test-eclwatch.yml
index ff4d53c36c1..aff7a59336f 100644
--- a/.github/workflows/build-test-eclwatch.yml
+++ b/.github/workflows/build-test-eclwatch.yml
@@ -51,7 +51,7 @@ jobs:
       - name: Lint
         working-directory: ./esp/src
         run: npm run lint
-      - name: Install Playwright browsers 
+      - name: Install Playwright browsers
         working-directory: ./esp/src
         run: npx playwright install --with-deps
       - name: Build
@@ -60,3 +60,9 @@ jobs:
       - name: Test
         working-directory: ./esp/src
         run: npm run test
+      - name: Upload Playwright test results
+        if: ${{ failure() }}
+        uses: actions/upload-artifact@v4
+        with:
+          name: eclwatch-test-results
+          path: ./esp/src/test-results/*
diff --git a/esp/src/eclwatch/ECLPlaygroundWidget.js b/esp/src/eclwatch/ECLPlaygroundWidget.js
index 2b652d96a2d..7637ab3a0d9 100644
--- a/esp/src/eclwatch/ECLPlaygroundWidget.js
+++ b/esp/src/eclwatch/ECLPlaygroundWidget.js
@@ -111,7 +111,7 @@ define([
                 var logicalCluster = context.targetSelectWidget.selectedTarget();
                 var submitBtn = registry.byId(context.id + "SubmitBtn");
                 var publishBtn = registry.byId(context.id + "PublishBtn");
-                if (logicalCluster.QueriesOnly) {
+                if (logicalCluster.QueriesOnly || logicalCluster.Type === "roxie") {
                     domStyle.set(submitBtn.domNode, "display", "none");
                     domStyle.set(publishBtn.domNode, "display", null);
                 } else {
diff --git a/esp/src/playwright.config.ts b/esp/src/playwright.config.ts
index 00031962989..e4fe3f588e4 100644
--- a/esp/src/playwright.config.ts
+++ b/esp/src/playwright.config.ts
@@ -1,6 +1,6 @@
 import { defineConfig, devices } from "@playwright/test";
 
-const baseURL = process.env.CI ? "https://play.hpccsystems.com:18010" : "http://127.0.0.1:8080";
+export const baseURL = process.env.CI ? "https://play.hpccsystems.com:18010" : "http://127.0.0.1:8080";
 
 /**
  * See https://playwright.dev/docs/test-configuration.
@@ -11,28 +11,43 @@ export default defineConfig({
     forbidOnly: !!process.env.CI,
     retries: process.env.CI ? 2 : 0,
     workers: process.env.CI ? 4 : undefined,
+    timeout: 60_000,
+    expect: {
+        timeout: 30_000
+    },
     reporter: "html",
     use: {
         baseURL,
         trace: "on-first-retry",
+        screenshot: "on-first-failure",
         ignoreHTTPSErrors: true
     },
 
     projects: [
+        {
+            name: "setup",
+            testMatch: /global\.setup\.ts/,
+            teardown: "teardown"
+        },
         {
             name: "chromium",
-            use: { ...devices["Desktop Chrome"] },
+            use: devices["Desktop Chrome"],
+            dependencies: ["setup"]
         },
-
         {
             name: "firefox",
-            use: { ...devices["Desktop Firefox"] },
+            use: devices["Desktop Firefox"],
+            dependencies: ["setup"]
         },
-
         {
             name: "webkit",
-            use: { ...devices["Desktop Safari"] },
+            use: devices["Desktop Safari"],
+            dependencies: ["setup"]
         },
+        {
+            name: "teardown",
+            testMatch: /global\.teardown\.ts/
+        }
 
     ],
 
diff --git a/esp/src/src-react/components/ECLPlayground.tsx b/esp/src/src-react/components/ECLPlayground.tsx
index ceca0b9eed9..1a14e4f8209 100644
--- a/esp/src/src-react/components/ECLPlayground.tsx
+++ b/esp/src/src-react/components/ECLPlayground.tsx
@@ -346,7 +346,7 @@ const ECLEditorToolbar: React.FunctionComponent<ECLEditorToolbarProps> = ({
                 className={playgroundStyles.inlineDropdown}
                 onChange={React.useCallback((evt, option: TargetClusterOption) => {
                     const selectedCluster = option.key.toString();
-                    if (option?.queriesOnly) {
+                    if (option?.queriesOnly || option?.type === "roxie") {
                         setShowSubmitBtn(false);
                     } else {
                         setShowSubmitBtn(true);
diff --git a/esp/src/src-react/components/forms/Fields.tsx b/esp/src/src-react/components/forms/Fields.tsx
index 81e453ab52c..50f8964302e 100644
--- a/esp/src/src-react/components/forms/Fields.tsx
+++ b/esp/src/src-react/components/forms/Fields.tsx
@@ -459,6 +459,7 @@ export interface TargetClusterTextFieldProps extends Omit<AsyncDropdownProps, "o
 }
 
 export interface TargetClusterOption extends IDropdownOption {
+    type: string;
     queriesOnly: boolean;
 }
 
diff --git a/esp/src/tests/eclwatch-v9.spec.ts b/esp/src/tests/eclwatch-v9.spec.ts
index 745a274254d..1becb25883d 100644
--- a/esp/src/tests/eclwatch-v9.spec.ts
+++ b/esp/src/tests/eclwatch-v9.spec.ts
@@ -1,9 +1,12 @@
 import { test, expect } from "@playwright/test";
 
-test.describe("ECLWatch V9", () => {
+test.describe("Basic ECLWatch V9 UI", () => {
 
-    test("Basic Frame", async ({ page }) => {
+    test.beforeEach(async ({ page }) => {
         await page.goto("/esp/files/index.html#/activities");
+    })
+
+    test("Frame Loaded", async ({ page }) => {
         await expect(page.getByRole("link", { name: "ECL Watch" })).toBeVisible();
         await expect(page.locator("button").filter({ hasText: "" })).toBeVisible();
         await expect(page.getByRole("button", { name: "Advanced" })).toBeVisible();
@@ -17,9 +20,7 @@ test.describe("ECLWatch V9", () => {
         await expect(page.getByRole("link", { name: "Event Scheduler" })).toBeVisible();
     });
 
-    test("Activities", async ({ page }) => {
-        await page.goto("/esp/files/index.html#/activities");
-        await page.getByTitle("Disk Usage").locator("i").click();
+    test("Activities page", async ({ page }) => {
         await expect(page.locator("svg").filter({ hasText: "%hthor" })).toBeVisible();
         await expect(page.locator(".reflex-splitter")).toBeVisible();
         await expect(page.getByRole("menubar")).toBeVisible();
@@ -32,11 +33,135 @@ test.describe("ECLWatch V9", () => {
         await expect(page.getByText("State")).toBeVisible();
         await expect(page.getByText("Owner")).toBeVisible();
         await expect(page.getByText("Job Name")).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "HThorServer - hthor" })).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "ThorMaster - thor", exact: true })).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "ThorMaster - thor_roxie" })).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "RoxieServer - roxie" })).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "myeclccserver - hthor." })).toBeVisible();
-        await expect(page.getByRole("gridcell", { name: "mydfuserver - dfuserver_queue" })).toBeVisible();
+        await expect(page.locator(".dgrid-row")).not.toHaveCount(0);
+    });
+});
+
+test.describe("Workunit tests", () => {
+
+    test.beforeEach(async ({ page }) => {
+        await page.goto("/esp/files/index.html#/workunits");
+    });
+
+    test("View the Workunits list page", async ({ page }) => {
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await expect(page.getByText("WUID")).toBeVisible();
+        await expect(page.getByText("Owner", { exact: true })).toBeVisible();
+        await expect(page.getByText("Job Name")).toBeVisible();
+        await expect(page.getByText("Cluster", { exact: true })).toBeVisible();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    });
+
+    test("Filter the Workunits list page", async ({ page }) => {
+        const date = new Date();
+        const month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
+        const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        const wuidField = await page.getByPlaceholder("W20200824-060035");
+        wuidField.fill(`W${date.getFullYear()}${month}${day}*`);
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByPlaceholder("W20200824-060035").fill(`W2023*`);
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).toHaveCount(0);
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByRole("button", { name: "Clear" }).click();
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    });
+
+    test("Protect / Unprotect a WU", async ({ page }) => {
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+        await page.locator(".ms-DetailsRow").first().locator(".ms-DetailsRow-check").click();
+        await expect(page.locator(".ms-DetailsRow.is-selected")).toHaveCount(1);
+        await page.getByRole("menuitem", { name: "Protect", exact: true }).click();
+        await expect(page.locator(".ms-DetailsRow").first().locator("[data-icon-name=\"LockSolid\"]")).toBeVisible();
+        await page.getByRole("menuitem", { name: "Unprotect" }).click();
+        await expect(page.locator(".ms-DetailsRow").first().locator("[data-icon-name=\"LockSolid\"]")).not.toBeVisible();
+    });
+
+    test("Set a WU to failed", async ({ page }) => {
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+        await page.locator(".ms-DetailsRow").filter({ hasText: "completed" }).last().locator(".ms-DetailsRow-check").click();
+        await expect(page.locator(".ms-DetailsRow.is-selected")).toHaveCount(1);
+        await page.getByRole("menuitem", { name: "Set To Failed", exact: true }).click();
+        await expect(page.locator(".ms-DetailsRow.is-selected").filter({ hasText: "failed" })).toBeVisible();
     });
+
+    // test("Delete a WU", async ({ page }) => {
+    //     const wuCount = await page.locator(".ms-DetailsRow").count();
+    //     await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    //     await page.locator(".ms-DetailsRow").filter({ hasText: "completed" }).first().locator(".ms-DetailsRow-check").click();
+    //     await expect(page.locator(".ms-DetailsRow.is-selected")).toHaveCount(1);
+    //     await page.getByRole("menuitem", { name: "Delete", exact: true }).click();
+    //     await page.getByRole("button", { name: "OK" }).click();
+    //     await expect(page.locator(".ms-DetailsRow")).toHaveCount(wuCount - 1);
+    // });
+
+});
+
+test.describe("File tests", () => {
+
+    test.beforeEach(async ({ page }) => {
+        await page.goto("/esp/files/index.html#/files");
+    });
+
+    test("View the Files list page", async ({ page }) => {
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await expect(page.getByText("Logical Name")).toBeVisible();
+        await expect(page.getByText("Owner", { exact: true })).toBeVisible();
+        await expect(page.getByText("Cluster")).toBeVisible();
+        await expect(page.getByText("Records")).toBeVisible();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    });
+
+    test("Filter the Files list page", async ({ page }) => {
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByPlaceholder("*::somefile*").fill("*allPeople*");
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).toHaveCount(1);
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByRole("button", { name: "Clear" }).click();
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(1);
+    });
+
+});
+
+test.describe("Query tests", () => {
+
+    test.beforeEach(async ({ page }) => {
+        await page.goto("/esp/files/index.html#/queries");
+    });
+
+    test("View the Queries list page", async ({ page }) => {
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await expect(page.getByText("ID", { exact: true })).toBeVisible();
+        await expect(page.getByText("Priority", { exact: true })).toBeVisible();
+        await expect(page.getByText("Name")).toBeVisible();
+        await expect(page.getByText("Target")).toBeVisible();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    });
+
+    test("Filter the Queries list page", async ({ page }) => {
+        await expect(page.getByRole("menubar")).toBeVisible();
+        await expect(page.getByRole("menuitem", { name: "Refresh" })).toBeVisible();
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByPlaceholder("My?Su?erQ*ry").fill("asdf");
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).toHaveCount(0);
+        await page.getByRole("menuitem", { name: "Filter" }).click();
+        await page.getByRole("button", { name: "Clear" }).click();
+        await page.getByRole("button", { name: "Apply" }).click();
+        await expect(page.locator(".ms-DetailsRow")).not.toHaveCount(0);
+    });
+
 });
diff --git a/esp/src/tests/global.setup.ts b/esp/src/tests/global.setup.ts
new file mode 100644
index 00000000000..310b9ffc0fd
--- /dev/null
+++ b/esp/src/tests/global.setup.ts
@@ -0,0 +1,145 @@
+import { test, expect } from "@playwright/test";
+import { Workunit, WUUpdate } from "@hpcc-js/comms";
+import { baseURL } from "../playwright.config";
+
+test.describe("Playground tests", () => {
+
+    let editor;
+
+    test.beforeEach(async ({ page }) => {
+        await page.goto("/esp/files/index.html#/play");
+        editor = await page.locator(".CodeMirror");
+        await editor?.click();
+        await page.keyboard.press("Control+A");
+        await page.keyboard.press("Backspace");
+    })
+
+    test("Execute Simple Sort sample", async ({ page }) => {
+        await page.getByText("Simple Filter").click();
+        await page.getByRole("option", { name: "Simple Sort" }).click();
+        await page.getByRole("button", { name: "Submit" }).click();
+        await expect(page.getByRole("link", { name: "completed" })).toBeVisible();
+        await expect(page.getByRole("tab", { name: "Result" })).toBeVisible();
+        await expect(page.locator("#dgrid_0-header")).toBeVisible();
+        await expect(page.locator(".dgrid-row")).toHaveCount(3);
+        await expect(page.locator("svg > g > g > g")).toBeVisible();
+    });
+
+    test("Create two simple files", async ({ page }) => {
+        await page.keyboard.type(`
+Layout_Person := RECORD
+    UNSIGNED1 PersonID;
+    STRING15 FirstName;
+    STRING25 LastName;
+END;
+
+allPeople := DATASET([  {1, 'Fred', 'Smith'},
+                        {2, 'Joe', 'Blow'},
+                        {3, 'Jane', 'Smith'}], Layout_Person);
+
+somePeople := allPeople(LastName = 'Smith');
+
+//  Outputs  ---
+OUTPUT(allPeople,,'~allPeople',OVERWRITE);
+OUTPUT(somePeople,,'~somePeople',OVERWRITE);
+            `);
+
+        await page.getByRole("button", { name: "Submit" }).click();
+        await expect(page.getByRole("link", { name: "completed" })).toBeVisible();
+        const result2 = await page.getByRole("tab", { name: "Result 2" });
+        await expect(result2).toBeVisible();
+        await result2.click();
+        await expect(page.locator(".dgrid-header-row")).toBeVisible();
+        await expect(page.locator(".dgrid-row")).toHaveCount(2);
+    });
+
+    /*
+    test("Publish a roxie query", async ({ page }) => {
+        await page.keyboard.type(`
+resistorCodes := dataset([{0, 'Black'},
+    {1, 'Brown'},
+    {2, 'Red'},
+    {3, 'Orange'},
+    {4, 'Yellow'},
+    {5, 'Green'},
+    {6, 'Blue'},
+    {7, 'Violet'},
+    {8, 'Grey'},
+    {9, 'White'}], {unsigned1 value, string color}) : stored('colorMap');
+
+color2code := DICTIONARY(resistorCodes, { color => value});
+
+colourDictionary := dictionary(recordof(color2code));
+
+bands := DATASET([{'Red'},{'Yellow'},{'Blue'}], {string band}) : STORED('bands');
+
+valrec := RECORD
+unsigned1 value;
+END;
+
+valrec getValue(bands L, colourDictionary mapping) := TRANSFORM
+SELF.value := mapping[L.band].value;
+END;
+
+results := allnodes(PROJECT(bands, getValue(LEFT, THISNODE(color2code))));
+
+ave(results, value);`
+        );
+        await page.getByText("hthor", { exact: true }).click();
+        await page.getByRole("option", { name: "roxie", exact: true }).click();
+        await page.getByLabel("Name", { exact: true }).fill("dictallnodes2");
+        await page.getByRole("button", { name: "Publish" }).click();
+        await expect(page.getByRole("link", { name: "compiled" })).toBeVisible();
+    });
+    */
+
+    test("Publish a roxie query", async ({ }) => {
+        const wu = await Workunit.create({ baseUrl: baseURL });
+
+        const query = `
+resistorCodes := dataset([{0, 'Black'},
+    {1, 'Brown'},
+    {2, 'Red'},
+    {3, 'Orange'},
+    {4, 'Yellow'},
+    {5, 'Green'},
+    {6, 'Blue'},
+    {7, 'Violet'},
+    {8, 'Grey'},
+    {9, 'White'}], {unsigned1 value, string color}) : stored('colorMap');
+
+color2code := DICTIONARY(resistorCodes, { color => value});
+
+colourDictionary := dictionary(recordof(color2code));
+
+bands := DATASET([{'Red'},{'Yellow'},{'Blue'}], {string band}) : STORED('bands');
+
+valrec := RECORD
+unsigned1 value;
+END;
+
+valrec getValue(bands L, colourDictionary mapping) := TRANSFORM
+SELF.value := mapping[L.band].value;
+END;
+
+results := allnodes(PROJECT(bands, getValue(LEFT, THISNODE(color2code))));
+
+ave(results, value);`;
+
+        await wu.update({ Jobname: "dictallnodes2", QueryText: query });
+        await wu.submit("roxie", WUUpdate.Action.Compile);
+        await wu.watchUntilComplete();
+        await wu.publish("dictallnodes2");
+    });
+
+    test("Create a few WUs", async ({ page }) => {
+        await page.keyboard.type(`OUTPUT('Hello World')`);
+        await page.getByRole("button", { name: "Submit" }).click();
+        await expect(page.getByRole("link", { name: "completed" })).toBeVisible();
+        await page.getByRole("button", { name: "Submit" }).click();
+        await expect(page.getByRole("link", { name: "completed" })).toBeVisible();
+        await page.getByRole("button", { name: "Submit" }).click();
+        await expect(page.getByRole("link", { name: "completed" })).toBeVisible();
+    });
+
+});
\ No newline at end of file
diff --git a/esp/src/tests/global.teardown.ts b/esp/src/tests/global.teardown.ts
new file mode 100644
index 00000000000..4025db2a1d2
--- /dev/null
+++ b/esp/src/tests/global.teardown.ts
@@ -0,0 +1,26 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("Teardown", () => {
+
+    test("Delete all Queries", async ({ page }) => {
+        await page.goto("/esp/files/index.html#/queries");
+        await page.locator(".ms-DetailsHeader .ms-DetailsRow-check").click();
+        await page.getByRole("menuitem", { name: "Delete" }).click();
+        await page.getByRole("button", { name: "OK" }).click();
+    });
+
+    test("Delete all Files", async ({ page }) => {
+        await page.goto("/esp/files/index.html#/files");
+        await page.locator(".ms-DetailsHeader .ms-DetailsRow-check").click();
+        await page.getByRole("menuitem", { name: "Delete" }).click();
+        await page.getByRole("button", { name: "OK" }).click();
+    });
+
+    test("Delete all Workunits", async ({ page }) => {
+        await page.goto("/esp/files/index.html#/workunits");
+        await page.locator(".ms-DetailsHeader .ms-DetailsRow-check").click();
+        await page.getByRole("menuitem", { name: "Delete" }).click();
+        await page.getByRole("button", { name: "OK" }).click();
+    });
+
+});
\ No newline at end of file