Skip to content
Merged
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
6 changes: 5 additions & 1 deletion docs/guide/core-concepts/playwright-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,15 @@ test.describe("Feature B", () => {

### Key: Unique Identifier

The `key` parameter must be unique across all `runOnce` calls in your test run. Use a descriptive name that reflects the operation:
The `key` must be globally unique across **all spec files and projects** in the same Playwright run. If two `runOnce` calls in different files use the same key, only the first one will execute. Use a prefix that includes the workspace or project name:

```typescript
// In tech-radar.spec.ts
await test.runOnce("tech-radar-deploy", async () => { ... });
await test.runOnce("tech-radar-data-provider", async () => { ... });

// In catalog.spec.ts
await test.runOnce("catalog-deploy", async () => { ... });
await test.runOnce("catalog-seed-data", async () => { ... });
```

Expand Down
44 changes: 23 additions & 21 deletions docs/overlay/examples/tech-radar.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,28 +222,30 @@ const setupScript = path.join(
);

test.describe("Test tech-radar plugin", () => {
// beforeAll runs once per worker before any tests
// Wrap in runOnce — the external service deployment is expensive
// and should not re-run when Playwright restarts the worker after a test failure
test.beforeAll(async ({ rhdh }) => {
// Get the namespace from deployment config
const project = rhdh.deploymentConfig.namespace;

// Configure RHDH with Keycloak authentication
await rhdh.configure({ auth: "keycloak" });

// Deploy the external data provider service
await $`bash ${setupScript} ${project}`;

// Get the route URL and set as environment variable
// Remove http:// prefix as the config expects just the host
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");

// Now deploy RHDH (will use the TECH_RADAR_DATA_URL env var)
await rhdh.deploy();
await test.runOnce("tech-radar-setup", async () => {
const project = rhdh.deploymentConfig.namespace;

// Configure RHDH with Keycloak authentication
await rhdh.configure({ auth: "keycloak" });

// Deploy the external data provider service
await $`bash ${setupScript} ${project}`;

// Get the route URL and set as environment variable
// Remove http:// prefix as the config expects just the host
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");

// Deploy RHDH (will use the TECH_RADAR_DATA_URL env var)
await rhdh.deploy();
});
});

// beforeEach runs before each test
Expand Down
128 changes: 71 additions & 57 deletions docs/overlay/tutorials/custom-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,32 @@ Your plugin tests may require pre-requisites when:

## The Pattern

Deploy pre-requisites **after** `rhdh.configure()` but **before** `rhdh.deploy()`:
Deploy pre-requisites **after** `rhdh.configure()` but **before** `rhdh.deploy()`. Since these operations are expensive and create persistent cluster resources, wrap the entire block in `test.runOnce` to prevent re-execution on worker restarts:

```typescript
test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
await test.runOnce("my-plugin-setup", async () => {
const project = rhdh.deploymentConfig.namespace;

// 1. Configure RHDH first
await rhdh.configure({ auth: "keycloak" });
// 1. Configure RHDH first
await rhdh.configure({ auth: "keycloak" });

// 2. Deploy pre-requisite service (see examples below)
// ...
// 2. Deploy pre-requisite service (see examples below)
// ...

// 3. Set environment variable if needed for RHDH config
process.env.MY_SERVICE_URL = "...";
// 3. Set environment variable if needed for RHDH config
process.env.MY_SERVICE_URL = "...";

// 4. Deploy RHDH (uses the environment variable)
await rhdh.deploy();
// 4. Deploy RHDH (uses the environment variable)
await rhdh.deploy();
});
});
```

::: tip When is `test.runOnce` needed?
`rhdh.deploy()` already skips automatically on worker restarts. But the pre-deploy steps (deploying external services, running scripts) don't have this protection. `test.runOnce` ensures the **entire setup** runs only once. The `key` must be **globally unique** across all spec files and projects in the same Playwright run — prefix it with your workspace name (e.g., `"tech-radar-setup"`). See [`test.runOnce`](/guide/core-concepts/playwright-fixtures#test-runonce-—-run-any-expensive-operation-once) for details.
:::

## Examples

### Using TypeScript / k8sClient
Expand All @@ -48,26 +54,28 @@ You can deploy pre-requisites directly in TypeScript using the Kubernetes client
import { test } from "@red-hat-developer-hub/e2e-test-utils/test";

test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
const k8s = rhdh.k8sClient;

await rhdh.configure({ auth: "keycloak" });

// Create a ConfigMap
await k8s.applyConfigMapFromObject(
"my-config",
{ "config.json": JSON.stringify({ key: "value" }) },
project,
);

// Create a Secret
await k8s.applySecretFromObject(
"my-secret",
{ stringData: { API_KEY: process.env.VAULT_API_KEY } },
project,
);

await rhdh.deploy();
await test.runOnce("my-plugin-k8s-setup", async () => {
const project = rhdh.deploymentConfig.namespace;
const k8s = rhdh.k8sClient;

await rhdh.configure({ auth: "keycloak" });

// Create a ConfigMap
await k8s.applyConfigMapFromObject(
"my-config",
{ "config.json": JSON.stringify({ key: "value" }) },
project,
);

// Create a Secret
await k8s.applySecretFromObject(
"my-secret",
{ stringData: { API_KEY: process.env.VAULT_API_KEY } },
project,
);

await rhdh.deploy();
});
});
```

Expand All @@ -80,21 +88,23 @@ import { test } from "@red-hat-developer-hub/e2e-test-utils/test";
import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils";

test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
await test.runOnce("my-plugin-oc-setup", async () => {
const project = rhdh.deploymentConfig.namespace;

await rhdh.configure({ auth: "keycloak" });
await rhdh.configure({ auth: "keycloak" });

// Deploy an app from image
await $`oc new-app my-image:tag --name=my-service --namespace=${project}`;
await $`oc expose svc/my-service --namespace=${project}`;
// Deploy an app from image
await $`oc new-app my-image:tag --name=my-service --namespace=${project}`;
await $`oc expose svc/my-service --namespace=${project}`;

// Get service URL
process.env.MY_SERVICE_URL = await rhdh.k8sClient.getRouteLocation(
project,
"my-service",
);
// Get service URL
process.env.MY_SERVICE_URL = await rhdh.k8sClient.getRouteLocation(
project,
"my-service",
);

await rhdh.deploy();
await rhdh.deploy();
});
});
```

Expand All @@ -109,11 +119,13 @@ import path from "path";
const setupScript = path.join(import.meta.dirname, "deploy-service.sh");

test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
await test.runOnce("my-plugin-script-setup", async () => {
const project = rhdh.deploymentConfig.namespace;

await rhdh.configure({ auth: "keycloak" });
await $`bash ${setupScript} ${project}`;
await rhdh.deploy();
await rhdh.configure({ auth: "keycloak" });
await $`bash ${setupScript} ${project}`;
await rhdh.deploy();
});
});
```

Expand All @@ -123,22 +135,24 @@ The tech-radar plugin requires an external data provider:

```typescript
test.beforeAll(async ({ rhdh }) => {
const project = rhdh.deploymentConfig.namespace;
await test.runOnce("tech-radar-setup", async () => {
const project = rhdh.deploymentConfig.namespace;

await rhdh.configure({ auth: "keycloak" });
await rhdh.configure({ auth: "keycloak" });

// Deploy the data provider service
await $`bash ${setupScript} ${project}`;
// Deploy the data provider service
await $`bash ${setupScript} ${project}`;

// Get URL and set as env var for rhdh-secrets.yaml substitution
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");
// Get URL and set as env var for rhdh-secrets.yaml substitution
process.env.TECH_RADAR_DATA_URL = (
await rhdh.k8sClient.getRouteLocation(
project,
"test-backstage-customization-provider",
)
).replace("http://", "");

await rhdh.deploy();
await rhdh.deploy();
});
});
```

Expand Down