@camunda/e2e-test-suite 0.0.1252 → 0.0.1254

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 (31) hide show
  1. package/dist/pages/8.10/AppsPage.js +6 -2
  2. package/dist/pages/8.10/ClusterDetailsPage.d.ts +2 -2
  3. package/dist/pages/8.10/ClusterDetailsPage.js +75 -31
  4. package/dist/pages/8.10/ClusterPage.js +30 -2
  5. package/dist/pages/8.10/ClusterSecretsPage.js +142 -10
  6. package/dist/pages/8.10/HomePage.d.ts +2 -0
  7. package/dist/pages/8.10/HomePage.js +40 -2
  8. package/dist/pages/8.10/LoginPage.d.ts +1 -0
  9. package/dist/pages/8.10/LoginPage.js +29 -0
  10. package/dist/pages/8.10/ModelerHomePage.d.ts +8 -0
  11. package/dist/pages/8.10/ModelerHomePage.js +221 -35
  12. package/dist/pages/8.10/UtilitiesPage.js +58 -2
  13. package/dist/pages/8.7/AppsPage.d.ts +1 -0
  14. package/dist/pages/8.7/AppsPage.js +23 -4
  15. package/dist/pages/8.7/ClusterDetailsPage.js +14 -2
  16. package/dist/pages/8.7/ClusterPage.js +8 -0
  17. package/dist/pages/8.7/ConnectorMarketplacePage.d.ts +1 -0
  18. package/dist/pages/8.7/ConnectorMarketplacePage.js +24 -2
  19. package/dist/pages/8.7/ConsoleOrganizationPage.js +11 -0
  20. package/dist/pages/8.7/FormJsPage.js +30 -4
  21. package/dist/pages/8.7/ModelerCreatePage.js +33 -1
  22. package/dist/pages/8.7/ModelerHomePage.d.ts +2 -0
  23. package/dist/pages/8.7/ModelerHomePage.js +18 -0
  24. package/dist/pages/8.7/ProcessApplicationPage.d.ts +1 -0
  25. package/dist/pages/8.7/ProcessApplicationPage.js +87 -0
  26. package/dist/pages/8.7/UtilitiesPage.js +7 -1
  27. package/dist/tests/8.10/test-setup.spec.js +5 -2
  28. package/dist/tests/8.7/web-modeler-user-flows.spec.js +3 -15
  29. package/dist/utils/modelerCluster.d.ts +13 -0
  30. package/dist/utils/modelerCluster.js +2 -1
  31. package/package.json +1 -1
@@ -261,8 +261,12 @@ class AppsPage {
261
261
  }
262
262
  };
263
263
  for (let attempt = 0; attempt < maxRetries; attempt++) {
264
- await this.clickCamundaApps();
265
- await this.clickModeler();
264
+ // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
265
+ // app switcher: its home already is the Modeler home.
266
+ if (!new URL(this.page.url()).hostname.startsWith('hub.')) {
267
+ await this.clickCamundaApps();
268
+ await this.clickModeler();
269
+ }
266
270
  try {
267
271
  await modelerReady();
268
272
  return;
@@ -32,6 +32,7 @@ declare class ClusterDetailsPage {
32
32
  readonly createAlertButton: Locator;
33
33
  readonly createNewClientButton: Locator;
34
34
  readonly clientsList: Locator;
35
+ readonly clientsFilterInput: Locator;
35
36
  readonly envVarsTab: Locator;
36
37
  readonly zeebeCheckbox: Locator;
37
38
  readonly apiClientNameTextbox: Locator;
@@ -50,7 +51,7 @@ declare class ClusterDetailsPage {
50
51
  readonly restartRequiredDialog: Locator;
51
52
  readonly restartConfirmButton: Locator;
52
53
  readonly mcpTab: Locator;
53
- readonly mcpTextarea: Locator;
54
+ readonly mcpConfigBlock: Locator;
54
55
  constructor(page: Page);
55
56
  clickAPITab(): Promise<void>;
56
57
  dismissOopsIfPresent(): Promise<void>;
@@ -64,7 +65,6 @@ declare class ClusterDetailsPage {
64
65
  clickCreateButton(): Promise<void>;
65
66
  clickCloseModalButton(): Promise<void>;
66
67
  assertAPIClientScopes(name: string): Promise<void>;
67
- filterClientsList(name: string): Promise<void>;
68
68
  clickSettingsTab(): Promise<void>;
69
69
  private waitForAuthorizationsSection;
70
70
  enableAuthorizations(): Promise<void>;
@@ -39,6 +39,7 @@ class ClusterDetailsPage {
39
39
  createAlertButton;
40
40
  createNewClientButton;
41
41
  clientsList;
42
+ clientsFilterInput;
42
43
  envVarsTab;
43
44
  zeebeCheckbox;
44
45
  apiClientNameTextbox;
@@ -57,7 +58,7 @@ class ClusterDetailsPage {
57
58
  restartRequiredDialog;
58
59
  restartConfirmButton;
59
60
  mcpTab;
60
- mcpTextarea;
61
+ mcpConfigBlock;
61
62
  constructor(page) {
62
63
  this.page = page;
63
64
  this.apiTab = page.getByRole('tab', { name: 'API' });
@@ -101,7 +102,14 @@ class ClusterDetailsPage {
101
102
  this.clientCredentialsDialog = page.getByRole('dialog').filter({
102
103
  has: page.getByText('The Client Secret will not be shown again.'),
103
104
  });
104
- this.closeModalButton = this.clientCredentialsDialog.getByLabel('Close');
105
+ // Hub's client-credentials dialog (design-system Dialog, not the old
106
+ // Carbon one) exposes its close affix as an accessible button named
107
+ // "Close" rather than via an aria-label picked up by getByLabel -- see
108
+ // camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
109
+ // (clientCredentialsDialogCloseButton), which already uses this pattern.
110
+ this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
111
+ name: 'Close',
112
+ });
105
113
  this.clustersLink = page
106
114
  .getByRole('banner')
107
115
  .getByRole('link', { name: 'Clusters' });
@@ -129,6 +137,17 @@ class ClusterDetailsPage {
129
137
  name: 'Create new client',
130
138
  });
131
139
  this.clientsList = page.getByRole('row').filter({ hasNotText: 'Scopes' });
140
+ // The clients table paginates at ten rows and is not ordered by creation
141
+ // time, so on a cluster that already has clients a newly created one is
142
+ // usually on a page other than the one being displayed -- matching a row
143
+ // without filtering first reports it as missing (main's filterClientsList
144
+ // existed for exactly this). "Filter table" was Carbon's default
145
+ // TableToolbarSearch label; Hub's rebranded panel renders a plain
146
+ // "Search" input instead, so match either.
147
+ this.clientsFilterInput = page
148
+ .getByRole('searchbox', { name: 'Filter table' })
149
+ .or(page.getByRole('searchbox', { name: 'Search' }))
150
+ .or(page.getByPlaceholder('Search'));
132
151
  this.envVarsTab = this.clientCredentialsDialog.getByRole('tab', {
133
152
  name: 'Env Vars',
134
153
  });
@@ -179,7 +198,13 @@ class ClusterDetailsPage {
179
198
  name: 'Continue',
180
199
  });
181
200
  this.mcpTab = page.getByRole('tab', { name: 'MCP' });
182
- this.mcpTextarea = page.getByRole('textbox', { name: 'Copy to clipboard' });
201
+ // The config text renders in a <pre> inside the MCP-labelled panel, not
202
+ // a textbox -- matches the already-working 8.9 sibling's locator. A live
203
+ // trace confirmed "Copy to clipboard" is actually the aria-label/title of
204
+ // an unrelated icon-only <button> positioned over that block, which is
205
+ // why getByRole('textbox', {name: 'Copy to clipboard'}) always found
206
+ // nothing here.
207
+ this.mcpConfigBlock = page.getByLabel('MCP', { exact: true }).locator('pre');
183
208
  }
184
209
  async clickAPITab() {
185
210
  await (0, test_1.expect)(this.apiTab).toBeVisible({ timeout: 60000 });
@@ -252,33 +277,34 @@ class ClusterDetailsPage {
252
277
  async assertAPIClientScopes(name) {
253
278
  const row = this.clientsList.filter({ hasText: name });
254
279
  const scopes = /(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/;
255
- await this.filterClientsList(name);
256
- try {
257
- await (0, test_1.expect)(row).toContainText(scopes, { timeout: 30000 });
280
+ const maxAttempts = 3;
281
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
282
+ try {
283
+ // The table paginates and isn't ordered by creation time, so a newly
284
+ // created client is usually on a page other than the one displayed --
285
+ // filter down to it first, the same way main's filterClientsList did,
286
+ // rather than matching rows only on the currently rendered page.
287
+ await (0, test_1.expect)(this.clientsFilterInput).toBeVisible({ timeout: 60000 });
288
+ await this.clientsFilterInput.fill(name);
289
+ await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
290
+ return;
291
+ }
292
+ catch (error) {
293
+ if (attempt === maxAttempts) {
294
+ throw error;
295
+ }
296
+ // A panel that rendered before the client existed — or that failed
297
+ // into the "Oops ... something went wrong." state — only picks the
298
+ // new client up on a refetch, which is what reloading the API tab
299
+ // forces. A single reload isn't always enough on a nightly cluster
300
+ // this suite reuses across parallel test workers, so retry more
301
+ // than once before failing the test.
302
+ console.warn(`assertAPIClientScopes: "${name}" is not listed yet (attempt ${attempt}/${maxAttempts}); reloading the API tab and retrying. ${error}`);
303
+ await this.dismissOopsIfPresent();
304
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
305
+ await this.clickAPITab();
306
+ }
258
307
  }
259
- catch (error) {
260
- // A panel that rendered before the client existed — or that failed into
261
- // the "Oops ... something went wrong." state — only picks the new client
262
- // up on a refetch, which is what reloading the API tab forces.
263
- console.warn(`assertAPIClientScopes: "${name}" is not listed yet; reloading the API tab and retrying. ${error}`);
264
- await this.dismissOopsIfPresent();
265
- await this.page.reload({ waitUntil: 'domcontentloaded' });
266
- await this.clickAPITab();
267
- await this.filterClientsList(name);
268
- await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
269
- }
270
- }
271
- // Narrows the clients table to one client through the table's own filter.
272
- // The table paginates at ten rows and is not ordered by creation time, so on
273
- // a cluster that already has clients a newly created one is usually on a
274
- // page other than the one being displayed — matching a row without filtering
275
- // first reports it as missing.
276
- async filterClientsList(name) {
277
- const filterInput = this.page.getByRole('searchbox', {
278
- name: 'Filter table',
279
- });
280
- await (0, test_1.expect)(filterInput).toBeVisible({ timeout: 60000 });
281
- await filterInput.fill(name);
282
308
  }
283
309
  async clickSettingsTab() {
284
310
  // Console paints the cluster-details tab list before it finishes
@@ -772,8 +798,26 @@ class ClusterDetailsPage {
772
798
  await this.mcpTab.click();
773
799
  }
774
800
  async getMcpConfigurationText() {
775
- await (0, test_1.expect)(this.mcpTextarea).toBeVisible({ timeout: 60000 });
776
- return await this.mcpTextarea.innerText();
801
+ // CI (PR #3629, "Create Agentic Orchestration Cluster") showed the MCP
802
+ // panel's config block still not rendered 60s after clickMCPTab()
803
+ // reported a successful click -- the same "click landed but the panel
804
+ // never switched" race already seen on the Connector Secrets tab.
805
+ // Re-click the tab and retry rather than failing on the first miss.
806
+ const maxAttempts = 3;
807
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
808
+ try {
809
+ await (0, test_1.expect)(this.mcpConfigBlock).toBeVisible({ timeout: 60000 });
810
+ return await this.mcpConfigBlock.innerText();
811
+ }
812
+ catch (error) {
813
+ if (attempt === maxAttempts) {
814
+ throw error;
815
+ }
816
+ console.warn(`getMcpConfigurationText: MCP panel did not render (attempt ${attempt}/${maxAttempts}); re-clicking the MCP tab and retrying. ${error}`);
817
+ await this.clickMCPTab();
818
+ }
819
+ }
820
+ throw new Error('getMcpConfigurationText: unreachable');
777
821
  }
778
822
  async createMCPClientAndReturnVariables(name, setEnvVariables = true) {
779
823
  await this.createAPIClient(name);
@@ -143,10 +143,38 @@ class ClusterPage {
143
143
  this.clusterVersion = (clusterName, versionText) => this.clusterRow(clusterName).getByText(versionText, { exact: false });
144
144
  }
145
145
  async clickClusterBanner() {
146
- await this.page
146
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
147
+ // persistent "Main navigation" sidebar, and once the org has environments
148
+ // enabled that link is further renamed "Environments" (same rollout
149
+ // HomePage.clusterTab handles).
150
+ const clusterLink = this.page
147
151
  .getByRole('banner')
148
152
  .getByRole('link', { name: 'Clusters' })
149
- .click({ timeout: 60000 });
153
+ .or(this.page
154
+ .getByRole('navigation', { name: 'Main navigation' })
155
+ .getByRole('link', { name: 'Clusters', exact: true }))
156
+ .or(this.page
157
+ .getByRole('navigation', { name: 'Main navigation' })
158
+ .getByRole('link', { name: 'Environments', exact: true }));
159
+ await clusterLink.click({ timeout: 60000 });
160
+ // Landing on Hub's Environments overview (see above) renders clusters as
161
+ // cards, not the getByRole('row') table this page's own locators expect
162
+ // -- route through the page's own "Clusters" tab to reach the classic
163
+ // route. Same fix as HomePage.clickClusters().
164
+ const clustersPageTab = this.page
165
+ .getByRole('navigation', { name: 'Environments views' })
166
+ .getByRole('link', { name: 'Clusters', exact: true });
167
+ // On Hub this tab is required -- the row-based locators callers use right
168
+ // after this method return nothing on the card-based Environments
169
+ // overview, so a slow render must not be treated as "tab doesn't exist"
170
+ // the way the legacy (non-Hub) shell's 5s probe is allowed to.
171
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
172
+ await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
173
+ await clustersPageTab.click({ timeout: 15000 });
174
+ }
175
+ else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
176
+ await clustersPageTab.click({ timeout: 15000 });
177
+ }
150
178
  }
151
179
  async confirmClusterDeletion() {
152
180
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 60000 });
@@ -56,15 +56,46 @@ class ClusterSecretsPage {
56
56
  name: 'Cancel',
57
57
  exact: true,
58
58
  });
59
- this.bulkImportErrorBanner = this.dialog.getByText(/Imported \d+ secrets?, \d+ failed/);
60
- this.secretsSearchBox = page.getByRole('searchbox', { name: 'Filter table' });
59
+ // Hub's Import secrets dialog reports partial failures as "Imported X of
60
+ // Y secrets. Not imported: <key> — a secret with this key already
61
+ // exists; ..." -- confirmed from the PR #3629 CI trace screenshot for
62
+ // "Create AWS Cluster". This page object is also still exercised on the
63
+ // legacy Console under IS_PROD (that shell reports "Imported X secrets, Y
64
+ // failed" instead), so match either rather than assuming every env has
65
+ // rolled Hub out.
66
+ this.bulkImportErrorBanner = this.dialog
67
+ .getByText(/Imported \d+ of \d+ secrets?\./)
68
+ .or(this.dialog.getByText(/Imported \d+ secrets?, \d+ failed/));
69
+ // "Filter table" was Carbon's default TableToolbarSearch label. Camunda
70
+ // Hub's rebranded panel renders a plain "Search" input instead, so the
71
+ // old exact-name match never resolves and assertSecretsOrCreate falls
72
+ // through to (re-)creating secrets that may already exist. Match either
73
+ // (see the 8.9 sibling fix, confirmed via a live trace).
74
+ this.secretsSearchBox = page
75
+ .getByRole('searchbox', { name: 'Filter table' })
76
+ .or(page.getByRole('searchbox', { name: 'Search' }))
77
+ .or(page.getByPlaceholder('Search'));
78
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
79
+ // persistent "Main navigation" sidebar, and once the org has environments
80
+ // enabled that link is further renamed "Environments" (same rollout
81
+ // HomePage.clusterTab handles).
61
82
  this.clusterBanner = this.page
62
83
  .getByRole('banner')
63
- .getByRole('link', { name: 'Clusters' });
84
+ .getByRole('link', { name: 'Clusters' })
85
+ .or(this.page
86
+ .getByRole('navigation', { name: 'Main navigation' })
87
+ .getByRole('link', { name: 'Clusters', exact: true }))
88
+ .or(this.page
89
+ .getByRole('navigation', { name: 'Main navigation' })
90
+ .getByRole('link', { name: 'Environments', exact: true }));
64
91
  this.clusterLink = (name) => page.getByRole('link', { name: name });
65
- this.clusterSecretTab = this.page.getByRole('tab', {
66
- name: 'Connector Secrets',
67
- });
92
+ // Some environments render this tab as "Cluster secrets" rather than
93
+ // "Connector Secrets" -- match either (see ClusterPage's identical
94
+ // connectorSecretsTab fallback; this page object has its own separate
95
+ // locator and was missing it).
96
+ this.clusterSecretTab = this.page
97
+ .getByRole('tab', { name: 'Cluster secrets' })
98
+ .or(this.page.getByRole('tab', { name: 'Connector Secrets' }));
68
99
  this.confirmDeleteInput = page.getByRole('textbox', {
69
100
  name: 'Type the word DELETE to confirm',
70
101
  });
@@ -106,17 +137,55 @@ class ClusterSecretsPage {
106
137
  async clickCreateNewSecretButton(cluster) {
107
138
  try {
108
139
  await this.createNewSecretButton.click({ timeout: 60000 });
140
+ return;
109
141
  }
110
142
  catch (error) {
143
+ // fall through to the recovery path below
144
+ }
145
+ // CI (PR #3629, "Create Default Cluster" / "Create AWS Cluster") showed
146
+ // the "Connector Secrets" tab still not rendered 30s after navigating
147
+ // back to the cluster -- a single recovery attempt isn't always enough
148
+ // once the page is this slow to settle, so retry the whole
149
+ // banner->cluster->tab sequence rather than failing on the first miss.
150
+ const maxAttempts = 3;
151
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
111
152
  await this.clickClusterBanner();
112
153
  await this.clickCluster(cluster);
113
- await this.clusterSecretTab.click({ timeout: 30000 });
114
- await this.createNewSecretButton.click({ timeout: 30000 });
154
+ try {
155
+ await this.clusterSecretTab.click({ timeout: 30000 });
156
+ await this.createNewSecretButton.click({ timeout: 30000 });
157
+ return;
158
+ }
159
+ catch (error) {
160
+ if (attempt === maxAttempts) {
161
+ throw error;
162
+ }
163
+ console.warn(`clickCreateNewSecretButton: recovery attempt ${attempt}/${maxAttempts} failed; retrying. ${error}`);
164
+ }
115
165
  }
116
166
  }
117
167
  async clickClusterBanner() {
118
168
  await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
119
169
  await this.clusterBanner.click({ timeout: 60000 });
170
+ // Landing on Hub's Environments overview (see clusterBanner above) renders
171
+ // clusters as cards, not the getByRole('link') the clusterLink() locator
172
+ // below expects to find directly -- route through the page's own
173
+ // "Clusters" tab to reach the classic route. Same fix as
174
+ // HomePage.clickClusters().
175
+ const clustersPageTab = this.page
176
+ .getByRole('navigation', { name: 'Environments views' })
177
+ .getByRole('link', { name: 'Clusters', exact: true });
178
+ // On Hub this tab is required -- clickCluster()'s row-based clusterLink
179
+ // resolves nothing on the card-based Environments overview, so a slow
180
+ // render must not be treated as "tab doesn't exist" the way the legacy
181
+ // (non-Hub) shell's 5s probe is allowed to.
182
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
183
+ await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
184
+ await clustersPageTab.click({ timeout: 15000 });
185
+ }
186
+ else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
187
+ await clustersPageTab.click({ timeout: 15000 });
188
+ }
120
189
  }
121
190
  async clickCluster(name) {
122
191
  await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
@@ -171,9 +240,72 @@ class ClusterSecretsPage {
171
240
  await (0, test_1.expect)(this.page.getByText('Creating...')).not.toBeVisible({
172
241
  timeout: 60000,
173
242
  });
174
- await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
175
- timeout: 60000,
243
+ const secretRow = this.page.getByRole('row', {
244
+ name: new RegExp(`^${key}\\b`, 'i'),
176
245
  });
246
+ // Two separate CI runs (35854706213 with 7 secrets, and 36011511221 with
247
+ // 13) each showed 100% of secrets taking the reload path on attempt 1 --
248
+ // not an occasional race, and not even "near-universal": zero exceptions
249
+ // across 20 samples. A wait on the unreloaded panel was therefore never
250
+ // going to pay off, so reload unconditionally before the first check
251
+ // instead of probing a state that has never once been the real one.
252
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
253
+ await this.clickClusterBanner();
254
+ await this.clickCluster(cluster);
255
+ await this.clusterSecretTab.click({ timeout: 30000 });
256
+ const maxAttempts = 3;
257
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
258
+ try {
259
+ await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
260
+ timeout: 60000,
261
+ });
262
+ // The button reappearing only proves the panel re-rendered, not that
263
+ // this specific secret was created -- a failed create request can
264
+ // leave the panel in the same state. Verify the row before
265
+ // returning: CI showed the row can lag briefly behind the button on
266
+ // a slow reload.
267
+ //
268
+ // Filter down to this key first: a live CI trace (run 36001802609,
269
+ // "Create Default Cluster") showed the unfiltered check time out on
270
+ // both attempts even though the key was confirmed present -- the
271
+ // create POST itself returned 409 (already exists) and the API's own
272
+ // list included it -- because the cluster's shared, reused secret set
273
+ // had grown past what the alphabetically-sorted table renders without
274
+ // narrowing it, the same "off-screen in a long list" pattern
275
+ // documented for Hub's other list views. assertSecretsOrCreate
276
+ // already searches before deciding a secret is missing; do the same
277
+ // here before concluding the create failed.
278
+ await (0, test_1.expect)(this.secretsSearchBox).toBeVisible({ timeout: 10000 });
279
+ await this.secretsSearchBox.fill(key);
280
+ await (0, test_1.expect)(secretRow).toBeVisible({ timeout: 60000 });
281
+ return;
282
+ }
283
+ catch (error) {
284
+ if (attempt === maxAttempts) {
285
+ throw error;
286
+ }
287
+ // The secrets panel can land in a Carbon "Oops ... something went
288
+ // wrong." state after a background request fails post-create, which
289
+ // never recovers on its own and only clears on a reload. Reload
290
+ // unconditionally rather than gating on an "Oops" heading: PR#3627's
291
+ // CI hit this same timeout with no Oops heading ever rendered, and
292
+ // the panel still only recovered on a reload -- the same
293
+ // unconditional reload bulkImportSecrets() already uses on this
294
+ // identical panel. A single retry wasn't always enough either on
295
+ // this suite's shared, reused cluster, so retry more than once.
296
+ console.warn(`createNewSecret: "${key}" was created but the secrets list did not re-render (attempt ${attempt}/${maxAttempts}); reloading and retrying. ${error}`);
297
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
298
+ // The reload lands back on the cluster's default Overview tab, not
299
+ // Connector Secrets -- a CI failure screenshot confirmed the page
300
+ // stuck there with the secrets panel never reached, so
301
+ // createNewSecretButton was never going to reappear on the next
302
+ // attempt regardless of whether the reload fixed the underlying
303
+ // render issue. Re-navigate to the tab before retrying.
304
+ await this.clickClusterBanner();
305
+ await this.clickCluster(cluster);
306
+ await this.clusterSecretTab.click({ timeout: 30000 });
307
+ }
308
+ }
177
309
  }
178
310
  async createSetOfSecrets(cluster, secrets) {
179
311
  await this.bulkImportSecrets(secrets);
@@ -12,6 +12,8 @@ declare class HomePage {
12
12
  readonly appSwitcher: Locator;
13
13
  readonly openOrganizationButton: Locator;
14
14
  readonly consoleBanner: Locator;
15
+ readonly hubHeader: Locator;
16
+ readonly hubSidebar: Locator;
15
17
  readonly modelerBanner: Locator;
16
18
  readonly gettingStartedHeading: Locator;
17
19
  readonly buttonSkipCustomization: Locator;
@@ -15,16 +15,35 @@ class HomePage {
15
15
  appSwitcher;
16
16
  openOrganizationButton;
17
17
  consoleBanner;
18
+ hubHeader;
19
+ hubSidebar;
18
20
  modelerBanner;
19
21
  gettingStartedHeading;
20
22
  buttonSkipCustomization;
21
23
  constructor(page) {
22
24
  this.page = page;
25
+ // Camunda Hub replaces Console on SaaS: an app header (only carrying the
26
+ // user menu once authenticated) plus a sidebar whose Console section
27
+ // links to /org/<id>/clusters and /org/<id>/management.
28
+ this.hubHeader = page
29
+ .locator('header[data-slot="app-header"]')
30
+ .filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
31
+ this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
23
32
  this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
33
+ // Once an org has environmentsEnabled, camunda-hub's
34
+ // use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
35
+ // labelled "Environments" whose href is '/environments', not
36
+ // '/clusters' -- accept both hrefs so this resolves either way.
24
37
  this.clusterTab = page
25
38
  .getByRole('banner')
26
- .getByRole('link', { name: 'Clusters' });
27
- this.organizationTab = page.getByRole('link', { name: 'Organization' });
39
+ .getByRole('link', { name: 'Clusters' })
40
+ .or(this.hubSidebar.locator('a[href$="/clusters"]'))
41
+ .or(this.hubSidebar.locator('a[href$="/environments"]'))
42
+ .first();
43
+ this.organizationTab = page
44
+ .getByRole('link', { name: 'Organization' })
45
+ .or(this.hubSidebar.locator('a[href$="/management"]'))
46
+ .first();
28
47
  this.organizationViewTitle = page.getByText('Organization Management');
29
48
  this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
30
49
  this.informationDialog = page.getByRole('button', {
@@ -58,6 +77,25 @@ class HomePage {
58
77
  await this.closeInformationDialog();
59
78
  }
60
79
  await this.clusterTab.click({ timeout: 15000 });
80
+ // Landing on Hub's Environments overview (see clusterTab above) doesn't
81
+ // put us on the classic row-based cluster list ClusterPage.ts expects --
82
+ // that page renders clusters as cards. Its own header renders a separate
83
+ // "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
84
+ // that does.
85
+ const clustersPageTab = this.page
86
+ .getByRole('navigation', { name: 'Environments views' })
87
+ .getByRole('link', { name: 'Clusters', exact: true });
88
+ // On Hub this tab is required -- the row-based locators callers use right
89
+ // after this method return nothing on the card-based Environments
90
+ // overview, so a slow render must not be treated as "tab doesn't exist"
91
+ // the way the legacy (non-Hub) shell's 5s probe is allowed to.
92
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
93
+ await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
94
+ await clustersPageTab.click({ timeout: 15000 });
95
+ }
96
+ else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
97
+ await clustersPageTab.click({ timeout: 15000 });
98
+ }
61
99
  }
62
100
  async clickDashboard() {
63
101
  await this.dashboardTab.click({ timeout: 90000 });
@@ -26,6 +26,7 @@ declare class LoginPage {
26
26
  clickContinueButton(): Promise<void>;
27
27
  fillPassword(password: string): Promise<void>;
28
28
  clickLoginButton(): Promise<void>;
29
+ private throwIfCredentialsRejected;
29
30
  login(credentials?: {
30
31
  username?: string;
31
32
  password?: string;
@@ -4,6 +4,7 @@ exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
6
  const authErrorClassification_1 = require("../../utils/authErrorClassification");
7
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
7
8
  const userNameInputValidInputSchema = {
8
9
  _schema: {
9
10
  maxlength: 254,
@@ -75,14 +76,33 @@ class LoginPage {
75
76
  async clickLoginButton() {
76
77
  await this.loginButton.click({ timeout: 60000 });
77
78
  }
79
+ // Auth0 responds to a rejected password by re-rendering the same password
80
+ // step with an error message rather than navigating anywhere, so success
81
+ // and failure aren't told apart by a URL or element one side has and the
82
+ // other doesn't. Without this check, a permanently wrong password falls
83
+ // through into assertTestUsesCorrectOrganization's generic ~120s wait for a
84
+ // Console/Hub shell that is never coming, and loginWithRetry then spends
85
+ // its remaining attempts resubmitting the exact same doomed credentials.
86
+ async throwIfCredentialsRejected(username) {
87
+ const message = await (0, authErrorClassification_1.classifyLoginRejection)(this.page, username);
88
+ if (message) {
89
+ throw new Error(message);
90
+ }
91
+ }
78
92
  async login(credentials = {}) {
79
93
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
94
+ // The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page for
95
+ // the tab's whole lifetime; without clearing it here, a flow that logs a
96
+ // different user into the same page could have getUserToken() return the
97
+ // PREVIOUS user's token.
98
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
80
99
  await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 200000 });
81
100
  await this.fillUsername(username);
82
101
  await this.clickContinueButton();
83
102
  await this.fillPassword(password);
84
103
  await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
85
104
  await this.clickLoginButton();
105
+ await this.throwIfCredentialsRejected(username);
86
106
  await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
87
107
  }
88
108
  // The identity provider's email step submits through client-side JS, so a
@@ -128,6 +148,10 @@ class LoginPage {
128
148
  }
129
149
  async loginWithTestUser(credentials = {}) {
130
150
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
151
+ // See login() above: this re-authenticates the same page and must clear
152
+ // the Hub bearer-token cache too, or getUserToken() can return the
153
+ // previous user's cached token.
154
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
131
155
  // Skip navigation if the auth0 login page is already loaded — re-navigating
132
156
  // to '/' when already on auth0 undoes the redirect and causes a 60-second
133
157
  // waitForURL race. This is the common case when called from loginWithRetry,
@@ -158,6 +182,7 @@ class LoginPage {
158
182
  await this.fillPassword(password);
159
183
  await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
160
184
  await this.clickLoginButton();
185
+ await this.throwIfCredentialsRejected(username);
161
186
  await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
162
187
  }
163
188
  async loginFailAssertion() {
@@ -170,6 +195,10 @@ class LoginPage {
170
195
  }
171
196
  async loginWithoutOrgAssertion(credentials = {}) {
172
197
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
198
+ // See login() above: this re-authenticates the same page and must clear
199
+ // the Hub bearer-token cache too, or getUserToken() can return the
200
+ // previous user's cached token.
201
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
173
202
  await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 180000 });
174
203
  await this.fillUsername(username);
175
204
  await this.clickContinueButton();
@@ -1,6 +1,7 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private defaultFolderName;
4
+ private defaultClusterName;
4
5
  private defaultNewProjectName;
5
6
  private page;
6
7
  private processApplicationPage;
@@ -8,6 +9,11 @@ declare class ModelerHomePage {
8
9
  private processApplicationName?;
9
10
  readonly modelerPageBanner: Locator;
10
11
  readonly createNewProjectButton: Locator;
12
+ readonly createWorkspaceButton: Locator;
13
+ readonly workspaceWizardNameInput: Locator;
14
+ readonly workspaceWizardSubmitButton: Locator;
15
+ readonly workspaceWizardMembersStep: Locator;
16
+ readonly workspaceWizardEnvironmentsHeading: Locator;
11
17
  readonly projectNameInput: Locator;
12
18
  readonly formNameInput: Locator;
13
19
  readonly chooseBpmnTemplateButton: Locator;
@@ -62,11 +68,13 @@ declare class ModelerHomePage {
62
68
  returnToResourceContainer(): Promise<void>;
63
69
  startBlankBpmnDiagram(): Promise<void>;
64
70
  private becameVisible;
71
+ private createWorkspaceViaWizard;
65
72
  clickHomeBreadcrumb(): Promise<void>;
66
73
  clickOpenOrganizationsButton(): Promise<void>;
67
74
  createForm(formName: string): Promise<void>;
68
75
  clickCreateIdpApplicationButton(): Promise<void>;
69
76
  clickCrossComponentProjectFolder(): Promise<void>;
77
+ private waitForCrossComponentProjectFolder;
70
78
  createCrossComponentProjectFolder(): Promise<void>;
71
79
  clickManageButton(retries?: number): Promise<void>;
72
80
  clickUploadFilesButton(): Promise<void>;