Spec Files
Overlay Documentation
This page covers writing tests within rhdh-plugin-export-overlays. For using @red-hat-developer-hub/e2e-test-utils in external projects, see the Guide.
This page explains how to write test specification files for overlay E2E tests.
File Location
Spec files are placed in tests/specs/:
tests/specs/
├── <plugin>.spec.ts # Main test file
├── feature-a.spec.ts # Additional test files (optional)
└── deploy-*.sh # Deployment scripts (optional)2
3
4
Basic Structure
A typical spec file follows this structure:
import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test";
test.describe("Test <plugin>", () => {
// Setup: Deploy RHDH
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy(); // automatically skips if already deployed
});
// Login before each test
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsKeycloakUser();
});
// Test cases
test("Verify functionality", async ({ page, uiHelper }) => {
// Test implementation
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Automatic deployment protection
rhdh.deploy() automatically skips if the deployment already succeeded — even after Playwright restarts the worker due to a test failure. No extra wrapping needed for simple setups. For pre-deploy setup (external services, scripts), wrap the entire block in test.runOnce. See Deployment Protection for details.
Automatic Cleanup
In CI, namespaces are automatically deleted after all tests complete via the built-in teardown reporter. No manual cleanup code is needed. See Namespace Cleanup for details.
Imports
Import test utilities from @red-hat-developer-hub/e2e-test-utils:
// Core test fixtures
import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test";
// Utility functions
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";
// Node.js modules
import path from "path";2
3
4
5
6
7
8
Available Fixtures
The following fixtures are automatically injected into tests:
| Fixture | Type | Scope | Description |
|---|---|---|---|
rhdh | RHDHDeployment | Worker | RHDH deployment management |
uiHelper | UIhelper | Test | UI interaction helper |
loginHelper | LoginHelper | Test | Authentication helper |
page | Page | Test | Playwright Page object |
baseURL | string | Worker | RHDH instance URL |
Setup Patterns
There are two main scenarios for test setup:
- Without pre-requisites: Just configure plugin settings and deploy RHDH
- With pre-requisites: Deploy external services first, then deploy RHDH
Scenario 1: Without Pre-requisites
For plugins that only need configuration (no external services):
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({ auth: "keycloak" });
await rhdh.deploy();
});2
3
4
This is the simplest setup. RHDH is configured and deployed directly. deploy() automatically skips if already deployed (e.g., after a worker restart). Plugin configuration comes from tests/config/app-config-rhdh.yaml.
Scenario 2: With Pre-requisites (External Services)
Some plugins require external services to be running before RHDH starts. For example, the Tech Radar plugin needs a data provider service.
The order matters:
- Configure RHDH
- Deploy external service(s)
- Get service URL and set as environment variable
- Deploy RHDH (uses the environment variable in its configuration)
Since this setup includes an external service deployment (step 2) that is expensive and shouldn't repeat, wrap the entire block in test.runOnce:
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";
import path from "path";
const setupScript = path.join(
import.meta.dirname,
"deploy-customization-provider.sh",
);
test.beforeAll(async ({ rhdh }) => {
await test.runOnce("tech-radar-setup", async () => {
const project = rhdh.deploymentConfig.namespace;
// 1. Configure RHDH first
await rhdh.configure({ auth: "keycloak" });
// 2. Deploy external service
await $`bash ${setupScript} ${project}`;
// 3. Get service URL and set as env var
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");
// 4. Deploy RHDH (has its own built-in protection, nesting is safe)
await rhdh.deploy();
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
See Tech Radar Example for a complete implementation.
Guest Auth Setup
For simpler tests without Keycloak:
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
Login Patterns
Keycloak Login
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsKeycloakUser();
});2
3
Default Keycloak Users
The global setup creates these users automatically:
| Username | Password | Groups |
|---|---|---|
test1 | test1@123 | developers |
test2 | test2@123 | developers |
Custom Credentials
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsKeycloakUser("user2", "password2");
});2
3
Guest Login
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsGuest();
});2
3
Writing Tests
UI Navigation
test("Navigate to plugin", async ({ uiHelper }) => {
await uiHelper.openSidebar("Tech Radar");
await uiHelper.verifyHeading("Tech Radar");
});2
3
4
Verify Content
test("Verify content exists", async ({ uiHelper }) => {
await uiHelper.verifyText("Expected text");
await uiHelper.verifyHeading("Expected heading");
});2
3
4
Custom Locators
test("Verify specific element", async ({ page }) => {
const element = page.locator('h2:has-text("Section")');
await expect(element).toBeVisible();
});2
3
4
Helper Functions
Create reusable verification functions:
async function verifyRadarDetails(page: Page, section: string, text: string) {
const sectionLocator = page
.locator(`h2:has-text("${section}")`)
.locator("xpath=ancestor::*")
.locator(`text=${text}`);
await sectionLocator.scrollIntoViewIfNeeded();
await expect(sectionLocator).toBeVisible();
}
test("Verify radar sections", async ({ page }) => {
await verifyRadarDetails(page, "Languages", "JavaScript");
await verifyRadarDetails(page, "Frameworks", "React");
});2
3
4
5
6
7
8
9
10
11
12
13
Real Example: Tech Radar
Complete spec file from the tech-radar workspace:
import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test";
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";
import path from "path";
const setupScript = path.join(
import.meta.dirname,
"deploy-customization-provider.sh",
);
test.describe("Test tech-radar plugin", () => {
test.beforeAll(async ({ rhdh }) => {
await test.runOnce("tech-radar-setup", async () => {
const project = rhdh.deploymentConfig.namespace;
await rhdh.configure({ auth: "keycloak" });
await $`bash ${setupScript} ${project}`;
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");
await rhdh.deploy(); // built-in protection, safe to nest inside runOnce
});
});
test.beforeEach(async ({ loginHelper }) => {
await loginHelper.loginAsKeycloakUser();
});
test("Verify tech-radar", async ({ page, uiHelper }) => {
await uiHelper.openSidebar("Tech Radar");
await uiHelper.verifyHeading("Tech Radar");
await uiHelper.verifyHeading("Company Radar");
await verifyRadarDetails(page, "Languages", "JavaScript");
await verifyRadarDetails(page, "Frameworks", "React");
await verifyRadarDetails(page, "Infrastructure", "GitHub Actions");
});
});
async function verifyRadarDetails(page: Page, section: string, text: string) {
const sectionLocator = page
.locator(`h2:has-text("${section}")`)
.locator("xpath=ancestor::*")
.locator(`text=${text}`);
await sectionLocator.scrollIntoViewIfNeeded();
await expect(sectionLocator).toBeVisible();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
UIhelper Methods
Common methods from UIhelper:
| Method | Description |
|---|---|
openSidebar(name) | Click sidebar item |
verifyHeading(text) | Verify H1-H6 heading |
verifyText(text) | Verify text is visible |
clickButton(name) | Click button by name |
clickLink(text) | Click link by text |
waitForLoad() | Wait for page load |
See UIhelper API for full reference.
Related Pages
- Directory Layout - File placement
- Configuration Files - YAML setup
- Pre-requisite Services - Deploy dependencies before RHDH
- UIhelper API - Full helper reference