@camunda/e2e-test-suite 0.0.1277 → 0.0.1279
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.7/AppsPage.js +15 -7
- package/dist/pages/8.7/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.7/ClusterDetailsPage.js +15 -0
- package/dist/pages/8.7/ClusterSecretsPage.js +9 -2
- package/dist/pages/8.7/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/8.7/ConnectorMarketplacePage.js +27 -3
- package/dist/pages/8.7/LoginPage.js +26 -4
- package/dist/pages/8.7/UtilitiesPage.js +6 -1
- package/dist/pages/8.8/AppsPage.d.ts +0 -1
- package/dist/pages/8.8/AppsPage.js +15 -37
- package/dist/pages/8.8/WorkspaceMembersPage.d.ts +20 -0
- package/dist/pages/8.8/WorkspaceMembersPage.js +258 -0
- package/dist/tests/8.7/connectors-user-flows.spec.js +3 -1
- package/dist/tests/8.8/access-user-role-management.spec.js +95 -0
- package/package.json +1 -1
|
@@ -83,8 +83,14 @@ class AppsPage {
|
|
|
83
83
|
// (see clickHubClusterApp and isHubClusterAppPresent below) gets at most
|
|
84
84
|
// one match, scoped to this cluster.
|
|
85
85
|
async expandHubClusterGroup(clusterName) {
|
|
86
|
+
// Anchored at the start: an unanchored substring match on "Test Cluster"
|
|
87
|
+
// also matches "UI Test Cluster" (created by console-user-flows in the
|
|
88
|
+
// same org), and .first() then opened that cluster's popover instead --
|
|
89
|
+
// a nightly trace shows the "UI Test Cluster" group open while the
|
|
90
|
+
// "Tasklist"/"Operate" lookup for "Test Cluster" timed out.
|
|
91
|
+
const clusterNamePattern = new RegExp(`^${escapeRegExp(clusterName)}\\b`);
|
|
86
92
|
const toggle = this.hubSidebar
|
|
87
|
-
.getByRole('button', { name:
|
|
93
|
+
.getByRole('button', { name: clusterNamePattern })
|
|
88
94
|
.first();
|
|
89
95
|
// The diagram editor (and other full-screen views reached from it, e.g.
|
|
90
96
|
// right after runProcessInstance/deployDiagram) has no "Main navigation"
|
|
@@ -165,14 +171,16 @@ class AppsPage {
|
|
|
165
171
|
// of its own, in which case hasNotText would fail to exclude the toggle
|
|
166
172
|
// we just expanded and this loop would immediately collapse it again.
|
|
167
173
|
// Match by accessible name (same algorithm getByRole's own name option
|
|
168
|
-
// uses) instead, excluding any name
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
174
|
+
// uses) instead, excluding any name STARTING WITH clusterName (the same
|
|
175
|
+
// anchoring as the toggle locator, so "UI Test Cluster" still counts as
|
|
176
|
+
// another group) -- not just one exactly equal to it, since (per the
|
|
177
|
+
// toggle locator above) the current cluster's own toggle name is not
|
|
178
|
+
// exactly clusterName either, and an exact-equality exclusion would
|
|
179
|
+
// wrongly treat it as "other" and immediately re-collapse the group this
|
|
180
|
+
// method just expanded.
|
|
173
181
|
const otherExpandedToggles = this.hubSidebar.getByRole('button', {
|
|
174
182
|
expanded: true,
|
|
175
|
-
name: new RegExp(`^(
|
|
183
|
+
name: new RegExp(`^(?!${escapeRegExp(clusterName)}\\b).+$`),
|
|
176
184
|
});
|
|
177
185
|
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
178
186
|
// Let a failed collapse throw rather than swallowing it: the app link
|
|
@@ -62,6 +62,7 @@ declare class ClusterDetailsPage {
|
|
|
62
62
|
clickCreateButton(): Promise<void>;
|
|
63
63
|
clickCloseModalButton(): Promise<void>;
|
|
64
64
|
clickSettingsTab(): Promise<void>;
|
|
65
|
+
private isRBAToggleInState;
|
|
65
66
|
enableRBA(): Promise<void>;
|
|
66
67
|
disableRBA(): Promise<void>;
|
|
67
68
|
searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
|
|
@@ -234,9 +234,21 @@ class ClusterDetailsPage {
|
|
|
234
234
|
async clickSettingsTab() {
|
|
235
235
|
await this.settingsTab.click({ timeout: 60000 });
|
|
236
236
|
}
|
|
237
|
+
// The switch reports its state through aria-checked. Clicking it
|
|
238
|
+
// unconditionally opened the opposite confirmation dialog whenever the
|
|
239
|
+
// cluster was already in the requested state (e.g. "Ensure RBA is
|
|
240
|
+
// Disabled" on a cluster that never had it on), so the Enable/Disable
|
|
241
|
+
// dialog wait below timed out. Returns false when the state is unknown.
|
|
242
|
+
async isRBAToggleInState(enabled) {
|
|
243
|
+
const checked = await this.rbaToggle.getAttribute('aria-checked');
|
|
244
|
+
return checked !== null && (checked === 'true') === enabled;
|
|
245
|
+
}
|
|
237
246
|
async enableRBA() {
|
|
238
247
|
await (0, test_1.expect)(this.rbaHeading).toBeVisible({ timeout: 60000 });
|
|
239
248
|
await (0, test_1.expect)(this.rbaToggle).toBeVisible({ timeout: 30000 });
|
|
249
|
+
if (await this.isRBAToggleInState(true)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
240
252
|
await this.rbaToggle.click();
|
|
241
253
|
await (0, test_1.expect)(this.enableRBADialog).toBeVisible({ timeout: 60000 });
|
|
242
254
|
await this.rbaEnableButton.click();
|
|
@@ -247,6 +259,9 @@ class ClusterDetailsPage {
|
|
|
247
259
|
async disableRBA() {
|
|
248
260
|
await (0, test_1.expect)(this.rbaHeading).toBeVisible({ timeout: 60000 });
|
|
249
261
|
await (0, test_1.expect)(this.rbaToggle).toBeVisible({ timeout: 30000 });
|
|
262
|
+
if (await this.isRBAToggleInState(false)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
250
265
|
await this.rbaToggle.click();
|
|
251
266
|
await (0, test_1.expect)(this.disableRBADialog).toBeVisible({ timeout: 60000 });
|
|
252
267
|
await this.rbaDisableButton.click();
|
|
@@ -31,9 +31,16 @@ class ClusterSecretsPage {
|
|
|
31
31
|
this.connectorSecretsTable = page.getByRole('table', {
|
|
32
32
|
name: 'Connector secrets',
|
|
33
33
|
});
|
|
34
|
+
// Legacy Carbon table: an "Options" cell holding the kebab. Hub's
|
|
35
|
+
// design-system data table instead labels each row's kebab
|
|
36
|
+
// "Row actions" (data-slot="data-table-actions-trigger"), with no
|
|
37
|
+
// "Options" cell -- so the old locator found nothing, the pre-clean
|
|
38
|
+
// returned early, and re-creating an existing secret failed with
|
|
39
|
+
// "Secret names must be unique".
|
|
34
40
|
this.optionsButton = page
|
|
35
41
|
.getByRole('cell', { name: 'Options' })
|
|
36
|
-
.getByRole('button')
|
|
42
|
+
.getByRole('button')
|
|
43
|
+
.or(page.getByRole('button', { name: 'Row actions', exact: true }));
|
|
37
44
|
this.deleteConnectorSecretButton = page.getByRole('menuitem', {
|
|
38
45
|
name: 'Delete',
|
|
39
46
|
exact: true,
|
|
@@ -124,7 +131,7 @@ class ClusterSecretsPage {
|
|
|
124
131
|
// that nothing *deletable* is left behind.
|
|
125
132
|
const deletableSecretRows = this.page
|
|
126
133
|
.getByRole('row')
|
|
127
|
-
.filter({ has: this.
|
|
134
|
+
.filter({ has: this.optionsButton })
|
|
128
135
|
.filter({ hasNotText: 'CAMUNDA_PROVIDED_' });
|
|
129
136
|
await (0, test_1.expect)(deletableSecretRows).toHaveCount(0, { timeout: 30000 });
|
|
130
137
|
}
|
|
@@ -7,6 +7,7 @@ declare class ConnectorMarketplacePage {
|
|
|
7
7
|
readonly replaceResourceButton: Locator;
|
|
8
8
|
readonly addToProjectButton: Locator;
|
|
9
9
|
readonly cancelButton: Locator;
|
|
10
|
+
readonly tryAgainButton: Locator;
|
|
10
11
|
readonly closeButton: Locator;
|
|
11
12
|
readonly browseAllMarketplaceDetailsButton: Locator;
|
|
12
13
|
constructor(page: Page);
|
|
@@ -10,6 +10,7 @@ class ConnectorMarketplacePage {
|
|
|
10
10
|
replaceResourceButton;
|
|
11
11
|
addToProjectButton;
|
|
12
12
|
cancelButton;
|
|
13
|
+
tryAgainButton;
|
|
13
14
|
closeButton;
|
|
14
15
|
browseAllMarketplaceDetailsButton;
|
|
15
16
|
constructor(page) {
|
|
@@ -38,6 +39,7 @@ class ConnectorMarketplacePage {
|
|
|
38
39
|
name: 'Add to project',
|
|
39
40
|
});
|
|
40
41
|
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
42
|
+
this.tryAgainButton = page.getByRole('button', { name: 'Try again' });
|
|
41
43
|
this.closeButton = page.getByRole('button', { name: 'Close' });
|
|
42
44
|
}
|
|
43
45
|
async clickSearchForConnectorTextbox() {
|
|
@@ -72,11 +74,33 @@ class ConnectorMarketplacePage {
|
|
|
72
74
|
// genuine timeout here throw rather than silently falling through to the
|
|
73
75
|
// immediate isVisible() probes below, which would just repeat the same
|
|
74
76
|
// race this wait exists to close.
|
|
75
|
-
|
|
77
|
+
//
|
|
78
|
+
// The Browse all review step first "checks this project" (camunda-hub
|
|
79
|
+
// marketplace-connector-detail-state.ts), which waits on the project's
|
|
80
|
+
// element-template metadata request -- nightly and live traces show that
|
|
81
|
+
// request still pending 30s in. When the check fails the footer offers
|
|
82
|
+
// "Try again" (RETRY_INSPECTION) instead of "Add to project", so give the
|
|
83
|
+
// check longer and re-run it on failure rather than timing out.
|
|
84
|
+
const outcome = this.addToProjectButton
|
|
76
85
|
.or(this.replaceResourceButton)
|
|
77
86
|
.or(this.cancelButton)
|
|
78
|
-
.first()
|
|
79
|
-
|
|
87
|
+
.first();
|
|
88
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
89
|
+
await outcome
|
|
90
|
+
.or(this.tryAgainButton)
|
|
91
|
+
.first()
|
|
92
|
+
.waitFor({ state: 'visible', timeout: 120000 });
|
|
93
|
+
if (await outcome.isVisible()) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// The retrying check keeps "Try again" on screen (as a loading button),
|
|
97
|
+
// so wait for its result here instead of re-polling the union above.
|
|
98
|
+
await this.tryAgainButton.click({ timeout: 10000 });
|
|
99
|
+
await outcome
|
|
100
|
+
.waitFor({ state: 'visible', timeout: 120000 })
|
|
101
|
+
.catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
await outcome.waitFor({ state: 'visible', timeout: 10000 });
|
|
80
104
|
let imported = false;
|
|
81
105
|
// A fresh download shows "Add to project"; an already-present resource
|
|
82
106
|
// shows "Replace resource". Pick whichever the ImportModal rendered —
|
|
@@ -77,8 +77,20 @@ class LoginPage {
|
|
|
77
77
|
// before) must clear it too, or getUserToken() can return the previous
|
|
78
78
|
// user's cached token.
|
|
79
79
|
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
80
|
-
// Navigate to app root to start the auth flow.
|
|
81
|
-
|
|
80
|
+
// Navigate to app root to start the auth flow. Callers usually arrive
|
|
81
|
+
// here straight from a "Log out" click, whose own redirect chain (Hub's
|
|
82
|
+
// /login?returnUrl=/logout -> Auth0 logout) can still be in flight and
|
|
83
|
+
// interrupt this goto. Let that navigation land, then start over once.
|
|
84
|
+
try {
|
|
85
|
+
await this.page.goto('/');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!String(error).includes('is interrupted by another navigation')) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
await this.page.waitForLoadState('domcontentloaded', { timeout: 60000 });
|
|
92
|
+
await this.page.goto('/');
|
|
93
|
+
}
|
|
82
94
|
await this.page
|
|
83
95
|
.waitForURL((url) => !url.hostname.includes('console'), { timeout: 60000 })
|
|
84
96
|
.catch(() => { });
|
|
@@ -86,8 +98,18 @@ class LoginPage {
|
|
|
86
98
|
// logout/session-reset, keeping the page on Console instead of redirecting
|
|
87
99
|
// to the Auth0 login form. Detect Console (settings button visible) and
|
|
88
100
|
// force logout until the Auth0 email input appears.
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
// Hub's design-system shell has no "Open Settings" button: its user menu
|
|
102
|
+
// is a "User menu, <full name>" button whose "Log out" is a menuitem (see
|
|
103
|
+
// SettingsPage). Without these the loop never noticed it had been
|
|
104
|
+
// silently re-authenticated on Hub and waited out the username input.
|
|
105
|
+
const settingsBtn = this.page
|
|
106
|
+
.getByLabel('Open Settings')
|
|
107
|
+
.or(this.page.getByRole('button', { name: /^User menu,/ }))
|
|
108
|
+
.first();
|
|
109
|
+
const logoutBtn = this.page
|
|
110
|
+
.getByRole('button', { name: 'Log out' })
|
|
111
|
+
.or(this.page.getByRole('menuitem', { name: 'Log out' }))
|
|
112
|
+
.first();
|
|
91
113
|
for (let i = 0; i < 3; i++) {
|
|
92
114
|
const onAuth0 = await this.usernameInput
|
|
93
115
|
.waitFor({ state: 'visible', timeout: 15000 })
|
|
@@ -8,6 +8,7 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
8
8
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
9
9
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
10
10
|
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
11
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
11
12
|
// Tracks which worker-derived `timeout` values have already paid the
|
|
12
13
|
// pre-flight stagger below, so a worker only pays it once per process
|
|
13
14
|
// lifetime rather than on every call -- see the comment at the call sites.
|
|
@@ -601,7 +602,11 @@ async function modelAndRunConnectorsDocHandlingDiagram(modelerCreatePage, modele
|
|
|
601
602
|
});
|
|
602
603
|
await connectorMarketplacePage.downloadConnectorToProject();
|
|
603
604
|
await (0, sleep_1.sleep)(5000);
|
|
604
|
-
|
|
605
|
+
// A live run showed this reload bounce through Hub's /login?returnUrl=...
|
|
606
|
+
// and never reach "load" -- the stuck-session case recoverStuckModelerLogin
|
|
607
|
+
// exists for. Wait for the document only, then recover if it is stuck.
|
|
608
|
+
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
609
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(page);
|
|
605
610
|
await (0, sleep_1.sleep)(5000);
|
|
606
611
|
await modelerCreatePage.fillStartEventVariablesForDocHandling(zeebeUrl, zeebeClientId, zeebeClientSecret);
|
|
607
612
|
await modelerCreatePage.runProcessInstance(clusterName);
|
|
@@ -22,7 +22,6 @@ declare class AppsPage {
|
|
|
22
22
|
constructor(page: Page);
|
|
23
23
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
24
24
|
private expandHubClusterGroup;
|
|
25
|
-
private expandHubSidebar;
|
|
26
25
|
private showAllWorkspaces;
|
|
27
26
|
private clickHubClusterApp;
|
|
28
27
|
private isOnHub;
|
|
@@ -113,11 +113,6 @@ class AppsPage {
|
|
|
113
113
|
// /workspaces, right after a mid-test re-login. Route through the same
|
|
114
114
|
// clickModeler() recovery whenever the toggle itself hasn't shown up
|
|
115
115
|
// either, not only when the whole sidebar is missing.
|
|
116
|
-
// Before deciding whether the group is reachable: a collapsed rail hides
|
|
117
|
-
// every cluster trigger, so the probe below would report "not ready" and
|
|
118
|
-
// the recovery would then wait out its own timeout on the same hidden
|
|
119
|
-
// element.
|
|
120
|
-
await this.expandHubSidebar();
|
|
121
116
|
const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
|
|
122
117
|
(await toggle
|
|
123
118
|
.waitFor({ state: 'visible', timeout: 10000 })
|
|
@@ -126,18 +121,21 @@ class AppsPage {
|
|
|
126
121
|
if (!sidebarReady) {
|
|
127
122
|
await this.clickModeler();
|
|
128
123
|
await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
124
|
+
// Hub's Workspaces overview defaults to "My workspaces" -- only the
|
|
125
|
+
// ones the current user is an EXPLICIT member of -- and resets to it on
|
|
126
|
+
// every load (camunda-hub #28956). That default is what a real user
|
|
127
|
+
// sees, so leave it alone and widen only when this cluster's group is
|
|
128
|
+
// genuinely not in it: the shared workspace is created by whichever
|
|
129
|
+
// setup user won the race, so a role-flow user otherwise lands on "You
|
|
130
|
+
// don't have explicit membership in any workspace in this
|
|
131
|
+
// organization" and the group never renders.
|
|
132
|
+
const groupUnderDefaultScope = await toggle
|
|
133
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
134
|
+
.then(() => true)
|
|
135
|
+
.catch(() => false);
|
|
136
|
+
if (!groupUnderDefaultScope) {
|
|
137
|
+
await this.showAllWorkspaces(5000);
|
|
138
|
+
}
|
|
141
139
|
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
|
|
142
140
|
}
|
|
143
141
|
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
|
|
@@ -211,26 +209,6 @@ class AppsPage {
|
|
|
211
209
|
// Apps -> Modeler navigation path circular. The control is absent outside
|
|
212
210
|
// Hub and for non-elevated users, where a membership-scoped list is already
|
|
213
211
|
// correct, so a missing control is a no-op.
|
|
214
|
-
// Opens Hub's collapsed left rail. While collapsed it shows only the
|
|
215
|
-
// top-level nav icons: the per-cluster group triggers are in the DOM
|
|
216
|
-
// (data-slot="app-sidebar-group-trigger", aria-label="<cluster>") but never
|
|
217
|
-
// visible, so a toBeVisible() wait on one can only time out. A failing run's
|
|
218
|
-
// trace carries "Expand sidebar" seven times and "Collapse sidebar" not
|
|
219
|
-
// once, i.e. the rail stayed shut for its whole duration. A no-op when the
|
|
220
|
-
// rail is already open, since the toggle is then labelled "Collapse sidebar".
|
|
221
|
-
async expandHubSidebar() {
|
|
222
|
-
const expandToggle = this.page
|
|
223
|
-
.getByRole('button', { name: 'Expand sidebar' })
|
|
224
|
-
.or(this.page
|
|
225
|
-
.locator('[data-slot="app-sidebar-collapse-toggle"]')
|
|
226
|
-
.and(this.page.getByLabel('Expand sidebar')))
|
|
227
|
-
.first();
|
|
228
|
-
if (!(await expandToggle.isVisible().catch(() => false))) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
await expandToggle.click({ timeout: 10000 });
|
|
232
|
-
await (0, test_1.expect)(expandToggle).toBeHidden({ timeout: 10000 });
|
|
233
|
-
}
|
|
234
212
|
async showAllWorkspaces(probeTimeout) {
|
|
235
213
|
// The empty state offers an explicit "Switch to All workspaces" button
|
|
236
214
|
// next to "You don't have explicit membership in any workspace in this
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test';
|
|
2
|
+
type WorkspaceRole = 'Workspace Admin' | 'Editor' | 'Commenter' | 'Viewer';
|
|
3
|
+
declare class WorkspaceMembersPage {
|
|
4
|
+
private page;
|
|
5
|
+
readonly membersTab: Locator;
|
|
6
|
+
readonly manageMembersHeading: Locator;
|
|
7
|
+
readonly addMembersButton: Locator;
|
|
8
|
+
readonly addMemberSearchbox: Locator;
|
|
9
|
+
readonly addMemberSubmitButton: Locator;
|
|
10
|
+
constructor(page: Page);
|
|
11
|
+
showAllWorkspaces(): Promise<void>;
|
|
12
|
+
openManageMembers(workspaceName: string): Promise<void>;
|
|
13
|
+
private memberRow;
|
|
14
|
+
isMember(email: string): Promise<boolean>;
|
|
15
|
+
addMember(email: string, role: WorkspaceRole): Promise<void>;
|
|
16
|
+
private selectRoleIn;
|
|
17
|
+
setMemberRole(email: string, role: WorkspaceRole): Promise<void>;
|
|
18
|
+
removeMember(email: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export { WorkspaceMembersPage, WorkspaceRole };
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkspaceMembersPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
// Camunda Hub's workspace "Manage members" view: Members / Environments /
|
|
6
|
+
// General tabs over a member table of Name | Email | Workspace role | Joined,
|
|
7
|
+
// an "Add members" panel, and a role cell that becomes a select once clicked.
|
|
8
|
+
//
|
|
9
|
+
// This page matters to the role flows because an organization role alone no
|
|
10
|
+
// longer decides what a non-admin can reach. Since camunda-hub's environments
|
|
11
|
+
// work, an environment -- and so the cluster app behind it -- is visible only
|
|
12
|
+
// to a user who is Editor or Workspace Admin on a workspace it is assigned to
|
|
13
|
+
// (pdp-3715); organization admins and owners see every environment.
|
|
14
|
+
class WorkspaceMembersPage {
|
|
15
|
+
page;
|
|
16
|
+
membersTab;
|
|
17
|
+
manageMembersHeading;
|
|
18
|
+
addMembersButton;
|
|
19
|
+
addMemberSearchbox;
|
|
20
|
+
addMemberSubmitButton;
|
|
21
|
+
constructor(page) {
|
|
22
|
+
this.page = page;
|
|
23
|
+
this.membersTab = page.getByRole('tab', { name: 'Members' });
|
|
24
|
+
this.manageMembersHeading = page.getByRole('heading', {
|
|
25
|
+
name: 'Manage members',
|
|
26
|
+
});
|
|
27
|
+
this.addMembersButton = page
|
|
28
|
+
.locator('[data-test="add-members-button"]')
|
|
29
|
+
.or(page.getByRole('button', { name: 'Add members' }))
|
|
30
|
+
.first();
|
|
31
|
+
// A cmdk combobox: <input cmdk-input role="combobox"
|
|
32
|
+
// aria-label="Search members" placeholder="Search members or enter an
|
|
33
|
+
// email to invite">. Prefer the accessible name over the placeholder,
|
|
34
|
+
// which is the longer and more churn-prone of the two.
|
|
35
|
+
this.addMemberSearchbox = page
|
|
36
|
+
.getByRole('combobox', { name: 'Search members' })
|
|
37
|
+
.or(page.getByPlaceholder('Search members or enter an email to invite'))
|
|
38
|
+
.first();
|
|
39
|
+
// `exact` matters: the panel's own trigger is "Add members", which a
|
|
40
|
+
// substring match on "Add" would also resolve.
|
|
41
|
+
this.addMemberSubmitButton = page.getByRole('button', {
|
|
42
|
+
name: 'Add',
|
|
43
|
+
exact: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Hub's Workspaces overview defaults to "My workspaces" -- only the ones the
|
|
47
|
+
// current user is an explicit member of -- and resets to it on every load,
|
|
48
|
+
// so a workspace created by another user is invisible until the scope is
|
|
49
|
+
// widened. The empty state offers an explicit button; the populated view
|
|
50
|
+
// offers the filter instead. A no-op where neither is rendered.
|
|
51
|
+
async showAllWorkspaces() {
|
|
52
|
+
const switchButton = this.page.getByRole('button', {
|
|
53
|
+
name: 'Switch to All workspaces',
|
|
54
|
+
});
|
|
55
|
+
const scopeFilter = this.page.getByRole('combobox', {
|
|
56
|
+
name: 'Filter workspaces',
|
|
57
|
+
});
|
|
58
|
+
const appeared = await switchButton
|
|
59
|
+
.or(scopeFilter)
|
|
60
|
+
.first()
|
|
61
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
62
|
+
.then(() => true)
|
|
63
|
+
.catch(() => false);
|
|
64
|
+
if (!appeared) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (await switchButton.isVisible().catch(() => false)) {
|
|
68
|
+
await switchButton.click({ timeout: 10000 });
|
|
69
|
+
await (0, test_1.expect)(switchButton).toBeHidden({ timeout: 10000 });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if ((await scopeFilter.innerText()).includes('All workspaces')) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await scopeFilter.click({ timeout: 10000 });
|
|
76
|
+
await this.page
|
|
77
|
+
.getByRole('option', { name: 'All workspaces' })
|
|
78
|
+
.click({ timeout: 10000 });
|
|
79
|
+
await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
|
|
80
|
+
timeout: 10000,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async openManageMembers(workspaceName) {
|
|
84
|
+
// `.first()`: an org can hold several workspaces sharing a name -- the
|
|
85
|
+
// nightly's environments card lists "Cross Component Test Project" twice
|
|
86
|
+
// -- and an unqualified match is then a strict-mode violation.
|
|
87
|
+
const manageButton = this.page
|
|
88
|
+
.getByRole('button', { name: `Manage workspace ${workspaceName}` })
|
|
89
|
+
.first();
|
|
90
|
+
// Keep Hub's default "My workspaces" scope, which is what a real user
|
|
91
|
+
// sees, and widen only when the target is not in it -- e.g. it was
|
|
92
|
+
// created by another user and this one holds no explicit membership.
|
|
93
|
+
const visibleUnderDefaultScope = await manageButton
|
|
94
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
95
|
+
.then(() => true)
|
|
96
|
+
.catch(() => false);
|
|
97
|
+
if (!visibleUnderDefaultScope) {
|
|
98
|
+
await this.showAllWorkspaces();
|
|
99
|
+
}
|
|
100
|
+
await (0, test_1.expect)(manageButton).toBeVisible({ timeout: 60000 });
|
|
101
|
+
await manageButton.click({ timeout: 30000 });
|
|
102
|
+
await (0, test_1.expect)(this.manageMembersHeading).toBeVisible({ timeout: 60000 });
|
|
103
|
+
if ((await this.membersTab
|
|
104
|
+
.getAttribute('aria-selected')
|
|
105
|
+
.catch(() => null)) !== 'true') {
|
|
106
|
+
await this.membersTab.click({ timeout: 30000 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
memberRow(email) {
|
|
110
|
+
return this.page.getByRole('row').filter({ hasText: email }).first();
|
|
111
|
+
}
|
|
112
|
+
async isMember(email) {
|
|
113
|
+
return this.memberRow(email)
|
|
114
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
115
|
+
.then(() => true)
|
|
116
|
+
.catch(() => false);
|
|
117
|
+
}
|
|
118
|
+
// Adds a user who is not on the member list yet. The panel is collapsed
|
|
119
|
+
// until its trigger is clicked, and its own "Workspace role" select already
|
|
120
|
+
// defaults to Editor, so it is only driven when a different role is wanted.
|
|
121
|
+
async addMember(email, role) {
|
|
122
|
+
await this.addMembersButton.click({ timeout: 30000 });
|
|
123
|
+
await (0, test_1.expect)(this.addMemberSearchbox).toBeVisible({ timeout: 30000 });
|
|
124
|
+
await this.addMemberSearchbox.click({ timeout: 30000 });
|
|
125
|
+
await this.addMemberSearchbox.fill(email);
|
|
126
|
+
// An org member shows up as a pickable result; picking it is what enables
|
|
127
|
+
// the submit. Typing alone leaves an invite-by-email candidate that some
|
|
128
|
+
// builds require to be committed the same way.
|
|
129
|
+
// cmdk renders its results as [cmdk-item] nodes with role="option",
|
|
130
|
+
// filtered live against what was typed.
|
|
131
|
+
const result = this.page
|
|
132
|
+
.getByRole('option', { name: new RegExp(escapeRegExp(email), 'i') })
|
|
133
|
+
.or(this.page.locator('[cmdk-item]').filter({ hasText: email }))
|
|
134
|
+
.first();
|
|
135
|
+
// The result list is a multi-select, and its checkbox is NOT a checkbox:
|
|
136
|
+
// a trace of the live DOM shows each row as
|
|
137
|
+
// <div cmdk-item role="option" data-staged="false" aria-selected="true">
|
|
138
|
+
// <svg aria-hidden="true"><rect .../></svg> <div>name / email</div>
|
|
139
|
+
// so getByRole('checkbox') matches nothing, and clicking the row only
|
|
140
|
+
// sets cmdk's highlight -- aria-selected and data-selected both read
|
|
141
|
+
// "true" while data-staged stays "false" and "Add" stays disabled.
|
|
142
|
+
// Click the leading svg, which is the box itself, and gate on the one
|
|
143
|
+
// attribute that actually tracks the pick.
|
|
144
|
+
await (0, test_1.expect)(result).toBeVisible({ timeout: 30000 });
|
|
145
|
+
// Neither the row nor its box can be clicked into a staged state: the box
|
|
146
|
+
// is a decorative svg the design system gives `pointer-events: none`, so
|
|
147
|
+
// that click lands on the row, and a row click only moves cmdk's
|
|
148
|
+
// highlight -- across three live runs `data-staged` never left "false"
|
|
149
|
+
// while `aria-selected` read "true" and "Add" stayed disabled. The
|
|
150
|
+
// list's "Select all" control is what actually stages. It is a
|
|
151
|
+
// <button data-test="members-picker-select-all">, not a cmdk item, and
|
|
152
|
+
// its label is split across text nodes ("Select all (", "1", ")"), so it
|
|
153
|
+
// is addressed by its test id. The search box above has already been
|
|
154
|
+
// filled with this one address, so the guard below holds it to exactly
|
|
155
|
+
// the intended user rather than trusting the label's count.
|
|
156
|
+
const candidates = this.page
|
|
157
|
+
.locator('[cmdk-item]')
|
|
158
|
+
.filter({ hasText: email });
|
|
159
|
+
await (0, test_1.expect)(candidates).toHaveCount(1, { timeout: 15000 });
|
|
160
|
+
await this.page
|
|
161
|
+
.locator('[data-test="members-picker-select-all"]')
|
|
162
|
+
.click({ timeout: 15000 });
|
|
163
|
+
await (0, test_1.expect)(result).toHaveAttribute('data-staged', 'true', {
|
|
164
|
+
timeout: 15000,
|
|
165
|
+
});
|
|
166
|
+
if (role !== 'Editor') {
|
|
167
|
+
await this.selectRoleIn(this.page.getByRole('combobox', { name: 'Workspace role' }).first(), role);
|
|
168
|
+
}
|
|
169
|
+
await (0, test_1.expect)(this.addMemberSubmitButton).toBeEnabled({ timeout: 30000 });
|
|
170
|
+
await this.addMemberSubmitButton.click({ timeout: 30000 });
|
|
171
|
+
await (0, test_1.expect)(this.memberRow(email)).toBeVisible({ timeout: 60000 });
|
|
172
|
+
}
|
|
173
|
+
async selectRoleIn(control, role) {
|
|
174
|
+
await control.click({ timeout: 30000 });
|
|
175
|
+
await this.page
|
|
176
|
+
.getByRole('option', { name: role, exact: true })
|
|
177
|
+
.or(this.page.getByRole('menuitem', { name: role, exact: true }))
|
|
178
|
+
.first()
|
|
179
|
+
.click({ timeout: 30000 });
|
|
180
|
+
}
|
|
181
|
+
// Ensures the user holds `role` on this workspace, adding them first when
|
|
182
|
+
// they are not a member at all -- which is the usual case for a role flow,
|
|
183
|
+
// since the shared workspace is created by whichever setup user won the
|
|
184
|
+
// race and nobody else is added to it.
|
|
185
|
+
async setMemberRole(email, role) {
|
|
186
|
+
if (!(await this.isMember(email))) {
|
|
187
|
+
await this.addMember(email, role);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const row = this.memberRow(email);
|
|
191
|
+
if ((await row.getByText(role, { exact: true }).count()) > 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// The role cell renders as a static pill until clicked, then becomes a
|
|
195
|
+
// select with its own confirm control; leaving it unconfirmed silently
|
|
196
|
+
// reverts the row to its previous role.
|
|
197
|
+
const roleControl = row
|
|
198
|
+
.getByRole('combobox')
|
|
199
|
+
.or(row.getByRole('button', {
|
|
200
|
+
name: /^(Workspace Admin|Editor|Commenter|Viewer)$/,
|
|
201
|
+
}))
|
|
202
|
+
.first();
|
|
203
|
+
await roleControl.click({ timeout: 30000 });
|
|
204
|
+
await this.selectRoleIn(roleControl, role);
|
|
205
|
+
// Clicked unconditionally, not behind an `isVisible()` probe: the
|
|
206
|
+
// confirmation is what persists the change, and the control can render a
|
|
207
|
+
// beat after the select. A probe would read `false` in that window, skip
|
|
208
|
+
// the click, and let the assertion below match the still-uncommitted
|
|
209
|
+
// value in the open editor just before the row reverts. Clicking outright
|
|
210
|
+
// makes Playwright wait for the control and fail here when it never
|
|
211
|
+
// arrives, rather than passing on an edit that was never saved.
|
|
212
|
+
await row
|
|
213
|
+
.getByRole('button', { name: /^(Confirm|Save|Apply|Update)$/i })
|
|
214
|
+
.or(row.locator('button[type="submit"]'))
|
|
215
|
+
.first()
|
|
216
|
+
.click({ timeout: 30000 });
|
|
217
|
+
await (0, test_1.expect)(row.getByText(role, { exact: true })).toBeVisible({
|
|
218
|
+
timeout: 60000,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// Undoes `setMemberRole`/`addMember`. Workspace membership is durable org
|
|
222
|
+
// state: left behind, it survives the test that granted it, so a Playwright
|
|
223
|
+
// retry starts with the user already an Editor and any assertion about the
|
|
224
|
+
// un-granted state fails on the second attempt. A no-op when the user is
|
|
225
|
+
// not on the list, so it is safe to call from teardown after a failure at
|
|
226
|
+
// any point in the flow.
|
|
227
|
+
async removeMember(email) {
|
|
228
|
+
if (!(await this.isMember(email))) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const row = this.memberRow(email);
|
|
232
|
+
// The destructive action lives behind the row's own overflow menu; its
|
|
233
|
+
// trigger is an icon button, so it is matched on its accessible name
|
|
234
|
+
// rather than on text.
|
|
235
|
+
await row
|
|
236
|
+
.getByRole('button', { name: /(more|actions|options|menu)/i })
|
|
237
|
+
.or(row.locator('[data-test="member-row-actions"]'))
|
|
238
|
+
.first()
|
|
239
|
+
.click({ timeout: 30000 });
|
|
240
|
+
await this.page
|
|
241
|
+
.getByRole('menuitem', { name: /^Remove( member)?$/i })
|
|
242
|
+
.or(this.page.getByRole('option', { name: /^Remove( member)?$/i }))
|
|
243
|
+
.first()
|
|
244
|
+
.click({ timeout: 30000 });
|
|
245
|
+
// Removal is confirmed in a dialog. Scoped to it so the match cannot
|
|
246
|
+
// resolve back to the menu item that opened it.
|
|
247
|
+
const dialog = this.page.getByRole('dialog').first();
|
|
248
|
+
await dialog
|
|
249
|
+
.getByRole('button', { name: /^(Remove|Confirm|Delete)$/i })
|
|
250
|
+
.first()
|
|
251
|
+
.click({ timeout: 30000 });
|
|
252
|
+
await (0, test_1.expect)(row).toBeHidden({ timeout: 60000 });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.WorkspaceMembersPage = WorkspaceMembersPage;
|
|
256
|
+
function escapeRegExp(value) {
|
|
257
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
258
|
+
}
|
|
@@ -182,7 +182,9 @@ _8_7_1.test.describe('Connectors User Flow Tests', () => {
|
|
|
182
182
|
timeout: 20000,
|
|
183
183
|
});
|
|
184
184
|
await appsPage.clickCamundaApps();
|
|
185
|
-
|
|
185
|
+
// No separate modelerLink check: Hub has no app-switcher "Modeler" link
|
|
186
|
+
// (clickModeler() goes through its home link instead), and on the
|
|
187
|
+
// legacy shell clickModeler() asserts modelerLink itself.
|
|
186
188
|
await appsPage.clickModeler();
|
|
187
189
|
await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
|
|
188
190
|
timeout: 120000,
|
|
@@ -6,9 +6,15 @@ const UtilitiesPage_1 = require("../../pages/8.8/UtilitiesPage");
|
|
|
6
6
|
const sleep_1 = require("../../utils/sleep");
|
|
7
7
|
const resetSession_1 = require("../../utils/resetSession");
|
|
8
8
|
const LoginPage_1 = require("../../pages/8.8/LoginPage");
|
|
9
|
+
const WorkspaceMembersPage_1 = require("../../pages/8.8/WorkspaceMembersPage");
|
|
9
10
|
const test_1 = require("@playwright/test");
|
|
10
11
|
const users_1 = require("../../utils/users");
|
|
11
12
|
const testUser = (0, users_1.getTestUser)('mainUser');
|
|
13
|
+
// The workspace the Analyst flow borrows Editor rights on, and whether that
|
|
14
|
+
// grant is currently outstanding. Read by the teardown below so it only
|
|
15
|
+
// reaches for Hub's Manage members view when there is something to undo.
|
|
16
|
+
const analystWorkspaceName = 'Cross Component Test Project';
|
|
17
|
+
let analystWorkspaceGrantMade = false;
|
|
12
18
|
_8_8_1.test.describe.configure({ mode: 'parallel' });
|
|
13
19
|
_8_8_1.test.describe('User Roles User Flow', () => {
|
|
14
20
|
const clusterNames = {
|
|
@@ -35,6 +41,29 @@ _8_8_1.test.describe('User Roles User Flow', () => {
|
|
|
35
41
|
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
36
42
|
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
37
43
|
});
|
|
44
|
+
// Workspace membership is durable organization state, so the Editor rights
|
|
45
|
+
// the Analyst flow grants must be handed back whether the test passed or
|
|
46
|
+
// failed. Left behind, a retry would start with the analyst already an
|
|
47
|
+
// Editor and its own "not reachable without workspace access" assertion
|
|
48
|
+
// would fail on the second attempt; the grant would also leak into every
|
|
49
|
+
// later nightly run against the same org.
|
|
50
|
+
_8_8_1.test.afterEach(async ({ page, browser }, testInfo) => {
|
|
51
|
+
if (!testInfo.title.startsWith('Analyst Role User Flow')) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!analystWorkspaceGrantMade) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await (0, resetSession_1.resetSession)(browser, page);
|
|
58
|
+
await new LoginPage_1.LoginPage(page).loginWithTestUser({
|
|
59
|
+
username: process.env.C8_USERNAME,
|
|
60
|
+
password: process.env.C8_PASSWORD,
|
|
61
|
+
});
|
|
62
|
+
const workspaceMembersPage = new WorkspaceMembersPage_1.WorkspaceMembersPage(page);
|
|
63
|
+
await workspaceMembersPage.openManageMembers(analystWorkspaceName);
|
|
64
|
+
await workspaceMembersPage.removeMember(process.env.C8_USERNAME_TEST_9);
|
|
65
|
+
analystWorkspaceGrantMade = false;
|
|
66
|
+
});
|
|
38
67
|
// Skipped due to bug #58514: https://github.com/camunda/camunda/issues/58514
|
|
39
68
|
_8_8_1.test.skip('Modeler Role User Flow', async ({ page, modelerHomePage, appsPage, homePage, clusterPage, clusterDetailsPage, context, consoleOrganizationsPage, settingsPage, operateHomePage, ocIdentityHomePage, taskPanelPage, optimizeHomePage, modelerCreatePage, }) => {
|
|
40
69
|
const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
|
|
@@ -158,6 +187,72 @@ _8_8_1.test.describe('User Roles User Flow', () => {
|
|
|
158
187
|
password: process.env.C8_PASSWORD_TEST_9,
|
|
159
188
|
});
|
|
160
189
|
});
|
|
190
|
+
// An organization role no longer decides on its own what a non-admin can
|
|
191
|
+
// reach. Since camunda-hub's environments work (pdp-3443, pdp-3715), a
|
|
192
|
+
// cluster's apps are surfaced through an ENVIRONMENT, and an environment
|
|
193
|
+
// is visible only to users who are Editor or Workspace Admin on a
|
|
194
|
+
// workspace it is assigned to -- organization admins and owners excepted,
|
|
195
|
+
// who see every environment unconditionally. An Analyst with no workspace
|
|
196
|
+
// membership therefore reaches nothing, which is the decided design
|
|
197
|
+
// rather than a defect: "Clusters are shown only to org admins.
|
|
198
|
+
// Non-admins do not have cluster visibility (they operate with
|
|
199
|
+
// environments instead)."
|
|
200
|
+
//
|
|
201
|
+
// Assert that gate before granting the membership the rest of this test
|
|
202
|
+
// depends on, so both halves are covered: the role alone grants no
|
|
203
|
+
// access, and the role plus workspace editorship does.
|
|
204
|
+
await _8_8_1.test.step('Ensure Optimize is not reachable without workspace access', async () => {
|
|
205
|
+
// Assert the cause, not just the consequence. Hub shows the analyst an
|
|
206
|
+
// empty Workspaces overview -- "You don't have explicit membership in
|
|
207
|
+
// any workspace in this organization" before the scope is widened, and
|
|
208
|
+
// "Your organization has no workspaces yet" after -- because the
|
|
209
|
+
// workspace belongs to whoever created it. With no workspace there is
|
|
210
|
+
// no environment, and with no environment no cluster app, so checking
|
|
211
|
+
// the Optimize banner alone would pass for unrelated reasons too.
|
|
212
|
+
await appsPage.clickCamundaApps();
|
|
213
|
+
await (0, test_1.expect)(page
|
|
214
|
+
.getByText('Your organization has no workspaces yet')
|
|
215
|
+
.or(page.getByText("You don't have explicit membership in any workspace"))
|
|
216
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
217
|
+
// Assert the absence of THIS cluster's Optimize target directly rather
|
|
218
|
+
// than calling clickOptimize() and swallowing whatever it throws: a
|
|
219
|
+
// swallowed error would let this step pass because the hand-off helper
|
|
220
|
+
// itself broke and the page simply stayed on Hub, where the Optimize
|
|
221
|
+
// banner is absent for reasons that have nothing to do with the gate.
|
|
222
|
+
// On Hub a cluster's apps hang off the sidebar group named for the
|
|
223
|
+
// cluster, so with no environment neither the group nor the Optimize
|
|
224
|
+
// link inside it is ever rendered.
|
|
225
|
+
const hubSidebar = page.getByRole('navigation', {
|
|
226
|
+
name: 'Main navigation',
|
|
227
|
+
});
|
|
228
|
+
await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toHaveCount(0, { timeout: 30000 });
|
|
229
|
+
await (0, test_1.expect)(hubSidebar.getByRole('link', { name: /Optimize/ })).toHaveCount(0, { timeout: 30000 });
|
|
230
|
+
await (0, test_1.expect)(optimizeHomePage.optimizeBanner).not.toBeVisible({
|
|
231
|
+
timeout: 30000,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
await _8_8_1.test.step('Grant the analyst Editor rights on the shared workspace', async () => {
|
|
235
|
+
// Only an organization admin may change workspace membership, so this
|
|
236
|
+
// switches to the main user and back rather than doing it as the
|
|
237
|
+
// analyst under test.
|
|
238
|
+
await (0, resetSession_1.resetSession)(browser, page);
|
|
239
|
+
await new LoginPage_1.LoginPage(page).loginWithTestUser({
|
|
240
|
+
username: process.env.C8_USERNAME,
|
|
241
|
+
password: process.env.C8_PASSWORD,
|
|
242
|
+
});
|
|
243
|
+
const workspaceMembersPage = new WorkspaceMembersPage_1.WorkspaceMembersPage(page);
|
|
244
|
+
await workspaceMembersPage.openManageMembers(analystWorkspaceName);
|
|
245
|
+
// Flagged before the grant, not after: a failure partway through
|
|
246
|
+
// setMemberRole can still have added the member, and teardown must
|
|
247
|
+
// clean that up too.
|
|
248
|
+
analystWorkspaceGrantMade = true;
|
|
249
|
+
await workspaceMembersPage.setMemberRole(process.env.C8_USERNAME_TEST_9, 'Editor');
|
|
250
|
+
await (0, resetSession_1.resetSession)(browser, page);
|
|
251
|
+
await new LoginPage_1.LoginPage(page).loginWithTestUser({
|
|
252
|
+
username: process.env.C8_USERNAME_TEST_9,
|
|
253
|
+
password: process.env.C8_PASSWORD_TEST_9,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
161
256
|
await _8_8_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
162
257
|
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
163
258
|
// Optimize once its permission cache refreshes, and reaching Optimize is
|