@webship-js/odoo-steps 2.0.0 → 2.1.0
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/README.md +112 -56
- package/STEPS.md +141 -0
- package/package.json +6 -3
- package/tests/step-definitions/README.md +11 -1
- package/tests/step-definitions/odoo-helpers.js +541 -0
- package/tests/step-definitions/odoo-record.steps.js +1398 -0
- package/tests/step-definitions/odoo-session.steps.js +402 -0
- package/tests/step-definitions/odoo-webclient.steps.js +1236 -0
- package/tests/step-definitions/odoo.steps.js +431 -304
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Session steps for Odoo: the login screen (`/web/login`), the users registry
|
|
4
|
+
// of cucumber.js, the user menu of the web client and of the website, logging
|
|
5
|
+
// out, and what the server says about the session.
|
|
6
|
+
//
|
|
7
|
+
// Accounts are configuration. They are created once on the site under test
|
|
8
|
+
// (scripts/build-odoo.sh does it for this pack's own suite) and named in
|
|
9
|
+
// `worldParameters.users`; a feature never creates or deletes an account and
|
|
10
|
+
// never carries a password, except where the credentials are the subject.
|
|
11
|
+
|
|
12
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
13
|
+
const { waitForPageLoad, friendly } = require('webship-js/tests/step-definitions/webship');
|
|
14
|
+
const {
|
|
15
|
+
WAIT_TIMEOUT,
|
|
16
|
+
SHORT_TIMEOUT,
|
|
17
|
+
budget,
|
|
18
|
+
settle,
|
|
19
|
+
navigate,
|
|
20
|
+
assertPresence,
|
|
21
|
+
assertText,
|
|
22
|
+
listTexts,
|
|
23
|
+
loginPath,
|
|
24
|
+
submitLogin,
|
|
25
|
+
sessionInfo,
|
|
26
|
+
loginAs,
|
|
27
|
+
textPattern,
|
|
28
|
+
} = require('./odoo-helpers');
|
|
29
|
+
|
|
30
|
+
const LOGIN_FORM = 'form[action^="/web/login"]';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Open the user menu: the avatar menu of the web client's navigation bar
|
|
34
|
+
* (`.o_user_menu`) or, on the website and the portal, the menu under the
|
|
35
|
+
* user's name in the header. Returns the open menu.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} world
|
|
38
|
+
* @returns {Promise<import('playwright').Locator>}
|
|
39
|
+
*/
|
|
40
|
+
async function openUserMenu(world) {
|
|
41
|
+
const page = world.page;
|
|
42
|
+
const backend = page.locator('.o_main_navbar .o_user_menu').first();
|
|
43
|
+
if (await backend.isVisible().catch(() => false)) {
|
|
44
|
+
const menu = backend.locator('.dropdown-menu');
|
|
45
|
+
if (!(await menu.isVisible().catch(() => false))) {
|
|
46
|
+
await backend.locator('button.dropdown-toggle').click();
|
|
47
|
+
}
|
|
48
|
+
await menu.waitFor({ state: 'visible', timeout: SHORT_TIMEOUT });
|
|
49
|
+
return menu;
|
|
50
|
+
}
|
|
51
|
+
const frontend = page.locator('header li.dropdown:has(#o_logout):visible, header .dropdown:has(#o_logout):visible').first();
|
|
52
|
+
if (await frontend.isVisible().catch(() => false)) {
|
|
53
|
+
const menu = frontend.locator('.dropdown-menu').first();
|
|
54
|
+
if (!(await menu.isVisible().catch(() => false))) {
|
|
55
|
+
await frontend.locator('a.dropdown-toggle, [data-bs-toggle="dropdown"]').first().click();
|
|
56
|
+
}
|
|
57
|
+
await menu.waitFor({ state: 'visible', timeout: SHORT_TIMEOUT });
|
|
58
|
+
return menu;
|
|
59
|
+
}
|
|
60
|
+
throw friendly({
|
|
61
|
+
message: 'Expected a user menu on the page.',
|
|
62
|
+
cause: `the page is ${page.url()}`,
|
|
63
|
+
hint: 'The user menu is shown to a logged-in user: in the navigation bar of the web client, or under the user\'s name in the website header. Log in first.',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Navigate to the login screen at `/web/login`. The screen ships with the
|
|
69
|
+
* base "web" module, so every Odoo site has it.
|
|
70
|
+
*
|
|
71
|
+
* Example #1: Given I am on the login page
|
|
72
|
+
* Example #2: Given we are on the login page
|
|
73
|
+
* Example #3: Given I am on the sign in page
|
|
74
|
+
* Example #4: And on the sign-in page
|
|
75
|
+
* Example #5: Given I am on the web/login page
|
|
76
|
+
*
|
|
77
|
+
*/
|
|
78
|
+
Given(/^(I am |we are )*on the (?:sign[- ]?in|web\/login|login) page$/, async function (pronounCase) {
|
|
79
|
+
await navigate(this, loginPath(this));
|
|
80
|
+
const page = this.page;
|
|
81
|
+
if (/\/web\/database\/selector/.test(page.url())) {
|
|
82
|
+
throw friendly({
|
|
83
|
+
message: 'Expected the login screen, but the server shows the database selector.',
|
|
84
|
+
cause: `the page is ${page.url()}`,
|
|
85
|
+
hint: 'The server holds more than one database: set worldParameters.odoo.database in cucumber.js, or start Odoo with --db-filter.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// A logged-in visitor is redirected away from /web/login; the step does not
|
|
89
|
+
// hide that, so the assertion that follows can report it.
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Assert the login form is, or is not, visible: the form posting to
|
|
94
|
+
* `/web/login`, with its `#login` and `#password` fields and its submit
|
|
95
|
+
* button. Each part is checked separately so a failure names the missing
|
|
96
|
+
* one.
|
|
97
|
+
*
|
|
98
|
+
* Example #1: Then I should see the login form
|
|
99
|
+
* Example #2: And I should see the sign in form
|
|
100
|
+
* Example #3: Then we should see the login form
|
|
101
|
+
* Example #4: And should not see the login form
|
|
102
|
+
* Example #5: Then I should see the sign-in form
|
|
103
|
+
*
|
|
104
|
+
*/
|
|
105
|
+
Then(/^(I |we )*should( not)* see the (?:sign[- ]?in|login) form$/, async function (pronounCase, notCase) {
|
|
106
|
+
const hint = 'The login screen is /web/login; a logged-in visitor is redirected away from it to /web.';
|
|
107
|
+
await assertPresence(this.page.locator(LOGIN_FORM), notCase, 'the login form ("form[action=\\"/web/login\\"]")', { hint });
|
|
108
|
+
if (notCase) return;
|
|
109
|
+
await assertPresence(this.page.locator(`${LOGIN_FORM} #login`), undefined, 'the login field ("#login")', { hint, timeout: SHORT_TIMEOUT });
|
|
110
|
+
await assertPresence(this.page.locator(`${LOGIN_FORM} #password`), undefined, 'the password field ("#password")', { hint, timeout: SHORT_TIMEOUT });
|
|
111
|
+
await assertPresence(this.page.locator(`${LOGIN_FORM} button[type="submit"]`).first(), undefined, 'the submit button of the login form', { hint, timeout: SHORT_TIMEOUT });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fill and submit the login form with the given login and password, for
|
|
116
|
+
* scenarios where the credentials are the subject (a wrong password, a
|
|
117
|
+
* disabled account). The login screen must be open already. For everything
|
|
118
|
+
* else use the users registry: `Given I am a logged in user with the "admin"
|
|
119
|
+
* user`.
|
|
120
|
+
*
|
|
121
|
+
* Example #1: When I log in as "admin" with password "secret"
|
|
122
|
+
* Example #2: When we log in as "demo@example.com" with password "p@ssw0rd"
|
|
123
|
+
* Example #3: And I sign in as "admin" with password "secret"
|
|
124
|
+
* Example #4: When we sign-in as "reviewer" with password "hunter2"
|
|
125
|
+
* Example #5: And log in as "portal_user" with password "letmein"
|
|
126
|
+
*
|
|
127
|
+
*/
|
|
128
|
+
When(/^(I |we )*(?:log in|sign[- ]?in) as "([^"]*)?" with password "([^"]*)?"$/, async function (pronounCase, username, password) {
|
|
129
|
+
await submitLogin(this, username, password);
|
|
130
|
+
await this.page.locator(`.o_main_navbar, #wrapwrap, ${LOGIN_FORM}`).first().waitFor({ state: 'attached', timeout: WAIT_TIMEOUT }).catch(() => {});
|
|
131
|
+
await settle(this, budget(this));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Log in as a user of the registry in cucumber.js (`worldParameters.users`),
|
|
136
|
+
* so no password appears in a feature file. Each handle names one account:
|
|
137
|
+
* `{ admin: { username: 'admin', password: process.env.ODOO_ADMIN_PASSWORD } }`.
|
|
138
|
+
* The step starts a new session, opens the login screen, submits the form and
|
|
139
|
+
* waits for the page the user lands on: the web client for an internal user,
|
|
140
|
+
* the portal for a portal user. A refused login fails here, with the message
|
|
141
|
+
* the login form shows.
|
|
142
|
+
*
|
|
143
|
+
* Example #1: Given I am a logged in user with the "admin" user
|
|
144
|
+
* Example #2: Given I am a logged in user with the username "admin"
|
|
145
|
+
* Example #3: Given we are a logged in user with "portal"
|
|
146
|
+
* Example #4: And a logged in user with the "salesperson" user
|
|
147
|
+
* Example #5: Given I am a logged in user with the username "accountant" user
|
|
148
|
+
*
|
|
149
|
+
*/
|
|
150
|
+
Given(/^(I am |we are )*a logged in user with( the)*( username)* "([^"]*)?"( user)*$/, async function (pronounCase, theCase, usernameCase, handle, userCase) {
|
|
151
|
+
await loginAs(this, handle);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* End any session and land on the login screen as a visitor: cookies and web
|
|
156
|
+
* storage are cleared, `/web/login` is opened and the login form is
|
|
157
|
+
* confirmed. Use it at the start of a scenario that must run as a visitor.
|
|
158
|
+
* To log out the way a user does, use `When I log out`.
|
|
159
|
+
*
|
|
160
|
+
* Example #1: Given I am not logged in
|
|
161
|
+
* Example #2: Given we are not logged in
|
|
162
|
+
* Example #3: And we are not logged in
|
|
163
|
+
* Example #4: Given not logged in
|
|
164
|
+
* Example #5: But not logged in
|
|
165
|
+
*
|
|
166
|
+
*/
|
|
167
|
+
Given(/^(I am |we are )*not logged in$/, async function (pronounCase) {
|
|
168
|
+
await this.context.clearCookies();
|
|
169
|
+
try {
|
|
170
|
+
await this.page.evaluate(() => {
|
|
171
|
+
try { localStorage.clear(); } catch { /* storage may be unavailable */ }
|
|
172
|
+
try { sessionStorage.clear(); } catch { /* storage may be unavailable */ }
|
|
173
|
+
});
|
|
174
|
+
} catch { /* the page may not be on a real URL yet */ }
|
|
175
|
+
await navigate(this, loginPath(this));
|
|
176
|
+
await assertPresence(this.page.locator(LOGIN_FORM), undefined, 'the login form (the session should have ended)', {
|
|
177
|
+
timeout: SHORT_TIMEOUT,
|
|
178
|
+
hint: 'A logged-in visitor is redirected from /web/login to /web; clearing the cookies should have ended the session.',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Assert the login screen is, or is not, showing an error, optionally with a
|
|
184
|
+
* text. Matches the `.alert-danger` message the login form renders after a
|
|
185
|
+
* wrong login or password ("Wrong login/password").
|
|
186
|
+
*
|
|
187
|
+
* Example #1: Then I should see an login error
|
|
188
|
+
* Example #2: And I should see a sign in error
|
|
189
|
+
* Example #3: Then we should see a login error "Wrong login/password"
|
|
190
|
+
* Example #4: And should not see a login error
|
|
191
|
+
* Example #5: Then I should see an authentication error message
|
|
192
|
+
*
|
|
193
|
+
*/
|
|
194
|
+
Then(/^(I |we )*should( not)* see an? (?:login|sign[- ]?in|authentication) error(?: message)?(?: "([^"]*)")?$/, async function (pronounCase, notCase, expectedText) {
|
|
195
|
+
const error = this.page.locator(`${LOGIN_FORM} .alert-danger`).first();
|
|
196
|
+
const hint = 'The login screen renders the message inside the form after a wrong login or password.';
|
|
197
|
+
if (notCase && !expectedText) {
|
|
198
|
+
await assertPresence(error, notCase, 'a login error message (".alert-danger")', { hint });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await assertPresence(error, undefined, 'a login error message (".alert-danger")', { hint });
|
|
202
|
+
if (expectedText) {
|
|
203
|
+
await assertText(error, expectedText, notCase, 'the login error message', { hint });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Log out the way a user does: open the user menu and choose "Log out" (the
|
|
209
|
+
* web client) or "Logout" (the website and the portal). The step then waits
|
|
210
|
+
* until the server reports no session.
|
|
211
|
+
*
|
|
212
|
+
* Example #1: When I log out
|
|
213
|
+
* Example #2: When we log out
|
|
214
|
+
* Example #3: And log out
|
|
215
|
+
* Example #4: When I sign out
|
|
216
|
+
* Example #5: And we sign out
|
|
217
|
+
*
|
|
218
|
+
*/
|
|
219
|
+
When(/^(I |we )*(?:log|sign) out$/, async function (pronounCase) {
|
|
220
|
+
const page = this.page;
|
|
221
|
+
const menu = await openUserMenu(this);
|
|
222
|
+
const entry = menu.locator('[data-menu="logout"], #o_logout, a[href*="/web/session/logout"]').first();
|
|
223
|
+
try {
|
|
224
|
+
await entry.waitFor({ state: 'visible', timeout: SHORT_TIMEOUT });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw friendly({
|
|
227
|
+
message: 'Expected a "Log out" entry in the user menu.',
|
|
228
|
+
cause: `the menu offers ${await listTexts(menu.locator('.dropdown-item'))}`,
|
|
229
|
+
hint: 'The entry is part of the standard user menu; a customised menu needs its own step.',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
await Promise.all([
|
|
233
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: WAIT_TIMEOUT }).catch(() => {}),
|
|
234
|
+
entry.click(),
|
|
235
|
+
]);
|
|
236
|
+
await waitForPageLoad(page, budget(this));
|
|
237
|
+
const session = await sessionInfo(this);
|
|
238
|
+
if (session.uid) {
|
|
239
|
+
throw friendly({
|
|
240
|
+
message: 'Expected the session to end after logging out.',
|
|
241
|
+
cause: `the server still reports a session for "${session.username}"`,
|
|
242
|
+
hint: 'Check that /web/session/logout is reachable and not blocked by a proxy rule.',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Assert the browser holds, or does not hold, a session, as the server
|
|
249
|
+
* reports it (`/web/session/get_session_info`). The check does not depend on
|
|
250
|
+
* the page on screen, so it works on the web client, the website and the
|
|
251
|
+
* portal alike.
|
|
252
|
+
*
|
|
253
|
+
* Example #1: Then I should be logged in
|
|
254
|
+
* Example #2: And we should be logged in
|
|
255
|
+
* Example #3: Then I should not be logged in
|
|
256
|
+
* Example #4: And should be logged in
|
|
257
|
+
* Example #5: Then we should not be logged in
|
|
258
|
+
*
|
|
259
|
+
*/
|
|
260
|
+
Then(/^(I |we )*should( not)* be logged in$/, async function (pronounCase, notCase) {
|
|
261
|
+
const session = await sessionInfo(this);
|
|
262
|
+
if (notCase && session.uid) {
|
|
263
|
+
throw friendly({
|
|
264
|
+
message: 'Expected no session.',
|
|
265
|
+
cause: `the server reports a session for "${session.username}" (${session.name})`,
|
|
266
|
+
hint: 'Log out first ("When I log out"), or start the scenario with "Given I am not logged in".',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (!notCase && !session.uid) {
|
|
270
|
+
throw friendly({
|
|
271
|
+
message: 'Expected a session.',
|
|
272
|
+
cause: 'the server reports no session for this browser',
|
|
273
|
+
hint: 'Log in first ("Given I am a logged in user with the "admin" user").',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Assert the session belongs, or does not belong, to the given user. The text
|
|
280
|
+
* is compared with the login and with the name of the user, as the server
|
|
281
|
+
* reports them, so both "admin" and "Mitchell Admin" identify the
|
|
282
|
+
* administrator of the demo data.
|
|
283
|
+
*
|
|
284
|
+
* Example #1: Then I should be logged in as "admin"
|
|
285
|
+
* Example #2: And I should be logged in as "Mitchell Admin"
|
|
286
|
+
* Example #3: Then we should be logged in as "salesperson"
|
|
287
|
+
* Example #4: And I should not be logged in as "admin"
|
|
288
|
+
* Example #5: Then I should be logged in as "portal"
|
|
289
|
+
*
|
|
290
|
+
*/
|
|
291
|
+
Then(/^(I |we )*should( not)* be logged in as "([^"]*)"$/, async function (pronounCase, notCase, expected) {
|
|
292
|
+
const session = await sessionInfo(this);
|
|
293
|
+
const matches = Boolean(session.uid) && (session.username === expected || session.name === expected);
|
|
294
|
+
if (Boolean(notCase) === matches) {
|
|
295
|
+
throw friendly({
|
|
296
|
+
message: notCase ? `Expected the session not to belong to "${expected}".` : `Expected the session to belong to "${expected}".`,
|
|
297
|
+
cause: session.uid ? `the server reports the login "${session.username}" and the name "${session.name}"` : 'the server reports no session for this browser',
|
|
298
|
+
hint: 'The text is compared with the login and with the name of the user, exactly.',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Assert the page is, or is not, the web client: the application internal
|
|
305
|
+
* users work in (`body.o_web_client` with the navigation bar
|
|
306
|
+
* `.o_main_navbar`), as opposed to the website and the portal.
|
|
307
|
+
*
|
|
308
|
+
* Example #1: Then I should be in the web client
|
|
309
|
+
* Example #2: And we should be in the web client
|
|
310
|
+
* Example #3: Then we should be in the backend
|
|
311
|
+
* Example #4: And I should not be in the web client
|
|
312
|
+
* Example #5: Then I should not be in the backend
|
|
313
|
+
*
|
|
314
|
+
*/
|
|
315
|
+
Then(/^(I |we )*should( not)* be in the (?:web client|backend)$/, async function (pronounCase, notCase) {
|
|
316
|
+
await assertPresence(this.page.locator('body.o_web_client .o_main_navbar').first(), notCase, 'the web client (".o_web_client .o_main_navbar")', {
|
|
317
|
+
hint: 'Internal users land on the web client at /web after logging in; portal users are sent to /my and visitors to the login screen.',
|
|
318
|
+
});
|
|
319
|
+
if (!notCase) await settle(this);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Assert the page is, or is not, the portal: the "My Account" area a portal
|
|
324
|
+
* user lands on (`/my`, rendered inside `#wrap.o_portal_wrap`).
|
|
325
|
+
*
|
|
326
|
+
* Example #1: Then I should be in the portal
|
|
327
|
+
* Example #2: And we should be in the portal
|
|
328
|
+
* Example #3: Then we should be in the customer portal
|
|
329
|
+
* Example #4: And I should not be in the portal
|
|
330
|
+
* Example #5: Then I should not be in the customer portal
|
|
331
|
+
*
|
|
332
|
+
*/
|
|
333
|
+
Then(/^(I |we )*should( not)* be in the (?:customer )?portal$/, async function (pronounCase, notCase) {
|
|
334
|
+
await assertPresence(this.page.locator('.o_portal_wrap').first(), notCase, 'the portal ("#wrap.o_portal_wrap")', {
|
|
335
|
+
attached: true,
|
|
336
|
+
hint: 'A portal user lands on /my after logging in; an internal user lands on the web client.',
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Open the user menu: the avatar menu at the right of the web client's
|
|
342
|
+
* navigation bar, or the menu under the user's name in the website header.
|
|
343
|
+
*
|
|
344
|
+
* Example #1: When I open the user menu
|
|
345
|
+
* Example #2: When we open the user menu
|
|
346
|
+
* Example #3: And open the user menu
|
|
347
|
+
* Example #4: When I open the account menu
|
|
348
|
+
* Example #5: And we open the account menu
|
|
349
|
+
*
|
|
350
|
+
*/
|
|
351
|
+
When(/^(I |we )*open the (?:user|account) menu$/, async function (pronounCase) {
|
|
352
|
+
await openUserMenu(this);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Assert the user menu offers, or does not offer, an entry. The menu is
|
|
357
|
+
* opened when it is closed.
|
|
358
|
+
*
|
|
359
|
+
* Example #1: Then I should see "My Profile" in the user menu
|
|
360
|
+
* Example #2: And I should see "Log out" in the user menu
|
|
361
|
+
* Example #3: Then we should see "My Account" in the user menu
|
|
362
|
+
* Example #4: And I should not see "Settings" in the user menu
|
|
363
|
+
* Example #5: Then I should not see "Documentation" in the account menu
|
|
364
|
+
*
|
|
365
|
+
*/
|
|
366
|
+
Then(/^(I |we )*should( not)* see "([^"]*)" in the (?:user|account) menu$/, async function (pronounCase, notCase, text) {
|
|
367
|
+
const menu = await openUserMenu(this);
|
|
368
|
+
const entry = menu.locator('.dropdown-item').filter({ hasText: textPattern(text, { exact: true }) }).first();
|
|
369
|
+
const offered = await listTexts(menu.locator('.dropdown-item'));
|
|
370
|
+
await assertPresence(entry, notCase, `the "${text}" entry of the user menu`, {
|
|
371
|
+
timeout: SHORT_TIMEOUT,
|
|
372
|
+
hint: `The menu offers ${offered}.`,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Choose an entry of the user menu, for example "My Profile". The menu is
|
|
378
|
+
* opened when it is closed.
|
|
379
|
+
*
|
|
380
|
+
* Example #1: When I choose "My Profile" in the user menu
|
|
381
|
+
* Example #2: When we choose "My Account" in the user menu
|
|
382
|
+
* Example #3: And I choose "Shortcuts" in the user menu
|
|
383
|
+
* Example #4: When I choose "My Profile" in the account menu
|
|
384
|
+
* Example #5: And we choose "Log out" in the account menu
|
|
385
|
+
*
|
|
386
|
+
*/
|
|
387
|
+
When(/^(I |we )*choose "([^"]*)" in the (?:user|account) menu$/, async function (pronounCase, text) {
|
|
388
|
+
const menu = await openUserMenu(this);
|
|
389
|
+
const entry = menu.locator('.dropdown-item').filter({ hasText: textPattern(text, { exact: true }) }).first();
|
|
390
|
+
try {
|
|
391
|
+
await entry.waitFor({ state: 'visible', timeout: SHORT_TIMEOUT });
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw friendly({
|
|
394
|
+
message: `Expected a "${text}" entry in the user menu.`,
|
|
395
|
+
cause: `the menu offers ${await listTexts(menu.locator('.dropdown-item'))}`,
|
|
396
|
+
hint: 'The entry is matched by its whole text, case-sensitive.',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
await entry.click();
|
|
400
|
+
await waitForPageLoad(this.page, budget(this));
|
|
401
|
+
await settle(this);
|
|
402
|
+
});
|