diff --git a/core/database/foxx/api/user_router.js b/core/database/foxx/api/user_router.js index 95cd4d368..5e233d1f5 100644 --- a/core/database/foxx/api/user_router.js +++ b/core/database/foxx/api/user_router.js @@ -393,6 +393,7 @@ router result = [user.new]; }, }); + res.send(result); logger.logRequestSuccess({ client: client?._id, correlationId: req.headers["x-correlation-id"], diff --git a/tests/end-to-end/README.md b/tests/end-to-end/README.md index fbcea5878..939f2bda0 100644 --- a/tests/end-to-end/README.md +++ b/tests/end-to-end/README.md @@ -14,3 +14,76 @@ To use the python API you will need to build it cmake -S. -B build -DBUILD_PYTHON_CLIENT=ON cmake --build build --target pydatafed ``` + +## Playwright + +On windows, it is recommended to run playwright directly on windows and not in +a docker container or on wsl2. If you do take that approach you will likely +encounter compatibility problems, and will still need to stand up an XServer +on the windows host. + +To run + +```bash +npm install . +npx playwright install +npx playwright test +``` + +You can also use the playwright code generator to add additional tests. + +```bash +npx playwright codegen +``` + +If you are running on linux you might be able to get away with running in a +docker image. + +Below is a minimal dockerfile to build playwright with a few useful developer tools. + +```Dockerfile +FROM mcr.microsoft.com/playwright:v1.45.1-noble + +# Install Chromium only +WORKDIR /work +RUN npx playwright install chromium --with-deps; npx playwright install +RUN apt-get update && apt-get install -y ca-certificates bash vim && update-ca-certificates +``` + +Build it with. + +```bash +docker build . -t playwright:latest +``` + +```bash +docker run --rm -v "$PWD:/work" -w /work -e DATAFED_WEB_TEST_USERNAME="$DATAFED_WEB_TEST_USERNAME" -e DATAFED_WEB_TEST_PASSWORD="$DATAFED_WEB_TEST_PASSWORD" -e DATAFED_DOMAIN="$DATAFED_DOMAIN" -e DISPLAY=host.docker. +internal:0 playwright:latest npx -y playwright test +``` + +NOTE: By default the web tests are setup to run in headless mode but if you +wish to see the web tests as they execute while debugging etc you will need +to edit the configuration in playwright.config.js + +This might need to be specified in the following places +``` + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + headless: false, // optional: run headed + }, + }, + ] +``` + +``` + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + headless: false, + screenshot: "only-on-failure", + +``` + diff --git a/tests/end-to-end/web-UI/CMakeLists.txt b/tests/end-to-end/web-UI/CMakeLists.txt index 4e21e2fc6..cf70c32f5 100644 --- a/tests/end-to-end/web-UI/CMakeLists.txt +++ b/tests/end-to-end/web-UI/CMakeLists.txt @@ -8,7 +8,7 @@ configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/auth.setup.js" @ONLY ) - +message("ENABLE END TO END!!!!!!!") #FIXTHIS # For E2E web ui test if(ENABLE_END_TO_END_WEB_TESTS) diff --git a/tests/end-to-end/web-UI/auth.setup.js.in b/tests/end-to-end/web-UI/auth.setup.js.in index 9d898dacc..eea652183 100644 --- a/tests/end-to-end/web-UI/auth.setup.js.in +++ b/tests/end-to-end/web-UI/auth.setup.js.in @@ -1,69 +1,102 @@ -const { chromium } = require('playwright'); -const path = require('path'); -const process = require('process'); +const { chromium } = require("playwright"); +const path = require("path"); +const process = require("process"); console.log("INFO - ******Inside Setup file******"); module.exports = async function () { // if a playwright page object doesn't exist, create one const browser = await chromium.launch({ - args: ['--ignore-certificate-errors'], + args: ["--ignore-certificate-errors"], headless: true, }); - const page = await browser.newPage(); + const page = await browser.newPage(); console.log("INFO - new page object created"); - + // Extra safety for debugging slow loads - page.on('console', msg => console.log('INFO - [PAGE LOG] ', msg.text())); - page.on('response', res => { - if (res.status() >= 400) - console.log(`ERROR - [HTTP ${res.status()}] ${res.url()}`); + page.on("console", (msg) => console.log("INFO - [PAGE LOG] ", msg.text())); + page.on("response", (res) => { + if (res.status() >= 400) console.log(`ERROR - [HTTP ${res.status()}] ${res.url()}`); }); // --- Step 1: Go to DataFed --- console.log("INFO - 1. Got to DataFed"); - await page.goto('https://@DATAFED_DOMAIN@/ui/welcome', { waitUntil: 'networkidle', timeout: 60000 }); + await page.goto("https://@DATAFED_DOMAIN@/ui/welcome", { + waitUntil: "networkidle", + timeout: 60000, + }); // --- Step 2: Click Login/Register --- console.log("INFO - 2. Login and Register"); - const loginButton = page.getByRole('button', { name: 'Log In / Register' }); - await loginButton.waitFor({ state: 'visible', timeout: 30000 }); + const loginButton = page.getByRole("button", { name: "Log In / Register" }); + await loginButton.waitFor({ state: "visible", timeout: 30000 }); await loginButton.click(); // --- Step 3: Check if link is visible --- console.log("INFO - 3. Check if link is visible"); - const globusLink = page.getByRole('link', { name: /Globus/i }); - await globusLink.waitFor({ state: 'visible', timeout: 30000 }); + const globusLink = page.getByRole("link", { name: /Globus/i }); + await globusLink.waitFor({ state: "visible", timeout: 30000 }); + await page.waitForTimeout(1000); // --- Step 4: Click Globus ID to sign in --- console.log("INFO - 4. Use Globus ID to sign in"); - const globusIDButton = page.getByRole('button', { name: 'Globus ID to sign in' }); - await globusIDButton.waitFor({ state: 'visible', timeout: 30000 }); + const globusIDButton = page.getByRole("button", { name: "Globus ID to sign in" }); + await globusIDButton.waitFor({ state: "visible", timeout: 30000 }); await globusIDButton.click(); + await page.waitForTimeout(1000); // --- Step 5: Wait for Globus redirect --- console.log("INFO - 5. Wait for Globus login to redirect"); - await page.waitForLoadState('networkidle', { timeout: 45000 }); + await page.waitForLoadState("networkidle", { timeout: 45000 }); // --- Step 6: Fill in credentials robustly --- console.log("INFO - 6. Fill in credentials"); const usernameField = page.getByLabel(/username/i); - await usernameField.waitFor({ state: 'visible', timeout: 45000 }); + await usernameField.waitFor({ state: "visible", timeout: 45000 }); await usernameField.fill(process.env.DATAFED_WEB_TEST_USERNAME); const passwordField = page.getByLabel(/password/i); - await passwordField.waitFor({ state: 'visible', timeout: 45000 }); + await passwordField.waitFor({ state: "visible", timeout: 45000 }); await passwordField.fill(process.env.DATAFED_WEB_TEST_PASSWORD); // --- Step 7: Submit form --- await page.click('button[type="submit"]'); - await page.waitForURL('https://@DATAFED_DOMAIN@/ui/main') + await Promise.race([ + page.waitForURL(/\/ui\/main$/, { timeout: 45000 }), + page.waitForURL(/\/ui\/register$/, { timeout: 45000 }), + ]); + + let current = page.url(); + console.log("INFO - 7a Redirected to:", current); + + if (current.includes("/ui/register")) { + console.log("INFO - 7b. Registering:", current); + await page.getByRole('button', { name: 'Continue Registration' }).click(); + } else { + console.log("INFO - 7b. Grabbing fresh url:", current); + if (!page.url().includes("/ui/main")) { + await page.waitForURL("https://@DATAFED_DOMAIN@/ui/main", { timeout: 45000 }); + } + current = page.url(); + console.log("INFO - 7c. URL is: ", current); + } + + await page.screenshot({ path: "after_login.png", fullPage: true }); + if (current.includes("/ui/main")) { + console.log("INFO - 8b Successful: ", current); + } else { + throw new Error(`Unexpected redirect URL: ${current}`); + } + //await Promise.all([ + // page.waitForNavigation({ url: /\/ui\/main/ }), // robust matching + // page.click('button[type="submit"]'), + //]); + //page.screenshot(path="final.png", full_page=True) console.log("INFO - ******PAST LOGIN******"); - await page.context().storageState({ path: './.auth/auth.json'}); //TESTING + await page.context().storageState({ path: "./.auth/auth.json" }); //TESTING console.log("INFO - ******Done with login******"); await browser.close(); }; - diff --git a/tests/end-to-end/web-UI/playwright.config.js b/tests/end-to-end/web-UI/playwright.config.js index cae11dd50..630b3bcba 100644 --- a/tests/end-to-end/web-UI/playwright.config.js +++ b/tests/end-to-end/web-UI/playwright.config.js @@ -50,19 +50,5 @@ module.exports = defineConfig({ ...devices["Desktop Chrome"], }, }, - - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, - - // { - // name: 'webkit', - // use: { - // ...devices['Desktop Safari'], - // }, - // }, ], }); diff --git a/tests/end-to-end/web-UI/scripts/testingBasicFunction.spec.js b/tests/end-to-end/web-UI/scripts/testingBasicFunction.spec.js index c45c37898..ff516510c 100644 --- a/tests/end-to-end/web-UI/scripts/testingBasicFunction.spec.js +++ b/tests/end-to-end/web-UI/scripts/testingBasicFunction.spec.js @@ -1,54 +1,46 @@ import { test, expect } from "@playwright/test"; // checking visibility and expanding some dropdowns -test("test visibility", async ({ page }) => { - try { - console.log("******Begin test******"); - // Temporary fix - let domain = process.env.DATAFED_DOMAIN; - await page.goto("https://" + domain + "/"); - if (await page.getByRole("button", { name: "Log In / Register" }).isVisible()) { - console.log("NOT LOGGED IN"); +test.describe("DataFed UI Navigation", () => { + test("should display main navigation elements", async ({ page }) => { + const domain = process.env.DATAFED_DOMAIN; + if (!domain) { + throw new Error("DATAFED_DOMAIN environment variable not set"); } - if (await expect(page.getByText("Continue Registration")).toBeVisible()) { - await page.getByText("Continue Registration").click({ timeout: 20000 }); - } - await expect(page.locator(".ui-icon").first()).toBeVisible({ - timeout: 20000, - }); + + await page.goto(`https://${domain}/ui/main`); + + // Verify main elements + await expect(page.locator(".ui-icon").first()).toBeVisible(); await expect(page.getByText("DataFed - Scientific Data")).toBeVisible(); await expect(page.getByRole("link", { name: "My Data" })).toBeVisible(); await expect(page.getByRole("link", { name: "Catalog" })).toBeVisible(); - await expect(page.getByRole("button", { name: "" })).toBeVisible(); + }); + + test("should expand tree navigation items", async ({ page }) => { + const domain = process.env.DATAFED_DOMAIN; + if (!domain) { + throw new Error("DATAFED_DOMAIN environment variable not set"); + } + + await page.goto(`https://${domain}/ui/main`); + + // Define tree items to expand + const treeItems = [ + "Public Collections", + "Allocations", + "Project Data", + "Shared Data", + "Saved Queries", + "By User", + ]; - await page - .getByRole("treeitem", { name: "  Public Collections" }) - .getByRole("button") - .click(); - await page - .getByRole("treeitem", { name: "  Public Collections" }) - .getByRole("group") - .click(); - await page.getByRole("treeitem", { name: "  Allocations" }).getByRole("button").click(); - await page.getByRole("treeitem", { name: "  Project Data" }).getByRole("button").click(); - await page.getByRole("treeitem", { name: "  Shared Data" }).getByRole("button").click(); - await page - .getByRole("treeitem", { name: "  Saved Queries" }) - .locator("span") - .first() - .click(); - await page.getByRole("treeitem", { name: "  Saved Queries" }).getByRole("button").click(); - await page.getByText("Provenance Annotate Upload").click({ timeout: 20000 }); - await page.getByRole("treeitem", { name: "  By User" }).getByRole("button").click(); - } catch (error) { - // element not visible, either the test broke due to tags changing, or not logged in - // try to log out, because if not logged out, future tests will fail due to globus being annoying - if (await page.getByRole("button", { name: "" }).isVisible()) { - await page.getByRole("button", { name: "" }).click(); - } else { - // if in here, check if you logged out properly - throw error; + for (const item of treeItems) { + const treeItem = page.getByRole("treeitem", { name: new RegExp(item) }); + const button = treeItem.getByRole("button").first(); + await expect(button).toBeVisible(); + await button.click(); + // Add assertion that it expanded if needed } - } - //removed logout + }); }); diff --git a/tests/end-to-end/web-UI/scripts/testingUserPasswordChange.spec.js b/tests/end-to-end/web-UI/scripts/testingUserPasswordChange.spec.js new file mode 100644 index 000000000..dbbc8f48d --- /dev/null +++ b/tests/end-to-end/web-UI/scripts/testingUserPasswordChange.spec.js @@ -0,0 +1,73 @@ +import { test, expect } from "@playwright/test"; + +// checking visibility and expanding some dropdowns +test.describe("DataFed UI password change", () => { + // Since auth is handled by global setup, we start from authenticated state + test.beforeEach(async ({ page }) => { + const domain = process.env.DATAFED_DOMAIN || "domain.com"; + + // Navigate to main page (already authenticated via storageState) + await page.goto(`https://${domain}/ui/main`, { + waitUntil: "networkidle", + }); + + // Verify we're on the main page and authenticated + await expect(page).toHaveURL(new RegExp(`https://${domain}/ui/main`)); + + // Wait for main UI to be ready + await page + .waitForSelector('[data-testid="main-content"], .main-content, #main', { + state: "visible", + timeout: 3000, + }) + .catch(() => { + // Fallback: just wait for any main element + return page.waitForTimeout(2000); + }); + + // Log current page state for debugging + console.log(`INFO - Test starting on: ${page.url()}`); + }); + + test("should display main page after entering password", async ({ page }) => { + await test.step("Open settings menu", async () => { + const button = page.locator("#btn_settings"); + + if (await button.isVisible()) { + console.log("INFO - Button is visible (btn_settings)"); + await button.click(); + } else { + console.log("INFO - Button is not visible (btn_settings)"); + } + }); + + await test.step("Enter password and confirmation password", async () => { + // Define locators for the elements + const newPasswordInput = page.locator("#cli_new_pw"); + const confirmPasswordInput = page.locator("#cli_confirm_pw"); + const revokeButton = page.locator("#btn_revoke_cred"); + const saveButton = page.getByRole("button", { name: "Save" }); + + // Wait for all elements to be visible + await Promise.all([ + newPasswordInput.waitFor({ state: "visible", timeout: 5000 }), + confirmPasswordInput.waitFor({ state: "visible", timeout: 5000 }), + revokeButton.waitFor({ state: "visible", timeout: 5000 }), + saveButton.waitFor({ state: "visible", timeout: 5000 }), + ]); + await newPasswordInput.click(); + await newPasswordInput.fill("Terrible2s!!!"); + await confirmPasswordInput.click(); + await confirmPasswordInput.fill("Terrible2s!!!"); + await saveButton.click(); + // Make sure an error does not appear. + // Unfortunately it take a while for the error to show up + await page.waitForTimeout(15000); + + await page.screenshot({ path: "change-password-has-error.png", fullPage: true }); + + // These WILL fail if error appears + await expect(page.getByText("Save Settings Error")).not.toBeVisible(); + }); + }); +});