@camunda/e2e-test-suite 0.0.1248 → 0.0.1250

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 (35) hide show
  1. package/dist/pages/8.7/AppsPage.d.ts +7 -3
  2. package/dist/pages/8.7/AppsPage.js +187 -3
  3. package/dist/pages/8.7/ClusterPage.js +22 -2
  4. package/dist/pages/8.7/ClusterSecretsPage.js +22 -1
  5. package/dist/pages/8.7/HomePage.d.ts +2 -0
  6. package/dist/pages/8.7/HomePage.js +70 -6
  7. package/dist/pages/8.7/LoginPage.js +15 -0
  8. package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -0
  9. package/dist/pages/8.7/ModelerCreatePage.js +59 -5
  10. package/dist/pages/8.7/ModelerHomePage.d.ts +11 -0
  11. package/dist/pages/8.7/ModelerHomePage.js +184 -10
  12. package/dist/pages/8.7/ProcessApplicationPage.js +27 -7
  13. package/dist/pages/8.7/UtilitiesPage.js +54 -1
  14. package/dist/pages/8.9/AppsPage.d.ts +2 -0
  15. package/dist/pages/8.9/AppsPage.js +114 -0
  16. package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
  17. package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
  18. package/dist/pages/8.9/ClusterPage.js +49 -8
  19. package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
  20. package/dist/pages/8.9/HomePage.d.ts +3 -0
  21. package/dist/pages/8.9/HomePage.js +44 -5
  22. package/dist/pages/8.9/LoginPage.d.ts +1 -0
  23. package/dist/pages/8.9/LoginPage.js +19 -0
  24. package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
  25. package/dist/pages/8.9/ModelerHomePage.js +122 -3
  26. package/dist/pages/8.9/UtilitiesPage.js +60 -2
  27. package/dist/tests/8.7/test-setup.spec.js +18 -5
  28. package/dist/tests/8.7/web-modeler-user-flows.spec.js +14 -14
  29. package/dist/tests/8.9/test-setup.spec.js +11 -4
  30. package/dist/utils/authErrorClassification.d.ts +5 -0
  31. package/dist/utils/authErrorClassification.js +49 -1
  32. package/dist/utils/consoleApiHelpers.d.ts +1 -0
  33. package/dist/utils/consoleApiHelpers.js +183 -30
  34. package/dist/utils/resetSession.js +7 -0
  35. package/package.json +1 -1
@@ -9,6 +9,11 @@ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
10
  class ModelerHomePage {
11
11
  defaultFolderName = 'Cross Component Test Project';
12
+ // The default cluster test-setup.spec.ts's "Create Default Cluster" test
13
+ // provisions for the whole suite -- assigned to the shared workspace's
14
+ // Environments step so downstream specs can deploy to it without a manual
15
+ // assignment step first.
16
+ defaultClusterName = 'Test Cluster';
12
17
  page;
13
18
  // Since camunda/camunda-hub#25831 files and folders cannot sit at a
14
19
  // workspace ("project" in today's UI) root -- only process applications and
@@ -25,6 +30,11 @@ class ModelerHomePage {
25
30
  processApplicationName;
26
31
  modelerPageBanner;
27
32
  createNewProjectButton;
33
+ createWorkspaceButton;
34
+ workspaceWizardNameInput;
35
+ workspaceWizardSubmitButton;
36
+ workspaceWizardMembersStep;
37
+ workspaceWizardEnvironmentsHeading;
28
38
  projectNameInput;
29
39
  formNameInput;
30
40
  chooseBpmnTemplateButton;
@@ -53,9 +63,24 @@ class ModelerHomePage {
53
63
  constructor(page) {
54
64
  this.page = page;
55
65
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
56
- this.modelerPageBanner = page.getByRole('banner', {
66
+ // Hub's design-system shell renders the app header as a role=banner with no
67
+ // accessible name after the Camunda Hub rebrand, so the classic
68
+ // name-matched banner no longer resolves; the Modeler logo link inside it
69
+ // carries "Camunda Hub home" when the org has workspacesEnabled, else
70
+ // "Camunda Modeler home" (camunda-hub NavShellDs.common.tsx) -- match
71
+ // either. Keep the legacy named-banner branch for shells where the rename
72
+ // has not shipped yet.
73
+ this.modelerPageBanner = page
74
+ .getByRole('banner', {
57
75
  name: 'Camunda Modeler',
58
- });
76
+ })
77
+ .or(page
78
+ .getByRole('link', { name: 'Camunda Hub home' })
79
+ .and(page.locator(':not([href$="/login"])')))
80
+ .or(page
81
+ .getByRole('link', { name: 'Camunda Modeler home' })
82
+ .and(page.locator(':not([href$="/login"])')))
83
+ .first();
59
84
  // Hub renamed this button to "Create project" on both the classic and the
60
85
  // design-system shell. The legacy label is kept so the flow still works
61
86
  // against Modeler builds where the rename has not shipped yet. exact:true
@@ -68,6 +93,24 @@ class ModelerHomePage {
68
93
  name: 'Create project',
69
94
  exact: true,
70
95
  }));
96
+ // Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
97
+ // hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
98
+ // two-step wizard (General -> Members) that names the workspace up front.
99
+ // Same hooks as pages/SM-8.10/ModelerHomePage.ts. exact:true keeps the
100
+ // split button's options chevron out of the match.
101
+ this.createWorkspaceButton = page.getByRole('button', {
102
+ name: 'Create workspace',
103
+ exact: true,
104
+ });
105
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
106
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
107
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
108
+ // When the org has environments enabled, an "Assign environments" step is
109
+ // inserted after Members -- camunda-hub's own
110
+ // e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
111
+ this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
112
+ name: 'Assign environments',
113
+ });
71
114
  this.projectNameInput = page.locator('[data-test="editable-input"]');
72
115
  this.formNameInput = page.locator('[data-test="editable-input"]');
73
116
  this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
@@ -105,8 +148,11 @@ class ModelerHomePage {
105
148
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
106
149
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
107
150
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
151
+ // On the Camunda Hub Workspaces overview the folder is a workspace tile
152
+ // whose "Open workspace <name>" button navigates into it.
108
153
  this.crossComponentProjectFolder = page
109
- .getByTitle(this.defaultFolderName, { exact: true })
154
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
155
+ .or(page.getByTitle(this.defaultFolderName, { exact: true }))
110
156
  .first();
111
157
  this.rows = page.getByRole('row');
112
158
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
@@ -444,7 +490,76 @@ class ModelerHomePage {
444
490
  .then(() => true)
445
491
  .catch(() => false);
446
492
  }
493
+ // Drives Camunda Hub's workspace-creation wizard. The name is entered on
494
+ // the General step, which enables "Next"; the same submit button then
495
+ // relabels itself through the remaining steps and finally reads "Create
496
+ // workspace" once it creates the workspace and routes the creator into its
497
+ // projects listing. Gate the Members-step click on that step becoming
498
+ // active so the shared submit button is not clicked twice while still on
499
+ // General.
500
+ async createWorkspaceViaWizard(name) {
501
+ await this.dismissOverlays();
502
+ await this.createWorkspaceButton.click({ timeout: 60000 });
503
+ await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
504
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
505
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
506
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
507
+ // When the org has environments enabled, this only advances the wizard
508
+ // into an "Assign environments" step instead of submitting. Assign the
509
+ // suite's default cluster here rather than submitting with an empty
510
+ // selection -- otherwise this shared workspace starts with no
511
+ // environments and every downstream spec that deploys to it needs its
512
+ // own manual assignment first. Each card's accessible name is
513
+ // "Select <cluster name>" before it is picked (camunda-hub's own
514
+ // create-workspace-environments.spec.tsx asserts the same pattern).
515
+ // Sampled immediately (isVisible() doesn't wait) since the step change
516
+ // is a synchronous client-side re-render.
517
+ if (await this.workspaceWizardEnvironmentsHeading
518
+ .isVisible()
519
+ .catch(() => false)) {
520
+ // "Create Default Cluster" runs in parallel with this flow
521
+ // (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
522
+ // lists may still be provisioning -- wait for it a generous 90s rather
523
+ // than sampling once. Hub itself treats submitting unassigned as a
524
+ // supported, recoverable path for a real user ("Could not load
525
+ // environments. You can finish creating the workspace and assign them
526
+ // later."), but this shared workspace is relied on by every downstream
527
+ // spec that deploys to it -- silently submitting without the default
528
+ // environment turns a real setup problem here into confusing failures
529
+ // scattered across unrelated specs later. Fail loudly here instead.
530
+ const clusterOption = this.page.getByRole('button', {
531
+ name: new RegExp(`^Select ${this.defaultClusterName}`),
532
+ });
533
+ if (await this.becameVisible(clusterOption, 90000)) {
534
+ await clusterOption.click({ timeout: 10000 });
535
+ }
536
+ else {
537
+ throw new Error(`"${this.defaultClusterName}" was not selectable on the Environments step within 90s -- refusing to create the shared workspace without its default environment assigned, since every downstream spec that deploys to it depends on that assignment.`);
538
+ }
539
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
540
+ }
541
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
542
+ timeout: 120000,
543
+ });
544
+ }
447
545
  async clickHomeBreadcrumb() {
546
+ // Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
547
+ // overview, reached through the header logo link so the SPA keeps its
548
+ // in-memory session (a full navigation re-runs the whole Auth0 hand-off).
549
+ // The link's accessible name is "Camunda Hub home" when the org has
550
+ // workspacesEnabled, else "Camunda Modeler home" (camunda-hub
551
+ // NavShellDs.common.tsx) -- match either.
552
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
553
+ await this.page
554
+ .locator('header[data-slot="app-header"]')
555
+ .getByRole('link', { name: 'Camunda Hub home' })
556
+ .or(this.page
557
+ .locator('header[data-slot="app-header"]')
558
+ .getByRole('link', { name: 'Camunda Modeler home' }))
559
+ .first()
560
+ .click({ timeout: 30000 });
561
+ return;
562
+ }
448
563
  try {
449
564
  await this.homeBreadcrumb.click();
450
565
  }
@@ -537,6 +652,10 @@ class ModelerHomePage {
537
652
  return;
538
653
  }
539
654
  try {
655
+ if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
656
+ await this.createWorkspaceViaWizard(this.defaultFolderName);
657
+ return;
658
+ }
540
659
  await this.clickCreateNewProjectButton();
541
660
  await this.enterNewProjectName(this.defaultFolderName);
542
661
  return;
@@ -9,11 +9,34 @@ const sleep_1 = require("../../utils/sleep");
9
9
  const randomSleep_1 = require("../../utils/randomSleep");
10
10
  const fileUpload_1 = require("../../utils/fileUpload");
11
11
  const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
12
13
  // Tracks which worker-derived `timeout` values have already paid the
13
14
  // stagger sleep below, so a worker only pays it once per process lifetime
14
15
  // rather than on every call -- see the comment at the call site.
15
16
  const staggeredWorkerTimeouts = new Set();
17
+ const onboardingTourSuppressed = new WeakSet();
18
+ // Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
19
+ // (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
20
+ // "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
21
+ // hides the post-login banner link from the accessibility tree until dismissed, which times
22
+ // out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
23
+ // script on every navigation/reload, so there's no login-response race to lose; the flag is
24
+ // matched by prefix since the user id isn't known ahead of login.
25
+ async function suppressOnboardingTour(page) {
26
+ if (onboardingTourSuppressed.has(page)) {
27
+ return;
28
+ }
29
+ onboardingTourSuppressed.add(page);
30
+ await page.addInitScript((prefixPattern) => {
31
+ const pattern = new RegExp(prefixPattern);
32
+ const originalGetItem = Storage.prototype.getItem;
33
+ Storage.prototype.getItem = function (key) {
34
+ return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
35
+ };
36
+ }, '^hub\\.onboarding\\..*\\.seen\\.');
37
+ }
16
38
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
39
+ await suppressOnboardingTour(page);
17
40
  let lastError;
18
41
  // `timeout` is a stable per-worker identifier (derived from workerIndex),
19
42
  // and a worker is a long-lived process across every test/repeat it runs, so
@@ -25,6 +48,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
25
48
  for (let attempt = 0; attempt < maxRetries; attempt++) {
26
49
  try {
27
50
  await page.context().clearCookies();
51
+ // The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
52
+ // for the tab's whole lifetime; without clearing it here alongside
53
+ // cookies/storage, a flow that logs a different user into the same
54
+ // page could have getUserToken() return the PREVIOUS user's token.
55
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
28
56
  await page.goto('about:blank');
29
57
  await page
30
58
  .evaluate(() => {
@@ -61,6 +89,17 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
61
89
  }
62
90
  catch (error) {
63
91
  lastError = error;
92
+ // A rejected password or a blocked account is a credential/
93
+ // provisioning-level failure, not a transient one: every attempt
94
+ // resubmits the same env-supplied credentials, so retrying just
95
+ // reproduces the same rejection up to maxRetries times (observed:
96
+ // ~6 minutes burned across 5 identical "Wrong email or password"
97
+ // cycles for one seeded user). Fail on the first rejection instead.
98
+ if (String(error).includes(authErrorClassification_1.AUTH0_INVALID_CREDENTIALS) ||
99
+ String(error).includes(authErrorClassification_1.AUTH0_ACCOUNT_BLOCKED)) {
100
+ console.error(lastError);
101
+ throw lastError;
102
+ }
64
103
  if (attempt < maxRetries - 1) {
65
104
  const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
66
105
  console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
@@ -368,8 +407,14 @@ async function assertTestUsesCorrectOrganization(page) {
368
407
  const homePage = new HomePage_1.HomePage(page);
369
408
  if (page.url().includes('?code=') || page.url().includes('&state=')) {
370
409
  console.log('OAuth callback detected, waiting for redirect...');
410
+ // Camunda Hub (which replaced Console on SaaS) only exchanges the code
411
+ // once its SPA has booted, and on CI runners that boot alone was measured
412
+ // at ~45s past /login-callback -- a 30s wait aborted the exchange on every
413
+ // attempt. Ported from 8.10's already-verified fix (same run, confirmed
414
+ // via CI run 36003828752: "Create Project Folder for User 7" exhausted
415
+ // all 5 login retries on this exact 30s timeout).
371
416
  await page.waitForURL((url) => !url.toString().includes('?code='), {
372
- timeout: 30000,
417
+ timeout: 120000,
373
418
  });
374
419
  await (0, sleep_1.sleep)(5000);
375
420
  }
@@ -379,7 +424,20 @@ async function assertTestUsesCorrectOrganization(page) {
379
424
  await (0, test_1.expect)(homePage.modelerBanner).toBeVisible({ timeout: 30000 });
380
425
  }
381
426
  else {
382
- await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 60000 });
427
+ await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
428
+ timeout: 60000,
429
+ });
430
+ }
431
+ // Camunda Hub replaces Console on SaaS (8.10 release notes), and the Hub
432
+ // shell has neither the "Camunda logo Console" link nor the "Open
433
+ // Organizations" switcher, and lands on /workspaces without the org id in
434
+ // the URL. Its sidebar scopes the Clusters/Organization links to the active
435
+ // org, so assert the expected org id there instead.
436
+ if (await homePage.hubHeader.isVisible()) {
437
+ await (0, test_1.expect)(homePage.hubSidebar
438
+ .locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
439
+ .first()).toBeVisible({ timeout: 180000 });
440
+ return;
383
441
  }
384
442
  await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
385
443
  try {
@@ -12,7 +12,8 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
12
12
  await (0, _setup_1.captureScreenshot)(page, testInfo);
13
13
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
14
14
  });
15
- (0, _8_7_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
15
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
16
+ _8_7_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
16
17
  _8_7_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
17
18
  _8_7_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
18
19
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
@@ -60,10 +61,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
60
61
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
61
62
  });
62
63
  await _8_7_1.test.step('Navigate to Web Modeler', async () => {
63
- await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
64
- timeout: 120000,
65
- });
66
- await appsPage.clickCamundaApps();
64
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
65
+ // Modeler home directly and has no app switcher to open.
66
+ if (!new URL(page.url()).hostname.startsWith('hub.')) {
67
+ await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
68
+ timeout: 120000,
69
+ });
70
+ await appsPage.clickCamundaApps();
71
+ }
67
72
  await appsPage.clickModeler();
68
73
  await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
69
74
  timeout: 180000,
@@ -71,6 +76,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
71
76
  });
72
77
  await _8_7_1.test.step('Create Cross Component Project Folder', async () => {
73
78
  await homePage.clickSkipCustomization();
79
+ // Do not attempt a "only User 1 creates, everyone else waits" fix for
80
+ // the parallel race here: the 8.10 sibling already tried exactly this
81
+ // and reverted it (see tests/8.10/test-setup.spec.ts) after finding
82
+ // the created workspace never became visible to the other users'
83
+ // sessions at all -- not a wait-budget problem, so no amount of
84
+ // retrying fixes it. Every user independently checking-and-creating
85
+ // still races occasionally, but that is the smaller, known failure
86
+ // mode; see camunda/c8-cross-component-e2e-tests#3649.
74
87
  await modelerHomePage.createCrossComponentProjectFolder();
75
88
  await modelerHomePage.clickHomeBreadcrumb();
76
89
  await modelerHomePage.assertCrossComponentProjectFolderVisible();
@@ -134,13 +134,15 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
134
134
  });
135
135
  await (0, sleep_1.sleep)(30000);
136
136
  await appsPage.clickCamundaApps();
137
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible({
138
- timeout: 30000,
139
- });
137
+ // clickCamundaApps() is a no-op on Hub (no global app-switcher panel to
138
+ // open -- see its own comment), so modelerLink has nothing to assert
139
+ // there: Hub's authenticated shell already proves Modeler access by
140
+ // having been reached at all. consoleLink's accessible name ("Console")
141
+ // matches Hub's persistent sidebar link unchanged, so that assertion
142
+ // still applies on both shells.
140
143
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible();
141
- await (0, test_1.expect)(appsPage.operateLink).not.toBeVisible();
142
- await (0, test_1.expect)(appsPage.tasklistLink).not.toBeVisible();
143
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
144
+ await appsPage.assertOperatePresent(clusterName, false);
145
+ await appsPage.assertTasklistPresent(clusterName, false);
144
146
  await appsPage.clickModeler();
145
147
  await homePage.clickSkipCustomization();
146
148
  await modelerHomePage.createCrossComponentProjectFolder();
@@ -206,10 +208,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
206
208
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
207
209
  timeout: 30000,
208
210
  });
209
- await appsPage.assertOperatePresent();
210
- await appsPage.assertTasklistPresent();
211
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
212
- await appsPage.assertOptimizePresent(false);
211
+ await appsPage.assertOperatePresent(clusterName);
212
+ await appsPage.assertTasklistPresent(clusterName);
213
+ await appsPage.assertOptimizePresent(clusterName, false);
213
214
  await appsPage.clickModeler();
214
215
  await modelerHomePage.clickCrossComponentProjectFolder();
215
216
  await modelerHomePage.clickProcessDiagram(processName);
@@ -266,10 +267,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
266
267
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
267
268
  timeout: 20000,
268
269
  });
269
- await appsPage.assertOptimizePresent();
270
- await appsPage.assertOperatePresent();
271
- await appsPage.assertTasklistPresent();
272
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
270
+ await appsPage.assertOptimizePresent(clusterName);
271
+ await appsPage.assertOperatePresent(clusterName);
272
+ await appsPage.assertTasklistPresent(clusterName);
273
273
  await appsPage.clickModeler();
274
274
  await modelerHomePage.clickCrossComponentProjectFolder();
275
275
  await modelerHomePage.clickProcessDiagram(processName);
@@ -18,7 +18,8 @@ _8_9_1.test.describe('Cluster Setup Tests', () => {
18
18
  await (0, _setup_1.captureScreenshot)(page, testInfo);
19
19
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
20
20
  });
21
- (0, _8_9_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
21
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
22
+ _8_9_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
22
23
  _8_9_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
23
24
  _8_9_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
24
25
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
@@ -128,11 +129,17 @@ _8_9_1.test.describe('Cluster Setup Tests', () => {
128
129
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
129
130
  });
130
131
  await _8_9_1.test.step('Navigate to Web Modeler', async () => {
131
- await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
132
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
133
+ // Modeler home directly and has no app switcher to wait for.
134
+ await (0, test_1.expect)(homePage.camundaComponentsButton.or(homePage.hubHeader)).toBeVisible({
132
135
  timeout: 120000,
133
136
  });
134
- await appsPage.clickCamundaApps();
135
- await appsPage.clickModeler();
137
+ // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
138
+ // app switcher: its home already is the Modeler home.
139
+ if (!new URL(page.url()).hostname.startsWith('hub.')) {
140
+ await appsPage.clickCamundaApps();
141
+ await appsPage.clickModeler();
142
+ }
136
143
  await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
137
144
  timeout: 180000,
138
145
  });
@@ -6,3 +6,8 @@ export declare const AUTH0_QUOTA_TEXT: RegExp;
6
6
  export declare const AUTH0_GENERIC_ERROR_TEXT: RegExp;
7
7
  export declare function classifyAuthErrorPage(page: Page): Promise<string | null>;
8
8
  export declare function throwIfAuthErrorPage(page: Page): Promise<void>;
9
+ export declare const AUTH0_INVALID_CREDENTIALS = "AUTH0_INVALID_CREDENTIALS";
10
+ export declare const AUTH0_ACCOUNT_BLOCKED = "AUTH0_ACCOUNT_BLOCKED";
11
+ export declare const AUTH0_INVALID_CREDENTIALS_TEXT: RegExp;
12
+ export declare const AUTH0_ACCOUNT_BLOCKED_TEXT: RegExp;
13
+ export declare function classifyLoginRejection(page: Page, username: string): Promise<string | null>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.throwIfAuthErrorPage = exports.classifyAuthErrorPage = exports.AUTH0_GENERIC_ERROR_TEXT = exports.AUTH0_QUOTA_TEXT = exports.AUTH0_RATE_LIMIT_GENERIC = exports.AUTH0_RATE_LIMIT_QUOTA = exports.AUTH0_RATE_LIMIT = void 0;
3
+ exports.classifyLoginRejection = exports.AUTH0_ACCOUNT_BLOCKED_TEXT = exports.AUTH0_INVALID_CREDENTIALS_TEXT = exports.AUTH0_ACCOUNT_BLOCKED = exports.AUTH0_INVALID_CREDENTIALS = exports.throwIfAuthErrorPage = exports.classifyAuthErrorPage = exports.AUTH0_GENERIC_ERROR_TEXT = exports.AUTH0_QUOTA_TEXT = exports.AUTH0_RATE_LIMIT_GENERIC = exports.AUTH0_RATE_LIMIT_QUOTA = exports.AUTH0_RATE_LIMIT = void 0;
4
4
  // Auth0 renders an error page for two structurally different failures, and
5
5
  // until now both were counted as one signal. They need different responses:
6
6
  //
@@ -93,3 +93,51 @@ async function throwIfAuthErrorPage(page) {
93
93
  }
94
94
  }
95
95
  exports.throwIfAuthErrorPage = throwIfAuthErrorPage;
96
+ // Auth0 rejects a submitted password by re-rendering the same password step
97
+ // with one of these two messages, rather than serving a distinct error page
98
+ // -- so this needs its own probe after a password submission, unlike
99
+ // classifyAuthErrorPage above which covers the interstitial pages Auth0
100
+ // serves before any credentials are entered.
101
+ //
102
+ // Both are credential/provisioning-level rejections: the password Auth0 has
103
+ // on file for this account does not match what the runner was given, or the
104
+ // account is locked. Neither self-heals, so loginWithRetry treats this
105
+ // marker as fail-fast rather than the default retry-with-backoff -- retrying
106
+ // with the same env-supplied credentials 5 times only reproduces the same
107
+ // 400 response 5 times (see docs/saas-login-investigation.md).
108
+ exports.AUTH0_INVALID_CREDENTIALS = 'AUTH0_INVALID_CREDENTIALS';
109
+ exports.AUTH0_ACCOUNT_BLOCKED = 'AUTH0_ACCOUNT_BLOCKED';
110
+ exports.AUTH0_INVALID_CREDENTIALS_TEXT = /Wrong email or password/i;
111
+ exports.AUTH0_ACCOUNT_BLOCKED_TEXT = /account has been blocked after multiple consecutive login attempts/i;
112
+ // Auth0 re-renders the password step with the rejection copy right after the
113
+ // POST, but not necessarily by the moment the caller checks -- a zero-wait
114
+ // isVisible() can run before that paint completes and miss it, which would
115
+ // silently fall through to the generic ~120s shell wait instead of failing
116
+ // fast. Give each marker a short window to render.
117
+ const REJECTION_RENDER_TIMEOUT = 3000;
118
+ async function classifyLoginRejection(page, username) {
119
+ const [invalid, blocked] = await Promise.all([
120
+ page
121
+ .getByText(exports.AUTH0_INVALID_CREDENTIALS_TEXT)
122
+ .first()
123
+ .waitFor({ state: 'visible', timeout: REJECTION_RENDER_TIMEOUT })
124
+ .then(() => true)
125
+ .catch(() => false),
126
+ page
127
+ .getByText(exports.AUTH0_ACCOUNT_BLOCKED_TEXT)
128
+ .first()
129
+ .waitFor({ state: 'visible', timeout: REJECTION_RENDER_TIMEOUT })
130
+ .then(() => true)
131
+ .catch(() => false),
132
+ ]);
133
+ if (!invalid && !blocked) {
134
+ return null;
135
+ }
136
+ const marker = blocked ? exports.AUTH0_ACCOUNT_BLOCKED : exports.AUTH0_INVALID_CREDENTIALS;
137
+ const reason = blocked
138
+ ? 'account has been blocked after multiple consecutive login attempts'
139
+ : 'wrong email or password';
140
+ return (`${marker}: Auth0 rejected the credentials for ${username} (${reason}) ` +
141
+ '-- this is a credential/provisioning issue, not something a retry fixes');
142
+ }
143
+ exports.classifyLoginRejection = classifyLoginRejection;
@@ -1,4 +1,5 @@
1
1
  import { Page } from '@playwright/test';
2
+ export declare function resetHubBearerToken(page: Page): void;
2
3
  export declare function deleteClusterViaApi(page: Page, clusterName: string): Promise<void>;
3
4
  export declare function createClusterViaApi(page: Page, clusterName: string, region?: string, tasklistV2Enabled?: boolean): Promise<string>;
4
5
  export declare function ensureClusterViaApi(page: Page, clusterName: string, region?: string, tasklistV2Enabled?: boolean): Promise<string>;