@red-hat-developer-hub/e2e-test-utils 1.1.41 → 1.1.43
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/dist/deployment/rhdh/config/operator/subscription.yaml +1 -1
- package/dist/deployment/rhdh/deployment.d.ts +1 -0
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +13 -37
- package/dist/deployment/rhdh/operator-setup.d.ts +2 -0
- package/dist/deployment/rhdh/operator-setup.d.ts.map +1 -0
- package/dist/deployment/rhdh/operator-setup.js +46 -0
- package/dist/playwright/global-setup.d.ts.map +1 -1
- package/dist/playwright/global-setup.js +2 -1
- package/dist/playwright/helpers/common.d.ts +1 -0
- package/dist/playwright/helpers/common.d.ts.map +1 -1
- package/dist/playwright/helpers/common.js +35 -14
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ export declare class RHDHDeployment {
|
|
|
34
34
|
private _applyDynamicPlugins;
|
|
35
35
|
private _deployWithHelm;
|
|
36
36
|
private _deployWithOperator;
|
|
37
|
+
private get _labelSelector();
|
|
37
38
|
rolloutRestart(): Promise<void>;
|
|
38
39
|
/**
|
|
39
40
|
* Performs a clean restart by scaling down to 0 first, waiting for pods to terminate,
|
|
@@ -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;AAwB1E,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;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YA0CpD,eAAe;YAmBf,aAAa;IA2B3B,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAE5B;IAEX;;;OAGG;YACW,iBAAiB;IAe/B;;OAEG;YACW,uBAAuB;IAgBrC;;;;;;;;OAQG;YACW,0BAA0B;YAsC1B,oBAAoB;YAWpB,eAAe;YAuEf,mBAAmB;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAwB1E,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;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YA0CpD,eAAe;YAmBf,aAAa;IA2B3B,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAE5B;IAEX;;;OAGG;YACW,iBAAiB;IAe/B;;OAEG;YACW,uBAAuB;IAgBrC;;;;;;;;OAQG;YACW,0BAA0B;YAsC1B,oBAAoB;YAWpB,eAAe;YAuEf,mBAAmB;IAmCjC,OAAO,KAAK,cAAc,GAIzB;IAEK,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;IA6BpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,iBAAiB;YASjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IA2CxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
|
|
@@ -229,44 +229,20 @@ export class RHDHDeployment {
|
|
|
229
229
|
});
|
|
230
230
|
this._log(`Catalog index image: ${catalogIndexImage}`);
|
|
231
231
|
}
|
|
232
|
-
this._logBoxen("
|
|
232
|
+
this._logBoxen("Backstage CR", subscriptionObject);
|
|
233
233
|
const subscriptionFilePath = path.join(os.tmpdir(), `${this.deploymentConfig.namespace}-subscription.yaml`);
|
|
234
234
|
fs.writeFileSync(subscriptionFilePath, yaml.dump(subscriptionObject));
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
versionArg = `-v ${version}`;
|
|
243
|
-
}
|
|
244
|
-
else if (version === "next") {
|
|
245
|
-
versionArg = "--next";
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
throw new Error(`Invalid RHDH version "${version}". Use semantic version (e.g., "1.5") or "next".`);
|
|
249
|
-
}
|
|
250
|
-
this._log(`Using operator branch: ${branch}, version arg: ${versionArg}`);
|
|
251
|
-
await $ `
|
|
252
|
-
set -e;
|
|
253
|
-
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
|
|
254
|
-
|
|
255
|
-
timeout 300 bash -c '
|
|
256
|
-
while ! oc get crd/backstages.rhdh.redhat.com -n "${this.deploymentConfig.namespace}" >/dev/null 2>&1; do
|
|
257
|
-
echo "Waiting for Backstage CRD to be created..."
|
|
258
|
-
sleep 20
|
|
259
|
-
done
|
|
260
|
-
echo "Backstage CRD is created."
|
|
261
|
-
' || echo "Error: Timed out waiting for Backstage CRD creation."
|
|
262
|
-
|
|
263
|
-
oc apply -f "${subscriptionFilePath}" -n "${this.deploymentConfig.namespace}"
|
|
264
|
-
`;
|
|
265
|
-
this._log("Operator deployment executed successfully.");
|
|
235
|
+
await $ `oc apply -f "${subscriptionFilePath}" -n "${this.deploymentConfig.namespace}"`;
|
|
236
|
+
this._log("Backstage CR applied successfully.");
|
|
237
|
+
}
|
|
238
|
+
get _labelSelector() {
|
|
239
|
+
return this.deploymentConfig.method === "operator"
|
|
240
|
+
? "rhdh.redhat.com/app=backstage-developer-hub"
|
|
241
|
+
: "app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)";
|
|
266
242
|
}
|
|
267
243
|
async rolloutRestart() {
|
|
268
244
|
this._log(`Restarting RHDH deployment in namespace ${this.deploymentConfig.namespace}...`);
|
|
269
|
-
await $ `oc rollout restart deployment -l '
|
|
245
|
+
await $ `oc rollout restart deployment -l '${this._labelSelector}' -n ${this.deploymentConfig.namespace}`;
|
|
270
246
|
this._log(`RHDH deployment restarted successfully in namespace ${this.deploymentConfig.namespace}`);
|
|
271
247
|
await this.waitUntilReady();
|
|
272
248
|
}
|
|
@@ -277,13 +253,13 @@ export class RHDHDeployment {
|
|
|
277
253
|
*/
|
|
278
254
|
async scaleDownAndRestart() {
|
|
279
255
|
const namespace = this.deploymentConfig.namespace;
|
|
280
|
-
await $ `oc scale deployment -l '
|
|
281
|
-
await $ `oc wait --for=delete pod -l '
|
|
282
|
-
await $ `oc scale deployment -l '
|
|
256
|
+
await $ `oc scale deployment -l '${this._labelSelector}' --replicas=0 -n ${namespace}`;
|
|
257
|
+
await $ `oc wait --for=delete pod -l '${this._labelSelector}' -n ${namespace} --timeout=120s || true`;
|
|
258
|
+
await $ `oc scale deployment -l '${this._labelSelector}' --replicas=1 -n ${namespace}`;
|
|
283
259
|
}
|
|
284
260
|
async waitUntilReady(timeout = 500) {
|
|
285
261
|
const namespace = this.deploymentConfig.namespace;
|
|
286
|
-
const labelSelector =
|
|
262
|
+
const labelSelector = this._labelSelector;
|
|
287
263
|
const startTime = Date.now();
|
|
288
264
|
try {
|
|
289
265
|
await this.k8sClient.waitForPodsWithFailureDetection(namespace, labelSelector, timeout);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operator-setup.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/operator-setup.ts"],"names":[],"mappings":"AAEA,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0DzD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { $ } from "../../utils/bash.js";
|
|
2
|
+
export async function installRHDHOperator() {
|
|
3
|
+
if (process.env.INSTALLATION_METHOD !== "operator") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (process.env.SKIP_OPERATOR_INSTALLATION === "true") {
|
|
7
|
+
console.log("Skipping RHDH operator installation");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const version = process.env.RHDH_VERSION ?? "next";
|
|
11
|
+
const isSemanticVersion = /^\d+(\.\d+)?$/.test(version);
|
|
12
|
+
const branch = isSemanticVersion ? `release-${version}` : "main";
|
|
13
|
+
let versionArgs;
|
|
14
|
+
if (isSemanticVersion) {
|
|
15
|
+
versionArgs = ["-v", version];
|
|
16
|
+
}
|
|
17
|
+
else if (version === "next") {
|
|
18
|
+
versionArgs = ["--next"];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
throw new Error(`Invalid RHDH version "${version}". Use semantic version (e.g., "1.5") or "next".`);
|
|
22
|
+
}
|
|
23
|
+
console.log(`Installing RHDH operator (branch: ${branch}, version: ${versionArgs.join(" ")})...`);
|
|
24
|
+
const scriptUrl = `https://raw.githubusercontent.com/redhat-developer/rhdh-operator/refs/heads/${branch}/.rhdh/scripts/install-rhdh-catalog-source.sh`;
|
|
25
|
+
try {
|
|
26
|
+
await $ `curl -sf ${scriptUrl} | bash -s -- ${versionArgs} --install-operator rhdh`;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(`install-rhdh-catalog-source.sh failed (branch: ${branch}, args: ${versionArgs.join(" ")} --install-operator rhdh).\nScript URL: ${scriptUrl}\n${error instanceof Error ? error.message : error}`, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await $ `
|
|
33
|
+
timeout 300 bash -c '
|
|
34
|
+
while ! oc get crd backstages.rhdh.redhat.com >/dev/null 2>&1; do
|
|
35
|
+
echo "Waiting for Backstage CRD to be created..."
|
|
36
|
+
sleep 10
|
|
37
|
+
done
|
|
38
|
+
echo "Backstage CRD is created."
|
|
39
|
+
'
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error("Timed out waiting for Backstage CRD (backstages.rhdh.redhat.com) after 300s. The operator may not have installed correctly.", { cause: error });
|
|
44
|
+
}
|
|
45
|
+
console.log("RHDH operator installation completed");
|
|
46
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../../src/playwright/global-setup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../../src/playwright/global-setup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA8EnD,wBAA8B,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E"}
|
|
@@ -7,6 +7,7 @@ import { resolve } from "path";
|
|
|
7
7
|
import { KubernetesClientHelper } from "../utils/kubernetes-client.js";
|
|
8
8
|
import { $ } from "../utils/bash.js";
|
|
9
9
|
import { KeycloakHelper } from "../deployment/keycloak/index.js";
|
|
10
|
+
import { installRHDHOperator } from "../deployment/rhdh/operator-setup.js";
|
|
10
11
|
import { DEFAULT_KEYCLOAK_CONFIG, DEFAULT_RHDH_CLIENT, DEFAULT_USERS, } from "../deployment/keycloak/constants.js";
|
|
11
12
|
import { loadLocalVaultSecrets } from "../utils/vault.js";
|
|
12
13
|
const REQUIRED_BINARIES = ["oc", "kubectl", "helm"];
|
|
@@ -67,7 +68,7 @@ export default async function globalSetup(config) {
|
|
|
67
68
|
await loadLocalVaultSecrets();
|
|
68
69
|
loadDotenvFromProjects(config);
|
|
69
70
|
await setClusterRouterBaseEnv();
|
|
70
|
-
await deployKeycloak();
|
|
71
|
+
await Promise.all([installRHDHOperator(), deployKeycloak()]);
|
|
71
72
|
console.log("Global setup completed successfully");
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
@@ -11,6 +11,7 @@ export declare class LoginHelper {
|
|
|
11
11
|
loginAsKeycloakUser(userid?: string, password?: string): Promise<void>;
|
|
12
12
|
loginAsGithubUser(userid?: string): Promise<void>;
|
|
13
13
|
checkAndReauthorizeGithubApp(): Promise<void>;
|
|
14
|
+
private handleGithubPopupReauth;
|
|
14
15
|
googleSignIn(email: string): Promise<void>;
|
|
15
16
|
checkAndClickOnGHloginPopup(force?: boolean): Promise<void>;
|
|
16
17
|
getButtonSelector(label: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAOhE,qBAAa,WAAW;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;gBAEP,IAAI,EAAE,IAAI;IAKhB,YAAY;IAcZ,OAAO;YAMC,aAAa;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAOhE,qBAAa,WAAW;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;gBAEP,IAAI,EAAE,IAAI;IAKhB,YAAY;IAcZ,OAAO;YAMC,aAAa;IA2CrB,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAO7D,mBAAmB,CACvB,MAAM,GAAE,MAAkC,EAC1C,QAAQ,GAAE,MAAkC;IAWxC,iBAAiB,CACrB,MAAM,GAAE,MAA+C;IAoDnD,4BAA4B;YASpB,uBAAuB;IAgB/B,YAAY,CAAC,KAAK,EAAE,MAAM;IA2B1B,2BAA2B,CAAC,KAAK,UAAQ;IAU/C,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIxC,mBAAmB,IAAI,MAAM;IAIvB,mBAAmB;IAgBzB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAevC,eAAe,IAAI,MAAM;IAKnB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;YAqCxC,sBAAsB;IAoD9B,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAYjE,2BAA2B,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;IAYb,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CA2C7D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ;;;GAWtE"}
|
|
@@ -46,13 +46,15 @@ export class LoginHelper {
|
|
|
46
46
|
}
|
|
47
47
|
await this.page.click('[value="Sign in"]');
|
|
48
48
|
await this.page.fill("#app_totp", this.getGitHub2FAOTP(userid));
|
|
49
|
-
test.setTimeout(
|
|
49
|
+
test.setTimeout(260_000);
|
|
50
50
|
if ((await this.uiHelper.isTextVisible("The two-factor code you entered has already been used")) ||
|
|
51
51
|
(await this.uiHelper.isTextVisible("too many codes have been submitted", 3000))) {
|
|
52
52
|
await this.page.waitForTimeout(60000);
|
|
53
53
|
await this.page.fill("#app_totp", this.getGitHub2FAOTP(userid));
|
|
54
54
|
}
|
|
55
|
-
await this.page
|
|
55
|
+
await this.page
|
|
56
|
+
.getByRole("heading", { name: "Home" })
|
|
57
|
+
.waitFor({ timeout: 30_000 });
|
|
56
58
|
}
|
|
57
59
|
async logintoKeycloak(popup, userid, password) {
|
|
58
60
|
await popup.waitForLoadState();
|
|
@@ -80,7 +82,23 @@ export class LoginHelper {
|
|
|
80
82
|
await this.page.goto("/");
|
|
81
83
|
await this.uiHelper.waitForLoad(12000);
|
|
82
84
|
await this.uiHelper.clickButton("Sign In");
|
|
83
|
-
|
|
85
|
+
// Wait for either: sidebar appears (auto-login) or popup opens (needs auth)
|
|
86
|
+
const navPromise = this.page
|
|
87
|
+
.waitForSelector("nav a", { timeout: 15_000 })
|
|
88
|
+
.then(() => "nav")
|
|
89
|
+
.catch(() => null);
|
|
90
|
+
const popupPromise = this.page
|
|
91
|
+
.waitForEvent("popup", { timeout: 15_000 })
|
|
92
|
+
.then((popup) => ({ popup }))
|
|
93
|
+
.catch(() => null);
|
|
94
|
+
const result = await Promise.race([navPromise, popupPromise]);
|
|
95
|
+
if (result === null) {
|
|
96
|
+
throw new Error("GitHub login failed: neither sidebar nor popup appeared after Sign In — session file may be stale");
|
|
97
|
+
}
|
|
98
|
+
if (typeof result === "object" && "popup" in result) {
|
|
99
|
+
// Popup opened — handle reauthorization
|
|
100
|
+
await this.handleGithubPopupReauth(result.popup);
|
|
101
|
+
}
|
|
84
102
|
}
|
|
85
103
|
else {
|
|
86
104
|
// Perform login if no session file exists, then save the state
|
|
@@ -97,21 +115,24 @@ export class LoginHelper {
|
|
|
97
115
|
async checkAndReauthorizeGithubApp() {
|
|
98
116
|
await new Promise((resolve) => {
|
|
99
117
|
this.page.once("popup", async (popup) => {
|
|
100
|
-
await
|
|
101
|
-
// Check for popup closure for up to 10 seconds before proceeding
|
|
102
|
-
for (let attempts = 0; attempts < 10 && !popup.isClosed(); attempts++) {
|
|
103
|
-
await this.page.waitForTimeout(1000); // Using page here because if the popup closes automatically, it throws an error during the wait
|
|
104
|
-
}
|
|
105
|
-
const locator = popup.locator("button.js-oauth-authorize-btn");
|
|
106
|
-
if (!popup.isClosed() && (await locator.isVisible())) {
|
|
107
|
-
await popup.locator("body").click();
|
|
108
|
-
await locator.waitFor();
|
|
109
|
-
await locator.click();
|
|
110
|
-
}
|
|
118
|
+
await this.handleGithubPopupReauth(popup);
|
|
111
119
|
resolve();
|
|
112
120
|
});
|
|
113
121
|
});
|
|
114
122
|
}
|
|
123
|
+
async handleGithubPopupReauth(popup) {
|
|
124
|
+
await popup.waitForLoadState();
|
|
125
|
+
// Check for popup closure for up to 10 seconds before proceeding
|
|
126
|
+
for (let attempts = 0; attempts < 10 && !popup.isClosed(); attempts++) {
|
|
127
|
+
await this.page.waitForTimeout(1000); // Using page here because if the popup closes automatically, it throws an error during the wait
|
|
128
|
+
}
|
|
129
|
+
const locator = popup.locator("button.js-oauth-authorize-btn");
|
|
130
|
+
if (!popup.isClosed() && (await locator.isVisible())) {
|
|
131
|
+
await popup.locator("body").click();
|
|
132
|
+
await locator.waitFor();
|
|
133
|
+
await locator.click();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
115
136
|
async googleSignIn(email) {
|
|
116
137
|
await new Promise((resolve) => {
|
|
117
138
|
this.page.once("popup", async (popup) => {
|