Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions e2e/getting-started.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2026 3A Systems, LLC.
*/

// @ts-check
import { test, expect } from "@playwright/test";
import { BASE_URL, assertNoErrors, clickDropdownItem, loginToAdmin } from "./helpers.mjs";

const IS_GETTING_STARTED = process.env.OPENIDM_SAMPLE === "samples/getting-started";
const MAPPING_NAME = "HumanResources_Engineering";
const MAPPING_PROPERTIES_URL = `${BASE_URL}/admin/#properties/${MAPPING_NAME}/`;
const ENGINEERING_LIST_URL = `${BASE_URL}/admin/#resource/system/engineering/account/list/`;

async function openMappingsPage(page) {
await clickDropdownItem(page, /configure/i, "#mapping/");
await expect(page.locator(".mapping-config-body").filter({ hasText: MAPPING_NAME }).first())
.toBeVisible({ timeout: 30000 });
}

async function openMappingProperties(page) {
await page.goto(MAPPING_PROPERTIES_URL);
await expect(page.locator("h1")).toContainText(MAPPING_NAME, { timeout: 30000 });
await page.locator("#propertiesTab").waitFor({ state: "visible", timeout: 30000 });
await expect(page.locator("#propertiesTab")).toHaveClass(/active/, { timeout: 30000 });
await expect(page.locator("#attributesGridHolder")).toBeVisible({ timeout: 30000 });
}

async function chooseJaneSanchezSample(page) {
// Selectize is initialized on the original <select id="findSampleSource"> element,
// so it does NOT generate an "#findSampleSource-selectized" sibling input
// (that suffix only applies when selectize wraps an <input>). Instead it inserts
// a `.selectize-control` wrapper next to the now-hidden <select>; the visible
// text input inside has the same placeholder as the original <select>.
const sampleSourceInput = page.locator(
'.selectize-control input[placeholder="Search to see preview"]'
).first();
await sampleSourceInput.waitFor({ state: "visible", timeout: 30000 });
await sampleSourceInput.click();
// selectize listens to keydown/keyup events to fire its `load` callback, so
// typing one character at a time (instead of `fill`, which only sets value +
// dispatches a single input event) is required to populate the dropdown.
await sampleSourceInput.pressSequentially("Sanchez", { delay: 80 });

const janeOption = page
.locator(".selectize-dropdown .option, .selectize-dropdown .fr-search-option")
.filter({ hasText: /Jane[\s\S]*Sanchez|Sanchez[\s\S]*Jane/i })
.first();
await janeOption.waitFor({ state: "visible", timeout: 15000 });
await janeOption.click();

// After selecting the sample source the AttributesGrid re-renders with sample
// values appended in parentheses next to the source/target property cells.
await expect(page.locator("#attributesGridHolder")).toContainText("Jane", { timeout: 30000 });
await expect(page.locator("#attributesGridHolder")).toContainText("Sanchez", { timeout: 30000 });
await expect(page.locator("#attributesGridHolder")).toContainText("jsanchez@example.com", { timeout: 30000 });
}

test.describe.serial("Getting Started Sample - HumanResources_Engineering UI", () => {
test.skip(!IS_GETTING_STARTED, "Only runs when OPENIDM_SAMPLE=samples/getting-started");

test.beforeEach(async ({ page }) => {
await loginToAdmin(page);
});

test("Configure > Mappings page lists HumanResources_Engineering", async ({ page }) => {
await openMappingsPage(page);
await assertNoErrors(page);
});

test("Open HumanResources_Engineering mapping > Properties tab shows expected attributes", async ({ page }) => {
await openMappingsPage(page);
await page.locator(".mapping-config-body").filter({ hasText: MAPPING_NAME }).first().click();

await expect(page).toHaveURL(new RegExp(`/admin/#properties/${MAPPING_NAME}/?$`), { timeout: 30000 });
await expect(page.locator("#propertiesTab")).toHaveClass(/active/, { timeout: 30000 });

for (const attribute of ["firstname", "lastname", "email", "telephoneNumber"]) {
await expect(page.locator("#attributesGridHolder")).toContainText(attribute, { timeout: 30000 });
}

await assertNoErrors(page);
});

test("Sample Source 'Sanchez' shows Jane Sanchez dropdown and selecting populates preview", async ({ page }) => {
await openMappingProperties(page);
await chooseJaneSanchezSample(page);
await assertNoErrors(page);
});

test("Reconcile Now completes successfully with 3 entries", async ({ page }) => {
await openMappingProperties(page);
await chooseJaneSanchezSample(page);

await page.evaluate(() => window.scrollTo(0, 0));
await page.locator("#syncNowButton").click();

// syncLabel switches to the "Last reconciled" / "Completed" translation when
// the recon ends successfully (see MappingBaseView.setReconEnded).
await expect(page.locator("#syncLabel")).toContainText(/completed/i, { timeout: 120000 });

// Expand the sync details widget so the entry counters render.
await page.locator("#syncStatus").click();
await expect(page.locator("#syncStatusDetails")).toBeVisible({ timeout: 30000 });
await expect(page.locator("#syncStatusDetails")).toContainText(/success/i, { timeout: 30000 });
await expect(page.locator("#syncStatusDetails .success-display.display-number"))
.toHaveText("3", { timeout: 30000 });

await assertNoErrors(page);
});

test("Reconciled Jane Sanchez appears in Manage > engineering system list", async ({ page }) => {
await page.goto(ENGINEERING_LIST_URL);
await expect(page.locator(".page-header h1")).toContainText(/engineering/i, { timeout: 30000 });
await expect(page.locator(".backgrid.table")).toContainText(/Sanchez/i, { timeout: 30000 });
await expect(page.locator(".backgrid.table"))
.toContainText(/Jane|jsanchez@example\.com/i, { timeout: 30000 });
await assertNoErrors(page);
});
});
97 changes: 97 additions & 0 deletions e2e/helpers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2026 3A Systems, LLC.
*/

// @ts-check
import { expect } from "@playwright/test";

export const BASE_URL = process.env.OPENIDM_URL || "http://localhost:8080";
export const CONTEXT_PATH = process.env.OPENIDM_CONTEXT_PATH || "/openidm";
export const ADMIN_USER = process.env.OPENIDM_ADMIN_USER || "openidm-admin";
export const ADMIN_PASS = process.env.OPENIDM_ADMIN_PASS || "openidm-admin";

/** Log in to the Admin UI and wait for the navigation bar to appear. */
export async function loginToAdmin(page) {
await page.goto(`${BASE_URL}/admin/`);
await page.waitForSelector("#login", { timeout: 30000 });
await page.fill("#login", ADMIN_USER);
await page.fill("#password", ADMIN_PASS);
await page.click("[type=submit], .btn-primary");
// Wait for the first dropdown toggle to appear (signals post-login render started).
await page.waitForSelector(".navbar-nav a.dropdown-toggle", {
state: "visible",
timeout: 60000,
});
// Configure and Manage toggles are populated by additional async REST calls and
// may render significantly later than Dashboards in slow CI environments.
await Promise.all([
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /configure/i })
.waitFor({ state: "visible", timeout: 90000 }),
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /manage/i })
.waitFor({ state: "visible", timeout: 90000 }),
]);
}

/** Log in to the Enduser UI and wait for the navigation bar to appear. */
export async function loginToEnduser(page) {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector("#login", { timeout: 30000 });
await page.fill("#login", ADMIN_USER);
await page.fill("#password", ADMIN_PASS);
await page.click("[type=submit], .btn-primary");
await page.waitForFunction(
() => document.querySelector("#content") !== null || document.querySelector(".navbar") !== null,
{ timeout: 30000 }
);
}

/** Assert that no visible .alert-danger elements are present on the page. */
export async function assertNoErrors(page) {
// Allow up to 3 s for transient notification banners to auto-dismiss before
// we check. Bootstrap alert-danger elements rendered by the Messages module
// are hidden via display:none (causing offsetParent to be null) when gone.
await page.waitForFunction(
() => [...document.querySelectorAll(".alert-danger")]
.every(el => !el.offsetParent),
{ timeout: 3000 }
).catch(() => { /* persistent alerts will be caught by the check below */ });

const alertDangerLocator = page.locator(".alert-danger");
const count = await alertDangerLocator.count();
let visibleErrors = 0;
for (let i = 0; i < count; i++) {
if (await alertDangerLocator.nth(i).isVisible()) {
visibleErrors++;
}
}
expect(visibleErrors).toBe(0);
}

/**
* Open a navbar dropdown by its visible text label and then click a sub-item
* identified by its href attribute. Waits for the sub-item to become visible
* before clicking so the dropdown animation has completed.
*/
export async function clickDropdownItem(page, dropdownLabel, itemHref) {
const toggle = page
.locator(".navbar-nav a.dropdown-toggle")
.filter({ hasText: dropdownLabel });
await toggle.waitFor({ state: "visible", timeout: 30000 });
await toggle.click();
const item = page.locator(`.dropdown-menu a[href="${itemHref}"]`).first();
await item.waitFor({ state: "visible", timeout: 15000 });
await item.click();
await page.waitForLoadState("networkidle");
}
89 changes: 10 additions & 79 deletions e2e/ui-smoke-test.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,85 +16,16 @@

// @ts-check
import { test, expect } from "@playwright/test";

const BASE_URL = process.env.OPENIDM_URL || "http://localhost:8080";
const CONTEXT_PATH = process.env.OPENIDM_CONTEXT_PATH || "/openidm";
const ADMIN_USER = process.env.OPENIDM_ADMIN_USER || "openidm-admin";
const ADMIN_PASS = process.env.OPENIDM_ADMIN_PASS || "openidm-admin";

/** Log in to the Admin UI and wait for the navigation bar to appear. */
async function loginToAdmin(page) {
await page.goto(`${BASE_URL}/admin/`);
await page.waitForSelector("#login", { timeout: 30000 });
await page.fill("#login", ADMIN_USER);
await page.fill("#password", ADMIN_PASS);
await page.click("[type=submit], .btn-primary");
// Wait for the first dropdown toggle to appear (signals post-login render started).
await page.waitForSelector(".navbar-nav a.dropdown-toggle", {
state: "visible",
timeout: 60000,
});
// Configure and Manage toggles are populated by additional async REST calls and
// may render significantly later than Dashboards in slow CI environments.
await Promise.all([
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /configure/i })
.waitFor({ state: "visible", timeout: 90000 }),
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /manage/i })
.waitFor({ state: "visible", timeout: 90000 }),
]);
}

/** Log in to the Enduser UI and wait for the navigation bar to appear. */
async function loginToEnduser(page) {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector("#login", { timeout: 30000 });
await page.fill("#login", ADMIN_USER);
await page.fill("#password", ADMIN_PASS);
await page.click("[type=submit], .btn-primary");
await page.waitForFunction(
() => document.querySelector("#content") !== null || document.querySelector(".navbar") !== null,
{ timeout: 30000 }
);
}

/** Assert that no visible .alert-danger elements are present on the page. */
async function assertNoErrors(page) {
// Allow up to 3 s for transient notification banners to auto-dismiss before
// we check. Bootstrap alert-danger elements rendered by the Messages module
// are hidden via display:none (causing offsetParent to be null) when gone.
await page.waitForFunction(
() => [...document.querySelectorAll(".alert-danger")]
.every(el => !el.offsetParent),
{ timeout: 3000 }
).catch(() => { /* persistent alerts will be caught by the check below */ });

const alertDangerLocator = page.locator(".alert-danger");
const count = await alertDangerLocator.count();
let visibleErrors = 0;
for (let i = 0; i < count; i++) {
if (await alertDangerLocator.nth(i).isVisible()) {
visibleErrors++;
}
}
expect(visibleErrors).toBe(0);
}

/**
* Open a navbar dropdown by its visible text label and then click a sub-item
* identified by its href attribute. Waits for the sub-item to become visible
* before clicking so the dropdown animation has completed.
*/
async function clickDropdownItem(page, dropdownLabel, itemHref) {
const toggle = page
.locator(".navbar-nav a.dropdown-toggle")
.filter({ hasText: dropdownLabel });
await toggle.waitFor({ state: "visible", timeout: 30000 });
await toggle.click();
const item = page.locator(`.dropdown-menu a[href="${itemHref}"]`).first();
await item.waitFor({ state: "visible", timeout: 15000 });
await item.click();
await page.waitForLoadState("networkidle");
}
import {
ADMIN_PASS,
ADMIN_USER,
BASE_URL,
CONTEXT_PATH,
assertNoErrors,
clickDropdownItem,
loginToAdmin,
loginToEnduser,
} from "./helpers.mjs";

test.describe("OpenIDM UI Smoke Tests", () => {

Expand Down
Loading