Skip to content

Commit c32cacc

Browse files
test.runOnce implementation
1 parent 06eab81 commit c32cacc

13 files changed

Lines changed: 415 additions & 81 deletions

File tree

docs/CLAUDE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ docs/
4646
import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";
4747

4848
test.describe("Feature", () => {
49-
test.beforeAll(async ({ rhdh }) => { /* deploy */ });
49+
test.beforeAll(async ({ rhdh }) => {
50+
await test.runOnce("my-plugin-deploy", async () => { /* deploy */ });
51+
});
5052
test.beforeEach(async ({ loginHelper }) => { /* login */ });
5153
test("should...", async ({ uiHelper }) => { /* test */ });
5254
});
@@ -121,6 +123,8 @@ When documenting, reference these source files:
121123
| Fixtures | `src/playwright/fixtures/test.ts` |
122124
| Base Config | `src/playwright/base-config.ts` |
123125
| Global Setup | `src/playwright/global-setup.ts` |
126+
| Teardown Reporter | `src/playwright/teardown-reporter.ts` |
127+
| Teardown Namespaces | `src/playwright/teardown-namespaces.ts` |
124128

125129
## Common Tasks
126130

@@ -209,5 +213,6 @@ Base URL is configured as `/rhdh-e2e-test-utils/` in `config.ts`.
209213
| Helpers | `@red-hat-developer-hub/e2e-test-utils/helpers` | UIhelper, LoginHelper, etc. |
210214
| Page objects | `@red-hat-developer-hub/e2e-test-utils/pages` | CatalogPage, HomePage, etc. |
211215
| Utilities | `@red-hat-developer-hub/e2e-test-utils/utils` | KubernetesClientHelper, etc. |
216+
| Teardown | `@red-hat-developer-hub/e2e-test-utils/teardown` | Custom namespace teardown registration |
212217
| ESLint | `@red-hat-developer-hub/e2e-test-utils/eslint` | ESLint config |
213218
| TypeScript | `@red-hat-developer-hub/e2e-test-utils/tsconfig` | TSConfig base |

docs/api/playwright/base-config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Raw base configuration object. Use for advanced customization.
5959
retries: Number(process.env.PLAYWRIGHT_RETRIES ?? 0),
6060
workers: process.env.PLAYWRIGHT_WORKERS || "50%",
6161
outputDir: "node_modules/.cache/e2e-test-results",
62-
reporter: [["list"], ["html"], ["json"]],
62+
reporter: [["list"], ["html"], ["json"], ["teardown-reporter"]],
6363
use: {
6464
viewport: { width: 1920, height: 1080 },
6565
video: { mode: "retain-on-failure", size: { width: 1280, height: 720 } },

docs/api/playwright/test-fixtures.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";
1616

1717
**Type:** `RHDHDeployment`
1818

19-
Shared RHDH deployment across all tests in a worker.
19+
Shared RHDH deployment across all tests in a worker. Wrap expensive setup in `test.runOnce` to avoid re-deploying when workers restart after test failures.
2020

2121
```typescript
2222
test.beforeAll(async ({ rhdh }) => {
23-
await rhdh.configure({ auth: "keycloak" });
24-
await rhdh.deploy();
23+
await test.runOnce("my-plugin-deploy", async () => {
24+
await rhdh.configure({ auth: "keycloak" });
25+
await rhdh.deploy();
26+
});
2527
});
2628

2729
test("access rhdh", async ({ rhdh }) => {
@@ -80,6 +82,30 @@ test("using baseURL", async ({ page, baseURL }) => {
8082
});
8183
```
8284

85+
## `test.runOnce`
86+
87+
```typescript
88+
test.runOnce(key: string, fn: () => Promise<void> | void): Promise<boolean>
89+
```
90+
91+
Executes `fn` exactly once per test run, even across worker restarts. Returns `true` if executed, `false` if skipped. Useful for expensive or persistent operations (deployments, database seeding, service provisioning) that should not repeat after a worker restart.
92+
93+
| Parameter | Type | Description |
94+
|-----------|------|-------------|
95+
| `key` | `string` | Unique identifier for this operation |
96+
| `fn` | `() => Promise<void> \| void` | Function to execute once |
97+
98+
```typescript
99+
test.beforeAll(async ({ rhdh }) => {
100+
await test.runOnce("my-deploy", async () => {
101+
await rhdh.configure({ auth: "keycloak" });
102+
await rhdh.deploy();
103+
});
104+
});
105+
```
106+
107+
See [Playwright Fixtures — `test.runOnce`](/guide/core-concepts/playwright-fixtures#test-runonce-—-execute-a-function-once-per-test-run) for detailed usage and examples.
108+
83109
## Exported Types
84110

85111
```typescript
@@ -95,8 +121,10 @@ import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";
95121

96122
test.describe("My Tests", () => {
97123
test.beforeAll(async ({ rhdh }) => {
98-
await rhdh.configure({ auth: "keycloak" });
99-
await rhdh.deploy();
124+
await test.runOnce("my-plugin-deploy", async () => {
125+
await rhdh.configure({ auth: "keycloak" });
126+
await rhdh.deploy();
127+
});
100128
});
101129

102130
test.beforeEach(async ({ page, loginHelper }) => {

docs/changelog.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [1.1.13] - Current
5+
## [1.1.14] - Current
6+
7+
### Added
8+
- **`test.runOnce(key, fn)`**: Execute a function exactly once per test run, even across worker restarts. Prevents re-running expensive operations (deployments, service provisioning, data seeding) when Playwright creates new workers after test failures.
9+
- **Teardown reporter**: Built-in Playwright reporter that automatically deletes Kubernetes namespaces after all tests complete. Active only in CI (`process.env.CI`).
10+
- **`registerTeardownNamespace(projectName, namespace)`**: Register custom namespaces for automatic cleanup. Import from `@red-hat-developer-hub/e2e-test-utils/teardown`.
11+
12+
### Changed
13+
- Namespace cleanup moved from worker fixture to teardown reporter to prevent premature deletion on test failures.
14+
15+
## [1.1.13]
616

717
### Added
818
- Support for GitHub authentication provider

docs/guide/core-concepts/playwright-fixtures.md

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,103 @@ export default defineConfig({
154154
});
155155
```
156156

157-
## Auto-Cleanup
157+
## `test.runOnce` — Execute a Function Once Per Test Run
158158

159-
In CI environments (when `CI` environment variable is set):
159+
Playwright's `beforeAll` runs once **per worker**, not once per test run. When a test fails, Playwright kills the worker and creates a new one for remaining tests — causing `beforeAll` to run again. For operations that are expensive or produce persistent side effects, this leads to unnecessary re-execution.
160160

161-
- Namespaces are automatically deleted after tests complete
162-
- Prevents resource accumulation on shared clusters
161+
`test.runOnce` ensures a function executes **exactly once per test run**, even across worker restarts:
163162

164-
For local development:
165-
- Namespaces are preserved for debugging
166-
- Manual cleanup may be required
163+
```typescript
164+
test.beforeAll(async ({ rhdh }) => {
165+
await test.runOnce("my-plugin-deploy", async () => {
166+
await rhdh.configure({ auth: "keycloak" });
167+
await rhdh.deploy();
168+
});
169+
});
170+
```
171+
172+
### How It Works
173+
174+
- Uses file-based flags scoped to the Playwright runner process
175+
- When a worker restarts after a test failure, `runOnce` detects the flag and skips
176+
- Any state created by the function (deployments, databases, services) stays alive
177+
- Flags reset automatically between test runs
178+
179+
### When to Use
180+
181+
Use `test.runOnce` when your `beforeAll` performs an operation that:
182+
- Is **expensive** (deployments, database seeding, service provisioning)
183+
- Creates **persistent state** that survives beyond the worker process (Kubernetes resources, external services, test data)
184+
- Should **not repeat** once successfully completed
185+
186+
Common examples:
187+
- RHDH deployment (`rhdh.deploy()`)
188+
- External service deployment (customization providers, mock APIs)
189+
- Database seeding or migration
190+
- Any setup script that takes significant time
191+
192+
### Key: Unique Identifier
193+
194+
The `key` parameter must be unique across all `runOnce` calls in your test run. Use a descriptive name:
195+
196+
```typescript
197+
// Deploy RHDH
198+
await test.runOnce("tech-radar-deploy", async () => {
199+
await rhdh.deploy();
200+
});
201+
202+
// Deploy an external service
203+
await test.runOnce("tech-radar-data-provider", async () => {
204+
await $`bash ${setupScript} ${namespace}`;
205+
});
206+
207+
// Seed test data
208+
await test.runOnce("catalog-seed-data", async () => {
209+
await apiHelper.importEntity("https://example.com/catalog-info.yaml");
210+
});
211+
```
212+
213+
## Namespace Cleanup (Teardown)
214+
215+
In CI environments (`CI` environment variable is set), namespaces are automatically deleted after all tests complete. This is handled by a built-in **teardown reporter** that:
216+
217+
1. Runs in the main Playwright process (survives worker restarts)
218+
2. Waits for **all tests** in a project to finish
219+
3. Deletes the namespace matching the project name
220+
221+
### Default Behavior
222+
223+
No configuration needed. The namespace is derived from your project name:
224+
225+
```typescript
226+
// playwright.config.ts
227+
projects: [
228+
{ name: "tech-radar" }, // Namespace "tech-radar" deleted after all tests
229+
{ name: "catalog" }, // Namespace "catalog" deleted after all tests
230+
]
231+
```
232+
233+
### Custom Namespaces
234+
235+
If you deploy to a namespace that differs from the project name, register it for cleanup:
236+
237+
```typescript
238+
import { registerTeardownNamespace } from "@red-hat-developer-hub/e2e-test-utils/teardown";
239+
240+
test.beforeAll(async ({ rhdh }) => {
241+
await test.runOnce("custom-deploy", async () => {
242+
await rhdh.configure({ namespace: "my-custom-ns", auth: "keycloak" });
243+
await rhdh.deploy();
244+
registerTeardownNamespace("my-project", "my-custom-ns");
245+
});
246+
});
247+
```
248+
249+
Multiple namespaces per project are supported — all registered namespaces are deleted after that project's tests complete.
250+
251+
### Local Development
252+
253+
Namespaces are **not** deleted locally (only in CI). This preserves deployments for debugging.
167254

168255
## Best Practices for Projects and Spec Files
169256

@@ -213,12 +300,14 @@ import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";
213300

214301
test.describe("My Plugin Tests", () => {
215302
test.beforeAll(async ({ rhdh }) => {
216-
await rhdh.configure({
217-
auth: "keycloak",
218-
appConfig: "tests/config/app-config.yaml",
219-
dynamicPlugins: "tests/config/plugins.yaml",
303+
await test.runOnce("my-plugin-deploy", async () => {
304+
await rhdh.configure({
305+
auth: "keycloak",
306+
appConfig: "tests/config/app-config.yaml",
307+
dynamicPlugins: "tests/config/plugins.yaml",
308+
});
309+
await rhdh.deploy();
220310
});
221-
await rhdh.deploy();
222311
});
223312

224313
test.beforeEach(async ({ page, loginHelper }) => {

docs/guide/deployment/rhdh-deployment.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import { test } from "@red-hat-developer-hub/e2e-test-utils/test";
3333

3434
test.beforeAll(async ({ rhdh }) => {
3535
// rhdh is already instantiated with namespace from project name
36-
await rhdh.configure({ auth: "keycloak" });
37-
await rhdh.deploy();
36+
await test.runOnce("my-plugin-deploy", async () => {
37+
await rhdh.configure({ auth: "keycloak" });
38+
await rhdh.deploy();
39+
});
3840
});
3941

4042
test("example", async ({ rhdh }) => {
@@ -176,7 +178,7 @@ await deployment.teardown();
176178
```
177179

178180
::: warning
179-
This permanently deletes all resources in the namespace. In CI, this happens automatically.
181+
You typically don't need to call this manually. In CI, the built-in teardown reporter automatically deletes namespaces after all tests complete. See [Namespace Cleanup](/guide/core-concepts/playwright-fixtures#namespace-cleanup-teardown).
180182
:::
181183

182184
## Properties
@@ -259,21 +261,23 @@ import { test } from "@red-hat-developer-hub/e2e-test-utils/test";
259261
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";
260262

261263
test.beforeAll(async ({ rhdh }) => {
262-
const namespace = rhdh.deploymentConfig.namespace;
264+
await test.runOnce("my-plugin-deploy", async () => {
265+
const namespace = rhdh.deploymentConfig.namespace;
263266

264-
// Configure RHDH
265-
await rhdh.configure({ auth: "keycloak" });
267+
// Configure RHDH
268+
await rhdh.configure({ auth: "keycloak" });
266269

267-
// Run custom setup before deployment
268-
await $`bash scripts/setup.sh ${namespace}`;
270+
// Run custom setup before deployment
271+
await $`bash scripts/setup.sh ${namespace}`;
269272

270-
// Set runtime environment variables
271-
process.env.MY_CUSTOM_URL = await rhdh.k8sClient.getRouteLocation(
272-
namespace,
273-
"my-service"
274-
);
273+
// Set runtime environment variables
274+
process.env.MY_CUSTOM_URL = await rhdh.k8sClient.getRouteLocation(
275+
namespace,
276+
"my-service"
277+
);
275278

276-
// Deploy RHDH (uses env vars set above)
277-
await rhdh.deploy();
279+
// Deploy RHDH (uses env vars set above)
280+
await rhdh.deploy();
281+
});
278282
});
279283
```

0 commit comments

Comments
 (0)