rhdh-e2e-test-utils 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,1170 +1,6 @@
|
|
|
1
1
|
# rhdh-e2e-test-utils
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Documentation is available online:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- [Overview](#overview)
|
|
8
|
-
- [Features](#features)
|
|
9
|
-
- [Installation](#installation)
|
|
10
|
-
- [Requirements](#requirements)
|
|
11
|
-
- [Package Exports](#package-exports)
|
|
12
|
-
- [Quick Start](#quick-start)
|
|
13
|
-
- [Detailed Usage](#detailed-usage)
|
|
14
|
-
- [Playwright Test Fixtures](#playwright-test-fixtures)
|
|
15
|
-
- [Playwright Configuration](#playwright-configuration)
|
|
16
|
-
- [Global Setup](#global-setup)
|
|
17
|
-
- [RHDH Deployment](#rhdh-deployment)
|
|
18
|
-
- [Keycloak Deployment](#keycloak-deployment)
|
|
19
|
-
- [Utilities](#utilities)
|
|
20
|
-
- [Helpers](#helpers)
|
|
21
|
-
- [Page Objects](#page-objects)
|
|
22
|
-
- [ESLint Configuration](#eslint-configuration)
|
|
23
|
-
- [TypeScript Configuration](#typescript-configuration)
|
|
24
|
-
- [Configuration Files](#configuration-files)
|
|
25
|
-
- [Environment Variables](#environment-variables)
|
|
26
|
-
- [Examples](#examples)
|
|
27
|
-
- [Development](#development)
|
|
28
|
-
- [Testing Local Changes in Consumer Projects](#testing-local-changes-in-consumer-projects)
|
|
29
|
-
|
|
30
|
-
## Overview
|
|
31
|
-
|
|
32
|
-
`rhdh-e2e-test-utils` simplifies end-to-end testing for RHDH plugins by providing:
|
|
33
|
-
|
|
34
|
-
- **Automated RHDH Deployment**: Deploy RHDH instances via Helm or the RHDH Operator
|
|
35
|
-
- **Keycloak Integration**: Deploy and configure Keycloak for OIDC authentication testing
|
|
36
|
-
- **Modular Auth Configuration**: Switch between guest and Keycloak authentication with a single option
|
|
37
|
-
- **Playwright Integration**: Custom test fixtures that manage deployment lifecycle
|
|
38
|
-
- **Kubernetes Utilities**: Helper functions for managing namespaces, ConfigMaps, Secrets, and Routes
|
|
39
|
-
- **Configuration Merging**: YAML merging with environment variable substitution
|
|
40
|
-
- **Standardized ESLint Rules**: Pre-configured linting for Playwright tests
|
|
41
|
-
|
|
42
|
-
## Features
|
|
43
|
-
|
|
44
|
-
- Deploy RHDH using Helm charts or the RHDH Operator
|
|
45
|
-
- Deploy Keycloak for authentication testing with automatic realm, client, and user configuration
|
|
46
|
-
- Modular authentication configuration (guest, Keycloak)
|
|
47
|
-
- Automatic namespace creation and cleanup
|
|
48
|
-
- Dynamic plugin configuration
|
|
49
|
-
- Helpers for UI, API and common Utils
|
|
50
|
-
- Kubernetes client helper for OpenShift resources
|
|
51
|
-
- Pre-configured Playwright settings optimized for RHDH testing
|
|
52
|
-
- ESLint configuration with Playwright and TypeScript best practices
|
|
53
|
-
|
|
54
|
-
## Installation
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npm install rhdh-e2e-test-utils
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Or directly from GitHub:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npm install github:redhat-developer/rhdh-e2e-test-utils#main
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Requirements
|
|
67
|
-
|
|
68
|
-
### System Requirements
|
|
69
|
-
|
|
70
|
-
- **Node.js**: >= 22
|
|
71
|
-
- **Yarn**: >= 3 (this project uses Yarn 3 with Corepack)
|
|
72
|
-
|
|
73
|
-
### OpenShift Cluster
|
|
74
|
-
|
|
75
|
-
You must be logged into an OpenShift cluster with sufficient permissions to:
|
|
76
|
-
|
|
77
|
-
- Create and delete namespaces
|
|
78
|
-
- Create ConfigMaps and Secrets
|
|
79
|
-
- Install Helm charts or use the RHDH Operator
|
|
80
|
-
- Read cluster ingress configuration
|
|
81
|
-
|
|
82
|
-
## Package Exports
|
|
83
|
-
|
|
84
|
-
The package provides multiple entry points for different use cases:
|
|
85
|
-
|
|
86
|
-
| Export Path | Description |
|
|
87
|
-
|-------------|-------------|
|
|
88
|
-
| `rhdh-e2e-test-utils/test` | Playwright test fixtures with RHDH deployment |
|
|
89
|
-
| `rhdh-e2e-test-utils/playwright-config` | Base Playwright configuration |
|
|
90
|
-
| `rhdh-e2e-test-utils/rhdh` | RHDH deployment class and types |
|
|
91
|
-
| `rhdh-e2e-test-utils/keycloak` | Keycloak deployment helper for authentication testing |
|
|
92
|
-
| `rhdh-e2e-test-utils/utils` | Utility functions (bash, YAML, Kubernetes) |
|
|
93
|
-
| `rhdh-e2e-test-utils/helpers` | UI, API, and login helper classes |
|
|
94
|
-
| `rhdh-e2e-test-utils/pages` | Page object classes for common RHDH pages |
|
|
95
|
-
| `rhdh-e2e-test-utils/eslint` | ESLint configuration factory |
|
|
96
|
-
| `rhdh-e2e-test-utils/tsconfig` | Base TypeScript configuration |
|
|
97
|
-
|
|
98
|
-
## Quick Start
|
|
99
|
-
|
|
100
|
-
### 1. Set Up Your E2E Test Project
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
mkdir e2e-tests && cd e2e-tests
|
|
104
|
-
yarn init -y
|
|
105
|
-
yarn add @playwright/test rhdh-e2e-test-utils
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 2. Create Playwright Configuration
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// playwright.config.ts
|
|
112
|
-
import { defineConfig } from "rhdh-e2e-test-utils/playwright-config";
|
|
113
|
-
|
|
114
|
-
export default defineConfig({
|
|
115
|
-
projects: [
|
|
116
|
-
{
|
|
117
|
-
name: "my-plugin",
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 3. Create Your Test
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// tests/my-plugin.spec.ts
|
|
127
|
-
import { test, expect } from "rhdh-e2e-test-utils/test";
|
|
128
|
-
|
|
129
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
130
|
-
await rhdh.deploy();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("my plugin test", async ({ page }) => {
|
|
134
|
-
await page.goto("/");
|
|
135
|
-
await expect(page).toHaveTitle(/Red Hat Developer Hub/);
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 4. Create Configuration Files
|
|
140
|
-
|
|
141
|
-
Create a `tests/config/` directory with your RHDH configuration:
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
tests/config/
|
|
145
|
-
├── app-config-rhdh.yaml # App configuration
|
|
146
|
-
├── dynamic-plugins.yaml # Dynamic plugins configuration
|
|
147
|
-
└── rhdh-secrets.yaml # Secrets (with env var placeholders)
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### 5. Set Environment Variables
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
export RHDH_VERSION="1.5" # RHDH version
|
|
154
|
-
export INSTALLATION_METHOD="helm" # "helm" or "operator"
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### 6. Run Tests
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
yarn playwright test
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Detailed Usage
|
|
164
|
-
|
|
165
|
-
### Playwright Test Fixtures
|
|
166
|
-
|
|
167
|
-
The package extends Playwright's test with RHDH-specific fixtures:
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { test, expect } from "rhdh-e2e-test-utils/test";
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
#### Available Fixtures
|
|
174
|
-
|
|
175
|
-
| Fixture | Scope | Description |
|
|
176
|
-
|---------|-------|-------------|
|
|
177
|
-
| `rhdh` | worker | Shared RHDHDeployment across all tests in a worker |
|
|
178
|
-
| `uiHelper` | test | UIhelper instance for common UI interactions |
|
|
179
|
-
| `loginHelper` | test | LoginHelper instance for authentication flows |
|
|
180
|
-
| `baseURL` | test | Automatically set to the RHDH instance URL |
|
|
181
|
-
|
|
182
|
-
#### Fixture Behavior
|
|
183
|
-
|
|
184
|
-
- **Automatic Namespace**: The namespace is derived from the Playwright project name
|
|
185
|
-
- **Auto-cleanup**: In CI environments, namespaces are automatically deleted after tests
|
|
186
|
-
- **Shared Deployment**: All tests in a worker share the same RHDH deployment
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
import { test, expect } from "rhdh-e2e-test-utils/test";
|
|
190
|
-
|
|
191
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
192
|
-
// Configure RHDH (creates namespace, and optional DeploymentOptions)
|
|
193
|
-
await rhdh.configure();
|
|
194
|
-
|
|
195
|
-
// Perform any pre-deployment setup
|
|
196
|
-
// ...
|
|
197
|
-
|
|
198
|
-
// Deploy RHDH
|
|
199
|
-
await rhdh.deploy();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("example test", async ({ page, rhdh, uiHelper, loginHelper }) => {
|
|
203
|
-
// page.goto("/") will use rhdh.rhdhUrl as base
|
|
204
|
-
await page.goto("/");
|
|
205
|
-
|
|
206
|
-
// Login as guest user
|
|
207
|
-
await loginHelper.loginAsGuest();
|
|
208
|
-
|
|
209
|
-
// Use UI helper for common interactions
|
|
210
|
-
await uiHelper.verifyHeading("Welcome");
|
|
211
|
-
await uiHelper.clickButton("Get Started");
|
|
212
|
-
|
|
213
|
-
// Access deployment info
|
|
214
|
-
console.log(`Namespace: ${rhdh.deploymentConfig.namespace}`);
|
|
215
|
-
console.log(`URL: ${rhdh.rhdhUrl}`);
|
|
216
|
-
|
|
217
|
-
// Perform any deployment/config update
|
|
218
|
-
// ...
|
|
219
|
-
await rhdh.rolloutRestart();
|
|
220
|
-
// ...
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Playwright Configuration
|
|
225
|
-
|
|
226
|
-
Use `defineConfig` for sensible defaults:
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// playwright.config.ts
|
|
230
|
-
import { defineConfig } from "rhdh-e2e-test-utils/playwright-config";
|
|
231
|
-
|
|
232
|
-
export default defineConfig({
|
|
233
|
-
projects: [
|
|
234
|
-
{
|
|
235
|
-
name: "tech-radar", // Also used as namespace
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: "catalog",
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
});
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
#### Base Configuration Defaults
|
|
245
|
-
|
|
246
|
-
| Setting | Value |
|
|
247
|
-
|---------|-------|
|
|
248
|
-
| `testDir` | `./tests` |
|
|
249
|
-
| `timeout` | 90,000ms |
|
|
250
|
-
| `retries` | 2 in CI, 0 locally |
|
|
251
|
-
| `workers` | 50% of CPUs |
|
|
252
|
-
| `viewport` | 1920x1080 |
|
|
253
|
-
| `video` | Always on |
|
|
254
|
-
| `trace` | Retain on failure |
|
|
255
|
-
| `screenshot` | Only on failure |
|
|
256
|
-
|
|
257
|
-
#### Global Setup
|
|
258
|
-
|
|
259
|
-
The package includes a global setup function that runs once before all tests. It performs the following:
|
|
260
|
-
|
|
261
|
-
1. **Binary Check**: Verifies that required binaries (`oc`, `kubectl`, `helm`) are installed
|
|
262
|
-
2. **Cluster Router Base**: Fetches the OpenShift ingress domain and sets `K8S_CLUSTER_ROUTER_BASE`
|
|
263
|
-
3. **Keycloak Deployment**: Automatically deploys and configures Keycloak for OIDC authentication
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// playwright.config.ts
|
|
267
|
-
import { defineConfig } from "rhdh-e2e-test-utils/playwright-config";
|
|
268
|
-
|
|
269
|
-
export default defineConfig({
|
|
270
|
-
// Global setup is automatically included
|
|
271
|
-
projects: [{ name: "my-plugin" }],
|
|
272
|
-
});
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
**Keycloak Auto-Deployment Behavior:**
|
|
276
|
-
|
|
277
|
-
- Keycloak is deployed to the `rhdh-keycloak` namespace
|
|
278
|
-
- If Keycloak is already running, deployment is skipped
|
|
279
|
-
- All Keycloak environment variables are automatically set after deployment
|
|
280
|
-
- The following test credentials are created:
|
|
281
|
-
- Username: `test1`, Password: `test1@123`
|
|
282
|
-
- Username: `test2`, Password: `test2@123`
|
|
283
|
-
|
|
284
|
-
**Skip Keycloak Deployment:**
|
|
285
|
-
|
|
286
|
-
If your tests don't require Keycloak/OIDC authentication, set the environment variable:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
SKIP_KEYCLOAK_DEPLOYMENT=true yarn playwright test
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### RHDH Deployment
|
|
293
|
-
|
|
294
|
-
#### RHDHDeployment Class
|
|
295
|
-
|
|
296
|
-
The core class for managing RHDH deployments:
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { RHDHDeployment } from "rhdh-e2e-test-utils/rhdh";
|
|
300
|
-
|
|
301
|
-
// Create deployment with namespace (required)
|
|
302
|
-
const deployment = new RHDHDeployment("my-test-namespace");
|
|
303
|
-
|
|
304
|
-
// Configure with options (call before deploy)
|
|
305
|
-
await deployment.configure({
|
|
306
|
-
version: "1.5", // Optional, uses RHDH_VERSION env
|
|
307
|
-
method: "helm", // Optional, uses INSTALLATION_METHOD env
|
|
308
|
-
auth: "keycloak", // Optional, defaults to "keycloak"
|
|
309
|
-
appConfig: "config/app-config-rhdh.yaml", // Optional
|
|
310
|
-
secrets: "config/rhdh-secrets.yaml", // Optional
|
|
311
|
-
dynamicPlugins: "config/dynamic-plugins.yaml", // Optional
|
|
312
|
-
valueFile: "config/value_file.yaml", // Optional & Helm only
|
|
313
|
-
subscription: "config/subscription.yaml", // Optional & Operator only
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// Deploy RHDH
|
|
317
|
-
await deployment.deploy();
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
#### Deployment Options
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
type DeploymentOptions = {
|
|
324
|
-
namespace?: string; // Kubernetes namespace (set via constructor)
|
|
325
|
-
version?: string; // RHDH version (e.g., "1.5", "1.5.1-CI")
|
|
326
|
-
method?: "helm" | "operator"; // Installation method
|
|
327
|
-
auth?: "guest" | "keycloak"; // Authentication provider (default: "keycloak")
|
|
328
|
-
appConfig?: string; // Path to app-config YAML
|
|
329
|
-
secrets?: string; // Path to secrets YAML
|
|
330
|
-
dynamicPlugins?: string; // Path to dynamic-plugins YAML
|
|
331
|
-
valueFile?: string; // Helm values file (helm only)
|
|
332
|
-
subscription?: string; // Backstage CR file (operator only)
|
|
333
|
-
};
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
#### Authentication Providers
|
|
337
|
-
|
|
338
|
-
The package supports modular authentication configuration. Set the `auth` option to automatically include the appropriate auth-specific configurations:
|
|
339
|
-
|
|
340
|
-
| Provider | Description |
|
|
341
|
-
|----------|-------------|
|
|
342
|
-
| `guest` | Guest authentication for development/testing |
|
|
343
|
-
| `keycloak` | OIDC authentication via Keycloak (default) |
|
|
344
|
-
|
|
345
|
-
Auth-specific configurations are automatically merged with your project configurations. For Keycloak authentication, see [Keycloak Deployment](#keycloak-deployment).
|
|
346
|
-
|
|
347
|
-
#### Deployment Methods
|
|
348
|
-
|
|
349
|
-
##### Helm Deployment
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
const rhdh = new RHDHDeployment({
|
|
353
|
-
namespace: "my-plugin-tests",
|
|
354
|
-
method: "helm",
|
|
355
|
-
valueFile: "config/value_file.yaml",
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
await rhdh.deploy();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
##### Operator Deployment
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
const rhdh = new RHDHDeployment({
|
|
365
|
-
namespace: "my-plugin-tests",
|
|
366
|
-
method: "operator",
|
|
367
|
-
subscription: "config/subscription.yaml",
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
await rhdh.deploy();
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
#### RHDHDeployment API
|
|
374
|
-
|
|
375
|
-
| Method | Description |
|
|
376
|
-
|--------|-------------|
|
|
377
|
-
| `configure(options?)` | Create namespace and prepare for deployment |
|
|
378
|
-
| `deploy()` | Full deployment (configs, secrets, plugins, RHDH) |
|
|
379
|
-
| `waitUntilReady(timeout?)` | Wait for deployment to be ready |
|
|
380
|
-
| `rolloutRestart()` | Restart the RHDH deployment |
|
|
381
|
-
| `teardown()` | Delete the namespace and all resources |
|
|
382
|
-
|
|
383
|
-
#### Properties
|
|
384
|
-
|
|
385
|
-
| Property | Type | Description |
|
|
386
|
-
|----------|------|-------------|
|
|
387
|
-
| `rhdhUrl` | `string` | The RHDH instance URL |
|
|
388
|
-
| `deploymentConfig` | `DeploymentConfig` | Current deployment configuration |
|
|
389
|
-
| `k8sClient` | `KubernetesClientHelper` | Kubernetes client instance |
|
|
390
|
-
|
|
391
|
-
### Keycloak Deployment
|
|
392
|
-
|
|
393
|
-
The package provides a `KeycloakHelper` class for deploying and configuring Keycloak in OpenShift, enabling OIDC authentication testing with RHDH.
|
|
394
|
-
|
|
395
|
-
#### KeycloakHelper Class
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
import { KeycloakHelper } from "rhdh-e2e-test-utils/keycloak";
|
|
399
|
-
|
|
400
|
-
const keycloak = new KeycloakHelper({
|
|
401
|
-
namespace: "rhdh-keycloak", // Optional, defaults to "rhdh-keycloak"
|
|
402
|
-
releaseName: "keycloak", // Optional, defaults to "keycloak"
|
|
403
|
-
adminUser: "admin", // Optional, defaults to "admin"
|
|
404
|
-
adminPassword: "admin123", // Optional, defaults to "admin123"
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Deploy Keycloak using Bitnami Helm chart
|
|
408
|
-
await keycloak.deploy();
|
|
409
|
-
|
|
410
|
-
// Configure realm, client, groups, and users for RHDH
|
|
411
|
-
await keycloak.configureForRHDH();
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### Deployment Options
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
type KeycloakDeploymentOptions = {
|
|
418
|
-
namespace?: string; // Kubernetes namespace (default: "rhdh-keycloak")
|
|
419
|
-
releaseName?: string; // Helm release name (default: "keycloak")
|
|
420
|
-
valuesFile?: string; // Custom Helm values file
|
|
421
|
-
adminUser?: string; // Admin username (default: "admin")
|
|
422
|
-
adminPassword?: string; // Admin password (default: "admin123")
|
|
423
|
-
};
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
#### KeycloakHelper API
|
|
427
|
-
|
|
428
|
-
| Method | Description |
|
|
429
|
-
|--------|-------------|
|
|
430
|
-
| `deploy()` | Deploy Keycloak using Helm and wait for it to be ready |
|
|
431
|
-
| `configureForRHDH(options?)` | Configure realm, client, groups, and users for RHDH |
|
|
432
|
-
| `isRunning()` | Check if Keycloak is accessible |
|
|
433
|
-
| `connect(config)` | Connect to an existing Keycloak instance |
|
|
434
|
-
| `createRealm(config)` | Create a new realm |
|
|
435
|
-
| `createClient(realm, config)` | Create a client in a realm |
|
|
436
|
-
| `createGroup(realm, config)` | Create a group in a realm |
|
|
437
|
-
| `createUser(realm, config)` | Create a user with optional group membership |
|
|
438
|
-
| `getUsers(realm)` | Get all users in a realm |
|
|
439
|
-
| `getGroups(realm)` | Get all groups in a realm |
|
|
440
|
-
| `deleteUser(realm, username)` | Delete a user |
|
|
441
|
-
| `deleteGroup(realm, groupName)` | Delete a group |
|
|
442
|
-
| `deleteRealm(realm)` | Delete a realm |
|
|
443
|
-
| `teardown()` | Delete the Keycloak namespace |
|
|
444
|
-
| `waitUntilReady(timeout?)` | Wait for Keycloak StatefulSet to be ready |
|
|
445
|
-
|
|
446
|
-
#### Properties
|
|
447
|
-
|
|
448
|
-
| Property | Type | Description |
|
|
449
|
-
|----------|------|-------------|
|
|
450
|
-
| `keycloakUrl` | `string` | The Keycloak instance URL |
|
|
451
|
-
| `realm` | `string` | Configured realm name |
|
|
452
|
-
| `clientId` | `string` | Configured client ID |
|
|
453
|
-
| `clientSecret` | `string` | Configured client secret |
|
|
454
|
-
| `deploymentConfig` | `KeycloakDeploymentConfig` | Current deployment configuration |
|
|
455
|
-
| `k8sClient` | `KubernetesClientHelper` | Kubernetes client instance |
|
|
456
|
-
|
|
457
|
-
#### Default Configuration
|
|
458
|
-
|
|
459
|
-
When using `configureForRHDH()`, the following defaults are applied:
|
|
460
|
-
|
|
461
|
-
**Default Realm**: `rhdh`
|
|
462
|
-
|
|
463
|
-
**Default Client** (`rhdh-client`):
|
|
464
|
-
- Client secret: `rhdh-client-secret`
|
|
465
|
-
- Standard flow, implicit flow, direct access grants enabled
|
|
466
|
-
- Service accounts enabled with realm-management roles
|
|
467
|
-
|
|
468
|
-
**Default Groups**:
|
|
469
|
-
- `developers`
|
|
470
|
-
- `admins`
|
|
471
|
-
- `viewers`
|
|
472
|
-
|
|
473
|
-
**Default Users**:
|
|
474
|
-
| Username | Password | Groups |
|
|
475
|
-
|----------|----------|--------|
|
|
476
|
-
| `test1` | `test1@123` | developers |
|
|
477
|
-
| `test2` | `test2@123` | developers |
|
|
478
|
-
|
|
479
|
-
#### Example: Full RHDH + Keycloak Setup
|
|
480
|
-
|
|
481
|
-
```typescript
|
|
482
|
-
import { test } from "rhdh-e2e-test-utils/test";
|
|
483
|
-
import { KeycloakHelper } from "rhdh-e2e-test-utils/keycloak";
|
|
484
|
-
|
|
485
|
-
let keycloak: KeycloakHelper;
|
|
486
|
-
|
|
487
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
488
|
-
// Deploy Keycloak
|
|
489
|
-
keycloak = new KeycloakHelper({ namespace: "rhdh-keycloak" });
|
|
490
|
-
await keycloak.deploy();
|
|
491
|
-
await keycloak.configureForRHDH();
|
|
492
|
-
|
|
493
|
-
// Set environment variables for RHDH
|
|
494
|
-
process.env.KEYCLOAK_BASE_URL = keycloak.keycloakUrl;
|
|
495
|
-
process.env.KEYCLOAK_REALM = keycloak.realm;
|
|
496
|
-
process.env.KEYCLOAK_CLIENT_ID = keycloak.clientId;
|
|
497
|
-
process.env.KEYCLOAK_CLIENT_SECRET = keycloak.clientSecret;
|
|
498
|
-
process.env.KEYCLOAK_METADATA_URL = `${keycloak.keycloakUrl}/realms/${keycloak.realm}/.well-known/openid-configuration`;
|
|
499
|
-
process.env.KEYCLOAK_LOGIN_REALM = keycloak.realm;
|
|
500
|
-
|
|
501
|
-
// Deploy RHDH with Keycloak authentication
|
|
502
|
-
await rhdh.configure({ auth: "keycloak" });
|
|
503
|
-
await rhdh.deploy();
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
test("login with Keycloak user", async ({ page, loginHelper }) => {
|
|
507
|
-
await page.goto("/");
|
|
508
|
-
await loginHelper.loginAsKeycloakUser("test1", "test1@123");
|
|
509
|
-
// ... test assertions
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
test.afterAll(async () => {
|
|
513
|
-
await keycloak.teardown();
|
|
514
|
-
});
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
#### Connect to Existing Keycloak
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
import { KeycloakHelper } from "rhdh-e2e-test-utils/keycloak";
|
|
521
|
-
|
|
522
|
-
const keycloak = new KeycloakHelper();
|
|
523
|
-
|
|
524
|
-
// Connect with admin credentials
|
|
525
|
-
await keycloak.connect({
|
|
526
|
-
baseUrl: "https://keycloak.example.com",
|
|
527
|
-
username: "admin",
|
|
528
|
-
password: "admin-password",
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Or connect with client credentials
|
|
532
|
-
await keycloak.connect({
|
|
533
|
-
baseUrl: "https://keycloak.example.com",
|
|
534
|
-
realm: "my-realm",
|
|
535
|
-
clientId: "admin-client",
|
|
536
|
-
clientSecret: "client-secret",
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Now you can manage users, groups, etc.
|
|
540
|
-
await keycloak.createUser("my-realm", {
|
|
541
|
-
username: "newuser",
|
|
542
|
-
password: "password123",
|
|
543
|
-
groups: ["developers"],
|
|
544
|
-
});
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
### Utilities
|
|
548
|
-
|
|
549
|
-
#### Bash Command Execution
|
|
550
|
-
|
|
551
|
-
Execute shell commands using `zx`:
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
import { $ } from "rhdh-e2e-test-utils/utils";
|
|
555
|
-
|
|
556
|
-
// Execute commands
|
|
557
|
-
await $`oc get pods -n my-namespace`;
|
|
558
|
-
|
|
559
|
-
// With variables
|
|
560
|
-
const namespace = "my-namespace";
|
|
561
|
-
await $`oc get pods -n ${namespace}`;
|
|
562
|
-
|
|
563
|
-
// Capture output
|
|
564
|
-
const result = await $`oc get pods -o json`;
|
|
565
|
-
console.log(result.stdout);
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
#### Kubernetes Client Helper
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
import { KubernetesClientHelper } from "rhdh-e2e-test-utils/utils";
|
|
572
|
-
|
|
573
|
-
const k8sClient = new KubernetesClientHelper();
|
|
574
|
-
|
|
575
|
-
// Create namespace
|
|
576
|
-
await k8sClient.createNamespaceIfNotExists("my-namespace");
|
|
577
|
-
|
|
578
|
-
// Apply ConfigMap from object
|
|
579
|
-
await k8sClient.applyConfigMapFromObject(
|
|
580
|
-
"my-config",
|
|
581
|
-
{ key: "value" },
|
|
582
|
-
"my-namespace"
|
|
583
|
-
);
|
|
584
|
-
|
|
585
|
-
// Apply Secret from object
|
|
586
|
-
await k8sClient.applySecretFromObject(
|
|
587
|
-
"my-secret",
|
|
588
|
-
{ stringData: { TOKEN: "secret-value" } },
|
|
589
|
-
"my-namespace"
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
// Get route URL
|
|
593
|
-
const url = await k8sClient.getRouteLocation("my-namespace", "my-route");
|
|
594
|
-
|
|
595
|
-
// Get cluster ingress domain
|
|
596
|
-
const domain = await k8sClient.getClusterIngressDomain();
|
|
597
|
-
|
|
598
|
-
// Delete namespace
|
|
599
|
-
await k8sClient.deleteNamespace("my-namespace");
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
#### Environment Variable Substitution
|
|
603
|
-
|
|
604
|
-
```typescript
|
|
605
|
-
import { envsubst } from "rhdh-e2e-test-utils/utils";
|
|
606
|
-
|
|
607
|
-
// Simple substitution
|
|
608
|
-
const result = envsubst("Hello $USER");
|
|
609
|
-
|
|
610
|
-
// With default values
|
|
611
|
-
const result = envsubst("Port: ${PORT:-8080}");
|
|
612
|
-
|
|
613
|
-
// With braces
|
|
614
|
-
const result = envsubst("API: ${API_URL}");
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
### Helpers
|
|
618
|
-
|
|
619
|
-
The package provides helper classes for common testing operations.
|
|
620
|
-
|
|
621
|
-
#### UIhelper
|
|
622
|
-
|
|
623
|
-
A utility class for common UI interactions with Material-UI components:
|
|
624
|
-
|
|
625
|
-
```typescript
|
|
626
|
-
import { UIhelper } from "rhdh-e2e-test-utils/helpers";
|
|
627
|
-
|
|
628
|
-
const uiHelper = new UIhelper(page);
|
|
629
|
-
|
|
630
|
-
// Wait for page to fully load
|
|
631
|
-
await uiHelper.waitForLoad();
|
|
632
|
-
|
|
633
|
-
// Verify headings and text
|
|
634
|
-
await uiHelper.verifyHeading("Welcome to RHDH");
|
|
635
|
-
await uiHelper.verifyText("Some content");
|
|
636
|
-
|
|
637
|
-
// Button interactions
|
|
638
|
-
await uiHelper.clickButton("Submit");
|
|
639
|
-
await uiHelper.clickButtonByLabel("Close");
|
|
640
|
-
|
|
641
|
-
// Navigation
|
|
642
|
-
await uiHelper.openSidebar("Catalog");
|
|
643
|
-
await uiHelper.clickTab("Overview");
|
|
644
|
-
|
|
645
|
-
// Table operations
|
|
646
|
-
await uiHelper.verifyRowsInTable(["row1", "row2"]);
|
|
647
|
-
await uiHelper.verifyCellsInTable(["cell1", "cell2"]);
|
|
648
|
-
|
|
649
|
-
// MUI component interactions
|
|
650
|
-
await uiHelper.selectMuiBox("Kind", "Component");
|
|
651
|
-
await uiHelper.fillTextInputByLabel("Name", "my-component");
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
#### LoginHelper
|
|
655
|
-
|
|
656
|
-
Handles authentication flows for different providers:
|
|
657
|
-
|
|
658
|
-
```typescript
|
|
659
|
-
import { LoginHelper } from "rhdh-e2e-test-utils/helpers";
|
|
660
|
-
|
|
661
|
-
const loginHelper = new LoginHelper(page);
|
|
662
|
-
|
|
663
|
-
// Guest authentication
|
|
664
|
-
await loginHelper.loginAsGuest();
|
|
665
|
-
await loginHelper.signOut();
|
|
666
|
-
|
|
667
|
-
// Keycloak authentication
|
|
668
|
-
await loginHelper.loginAsKeycloakUser("username", "password");
|
|
669
|
-
|
|
670
|
-
// GitHub authentication (requires environment variables)
|
|
671
|
-
await loginHelper.loginAsGithubUser();
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
#### APIHelper
|
|
675
|
-
|
|
676
|
-
Provides utilities for API interactions with both GitHub and Backstage catalog:
|
|
677
|
-
|
|
678
|
-
```typescript
|
|
679
|
-
import { APIHelper } from "rhdh-e2e-test-utils/helpers";
|
|
680
|
-
|
|
681
|
-
// GitHub API operations
|
|
682
|
-
await APIHelper.createGitHubRepo("owner", "repo-name");
|
|
683
|
-
await APIHelper.deleteGitHubRepo("owner", "repo-name");
|
|
684
|
-
const prs = await APIHelper.getGitHubPRs("owner", "repo", "open");
|
|
685
|
-
|
|
686
|
-
// Backstage catalog API operations
|
|
687
|
-
const apiHelper = new APIHelper();
|
|
688
|
-
await apiHelper.setBaseUrl(rhdhUrl);
|
|
689
|
-
await apiHelper.setStaticToken(token);
|
|
690
|
-
|
|
691
|
-
const users = await apiHelper.getAllCatalogUsersFromAPI();
|
|
692
|
-
const groups = await apiHelper.getAllCatalogGroupsFromAPI();
|
|
693
|
-
const locations = await apiHelper.getAllCatalogLocationsFromAPI();
|
|
694
|
-
|
|
695
|
-
// Schedule entity refresh
|
|
696
|
-
await apiHelper.scheduleEntityRefreshFromAPI("my-component", "component", token);
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
#### setupBrowser
|
|
700
|
-
|
|
701
|
-
Utility function for setting up a shared browser context with video recording. Use this in `test.beforeAll` for serial test suites or when you want to persist the browser context across multiple tests (e.g., to avoid repeated logins):
|
|
702
|
-
|
|
703
|
-
```typescript
|
|
704
|
-
import { test } from "@playwright/test";
|
|
705
|
-
import { setupBrowser, LoginHelper } from "rhdh-e2e-test-utils/helpers";
|
|
706
|
-
import type { Page, BrowserContext } from "@playwright/test";
|
|
707
|
-
|
|
708
|
-
test.describe.configure({ mode: "serial" });
|
|
709
|
-
|
|
710
|
-
let page: Page;
|
|
711
|
-
let context: BrowserContext;
|
|
712
|
-
|
|
713
|
-
test.beforeAll(async ({ browser }, testInfo) => {
|
|
714
|
-
// Setup shared browser context with video recording
|
|
715
|
-
({ page, context } = await setupBrowser(browser, testInfo));
|
|
716
|
-
|
|
717
|
-
// Login once, session persists across all tests in this suite
|
|
718
|
-
const loginHelper = new LoginHelper(page);
|
|
719
|
-
await page.goto("/");
|
|
720
|
-
await loginHelper.loginAsKeycloakUser();
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
test.afterAll(async () => {
|
|
724
|
-
await context.close();
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
test("first test - already logged in", async () => {
|
|
728
|
-
await page.goto("/catalog");
|
|
729
|
-
// No need to login again
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
test("second test - session persists", async () => {
|
|
733
|
-
await page.goto("/settings");
|
|
734
|
-
// Still logged in from beforeAll
|
|
735
|
-
});
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
### Page Objects
|
|
739
|
-
|
|
740
|
-
Pre-built page object classes for common RHDH pages:
|
|
741
|
-
|
|
742
|
-
```typescript
|
|
743
|
-
import {
|
|
744
|
-
CatalogPage,
|
|
745
|
-
HomePage,
|
|
746
|
-
CatalogImportPage,
|
|
747
|
-
ExtensionsPage,
|
|
748
|
-
NotificationPage,
|
|
749
|
-
} from "rhdh-e2e-test-utils/pages";
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
#### CatalogPage
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
const catalogPage = new CatalogPage(page);
|
|
756
|
-
|
|
757
|
-
// Navigate to catalog
|
|
758
|
-
await catalogPage.go();
|
|
759
|
-
|
|
760
|
-
// Search for entities
|
|
761
|
-
await catalogPage.search("my-component");
|
|
762
|
-
|
|
763
|
-
// Navigate to specific component
|
|
764
|
-
await catalogPage.goToByName("my-component");
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
#### HomePage
|
|
768
|
-
|
|
769
|
-
```typescript
|
|
770
|
-
const homePage = new HomePage(page);
|
|
771
|
-
|
|
772
|
-
// Verify quick search functionality
|
|
773
|
-
await homePage.verifyQuickSearchBar("search-term");
|
|
774
|
-
|
|
775
|
-
// Verify quick access sections
|
|
776
|
-
await homePage.verifyQuickAccess("Favorites", "My Component");
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
#### CatalogImportPage
|
|
780
|
-
|
|
781
|
-
```typescript
|
|
782
|
-
const catalogImportPage = new CatalogImportPage(page);
|
|
783
|
-
|
|
784
|
-
// Register or refresh an existing component
|
|
785
|
-
const wasAlreadyRegistered = await catalogImportPage.registerExistingComponent(
|
|
786
|
-
"https://github.com/org/repo/blob/main/catalog-info.yaml"
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
// Analyze a component URL
|
|
790
|
-
await catalogImportPage.analyzeComponent("https://github.com/org/repo/blob/main/catalog-info.yaml");
|
|
791
|
-
|
|
792
|
-
// Inspect entity and verify YAML content
|
|
793
|
-
await catalogImportPage.inspectEntityAndVerifyYaml("kind: Component");
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
#### ExtensionsPage
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
const extensionsPage = new ExtensionsPage(page);
|
|
800
|
-
|
|
801
|
-
// Filter by support type
|
|
802
|
-
await extensionsPage.selectSupportTypeFilter("Red Hat");
|
|
803
|
-
|
|
804
|
-
// Verify plugin details
|
|
805
|
-
await extensionsPage.verifyPluginDetails({
|
|
806
|
-
pluginName: "Topology",
|
|
807
|
-
badgeLabel: "Red Hat support",
|
|
808
|
-
badgeText: "Red Hat",
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
// Search and verify results
|
|
812
|
-
await extensionsPage.waitForSearchResults("catalog");
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
#### NotificationPage
|
|
816
|
-
|
|
817
|
-
```typescript
|
|
818
|
-
const notificationPage = new NotificationPage(page);
|
|
819
|
-
|
|
820
|
-
// Navigate to notifications
|
|
821
|
-
await notificationPage.clickNotificationsNavBarItem();
|
|
822
|
-
|
|
823
|
-
// Check notification content
|
|
824
|
-
await notificationPage.notificationContains("Build completed");
|
|
825
|
-
|
|
826
|
-
// Manage notifications
|
|
827
|
-
await notificationPage.markAllNotificationsAsRead();
|
|
828
|
-
await notificationPage.selectSeverity("critical");
|
|
829
|
-
await notificationPage.viewSaved();
|
|
830
|
-
await notificationPage.sortByNewestOnTop();
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
### ESLint Configuration
|
|
834
|
-
|
|
835
|
-
Pre-configured ESLint rules for Playwright tests:
|
|
836
|
-
|
|
837
|
-
```javascript
|
|
838
|
-
// eslint.config.js
|
|
839
|
-
import { createEslintConfig } from "rhdh-e2e-test-utils/eslint";
|
|
840
|
-
|
|
841
|
-
export default createEslintConfig(import.meta.dirname);
|
|
842
|
-
```
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
### TypeScript Configuration
|
|
846
|
-
|
|
847
|
-
Extend the base tsconfig:
|
|
848
|
-
|
|
849
|
-
```json
|
|
850
|
-
{
|
|
851
|
-
"extends": "rhdh-e2e-test-utils/tsconfig",
|
|
852
|
-
"include": ["tests/**/*.ts"]
|
|
853
|
-
}
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
## Configuration Files
|
|
857
|
-
|
|
858
|
-
### Default Configuration Structure
|
|
859
|
-
|
|
860
|
-
The package includes default configurations organized in a modular structure:
|
|
861
|
-
|
|
862
|
-
```
|
|
863
|
-
src/deployment/rhdh/config/
|
|
864
|
-
├── common/ # Base configurations (always applied)
|
|
865
|
-
│ ├── app-config-rhdh.yaml # Base app configuration
|
|
866
|
-
│ ├── dynamic-plugins.yaml # Default dynamic plugins
|
|
867
|
-
│ └── rhdh-secrets.yaml # Base secrets template
|
|
868
|
-
├── auth/ # Auth-specific configurations
|
|
869
|
-
│ ├── guest/
|
|
870
|
-
│ │ └── app-config.yaml # Guest auth configuration
|
|
871
|
-
│ └── keycloak/
|
|
872
|
-
│ ├── app-config.yaml # Keycloak OIDC configuration
|
|
873
|
-
│ ├── dynamic-plugins.yaml # Keycloak-specific plugins
|
|
874
|
-
│ └── secrets.yaml # Keycloak secrets template
|
|
875
|
-
├── helm/
|
|
876
|
-
│ └── value_file.yaml # Default Helm values
|
|
877
|
-
└── operator/
|
|
878
|
-
└── subscription.yaml # Default Backstage CR
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
### Configuration Merging
|
|
882
|
-
|
|
883
|
-
Configurations are merged in the following order (later overrides earlier):
|
|
884
|
-
|
|
885
|
-
1. **Common configs** (`config/common/`) - Base configurations
|
|
886
|
-
2. **Auth configs** (`config/auth/{provider}/`) - Auth-provider-specific configurations
|
|
887
|
-
3. **Project configs** (`tests/config/`) - Your project's custom configurations
|
|
888
|
-
|
|
889
|
-
This allows you to use built-in defaults while only overriding what you need.
|
|
890
|
-
|
|
891
|
-
### Project Configuration
|
|
892
|
-
|
|
893
|
-
Create these files in your project's `tests/config/` directory:
|
|
894
|
-
|
|
895
|
-
#### app-config-rhdh.yaml
|
|
896
|
-
|
|
897
|
-
```yaml
|
|
898
|
-
app:
|
|
899
|
-
title: My RHDH Test Instance
|
|
900
|
-
|
|
901
|
-
backend:
|
|
902
|
-
reading:
|
|
903
|
-
allow:
|
|
904
|
-
- host: ${MY_BACKEND_HOST}
|
|
905
|
-
|
|
906
|
-
# Plugin-specific config
|
|
907
|
-
techRadar:
|
|
908
|
-
url: "http://${DATA_SOURCE_URL}/tech-radar"
|
|
909
|
-
|
|
910
|
-
# Note: Auth configuration is automatically included based on the 'auth' option
|
|
911
|
-
# You only need to add auth config here if you want to override the defaults
|
|
912
|
-
```
|
|
913
|
-
|
|
914
|
-
#### dynamic-plugins.yaml
|
|
915
|
-
|
|
916
|
-
```yaml
|
|
917
|
-
includes:
|
|
918
|
-
- dynamic-plugins.default.yaml
|
|
919
|
-
|
|
920
|
-
plugins:
|
|
921
|
-
- package: ./dynamic-plugins/dist/my-frontend-plugin
|
|
922
|
-
disabled: false
|
|
923
|
-
- package: ./dynamic-plugins/dist/my-backend-plugin-dynamic
|
|
924
|
-
disabled: false
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
#### rhdh-secrets.yaml
|
|
928
|
-
|
|
929
|
-
Secrets support environment variable substitution (`$VAR` or `${VAR}` syntax).
|
|
930
|
-
|
|
931
|
-
```yaml
|
|
932
|
-
apiVersion: v1
|
|
933
|
-
kind: Secret
|
|
934
|
-
metadata:
|
|
935
|
-
name: rhdh-secrets
|
|
936
|
-
type: Opaque
|
|
937
|
-
stringData:
|
|
938
|
-
GITHUB_TOKEN: $GITHUB_TOKEN
|
|
939
|
-
MY_API_KEY: $MY_API_KEY
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
- **Local development**: Define secrets in a `.env` file at your project root
|
|
943
|
-
- **CI**: Set environment variables directly in your CI pipeline
|
|
944
|
-
- **Runtime secrets**: Set `process.env.MY_SECRET` before calling `rhdh.deploy()`
|
|
945
|
-
|
|
946
|
-
## Environment Variables
|
|
947
|
-
|
|
948
|
-
### Required Environment Variables
|
|
949
|
-
|
|
950
|
-
| Variable | Description |
|
|
951
|
-
|----------|-------------|
|
|
952
|
-
| `RHDH_VERSION` | RHDH version to deploy (e.g., "1.5") |
|
|
953
|
-
| `INSTALLATION_METHOD` | Deployment method ("helm" or "operator") |
|
|
954
|
-
|
|
955
|
-
### Auto-Generated Environment Variables
|
|
956
|
-
|
|
957
|
-
| Variable | Description |
|
|
958
|
-
|----------|-------------|
|
|
959
|
-
| `K8S_CLUSTER_ROUTER_BASE` | OpenShift ingress domain (set by global setup) |
|
|
960
|
-
| `RHDH_BASE_URL` | Full RHDH URL (set during deployment) |
|
|
961
|
-
|
|
962
|
-
### Optional Environment Variables
|
|
963
|
-
|
|
964
|
-
| Variable | Description |
|
|
965
|
-
|----------|-------------|
|
|
966
|
-
| `CI` | If set, namespaces are auto-deleted after tests |
|
|
967
|
-
| `CHART_URL` | Custom Helm chart URL (default: `oci://quay.io/rhdh/chart`) |
|
|
968
|
-
| `SKIP_KEYCLOAK_DEPLOYMENT` | Set to `true` to skip automatic Keycloak deployment in global setup |
|
|
969
|
-
|
|
970
|
-
### Keycloak Environment Variables (for `auth: "keycloak"`)
|
|
971
|
-
|
|
972
|
-
When using Keycloak authentication, these environment variables are required:
|
|
973
|
-
|
|
974
|
-
| Variable | Description |
|
|
975
|
-
|----------|-------------|
|
|
976
|
-
| `KEYCLOAK_BASE_URL` | Keycloak instance URL |
|
|
977
|
-
| `KEYCLOAK_METADATA_URL` | OIDC metadata URL (e.g., `{KEYCLOAK_BASE_URL}/realms/{realm}/.well-known/openid-configuration`) |
|
|
978
|
-
| `KEYCLOAK_CLIENT_ID` | OIDC client ID |
|
|
979
|
-
| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret |
|
|
980
|
-
| `KEYCLOAK_REALM` | Keycloak realm name |
|
|
981
|
-
| `KEYCLOAK_LOGIN_REALM` | Login realm (usually same as `KEYCLOAK_REALM`) |
|
|
982
|
-
|
|
983
|
-
These are automatically set when using `KeycloakHelper.configureForRHDH()`. See [Keycloak Deployment](#keycloak-deployment) for details.
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
## Examples
|
|
987
|
-
|
|
988
|
-
### Custom Deployment Configuration
|
|
989
|
-
|
|
990
|
-
```typescript
|
|
991
|
-
import { test } from "rhdh-e2e-test-utils/test";
|
|
992
|
-
|
|
993
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
994
|
-
await rhdh.configure({
|
|
995
|
-
version: "1.5",
|
|
996
|
-
method: "helm",
|
|
997
|
-
auth: "keycloak", // or "guest" for development
|
|
998
|
-
appConfig: "tests/config/app-config.yaml",
|
|
999
|
-
secrets: "tests/config/secrets.yaml",
|
|
1000
|
-
dynamicPlugins: "tests/config/plugins.yaml",
|
|
1001
|
-
valueFile: "tests/config/values.yaml",
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
await rhdh.deploy();
|
|
1005
|
-
});
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
### Guest Authentication (Development)
|
|
1009
|
-
|
|
1010
|
-
```typescript
|
|
1011
|
-
import { test } from "rhdh-e2e-test-utils/test";
|
|
1012
|
-
|
|
1013
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
1014
|
-
await rhdh.configure({ auth: "guest" });
|
|
1015
|
-
await rhdh.deploy();
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
test("test with guest login", async ({ page, loginHelper }) => {
|
|
1019
|
-
await page.goto("/");
|
|
1020
|
-
await loginHelper.loginAsGuest();
|
|
1021
|
-
// ... test assertions
|
|
1022
|
-
});
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
### Using Helpers and Page Objects
|
|
1026
|
-
|
|
1027
|
-
```typescript
|
|
1028
|
-
import { test, expect } from "rhdh-e2e-test-utils/test";
|
|
1029
|
-
import { CatalogPage } from "rhdh-e2e-test-utils/pages";
|
|
1030
|
-
import { APIHelper } from "rhdh-e2e-test-utils/helpers";
|
|
1031
|
-
|
|
1032
|
-
test.beforeAll(async ({ rhdh }) => {
|
|
1033
|
-
await rhdh.deploy();
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
test("catalog interaction", async ({ page, uiHelper, loginHelper }) => {
|
|
1037
|
-
// Login
|
|
1038
|
-
await loginHelper.loginAsKeycloakUser();
|
|
1039
|
-
|
|
1040
|
-
// Use page object for catalog operations
|
|
1041
|
-
const catalogPage = new CatalogPage(page);
|
|
1042
|
-
await catalogPage.go();
|
|
1043
|
-
await catalogPage.search("my-component");
|
|
1044
|
-
|
|
1045
|
-
// Use UI helper for assertions
|
|
1046
|
-
await uiHelper.verifyRowsInTable(["my-component"]);
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
test("API operations", async ({ rhdh }) => {
|
|
1050
|
-
// Create GitHub repo via API
|
|
1051
|
-
await APIHelper.createGitHubRepo("my-org", "test-repo");
|
|
1052
|
-
|
|
1053
|
-
// Clean up
|
|
1054
|
-
await APIHelper.deleteGitHubRepo("my-org", "test-repo");
|
|
1055
|
-
});
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
## Development
|
|
1060
|
-
|
|
1061
|
-
### Setup
|
|
1062
|
-
|
|
1063
|
-
This project uses Yarn 3 with Corepack. To get started:
|
|
1064
|
-
|
|
1065
|
-
```bash
|
|
1066
|
-
# Enable Corepack (if not already enabled)
|
|
1067
|
-
corepack enable
|
|
1068
|
-
|
|
1069
|
-
# Install dependencies
|
|
1070
|
-
yarn install
|
|
1071
|
-
|
|
1072
|
-
# Build the project
|
|
1073
|
-
yarn build
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
### Available Scripts
|
|
1077
|
-
|
|
1078
|
-
| Script | Description |
|
|
1079
|
-
|--------|-------------|
|
|
1080
|
-
| `yarn build` | Clean and build the TypeScript project |
|
|
1081
|
-
| `yarn check` | Run typecheck, lint, and prettier checks |
|
|
1082
|
-
| `yarn lint:check` | Check for ESLint issues |
|
|
1083
|
-
| `yarn lint:fix` | Auto-fix ESLint issues |
|
|
1084
|
-
| `yarn prettier:check` | Check code formatting |
|
|
1085
|
-
| `yarn prettier:fix` | Auto-fix code formatting |
|
|
1086
|
-
|
|
1087
|
-
### Testing Local Changes in Consumer Projects
|
|
1088
|
-
|
|
1089
|
-
When developing features or fixes in `rhdh-e2e-test-utils`, you can test your local changes in a consumer project (e.g., a plugin's e2e-tests) before publishing.
|
|
1090
|
-
|
|
1091
|
-
#### 1. Build your local changes
|
|
1092
|
-
|
|
1093
|
-
```bash
|
|
1094
|
-
cd /path/to/rhdh-e2e-test-utils
|
|
1095
|
-
yarn build
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
#### 2. Update the consumer project's package.json
|
|
1099
|
-
|
|
1100
|
-
In your e2e-tests project, update the dependency to point to your local package using the `file:` protocol:
|
|
1101
|
-
|
|
1102
|
-
```json
|
|
1103
|
-
"rhdh-e2e-test-utils": "file:/path/to/rhdh-e2e-test-utils"
|
|
1104
|
-
```
|
|
1105
|
-
|
|
1106
|
-
Example:
|
|
1107
|
-
```json
|
|
1108
|
-
"rhdh-e2e-test-utils": "file:/Users/yourname/Documents/rhdh/rhdh-e2e-test-utils"
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
#### 3. Install dependencies in the consumer project
|
|
1112
|
-
|
|
1113
|
-
```bash
|
|
1114
|
-
yarn install
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
#### 4. Run tests with NODE_PRESERVE_SYMLINKS
|
|
1118
|
-
|
|
1119
|
-
When running tests with a local symlinked package, you **must** set the `NODE_PRESERVE_SYMLINKS` environment variable:
|
|
1120
|
-
|
|
1121
|
-
```bash
|
|
1122
|
-
NODE_PRESERVE_SYMLINKS=1 yarn test
|
|
1123
|
-
NODE_PRESERVE_SYMLINKS=1 yarn test:headed
|
|
1124
|
-
NODE_PRESERVE_SYMLINKS=1 yarn test:ui
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
> **Why is NODE_PRESERVE_SYMLINKS needed?**
|
|
1128
|
-
>
|
|
1129
|
-
> When using local packages via `file:` protocol, the package manager creates a symlink. Node.js follows symlinks by default and tries to resolve peer dependencies (like `@playwright/test`) from the original package location. This causes duplicate Playwright instances which fails with:
|
|
1130
|
-
> ```
|
|
1131
|
-
> Error: Requiring @playwright/test second time
|
|
1132
|
-
> ```
|
|
1133
|
-
> Setting `NODE_PRESERVE_SYMLINKS=1` tells Node.js to resolve dependencies from the symlink location (your project's `node_modules`) instead of the original package location.
|
|
1134
|
-
|
|
1135
|
-
#### 5. Rebuild after making changes
|
|
1136
|
-
|
|
1137
|
-
When you make further changes to `rhdh-e2e-test-utils`, rebuild before running tests:
|
|
1138
|
-
|
|
1139
|
-
```bash
|
|
1140
|
-
cd /path/to/rhdh-e2e-test-utils
|
|
1141
|
-
yarn build
|
|
1142
|
-
```
|
|
1143
|
-
|
|
1144
|
-
Then run your tests again in the consumer project (no need to reinstall).
|
|
1145
|
-
|
|
1146
|
-
#### 6. Restore the published version
|
|
1147
|
-
|
|
1148
|
-
After testing, restore the published version in the consumer project's `package.json`:
|
|
1149
|
-
|
|
1150
|
-
```json
|
|
1151
|
-
"rhdh-e2e-test-utils": "^1.0.0"
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
Then run:
|
|
1155
|
-
```bash
|
|
1156
|
-
yarn install
|
|
1157
|
-
```
|
|
1158
|
-
|
|
1159
|
-
You can now run tests normally without `NODE_PRESERVE_SYMLINKS`.
|
|
1160
|
-
|
|
1161
|
-
### CI/CD
|
|
1162
|
-
|
|
1163
|
-
The project includes GitHub Actions workflows:
|
|
1164
|
-
|
|
1165
|
-
- **PR Build and Check**: Runs on pull requests to `main`. Executes linting, type checking, and build verification.
|
|
1166
|
-
- **Publish to NPM**: Manual workflow dispatch to publish the package to npm registry.
|
|
1167
|
-
|
|
1168
|
-
## License
|
|
1169
|
-
|
|
1170
|
-
Apache-2.0
|
|
5
|
+
- Package documentation: https://redhat-developer.github.io/rhdh-e2e-test-utils/
|
|
6
|
+
- Overlay testing documentation: https://redhat-developer.github.io/rhdh-e2e-test-utils/overlay/
|
|
@@ -26,6 +26,11 @@ export declare class RHDHDeployment {
|
|
|
26
26
|
waitUntilReady(timeout?: number): Promise<void>;
|
|
27
27
|
teardown(): Promise<void>;
|
|
28
28
|
private _resolveChartVersion;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the semantic version from the "next" tag by looking up the
|
|
31
|
+
* downstream image (rhdh-hub-rhel9) and finding tags with the same digest.
|
|
32
|
+
*/
|
|
33
|
+
private _resolveVersionFromNextTag;
|
|
29
34
|
private _buildDeploymentConfig;
|
|
30
35
|
configure(deploymentOptions?: DeploymentOptions): Promise<void>;
|
|
31
36
|
private _buildBaseUrl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAiB1E,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;gBAE9B,SAAS,EAAE,MAAM;IAUvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBf,eAAe;YAgBf,aAAa;IAqB3B;;;OAGG;YACW,0BAA0B;YAuC1B,oBAAoB;YAWpB,eAAe;YAmCf,mBAAmB;IAiD3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IAoCxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
|
|
@@ -5,6 +5,7 @@ import { test } from "@playwright/test";
|
|
|
5
5
|
import { mergeYamlFilesIfExists, deepMerge } from "../../utils/merge-yamls.js";
|
|
6
6
|
import { loadAndInjectPluginMetadata, generateDynamicPluginsConfigFromMetadata, } from "../../utils/plugin-metadata.js";
|
|
7
7
|
import { envsubst } from "../../utils/common.js";
|
|
8
|
+
import cloneDeepWith from "lodash.clonedeepwith";
|
|
8
9
|
import fs from "fs-extra";
|
|
9
10
|
import { DEFAULT_CONFIG_PATHS, AUTH_CONFIG_PATHS, CHART_URL, } from "./constants.js";
|
|
10
11
|
export class RHDHDeployment {
|
|
@@ -51,7 +52,13 @@ export class RHDHDeployment {
|
|
|
51
52
|
authConfig.secrets,
|
|
52
53
|
this.deploymentConfig.secrets,
|
|
53
54
|
]);
|
|
54
|
-
|
|
55
|
+
// Use cloneDeepWith to substitute env vars in-place, avoiding JSON.parse issues
|
|
56
|
+
// with control characters in secrets (e.g., private keys with newlines)
|
|
57
|
+
const substituted = cloneDeepWith(secretsYaml, (value) => {
|
|
58
|
+
if (typeof value === "string")
|
|
59
|
+
return envsubst(value);
|
|
60
|
+
});
|
|
61
|
+
await this.k8sClient.applySecretFromObject("rhdh-secrets", substituted, this.deploymentConfig.namespace);
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* Builds the merged dynamic plugins configuration.
|
|
@@ -117,9 +124,25 @@ export class RHDHDeployment {
|
|
|
117
124
|
]);
|
|
118
125
|
this._logBoxen("Subscription", subscriptionObject);
|
|
119
126
|
fs.writeFileSync(`/tmp/${this.deploymentConfig.namespace}-subscription.yaml`, yaml.dump(subscriptionObject));
|
|
127
|
+
const version = this.deploymentConfig.version;
|
|
128
|
+
const isSemanticVersion = /^\d+(\.\d+)?$/.test(version);
|
|
129
|
+
// Use main branch for non-semantic versions (e.g., "next", "latest")
|
|
130
|
+
const branch = isSemanticVersion ? `release-${version}` : "main";
|
|
131
|
+
// Build version argument based on version type
|
|
132
|
+
let versionArg;
|
|
133
|
+
if (isSemanticVersion) {
|
|
134
|
+
versionArg = `-v ${version}`;
|
|
135
|
+
}
|
|
136
|
+
else if (version === "next") {
|
|
137
|
+
versionArg = "--next";
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new Error(`Invalid RHDH version "${version}". Use semantic version (e.g., "1.5") or "next".`);
|
|
141
|
+
}
|
|
142
|
+
this._log(`Using operator branch: ${branch}, version arg: ${versionArg}`);
|
|
120
143
|
await $ `
|
|
121
144
|
set -e;
|
|
122
|
-
curl -
|
|
145
|
+
curl -sf https://raw.githubusercontent.com/redhat-developer/rhdh-operator/refs/heads/${branch}/.rhdh/scripts/install-rhdh-catalog-source.sh | bash -s -- ${versionArg} --install-operator rhdh
|
|
123
146
|
|
|
124
147
|
timeout 300 bash -c '
|
|
125
148
|
while ! oc get crd/backstages.rhdh.redhat.com -n "${this.deploymentConfig.namespace}" >/dev/null 2>&1; do
|
|
@@ -165,33 +188,66 @@ export class RHDHDeployment {
|
|
|
165
188
|
await this.k8sClient.deleteNamespace(this.deploymentConfig.namespace);
|
|
166
189
|
}
|
|
167
190
|
async _resolveChartVersion(version) {
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
let resolvedVersion = version;
|
|
192
|
+
// Handle "next" tag by looking up the corresponding version from downstream image
|
|
193
|
+
if (version === "next") {
|
|
194
|
+
resolvedVersion = await this._resolveVersionFromNextTag();
|
|
195
|
+
this._log(`Resolved "next" tag to version: ${resolvedVersion}`);
|
|
196
|
+
}
|
|
197
|
+
// Semantic versions (e.g., 1.2, 1.10)
|
|
198
|
+
if (/^(\d+(\.\d+)?)$/.test(resolvedVersion)) {
|
|
170
199
|
const response = await fetch("https://quay.io/api/v1/repository/rhdh/chart/tag/?onlyActiveTags=true&limit=600");
|
|
171
200
|
if (!response.ok)
|
|
172
201
|
throw new Error(`Failed to fetch chart versions: ${response.statusText}`);
|
|
173
202
|
const data = (await response.json());
|
|
174
203
|
const matching = data.tags
|
|
175
204
|
.map((t) => t.name)
|
|
176
|
-
.filter((name) => name.startsWith(`${
|
|
205
|
+
.filter((name) => name.startsWith(`${resolvedVersion}-`))
|
|
177
206
|
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
178
207
|
const latest = matching.at(-1);
|
|
179
208
|
if (!latest)
|
|
180
|
-
throw new Error(`No chart version found for ${
|
|
209
|
+
throw new Error(`No chart version found for ${resolvedVersion}`);
|
|
181
210
|
return latest;
|
|
182
211
|
}
|
|
183
212
|
// CI build versions (e.g., 1.2.3-CI)
|
|
184
|
-
if (
|
|
185
|
-
return
|
|
213
|
+
if (resolvedVersion.endsWith("CI"))
|
|
214
|
+
return resolvedVersion;
|
|
186
215
|
throw new Error(`Invalid Helm chart version format: "${version}"`);
|
|
187
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the semantic version from the "next" tag by looking up the
|
|
219
|
+
* downstream image (rhdh-hub-rhel9) and finding tags with the same digest.
|
|
220
|
+
*/
|
|
221
|
+
async _resolveVersionFromNextTag() {
|
|
222
|
+
// Fetch all active tags in a single API call
|
|
223
|
+
const response = await fetch("https://quay.io/api/v1/repository/rhdh/rhdh-hub-rhel9/tag/?onlyActiveTags=true&limit=75");
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Failed to fetch image tags: ${response.statusText}`);
|
|
226
|
+
}
|
|
227
|
+
// Use Record to avoid snake_case linting issues with Quay API response
|
|
228
|
+
const data = (await response.json());
|
|
229
|
+
// Find the "next" tag and get its digest
|
|
230
|
+
const nextTag = data.tags.find((t) => t["name"] === "next");
|
|
231
|
+
if (!nextTag) {
|
|
232
|
+
throw new Error('No "next" tag found in rhdh-hub-rhel9 repository');
|
|
233
|
+
}
|
|
234
|
+
const digest = nextTag["manifest_digest"];
|
|
235
|
+
this._log(`"next" tag digest: ${digest}`);
|
|
236
|
+
// Find semantic version tag (e.g., "1.10") with the same digest
|
|
237
|
+
const semanticVersionTag = data.tags.find((t) => t["manifest_digest"] === digest &&
|
|
238
|
+
/^\d+\.\d+$/.test(t["name"]));
|
|
239
|
+
if (!semanticVersionTag) {
|
|
240
|
+
throw new Error(`Could not find semantic version tag for "next" (digest: ${digest})`);
|
|
241
|
+
}
|
|
242
|
+
return semanticVersionTag["name"];
|
|
243
|
+
}
|
|
188
244
|
_buildDeploymentConfig(input) {
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
245
|
+
// Default to "next" if RHDH_VERSION not set
|
|
246
|
+
const version = input.version ?? process.env.RHDH_VERSION ?? "next";
|
|
247
|
+
// Default to "helm" if INSTALLATION_METHOD not set
|
|
248
|
+
const method = input.method ??
|
|
249
|
+
process.env.INSTALLATION_METHOD ??
|
|
250
|
+
"helm";
|
|
195
251
|
const base = {
|
|
196
252
|
version,
|
|
197
253
|
namespace: input.namespace ?? this.deploymentConfig.namespace,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhdh-e2e-test-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Test utilities for RHDH E2E tests",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@playwright/test": "^1.57.0",
|
|
75
75
|
"@types/fs-extra": "^11.0.4",
|
|
76
76
|
"@types/js-yaml": "^4.0.9",
|
|
77
|
+
"@types/lodash.clonedeepwith": "^4.5.9",
|
|
77
78
|
"@types/lodash.mergewith": "^4.6.9",
|
|
78
79
|
"@types/node": "^24.10.1"
|
|
79
80
|
},
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
"eslint-plugin-playwright": "^2.4.0",
|
|
88
89
|
"fs-extra": "^11.3.2",
|
|
89
90
|
"js-yaml": "^4.1.1",
|
|
91
|
+
"lodash.clonedeepwith": "^4.5.0",
|
|
90
92
|
"lodash.mergewith": "^4.6.2",
|
|
91
93
|
"otplib": "12.0.1",
|
|
92
94
|
"prettier": "^3.7.4",
|