@camunda/e2e-test-suite 0.0.1443 → 0.0.1445
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/.github/c8_versions.json +1 -0
- package/README.md +2 -0
- package/dist/fixtures/SM-8.11.d.ts +77 -0
- package/dist/fixtures/SM-8.11.js +176 -0
- package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
- package/dist/pages/SM-8.11/Authorization.js +349 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
- package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
- package/dist/pages/SM-8.11/ClusterPage.js +37 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
- package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
- package/dist/pages/SM-8.11/FormJsPage.js +109 -0
- package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
- package/dist/pages/SM-8.11/HomePage.js +49 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
- package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
- package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
- package/dist/pages/SM-8.11/LoginPage.js +112 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
- package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
- package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
- package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
- package/dist/pages/SM-8.11/NavigationPage.js +389 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
- package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
- package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
- package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
- package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
- package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
- package/dist/pages/SM-8.11/PlayPage.js +389 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
- package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
- package/dist/pages/SM-8.11/SettingsPage.js +34 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
- package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
- package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
- package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
- package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/login.spec.js +24 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
- package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/play.spec.js +144 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
- package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
- package/package.json +1 -1
- package/playwright.config.ts +21 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
declare class NavigationPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly modelerPageBanner: Locator;
|
|
5
|
+
readonly operatePageBanner: Locator;
|
|
6
|
+
readonly tasklistPageBanner: Locator;
|
|
7
|
+
readonly optimizePageBanner: Locator;
|
|
8
|
+
readonly identityPageBanner: Locator;
|
|
9
|
+
readonly keyboardPageBanner: Locator;
|
|
10
|
+
readonly managementIdentityPageBanner: Locator;
|
|
11
|
+
private onboardingTourSuppressed;
|
|
12
|
+
constructor(page: Page);
|
|
13
|
+
private isGatewayErrorPage;
|
|
14
|
+
private waitForBanner;
|
|
15
|
+
goTo(url: string, banner: Locator, sleepTimeout?: number, { username, password, }?: {
|
|
16
|
+
username?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
}, maxRetries?: number): Promise<void>;
|
|
19
|
+
goToModeler(sleepTimeout?: number, credentials?: {
|
|
20
|
+
username: string;
|
|
21
|
+
password: string;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
private suppressOnboardingTour;
|
|
24
|
+
goToTasklist(sleepTimeout?: number, credentials?: {
|
|
25
|
+
username: string;
|
|
26
|
+
password: string;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
goToOperate(sleepTimeout?: number, credentials?: {
|
|
29
|
+
username: string;
|
|
30
|
+
password: string;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
goToOptimize(sleepTimeout?: number, credentials?: {
|
|
33
|
+
username: string;
|
|
34
|
+
password: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
goToOptimizeWithoutAccess({ username, password, }?: {
|
|
37
|
+
username?: string;
|
|
38
|
+
password?: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
goToModelerWithoutAccess({ username, password, }?: {
|
|
41
|
+
username?: string;
|
|
42
|
+
password?: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
goToKeycloak(maxRetries?: number): Promise<void>;
|
|
45
|
+
goToOCAdmin(sleepTimeout?: number, credentials?: {
|
|
46
|
+
username: string;
|
|
47
|
+
password: string;
|
|
48
|
+
}, maxRetries?: number): Promise<void>;
|
|
49
|
+
goToClusters(sleepTimeout?: number, credentials?: {
|
|
50
|
+
username: string;
|
|
51
|
+
password: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
goToManagementIdentity(sleepTimeout?: number, credentials?: {
|
|
54
|
+
username: string;
|
|
55
|
+
password: string;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export { NavigationPage };
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NavigationPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const LoginPage_1 = require("./LoginPage");
|
|
6
|
+
const sleep_1 = require("../../utils/sleep");
|
|
7
|
+
const constants_1 = require("../../utils/constants");
|
|
8
|
+
const env_1 = require("../../utils/env");
|
|
9
|
+
// Context paths use `||`, not `??`: an env var set to the empty string must fall back to the
|
|
10
|
+
// default. CI wiring that always exports a variable (set to "" when the leg has no override)
|
|
11
|
+
// otherwise defeats the default — `page.goto('')` resolves to Playwright's baseURL, so the app
|
|
12
|
+
// under test is never reached and the failure surfaces as an unrelated navigation timeout.
|
|
13
|
+
const ORCHESTRATION_CONTEXT_PATH = process.env.ORCHESTRATION_CONTEXT_PATH || '/orchestration';
|
|
14
|
+
const NORMALIZED_ORCHESTRATION_CONTEXT_PATH = ORCHESTRATION_CONTEXT_PATH.startsWith('/')
|
|
15
|
+
? ORCHESTRATION_CONTEXT_PATH
|
|
16
|
+
: `/${ORCHESTRATION_CONTEXT_PATH}`;
|
|
17
|
+
const MODELER_CONTEXT_PATH = process.env.MODELER_CONTEXT_PATH || '/modeler';
|
|
18
|
+
const OPTIMIZE_CONTEXT_PATH = process.env.OPTIMIZE_CONTEXT_PATH || '/optimize';
|
|
19
|
+
const MANAGEMENT_IDENTITY_CONTEXT_PATH = process.env.MANAGEMENT_IDENTITY_CONTEXT_PATH || '/identity';
|
|
20
|
+
const IDENTITY_FIRSTUSER_USERNAME = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_USERNAME || 'demo';
|
|
21
|
+
const IDENTITY_FIRSTUSER_PASSWORD = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD || 'demo';
|
|
22
|
+
class NavigationPage {
|
|
23
|
+
page;
|
|
24
|
+
modelerPageBanner;
|
|
25
|
+
operatePageBanner;
|
|
26
|
+
tasklistPageBanner;
|
|
27
|
+
optimizePageBanner;
|
|
28
|
+
identityPageBanner;
|
|
29
|
+
keyboardPageBanner;
|
|
30
|
+
managementIdentityPageBanner;
|
|
31
|
+
onboardingTourSuppressed = false;
|
|
32
|
+
constructor(page) {
|
|
33
|
+
this.page = page;
|
|
34
|
+
// Operate renders the Design System header (AppHeaderV2) whenever the
|
|
35
|
+
// `navV2Enabled` client config is on -- its home link is
|
|
36
|
+
// aria-label="Camunda Operate" instead of the legacy shell's
|
|
37
|
+
// "Camunda logo Operate" text. Match either so the banner resolves with
|
|
38
|
+
// the flag on or off.
|
|
39
|
+
this.operatePageBanner = page
|
|
40
|
+
.getByRole('link', { name: 'Camunda logo Operate' })
|
|
41
|
+
.or(page.getByRole('link', { name: 'Camunda Operate', exact: true }))
|
|
42
|
+
.first();
|
|
43
|
+
// Web Modeler renders the Design System nav shell (NavShellDs) whenever
|
|
44
|
+
// `design-system-enabled` is on. Its home link was
|
|
45
|
+
// aria-label="Camunda Modeler home"; the Camunda Hub rebrand renamed it to
|
|
46
|
+
// aria-label="Camunda Hub home" (still an aria-hidden logo, href .../modeler/),
|
|
47
|
+
// so the legacy shell's "Camunda logoModeler" text no longer exists. The
|
|
48
|
+
// unauthenticated shell reuses the same aria-label pointing at /login, and
|
|
49
|
+
// waitForBanner() gates only on visibility, so exclude that href here:
|
|
50
|
+
// callers use this banner as the logged-in gate.
|
|
51
|
+
// Match every shell so the banner resolves flag on or off, pre- or post-rebrand.
|
|
52
|
+
this.modelerPageBanner = page
|
|
53
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
54
|
+
.or(page.getByRole('link', { name: 'Camunda Modeler home' }))
|
|
55
|
+
.and(page.locator(':not([href$="/login"])'))
|
|
56
|
+
.or(page.locator('a').filter({ hasText: 'Camunda logoModeler' }))
|
|
57
|
+
.first();
|
|
58
|
+
// Tasklist renders the Design System header since the Carbon shell was
|
|
59
|
+
// deleted -- the logo is its own anchor (aria-label="Camunda logo", with
|
|
60
|
+
// an aria-hidden svg) and "Tasklist" moved into the breadcrumb nav, so
|
|
61
|
+
// the legacy shell's combined "Camunda logo Tasklist" name no longer
|
|
62
|
+
// exists. Match either shell so the banner resolves on both, and scope
|
|
63
|
+
// the generic logo-link fallback to Tasklist's orchestration href so it
|
|
64
|
+
// stays aligned with overridden context paths too.
|
|
65
|
+
this.tasklistPageBanner = page
|
|
66
|
+
.getByRole('navigation', { name: 'Camunda context' })
|
|
67
|
+
.getByRole('link', { name: 'Tasklist', exact: true })
|
|
68
|
+
.or(page
|
|
69
|
+
.getByRole('link', { name: 'Camunda logo', exact: true })
|
|
70
|
+
.and(page.locator(`[href$="${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/tasklist"]`)))
|
|
71
|
+
.or(page.getByRole('link', { name: 'Camunda logo Tasklist' }))
|
|
72
|
+
.first();
|
|
73
|
+
// Optimize renders the Design System header (HeaderV2) whenever the nav
|
|
74
|
+
// V2 client config is on -- its home link is aria-label="Camunda
|
|
75
|
+
// Optimize" wrapping an aria-hidden logo svg, so the legacy shell's
|
|
76
|
+
// "Camunda logo Optimize" text no longer exists. Match either so the
|
|
77
|
+
// banner resolves with the flag on or off (mirrors operatePageBanner and
|
|
78
|
+
// pages/8.10/OptimizeHomePage.optimizeBanner).
|
|
79
|
+
this.optimizePageBanner = page
|
|
80
|
+
.getByRole('link', { name: 'Camunda logo Optimize' })
|
|
81
|
+
.or(page.getByRole('link', { name: 'Camunda Optimize', exact: true }))
|
|
82
|
+
.first();
|
|
83
|
+
// TODO: Drop support for Identity once we fully rename it to Admin
|
|
84
|
+
this.identityPageBanner = page
|
|
85
|
+
.getByRole('link', { name: 'Camunda Admin', exact: true })
|
|
86
|
+
.or(page
|
|
87
|
+
.locator('a')
|
|
88
|
+
.filter({ hasText: /^Camunda logo\s*(Identity|Admin)$/ }))
|
|
89
|
+
.first();
|
|
90
|
+
this.managementIdentityPageBanner = page
|
|
91
|
+
.locator('a')
|
|
92
|
+
.filter({ hasText: /^Camunda logo\s*Management Identity$/ })
|
|
93
|
+
.first();
|
|
94
|
+
this.keyboardPageBanner = page.locator('#keycloak-bg');
|
|
95
|
+
}
|
|
96
|
+
async isGatewayErrorPage() {
|
|
97
|
+
return this.page
|
|
98
|
+
.getByRole('heading', {
|
|
99
|
+
name: /50[234]\s+(Bad Gateway|Service Temporarily Unavailable|Gateway Time-?out)/i,
|
|
100
|
+
})
|
|
101
|
+
.first()
|
|
102
|
+
.isVisible()
|
|
103
|
+
.catch(() => false);
|
|
104
|
+
}
|
|
105
|
+
// Waits for the app shell's banner, but treats the two states that cannot
|
|
106
|
+
// resolve on their own as a reason to act rather than to keep waiting.
|
|
107
|
+
//
|
|
108
|
+
// The passive `expect(banner).toBeVisible({timeout: 60000})` this replaces
|
|
109
|
+
// could not tell a shell that is merely slow (worth waiting for) from one
|
|
110
|
+
// where nothing is coming at all: `login` swallowed a missing form and
|
|
111
|
+
// submitted nothing, so no transition is pending, or the ingress served a
|
|
112
|
+
// 50x page for the OAuth callback, which never becomes the app shell. Both
|
|
113
|
+
// spend the whole 60s and lose the attempt, and the only surviving evidence
|
|
114
|
+
// is "toBeVisible ... <element(s) not found>" -- repeated once per attempt,
|
|
115
|
+
// that is exactly the reported "Login (/modeler) failed after 8 attempts".
|
|
116
|
+
// The 60s budget is unchanged: a genuinely slow shell still gets all of it,
|
|
117
|
+
// and a sustained outage still fails.
|
|
118
|
+
async waitForBanner(url, banner, submitted) {
|
|
119
|
+
const totalTimeout = constants_1._1_SECOND_IN_MS * 60;
|
|
120
|
+
const deadline = Date.now() + totalTimeout;
|
|
121
|
+
let lastState = 'no banner';
|
|
122
|
+
while (Date.now() < deadline) {
|
|
123
|
+
const round = Math.min(constants_1._1_SECOND_IN_MS * 20, deadline - Date.now());
|
|
124
|
+
try {
|
|
125
|
+
await (0, test_1.expect)(banner).toBeVisible({ timeout: round });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
lastState = `no banner on ${this.page.url()}`;
|
|
130
|
+
}
|
|
131
|
+
if (!submitted) {
|
|
132
|
+
// Nothing was submitted and no banner rendered, so there is no pending
|
|
133
|
+
// transition for the rest of the budget to wait on. Hand back to the
|
|
134
|
+
// caller's retry loop now — it re-navigates and logs in again, which
|
|
135
|
+
// is the only thing that can change this state.
|
|
136
|
+
throw new Error(`${url} rendered no banner and no credentials were submitted: ${lastState}`);
|
|
137
|
+
}
|
|
138
|
+
if (await this.isGatewayErrorPage()) {
|
|
139
|
+
lastState = `a 50x gateway page on ${this.page.url()}`;
|
|
140
|
+
if (Date.now() >= deadline) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
await this.page.goto(url, {
|
|
144
|
+
timeout: 60000,
|
|
145
|
+
waitUntil: 'domcontentloaded',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`${url} did not render its banner within ${totalTimeout}ms: ${lastState}`);
|
|
150
|
+
}
|
|
151
|
+
async goTo(url, banner, sleepTimeout, { username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}, maxRetries = 8) {
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
let timeout = constants_1._1_SECOND_IN_MS * 10;
|
|
154
|
+
if (url === '/modeler') {
|
|
155
|
+
timeout = constants_1._1_SECOND_IN_MS * 20;
|
|
156
|
+
}
|
|
157
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
if (sleepTimeout) {
|
|
160
|
+
await (0, sleep_1.sleep)(sleepTimeout);
|
|
161
|
+
}
|
|
162
|
+
await this.page.goto(url, {
|
|
163
|
+
timeout: 60000,
|
|
164
|
+
waitUntil: 'domcontentloaded',
|
|
165
|
+
});
|
|
166
|
+
// A transient ingress or pod-reschedule blip on the shared cluster
|
|
167
|
+
// serves an nginx 50x gateway page in place of the app. Reload with a
|
|
168
|
+
// bounded budget so a short blip self-heals; a sustained outage
|
|
169
|
+
// exhausts the budget and still surfaces as a failed navigation.
|
|
170
|
+
// The bound is wall clock as well as count: 8 reloads that each wait
|
|
171
|
+
// out the 60s goto budget are ~520s of the 720s test timeout inside a
|
|
172
|
+
// single attempt, which starves the retry loop below of the attempts
|
|
173
|
+
// it needs to ride the outage out.
|
|
174
|
+
const gatewayDeadline = Date.now() + constants_1._1_SECOND_IN_MS * 60;
|
|
175
|
+
let gatewayReloads = 0;
|
|
176
|
+
while (gatewayReloads < 8 &&
|
|
177
|
+
Date.now() < gatewayDeadline &&
|
|
178
|
+
(await this.isGatewayErrorPage())) {
|
|
179
|
+
await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
|
|
180
|
+
await this.page.goto(url, {
|
|
181
|
+
timeout: 60000,
|
|
182
|
+
waitUntil: 'domcontentloaded',
|
|
183
|
+
});
|
|
184
|
+
gatewayReloads++;
|
|
185
|
+
}
|
|
186
|
+
const loginPage = new LoginPage_1.LoginPage(this.page);
|
|
187
|
+
const loginEntryPoint = banner
|
|
188
|
+
.or(loginPage.usernameInput)
|
|
189
|
+
.or(loginPage.coreUsernameInput)
|
|
190
|
+
.or(loginPage.keycloakBanner)
|
|
191
|
+
.first();
|
|
192
|
+
await (0, test_1.expect)(loginEntryPoint).toBeVisible({ timeout: 60000 });
|
|
193
|
+
const isBannerVisible = await banner.isVisible().catch(() => false);
|
|
194
|
+
// Guard against the Modeler SPA rendering its header on the login
|
|
195
|
+
// page before the OAuth redirect fires. If the banner is visible
|
|
196
|
+
// but the URL is still a login/auth path, the user is NOT actually
|
|
197
|
+
// authenticated — wait for the real login form to appear.
|
|
198
|
+
const currentUrl = this.page.url();
|
|
199
|
+
const onLoginPath = currentUrl.includes('/login') || currentUrl.includes('/auth/');
|
|
200
|
+
let submitted = false;
|
|
201
|
+
if (isBannerVisible && !onLoginPath) {
|
|
202
|
+
// Probe with the same #username signal detectLoginForm uses: the
|
|
203
|
+
// KC26 default theme does not expose the 'Username or email' label,
|
|
204
|
+
// so the old locator reported "no form" on a rendered Keycloak login
|
|
205
|
+
// page and left the attempt unauthenticated.
|
|
206
|
+
const loginVisible = await this.page
|
|
207
|
+
.locator('#username')
|
|
208
|
+
.or(loginPage.usernameInput)
|
|
209
|
+
.first()
|
|
210
|
+
.isVisible()
|
|
211
|
+
.catch(() => false);
|
|
212
|
+
if (loginVisible) {
|
|
213
|
+
submitted = await loginPage.login(username, password);
|
|
214
|
+
}
|
|
215
|
+
// If login selector not visible, banner is real - navigation succeeded
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
submitted = await loginPage.login(username, password);
|
|
219
|
+
}
|
|
220
|
+
await this.waitForBanner(url, banner, submitted);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const elapsed = Date.now() - startTime;
|
|
226
|
+
if (attempt < maxRetries - 1) {
|
|
227
|
+
// Progressive backoff: a transient Keycloak 5xx during the OAuth
|
|
228
|
+
// login (the "We are sorry... internal server error" /
|
|
229
|
+
// "Unexpected error when handling authentication request to
|
|
230
|
+
// identity provider" pages) can persist for several seconds.
|
|
231
|
+
// Widening the gap between attempts (2s, 4s, 6s, ...) lets the
|
|
232
|
+
// auth server recover instead of burning every retry inside one
|
|
233
|
+
// short outage window. Bumped from 5 to 8 retries after run
|
|
234
|
+
// 30080472743's "Basic Login" failed all 5 attempts against a
|
|
235
|
+
// freshly-installed cluster, each with a genuine 400/500 from
|
|
236
|
+
// Keycloak's own login-actions/authenticate endpoint -- the prior
|
|
237
|
+
// budget (~20s of backoff) wasn't consistently enough to outlast
|
|
238
|
+
// Keycloak's post-install warm-up window.
|
|
239
|
+
await (0, sleep_1.sleep)(2000 * (attempt + 1));
|
|
240
|
+
if (!this.page.isClosed()) {
|
|
241
|
+
try {
|
|
242
|
+
await this.page.waitForLoadState('load', { timeout: timeout });
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const now2 = new Date();
|
|
246
|
+
const elapsed2 = Date.now() - startTime;
|
|
247
|
+
console.warn(`[${now2.toISOString()}] (+${elapsed2}ms) [NavigationPage] Load state timeout or error: ${error}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(`[${now.toISOString()}] (+${elapsed}ms) [NavigationPage] Login (${url}) failed after ${maxRetries} attempts: ${error}`);
|
|
253
|
+
// Carry the final attempt's error into the thrown message. Without
|
|
254
|
+
// it the only surviving signal is the attempt count, which says
|
|
255
|
+
// nothing about WHY all 8 attempts failed -- a Keycloak 5xx, a
|
|
256
|
+
// gateway page, a missing banner and a closed page all collapse to
|
|
257
|
+
// the same string, so triage of this signature has to guess. Every
|
|
258
|
+
// sibling helper already does this (goToKeycloak below rethrows
|
|
259
|
+
// lastError; KeycloakUtils and pages/8.10/UtilitiesPage interpolate
|
|
260
|
+
// the cause).
|
|
261
|
+
throw new Error(`Login (${url}) failed after ${maxRetries} attempts: ${error}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async goToModeler(sleepTimeout, credentials) {
|
|
267
|
+
await this.suppressOnboardingTour();
|
|
268
|
+
await this.goTo(MODELER_CONTEXT_PATH, this.modelerPageBanner, sleepTimeout, credentials, 8);
|
|
269
|
+
}
|
|
270
|
+
// Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
|
|
271
|
+
// (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens, rather
|
|
272
|
+
// than dismissing it reactively (the two prior fixes here -- both raced the dialog's mount
|
|
273
|
+
// and lost, since the user id needed for the real key isn't known until after that same
|
|
274
|
+
// race). addInitScript avoids the race by running before any page script, matching the
|
|
275
|
+
// flag by prefix since the user id isn't known up front.
|
|
276
|
+
async suppressOnboardingTour() {
|
|
277
|
+
if (this.onboardingTourSuppressed) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.onboardingTourSuppressed = true;
|
|
281
|
+
await this.page.addInitScript((prefixPattern) => {
|
|
282
|
+
const pattern = new RegExp(prefixPattern);
|
|
283
|
+
const originalGetItem = Storage.prototype.getItem;
|
|
284
|
+
Storage.prototype.getItem = function (key) {
|
|
285
|
+
return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
|
|
286
|
+
};
|
|
287
|
+
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
288
|
+
}
|
|
289
|
+
async goToTasklist(sleepTimeout, credentials) {
|
|
290
|
+
await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/tasklist`, this.tasklistPageBanner, sleepTimeout, credentials);
|
|
291
|
+
}
|
|
292
|
+
async goToOperate(sleepTimeout, credentials) {
|
|
293
|
+
await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/operate`, this.operatePageBanner, sleepTimeout, credentials);
|
|
294
|
+
}
|
|
295
|
+
async goToOptimize(sleepTimeout, credentials) {
|
|
296
|
+
await this.goTo(OPTIMIZE_CONTEXT_PATH, this.optimizePageBanner, sleepTimeout, credentials);
|
|
297
|
+
}
|
|
298
|
+
// `goTo` ends by asserting the app banner, so it cannot be used to reach
|
|
299
|
+
// Optimize as a user without the Optimize role: that request ends on a
|
|
300
|
+
// `text/plain` 403 page carrying neither the banner nor a login form, and
|
|
301
|
+
// every retry burns a full 60s timeout. Navigate and log in only; the
|
|
302
|
+
// caller asserts what the denied page shows.
|
|
303
|
+
async goToOptimizeWithoutAccess({ username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}) {
|
|
304
|
+
await this.page.goto(OPTIMIZE_CONTEXT_PATH, {
|
|
305
|
+
timeout: 60000,
|
|
306
|
+
waitUntil: 'domcontentloaded',
|
|
307
|
+
});
|
|
308
|
+
// On a fresh install Optimize can sit behind an nginx 503 for minutes
|
|
309
|
+
// after the other apps are up. `goTo` rides that out across its retry
|
|
310
|
+
// attempts; this helper has no retry loop, so it waits on the wall clock
|
|
311
|
+
// instead. If the gateway page outlasts the budget, fail on it here: the
|
|
312
|
+
// caller's denial assertion would otherwise time out with a "not found"
|
|
313
|
+
// that hides the outage.
|
|
314
|
+
const gatewayBudgetMs = constants_1._1_SECOND_IN_MS * 180;
|
|
315
|
+
const gatewayDeadline = Date.now() + gatewayBudgetMs;
|
|
316
|
+
while (Date.now() < gatewayDeadline && (await this.isGatewayErrorPage())) {
|
|
317
|
+
await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
|
|
318
|
+
await this.page.goto(OPTIMIZE_CONTEXT_PATH, {
|
|
319
|
+
timeout: 60000,
|
|
320
|
+
waitUntil: 'domcontentloaded',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (await this.isGatewayErrorPage()) {
|
|
324
|
+
throw new Error(`${OPTIMIZE_CONTEXT_PATH} still served a 50x gateway page after ${gatewayBudgetMs}ms`);
|
|
325
|
+
}
|
|
326
|
+
await new LoginPage_1.LoginPage(this.page).login(username, password);
|
|
327
|
+
}
|
|
328
|
+
// Same reason as `goToOptimizeWithoutAccess`: a user whose Web Modeler role
|
|
329
|
+
// was removed never reaches a page carrying the Modeler banner — the OAuth
|
|
330
|
+
// round trip ends back on the Keycloak authorize URL — so `goTo` burns all 8
|
|
331
|
+
// attempts (~570s) and throws instead of letting the caller assert the denial.
|
|
332
|
+
// Navigate and log in only; the caller asserts what the denied page shows.
|
|
333
|
+
async goToModelerWithoutAccess({ username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}) {
|
|
334
|
+
await this.page.goto(MODELER_CONTEXT_PATH, {
|
|
335
|
+
timeout: 60000,
|
|
336
|
+
waitUntil: 'domcontentloaded',
|
|
337
|
+
});
|
|
338
|
+
let gatewayReloads = 0;
|
|
339
|
+
while (gatewayReloads < 8 && (await this.isGatewayErrorPage())) {
|
|
340
|
+
await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
|
|
341
|
+
await this.page.goto(MODELER_CONTEXT_PATH, {
|
|
342
|
+
timeout: 60000,
|
|
343
|
+
waitUntil: 'domcontentloaded',
|
|
344
|
+
});
|
|
345
|
+
gatewayReloads++;
|
|
346
|
+
}
|
|
347
|
+
await new LoginPage_1.LoginPage(this.page).login(username, password);
|
|
348
|
+
}
|
|
349
|
+
async goToKeycloak(maxRetries = 5) {
|
|
350
|
+
const keycloakUrl = process.env.KEYCLOAK_URL
|
|
351
|
+
? `${process.env.KEYCLOAK_URL}/auth`
|
|
352
|
+
: '/auth';
|
|
353
|
+
let lastError;
|
|
354
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
355
|
+
try {
|
|
356
|
+
await this.page.goto(keycloakUrl);
|
|
357
|
+
await (0, test_1.expect)(this.keyboardPageBanner).toBeVisible({ timeout: 60000 });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
lastError = error;
|
|
362
|
+
if (attempt < maxRetries - 1) {
|
|
363
|
+
await (0, sleep_1.sleep)(5000);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw lastError instanceof Error
|
|
368
|
+
? lastError
|
|
369
|
+
: new Error(`goToKeycloak failed after ${maxRetries} attempts`);
|
|
370
|
+
}
|
|
371
|
+
async goToOCAdmin(sleepTimeout, credentials, maxRetries) {
|
|
372
|
+
await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/admin`, this.identityPageBanner, sleepTimeout, credentials,
|
|
373
|
+
// 8, not 5, to match goTo's own default: the comment there records that
|
|
374
|
+
// ~20s of backoff (all 5 attempts buy) was not consistently enough to
|
|
375
|
+
// outlast Keycloak's warm-up, and /admin additionally serves a
|
|
376
|
+
// forbidden redirect while the user's admin authorization propagates.
|
|
377
|
+
// Call sites that wrap goToOCAdmin in their own toPass loop still pass
|
|
378
|
+
// maxRetries explicitly (see document-handling-user-flows).
|
|
379
|
+
maxRetries ?? (env_1.isOpenSearch ? 10 : 8));
|
|
380
|
+
}
|
|
381
|
+
async goToClusters(sleepTimeout, credentials) {
|
|
382
|
+
await this.suppressOnboardingTour();
|
|
383
|
+
await this.goTo(`${MODELER_CONTEXT_PATH}/clusters`, this.modelerPageBanner, sleepTimeout, credentials);
|
|
384
|
+
}
|
|
385
|
+
async goToManagementIdentity(sleepTimeout, credentials) {
|
|
386
|
+
await this.goTo(MANAGEMENT_IDENTITY_CONTEXT_PATH, this.managementIdentityPageBanner, sleepTimeout, credentials);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
exports.NavigationPage = NavigationPage;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Authorization } from '../SM-8.11/Authorization';
|
|
2
|
+
import { Page } from '@playwright/test';
|
|
3
|
+
declare class OCIdentityAuthorizationsPage extends Authorization {
|
|
4
|
+
constructor(page: Page);
|
|
5
|
+
clickProcessDefinitionTab(): Promise<void>;
|
|
6
|
+
clickAuthorizationTab(): Promise<void>;
|
|
7
|
+
clickComponentTab(): Promise<void>;
|
|
8
|
+
clickResourceTab(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export { OCIdentityAuthorizationsPage };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OCIdentityAuthorizationsPage = void 0;
|
|
4
|
+
const Authorization_1 = require("../SM-8.11/Authorization");
|
|
5
|
+
class OCIdentityAuthorizationsPage extends Authorization_1.Authorization {
|
|
6
|
+
constructor(page) {
|
|
7
|
+
super(page);
|
|
8
|
+
}
|
|
9
|
+
async clickProcessDefinitionTab() {
|
|
10
|
+
await this.selectResourceType('PROCESS_DEFINITION');
|
|
11
|
+
}
|
|
12
|
+
async clickAuthorizationTab() {
|
|
13
|
+
await this.selectResourceType('AUTHORIZATION');
|
|
14
|
+
}
|
|
15
|
+
async clickComponentTab() {
|
|
16
|
+
await this.selectResourceType('COMPONENT');
|
|
17
|
+
}
|
|
18
|
+
async clickResourceTab() {
|
|
19
|
+
await this.selectResourceType('RESOURCE');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.OCIdentityAuthorizationsPage = OCIdentityAuthorizationsPage;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
export declare class OCIdentityClusterVariablesPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly variablesList: Locator;
|
|
5
|
+
readonly editVariableButton: (variableName: string) => Locator;
|
|
6
|
+
readonly editVariableModal: Locator;
|
|
7
|
+
readonly variableValueField: Locator;
|
|
8
|
+
readonly saveVariableButton: Locator;
|
|
9
|
+
readonly closeEditVariableModal: Locator;
|
|
10
|
+
readonly cancelButton: Locator;
|
|
11
|
+
readonly variableRow: (variableName: string) => Locator;
|
|
12
|
+
readonly monacoEditor: Locator;
|
|
13
|
+
readonly monacoEditorTextArea: Locator;
|
|
14
|
+
readonly monacoSuggestWidget: Locator;
|
|
15
|
+
readonly successMessage: Locator;
|
|
16
|
+
readonly editMenuItem: Locator;
|
|
17
|
+
readonly editModalHeading: Locator;
|
|
18
|
+
constructor(page: Page);
|
|
19
|
+
editVariable(variableName: string, newValue: string): Promise<void>;
|
|
20
|
+
assertVariableExists(variableName: string): Promise<void>;
|
|
21
|
+
assertVariableValue(variableName: string, expectedValue: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OCIdentityClusterVariablesPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
class OCIdentityClusterVariablesPage {
|
|
7
|
+
page;
|
|
8
|
+
variablesList;
|
|
9
|
+
editVariableButton;
|
|
10
|
+
editVariableModal;
|
|
11
|
+
variableValueField;
|
|
12
|
+
saveVariableButton;
|
|
13
|
+
closeEditVariableModal;
|
|
14
|
+
cancelButton;
|
|
15
|
+
variableRow;
|
|
16
|
+
monacoEditor;
|
|
17
|
+
monacoEditorTextArea;
|
|
18
|
+
monacoSuggestWidget;
|
|
19
|
+
successMessage;
|
|
20
|
+
editMenuItem;
|
|
21
|
+
editModalHeading;
|
|
22
|
+
constructor(page) {
|
|
23
|
+
this.page = page;
|
|
24
|
+
// The Admin UI is migrating off Carbon onto the Camunda design system:
|
|
25
|
+
// `identity/client/src/pages/cluster-variables/index.tsx` renders ListV2
|
|
26
|
+
// (design-system `DataTable`, Radix dialog/menu) when the
|
|
27
|
+
// `isNewDesignSystemEnabled` client config is on, which the orchestration
|
|
28
|
+
// server defaults to for Self-Managed. Released 8.10 alpha images predate
|
|
29
|
+
// ListV2 and still render Carbon, so the locators that differ between the
|
|
30
|
+
// two DOMs accept either.
|
|
31
|
+
this.variablesList = page.locator('table.cds--data-table, table[data-slot="table"]');
|
|
32
|
+
this.variableRow = (variableName) => this.variablesList
|
|
33
|
+
.locator('tbody')
|
|
34
|
+
.getByRole('row')
|
|
35
|
+
.filter({ hasText: variableName });
|
|
36
|
+
// Carbon's OverflowMenu labels its trigger "Options"; the design-system
|
|
37
|
+
// DataTable labels it "Row actions".
|
|
38
|
+
this.editVariableButton = (variableName) => this.variableRow(variableName).getByRole('button', {
|
|
39
|
+
name: /^(Options|Row actions)$/,
|
|
40
|
+
});
|
|
41
|
+
this.editVariableModal = page.getByRole('dialog', {
|
|
42
|
+
name: /Edit.*variable/i,
|
|
43
|
+
});
|
|
44
|
+
this.variableValueField = this.editVariableModal.getByRole('textbox', {
|
|
45
|
+
name: 'Editor content',
|
|
46
|
+
});
|
|
47
|
+
// Both modals pass `confirmLabel={t("save")}`, so the accessible name is
|
|
48
|
+
// stable across the two footers.
|
|
49
|
+
this.saveVariableButton = this.editVariableModal.getByRole('button', {
|
|
50
|
+
name: 'Save',
|
|
51
|
+
});
|
|
52
|
+
this.closeEditVariableModal = this.editVariableModal.getByRole('button', {
|
|
53
|
+
name: 'Close',
|
|
54
|
+
});
|
|
55
|
+
this.cancelButton = this.editVariableModal.getByRole('button', {
|
|
56
|
+
name: 'Cancel',
|
|
57
|
+
});
|
|
58
|
+
this.monacoEditor = this.editVariableModal.locator('.monaco-editor');
|
|
59
|
+
this.monacoEditorTextArea = this.monacoEditor.locator('textarea.inputarea');
|
|
60
|
+
this.monacoSuggestWidget = this.monacoEditor.locator('.editor-widget.suggest-widget');
|
|
61
|
+
this.successMessage = this.page.getByText('Cluster variable updated');
|
|
62
|
+
// Carbon renders the modal title as an h3, the design-system dialog as an
|
|
63
|
+
// h2; the heading role covers both.
|
|
64
|
+
this.editModalHeading = this.editVariableModal.getByRole('heading').first();
|
|
65
|
+
// Carbon menu items are buttons, Radix menu items are divs — match on the
|
|
66
|
+
// role instead of the tag.
|
|
67
|
+
this.editMenuItem = this.page.getByRole('menuitem', {
|
|
68
|
+
name: 'Edit',
|
|
69
|
+
exact: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async editVariable(variableName, newValue) {
|
|
73
|
+
await this.editVariableButton(variableName).click();
|
|
74
|
+
await (0, test_1.expect)(this.editMenuItem).toBeVisible();
|
|
75
|
+
await this.editMenuItem.click();
|
|
76
|
+
await (0, test_1.expect)(this.variableValueField).toBeVisible();
|
|
77
|
+
await (0, test_1.expect)(this.monacoEditor).toBeVisible();
|
|
78
|
+
// Monaco editor doesn't respond reliably to clear()/fill() on the
|
|
79
|
+
// hidden textarea. Use keyboard shortcuts to select-all and replace,
|
|
80
|
+
// which mirrors real user interaction.
|
|
81
|
+
// Retry the clear-type-enable cycle because Monaco auto-completion of
|
|
82
|
+
// brackets / quotes can corrupt JSON input on the first attempt.
|
|
83
|
+
const MAX_TYPE_ATTEMPTS = 3;
|
|
84
|
+
for (let attempt = 1; attempt <= MAX_TYPE_ATTEMPTS; attempt++) {
|
|
85
|
+
await this.monacoEditor.click();
|
|
86
|
+
await this.monacoEditorTextArea.press('Control+a');
|
|
87
|
+
await this.monacoEditorTextArea.press('Delete');
|
|
88
|
+
// Let Monaco settle after clearing content
|
|
89
|
+
await (0, sleep_1.sleep)(500);
|
|
90
|
+
await this.monacoEditorTextArea.pressSequentially(newValue, { delay: 20 });
|
|
91
|
+
// Dismiss any auto-complete popup by clicking the modal heading.
|
|
92
|
+
// Do NOT use press('Escape') on the textarea — when no autocomplete
|
|
93
|
+
// is showing, the Escape event propagates to the Carbon modal and
|
|
94
|
+
// closes the entire dialog.
|
|
95
|
+
await this.editModalHeading.click();
|
|
96
|
+
// Let Monaco settle and validate the new content
|
|
97
|
+
await (0, sleep_1.sleep)(500);
|
|
98
|
+
try {
|
|
99
|
+
await (0, test_1.expect)(this.saveVariableButton).toBeEnabled({ timeout: 10000 });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
if (attempt === MAX_TYPE_ATTEMPTS) {
|
|
104
|
+
throw new Error(`Save button did not become enabled after ${MAX_TYPE_ATTEMPTS} typing attempts`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await this.saveVariableButton.click();
|
|
109
|
+
await (0, test_1.expect)(this.successMessage).toBeVisible();
|
|
110
|
+
await (0, test_1.expect)(this.editVariableModal).toBeHidden();
|
|
111
|
+
}
|
|
112
|
+
async assertVariableExists(variableName) {
|
|
113
|
+
await (0, test_1.expect)(this.variableRow(variableName)).toBeVisible();
|
|
114
|
+
}
|
|
115
|
+
async assertVariableValue(variableName, expectedValue) {
|
|
116
|
+
const row = this.variableRow(variableName);
|
|
117
|
+
await (0, test_1.expect)(row).toContainText(expectedValue);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.OCIdentityClusterVariablesPage = OCIdentityClusterVariablesPage;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
export declare class OCIdentityGroupsPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly groupsList: Locator;
|
|
5
|
+
readonly createGroupButton: Locator;
|
|
6
|
+
readonly editGroupButton: (rowName?: string) => Locator;
|
|
7
|
+
readonly deleteGroupButton: (rowName?: string) => Locator;
|
|
8
|
+
readonly createGroupModal: Locator;
|
|
9
|
+
readonly closeCreateGroupModal: Locator;
|
|
10
|
+
readonly createGroupIdField: Locator;
|
|
11
|
+
readonly createNameField: Locator;
|
|
12
|
+
readonly createDescriptionField: Locator;
|
|
13
|
+
readonly createGroupModalCancelButton: Locator;
|
|
14
|
+
readonly createGroupModalCreateButton: Locator;
|
|
15
|
+
readonly editGroupModal: Locator;
|
|
16
|
+
readonly closeEditGroupModal: Locator;
|
|
17
|
+
readonly editNameField: Locator;
|
|
18
|
+
readonly editDescriptionField: Locator;
|
|
19
|
+
readonly editGroupModalCancelButton: Locator;
|
|
20
|
+
readonly editGroupModalUpdateButton: Locator;
|
|
21
|
+
readonly deleteGroupModal: Locator;
|
|
22
|
+
readonly closeDeleteGroupModal: Locator;
|
|
23
|
+
readonly deleteGroupModalCancelButton: Locator;
|
|
24
|
+
readonly deleteGroupModalDeleteButton: Locator;
|
|
25
|
+
readonly emptyState: Locator;
|
|
26
|
+
readonly assignUserButton: Locator;
|
|
27
|
+
readonly searchBox: Locator;
|
|
28
|
+
readonly searchBoxResult: Locator;
|
|
29
|
+
readonly assignUserButtonModal: Locator;
|
|
30
|
+
readonly selectGroupRow: (name: string) => Locator;
|
|
31
|
+
readonly groupCell: (name: string) => Locator;
|
|
32
|
+
constructor(page: Page);
|
|
33
|
+
createGroup(groupId: string, groupName: string, description?: string): Promise<void>;
|
|
34
|
+
editGroup(currentName: string, newName: string, newDescription?: string): Promise<void>;
|
|
35
|
+
deleteGroup(groupName: string): Promise<void>;
|
|
36
|
+
assertGroupExists(groupName: string): Promise<void>;
|
|
37
|
+
clickGroupId(groupName: string): Promise<void>;
|
|
38
|
+
assignUserToGroup(userName: string): Promise<void>;
|
|
39
|
+
}
|