@camunda/e2e-test-suite 0.0.1192 → 0.0.1194
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.
|
@@ -23,31 +23,48 @@ class OCIdentityHomePage {
|
|
|
23
23
|
this.adminBanner = page.getByRole('link', {
|
|
24
24
|
name: 'Camunda Admin',
|
|
25
25
|
});
|
|
26
|
+
// The same redesign that renamed the logo link moved the nav out of the
|
|
27
|
+
// header: AppHeaderV2 renders an AppSidebar, a <nav aria-label="Main
|
|
28
|
+
// navigation">, so nothing is left under the old
|
|
29
|
+
// banner/aria-label="Admin" scope these tabs were written against. Both
|
|
30
|
+
// shells are matched, the way pages/SM-8.10/OCIdentityHomePage.ts already
|
|
31
|
+
// does, because the design-system rollout is still in flight.
|
|
26
32
|
this.groupsTab = page
|
|
33
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
34
|
+
.getByRole('link', { name: 'Groups', exact: true })
|
|
35
|
+
.or(page
|
|
27
36
|
.getByRole('banner')
|
|
28
37
|
.getByLabel('Admin')
|
|
29
38
|
.locator('a')
|
|
30
|
-
.filter({ hasText: 'Groups' })
|
|
39
|
+
.filter({ hasText: 'Groups' }))
|
|
40
|
+
.first();
|
|
31
41
|
this.rolesTab = page
|
|
42
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
43
|
+
.getByRole('link', { name: 'Roles', exact: true })
|
|
44
|
+
.or(page
|
|
32
45
|
.getByRole('banner')
|
|
33
46
|
.getByLabel('Admin')
|
|
34
47
|
.locator('a')
|
|
35
|
-
.filter({ hasText: 'Roles' })
|
|
48
|
+
.filter({ hasText: 'Roles' }))
|
|
49
|
+
.first();
|
|
36
50
|
this.authorizationsTab = page
|
|
51
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
52
|
+
.getByRole('link', { name: 'Authorizations', exact: true })
|
|
53
|
+
.or(page
|
|
37
54
|
.getByRole('banner')
|
|
38
55
|
.getByLabel('Admin')
|
|
39
56
|
.locator('a')
|
|
40
|
-
.filter({ hasText: /^Authorizations$/ })
|
|
57
|
+
.filter({ hasText: /^Authorizations$/ }))
|
|
58
|
+
.first();
|
|
41
59
|
this.clusterVariablesTab = page
|
|
42
|
-
.getByRole('
|
|
43
|
-
.
|
|
44
|
-
.locator('a')
|
|
45
|
-
.filter({ hasText: /Cluster variables/i })
|
|
60
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
61
|
+
.getByRole('link', { name: 'Cluster variables', exact: true })
|
|
46
62
|
.or(page
|
|
47
63
|
.getByRole('banner')
|
|
48
64
|
.getByLabel('Admin')
|
|
49
65
|
.locator('a')
|
|
50
|
-
.filter({ hasText: /
|
|
66
|
+
.filter({ hasText: /Cluster variables/i }))
|
|
67
|
+
.first();
|
|
51
68
|
this.licenseKeyTagNonProduction = page
|
|
52
69
|
.getByText('Non-production license')
|
|
53
70
|
.first();
|
|
@@ -2,6 +2,8 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class TaskPanelPage {
|
|
3
3
|
private page;
|
|
4
4
|
readonly availableTasks: Locator;
|
|
5
|
+
readonly filterPanelExpandButton: Locator;
|
|
6
|
+
readonly filterPanelCollapseButton: Locator;
|
|
5
7
|
readonly filterSelectButton: Locator;
|
|
6
8
|
readonly taskListPageBanner: Locator;
|
|
7
9
|
readonly processesPageTab: Locator;
|
|
@@ -12,6 +14,9 @@ declare class TaskPanelPage {
|
|
|
12
14
|
taskInList(name: string): Locator;
|
|
13
15
|
openTask(name: string, totalTimeout?: number): Promise<void>;
|
|
14
16
|
filterBy(option: 'All open tasks' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
|
|
17
|
+
private openFilterControls;
|
|
18
|
+
private collapseFilterPanel;
|
|
19
|
+
private static appliedFilter;
|
|
15
20
|
taskCount(name: string): Promise<number>;
|
|
16
21
|
clickTasksTab(): Promise<void>;
|
|
17
22
|
scrollToLastTask(name: string): Promise<void>;
|
|
@@ -8,6 +8,8 @@ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLoc
|
|
|
8
8
|
class TaskPanelPage {
|
|
9
9
|
page;
|
|
10
10
|
availableTasks;
|
|
11
|
+
filterPanelExpandButton;
|
|
12
|
+
filterPanelCollapseButton;
|
|
11
13
|
filterSelectButton;
|
|
12
14
|
taskListPageBanner;
|
|
13
15
|
processesPageTab;
|
|
@@ -17,12 +19,19 @@ class TaskPanelPage {
|
|
|
17
19
|
constructor(page) {
|
|
18
20
|
this.page = page;
|
|
19
21
|
this.availableTasks = page.getByTitle('Available tasks');
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// 8.10 still serves Tasklist on the Carbon shell, so filtering lives in
|
|
23
|
+
// the collapsible filter panel: the collapsed rail carries the expand
|
|
24
|
+
// control (aria-controls="task-nav-bar") and the expanded nav lists each
|
|
25
|
+
// filter as a link. Tasklist's design-system redesign -- a single
|
|
26
|
+
// "Filters" dropdown trigger opening a Radix menu -- landed on main
|
|
27
|
+
// (8.11) only: `stable/8.10` has no FilterSelect.tsx and its Filters.tsx
|
|
28
|
+
// still renders the Carbon panel against the /_carbon route tree, which
|
|
29
|
+
// is why targeting the redesign found no "Filters" button at all.
|
|
30
|
+
// Both shells are matched below because the SaaS design-system rollout is
|
|
31
|
+
// per app and still in flight (Admin has already flipped, hence the
|
|
32
|
+
// dual-shell locators in OCIdentityHomePage).
|
|
33
|
+
this.filterPanelExpandButton = page.locator('button[aria-controls="task-nav-bar"][aria-expanded="false"]');
|
|
34
|
+
this.filterPanelCollapseButton = page.locator('button[aria-controls="task-nav-bar"][aria-expanded="true"]');
|
|
26
35
|
this.filterSelectButton = page.getByRole('button', {
|
|
27
36
|
name: 'Filters',
|
|
28
37
|
exact: true,
|
|
@@ -87,47 +96,100 @@ class TaskPanelPage {
|
|
|
87
96
|
`${totalTimeout}ms: ${lastError}`);
|
|
88
97
|
}
|
|
89
98
|
async filterBy(option) {
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// TasksLayoutPage renders <Filters disabled={isPending} />, so a task
|
|
99
|
-
// list refetch landing while the menu is open disables the trigger and
|
|
100
|
-
// tears the menu down before Radix runs the item's onSelect: the menu
|
|
101
|
-
// closes but the filter never changes. Re-open and pick again rather
|
|
102
|
-
// than returning with the list still on the previous filter (same
|
|
103
|
-
// bounded retry as pages/c8Run-8.10/TaskPanelPage.ts).
|
|
104
|
-
const menuItem = this.page.getByRole('menuitem', {
|
|
105
|
-
name: option,
|
|
106
|
-
exact: true,
|
|
107
|
-
});
|
|
108
|
-
const triggerShowingOption = this.filterSelectButton.filter({
|
|
109
|
-
hasText: new RegExp(`^${option}$`),
|
|
110
|
-
});
|
|
99
|
+
// The filter is a link in the Carbon panel's expanded nav and a
|
|
100
|
+
// role="menuitem" in the redesigned dropdown, so match either. The
|
|
101
|
+
// Carbon links are scoped to #task-nav-bar so "Completed" cannot pick up
|
|
102
|
+
// a custom filter or a task row carrying the same words.
|
|
103
|
+
const filterOption = this.page
|
|
104
|
+
.locator('#task-nav-bar')
|
|
105
|
+
.getByRole('link', { name: option, exact: true })
|
|
106
|
+
.or(this.page.getByRole('menuitem', { name: option, exact: true }));
|
|
111
107
|
const maxAttempts = 3;
|
|
112
108
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
await this.openFilterControls();
|
|
110
|
+
// Only the first attempt waits out a slow first render; later attempts
|
|
111
|
+
// pick from controls that have already rendered once, so three
|
|
112
|
+
// attempts at the full budget cannot outlast the test timeout.
|
|
113
|
+
const renderTimeout = attempt === 1 ? 60000 : 15000;
|
|
114
|
+
await (0, test_1.expect)(filterOption).toBeVisible({ timeout: renderTimeout });
|
|
115
|
+
await filterOption.click({ timeout: renderTimeout });
|
|
116
|
+
// The route -- not the panel -- is what proves the pick landed.
|
|
117
|
+
// Tasklist refetches the list every 5s behind a suspense boundary, and
|
|
118
|
+
// a poll landing on the click tears the controls down before the
|
|
119
|
+
// router navigates: they close but the filter never changes, and
|
|
120
|
+
// nothing downstream notices until a much later assertion reads the
|
|
121
|
+
// still-unfiltered list.
|
|
122
|
+
// Only a timeout means "the pick did not land", which is what the
|
|
123
|
+
// retry below is for. Anything else -- a closed page, a failed
|
|
124
|
+
// navigation -- is fatal, and swallowing it would surface three
|
|
125
|
+
// attempts later as the misleading "filter was not applied" error
|
|
126
|
+
// instead of its real cause.
|
|
127
|
+
let applied = true;
|
|
128
|
+
try {
|
|
129
|
+
await this.page.waitForURL(TaskPanelPage.appliedFilter(option), {
|
|
130
|
+
timeout: 15000,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!(error instanceof test_1.errors.TimeoutError)) {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
applied = false;
|
|
138
|
+
}
|
|
121
139
|
if (applied) {
|
|
140
|
+
await this.collapseFilterPanel();
|
|
122
141
|
return;
|
|
123
142
|
}
|
|
124
143
|
console.warn(`Attempt ${attempt} to apply the "${option}" task filter did not ` +
|
|
125
144
|
'take effect. Retrying...');
|
|
126
145
|
await this.page.keyboard.press('Escape');
|
|
127
|
-
await (0, test_1.expect)(this.filterSelectButton).toHaveAttribute('aria-expanded', 'false', { timeout: 10000 });
|
|
128
146
|
}
|
|
129
147
|
throw new Error(`Task filter "${option}" was not applied after ${maxAttempts} attempts`);
|
|
130
148
|
}
|
|
149
|
+
// The Carbon panel renders either the collapsed rail (which carries the
|
|
150
|
+
// expand control) or the expanded nav -- never both -- and picking a filter
|
|
151
|
+
// leaves it expanded, so a retry must not click "expand" a second time. The
|
|
152
|
+
// redesigned dropdown instead has to be re-opened every attempt, because
|
|
153
|
+
// picking an option closes the menu.
|
|
154
|
+
async openFilterControls() {
|
|
155
|
+
const alreadyExpanded = await this.filterPanelCollapseButton
|
|
156
|
+
.waitFor({ state: 'visible', timeout: 2000 })
|
|
157
|
+
.then(() => true)
|
|
158
|
+
.catch(() => false);
|
|
159
|
+
if (alreadyExpanded) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await this.filterPanelExpandButton
|
|
163
|
+
.or(this.filterSelectButton)
|
|
164
|
+
.first()
|
|
165
|
+
.click({ timeout: 60000 });
|
|
166
|
+
}
|
|
167
|
+
// Picking a filter leaves the Carbon panel expanded, and it floats over the
|
|
168
|
+
// task list -- callers that go straight to a task row would be clicking
|
|
169
|
+
// through the overlay (pages/SM-8.10 collapses it from the specs for the
|
|
170
|
+
// same reason). The redesigned dropdown closes itself on select, so this is
|
|
171
|
+
// a probe rather than an unconditional click.
|
|
172
|
+
async collapseFilterPanel() {
|
|
173
|
+
const expanded = await this.filterPanelCollapseButton
|
|
174
|
+
.waitFor({ state: 'visible', timeout: 2000 })
|
|
175
|
+
.then(() => true)
|
|
176
|
+
.catch(() => false);
|
|
177
|
+
if (expanded) {
|
|
178
|
+
await this.filterPanelCollapseButton.click({ timeout: 30000 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Tasklist keeps the active filter in the `filter` search param, on both
|
|
182
|
+
// shells. "All open tasks" is the route default and is stripped from the
|
|
183
|
+
// URL, so it is asserted as the absence of the param.
|
|
184
|
+
static appliedFilter(option) {
|
|
185
|
+
const filterParam = {
|
|
186
|
+
Unassigned: 'unassigned',
|
|
187
|
+
'Assigned to me': 'assigned-to-me',
|
|
188
|
+
Completed: 'completed',
|
|
189
|
+
};
|
|
190
|
+
const param = filterParam[option] ?? null;
|
|
191
|
+
return (url) => url.searchParams.get('filter') === param;
|
|
192
|
+
}
|
|
131
193
|
async taskCount(name) {
|
|
132
194
|
await (0, sleep_1.sleep)(20000);
|
|
133
195
|
return this.availableTasks.getByText(name, { exact: true }).count();
|
|
@@ -16,9 +16,9 @@ declare class TaskPanelPage {
|
|
|
16
16
|
filterBy(option: 'All open' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
|
|
17
17
|
private expandFilterPanel;
|
|
18
18
|
private static appliedFilterUrl;
|
|
19
|
+
clickCollapseFilter(): Promise<void>;
|
|
19
20
|
scrollToLastTask(name: string): Promise<void>;
|
|
20
21
|
scrollToFirstTask(name: string): Promise<void>;
|
|
21
|
-
clickCollapseFilter(): Promise<void>;
|
|
22
22
|
clickProcessesTab(): Promise<void>;
|
|
23
23
|
taskCount(name: string, waitForStable?: boolean): Promise<number>;
|
|
24
24
|
clickTasksTab(): Promise<void>;
|
|
@@ -76,35 +76,46 @@ class TaskPanelPage {
|
|
|
76
76
|
// landing on the filter click tears the floating filter panel down before
|
|
77
77
|
// the router navigates: the panel closes but the filter never changes.
|
|
78
78
|
// Nothing downstream notices, so the test fails much later on an assertion
|
|
79
|
-
// that reads the still-unfiltered list
|
|
80
|
-
//
|
|
81
|
-
// Confirm the route actually carries the filter, and re-pick if it does not.
|
|
79
|
+
// that reads the still-unfiltered list. Confirm the route actually
|
|
80
|
+
// carries the filter, and re-pick if it does not.
|
|
82
81
|
const maxAttempts = 3;
|
|
82
|
+
let lastError;
|
|
83
83
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
try {
|
|
85
|
+
await this.expandFilterPanel();
|
|
86
|
+
// Deliberately not `exact`: the panel spells the default filter
|
|
87
|
+
// "All open tasks" while callers pass "All open".
|
|
88
|
+
const filterLink = this.page.getByRole('link', { name: option });
|
|
89
|
+
// Only the first attempt waits out a slow first render. Later
|
|
90
|
+
// attempts re-pick from a panel that has already rendered once, so
|
|
91
|
+
// they stay short -- three attempts at the full budget would outlast
|
|
92
|
+
// the 12-minute test timeout and turn a dropped click into a timeout
|
|
93
|
+
// with no retry spent.
|
|
94
|
+
const renderTimeout = attempt === 1 ? 120000 : 15000;
|
|
95
|
+
await (0, test_1.expect)(filterLink).toBeVisible({ timeout: renderTimeout });
|
|
96
|
+
await filterLink.click({ timeout: renderTimeout });
|
|
97
|
+
const applied = await this.page
|
|
98
|
+
.waitForURL(TaskPanelPage.appliedFilterUrl(option), { timeout: 15000 })
|
|
99
|
+
.then(() => true)
|
|
100
|
+
.catch(() => false);
|
|
101
|
+
if (applied) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
lastError = new Error(`Filter "${option}" was picked but the URL never carried it`);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Without this catch, a thrown assertion (the expand control or the
|
|
108
|
+
// filter link never appearing) propagated straight out of filterBy()
|
|
109
|
+
// on attempt 1, so attempts 2 and 3 -- and their shorter, faster-
|
|
110
|
+
// failing budget -- could never run. maxAttempts was dead code for
|
|
111
|
+
// every failure mode except a click that landed but didn't navigate.
|
|
112
|
+
lastError = error;
|
|
102
113
|
}
|
|
103
114
|
if (attempt < maxAttempts) {
|
|
104
115
|
console.warn(`Attempt ${attempt} did not apply the "${option}" filter. Retrying...`);
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
|
-
throw new Error(`Filter "${option}" was not applied after ${maxAttempts} attempts`);
|
|
118
|
+
throw new Error(`Filter "${option}" was not applied after ${maxAttempts} attempts: ${lastError}`);
|
|
108
119
|
}
|
|
109
120
|
// The filter panel renders either the collapsed rail (which carries the
|
|
110
121
|
// expand control) or the expanded nav -- never both -- and picking a filter
|
|
@@ -133,27 +144,52 @@ class TaskPanelPage {
|
|
|
133
144
|
? /\/tasklist(?:\/[^?]*)?(?:\?(?!.*\bfilter=).*)?$/
|
|
134
145
|
: new RegExp(`[?&]filter=${param}(?:&|$)`);
|
|
135
146
|
}
|
|
147
|
+
async clickCollapseFilter() {
|
|
148
|
+
await this.collapseFilter.click({ timeout: 45000 });
|
|
149
|
+
}
|
|
136
150
|
async scrollToLastTask(name) {
|
|
137
151
|
await this.page.getByText(name).last().scrollIntoViewIfNeeded();
|
|
138
152
|
}
|
|
139
153
|
async scrollToFirstTask(name) {
|
|
140
154
|
await this.page.getByText(name).first().scrollIntoViewIfNeeded();
|
|
141
155
|
}
|
|
142
|
-
async clickCollapseFilter() {
|
|
143
|
-
await this.collapseFilter.click({ timeout: 45000 });
|
|
144
|
-
}
|
|
145
156
|
async clickProcessesTab() {
|
|
146
157
|
await (0, test_1.expect)(this.processesPageTab).toBeVisible();
|
|
158
|
+
// Landing on Processes shows either the process search box or, on the
|
|
159
|
+
// first visit, the "Start your process on demand" onboarding dialog.
|
|
160
|
+
// `FirstTimeProcessWarning` renders the Processes content only after
|
|
161
|
+
// consent, so while that dialog is open the search box is not in the
|
|
162
|
+
// DOM at all -- confirming via the search box alone would never resolve
|
|
163
|
+
// on a first visit, and the retry would re-click Processes, which the
|
|
164
|
+
// dialog's modal overlay now covers. Treat either as proof the Processes
|
|
165
|
+
// view rendered; the caller dismisses the dialog.
|
|
166
|
+
const processesReady = this.page
|
|
167
|
+
.getByRole('searchbox', { name: 'Search processes' })
|
|
168
|
+
.or(this.page.getByRole('dialog', {
|
|
169
|
+
name: 'Start your process on demand',
|
|
170
|
+
}));
|
|
147
171
|
const maxAttempts = 3;
|
|
172
|
+
let lastError;
|
|
148
173
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
149
|
-
|
|
174
|
+
try {
|
|
175
|
+
await this.processesPageTab.click();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// The click belongs inside the try for the same reason filterBy()'s
|
|
179
|
+
// assertions do: the onboarding dialog's modal overlay covers the
|
|
180
|
+
// nav, so a click that lands on it throws once the repo's 10s action
|
|
181
|
+
// timeout expires. Letting that escape made maxAttempts dead code on
|
|
182
|
+
// exactly the path that needs it. Fall through to the landing probe
|
|
183
|
+
// below -- a click that threw may still have navigated.
|
|
184
|
+
lastError = error;
|
|
185
|
+
}
|
|
150
186
|
// A newly-arrived task can bounce the app back to the Tasks tab right
|
|
151
187
|
// as this click lands (nightly SM-8.10 run 31782502787: the click
|
|
152
188
|
// itself succeeded but the app ended up back on Tasks with the new
|
|
153
189
|
// task selected), so confirm the Processes view actually rendered
|
|
154
190
|
// before returning instead of trusting the click alone.
|
|
155
|
-
const landedOnProcesses = await
|
|
156
|
-
.
|
|
191
|
+
const landedOnProcesses = await processesReady
|
|
192
|
+
.first()
|
|
157
193
|
.waitFor({ state: 'visible', timeout: 5000 })
|
|
158
194
|
.then(() => true)
|
|
159
195
|
.catch(() => false);
|
|
@@ -164,7 +200,7 @@ class TaskPanelPage {
|
|
|
164
200
|
console.warn(`Attempt ${attempt} landed back on Tasks instead of Processes. Retrying...`);
|
|
165
201
|
}
|
|
166
202
|
}
|
|
167
|
-
throw new Error(
|
|
203
|
+
throw new Error(`Processes tab did not open after ${maxAttempts} attempts: ${lastError}`);
|
|
168
204
|
}
|
|
169
205
|
async taskCount(name, waitForStable = true) {
|
|
170
206
|
// Removed waitForLoadState('networkidle') — Tasklist polls;
|