@camunda/e2e-test-suite 0.0.1434 → 0.0.1436

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.
@@ -204,10 +204,19 @@ class AppsPage {
204
204
  // current cluster's own toggle name is not exactly clusterName either,
205
205
  // and an exact-equality exclusion would wrongly treat it as "other" and
206
206
  // immediately re-collapse the group this method just expanded.
207
- const otherExpandedToggles = this.hubSidebar.getByRole('button', {
207
+ // The sidebar's own rail toggle is also a button inside "Main navigation"
208
+ // with aria-expanded="true" once the rail is open ("Collapse sidebar").
209
+ // Without excluding it this loop clicked it right after the group was
210
+ // expanded, collapsing the rail and taking the app links with it -- the
211
+ // 8.8 nightly trace shows exactly that sequence (expand "Test Cluster",
212
+ // count 1, click "Collapse sidebar", app link never appears). Same
213
+ // exclusion as pages/8.9/AppsPage.ts.
214
+ const otherExpandedToggles = this.hubSidebar
215
+ .getByRole('button', {
208
216
  expanded: true,
209
217
  name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
210
- });
218
+ })
219
+ .and(this.page.locator(':not([data-slot="app-sidebar-collapse-toggle"])'));
211
220
  for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
212
221
  // Let a failed collapse throw rather than swallowing it: the app link
213
222
  // is intentionally sidebar-wide, so a group left expanded here produces
@@ -71,8 +71,8 @@ declare class ClusterDetailsPage {
71
71
  clickSettingsTab(): Promise<void>;
72
72
  private waitForAuthorizationsSection;
73
73
  private setAuthorizationsViaSwitch;
74
- enableAuthorizations(): Promise<void>;
75
- disableAuthorizations(): Promise<void>;
74
+ enableAuthorizations(): Promise<boolean>;
75
+ disableAuthorizations(): Promise<boolean>;
76
76
  searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
77
77
  deleteAPIClientsIfExist(name?: string): Promise<void>;
78
78
  clickAlertsTab(): Promise<void>;
@@ -326,11 +326,11 @@ class ClusterDetailsPage {
326
326
  // Carbon toggle.
327
327
  async setAuthorizationsViaSwitch(enabled) {
328
328
  if (!(await this.authorizationsSwitch.isVisible().catch(() => false))) {
329
- return false;
329
+ return 'absent';
330
330
  }
331
331
  const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
332
332
  if (isOn === enabled) {
333
- return true;
333
+ return 'unchanged';
334
334
  }
335
335
  await this.authorizationsSwitch.click({ timeout: 30000 });
336
336
  if (await this.dialog.isVisible({ timeout: 15000 }).catch(() => false)) {
@@ -341,13 +341,14 @@ class ClusterDetailsPage {
341
341
  await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
342
342
  }
343
343
  await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
344
- return true;
344
+ return 'changed';
345
345
  }
346
346
  async enableAuthorizations() {
347
347
  await (0, sleep_1.sleep)(5000);
348
348
  await this.waitForAuthorizationsSection();
349
- if (await this.setAuthorizationsViaSwitch(true)) {
350
- return;
349
+ const viaSwitch = await this.setAuthorizationsViaSwitch(true);
350
+ if (viaSwitch !== 'absent') {
351
+ return viaSwitch === 'changed';
351
352
  }
352
353
  // Locate all elements with class .cds--toggle__text
353
354
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
@@ -372,13 +373,16 @@ class ClusterDetailsPage {
372
373
  }
373
374
  await (0, sleep_1.sleep)(10000);
374
375
  console.log('Second toggle switch clicked.');
376
+ return true;
375
377
  }
376
378
  else {
377
379
  console.log('Second toggle switch not found or less than 2.');
380
+ return false;
378
381
  }
379
382
  }
380
383
  else {
381
384
  console.log("Second toggle text does not contain 'Disabled'.");
385
+ return false;
382
386
  }
383
387
  }
384
388
  else {
@@ -395,8 +399,9 @@ class ClusterDetailsPage {
395
399
  }
396
400
  async disableAuthorizations() {
397
401
  await this.waitForAuthorizationsSection();
398
- if (await this.setAuthorizationsViaSwitch(false)) {
399
- return;
402
+ const viaSwitch = await this.setAuthorizationsViaSwitch(false);
403
+ if (viaSwitch !== 'absent') {
404
+ return viaSwitch === 'changed';
400
405
  }
401
406
  // Locate all elements with class .cds--toggle__text
402
407
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
@@ -421,13 +426,16 @@ class ClusterDetailsPage {
421
426
  }
422
427
  console.log('Second toggle switch clicked.');
423
428
  await (0, sleep_1.sleep)(10000);
429
+ return true;
424
430
  }
425
431
  else {
426
432
  console.log('Second toggle switch not found or less than 2.');
433
+ return false;
427
434
  }
428
435
  }
429
436
  else {
430
437
  console.log("Second toggle text does not contain 'Enabled'.");
438
+ return false;
431
439
  }
432
440
  }
433
441
  else {
@@ -58,6 +58,7 @@ declare class ClusterPage {
58
58
  clickLatestStableForMinorVersion(): Promise<void>;
59
59
  clickClusterOption(version: string): Promise<void>;
60
60
  createClusterIfNotExists(clusterName: string, region?: string): Promise<void>;
61
+ waitForClusterRestartToStart(name: string, timeout?: number): Promise<void>;
61
62
  assertClusterHealthyStatusWithRetry(name: string, visibilityTimeout?: number, totalTimeout?: number, isUpgradeScenario?: boolean): Promise<void>;
62
63
  clickAlphaTab(): Promise<void>;
63
64
  clickDevTab(): Promise<void>;
@@ -362,20 +362,18 @@ class ClusterPage {
362
362
  await this.createClusterButton.click();
363
363
  }
364
364
  async clickClustersBreadcrumb(clusterName) {
365
- const clusterRow = this.cluster(clusterName);
366
- const clusterRowVisible = clusterRow
367
- .getByText(CREATING_STATUS)
368
- .or(clusterRow.getByText('Healthy', { exact: true }));
365
+ // Wait for the new cluster's row, not for a status: Hub can list a
366
+ // just-created cluster as "unknown" for longer than this wait before it
367
+ // switches to "Creating…". Every caller asserts Healthy afterwards.
368
+ const clusterRow = this.clusterLink(clusterName);
369
369
  try {
370
370
  await (0, test_1.expect)(this.clustersBreadcrumb).toBeVisible({ timeout: 60000 });
371
371
  await this.clustersBreadcrumb.click({ timeout: 60000 });
372
- await (0, test_1.expect)(clusterRow.getByText(CREATING_STATUS)).toBeVisible({
373
- timeout: 30000,
374
- });
372
+ await (0, test_1.expect)(clusterRow).toBeVisible({ timeout: 30000 });
375
373
  }
376
374
  catch (error) {
377
375
  await this.clickClusterBanner();
378
- await (0, test_1.expect)(clusterRowVisible).toBeVisible({ timeout: 30000 });
376
+ await (0, test_1.expect)(clusterRow).toBeVisible({ timeout: 30000 });
379
377
  }
380
378
  }
381
379
  async clickClusterLink(name) {
@@ -465,14 +463,51 @@ class ClusterPage {
465
463
  }
466
464
  async createClusterIfNotExists(clusterName, region = 'GCP') {
467
465
  await (0, sleep_1.sleep)(15000);
468
- if ((await this.cluster(clusterName).isVisible({ timeout: 20000 })) &&
469
- (await this.clusterHealthiness(clusterName).isVisible({ timeout: 20000 }))) {
466
+ // isVisible() does not wait, so a table that was still loading read as
467
+ // "no cluster" and a retry recreated the cluster its first attempt had
468
+ // already created. Give the row time to render before trusting a miss.
469
+ const exists = await this.cluster(clusterName)
470
+ .waitFor({ state: 'visible', timeout: 30000 })
471
+ .then(() => true)
472
+ .catch(() => false);
473
+ if (!exists) {
474
+ await this.createCluster(clusterName, region);
475
+ }
476
+ else if (await this.clusterHealthiness(clusterName).isVisible()) {
470
477
  console.log(`${clusterName} exists and is healthy.`);
478
+ return;
471
479
  }
472
480
  else {
473
- await this.createCluster(clusterName, region);
474
- await this.assertClusterHealthyStatusWithRetry(clusterName, 120000, 600000);
481
+ // Still provisioning, typically from the attempt this retry replaces.
482
+ // Recreating it adds a second cluster of the same name, because the
483
+ // pre-clean in createCluster does not delete it, and every exact-name
484
+ // locator then fails on strict mode.
485
+ console.log(`${clusterName} exists but is not Healthy yet; waiting for it to come up.`);
475
486
  }
487
+ await this.assertClusterHealthyStatusWithRetry(clusterName, 120000, 600000);
488
+ }
489
+ // A settings change that restarts the cluster, such as the authorizations
490
+ // toggle, leaves the status on "Healthy" until the operator reconciles the
491
+ // new spec. A Healthy check made straight after the change can therefore
492
+ // pass before the restart has begun, and the test then runs against a
493
+ // cluster that is about to restart. Wait for the status to leave Healthy
494
+ // first. The list updates over SSE, so no reload is needed while waiting.
495
+ async waitForClusterRestartToStart(name, timeout = 120000) {
496
+ const rowShown = await this.cluster(name)
497
+ .waitFor({ state: 'visible', timeout: 60000 })
498
+ .then(() => true)
499
+ .catch(() => false);
500
+ if (!rowShown) {
501
+ console.warn(`${name}: cluster row not shown, so the restart cannot be watched.`);
502
+ return;
503
+ }
504
+ const leftHealthy = await this.clusterHealthiness(name)
505
+ .waitFor({ state: 'hidden', timeout })
506
+ .then(() => true)
507
+ .catch(() => false);
508
+ console.log(leftHealthy
509
+ ? `${name}: restart started, status left Healthy.`
510
+ : `${name}: status stayed Healthy for ${timeout / 1000}s after the change.`);
476
511
  }
477
512
  async assertClusterHealthyStatusWithRetry(name, visibilityTimeout = 100000, totalTimeout = 420000, isUpgradeScenario = false) {
478
513
  // For upgrade scenarios, use much longer timeouts and more retries instead of fixed waits.
@@ -387,8 +387,11 @@ async function disableRBA(clusterName, homePage, clusterPage, clusterDetailsPage
387
387
  timeout: 90000,
388
388
  });
389
389
  await clusterDetailsPage.clickSettingsTab();
390
- await clusterDetailsPage.disableAuthorizations();
390
+ const changed = await clusterDetailsPage.disableAuthorizations();
391
391
  await homePage.clickClusters();
392
+ if (changed) {
393
+ await clusterPage.waitForClusterRestartToStart(clusterName);
394
+ }
392
395
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
393
396
  await clusterPage.clickClusterLink(clusterName);
394
397
  await (0, test_1.expect)(clusterDetailsPage.rbaEnabledMessage.first()).not.toBeVisible({
@@ -407,8 +410,11 @@ async function enableAuthorizations(clusterName, homePage, clusterPage, clusterD
407
410
  timeout: 90000,
408
411
  });
409
412
  await clusterDetailsPage.clickSettingsTab();
410
- await clusterDetailsPage.enableAuthorizations();
413
+ const changed = await clusterDetailsPage.enableAuthorizations();
411
414
  await homePage.clickClusters();
415
+ if (changed) {
416
+ await clusterPage.waitForClusterRestartToStart(clusterName);
417
+ }
412
418
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
413
419
  await clusterPage.clickClusterLink(clusterName);
414
420
  await clusterDetailsPage.assertComponentsHealth();
@@ -78,8 +78,8 @@ declare class ClusterDetailsPage {
78
78
  clickSettingsTab(): Promise<void>;
79
79
  private waitForAuthorizationsSection;
80
80
  private setAuthorizationsViaSwitch;
81
- enableAuthorizations(): Promise<void>;
82
- disableAuthorizations(): Promise<void>;
81
+ enableAuthorizations(): Promise<boolean>;
82
+ disableAuthorizations(): Promise<boolean>;
83
83
  searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
84
84
  deleteAPIClientsIfExist(name?: string): Promise<void>;
85
85
  clickAlertsTab(): Promise<void>;
@@ -401,20 +401,21 @@ class ClusterDetailsPage {
401
401
  },
402
402
  });
403
403
  }
404
- // Drives the design-system Authorizations switch. Returns false when this
404
+ // Drives the design-system Authorizations switch. Returns 'absent' when this
405
405
  // shell does not render one, so the caller falls through to the legacy
406
- // Carbon toggle.
406
+ // Carbon toggle, and 'changed' only when it flipped the setting, which
407
+ // restarts the cluster.
407
408
  async setAuthorizationsViaSwitch(enabled) {
408
409
  const hasSwitch = await this.authorizationsSwitch
409
410
  .waitFor({ state: 'visible', timeout: 10000 })
410
411
  .then(() => true)
411
412
  .catch(() => false);
412
413
  if (!hasSwitch) {
413
- return false;
414
+ return 'absent';
414
415
  }
415
416
  const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
416
417
  if (isOn === enabled) {
417
- return true;
418
+ return 'unchanged';
418
419
  }
419
420
  await this.authorizationsSwitch.click({ timeout: 30000 });
420
421
  const confirmOpened = await this.dialog
@@ -427,13 +428,14 @@ class ClusterDetailsPage {
427
428
  await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
428
429
  }
429
430
  await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
430
- return true;
431
+ return 'changed';
431
432
  }
432
433
  async enableAuthorizations() {
433
434
  await (0, sleep_1.sleep)(5000);
434
435
  await this.waitForAuthorizationsSection();
435
- if (await this.setAuthorizationsViaSwitch(true)) {
436
- return;
436
+ const viaSwitch = await this.setAuthorizationsViaSwitch(true);
437
+ if (viaSwitch !== 'absent') {
438
+ return viaSwitch === 'changed';
437
439
  }
438
440
  // Locate all elements with class .cds--toggle__text
439
441
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
@@ -458,13 +460,16 @@ class ClusterDetailsPage {
458
460
  }
459
461
  await (0, sleep_1.sleep)(10000);
460
462
  console.log('Second toggle switch clicked.');
463
+ return true;
461
464
  }
462
465
  else {
463
466
  console.log('Second toggle switch not found or less than 2.');
467
+ return false;
464
468
  }
465
469
  }
466
470
  else {
467
471
  console.log("Second toggle text does not contain 'Disabled'.");
472
+ return false;
468
473
  }
469
474
  }
470
475
  else {
@@ -477,8 +482,9 @@ class ClusterDetailsPage {
477
482
  }
478
483
  async disableAuthorizations() {
479
484
  await this.waitForAuthorizationsSection();
480
- if (await this.setAuthorizationsViaSwitch(false)) {
481
- return;
485
+ const viaSwitch = await this.setAuthorizationsViaSwitch(false);
486
+ if (viaSwitch !== 'absent') {
487
+ return viaSwitch === 'changed';
482
488
  }
483
489
  // Locate all elements with class .cds--toggle__text
484
490
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
@@ -504,13 +510,16 @@ class ClusterDetailsPage {
504
510
  }
505
511
  console.log('Second toggle switch clicked.');
506
512
  await (0, sleep_1.sleep)(10000);
513
+ return true;
507
514
  }
508
515
  else {
509
516
  console.log('Second toggle switch not found or less than 2.');
517
+ return false;
510
518
  }
511
519
  }
512
520
  else {
513
521
  console.log("Second toggle text does not contain 'Enabled'.");
522
+ return false;
514
523
  }
515
524
  }
516
525
  else {
@@ -56,6 +56,7 @@ declare class ClusterPage {
56
56
  clickLatestStableForMinorVersion(): Promise<void>;
57
57
  clickClusterOption(): Promise<void>;
58
58
  createClusterIfNotExists(clusterName: string, region?: string): Promise<void>;
59
+ waitForClusterRestartToStart(name: string, timeout?: number): Promise<void>;
59
60
  assertClusterHealthyStatusWithRetry(name: string, visibilityTimeout?: number, totalTimeout?: number, maxRetries?: number): Promise<void>;
60
61
  clickAlphaTab(): Promise<void>;
61
62
  clickDevTab(): Promise<void>;
@@ -314,17 +314,18 @@ class ClusterPage {
314
314
  await this.createClusterButton.click();
315
315
  }
316
316
  async clickClustersBreadcrumb(clusterName) {
317
- const clusterRow = this.cluster(clusterName);
318
- const creatingRow = clusterRow.getByText(CLUSTER_CREATING_STATUS);
319
- const clusterRowVisible = creatingRow.or(clusterRow.getByText('Healthy', { exact: true }));
317
+ // Wait for the new cluster's row, not for a status: Hub can list a
318
+ // just-created cluster as "unknown" for longer than this wait before it
319
+ // switches to "Creating…". Every caller asserts Healthy afterwards.
320
+ const clusterRow = this.clusterLink(clusterName);
320
321
  try {
321
322
  await (0, test_1.expect)(this.clustersBreadcrumb).toBeVisible({ timeout: 60000 });
322
323
  await this.clustersBreadcrumb.click({ timeout: 60000 });
323
- await (0, test_1.expect)(creatingRow).toBeVisible({ timeout: 30000 });
324
+ await (0, test_1.expect)(clusterRow).toBeVisible({ timeout: 30000 });
324
325
  }
325
326
  catch (error) {
326
327
  await this.clickClusterBanner();
327
- await (0, test_1.expect)(clusterRowVisible).toBeVisible({ timeout: 30000 });
328
+ await (0, test_1.expect)(clusterRow).toBeVisible({ timeout: 30000 });
328
329
  }
329
330
  }
330
331
  async clickClusterLink(name) {
@@ -432,22 +433,60 @@ class ClusterPage {
432
433
  }
433
434
  async createClusterIfNotExists(clusterName, region = 'GCP') {
434
435
  await (0, sleep_1.sleep)(15000);
435
- if ((await this.cluster(clusterName).isVisible({ timeout: 20000 })) &&
436
- (await this.clusterHealthiness(clusterName).isVisible({ timeout: 20000 }))) {
436
+ // isVisible() does not wait, so a table that was still loading read as
437
+ // "no cluster" and a retry recreated the cluster its first attempt had
438
+ // already created. Give the row time to render before trusting a miss.
439
+ const exists = await this.cluster(clusterName)
440
+ .waitFor({ state: 'visible', timeout: 30000 })
441
+ .then(() => true)
442
+ .catch(() => false);
443
+ if (!exists) {
444
+ await this.createCluster(clusterName, region);
445
+ }
446
+ else if (await this.clusterHealthiness(clusterName).isVisible()) {
437
447
  console.log(`${clusterName} exists and is healthy.`);
448
+ return;
438
449
  }
439
450
  else {
440
- await this.createCluster(clusterName, region);
441
- // The Healthy badge is only re-read after a reload, and
442
- // expectLocatorWithRetry reloads once per attempt -- so `maxRetries` is
443
- // what sets the polling resolution, not `totalTimeout`. With the old
444
- // 120s/600s/5 the loop reloaded 5 times and gave up after ~570s, short
445
- // of its own 600s budget. Polling every 15s instead reloads ~45 times,
446
- // and 900s gives a cluster provisioning alongside the other parallel
447
- // workers' clusters the room it actually needs (still well inside the
448
- // 36-minute test.slow() budget).
449
- await this.assertClusterHealthyStatusWithRetry(clusterName, 15000, 900000, 60);
451
+ // Still provisioning, typically from the attempt this retry replaces.
452
+ // Recreating it is not an option: createCluster's pre-clean finds no
453
+ // per-row Delete button in Hub's table (only "Row actions"), so it
454
+ // leaves this cluster in place and adds a second one of the same name,
455
+ // which the apps menu can then no longer tell apart.
456
+ console.log(`${clusterName} exists but is not Healthy yet; waiting for it to come up.`);
457
+ }
458
+ // The Healthy badge is only re-read after a reload, and
459
+ // expectLocatorWithRetry reloads once per attempt -- so `maxRetries` is
460
+ // what sets the polling resolution, not `totalTimeout`. With the old
461
+ // 120s/600s/5 the loop reloaded 5 times and gave up after ~570s, short
462
+ // of its own 600s budget. Polling every 15s instead reloads ~45 times,
463
+ // and 900s gives a cluster provisioning alongside the other parallel
464
+ // workers' clusters the room it actually needs (still well inside the
465
+ // 36-minute test.slow() budget).
466
+ await this.assertClusterHealthyStatusWithRetry(clusterName, 15000, 900000, 60);
467
+ }
468
+ // A settings change that restarts the cluster, such as the authorizations
469
+ // toggle, leaves the status on "Healthy" until the operator reconciles the
470
+ // new spec. A Healthy check made straight after the change can therefore
471
+ // pass before the restart has begun, and the test then runs against a
472
+ // cluster that is about to restart. Wait for the status to leave Healthy
473
+ // first. The list updates over SSE, so no reload is needed while waiting.
474
+ async waitForClusterRestartToStart(name, timeout = 120000) {
475
+ const rowShown = await this.cluster(name)
476
+ .waitFor({ state: 'visible', timeout: 60000 })
477
+ .then(() => true)
478
+ .catch(() => false);
479
+ if (!rowShown) {
480
+ console.warn(`${name}: cluster row not shown, so the restart cannot be watched.`);
481
+ return;
450
482
  }
483
+ const leftHealthy = await this.clusterHealthiness(name)
484
+ .waitFor({ state: 'hidden', timeout })
485
+ .then(() => true)
486
+ .catch(() => false);
487
+ console.log(leftHealthy
488
+ ? `${name}: restart started, status left Healthy.`
489
+ : `${name}: status stayed Healthy for ${timeout / 1000}s after the change.`);
451
490
  }
452
491
  async assertClusterHealthyStatusWithRetry(name, visibilityTimeout = 100000, totalTimeout = 500000, maxRetries) {
453
492
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.clusterHealthiness(name), {
@@ -422,10 +422,13 @@ async function disableRBA(clusterName, homePage, clusterPage, clusterDetailsPage
422
422
  timeout: 90000,
423
423
  });
424
424
  await clusterDetailsPage.clickSettingsTab();
425
- await clusterDetailsPage.disableAuthorizations();
425
+ const changed = await clusterDetailsPage.disableAuthorizations();
426
426
  // The toggle restarts the cluster; Console can crash out of the settings
427
427
  // route while it does, taking the Clusters nav link with it.
428
428
  await homePage.clickClustersWithReloadRecovery();
429
+ if (changed) {
430
+ await clusterPage.waitForClusterRestartToStart(clusterName);
431
+ }
429
432
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
430
433
  await clusterPage.clickClusterLink(clusterName);
431
434
  await (0, test_1.expect)(clusterDetailsPage.rbaEnabledMessage.first()).not.toBeVisible({
@@ -444,10 +447,13 @@ async function enableAuthorizations(clusterName, homePage, clusterPage, clusterD
444
447
  timeout: 90000,
445
448
  });
446
449
  await clusterDetailsPage.clickSettingsTab();
447
- await clusterDetailsPage.enableAuthorizations();
450
+ const changed = await clusterDetailsPage.enableAuthorizations();
448
451
  // The toggle restarts the cluster; Console can crash out of the settings
449
452
  // route while it does, taking the Clusters nav link with it.
450
453
  await homePage.clickClustersWithReloadRecovery();
454
+ if (changed) {
455
+ await clusterPage.waitForClusterRestartToStart(clusterName);
456
+ }
451
457
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
452
458
  await clusterPage.clickClusterLink(clusterName);
453
459
  await clusterDetailsPage.assertComponentsHealth();
@@ -44,10 +44,8 @@ _8_8_1.test.describe('User Roles User Flow', () => {
44
44
  });
45
45
  // Workspace membership is durable organization state, so the Editor rights
46
46
  // the Analyst flow grants must be handed back whether the test passed or
47
- // failed. Left behind, a retry would start with the analyst already an
48
- // Editor and its own "not reachable without workspace access" assertion
49
- // would fail on the second attempt; the grant would also leak into every
50
- // later nightly run against the same org.
47
+ // failed. Left behind, the grant would leak into every later nightly run
48
+ // against the same org.
51
49
  _8_8_1.test.afterEach(async ({ page, loginPage }, testInfo) => {
52
50
  if (!testInfo.title.startsWith('Analyst Role User Flow')) {
53
51
  return;
@@ -169,10 +167,9 @@ _8_8_1.test.describe('User Roles User Flow', () => {
169
167
  const processName = 'User Roles User Flow' + randomString;
170
168
  const clusterName = clusterNames['Analyst Role User Flow'];
171
169
  // A retry runs in a fresh worker, where analystWorkspaceGrantMade starts
172
- // false even when the previous attempt's teardown failed to hand the grant
173
- // back -- which is exactly what happens when that teardown dies on its
174
- // admin re-login. The "not reachable without workspace access" step below
175
- // would then always fail, because the analyst is still an Editor.
170
+ // false even when the previous attempt's teardown failed to hand the
171
+ // grant back. Remove a leftover grant so the membership does not leak
172
+ // into later runs against the same org.
176
173
  if (_8_8_1.test.info().retry > 0) {
177
174
  await _8_8_1.test.step('Hand back a workspace grant left by the previous attempt', async () => {
178
175
  await appsPage.clickModeler();
@@ -201,52 +198,23 @@ _8_8_1.test.describe('User Roles User Flow', () => {
201
198
  password: process.env.C8_PASSWORD_TEST_9,
202
199
  }, 1000);
203
200
  });
204
- // An organization role no longer decides on its own what a non-admin can
205
- // reach. Since camunda-hub's environments work (pdp-3443, pdp-3715), a
206
- // cluster's apps are surfaced through an ENVIRONMENT, and an environment
207
- // is visible only to users who are Editor or Workspace Admin on a
208
- // workspace it is assigned to -- organization admins and owners excepted,
209
- // who see every environment unconditionally. An Analyst with no workspace
210
- // membership therefore reaches nothing, which is the decided design
211
- // rather than a defect: "Clusters are shown only to org admins.
212
- // Non-admins do not have cluster visibility (they operate with
213
- // environments instead)."
214
- //
215
- // Assert that gate before granting the membership the rest of this test
216
- // depends on, so both halves are covered: the role alone grants no
217
- // access, and the role plus workspace editorship does.
218
- await _8_8_1.test.step('Ensure Optimize is not reachable without workspace access', async () => {
219
- // Assert the cause, not just the consequence. Hub shows the analyst an
220
- // empty Workspaces overview -- "You don't have explicit membership in
221
- // any workspace in this organization" before the scope is widened, and
222
- // "Your organization has no workspaces yet" after -- because the
223
- // workspace belongs to whoever created it. With no workspace there is
224
- // no environment, and with no environment no cluster app, so checking
225
- // the Optimize banner alone would pass for unrelated reasons too.
201
+ // Every SaaS organization member sees every environment in the
202
+ // organization, whether or not they belong to a workspace. Hub decided
203
+ // this in camunda-hub#30270 and shipped it in camunda-hub#30271; the bug
204
+ // that challenged it, camunda-hub#30300, was closed as works-as-intended.
205
+ // Assert it, so a revert to the stricter "assigned only" rule is caught.
206
+ // What the analyst may do inside each app stays gated by the cluster's
207
+ // authorizations, which the steps below assert.
208
+ await _8_8_1.test.step('Ensure the cluster is listed without workspace access', async () => {
226
209
  await appsPage.clickCamundaApps();
227
210
  await (0, test_1.expect)(page
228
211
  .getByText('Your organization has no workspaces yet')
229
212
  .or(page.getByText("You don't have explicit membership in any workspace"))
230
213
  .first()).toBeVisible({ timeout: 60000 });
231
- // Assert the absence of THIS cluster's Optimize target directly rather
232
- // than calling clickOptimize() and swallowing whatever it throws: a
233
- // swallowed error would let this step pass because the hand-off helper
234
- // itself broke and the page simply stayed on Hub, where the Optimize
235
- // banner is absent for reasons that have nothing to do with the gate.
236
- // On Hub a cluster's apps hang off the sidebar group named for the
237
- // cluster, so with no environment neither the group nor the Optimize
238
- // link inside it is ever rendered.
239
214
  const hubSidebar = page.getByRole('navigation', {
240
215
  name: 'Main navigation',
241
216
  });
242
- // The negative counts below mean nothing before the shell has rendered:
243
- // toHaveCount(0) passes vacuously on a page that has not painted yet.
244
- await (0, test_1.expect)(hubSidebar).toBeVisible({ timeout: 30000 });
245
- await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toHaveCount(0, { timeout: 30000 });
246
- await (0, test_1.expect)(hubSidebar.getByRole('link', { name: /Optimize/ })).toHaveCount(0, { timeout: 30000 });
247
- await (0, test_1.expect)(optimizeHomePage.optimizeBanner).not.toBeVisible({
248
- timeout: 30000,
249
- });
217
+ await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toBeVisible({ timeout: 30000 });
250
218
  });
251
219
  await _8_8_1.test.step('Grant the analyst Editor rights on the shared workspace', async () => {
252
220
  // Only an organization admin may change workspace membership, so this
@@ -40,9 +40,10 @@ _8_9_1.test.describe('User Roles User Flow', () => {
40
40
  await (0, _setup_1.captureScreenshot)(page, testInfo);
41
41
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
42
42
  });
43
- // Workspace membership is durable org state: left behind, a retry starts
44
- // with the analyst already an Editor and its "not reachable without
45
- // workspace access" step fails. Hand it back whether the test passed or not.
43
+ // Workspace membership is durable organization state, so the Editor rights
44
+ // the Analyst flow grants must be handed back whether the test passed or
45
+ // failed. Left behind, the grant would leak into every later nightly run
46
+ // against the same org.
46
47
  _8_9_1.test.afterEach(async ({ page, loginPage }, testInfo) => {
47
48
  if (!testInfo.title.startsWith('Analyst Role User Flow')) {
48
49
  return;
@@ -166,7 +167,8 @@ _8_9_1.test.describe('User Roles User Flow', () => {
166
167
  const clusterName = clusterNames['Analyst Role User Flow'];
167
168
  // A retry runs in a fresh worker, where analystWorkspaceGrantMade starts
168
169
  // false even when the previous attempt's teardown failed to hand the
169
- // grant back. The "not reachable" step below would then always fail.
170
+ // grant back. Remove a leftover grant so the membership does not leak
171
+ // into later runs against the same org.
170
172
  if (_8_9_1.test.info().retry > 0) {
171
173
  await _8_9_1.test.step('Hand back a workspace grant left by the previous attempt', async () => {
172
174
  await appsPage.clickModeler();
@@ -196,11 +198,14 @@ _8_9_1.test.describe('User Roles User Flow', () => {
196
198
  password: process.env.C8_PASSWORD_TEST_9,
197
199
  });
198
200
  });
199
- // Hub no longer lets an organization role alone reach a cluster's apps: a
200
- // non-admin sees only environments assigned to a workspace they are Editor
201
- // or Workspace Admin on (camunda-hub EnvironmentVisibilityResolver,
202
- // ProjectOperation.VIEW_ENVIRONMENTS). Assert that gate, then grant it.
203
- await _8_9_1.test.step('Ensure Optimize is not reachable without workspace access', async () => {
201
+ // Every SaaS organization member sees every environment in the
202
+ // organization, whether or not they belong to a workspace. Hub decided
203
+ // this in camunda-hub#30270 and shipped it in camunda-hub#30271; the bug
204
+ // that challenged it, camunda-hub#30300, was closed as works-as-intended.
205
+ // Assert it, so a revert to the stricter "assigned only" rule is caught.
206
+ // What the analyst may do inside each app stays gated by the cluster's
207
+ // authorizations, which the steps below assert.
208
+ await _8_9_1.test.step('Ensure the cluster is listed without workspace access', async () => {
204
209
  await appsPage.clickCamundaApps();
205
210
  await (0, test_1.expect)(page
206
211
  .getByText('Your organization has no workspaces yet')
@@ -209,13 +214,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
209
214
  const hubSidebar = page.getByRole('navigation', {
210
215
  name: 'Main navigation',
211
216
  });
212
- // The negative counts below mean nothing before the shell has rendered.
213
- await (0, test_1.expect)(hubSidebar).toBeVisible({ timeout: 30000 });
214
- await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toHaveCount(0, { timeout: 30000 });
215
- await (0, test_1.expect)(hubSidebar.getByRole('link', { name: /Optimize/ })).toHaveCount(0, { timeout: 30000 });
216
- await (0, test_1.expect)(optimizeHomePage.optimizeBanner).not.toBeVisible({
217
- timeout: 30000,
218
- });
217
+ await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toBeVisible({ timeout: 30000 });
219
218
  });
220
219
  await _8_9_1.test.step('Grant the analyst Editor rights on the shared workspace', async () => {
221
220
  // Membership and environment assignment are org-admin actions, so this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1434",
3
+ "version": "0.0.1436",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",