Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default defineConfig({
{ text: "Examples", link: "/examples/" },
{ text: "Overlay Testing", link: "/overlay/" },
{
text: "v1.1.30",
text: "v1.1.32",
items: [{ text: "Changelog", link: "/changelog" }],
},
],
Expand Down
7 changes: 6 additions & 1 deletion docs/api/deployment/rhdh-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ await rhdh.configure({
### `deploy()`

```typescript
async deploy(options?: { timeout?: number | null }): Promise<void>
async deploy(options?: { timeout?: number | null; force?: boolean }): Promise<void>
```

Deploy RHDH to the cluster. This:
Expand All @@ -83,6 +83,7 @@ Deploy RHDH to the cluster. This:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `options.timeout` | `number \| null` | `600_000` | Playwright test timeout (ms) for the deployment. Pass a custom number to override, `0` for no timeout, or `null` to skip and let the consumer control the timeout. |
| `options.force` | `boolean` | `false` | Force redeployment even if already deployed. Bypasses the built-in `runOnce` protection. Useful for complex test scenarios where multiple `describe` sections need different RHDH configurations. |

```typescript
// Default (600s timeout)
Expand All @@ -97,6 +98,10 @@ await rhdh.deploy({ timeout: 0 });
// Skip — consumer controls the timeout
test.setTimeout(900_000);
await rhdh.deploy({ timeout: null });

// Force redeploy with new configuration
await rhdh.configure({ dynamicPlugins: "tests/config/new-plugins.yaml" });
await rhdh.deploy({ force: true });
```

### `waitUntilReady()`
Expand Down
10 changes: 9 additions & 1 deletion docs/api/playwright/test-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";

**Type:** `RHDHDeployment`

Shared RHDH deployment across all tests in a worker. `deploy()` automatically skips if the deployment already succeeded, even after worker restarts.
Shared RHDH deployment across all tests in a worker. `deploy()` automatically skips if the deployment already succeeded, even after worker restarts. Use `deploy({ force: true })` to bypass this protection when you need to redeploy with different configurations in the same test file.

```typescript
test.beforeAll(async ({ rhdh }) => {
Expand All @@ -28,6 +28,14 @@ test("access rhdh", async ({ rhdh }) => {
console.log(rhdh.rhdhUrl);
console.log(rhdh.deploymentConfig.namespace);
});

// Force redeploy with new config
test.describe("Different Config", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({ appConfig: "tests/config/new-app-config-rhdh.yaml" });
await rhdh.deploy({ force: true });
});
});
```

### `uiHelper`
Expand Down
8 changes: 7 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

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

## [1.1.31] - Current
## [1.1.32] - Current

### Added

- **Force redeploy option for `rhdh.deploy()`**: Added optional `force` parameter to `rhdh.deploy({ force: true })` to bypass the built-in `runOnce` protection and force a fresh deployment. This enables complex test scenarios where multiple `describe` sections need different RHDH configurations (different app configs or dynamic plugin sets) within the same test file. Test writers can now call `rhdh.configure()` with new settings and `rhdh.deploy({ force: true })` to redeploy with the desired configuration.

## [1.1.31]

### Fixed

Expand Down
32 changes: 32 additions & 0 deletions docs/guide/core-concepts/playwright-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,38 @@ test.beforeAll(async ({ rhdh }) => {
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. Without protection, this would re-deploy RHDH from scratch every time a test fails.
:::

### Bypassing Protection with `force`

For complex test scenarios where multiple `describe` sections need different RHDH configurations (different app configs or dynamic plugin sets), you can use the `force` option to bypass the built-in protection:

```typescript
test.describe("Plugin Set A", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
dynamicPlugins: "tests/config/plugins-set-a.yaml",
});
await rhdh.deploy(); // First deployment
});

test("test with plugin set A", async ({ page }) => {
// Tests using plugin set A
});
});

test.describe("Plugin Set B", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
dynamicPlugins: "tests/config/plugins-set-b.yaml",
});
await rhdh.deploy({ force: true }); // Force redeploy with new config
});

test("test with plugin set B", async ({ page }) => {
// Tests using plugin set B
});
});
```

## `test.runOnce` — Run Any Expensive Operation Once

While `rhdh.deploy()` has built-in protection, you may have **other expensive operations** in your `beforeAll` that also shouldn't repeat on worker restart — deploying external services, seeding databases, running setup scripts, etc.
Expand Down
41 changes: 40 additions & 1 deletion docs/guide/deployment/rhdh-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ Deploy RHDH to the cluster:
await deployment.deploy();
```

The `deploy()` method accepts an optional `{ timeout }` parameter to control the Playwright test timeout during deployment. By default, it sets the timeout to 600 seconds (10 minutes).
The `deploy()` method accepts optional parameters:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `timeout` | `number \| null` | `600_000` | Playwright test timeout (ms) during deployment |
| `force` | `boolean` | `false` | Force redeployment even if already deployed |

```typescript
// Default (600s)
Expand All @@ -112,6 +117,40 @@ await rhdh.deploy({ timeout: null });

`deploy()` automatically skips if the deployment already succeeded in the current test run (e.g., after a worker restart due to test failure). This prevents expensive re-deployments.

#### Force Redeploy

Use the `force` option to bypass the built-in `runOnce` protection and force a fresh deployment. This is useful for complex test scenarios where multiple `describe` sections need different RHDH configurations (different app configs or dynamic plugin sets) within the same test file:

```typescript
test.describe("Plugin Set A", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
auth: "keycloak",
dynamicPlugins: "tests/config/plugins-set-a.yaml",
});
await rhdh.deploy(); // First deployment
});

test("test with plugin set A", async ({ page }) => {
// Tests using plugin set A
});
});

test.describe("Plugin Set B", () => {
test.beforeAll(async ({ rhdh }) => {
await rhdh.configure({
auth: "keycloak",
dynamicPlugins: "tests/config/plugins-set-b.yaml",
});
await rhdh.deploy({ force: true }); // Force redeploy with new config
});

test("test with plugin set B", async ({ page }) => {
// Tests using plugin set B
});
});
```

This method:
1. Merges configuration files (common → auth → project)
2. [Injects plugin metadata](/guide/configuration/config-files#plugin-metadata-injection) into dynamic plugins config
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@red-hat-developer-hub/e2e-test-utils",
"version": "1.1.31",
"version": "1.1.32",
"description": "Test utilities for RHDH E2E tests",
"license": "Apache-2.0",
"repository": {
Expand Down
62 changes: 37 additions & 25 deletions src/deployment/rhdh/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,40 +39,52 @@ export class RHDHDeployment {
this.rhdhUrl = this._buildBaseUrl();
}

async deploy(options?: { timeout?: number | null }): Promise<void> {
async deploy(options?: {
timeout?: number | null;
forceUpdate?: boolean;
}): Promise<void> {
// Default 600s, custom number to override, null to skip and let consumer control the timeout
const timeout = options?.timeout === undefined ? 600_000 : options.timeout;
if (timeout !== null) {
test.setTimeout(timeout);
}

const executed = await runOnce(
`deploy-${this.deploymentConfig.namespace}`,
async () => {
this._log("Starting RHDH deployment...");
this._log("RHDH Base URL: " + this.rhdhUrl);
console.table(this.deploymentConfig);
const deployFunc = async () => {
this._log("Starting RHDH deployment...");
this._log("RHDH Base URL: " + this.rhdhUrl);
console.table(this.deploymentConfig);

await this.k8sClient.createNamespaceIfNotExists(
this.deploymentConfig.namespace,
);
await this.k8sClient.createNamespaceIfNotExists(
this.deploymentConfig.namespace,
);

await this._applyAppConfig();
await this._applySecrets();

await this._applyAppConfig();
await this._applySecrets();

if (this.deploymentConfig.method === "helm") {
const isUpgrade = await this._deploymentExists();
await this._deployWithHelm(this.deploymentConfig.valueFile);
if (isUpgrade) {
await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes
}
} else {
await this._applyDynamicPlugins();
await this._deployWithOperator(this.deploymentConfig.subscription);
if (this.deploymentConfig.method === "helm") {
const isUpgrade = await this._deploymentExists();
await this._deployWithHelm(this.deploymentConfig.valueFile);
if (isUpgrade) {
await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes
}
await this.waitUntilReady();
},
);
} else {
await this._applyDynamicPlugins();
await this._deployWithOperator(this.deploymentConfig.subscription);
}
await this.waitUntilReady();
};

let executed: boolean;

if (options?.forceUpdate) {
await deployFunc();
executed = true;
} else {
executed = await runOnce(
`deploy-${this.deploymentConfig.namespace}`,
deployFunc,
);
}

if (!executed) {
this._log(
Expand Down
Loading