Skip to content

Commit 49f638a

Browse files
vharsekoCopilot
andauthored
Adding extended UI smoke tests for the getting-started scenario (#171)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vharseko <6818498+vharseko@users.noreply.github.com>
1 parent 7d1651f commit 49f638a

3 files changed

Lines changed: 238 additions & 79 deletions

File tree

e2e/getting-started.spec.mjs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems, LLC.
15+
*/
16+
17+
// @ts-check
18+
import { test, expect } from "@playwright/test";
19+
import { BASE_URL, assertNoErrors, clickDropdownItem, loginToAdmin } from "./helpers.mjs";
20+
21+
const IS_GETTING_STARTED = process.env.OPENIDM_SAMPLE === "samples/getting-started";
22+
const MAPPING_NAME = "HumanResources_Engineering";
23+
const MAPPING_PROPERTIES_URL = `${BASE_URL}/admin/#properties/${MAPPING_NAME}/`;
24+
const ENGINEERING_LIST_URL = `${BASE_URL}/admin/#resource/system/engineering/account/list/`;
25+
26+
async function openMappingsPage(page) {
27+
await clickDropdownItem(page, /configure/i, "#mapping/");
28+
await expect(page.locator(".mapping-config-body").filter({ hasText: MAPPING_NAME }).first())
29+
.toBeVisible({ timeout: 30000 });
30+
}
31+
32+
async function openMappingProperties(page) {
33+
await page.goto(MAPPING_PROPERTIES_URL);
34+
await expect(page.locator("h1")).toContainText(MAPPING_NAME, { timeout: 30000 });
35+
await page.locator("#propertiesTab").waitFor({ state: "visible", timeout: 30000 });
36+
await expect(page.locator("#propertiesTab")).toHaveClass(/active/, { timeout: 30000 });
37+
await expect(page.locator("#attributesGridHolder")).toBeVisible({ timeout: 30000 });
38+
}
39+
40+
async function chooseJaneSanchezSample(page) {
41+
// Selectize is initialized on the original <select id="findSampleSource"> element,
42+
// so it does NOT generate an "#findSampleSource-selectized" sibling input
43+
// (that suffix only applies when selectize wraps an <input>). Instead it inserts
44+
// a `.selectize-control` wrapper next to the now-hidden <select>; the visible
45+
// text input inside has the same placeholder as the original <select>.
46+
const sampleSourceInput = page.locator(
47+
'.selectize-control input[placeholder="Search to see preview"]'
48+
).first();
49+
await sampleSourceInput.waitFor({ state: "visible", timeout: 30000 });
50+
await sampleSourceInput.click();
51+
// selectize listens to keydown/keyup events to fire its `load` callback, so
52+
// typing one character at a time (instead of `fill`, which only sets value +
53+
// dispatches a single input event) is required to populate the dropdown.
54+
await sampleSourceInput.pressSequentially("Sanchez", { delay: 80 });
55+
56+
const janeOption = page
57+
.locator(".selectize-dropdown .option, .selectize-dropdown .fr-search-option")
58+
.filter({ hasText: /Jane[\s\S]*Sanchez|Sanchez[\s\S]*Jane/i })
59+
.first();
60+
await janeOption.waitFor({ state: "visible", timeout: 15000 });
61+
await janeOption.click();
62+
63+
// After selecting the sample source the AttributesGrid re-renders with sample
64+
// values appended in parentheses next to the source/target property cells.
65+
await expect(page.locator("#attributesGridHolder")).toContainText("Jane", { timeout: 30000 });
66+
await expect(page.locator("#attributesGridHolder")).toContainText("Sanchez", { timeout: 30000 });
67+
await expect(page.locator("#attributesGridHolder")).toContainText("jsanchez@example.com", { timeout: 30000 });
68+
}
69+
70+
test.describe.serial("Getting Started Sample - HumanResources_Engineering UI", () => {
71+
test.skip(!IS_GETTING_STARTED, "Only runs when OPENIDM_SAMPLE=samples/getting-started");
72+
73+
test.beforeEach(async ({ page }) => {
74+
await loginToAdmin(page);
75+
});
76+
77+
test("Configure > Mappings page lists HumanResources_Engineering", async ({ page }) => {
78+
await openMappingsPage(page);
79+
await assertNoErrors(page);
80+
});
81+
82+
test("Open HumanResources_Engineering mapping > Properties tab shows expected attributes", async ({ page }) => {
83+
await openMappingsPage(page);
84+
await page.locator(".mapping-config-body").filter({ hasText: MAPPING_NAME }).first().click();
85+
86+
await expect(page).toHaveURL(new RegExp(`/admin/#properties/${MAPPING_NAME}/?$`), { timeout: 30000 });
87+
await expect(page.locator("#propertiesTab")).toHaveClass(/active/, { timeout: 30000 });
88+
89+
for (const attribute of ["firstname", "lastname", "email", "telephoneNumber"]) {
90+
await expect(page.locator("#attributesGridHolder")).toContainText(attribute, { timeout: 30000 });
91+
}
92+
93+
await assertNoErrors(page);
94+
});
95+
96+
test("Sample Source 'Sanchez' shows Jane Sanchez dropdown and selecting populates preview", async ({ page }) => {
97+
await openMappingProperties(page);
98+
await chooseJaneSanchezSample(page);
99+
await assertNoErrors(page);
100+
});
101+
102+
test("Reconcile Now completes successfully with 3 entries", async ({ page }) => {
103+
await openMappingProperties(page);
104+
await chooseJaneSanchezSample(page);
105+
106+
await page.evaluate(() => window.scrollTo(0, 0));
107+
await page.locator("#syncNowButton").click();
108+
109+
// syncLabel switches to the "Last reconciled" / "Completed" translation when
110+
// the recon ends successfully (see MappingBaseView.setReconEnded).
111+
await expect(page.locator("#syncLabel")).toContainText(/completed/i, { timeout: 120000 });
112+
113+
// Expand the sync details widget so the entry counters render.
114+
await page.locator("#syncStatus").click();
115+
await expect(page.locator("#syncStatusDetails")).toBeVisible({ timeout: 30000 });
116+
await expect(page.locator("#syncStatusDetails")).toContainText(/success/i, { timeout: 30000 });
117+
await expect(page.locator("#syncStatusDetails .success-display.display-number"))
118+
.toHaveText("3", { timeout: 30000 });
119+
120+
await assertNoErrors(page);
121+
});
122+
123+
test("Reconciled Jane Sanchez appears in Manage > engineering system list", async ({ page }) => {
124+
await page.goto(ENGINEERING_LIST_URL);
125+
await expect(page.locator(".page-header h1")).toContainText(/engineering/i, { timeout: 30000 });
126+
await expect(page.locator(".backgrid.table")).toContainText(/Sanchez/i, { timeout: 30000 });
127+
await expect(page.locator(".backgrid.table"))
128+
.toContainText(/Jane|jsanchez@example\.com/i, { timeout: 30000 });
129+
await assertNoErrors(page);
130+
});
131+
});

e2e/helpers.mjs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems, LLC.
15+
*/
16+
17+
// @ts-check
18+
import { expect } from "@playwright/test";
19+
20+
export const BASE_URL = process.env.OPENIDM_URL || "http://localhost:8080";
21+
export const CONTEXT_PATH = process.env.OPENIDM_CONTEXT_PATH || "/openidm";
22+
export const ADMIN_USER = process.env.OPENIDM_ADMIN_USER || "openidm-admin";
23+
export const ADMIN_PASS = process.env.OPENIDM_ADMIN_PASS || "openidm-admin";
24+
25+
/** Log in to the Admin UI and wait for the navigation bar to appear. */
26+
export async function loginToAdmin(page) {
27+
await page.goto(`${BASE_URL}/admin/`);
28+
await page.waitForSelector("#login", { timeout: 30000 });
29+
await page.fill("#login", ADMIN_USER);
30+
await page.fill("#password", ADMIN_PASS);
31+
await page.click("[type=submit], .btn-primary");
32+
// Wait for the first dropdown toggle to appear (signals post-login render started).
33+
await page.waitForSelector(".navbar-nav a.dropdown-toggle", {
34+
state: "visible",
35+
timeout: 60000,
36+
});
37+
// Configure and Manage toggles are populated by additional async REST calls and
38+
// may render significantly later than Dashboards in slow CI environments.
39+
await Promise.all([
40+
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /configure/i })
41+
.waitFor({ state: "visible", timeout: 90000 }),
42+
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /manage/i })
43+
.waitFor({ state: "visible", timeout: 90000 }),
44+
]);
45+
}
46+
47+
/** Log in to the Enduser UI and wait for the navigation bar to appear. */
48+
export async function loginToEnduser(page) {
49+
await page.goto(`${BASE_URL}/`);
50+
await page.waitForSelector("#login", { timeout: 30000 });
51+
await page.fill("#login", ADMIN_USER);
52+
await page.fill("#password", ADMIN_PASS);
53+
await page.click("[type=submit], .btn-primary");
54+
await page.waitForFunction(
55+
() => document.querySelector("#content") !== null || document.querySelector(".navbar") !== null,
56+
{ timeout: 30000 }
57+
);
58+
}
59+
60+
/** Assert that no visible .alert-danger elements are present on the page. */
61+
export async function assertNoErrors(page) {
62+
// Allow up to 3 s for transient notification banners to auto-dismiss before
63+
// we check. Bootstrap alert-danger elements rendered by the Messages module
64+
// are hidden via display:none (causing offsetParent to be null) when gone.
65+
await page.waitForFunction(
66+
() => [...document.querySelectorAll(".alert-danger")]
67+
.every(el => !el.offsetParent),
68+
{ timeout: 3000 }
69+
).catch(() => { /* persistent alerts will be caught by the check below */ });
70+
71+
const alertDangerLocator = page.locator(".alert-danger");
72+
const count = await alertDangerLocator.count();
73+
let visibleErrors = 0;
74+
for (let i = 0; i < count; i++) {
75+
if (await alertDangerLocator.nth(i).isVisible()) {
76+
visibleErrors++;
77+
}
78+
}
79+
expect(visibleErrors).toBe(0);
80+
}
81+
82+
/**
83+
* Open a navbar dropdown by its visible text label and then click a sub-item
84+
* identified by its href attribute. Waits for the sub-item to become visible
85+
* before clicking so the dropdown animation has completed.
86+
*/
87+
export async function clickDropdownItem(page, dropdownLabel, itemHref) {
88+
const toggle = page
89+
.locator(".navbar-nav a.dropdown-toggle")
90+
.filter({ hasText: dropdownLabel });
91+
await toggle.waitFor({ state: "visible", timeout: 30000 });
92+
await toggle.click();
93+
const item = page.locator(`.dropdown-menu a[href="${itemHref}"]`).first();
94+
await item.waitFor({ state: "visible", timeout: 15000 });
95+
await item.click();
96+
await page.waitForLoadState("networkidle");
97+
}

e2e/ui-smoke-test.spec.mjs

Lines changed: 10 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,85 +16,16 @@
1616

1717
// @ts-check
1818
import { test, expect } from "@playwright/test";
19-
20-
const BASE_URL = process.env.OPENIDM_URL || "http://localhost:8080";
21-
const CONTEXT_PATH = process.env.OPENIDM_CONTEXT_PATH || "/openidm";
22-
const ADMIN_USER = process.env.OPENIDM_ADMIN_USER || "openidm-admin";
23-
const ADMIN_PASS = process.env.OPENIDM_ADMIN_PASS || "openidm-admin";
24-
25-
/** Log in to the Admin UI and wait for the navigation bar to appear. */
26-
async function loginToAdmin(page) {
27-
await page.goto(`${BASE_URL}/admin/`);
28-
await page.waitForSelector("#login", { timeout: 30000 });
29-
await page.fill("#login", ADMIN_USER);
30-
await page.fill("#password", ADMIN_PASS);
31-
await page.click("[type=submit], .btn-primary");
32-
// Wait for the first dropdown toggle to appear (signals post-login render started).
33-
await page.waitForSelector(".navbar-nav a.dropdown-toggle", {
34-
state: "visible",
35-
timeout: 60000,
36-
});
37-
// Configure and Manage toggles are populated by additional async REST calls and
38-
// may render significantly later than Dashboards in slow CI environments.
39-
await Promise.all([
40-
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /configure/i })
41-
.waitFor({ state: "visible", timeout: 90000 }),
42-
page.locator(".navbar-nav a.dropdown-toggle").filter({ hasText: /manage/i })
43-
.waitFor({ state: "visible", timeout: 90000 }),
44-
]);
45-
}
46-
47-
/** Log in to the Enduser UI and wait for the navigation bar to appear. */
48-
async function loginToEnduser(page) {
49-
await page.goto(`${BASE_URL}/`);
50-
await page.waitForSelector("#login", { timeout: 30000 });
51-
await page.fill("#login", ADMIN_USER);
52-
await page.fill("#password", ADMIN_PASS);
53-
await page.click("[type=submit], .btn-primary");
54-
await page.waitForFunction(
55-
() => document.querySelector("#content") !== null || document.querySelector(".navbar") !== null,
56-
{ timeout: 30000 }
57-
);
58-
}
59-
60-
/** Assert that no visible .alert-danger elements are present on the page. */
61-
async function assertNoErrors(page) {
62-
// Allow up to 3 s for transient notification banners to auto-dismiss before
63-
// we check. Bootstrap alert-danger elements rendered by the Messages module
64-
// are hidden via display:none (causing offsetParent to be null) when gone.
65-
await page.waitForFunction(
66-
() => [...document.querySelectorAll(".alert-danger")]
67-
.every(el => !el.offsetParent),
68-
{ timeout: 3000 }
69-
).catch(() => { /* persistent alerts will be caught by the check below */ });
70-
71-
const alertDangerLocator = page.locator(".alert-danger");
72-
const count = await alertDangerLocator.count();
73-
let visibleErrors = 0;
74-
for (let i = 0; i < count; i++) {
75-
if (await alertDangerLocator.nth(i).isVisible()) {
76-
visibleErrors++;
77-
}
78-
}
79-
expect(visibleErrors).toBe(0);
80-
}
81-
82-
/**
83-
* Open a navbar dropdown by its visible text label and then click a sub-item
84-
* identified by its href attribute. Waits for the sub-item to become visible
85-
* before clicking so the dropdown animation has completed.
86-
*/
87-
async function clickDropdownItem(page, dropdownLabel, itemHref) {
88-
const toggle = page
89-
.locator(".navbar-nav a.dropdown-toggle")
90-
.filter({ hasText: dropdownLabel });
91-
await toggle.waitFor({ state: "visible", timeout: 30000 });
92-
await toggle.click();
93-
const item = page.locator(`.dropdown-menu a[href="${itemHref}"]`).first();
94-
await item.waitFor({ state: "visible", timeout: 15000 });
95-
await item.click();
96-
await page.waitForLoadState("networkidle");
97-
}
19+
import {
20+
ADMIN_PASS,
21+
ADMIN_USER,
22+
BASE_URL,
23+
CONTEXT_PATH,
24+
assertNoErrors,
25+
clickDropdownItem,
26+
loginToAdmin,
27+
loginToEnduser,
28+
} from "./helpers.mjs";
9829

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

0 commit comments

Comments
 (0)