@camunda/e2e-test-suite 0.0.1248 → 0.0.1249

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.
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
5
5
  const authErrorClassification_1 = require("../../utils/authErrorClassification");
6
6
  const UtilitiesPage_1 = require("./UtilitiesPage");
7
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
8
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
8
9
  const userNameInputValidInputSchema = {
9
10
  _schema: {
10
11
  maxlength: 254,
@@ -66,9 +67,23 @@ class LoginPage {
66
67
  async clickLoginButton() {
67
68
  await this.loginButton.click({ timeout: 60000 });
68
69
  }
70
+ // Auth0 responds to a rejected password by re-rendering the same password
71
+ // step with an error message rather than navigating anywhere, so success
72
+ // and failure aren't told apart by a URL or element one side has and the
73
+ // other doesn't. Without this check, a permanently wrong password falls
74
+ // through into assertTestUsesCorrectOrganization's generic ~120s wait for a
75
+ // Console/Hub shell that is never coming, and loginWithRetry then spends
76
+ // its remaining attempts resubmitting the exact same doomed credentials.
77
+ async throwIfCredentialsRejected(username) {
78
+ const message = await (0, authErrorClassification_1.classifyLoginRejection)(this.page, username);
79
+ if (message) {
80
+ throw new Error(message);
81
+ }
82
+ }
69
83
  async login(credentials = {}) {
70
84
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
71
85
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
86
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
72
87
  await (0, test_1.expect)(this.loginMessage).toBeVisible({ timeout: 120000 });
73
88
  await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 30000 });
74
89
  await this.fillUsername(username);
@@ -76,11 +91,13 @@ class LoginPage {
76
91
  await this.fillPassword(password);
77
92
  await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
78
93
  await this.clickLoginButton();
94
+ await this.throwIfCredentialsRejected(username);
79
95
  await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
80
96
  }
81
97
  async loginWithTestUser(credentials = {}) {
82
98
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
83
99
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
100
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
84
101
  // Navigate to app root to trigger auth redirect. The Console app (at
85
102
  // console.* hostnames) embeds a hidden invite-user email input also labeled
86
103
  // "Email address" that would otherwise satisfy the locator below before the
@@ -157,6 +174,7 @@ class LoginPage {
157
174
  await this.fillPassword(password);
158
175
  await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
159
176
  await this.clickLoginButton();
177
+ await this.throwIfCredentialsRejected(username);
160
178
  await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
161
179
  }
162
180
  async loginFailAssertion() {
@@ -170,6 +188,7 @@ class LoginPage {
170
188
  async loginWithoutOrgAssertion(credentials = {}) {
171
189
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
172
190
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
191
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
173
192
  await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 180000 });
174
193
  await this.fillUsername(username);
175
194
  await this.clickContinueButton();
@@ -1,12 +1,18 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private defaultFolderName;
4
+ private defaultClusterName;
4
5
  private page;
5
6
  private processApplicationPage;
6
7
  private processApplicationUrl?;
7
8
  private processApplicationName?;
8
9
  readonly modelerPageBanner: Locator;
9
10
  readonly createNewProjectButton: Locator;
11
+ readonly createWorkspaceButton: Locator;
12
+ readonly workspaceWizardNameInput: Locator;
13
+ readonly workspaceWizardSubmitButton: Locator;
14
+ readonly workspaceWizardMembersStep: Locator;
15
+ readonly workspaceWizardEnvironmentsHeading: Locator;
10
16
  readonly projectNameInput: Locator;
11
17
  readonly formNameInput: Locator;
12
18
  readonly chooseBpmnTemplateButton: Locator;
@@ -57,6 +63,7 @@ declare class ModelerHomePage {
57
63
  returnToResourceContainer(): Promise<void>;
58
64
  startBlankBpmnDiagram(): Promise<void>;
59
65
  private becameVisible;
66
+ private createWorkspaceViaWizard;
60
67
  clickHomeBreadcrumb(): Promise<void>;
61
68
  clickOpenOrganizationsButton(): Promise<void>;
62
69
  createForm(formName: string): Promise<void>;
@@ -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 {
@@ -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>;