Common Patterns
Overlay Documentation
This page covers writing tests within rhdh-plugin-export-overlays. For using rhdh-e2e-test-utils in external projects, see the Guide.
This page documents common testing patterns used in overlay E2E tests.
Setup Patterns
Basic Keycloak Setup
test.describe("Plugin tests", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy();
});
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsKeycloakUser();
});
});2
3
4
5
6
7
8
9
10
Project and Spec Best Practices
Each Playwright project name creates a separate namespace. To keep deployments fast and predictable:
- Use one project named after the workspace.
- Keep one spec file per workspace unless your requirements differ.
If you need different auth/configs or separate namespaces, use multiple projects or manage deployments directly with RHDHDeployment.
Guest Authentication Setup
test.describe("Plugin tests", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({ auth: "guest" });
await rhdh.deploy();
});
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsGuest();
});
});2
3
4
5
6
7
8
9
10
Setup with External Service
import { $ } from "rhdh-e2e-test-utils/utils";
import path from "path";
const setupScript = path.join(import.meta.dirname, "deploy-service.sh");
test.describe("Plugin tests", () => {
test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
await rhdh.configure({ auth: "keycloak" });
await $`bash ${setupScript} ${project}`;
process.env.SERVICE_URL = await rhdh.k8sClient.getRouteLocation(
project,
"service-name",
);
await rhdh.deploy();
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Navigation Patterns
Sidebar Navigation
test("Navigate via sidebar", async ({ uiHelper }) => {
await uiHelper.openSidebar("Plugin Name");
await uiHelper.verifyHeading("Expected Heading");
});2
3
4
Tab Navigation
test("Navigate tabs", async ({ uiHelper }) => {
await uiHelper.openSidebar("Plugin Name");
await uiHelper.clickTab("Details");
await uiHelper.verifyText("Details content");
});2
3
4
5
Direct URL Navigation
test("Navigate by URL", async ({ page, uiHelper }) => {
await page.goto("/catalog");
await uiHelper.verifyHeading("Catalog");
});2
3
4
Verification Patterns
Verify Heading
await uiHelper.verifyHeading("Expected Heading");Verify Text
await uiHelper.verifyText("Expected text");Verify Element Visibility
await expect(page.locator('text="Expected"')).toBeVisible();Verify Element Hidden
await expect(page.locator('text="Hidden"')).not.toBeVisible();Verify Multiple Items
async function verifyItems(page: Page, items: string[]) {
for (const item of items) {
await expect(page.locator(`text="${item}"`)).toBeVisible();
}
}2
3
4
5
Interaction Patterns
Click Button
await uiHelper.clickButton("Submit");Click Link
await uiHelper.clickLink("Learn More");Fill Input
await uiHelper.fillTextInputByLabel("Username", "testuser");Search
await uiHelper.searchInputPlaceholder("Search...", "query");Select Dropdown
await uiHelper.selectMuiBox("Category", "Option 1");Table Patterns
Verify Table Rows
await uiHelper.verifyRowsInTable(["Row 1", "Row 2", "Row 3"]);Click Row Action
await uiHelper.clickOnButtonInTableByUniqueText("Row 1", "Edit");Verify Row Content
await uiHelper.verifyRowInTableByUniqueText("Row 1", ["Column 1", "Column 2"]);Waiting Patterns
Wait for Load
await uiHelper.waitForLoad();Wait for Text
await expect(page.locator('text="Loading..."')).not.toBeVisible();Wait for Element
await page.locator('text="Content"').waitFor({ state: "visible" });Custom Wait
await page.waitForSelector('[data-testid="loaded"]');Long-Running Setup and Deployments (Timeouts)
If beforeAll performs slow setup (deployment, external services), increase the timeout explicitly.
Which Timeout Are You Increasing?
test.setTimeout(...)insidebeforeAllincreases the timeout for that hook.test.setTimeout(...)inside a test increases the timeout for that test.- The Playwright config
timeoutis the default per-test timeout.
Increase beforeAll Timeout
test.beforeAll(async ({ rhdh }) => {
test.setTimeout(10 * 60 * 1000); // 10 minutes
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy();
});2
3
4
5
Extend RHDH Readiness Wait
test.beforeAll(async ({ rhdh }) => {
test.setTimeout(10 * 60 * 1000);
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy();
await rhdh.waitUntilReady(600); // seconds
});2
3
4
5
6
Note: rhdh.deploy() already increases the test timeout (500s). If your setup does more work before deploy, set a higher timeout in beforeAll.
Error Handling Patterns
Expect Error Message
test("Handle error", async ({ uiHelper }) => {
await uiHelper.clickButton("Invalid Action");
await uiHelper.verifyAlertErrorMessage("Error occurred");
});2
3
4
Try-Catch Pattern
test("Graceful failure", async ({ page }) => {
try {
await page.locator('text="Rare Element"').click({ timeout: 5000 });
} catch {
console.log("Element not present, continuing...");
}
});2
3
4
5
6
7
Helper Function Patterns
Reusable Verification
async function verifySection(page: Page, section: string, content: string) {
const locator = page
.locator(`h2:has-text("${section}")`)
.locator("xpath=ancestor::*")
.locator(`text=${content}`);
await locator.scrollIntoViewIfNeeded();
await expect(locator).toBeVisible();
}
test("Verify sections", async ({ page }) => {
await verifySection(page, "Languages", "JavaScript");
await verifySection(page, "Frameworks", "React");
});2
3
4
5
6
7
8
9
10
11
12
13
Data-Driven Tests
const testCases = [
{ name: "Case 1", input: "a", expected: "A" },
{ name: "Case 2", input: "b", expected: "B" },
];
for (const tc of testCases) {
test(`Test ${tc.name}`, async ({ page }) => {
await page.fill('input', tc.input);
await expect(page.locator('.result')).toHaveText(tc.expected);
});
}2
3
4
5
6
7
8
9
10
11
Serial Test Pattern
For tests that must run in order:
test.describe.configure({ mode: "serial" });
test.describe("Serial tests", () => {
test("Step 1: Create", async () => {
// Create resource
});
test("Step 2: Verify", async () => {
// Verify resource exists
});
test("Step 3: Delete", async () => {
// Delete resource
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
Screenshot Pattern
test("Take screenshot", async ({ page }) => {
await page.goto("/dashboard");
await page.screenshot({ path: "dashboard.png", fullPage: true });
});2
3
4
Page Objects
The package provides pre-built page objects for common RHDH pages (CatalogPage, HomePage, CatalogImportPage, ExtensionsPage, NotificationPage).
See Page Objects Guide for usage and available methods.
API Helper
For programmatic catalog or GitHub API operations, use APIHelper:
import { APIHelper } from "rhdh-e2e-test-utils/helpers";
test("verify catalog", async ({ rhdh }) => {
const apiHelper = new APIHelper();
await apiHelper.setBaseUrl(rhdh.rhdhUrl);
const users = await apiHelper.getAllCatalogUsersFromAPI();
expect(users.length).toBeGreaterThan(0);
});2
3
4
5
6
7
8
9
See APIHelper Guide for GitHub and catalog API operations.
Related Pages
- Spec Files - Writing tests
- UIhelper API - Full API reference
- Page Objects - Pre-built page abstractions
- APIHelper - Catalog and GitHub API
- Troubleshooting - Common issues