@camunda/e2e-test-suite 0.0.1253 → 0.0.1254
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.10/AppsPage.js +6 -2
- package/dist/pages/8.10/ClusterDetailsPage.d.ts +2 -2
- package/dist/pages/8.10/ClusterDetailsPage.js +75 -31
- package/dist/pages/8.10/ClusterPage.js +30 -2
- package/dist/pages/8.10/ClusterSecretsPage.js +142 -10
- package/dist/pages/8.10/HomePage.d.ts +2 -0
- package/dist/pages/8.10/HomePage.js +40 -2
- package/dist/pages/8.10/LoginPage.d.ts +1 -0
- package/dist/pages/8.10/LoginPage.js +29 -0
- package/dist/pages/8.10/ModelerHomePage.d.ts +8 -0
- package/dist/pages/8.10/ModelerHomePage.js +221 -35
- package/dist/pages/8.10/UtilitiesPage.js +58 -2
- package/dist/tests/8.10/test-setup.spec.js +5 -2
- package/package.json +1 -1
|
@@ -261,8 +261,12 @@ class AppsPage {
|
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
265
|
+
// app switcher: its home already is the Modeler home.
|
|
266
|
+
if (!new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
267
|
+
await this.clickCamundaApps();
|
|
268
|
+
await this.clickModeler();
|
|
269
|
+
}
|
|
266
270
|
try {
|
|
267
271
|
await modelerReady();
|
|
268
272
|
return;
|
|
@@ -32,6 +32,7 @@ declare class ClusterDetailsPage {
|
|
|
32
32
|
readonly createAlertButton: Locator;
|
|
33
33
|
readonly createNewClientButton: Locator;
|
|
34
34
|
readonly clientsList: Locator;
|
|
35
|
+
readonly clientsFilterInput: Locator;
|
|
35
36
|
readonly envVarsTab: Locator;
|
|
36
37
|
readonly zeebeCheckbox: Locator;
|
|
37
38
|
readonly apiClientNameTextbox: Locator;
|
|
@@ -50,7 +51,7 @@ declare class ClusterDetailsPage {
|
|
|
50
51
|
readonly restartRequiredDialog: Locator;
|
|
51
52
|
readonly restartConfirmButton: Locator;
|
|
52
53
|
readonly mcpTab: Locator;
|
|
53
|
-
readonly
|
|
54
|
+
readonly mcpConfigBlock: Locator;
|
|
54
55
|
constructor(page: Page);
|
|
55
56
|
clickAPITab(): Promise<void>;
|
|
56
57
|
dismissOopsIfPresent(): Promise<void>;
|
|
@@ -64,7 +65,6 @@ declare class ClusterDetailsPage {
|
|
|
64
65
|
clickCreateButton(): Promise<void>;
|
|
65
66
|
clickCloseModalButton(): Promise<void>;
|
|
66
67
|
assertAPIClientScopes(name: string): Promise<void>;
|
|
67
|
-
filterClientsList(name: string): Promise<void>;
|
|
68
68
|
clickSettingsTab(): Promise<void>;
|
|
69
69
|
private waitForAuthorizationsSection;
|
|
70
70
|
enableAuthorizations(): Promise<void>;
|
|
@@ -39,6 +39,7 @@ class ClusterDetailsPage {
|
|
|
39
39
|
createAlertButton;
|
|
40
40
|
createNewClientButton;
|
|
41
41
|
clientsList;
|
|
42
|
+
clientsFilterInput;
|
|
42
43
|
envVarsTab;
|
|
43
44
|
zeebeCheckbox;
|
|
44
45
|
apiClientNameTextbox;
|
|
@@ -57,7 +58,7 @@ class ClusterDetailsPage {
|
|
|
57
58
|
restartRequiredDialog;
|
|
58
59
|
restartConfirmButton;
|
|
59
60
|
mcpTab;
|
|
60
|
-
|
|
61
|
+
mcpConfigBlock;
|
|
61
62
|
constructor(page) {
|
|
62
63
|
this.page = page;
|
|
63
64
|
this.apiTab = page.getByRole('tab', { name: 'API' });
|
|
@@ -101,7 +102,14 @@ class ClusterDetailsPage {
|
|
|
101
102
|
this.clientCredentialsDialog = page.getByRole('dialog').filter({
|
|
102
103
|
has: page.getByText('The Client Secret will not be shown again.'),
|
|
103
104
|
});
|
|
104
|
-
|
|
105
|
+
// Hub's client-credentials dialog (design-system Dialog, not the old
|
|
106
|
+
// Carbon one) exposes its close affix as an accessible button named
|
|
107
|
+
// "Close" rather than via an aria-label picked up by getByLabel -- see
|
|
108
|
+
// camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
|
|
109
|
+
// (clientCredentialsDialogCloseButton), which already uses this pattern.
|
|
110
|
+
this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
|
|
111
|
+
name: 'Close',
|
|
112
|
+
});
|
|
105
113
|
this.clustersLink = page
|
|
106
114
|
.getByRole('banner')
|
|
107
115
|
.getByRole('link', { name: 'Clusters' });
|
|
@@ -129,6 +137,17 @@ class ClusterDetailsPage {
|
|
|
129
137
|
name: 'Create new client',
|
|
130
138
|
});
|
|
131
139
|
this.clientsList = page.getByRole('row').filter({ hasNotText: 'Scopes' });
|
|
140
|
+
// The clients table paginates at ten rows and is not ordered by creation
|
|
141
|
+
// time, so on a cluster that already has clients a newly created one is
|
|
142
|
+
// usually on a page other than the one being displayed -- matching a row
|
|
143
|
+
// without filtering first reports it as missing (main's filterClientsList
|
|
144
|
+
// existed for exactly this). "Filter table" was Carbon's default
|
|
145
|
+
// TableToolbarSearch label; Hub's rebranded panel renders a plain
|
|
146
|
+
// "Search" input instead, so match either.
|
|
147
|
+
this.clientsFilterInput = page
|
|
148
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
149
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
150
|
+
.or(page.getByPlaceholder('Search'));
|
|
132
151
|
this.envVarsTab = this.clientCredentialsDialog.getByRole('tab', {
|
|
133
152
|
name: 'Env Vars',
|
|
134
153
|
});
|
|
@@ -179,7 +198,13 @@ class ClusterDetailsPage {
|
|
|
179
198
|
name: 'Continue',
|
|
180
199
|
});
|
|
181
200
|
this.mcpTab = page.getByRole('tab', { name: 'MCP' });
|
|
182
|
-
|
|
201
|
+
// The config text renders in a <pre> inside the MCP-labelled panel, not
|
|
202
|
+
// a textbox -- matches the already-working 8.9 sibling's locator. A live
|
|
203
|
+
// trace confirmed "Copy to clipboard" is actually the aria-label/title of
|
|
204
|
+
// an unrelated icon-only <button> positioned over that block, which is
|
|
205
|
+
// why getByRole('textbox', {name: 'Copy to clipboard'}) always found
|
|
206
|
+
// nothing here.
|
|
207
|
+
this.mcpConfigBlock = page.getByLabel('MCP', { exact: true }).locator('pre');
|
|
183
208
|
}
|
|
184
209
|
async clickAPITab() {
|
|
185
210
|
await (0, test_1.expect)(this.apiTab).toBeVisible({ timeout: 60000 });
|
|
@@ -252,33 +277,34 @@ class ClusterDetailsPage {
|
|
|
252
277
|
async assertAPIClientScopes(name) {
|
|
253
278
|
const row = this.clientsList.filter({ hasText: name });
|
|
254
279
|
const scopes = /(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
const maxAttempts = 3;
|
|
281
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
282
|
+
try {
|
|
283
|
+
// The table paginates and isn't ordered by creation time, so a newly
|
|
284
|
+
// created client is usually on a page other than the one displayed --
|
|
285
|
+
// filter down to it first, the same way main's filterClientsList did,
|
|
286
|
+
// rather than matching rows only on the currently rendered page.
|
|
287
|
+
await (0, test_1.expect)(this.clientsFilterInput).toBeVisible({ timeout: 60000 });
|
|
288
|
+
await this.clientsFilterInput.fill(name);
|
|
289
|
+
await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (attempt === maxAttempts) {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
// A panel that rendered before the client existed — or that failed
|
|
297
|
+
// into the "Oops ... something went wrong." state — only picks the
|
|
298
|
+
// new client up on a refetch, which is what reloading the API tab
|
|
299
|
+
// forces. A single reload isn't always enough on a nightly cluster
|
|
300
|
+
// this suite reuses across parallel test workers, so retry more
|
|
301
|
+
// than once before failing the test.
|
|
302
|
+
console.warn(`assertAPIClientScopes: "${name}" is not listed yet (attempt ${attempt}/${maxAttempts}); reloading the API tab and retrying. ${error}`);
|
|
303
|
+
await this.dismissOopsIfPresent();
|
|
304
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
305
|
+
await this.clickAPITab();
|
|
306
|
+
}
|
|
258
307
|
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
// A panel that rendered before the client existed — or that failed into
|
|
261
|
-
// the "Oops ... something went wrong." state — only picks the new client
|
|
262
|
-
// up on a refetch, which is what reloading the API tab forces.
|
|
263
|
-
console.warn(`assertAPIClientScopes: "${name}" is not listed yet; reloading the API tab and retrying. ${error}`);
|
|
264
|
-
await this.dismissOopsIfPresent();
|
|
265
|
-
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
266
|
-
await this.clickAPITab();
|
|
267
|
-
await this.filterClientsList(name);
|
|
268
|
-
await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Narrows the clients table to one client through the table's own filter.
|
|
272
|
-
// The table paginates at ten rows and is not ordered by creation time, so on
|
|
273
|
-
// a cluster that already has clients a newly created one is usually on a
|
|
274
|
-
// page other than the one being displayed — matching a row without filtering
|
|
275
|
-
// first reports it as missing.
|
|
276
|
-
async filterClientsList(name) {
|
|
277
|
-
const filterInput = this.page.getByRole('searchbox', {
|
|
278
|
-
name: 'Filter table',
|
|
279
|
-
});
|
|
280
|
-
await (0, test_1.expect)(filterInput).toBeVisible({ timeout: 60000 });
|
|
281
|
-
await filterInput.fill(name);
|
|
282
308
|
}
|
|
283
309
|
async clickSettingsTab() {
|
|
284
310
|
// Console paints the cluster-details tab list before it finishes
|
|
@@ -772,8 +798,26 @@ class ClusterDetailsPage {
|
|
|
772
798
|
await this.mcpTab.click();
|
|
773
799
|
}
|
|
774
800
|
async getMcpConfigurationText() {
|
|
775
|
-
|
|
776
|
-
|
|
801
|
+
// CI (PR #3629, "Create Agentic Orchestration Cluster") showed the MCP
|
|
802
|
+
// panel's config block still not rendered 60s after clickMCPTab()
|
|
803
|
+
// reported a successful click -- the same "click landed but the panel
|
|
804
|
+
// never switched" race already seen on the Connector Secrets tab.
|
|
805
|
+
// Re-click the tab and retry rather than failing on the first miss.
|
|
806
|
+
const maxAttempts = 3;
|
|
807
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
808
|
+
try {
|
|
809
|
+
await (0, test_1.expect)(this.mcpConfigBlock).toBeVisible({ timeout: 60000 });
|
|
810
|
+
return await this.mcpConfigBlock.innerText();
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
if (attempt === maxAttempts) {
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
console.warn(`getMcpConfigurationText: MCP panel did not render (attempt ${attempt}/${maxAttempts}); re-clicking the MCP tab and retrying. ${error}`);
|
|
817
|
+
await this.clickMCPTab();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
throw new Error('getMcpConfigurationText: unreachable');
|
|
777
821
|
}
|
|
778
822
|
async createMCPClientAndReturnVariables(name, setEnvVariables = true) {
|
|
779
823
|
await this.createAPIClient(name);
|
|
@@ -143,10 +143,38 @@ class ClusterPage {
|
|
|
143
143
|
this.clusterVersion = (clusterName, versionText) => this.clusterRow(clusterName).getByText(versionText, { exact: false });
|
|
144
144
|
}
|
|
145
145
|
async clickClusterBanner() {
|
|
146
|
-
|
|
146
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
147
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
148
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
149
|
+
// HomePage.clusterTab handles).
|
|
150
|
+
const clusterLink = this.page
|
|
147
151
|
.getByRole('banner')
|
|
148
152
|
.getByRole('link', { name: 'Clusters' })
|
|
149
|
-
.
|
|
153
|
+
.or(this.page
|
|
154
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
155
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
156
|
+
.or(this.page
|
|
157
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
158
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
159
|
+
await clusterLink.click({ timeout: 60000 });
|
|
160
|
+
// Landing on Hub's Environments overview (see above) renders clusters as
|
|
161
|
+
// cards, not the getByRole('row') table this page's own locators expect
|
|
162
|
+
// -- route through the page's own "Clusters" tab to reach the classic
|
|
163
|
+
// route. Same fix as HomePage.clickClusters().
|
|
164
|
+
const clustersPageTab = this.page
|
|
165
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
166
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
167
|
+
// On Hub this tab is required -- the row-based locators callers use right
|
|
168
|
+
// after this method return nothing on the card-based Environments
|
|
169
|
+
// overview, so a slow render must not be treated as "tab doesn't exist"
|
|
170
|
+
// the way the legacy (non-Hub) shell's 5s probe is allowed to.
|
|
171
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
172
|
+
await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
|
|
173
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
174
|
+
}
|
|
175
|
+
else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
176
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
177
|
+
}
|
|
150
178
|
}
|
|
151
179
|
async confirmClusterDeletion() {
|
|
152
180
|
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 60000 });
|
|
@@ -56,15 +56,46 @@ class ClusterSecretsPage {
|
|
|
56
56
|
name: 'Cancel',
|
|
57
57
|
exact: true,
|
|
58
58
|
});
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Hub's Import secrets dialog reports partial failures as "Imported X of
|
|
60
|
+
// Y secrets. Not imported: <key> — a secret with this key already
|
|
61
|
+
// exists; ..." -- confirmed from the PR #3629 CI trace screenshot for
|
|
62
|
+
// "Create AWS Cluster". This page object is also still exercised on the
|
|
63
|
+
// legacy Console under IS_PROD (that shell reports "Imported X secrets, Y
|
|
64
|
+
// failed" instead), so match either rather than assuming every env has
|
|
65
|
+
// rolled Hub out.
|
|
66
|
+
this.bulkImportErrorBanner = this.dialog
|
|
67
|
+
.getByText(/Imported \d+ of \d+ secrets?\./)
|
|
68
|
+
.or(this.dialog.getByText(/Imported \d+ secrets?, \d+ failed/));
|
|
69
|
+
// "Filter table" was Carbon's default TableToolbarSearch label. Camunda
|
|
70
|
+
// Hub's rebranded panel renders a plain "Search" input instead, so the
|
|
71
|
+
// old exact-name match never resolves and assertSecretsOrCreate falls
|
|
72
|
+
// through to (re-)creating secrets that may already exist. Match either
|
|
73
|
+
// (see the 8.9 sibling fix, confirmed via a live trace).
|
|
74
|
+
this.secretsSearchBox = page
|
|
75
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
76
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
77
|
+
.or(page.getByPlaceholder('Search'));
|
|
78
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
79
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
80
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
81
|
+
// HomePage.clusterTab handles).
|
|
61
82
|
this.clusterBanner = this.page
|
|
62
83
|
.getByRole('banner')
|
|
63
|
-
.getByRole('link', { name: 'Clusters' })
|
|
84
|
+
.getByRole('link', { name: 'Clusters' })
|
|
85
|
+
.or(this.page
|
|
86
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
87
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
88
|
+
.or(this.page
|
|
89
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
90
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
64
91
|
this.clusterLink = (name) => page.getByRole('link', { name: name });
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
// Some environments render this tab as "Cluster secrets" rather than
|
|
93
|
+
// "Connector Secrets" -- match either (see ClusterPage's identical
|
|
94
|
+
// connectorSecretsTab fallback; this page object has its own separate
|
|
95
|
+
// locator and was missing it).
|
|
96
|
+
this.clusterSecretTab = this.page
|
|
97
|
+
.getByRole('tab', { name: 'Cluster secrets' })
|
|
98
|
+
.or(this.page.getByRole('tab', { name: 'Connector Secrets' }));
|
|
68
99
|
this.confirmDeleteInput = page.getByRole('textbox', {
|
|
69
100
|
name: 'Type the word DELETE to confirm',
|
|
70
101
|
});
|
|
@@ -106,17 +137,55 @@ class ClusterSecretsPage {
|
|
|
106
137
|
async clickCreateNewSecretButton(cluster) {
|
|
107
138
|
try {
|
|
108
139
|
await this.createNewSecretButton.click({ timeout: 60000 });
|
|
140
|
+
return;
|
|
109
141
|
}
|
|
110
142
|
catch (error) {
|
|
143
|
+
// fall through to the recovery path below
|
|
144
|
+
}
|
|
145
|
+
// CI (PR #3629, "Create Default Cluster" / "Create AWS Cluster") showed
|
|
146
|
+
// the "Connector Secrets" tab still not rendered 30s after navigating
|
|
147
|
+
// back to the cluster -- a single recovery attempt isn't always enough
|
|
148
|
+
// once the page is this slow to settle, so retry the whole
|
|
149
|
+
// banner->cluster->tab sequence rather than failing on the first miss.
|
|
150
|
+
const maxAttempts = 3;
|
|
151
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
111
152
|
await this.clickClusterBanner();
|
|
112
153
|
await this.clickCluster(cluster);
|
|
113
|
-
|
|
114
|
-
|
|
154
|
+
try {
|
|
155
|
+
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
156
|
+
await this.createNewSecretButton.click({ timeout: 30000 });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (attempt === maxAttempts) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
console.warn(`clickCreateNewSecretButton: recovery attempt ${attempt}/${maxAttempts} failed; retrying. ${error}`);
|
|
164
|
+
}
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
167
|
async clickClusterBanner() {
|
|
118
168
|
await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
|
|
119
169
|
await this.clusterBanner.click({ timeout: 60000 });
|
|
170
|
+
// Landing on Hub's Environments overview (see clusterBanner above) renders
|
|
171
|
+
// clusters as cards, not the getByRole('link') the clusterLink() locator
|
|
172
|
+
// below expects to find directly -- route through the page's own
|
|
173
|
+
// "Clusters" tab to reach the classic route. Same fix as
|
|
174
|
+
// HomePage.clickClusters().
|
|
175
|
+
const clustersPageTab = this.page
|
|
176
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
177
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
178
|
+
// On Hub this tab is required -- clickCluster()'s row-based clusterLink
|
|
179
|
+
// resolves nothing on the card-based Environments overview, so a slow
|
|
180
|
+
// render must not be treated as "tab doesn't exist" the way the legacy
|
|
181
|
+
// (non-Hub) shell's 5s probe is allowed to.
|
|
182
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
183
|
+
await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
|
|
184
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
185
|
+
}
|
|
186
|
+
else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
187
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
188
|
+
}
|
|
120
189
|
}
|
|
121
190
|
async clickCluster(name) {
|
|
122
191
|
await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
|
|
@@ -171,9 +240,72 @@ class ClusterSecretsPage {
|
|
|
171
240
|
await (0, test_1.expect)(this.page.getByText('Creating...')).not.toBeVisible({
|
|
172
241
|
timeout: 60000,
|
|
173
242
|
});
|
|
174
|
-
|
|
175
|
-
|
|
243
|
+
const secretRow = this.page.getByRole('row', {
|
|
244
|
+
name: new RegExp(`^${key}\\b`, 'i'),
|
|
176
245
|
});
|
|
246
|
+
// Two separate CI runs (35854706213 with 7 secrets, and 36011511221 with
|
|
247
|
+
// 13) each showed 100% of secrets taking the reload path on attempt 1 --
|
|
248
|
+
// not an occasional race, and not even "near-universal": zero exceptions
|
|
249
|
+
// across 20 samples. A wait on the unreloaded panel was therefore never
|
|
250
|
+
// going to pay off, so reload unconditionally before the first check
|
|
251
|
+
// instead of probing a state that has never once been the real one.
|
|
252
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
253
|
+
await this.clickClusterBanner();
|
|
254
|
+
await this.clickCluster(cluster);
|
|
255
|
+
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
256
|
+
const maxAttempts = 3;
|
|
257
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
|
|
260
|
+
timeout: 60000,
|
|
261
|
+
});
|
|
262
|
+
// The button reappearing only proves the panel re-rendered, not that
|
|
263
|
+
// this specific secret was created -- a failed create request can
|
|
264
|
+
// leave the panel in the same state. Verify the row before
|
|
265
|
+
// returning: CI showed the row can lag briefly behind the button on
|
|
266
|
+
// a slow reload.
|
|
267
|
+
//
|
|
268
|
+
// Filter down to this key first: a live CI trace (run 36001802609,
|
|
269
|
+
// "Create Default Cluster") showed the unfiltered check time out on
|
|
270
|
+
// both attempts even though the key was confirmed present -- the
|
|
271
|
+
// create POST itself returned 409 (already exists) and the API's own
|
|
272
|
+
// list included it -- because the cluster's shared, reused secret set
|
|
273
|
+
// had grown past what the alphabetically-sorted table renders without
|
|
274
|
+
// narrowing it, the same "off-screen in a long list" pattern
|
|
275
|
+
// documented for Hub's other list views. assertSecretsOrCreate
|
|
276
|
+
// already searches before deciding a secret is missing; do the same
|
|
277
|
+
// here before concluding the create failed.
|
|
278
|
+
await (0, test_1.expect)(this.secretsSearchBox).toBeVisible({ timeout: 10000 });
|
|
279
|
+
await this.secretsSearchBox.fill(key);
|
|
280
|
+
await (0, test_1.expect)(secretRow).toBeVisible({ timeout: 60000 });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (attempt === maxAttempts) {
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
// The secrets panel can land in a Carbon "Oops ... something went
|
|
288
|
+
// wrong." state after a background request fails post-create, which
|
|
289
|
+
// never recovers on its own and only clears on a reload. Reload
|
|
290
|
+
// unconditionally rather than gating on an "Oops" heading: PR#3627's
|
|
291
|
+
// CI hit this same timeout with no Oops heading ever rendered, and
|
|
292
|
+
// the panel still only recovered on a reload -- the same
|
|
293
|
+
// unconditional reload bulkImportSecrets() already uses on this
|
|
294
|
+
// identical panel. A single retry wasn't always enough either on
|
|
295
|
+
// this suite's shared, reused cluster, so retry more than once.
|
|
296
|
+
console.warn(`createNewSecret: "${key}" was created but the secrets list did not re-render (attempt ${attempt}/${maxAttempts}); reloading and retrying. ${error}`);
|
|
297
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
298
|
+
// The reload lands back on the cluster's default Overview tab, not
|
|
299
|
+
// Connector Secrets -- a CI failure screenshot confirmed the page
|
|
300
|
+
// stuck there with the secrets panel never reached, so
|
|
301
|
+
// createNewSecretButton was never going to reappear on the next
|
|
302
|
+
// attempt regardless of whether the reload fixed the underlying
|
|
303
|
+
// render issue. Re-navigate to the tab before retrying.
|
|
304
|
+
await this.clickClusterBanner();
|
|
305
|
+
await this.clickCluster(cluster);
|
|
306
|
+
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
177
309
|
}
|
|
178
310
|
async createSetOfSecrets(cluster, secrets) {
|
|
179
311
|
await this.bulkImportSecrets(secrets);
|
|
@@ -12,6 +12,8 @@ declare class HomePage {
|
|
|
12
12
|
readonly appSwitcher: Locator;
|
|
13
13
|
readonly openOrganizationButton: Locator;
|
|
14
14
|
readonly consoleBanner: Locator;
|
|
15
|
+
readonly hubHeader: Locator;
|
|
16
|
+
readonly hubSidebar: Locator;
|
|
15
17
|
readonly modelerBanner: Locator;
|
|
16
18
|
readonly gettingStartedHeading: Locator;
|
|
17
19
|
readonly buttonSkipCustomization: Locator;
|
|
@@ -15,16 +15,35 @@ class HomePage {
|
|
|
15
15
|
appSwitcher;
|
|
16
16
|
openOrganizationButton;
|
|
17
17
|
consoleBanner;
|
|
18
|
+
hubHeader;
|
|
19
|
+
hubSidebar;
|
|
18
20
|
modelerBanner;
|
|
19
21
|
gettingStartedHeading;
|
|
20
22
|
buttonSkipCustomization;
|
|
21
23
|
constructor(page) {
|
|
22
24
|
this.page = page;
|
|
25
|
+
// Camunda Hub replaces Console on SaaS: an app header (only carrying the
|
|
26
|
+
// user menu once authenticated) plus a sidebar whose Console section
|
|
27
|
+
// links to /org/<id>/clusters and /org/<id>/management.
|
|
28
|
+
this.hubHeader = page
|
|
29
|
+
.locator('header[data-slot="app-header"]')
|
|
30
|
+
.filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
|
|
31
|
+
this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
|
|
23
32
|
this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
|
|
33
|
+
// Once an org has environmentsEnabled, camunda-hub's
|
|
34
|
+
// use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
|
|
35
|
+
// labelled "Environments" whose href is '/environments', not
|
|
36
|
+
// '/clusters' -- accept both hrefs so this resolves either way.
|
|
24
37
|
this.clusterTab = page
|
|
25
38
|
.getByRole('banner')
|
|
26
|
-
.getByRole('link', { name: 'Clusters' })
|
|
27
|
-
|
|
39
|
+
.getByRole('link', { name: 'Clusters' })
|
|
40
|
+
.or(this.hubSidebar.locator('a[href$="/clusters"]'))
|
|
41
|
+
.or(this.hubSidebar.locator('a[href$="/environments"]'))
|
|
42
|
+
.first();
|
|
43
|
+
this.organizationTab = page
|
|
44
|
+
.getByRole('link', { name: 'Organization' })
|
|
45
|
+
.or(this.hubSidebar.locator('a[href$="/management"]'))
|
|
46
|
+
.first();
|
|
28
47
|
this.organizationViewTitle = page.getByText('Organization Management');
|
|
29
48
|
this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
|
|
30
49
|
this.informationDialog = page.getByRole('button', {
|
|
@@ -58,6 +77,25 @@ class HomePage {
|
|
|
58
77
|
await this.closeInformationDialog();
|
|
59
78
|
}
|
|
60
79
|
await this.clusterTab.click({ timeout: 15000 });
|
|
80
|
+
// Landing on Hub's Environments overview (see clusterTab above) doesn't
|
|
81
|
+
// put us on the classic row-based cluster list ClusterPage.ts expects --
|
|
82
|
+
// that page renders clusters as cards. Its own header renders a separate
|
|
83
|
+
// "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
|
|
84
|
+
// that does.
|
|
85
|
+
const clustersPageTab = this.page
|
|
86
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
87
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
88
|
+
// On Hub this tab is required -- the row-based locators callers use right
|
|
89
|
+
// after this method return nothing on the card-based Environments
|
|
90
|
+
// overview, so a slow render must not be treated as "tab doesn't exist"
|
|
91
|
+
// the way the legacy (non-Hub) shell's 5s probe is allowed to.
|
|
92
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
93
|
+
await (0, test_1.expect)(clustersPageTab).toBeVisible({ timeout: 30000 });
|
|
94
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
95
|
+
}
|
|
96
|
+
else if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
97
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
98
|
+
}
|
|
61
99
|
}
|
|
62
100
|
async clickDashboard() {
|
|
63
101
|
await this.dashboardTab.click({ timeout: 90000 });
|
|
@@ -26,6 +26,7 @@ declare class LoginPage {
|
|
|
26
26
|
clickContinueButton(): Promise<void>;
|
|
27
27
|
fillPassword(password: string): Promise<void>;
|
|
28
28
|
clickLoginButton(): Promise<void>;
|
|
29
|
+
private throwIfCredentialsRejected;
|
|
29
30
|
login(credentials?: {
|
|
30
31
|
username?: string;
|
|
31
32
|
password?: string;
|
|
@@ -4,6 +4,7 @@ exports.LoginPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
6
|
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
7
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
7
8
|
const userNameInputValidInputSchema = {
|
|
8
9
|
_schema: {
|
|
9
10
|
maxlength: 254,
|
|
@@ -75,14 +76,33 @@ class LoginPage {
|
|
|
75
76
|
async clickLoginButton() {
|
|
76
77
|
await this.loginButton.click({ timeout: 60000 });
|
|
77
78
|
}
|
|
79
|
+
// Auth0 responds to a rejected password by re-rendering the same password
|
|
80
|
+
// step with an error message rather than navigating anywhere, so success
|
|
81
|
+
// and failure aren't told apart by a URL or element one side has and the
|
|
82
|
+
// other doesn't. Without this check, a permanently wrong password falls
|
|
83
|
+
// through into assertTestUsesCorrectOrganization's generic ~120s wait for a
|
|
84
|
+
// Console/Hub shell that is never coming, and loginWithRetry then spends
|
|
85
|
+
// its remaining attempts resubmitting the exact same doomed credentials.
|
|
86
|
+
async throwIfCredentialsRejected(username) {
|
|
87
|
+
const message = await (0, authErrorClassification_1.classifyLoginRejection)(this.page, username);
|
|
88
|
+
if (message) {
|
|
89
|
+
throw new Error(message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
78
92
|
async login(credentials = {}) {
|
|
79
93
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
94
|
+
// The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page for
|
|
95
|
+
// the tab's whole lifetime; without clearing it here, a flow that logs a
|
|
96
|
+
// different user into the same page could have getUserToken() return the
|
|
97
|
+
// PREVIOUS user's token.
|
|
98
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
80
99
|
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 200000 });
|
|
81
100
|
await this.fillUsername(username);
|
|
82
101
|
await this.clickContinueButton();
|
|
83
102
|
await this.fillPassword(password);
|
|
84
103
|
await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
|
|
85
104
|
await this.clickLoginButton();
|
|
105
|
+
await this.throwIfCredentialsRejected(username);
|
|
86
106
|
await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
|
|
87
107
|
}
|
|
88
108
|
// The identity provider's email step submits through client-side JS, so a
|
|
@@ -128,6 +148,10 @@ class LoginPage {
|
|
|
128
148
|
}
|
|
129
149
|
async loginWithTestUser(credentials = {}) {
|
|
130
150
|
const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
|
|
151
|
+
// See login() above: this re-authenticates the same page and must clear
|
|
152
|
+
// the Hub bearer-token cache too, or getUserToken() can return the
|
|
153
|
+
// previous user's cached token.
|
|
154
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
131
155
|
// Skip navigation if the auth0 login page is already loaded — re-navigating
|
|
132
156
|
// to '/' when already on auth0 undoes the redirect and causes a 60-second
|
|
133
157
|
// waitForURL race. This is the common case when called from loginWithRetry,
|
|
@@ -158,6 +182,7 @@ class LoginPage {
|
|
|
158
182
|
await this.fillPassword(password);
|
|
159
183
|
await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
|
|
160
184
|
await this.clickLoginButton();
|
|
185
|
+
await this.throwIfCredentialsRejected(username);
|
|
161
186
|
await (0, UtilitiesPage_1.assertTestUsesCorrectOrganization)(this.page);
|
|
162
187
|
}
|
|
163
188
|
async loginFailAssertion() {
|
|
@@ -170,6 +195,10 @@ class LoginPage {
|
|
|
170
195
|
}
|
|
171
196
|
async loginWithoutOrgAssertion(credentials = {}) {
|
|
172
197
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
198
|
+
// See login() above: this re-authenticates the same page and must clear
|
|
199
|
+
// the Hub bearer-token cache too, or getUserToken() can return the
|
|
200
|
+
// previous user's cached token.
|
|
201
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
173
202
|
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 180000 });
|
|
174
203
|
await this.fillUsername(username);
|
|
175
204
|
await this.clickContinueButton();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ModelerHomePage {
|
|
3
3
|
private defaultFolderName;
|
|
4
|
+
private defaultClusterName;
|
|
4
5
|
private defaultNewProjectName;
|
|
5
6
|
private page;
|
|
6
7
|
private processApplicationPage;
|
|
@@ -8,6 +9,11 @@ declare class ModelerHomePage {
|
|
|
8
9
|
private processApplicationName?;
|
|
9
10
|
readonly modelerPageBanner: Locator;
|
|
10
11
|
readonly createNewProjectButton: Locator;
|
|
12
|
+
readonly createWorkspaceButton: Locator;
|
|
13
|
+
readonly workspaceWizardNameInput: Locator;
|
|
14
|
+
readonly workspaceWizardSubmitButton: Locator;
|
|
15
|
+
readonly workspaceWizardMembersStep: Locator;
|
|
16
|
+
readonly workspaceWizardEnvironmentsHeading: Locator;
|
|
11
17
|
readonly projectNameInput: Locator;
|
|
12
18
|
readonly formNameInput: Locator;
|
|
13
19
|
readonly chooseBpmnTemplateButton: Locator;
|
|
@@ -62,11 +68,13 @@ declare class ModelerHomePage {
|
|
|
62
68
|
returnToResourceContainer(): Promise<void>;
|
|
63
69
|
startBlankBpmnDiagram(): Promise<void>;
|
|
64
70
|
private becameVisible;
|
|
71
|
+
private createWorkspaceViaWizard;
|
|
65
72
|
clickHomeBreadcrumb(): Promise<void>;
|
|
66
73
|
clickOpenOrganizationsButton(): Promise<void>;
|
|
67
74
|
createForm(formName: string): Promise<void>;
|
|
68
75
|
clickCreateIdpApplicationButton(): Promise<void>;
|
|
69
76
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
77
|
+
private waitForCrossComponentProjectFolder;
|
|
70
78
|
createCrossComponentProjectFolder(): Promise<void>;
|
|
71
79
|
clickManageButton(retries?: number): Promise<void>;
|
|
72
80
|
clickUploadFilesButton(): Promise<void>;
|
|
@@ -8,6 +8,11 @@ const randomName_1 = require("../../utils/randomName");
|
|
|
8
8
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
9
|
class ModelerHomePage {
|
|
10
10
|
defaultFolderName = 'Cross Component Test Project';
|
|
11
|
+
// The default cluster test-setup.spec.ts's "Create Default Cluster" test
|
|
12
|
+
// provisions for the whole suite -- assigned to the shared workspace's
|
|
13
|
+
// Environments step so downstream specs can deploy to it without a manual
|
|
14
|
+
// assignment step first.
|
|
15
|
+
defaultClusterName = 'Test Cluster';
|
|
11
16
|
// The name hub gives a project on create, before the inline rename commits.
|
|
12
17
|
defaultNewProjectName = 'New project';
|
|
13
18
|
page;
|
|
@@ -26,6 +31,11 @@ class ModelerHomePage {
|
|
|
26
31
|
processApplicationName;
|
|
27
32
|
modelerPageBanner;
|
|
28
33
|
createNewProjectButton;
|
|
34
|
+
createWorkspaceButton;
|
|
35
|
+
workspaceWizardNameInput;
|
|
36
|
+
workspaceWizardSubmitButton;
|
|
37
|
+
workspaceWizardMembersStep;
|
|
38
|
+
workspaceWizardEnvironmentsHeading;
|
|
29
39
|
projectNameInput;
|
|
30
40
|
formNameInput;
|
|
31
41
|
chooseBpmnTemplateButton;
|
|
@@ -61,12 +71,17 @@ class ModelerHomePage {
|
|
|
61
71
|
// Hub's design-system shell renders the app header as a role=banner with no
|
|
62
72
|
// accessible name after the Camunda Hub rebrand, so the classic
|
|
63
73
|
// name-matched banner no longer resolves; the Modeler logo link inside it
|
|
64
|
-
//
|
|
65
|
-
//
|
|
74
|
+
// carries "Camunda Hub home" when the org has workspacesEnabled, else
|
|
75
|
+
// "Camunda Modeler home" (camunda-hub NavShellDs.common.tsx) -- match
|
|
76
|
+
// either. Keep the legacy named-banner branch for shells where the rename
|
|
77
|
+
// has not shipped yet.
|
|
66
78
|
this.modelerPageBanner = page
|
|
67
79
|
.getByRole('banner', {
|
|
68
80
|
name: 'Camunda Modeler',
|
|
69
81
|
})
|
|
82
|
+
.or(page
|
|
83
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
84
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
70
85
|
.or(page
|
|
71
86
|
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
72
87
|
.and(page.locator(':not([href$="/login"])')))
|
|
@@ -83,6 +98,24 @@ class ModelerHomePage {
|
|
|
83
98
|
name: 'Create project',
|
|
84
99
|
exact: true,
|
|
85
100
|
}));
|
|
101
|
+
// Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
|
|
102
|
+
// hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
|
|
103
|
+
// wizard (General -> Members) that names the workspace up front. Same
|
|
104
|
+
// hooks as pages/SM-8.10/ModelerHomePage.ts. exact:true keeps the split
|
|
105
|
+
// button's options chevron out of the match.
|
|
106
|
+
this.createWorkspaceButton = page.getByRole('button', {
|
|
107
|
+
name: 'Create workspace',
|
|
108
|
+
exact: true,
|
|
109
|
+
});
|
|
110
|
+
this.workspaceWizardNameInput = page.locator('#workspace-name');
|
|
111
|
+
this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
|
|
112
|
+
this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
|
|
113
|
+
// When the org has environments enabled, an "Assign environments" step is
|
|
114
|
+
// inserted after Members -- camunda-hub's own
|
|
115
|
+
// e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
|
|
116
|
+
this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
|
|
117
|
+
name: 'Assign environments',
|
|
118
|
+
});
|
|
86
119
|
this.projectNameInput = page.locator('[data-test="editable-input"]');
|
|
87
120
|
this.formNameInput = page.locator('[data-test="editable-input"]');
|
|
88
121
|
this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
|
|
@@ -121,10 +154,13 @@ class ModelerHomePage {
|
|
|
121
154
|
// than one project with this name — an unscoped by-title match throws a
|
|
122
155
|
// strict-mode violation the moment a duplicate exists, which fails every
|
|
123
156
|
// later run in that org rather than just the run that created it.
|
|
157
|
+
// On the Camunda Hub Workspaces overview the folder is a workspace tile
|
|
158
|
+
// whose "Open workspace <name>" button navigates into it.
|
|
124
159
|
this.crossComponentProjectFolder = page
|
|
125
|
-
.
|
|
160
|
+
.getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
|
|
161
|
+
.or(page.getByTitle(this.defaultFolderName, {
|
|
126
162
|
exact: true,
|
|
127
|
-
})
|
|
163
|
+
}))
|
|
128
164
|
.first();
|
|
129
165
|
this.rows = page.getByRole('row');
|
|
130
166
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
@@ -582,7 +618,83 @@ class ModelerHomePage {
|
|
|
582
618
|
.then(() => true)
|
|
583
619
|
.catch(() => false);
|
|
584
620
|
}
|
|
621
|
+
// Drives Camunda Hub's workspace-creation wizard. The name is entered on
|
|
622
|
+
// the General step, which enables "Next"; the same submit button then
|
|
623
|
+
// relabels itself through the remaining steps and finally reads "Create
|
|
624
|
+
// workspace" once it creates the workspace and routes the creator into its
|
|
625
|
+
// projects listing. Gate the Members-step click on that step becoming
|
|
626
|
+
// active so the shared submit button is not clicked twice while still on
|
|
627
|
+
// General.
|
|
628
|
+
async createWorkspaceViaWizard(name) {
|
|
629
|
+
await this.dismissOverlays();
|
|
630
|
+
await this.createWorkspaceButton.click({ timeout: 60000 });
|
|
631
|
+
await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
|
|
632
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
633
|
+
await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
|
|
634
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
635
|
+
// When the org has environments enabled, this only advances the wizard
|
|
636
|
+
// into an "Assign environments" step instead of submitting. Assign the
|
|
637
|
+
// suite's default cluster here rather than submitting with an empty
|
|
638
|
+
// selection -- otherwise this shared workspace starts with no
|
|
639
|
+
// environments and every downstream spec that deploys to it needs its
|
|
640
|
+
// own manual assignment first. Each card's accessible name is
|
|
641
|
+
// "Select <cluster name>" before it is picked (camunda-hub's own
|
|
642
|
+
// create-workspace-environments.spec.tsx asserts the same pattern).
|
|
643
|
+
// isVisible() only samples the current state -- if the wizard's step
|
|
644
|
+
// transition hasn't rendered yet at that exact instant, this would
|
|
645
|
+
// wrongly conclude the org has no environments step and immediately
|
|
646
|
+
// wait for the post-submit URL while the wizard is actually still open
|
|
647
|
+
// (mid-transition), timing out every workspace creation. Give the step
|
|
648
|
+
// transition a bounded wait instead of an instantaneous probe.
|
|
649
|
+
if (await this.becameVisible(this.workspaceWizardEnvironmentsHeading, 5000)) {
|
|
650
|
+
// "Create Default Cluster" runs in parallel with this flow
|
|
651
|
+
// (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
|
|
652
|
+
// lists may still be provisioning -- wait for it a generous 90s rather
|
|
653
|
+
// than sampling once. If it genuinely never appears (the cluster
|
|
654
|
+
// failed rather than just being slow), don't fail the whole shared
|
|
655
|
+
// workspace's setup over it: Hub's own recovery copy for this step
|
|
656
|
+
// ("Could not load environments. You can finish creating the
|
|
657
|
+
// workspace and assign them later.") confirms submitting unassigned
|
|
658
|
+
// is a supported, recoverable path, not a broken one.
|
|
659
|
+
const clusterOption = this.page.getByRole('button', {
|
|
660
|
+
name: new RegExp(`^Select ${this.defaultClusterName}`),
|
|
661
|
+
});
|
|
662
|
+
if (await this.becameVisible(clusterOption, 90000)) {
|
|
663
|
+
await clusterOption.click({ timeout: 10000 });
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
|
|
667
|
+
}
|
|
668
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
669
|
+
}
|
|
670
|
+
await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
|
|
671
|
+
timeout: 120000,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
585
674
|
async clickHomeBreadcrumb() {
|
|
675
|
+
// Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
|
|
676
|
+
// overview, reached through the header logo link so the SPA keeps its
|
|
677
|
+
// in-memory session (a full navigation re-runs the whole Auth0 hand-off).
|
|
678
|
+
// The link's accessible name is "Camunda Hub home" when the org has
|
|
679
|
+
// workspacesEnabled, else "Camunda Modeler home" (camunda-hub
|
|
680
|
+
// NavShellDs.common.tsx) -- match either.
|
|
681
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
682
|
+
// The authenticated header can still expose this same link with its
|
|
683
|
+
// href pointed at /login while the header is mid-update (same race
|
|
684
|
+
// modelerPageBanner already guards against) -- clicking it then sends
|
|
685
|
+
// the flow back through Auth0 and drops the session.
|
|
686
|
+
await this.page
|
|
687
|
+
.locator('header[data-slot="app-header"]')
|
|
688
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
689
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
690
|
+
.or(this.page
|
|
691
|
+
.locator('header[data-slot="app-header"]')
|
|
692
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
693
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
694
|
+
.first()
|
|
695
|
+
.click({ timeout: 30000 });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
586
698
|
try {
|
|
587
699
|
await this.homeBreadcrumb.click();
|
|
588
700
|
}
|
|
@@ -649,38 +761,112 @@ class ModelerHomePage {
|
|
|
649
761
|
},
|
|
650
762
|
});
|
|
651
763
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
764
|
+
// Hub's "Create workspace" CTA is a persistent toolbar button, rendered
|
|
765
|
+
// regardless of whether the existing-workspaces list has finished loading
|
|
766
|
+
// -- so a single slow or failed list fetch must not be read as "no
|
|
767
|
+
// workspace exists yet". Retry the wait once after a reload before
|
|
768
|
+
// concluding the folder is genuinely absent, since creating one when it
|
|
769
|
+
// already exists (just hadn't rendered) leaves a permanent duplicate that
|
|
770
|
+
// breaks every later run for that user.
|
|
771
|
+
async waitForCrossComponentProjectFolder() {
|
|
772
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
773
|
+
const found = await this.crossComponentProjectFolder
|
|
774
|
+
.waitFor({ state: 'visible', timeout: 60000 })
|
|
775
|
+
.then(() => true)
|
|
776
|
+
.catch(() => false);
|
|
777
|
+
if (found)
|
|
778
|
+
return true;
|
|
779
|
+
if (attempt === 0) {
|
|
780
|
+
await this.page.reload();
|
|
781
|
+
await this.page.waitForLoadState('networkidle').catch(() => { });
|
|
782
|
+
}
|
|
668
783
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
// Retries the whole detect-or-create sequence, not just its individual
|
|
787
|
+
// steps. Up to 27 test-user workers race to create/open this one shared
|
|
788
|
+
// workspace concurrently, and a losing worker can land on a workspace-root
|
|
789
|
+
// page that never finishes rendering (neither the folder tile nor a create
|
|
790
|
+
// control ever becomes visible) for reasons the per-step probes/reloads
|
|
791
|
+
// below cannot resolve on their own -- each step bounds itself, but their
|
|
792
|
+
// sum does not bound the whole method, and a single unlucky pass used to
|
|
793
|
+
// run long enough to hit the 12-minute test timeout outright (run
|
|
794
|
+
// 36110457453 lost 6 "Create Project Folder" tests this way, pending
|
|
795
|
+
// inside a reload with no error of its own -- the test timeout fired
|
|
796
|
+
// first). Mirrors the 8.9 sibling's outer retry loop, which this method
|
|
797
|
+
// lost when it grew its own per-step reload-retries in place of it.
|
|
798
|
+
async createCrossComponentProjectFolder() {
|
|
799
|
+
const maxAttempts = 3;
|
|
800
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
801
|
+
await this.dismissOverlays();
|
|
802
|
+
// The projects list renders asynchronously, so an instantaneous
|
|
803
|
+
// isVisible() reports "missing" whenever the list is still loading — and
|
|
804
|
+
// this method then creates a SECOND project with the same name. Those
|
|
805
|
+
// duplicates are permanent and break every subsequent run for that user,
|
|
806
|
+
// so wait for the folder to actually show up before concluding it is
|
|
807
|
+
// absent (this also replaces the blind 15s sleep the check relied on).
|
|
808
|
+
// Re-checked on every attempt: a sibling worker may finish creating the
|
|
809
|
+
// folder while this one is retrying.
|
|
810
|
+
const folderExists = await this.waitForCrossComponentProjectFolder();
|
|
811
|
+
if (folderExists) {
|
|
812
|
+
console.log('Cross Component Project folder already exists. Clicking into it');
|
|
813
|
+
await this.clickCrossComponentProjectFolder();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
// Same joint-probe pattern as clickCreateProcessApplication above: a
|
|
818
|
+
// single ORed probe with a generous timeout settles which shell
|
|
819
|
+
// actually rendered (Hub's "Create workspace" empty state vs. the
|
|
820
|
+
// legacy "New project" / "Create project" button) before a short
|
|
821
|
+
// confirmation decides the branch. A live CI trace showed probing
|
|
822
|
+
// createWorkspaceButton alone for 5s racing Hub's slower loads under
|
|
823
|
+
// worker contention: the probe reported "absent" on a workspaces
|
|
824
|
+
// overview that was merely still loading, sending the flow down the
|
|
825
|
+
// legacy branch -- which that overview never renders.
|
|
826
|
+
const rootLoaded = await this.becameVisible(this.createWorkspaceButton.or(this.createNewProjectButton).first(), 60000);
|
|
827
|
+
if (!rootLoaded) {
|
|
828
|
+
// Mirrors clickCreateProcessApplication's own reload-retry: a reload
|
|
829
|
+
// gives one more 60s window in case the root was still loading
|
|
830
|
+
// rather than genuinely absent, instead of committing to the wrong
|
|
831
|
+
// branch.
|
|
832
|
+
await this.page.reload();
|
|
833
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
834
|
+
await this.becameVisible(this.createWorkspaceButton.or(this.createNewProjectButton).first(), 60000);
|
|
835
|
+
}
|
|
836
|
+
if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
|
|
837
|
+
await this.createWorkspaceViaWizard(this.defaultFolderName);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
await this.clickCreateNewProjectButton();
|
|
841
|
+
await this.enterNewProjectName(this.defaultFolderName);
|
|
842
|
+
// Both branches must leave the browser inside the workspace: the
|
|
843
|
+
// callers go straight on to clickDiagramTypeDropdown(), whose create
|
|
844
|
+
// control only exists there. Creating normally navigates into the new
|
|
845
|
+
// project, but when hub leaves the browser on the projects list the
|
|
846
|
+
// rename happens in the row and no navigation follows.
|
|
847
|
+
//
|
|
848
|
+
// Keyed on "New project", which hub renders only on the projects list
|
|
849
|
+
// and never inside an open project, so it answers which view we are
|
|
850
|
+
// on directly. The folder tile is the wrong signal: on a slow list
|
|
851
|
+
// render it is still absent, which would read as "already inside" and
|
|
852
|
+
// skip a hop that was needed.
|
|
853
|
+
if (await this.becameVisible(this.createNewProjectButton, 5000)) {
|
|
854
|
+
await this.clickCrossComponentProjectFolder();
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (attempt >= maxAttempts - 1) {
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
// The stuck-root case above, or a wizard/rename step that threw, can
|
|
863
|
+
// leave the browser on a half-loaded or errored page that retrying
|
|
864
|
+
// in place cannot recover from. Return Home so the next attempt's
|
|
865
|
+
// existence check starts clean, instead of retrying against the same
|
|
866
|
+
// stuck page.
|
|
867
|
+
await this.clickHomeBreadcrumb().catch(() => { });
|
|
868
|
+
console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed; retrying...`);
|
|
869
|
+
}
|
|
684
870
|
}
|
|
685
871
|
}
|
|
686
872
|
async clickManageButton(retries = 3) {
|
|
@@ -10,6 +10,7 @@ const authErrorClassification_1 = require("../../utils/authErrorClassification")
|
|
|
10
10
|
const LoginPage_1 = require("./LoginPage");
|
|
11
11
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
12
12
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
13
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
13
14
|
async function clearWebStorage(page) {
|
|
14
15
|
await page
|
|
15
16
|
.evaluate(() => {
|
|
@@ -101,6 +102,27 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
101
102
|
// stagger sleep below, so a worker only pays it once per process lifetime
|
|
102
103
|
// rather than on every call -- see the comment at the call site.
|
|
103
104
|
const staggeredWorkerTimeouts = new Set();
|
|
105
|
+
const onboardingTourSuppressed = new WeakSet();
|
|
106
|
+
// Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
|
|
107
|
+
// (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
|
|
108
|
+
// "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
|
|
109
|
+
// hides the post-login shell from the accessibility tree until dismissed, which times out
|
|
110
|
+
// assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
|
|
111
|
+
// script on every navigation/reload, so there's no login-response race to lose; the flag is
|
|
112
|
+
// matched by prefix since the user id isn't known ahead of login.
|
|
113
|
+
async function suppressOnboardingTour(page) {
|
|
114
|
+
if (onboardingTourSuppressed.has(page)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
onboardingTourSuppressed.add(page);
|
|
118
|
+
await page.addInitScript((prefixPattern) => {
|
|
119
|
+
const pattern = new RegExp(prefixPattern);
|
|
120
|
+
const originalGetItem = Storage.prototype.getItem;
|
|
121
|
+
Storage.prototype.getItem = function (key) {
|
|
122
|
+
return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
|
|
123
|
+
};
|
|
124
|
+
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
125
|
+
}
|
|
104
126
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
|
|
105
127
|
// A blank credential is not something retrying can fix, and from inside the
|
|
106
128
|
// browser it does not look like a credential problem at all: the form takes
|
|
@@ -131,6 +153,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
131
153
|
'"Skip output ... since it may contain secret" warning on the ' +
|
|
132
154
|
'matching C8_*_PASSWORD output.');
|
|
133
155
|
}
|
|
156
|
+
await suppressOnboardingTour(page);
|
|
134
157
|
let lastError;
|
|
135
158
|
// `timeout` is a stable per-worker identifier (derived from workerIndex),
|
|
136
159
|
// and a worker is a long-lived process across every test/repeat it runs, so
|
|
@@ -142,6 +165,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
142
165
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
143
166
|
try {
|
|
144
167
|
await page.context().clearCookies();
|
|
168
|
+
// The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
|
|
169
|
+
// for the tab's whole lifetime; without clearing it here alongside
|
|
170
|
+
// cookies/storage, a flow that logs a different user into the same
|
|
171
|
+
// page could have getUserToken() return the PREVIOUS user's token.
|
|
172
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
145
173
|
await page.goto('about:blank');
|
|
146
174
|
await page
|
|
147
175
|
.evaluate(() => {
|
|
@@ -196,6 +224,17 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
196
224
|
}
|
|
197
225
|
catch (error) {
|
|
198
226
|
lastError = error;
|
|
227
|
+
// A rejected password or a blocked account is a credential/
|
|
228
|
+
// provisioning-level failure, not a transient one: every attempt
|
|
229
|
+
// resubmits the same env-supplied credentials, so retrying just
|
|
230
|
+
// reproduces the same rejection up to maxRetries times (observed:
|
|
231
|
+
// ~6 minutes burned across 5 identical "Wrong email or password"
|
|
232
|
+
// cycles for one seeded user). Fail on the first rejection instead.
|
|
233
|
+
if (String(error).includes(authErrorClassification_1.AUTH0_INVALID_CREDENTIALS) ||
|
|
234
|
+
String(error).includes(authErrorClassification_1.AUTH0_ACCOUNT_BLOCKED)) {
|
|
235
|
+
console.error(lastError);
|
|
236
|
+
throw lastError;
|
|
237
|
+
}
|
|
199
238
|
if (attempt < maxRetries - 1) {
|
|
200
239
|
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
201
240
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
@@ -731,8 +770,12 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
731
770
|
const homePage = new HomePage_1.HomePage(page);
|
|
732
771
|
if (page.url().includes('?code=') || page.url().includes('&state=')) {
|
|
733
772
|
console.log('OAuth callback detected, waiting for redirect...');
|
|
773
|
+
// Camunda Hub (which replaced Console on SaaS) only exchanges the code
|
|
774
|
+
// once its SPA has booted, and on CI runners that boot alone was measured
|
|
775
|
+
// at ~45s past /login-callback -- a 30s wait aborted the exchange on every
|
|
776
|
+
// attempt.
|
|
734
777
|
await page.waitForURL((url) => !url.toString().includes('?code='), {
|
|
735
|
-
timeout:
|
|
778
|
+
timeout: 120000,
|
|
736
779
|
});
|
|
737
780
|
await (0, sleep_1.sleep)(5000);
|
|
738
781
|
}
|
|
@@ -742,7 +785,20 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
742
785
|
await (0, test_1.expect)(homePage.modelerBanner).toBeVisible({ timeout: 30000 });
|
|
743
786
|
}
|
|
744
787
|
else {
|
|
745
|
-
await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({
|
|
788
|
+
await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
|
|
789
|
+
timeout: 60000,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// Camunda Hub replaces Console on SaaS (8.10 release notes), and the Hub
|
|
793
|
+
// shell has neither the "Camunda logo Console" link nor the "Open
|
|
794
|
+
// Organizations" switcher, and lands on /workspaces without the org id in
|
|
795
|
+
// the URL. Its sidebar scopes the Clusters/Organization links to the active
|
|
796
|
+
// org, so assert the expected org id there instead.
|
|
797
|
+
if (await homePage.hubHeader.isVisible()) {
|
|
798
|
+
await (0, test_1.expect)(homePage.hubSidebar
|
|
799
|
+
.locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
|
|
800
|
+
.first()).toBeVisible({ timeout: 180000 });
|
|
801
|
+
return;
|
|
746
802
|
}
|
|
747
803
|
await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
|
|
748
804
|
try {
|
|
@@ -18,7 +18,8 @@ _8_10_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_10_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
|
|
22
23
|
_8_10_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
|
|
23
24
|
_8_10_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);
|
|
@@ -165,7 +166,9 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
|
|
|
165
166
|
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, Math.min((testInfo.workerIndex + 1) * 5000, 60000));
|
|
166
167
|
});
|
|
167
168
|
await _8_10_1.test.step('Navigate to Web Modeler', async () => {
|
|
168
|
-
|
|
169
|
+
// Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
|
|
170
|
+
// Modeler home directly and has no app switcher to wait for.
|
|
171
|
+
await (0, test_1.expect)(homePage.camundaComponentsButton.or(homePage.hubHeader)).toBeVisible({
|
|
169
172
|
timeout: 120000,
|
|
170
173
|
});
|
|
171
174
|
await appsPage.openModelerWithRetry(modelerHomePage.modelerPageBanner, 180000, 3, undefined, () => modelerHomePage.dismissBlockingDialog());
|