@camunda/e2e-test-suite 0.0.1443 → 0.0.1444

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.
Files changed (122) hide show
  1. package/.github/c8_versions.json +1 -0
  2. package/README.md +2 -0
  3. package/dist/fixtures/SM-8.11.d.ts +77 -0
  4. package/dist/fixtures/SM-8.11.js +176 -0
  5. package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
  6. package/dist/pages/SM-8.11/Authorization.js +349 -0
  7. package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
  8. package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
  9. package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
  10. package/dist/pages/SM-8.11/ClusterPage.js +37 -0
  11. package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
  12. package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
  13. package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
  14. package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
  15. package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
  16. package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
  17. package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
  18. package/dist/pages/SM-8.11/FormJsPage.js +109 -0
  19. package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
  20. package/dist/pages/SM-8.11/HomePage.js +49 -0
  21. package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
  22. package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
  23. package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
  24. package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
  25. package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
  26. package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
  27. package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
  28. package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
  29. package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
  30. package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
  31. package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
  32. package/dist/pages/SM-8.11/LoginPage.js +112 -0
  33. package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
  34. package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
  35. package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
  36. package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
  37. package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
  38. package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
  39. package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
  40. package/dist/pages/SM-8.11/NavigationPage.js +389 -0
  41. package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
  42. package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
  43. package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
  44. package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
  45. package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
  46. package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
  47. package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
  48. package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
  49. package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
  50. package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
  51. package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
  52. package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
  53. package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
  54. package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
  55. package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
  56. package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
  57. package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
  58. package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
  59. package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
  60. package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
  61. package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
  62. package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
  63. package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
  64. package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
  65. package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
  66. package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
  67. package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
  68. package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
  69. package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
  70. package/dist/pages/SM-8.11/PlayPage.js +389 -0
  71. package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
  72. package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
  73. package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
  74. package/dist/pages/SM-8.11/SettingsPage.js +34 -0
  75. package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
  76. package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
  77. package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
  78. package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
  79. package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
  80. package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
  81. package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
  82. package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
  83. package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
  84. package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
  85. package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
  86. package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
  87. package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
  88. package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
  89. package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
  90. package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
  91. package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
  92. package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
  93. package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
  94. package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
  95. package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
  96. package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
  97. package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
  98. package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
  99. package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
  100. package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
  101. package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
  102. package/dist/tests/SM-8.11/login.spec.js +24 -0
  103. package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
  104. package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
  105. package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
  106. package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
  107. package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
  108. package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
  109. package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
  110. package/dist/tests/SM-8.11/play.spec.js +144 -0
  111. package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
  112. package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
  113. package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
  114. package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
  115. package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
  116. package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
  117. package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
  118. package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
  119. package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
  120. package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
  121. package/package.json +1 -1
  122. 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
+ }