@camunda/e2e-test-suite 0.0.1253 → 0.0.1255
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/pages/8.7/AppsPage.js +65 -11
- package/dist/pages/8.7/ClusterPage.js +22 -20
- package/dist/pages/8.7/ConsoleOrganizationPage.js +37 -6
- package/dist/pages/8.7/FormJsPage.js +36 -9
- package/dist/pages/8.7/LoginPage.js +4 -0
- package/dist/pages/8.7/ModelerCreatePage.js +72 -24
- package/dist/tests/8.10/test-setup.spec.js +5 -2
- package/dist/tests/8.7/test-setup.spec.js +16 -0
- 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>;
|