@camunda/e2e-test-suite 0.0.1247 → 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.
- package/dist/pages/8.9/AppsPage.d.ts +2 -0
- package/dist/pages/8.9/AppsPage.js +114 -0
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
- package/dist/pages/8.9/ClusterPage.js +49 -8
- package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
- package/dist/pages/8.9/HomePage.d.ts +3 -0
- package/dist/pages/8.9/HomePage.js +44 -5
- package/dist/pages/8.9/LoginPage.d.ts +1 -0
- package/dist/pages/8.9/LoginPage.js +19 -0
- package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/8.9/ModelerHomePage.js +122 -3
- package/dist/pages/8.9/UtilitiesPage.js +60 -2
- package/dist/pages/SM-8.10/NavigationPage.js +1 -0
- package/dist/tests/8.9/test-setup.spec.js +11 -4
- package/dist/utils/authErrorClassification.d.ts +5 -0
- package/dist/utils/authErrorClassification.js +49 -1
- package/dist/utils/consoleApiHelpers.d.ts +1 -0
- package/dist/utils/consoleApiHelpers.js +183 -30
- package/dist/utils/resetSession.js +7 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
.
|
|
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:
|
|
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({
|
|
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 {
|
|
@@ -370,6 +370,7 @@ class NavigationPage {
|
|
|
370
370
|
maxRetries ?? (env_1.isOpenSearch ? 10 : 8));
|
|
371
371
|
}
|
|
372
372
|
async goToClusters(sleepTimeout, credentials) {
|
|
373
|
+
await this.suppressOnboardingTour();
|
|
373
374
|
await this.goTo(`${MODELER_CONTEXT_PATH}/clusters`, this.modelerPageBanner, sleepTimeout, credentials);
|
|
374
375
|
}
|
|
375
376
|
async goToManagementIdentity(sleepTimeout, credentials) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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>;
|