@webship-js/odoo-steps 2.0.0-beta1 → 2.0.0-rc1
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 +3 -0
- package/package.json +4 -3
- package/tests/step-definitions/odoo.steps.js +421 -295
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ It is a **plugin for webship-js** — it does not run standalone. It ships one f
|
|
|
20
20
|
|
|
21
21
|
See [tests/step-definitions/odoo.steps.js](tests/step-definitions/odoo.steps.js) for the full list of steps with Gherkin examples.
|
|
22
22
|
|
|
23
|
+
The word "Odoo" is optional in every sentence: `Then I should see the top menu` and `Then I should see the Odoo top menu` are the same step, so a feature written for another webship-js step pack reads the same here. Every assertion takes `not` (`Then I should not see the login form`), waits for the page rather than reading it once, and fails with a message that says what was expected, why it failed and what to check.
|
|
24
|
+
|
|
23
25
|
## Requirements
|
|
24
26
|
|
|
25
27
|
- Node.js >= 20
|
|
@@ -100,6 +102,7 @@ Other scripts:
|
|
|
100
102
|
| --- | --- |
|
|
101
103
|
| `npm test` | Full suite, pretty output |
|
|
102
104
|
| `npm run test:dry` | `--dry-run` — checks for undefined/ambiguous steps only |
|
|
105
|
+
| `npm run test:examples` | No browser: every step's JSDoc carries five `Example #n:` lines that match its own definition and no other, and every feature step resolves to exactly one definition (also runs in CI) |
|
|
103
106
|
| `npm run test:headed` | Headed browser |
|
|
104
107
|
| `npm run test:chromium` / `test:firefox` / `test:webkit` | Pick a browser |
|
|
105
108
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship-js/odoo-steps",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-rc1",
|
|
4
4
|
"description": "Odoo-specific Cucumber step definitions plugin for webship-js — BDD testing for the main pages of any Odoo Website site (Playwright + Cucumber).",
|
|
5
5
|
"main": "tests/step-definitions/odoo.steps.js",
|
|
6
6
|
"files": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "cucumber-js --config cucumber.js",
|
|
13
13
|
"test:dry": "cucumber-js --config cucumber.js --dry-run",
|
|
14
|
+
"test:examples": "node tests/check-examples.js",
|
|
14
15
|
"test:chromium": "BROWSER=chromium cucumber-js --config cucumber.js",
|
|
15
16
|
"test:firefox": "BROWSER=firefox cucumber-js --config cucumber.js",
|
|
16
17
|
"test:webkit": "BROWSER=webkit cucumber-js --config cucumber.js",
|
|
@@ -46,11 +47,11 @@
|
|
|
46
47
|
"node": ">= 20.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependencies": {
|
|
49
|
-
"webship-js": "^2.0.
|
|
50
|
+
"webship-js": "^2.0.6"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"tsx": "^4.19.2",
|
|
53
|
-
"webship-js": "^2.0.
|
|
54
|
+
"webship-js": "^2.0.6"
|
|
54
55
|
},
|
|
55
56
|
"overrides": {
|
|
56
57
|
"@cucumber/cucumber": "^12"
|
|
@@ -3,27 +3,96 @@
|
|
|
3
3
|
// Odoo-specific Cucumber step definitions for webship-js. These steps
|
|
4
4
|
// understand the markup the Odoo Website builder (and the eCommerce, Blog,
|
|
5
5
|
// Events, Jobs and core web-login modules bundled with it) renders on every
|
|
6
|
-
// Odoo site
|
|
6
|
+
// Odoo site: the #wrapwrap/#top_menu layout, o_wblog_* blog templates,
|
|
7
7
|
// o_wsale_* eCommerce templates, o_wevent_* events templates, o_jobs
|
|
8
8
|
// recruitment templates, s_website_form snippet forms, and the core
|
|
9
|
-
// /web/login screen
|
|
10
|
-
//
|
|
9
|
+
// /web/login screen. Scenarios assert against the main pages of an Odoo site
|
|
10
|
+
// without hand-rolling CSS selectors in every feature file.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// The Gherkin phrasing stays product-agnostic: the word "Odoo" is optional in
|
|
13
|
+
// every sentence ("I should see the top menu" and "I should see the Odoo top
|
|
14
|
+
// menu" are the same step), so a feature written for another step pack reads
|
|
15
|
+
// the same here. Only these comments and the error messages name the product,
|
|
16
|
+
// to document which convention each step targets.
|
|
17
17
|
//
|
|
18
18
|
// Composes with webship-js's built-in steps (navigation.steps.js,
|
|
19
|
-
// assertion.steps.js, form.steps.js, response.steps.js,
|
|
20
|
-
// duplicating them
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// assertion.steps.js, form.steps.js, response.steps.js, ...) rather than
|
|
20
|
+
// duplicating them: use those directly in feature files wherever they already
|
|
21
|
+
// cover the need ("the response status code should be 200", "the path should
|
|
22
|
+
// be "/web"").
|
|
23
23
|
|
|
24
24
|
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const { gotoUrl, waitForPageLoad, fillField, friendly } = require('webship-js/tests/step-definitions/webship');
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
// Kept well under webship-js's 45s step timeout so a locator that never
|
|
32
|
+
// settles fails through the authored message (with its hint), not as
|
|
33
|
+
// "function timed out".
|
|
34
|
+
const WAIT_TIMEOUT = 30000;
|
|
35
|
+
|
|
36
|
+
// The per-page wait budget from `worldParameters.minWaitTime.page`.
|
|
37
|
+
function budget(world) {
|
|
38
|
+
return (world && world.minWaitTime && world.minWaitTime.page) || 3000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Join the launch URL and a path with exactly one slash.
|
|
42
|
+
function siteUrl(world, path) {
|
|
43
|
+
return String(world.launchUrl).replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Open a path on the site under test and wait for the page to settle.
|
|
47
|
+
async function navigate(world, path) {
|
|
48
|
+
await gotoUrl(world.page, siteUrl(world, path));
|
|
49
|
+
await world.page.waitForSelector('body', { state: 'attached', timeout: WAIT_TIMEOUT });
|
|
50
|
+
await waitForPageLoad(world.page, budget(world));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Assert an element is present or, when the step said "not", absent. The
|
|
54
|
+
// assertion is the wait: Playwright polls the locator for the wanted state and
|
|
55
|
+
// a miss fails through a friendly message with a hint. `attached` picks
|
|
56
|
+
// presence-in-DOM over visibility for template markers a theme may render
|
|
57
|
+
// off-screen.
|
|
58
|
+
async function assertPresence(locator, notCase, label, { attached = false, hint, timeout = WAIT_TIMEOUT } = {}) {
|
|
59
|
+
const state = attached ? (notCase ? 'detached' : 'attached') : (notCase ? 'hidden' : 'visible');
|
|
60
|
+
try {
|
|
61
|
+
await locator.waitFor({ state, timeout });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw friendly({
|
|
64
|
+
message: notCase ? `Expected ${label} to be absent.` : `Expected ${label} to be present.`,
|
|
65
|
+
cause: error,
|
|
66
|
+
hint,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Whitespace-tolerant, case-sensitive regex for "contains this text", so a
|
|
72
|
+
// Playwright `hasText` filter can be the wait.
|
|
73
|
+
function textPattern(text, { exact = false } = {}) {
|
|
74
|
+
const body = text.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s+');
|
|
75
|
+
return new RegExp(exact ? `^\\s*${body}\\s*$` : body);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Assert a locator's text contains (or, with `notCase`, does not contain) the
|
|
79
|
+
// expected text: the locator is narrowed to "has this text" and Playwright
|
|
80
|
+
// polls for that narrowed set to exist or to be gone.
|
|
81
|
+
async function assertText(locator, expectedText, notCase, label, { exact = false, hint, timeout = WAIT_TIMEOUT } = {}) {
|
|
82
|
+
const withText = locator.filter({ hasText: textPattern(expectedText, { exact }) });
|
|
83
|
+
try {
|
|
84
|
+
await withText.waitFor({ state: notCase ? 'detached' : 'attached', timeout });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const text = ((await locator.textContent({ timeout: 1000 }).catch(() => '')) || '').replace(/\s+/g, ' ').trim();
|
|
87
|
+
throw friendly({
|
|
88
|
+
message: notCase
|
|
89
|
+
? `Expected ${label} not to ${exact ? 'read' : 'contain'} "${expectedText}".`
|
|
90
|
+
: `Expected ${label} to ${exact ? 'read' : 'contain'} "${expectedText}".`,
|
|
91
|
+
cause: `it reads "${text}"`,
|
|
92
|
+
hint,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
27
96
|
|
|
28
97
|
// Fill and submit Odoo's login form. The submit is dispatched from inside the
|
|
29
98
|
// page rather than with a pointer click: a fixed overlay (a cookie bar, a chat
|
|
@@ -34,7 +103,18 @@ const { gotoUrl, waitForPageLoad, fillField } = require('webship-js/tests/step-d
|
|
|
34
103
|
// fires there and the shared settle helpers would not settle: wait for the
|
|
35
104
|
// navigation and DOM content only. An invalid login stays on /web/login
|
|
36
105
|
// without a full navigation, so this resolves quickly either way.
|
|
37
|
-
async function submitLogin(
|
|
106
|
+
async function submitLogin(world, username, password) {
|
|
107
|
+
const page = world.page;
|
|
108
|
+
try {
|
|
109
|
+
await page.locator('#login').waitFor({ state: 'visible', timeout: budget(world) });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw friendly({
|
|
112
|
+
action: 'find the login form',
|
|
113
|
+
target: 'form[action="/web/login"]',
|
|
114
|
+
cause: error,
|
|
115
|
+
hint: 'Open the login page first ("Given I am on the login page"); a site that replaces /web/login needs its own step.',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
38
118
|
await fillField(page, 'login', username);
|
|
39
119
|
await fillField(page, 'password', password);
|
|
40
120
|
await Promise.all([
|
|
@@ -54,63 +134,58 @@ async function submitLogin(page, username, password) {
|
|
|
54
134
|
// ---------------------------------------------------------------------------
|
|
55
135
|
|
|
56
136
|
/**
|
|
57
|
-
* Assert the current page was rendered by the Odoo Website
|
|
58
|
-
* has the `#wrapwrap` element every Odoo Website page wraps its
|
|
59
|
-
* main
|
|
60
|
-
* Blog, Events, Jobs, ...) rendered the page.
|
|
137
|
+
* Assert the current page was, or was not, rendered by the Odoo Website
|
|
138
|
+
* builder: it has the `#wrapwrap` element every Odoo Website page wraps its
|
|
139
|
+
* header, main and footer in, regardless of which front-end app (Website,
|
|
140
|
+
* eCommerce, Blog, Events, Jobs, ...) rendered the page.
|
|
61
141
|
*
|
|
62
142
|
* Example #1: Then I should be on an Odoo website page
|
|
63
|
-
* Example #2: And I should be on
|
|
143
|
+
* Example #2: And I should be on a website page
|
|
64
144
|
* Example #3: Then we should be on an Odoo website page
|
|
65
|
-
* Example #4: And should be on
|
|
66
|
-
* Example #5: Then I should be on
|
|
145
|
+
* Example #4: And should not be on a website page
|
|
146
|
+
* Example #5: Then I should not be on an Odoo website page
|
|
67
147
|
*
|
|
68
148
|
*/
|
|
69
|
-
Then(/^(I |we )*should be on an? Odoo website page$/, async function (pronounCase) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
149
|
+
Then(/^(I |we )*should( not)* be on an? (?:Odoo )?website page$/, async function (pronounCase, notCase) {
|
|
150
|
+
await assertPresence(this.page.locator('#wrapwrap'), notCase, 'the Website wrapper ("#wrapwrap")', {
|
|
151
|
+
attached: true,
|
|
152
|
+
hint: 'Every page the Odoo Website builder renders carries #wrapwrap; the backend webclient (/web) and the login screen do not.',
|
|
153
|
+
});
|
|
75
154
|
});
|
|
76
155
|
|
|
77
156
|
/**
|
|
78
|
-
* Assert the
|
|
79
|
-
* `#top_menu`, the `<ul>` every Odoo Website header renders its menu
|
|
80
|
-
* into.
|
|
157
|
+
* Assert the Website top menu (main navigation) is, or is not, visible.
|
|
158
|
+
* Matches `#top_menu`, the `<ul>` every Odoo Website header renders its menu
|
|
159
|
+
* items into.
|
|
81
160
|
*
|
|
82
161
|
* Example #1: Then I should see the Odoo top menu
|
|
83
|
-
* Example #2: And I should see the
|
|
162
|
+
* Example #2: And I should see the main menu
|
|
84
163
|
* Example #3: Then we should see the Odoo menu
|
|
85
|
-
* Example #4: And should see the
|
|
86
|
-
* Example #5: Then I should see the
|
|
164
|
+
* Example #4: And should not see the top menu
|
|
165
|
+
* Example #5: Then I should see the main navigation menu
|
|
87
166
|
*
|
|
88
167
|
*/
|
|
89
|
-
Then(/^(I |we )*should see the Odoo (?:top |main )?(?:menu|navigation menu)$/, async function (pronounCase) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
'Expected the Odoo Website top menu ("#top_menu") to be visible'
|
|
94
|
-
);
|
|
168
|
+
Then(/^(I |we )*should( not)* see the (?:Odoo )?(?:top |main )?(?:menu|navigation menu)$/, async function (pronounCase, notCase) {
|
|
169
|
+
await assertPresence(this.page.locator('#top_menu').first(), notCase, 'the Website top menu ("#top_menu")', {
|
|
170
|
+
hint: 'The header renders #top_menu on every Website page; a header template without a menu, or a hidden header, leaves it out.',
|
|
171
|
+
});
|
|
95
172
|
});
|
|
96
173
|
|
|
97
174
|
/**
|
|
98
|
-
* Assert the
|
|
99
|
-
* element every Odoo Website footer snippet renders into.
|
|
175
|
+
* Assert the Website footer is, or is not, visible. Matches `footer#bottom`,
|
|
176
|
+
* the element every Odoo Website footer snippet renders into.
|
|
100
177
|
*
|
|
101
178
|
* Example #1: Then I should see the Odoo footer
|
|
102
|
-
* Example #2: And I should see the
|
|
179
|
+
* Example #2: And I should see the site footer
|
|
103
180
|
* Example #3: Then we should see the Odoo page footer
|
|
104
|
-
* Example #4: And should see the
|
|
105
|
-
* Example #5: Then I should see the Odoo site footer
|
|
181
|
+
* Example #4: And should not see the footer
|
|
182
|
+
* Example #5: Then I should not see the Odoo site footer
|
|
106
183
|
*
|
|
107
184
|
*/
|
|
108
|
-
Then(/^(I |we )*should see the Odoo (?:site |page )?footer$/, async function (pronounCase) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'Expected the Odoo Website footer ("footer#bottom") to be visible'
|
|
113
|
-
);
|
|
185
|
+
Then(/^(I |we )*should( not)* see the (?:Odoo )?(?:site |page )?footer$/, async function (pronounCase, notCase) {
|
|
186
|
+
await assertPresence(this.page.locator('footer#bottom, #footer').first(), notCase, 'the Website footer ("footer#bottom")', {
|
|
187
|
+
hint: 'The footer can be hidden per page in the Website builder (page properties); check the page, not the theme.',
|
|
188
|
+
});
|
|
114
189
|
});
|
|
115
190
|
|
|
116
191
|
// ---------------------------------------------------------------------------
|
|
@@ -118,64 +193,65 @@ Then(/^(I |we )*should see the Odoo (?:site |page )?footer$/, async function (pr
|
|
|
118
193
|
// ---------------------------------------------------------------------------
|
|
119
194
|
|
|
120
195
|
/**
|
|
121
|
-
* Assert the current page is the
|
|
122
|
-
* `#o_wblog_index_content`, rendered by the Blog app's post-list view.
|
|
196
|
+
* Assert the current page is, or is not, the Blog index/listing template.
|
|
197
|
+
* Matches `#o_wblog_index_content`, rendered by the Blog app's post-list view.
|
|
123
198
|
*
|
|
124
199
|
* Example #1: Then I should be on the Odoo blog page
|
|
125
|
-
* Example #2: And I should be on the
|
|
200
|
+
* Example #2: And I should be on the blog index
|
|
126
201
|
* Example #3: Then we should be on the Odoo blog page
|
|
127
|
-
* Example #4: And should be on the
|
|
128
|
-
* Example #5: Then I should be on the
|
|
202
|
+
* Example #4: And should not be on the blog index
|
|
203
|
+
* Example #5: Then I should be on the blog listing page
|
|
129
204
|
*
|
|
130
205
|
*/
|
|
131
|
-
Then(/^(I |we )*should be on the Odoo blog (?:index|page|listing page)$/, async function (pronounCase) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
206
|
+
Then(/^(I |we )*should( not)* be on the (?:Odoo )?blog (?:index|page|listing page)$/, async function (pronounCase, notCase) {
|
|
207
|
+
await assertPresence(this.page.locator('#o_wblog_index_content'), notCase, 'the Blog index template ("#o_wblog_index_content")', {
|
|
208
|
+
attached: true,
|
|
209
|
+
hint: 'The Blog index lives at /blog once the website_blog app is installed and at least one blog exists.',
|
|
210
|
+
});
|
|
137
211
|
});
|
|
138
212
|
|
|
139
213
|
/**
|
|
140
|
-
* Assert the current page is a single
|
|
141
|
-
* `.o_wblog_post_title`, an element only the single-post view renders
|
|
142
|
-
* index view's post teaser cards do not include it).
|
|
214
|
+
* Assert the current page is, or is not, a single blog post template.
|
|
215
|
+
* Matches `.o_wblog_post_title`, an element only the single-post view renders
|
|
216
|
+
* (the index view's post teaser cards do not include it).
|
|
143
217
|
*
|
|
144
218
|
* Example #1: Then I should be on an Odoo blog post page
|
|
145
|
-
* Example #2: And I should be on a
|
|
146
|
-
* Example #3: Then we should be on
|
|
147
|
-
* Example #4: And should be on
|
|
148
|
-
* Example #5: Then I should be on the
|
|
219
|
+
* Example #2: And I should be on a blog post page
|
|
220
|
+
* Example #3: Then we should be on the Odoo single blog post page
|
|
221
|
+
* Example #4: And should not be on a blog post page
|
|
222
|
+
* Example #5: Then I should not be on the single blog post page
|
|
149
223
|
*
|
|
150
224
|
*/
|
|
151
|
-
Then(/^(I |we )*should be on an
|
|
225
|
+
Then(/^(I |we )*should( not)* be on (?:an?|the) (?:Odoo )?(?:single )?blog post page$/, async function (pronounCase, notCase) {
|
|
152
226
|
// .first(): the single-post template can also render a "related posts"
|
|
153
227
|
// teaser section further down the page reusing the same title class.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
228
|
+
await assertPresence(this.page.locator('.o_wblog_post_title').first(), notCase, 'the single blog post template (".o_wblog_post_title")', {
|
|
229
|
+
attached: true,
|
|
230
|
+
hint: 'Open a post from the blog index first; the index itself renders teaser cards without this title element.',
|
|
231
|
+
});
|
|
159
232
|
});
|
|
160
233
|
|
|
161
234
|
/**
|
|
162
|
-
* Assert the
|
|
163
|
-
* `.o_wblog_post_title`, the single-post view's title element
|
|
235
|
+
* Assert the blog post on screen has, or does not have, the given title.
|
|
236
|
+
* Reads `.o_wblog_post_title`, the single-post view's title element, and
|
|
237
|
+
* waits for the text to be there.
|
|
164
238
|
*
|
|
165
239
|
* Example #1: Then I should see the Odoo blog post titled "Customer Reviews"
|
|
166
|
-
* Example #2: And I should see the
|
|
240
|
+
* Example #2: And I should see the blog post titled "Employee Testimonials"
|
|
167
241
|
* Example #3: Then we should see the Odoo blog post titled "Release Notes"
|
|
168
|
-
* Example #4: And should see the
|
|
169
|
-
* Example #5: Then I should see the
|
|
242
|
+
* Example #4: And should not see the blog post titled "Product Update"
|
|
243
|
+
* Example #5: Then I should see the blog post titled "Company News"
|
|
170
244
|
*
|
|
171
245
|
*/
|
|
172
|
-
Then(/^(I |we )*should see the Odoo blog post titled "([^"]*)?"$/, async function (pronounCase, expectedTitle) {
|
|
246
|
+
Then(/^(I |we )*should( not)* see the (?:Odoo )?blog post titled "([^"]*)?"$/, async function (pronounCase, notCase, expectedTitle) {
|
|
173
247
|
const title = this.page.locator('.o_wblog_post_title').first();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
248
|
+
await assertPresence(title, undefined, 'the blog post title (".o_wblog_post_title")', {
|
|
249
|
+
attached: true,
|
|
250
|
+
hint: 'Open a single blog post first.',
|
|
251
|
+
});
|
|
252
|
+
await assertText(title, expectedTitle, notCase, 'the blog post title', {
|
|
253
|
+
hint: 'Text is matched as a substring, whitespace-collapsed but case-sensitive.',
|
|
254
|
+
});
|
|
179
255
|
});
|
|
180
256
|
|
|
181
257
|
// ---------------------------------------------------------------------------
|
|
@@ -183,147 +259,198 @@ Then(/^(I |we )*should see the Odoo blog post titled "([^"]*)?"$/, async functio
|
|
|
183
259
|
// ---------------------------------------------------------------------------
|
|
184
260
|
|
|
185
261
|
/**
|
|
186
|
-
* Assert the current page is the
|
|
262
|
+
* Assert the current page is, or is not, the eCommerce shop/catalog template.
|
|
187
263
|
* Matches `#products_grid`, the container the "website_sale" module renders
|
|
188
264
|
* the product listing into.
|
|
189
265
|
*
|
|
190
266
|
* Example #1: Then I should be on the Odoo shop page
|
|
191
|
-
* Example #2: And I should be on the
|
|
267
|
+
* Example #2: And I should be on the shop index
|
|
192
268
|
* Example #3: Then we should be on the Odoo shop page
|
|
193
|
-
* Example #4: And should be on the
|
|
194
|
-
* Example #5: Then I should be on the
|
|
269
|
+
* Example #4: And should not be on the shop index
|
|
270
|
+
* Example #5: Then I should be on the shop catalog page
|
|
195
271
|
*
|
|
196
272
|
*/
|
|
197
|
-
Then(/^(I |we )*should be on the Odoo shop (?:page|index|catalog page)$/, async function (pronounCase) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
);
|
|
273
|
+
Then(/^(I |we )*should( not)* be on the (?:Odoo )?shop (?:page|index|catalog page)$/, async function (pronounCase, notCase) {
|
|
274
|
+
await assertPresence(this.page.locator('#products_grid'), notCase, 'the shop product grid ("#products_grid")', {
|
|
275
|
+
attached: true,
|
|
276
|
+
hint: 'The shop lives at /shop once the website_sale app is installed and at least one product is published.',
|
|
277
|
+
});
|
|
203
278
|
});
|
|
204
279
|
|
|
205
280
|
/**
|
|
206
|
-
* Assert the current page is an
|
|
281
|
+
* Assert the current page is, or is not, an eCommerce product page. Matches
|
|
207
282
|
* `.o_wsale_product_page`, rendered by "website_sale" for every product
|
|
208
283
|
* detail page.
|
|
209
284
|
*
|
|
210
285
|
* Example #1: Then I should be on an Odoo product page
|
|
211
|
-
* Example #2: And I should be on a
|
|
212
|
-
* Example #3: Then we should be on
|
|
213
|
-
* Example #4: And should be on
|
|
214
|
-
* Example #5: Then I should be on the
|
|
286
|
+
* Example #2: And I should be on a shop product page
|
|
287
|
+
* Example #3: Then we should be on the Odoo product detail page
|
|
288
|
+
* Example #4: And should not be on a product page
|
|
289
|
+
* Example #5: Then I should not be on the product detail page
|
|
215
290
|
*
|
|
216
291
|
*/
|
|
217
|
-
Then(/^(I |we )*should be on an
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
292
|
+
Then(/^(I |we )*should( not)* be on (?:an?|the) (?:Odoo )?(?:shop )?product(?: detail)? page$/, async function (pronounCase, notCase) {
|
|
293
|
+
await assertPresence(this.page.locator('.o_wsale_product_page'), notCase, 'the product page (".o_wsale_product_page")', {
|
|
294
|
+
attached: true,
|
|
295
|
+
hint: 'Open a product from the shop grid first ("When I open the first product in the shop").',
|
|
296
|
+
});
|
|
223
297
|
});
|
|
224
298
|
|
|
225
299
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
300
|
+
* Open a product from the shop grid on screen: the first one by default, or
|
|
301
|
+
* the n-th. Reads the product link from `#products_grid` and navigates to it,
|
|
302
|
+
* so no product slug appears in the feature file.
|
|
303
|
+
*
|
|
304
|
+
* Example #1: When I open the first product in the Odoo shop
|
|
305
|
+
* Example #2: And I open the first product in the shop
|
|
306
|
+
* Example #3: When we open the 2nd product in the shop
|
|
307
|
+
* Example #4: And open the first item in the Odoo shop
|
|
308
|
+
* Example #5: When I open the 3rd product in the shop
|
|
309
|
+
*
|
|
310
|
+
*/
|
|
311
|
+
When(/^(I |we )*open the (first|\d+(?:st|nd|rd|th)) (?:product|item) in the (?:Odoo )?shop$/, async function (pronounCase, position) {
|
|
312
|
+
const index = position === 'first' ? 0 : parseInt(position, 10) - 1;
|
|
313
|
+
const links = this.page.locator('#products_grid .oe_product a[href*="/shop/"]');
|
|
314
|
+
const link = links.nth(index);
|
|
315
|
+
try {
|
|
316
|
+
await link.waitFor({ state: 'attached', timeout: WAIT_TIMEOUT });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw friendly({
|
|
319
|
+
action: 'find a product in the shop grid',
|
|
320
|
+
target: `product ${index + 1} in "#products_grid"`,
|
|
321
|
+
cause: error,
|
|
322
|
+
hint: 'Open the shop page first and make sure the catalog lists at least that many published products.',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const href = await link.getAttribute('href');
|
|
326
|
+
if (!href) {
|
|
327
|
+
throw friendly({
|
|
328
|
+
message: `Product ${index + 1} in the shop grid has no link.`,
|
|
329
|
+
hint: 'The product card should link to its /shop/<product> page; check the grid template.',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
await navigate(this, href);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Submit the shop's search form with the given term. Fills the
|
|
337
|
+
* `input[name="search"]` scoped to `.o_wsale_products_searchbar_form`, the
|
|
228
338
|
* shop listing's own search box, as opposed to the (hidden until toggled)
|
|
229
|
-
* search field "website_sale" also renders in the page header
|
|
339
|
+
* search field "website_sale" also renders in the page header, and presses
|
|
230
340
|
* Enter.
|
|
231
341
|
*
|
|
232
342
|
* Example #1: When I search the Odoo shop for "mug"
|
|
233
|
-
* Example #2: When we search the
|
|
234
|
-
* Example #3:
|
|
235
|
-
* Example #4: When we search the
|
|
236
|
-
* Example #5:
|
|
343
|
+
* Example #2: When we search the shop for "polo shirt"
|
|
344
|
+
* Example #3: And search the Odoo shop for "t-shirt"
|
|
345
|
+
* Example #4: When we search the shop for "backpack"
|
|
346
|
+
* Example #5: And I search the shop for "notebook"
|
|
237
347
|
*
|
|
238
348
|
*/
|
|
239
|
-
When(/^(I |we )*search the Odoo shop for "([^"]*)?"$/, async function (pronounCase, term) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
349
|
+
When(/^(I |we )*search the (?:Odoo )?shop for "([^"]*)?"$/, async function (pronounCase, term) {
|
|
350
|
+
const searchField = this.page.locator('.o_wsale_products_searchbar_form input[name="search"]').first();
|
|
351
|
+
try {
|
|
352
|
+
await searchField.waitFor({ state: 'visible', timeout: WAIT_TIMEOUT });
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw friendly({
|
|
355
|
+
action: 'find the shop search box',
|
|
356
|
+
target: '.o_wsale_products_searchbar_form input[name="search"]',
|
|
357
|
+
cause: error,
|
|
358
|
+
hint: 'Open the shop page first; the search bar is a shop option a site can turn off in the Website builder.',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
245
361
|
await searchField.fill(term);
|
|
246
362
|
await searchField.press('Enter');
|
|
247
|
-
await waitForPageLoad(this.page,
|
|
363
|
+
await waitForPageLoad(this.page, budget(this));
|
|
248
364
|
});
|
|
249
365
|
|
|
250
366
|
/**
|
|
251
|
-
* Assert the
|
|
252
|
-
*
|
|
367
|
+
* Assert the shop search returned results for the given term: the product
|
|
368
|
+
* grid is present and the URL's `search` query parameter carries the term.
|
|
253
369
|
*
|
|
254
370
|
* Example #1: Then I should see Odoo shop search results for "mug"
|
|
255
|
-
* Example #2: And I should see
|
|
371
|
+
* Example #2: And I should see shop search results for "polo shirt"
|
|
256
372
|
* Example #3: Then we should see Odoo shop search results for "t-shirt"
|
|
257
|
-
* Example #4: And should see
|
|
258
|
-
* Example #5: Then I should see
|
|
373
|
+
* Example #4: And should see shop search results for "backpack"
|
|
374
|
+
* Example #5: Then I should see the shop search results for "notebook"
|
|
259
375
|
*
|
|
260
376
|
*/
|
|
261
|
-
Then(/^(I |we )*should see Odoo shop search results for "([^"]*)?"$/, async function (pronounCase, term) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const encoded = encodeURIComponent(term)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
377
|
+
Then(/^(I |we )*should see (?:the )?(?:Odoo )?shop search results for "([^"]*)?"$/, async function (pronounCase, term) {
|
|
378
|
+
await assertPresence(this.page.locator('#products_grid'), undefined, 'the shop product grid ("#products_grid")', {
|
|
379
|
+
attached: true,
|
|
380
|
+
hint: 'A search that matches nothing still renders the grid with an empty-state message; a missing grid means the search left the shop.',
|
|
381
|
+
});
|
|
382
|
+
// A space reaches the URL as `+` or `%20` depending on whether the form or
|
|
383
|
+
// a direct navigation submitted it.
|
|
384
|
+
const encoded = encodeURIComponent(term);
|
|
385
|
+
const carriesTerm = (url) => url.includes(`search=${encoded.replace(/%20/g, '+')}`) || url.includes(`search=${encoded}`);
|
|
386
|
+
try {
|
|
387
|
+
await this.page.waitForURL((url) => carriesTerm(String(url)), { timeout: budget(this) });
|
|
388
|
+
} catch (error) {
|
|
389
|
+
throw friendly({
|
|
390
|
+
message: `Expected the shop URL to carry the search term "${term}".`,
|
|
391
|
+
cause: `the URL is ${this.page.url()}`,
|
|
392
|
+
hint: 'Run "When I search the shop for ..." first, with the same term.',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
273
395
|
});
|
|
274
396
|
|
|
275
397
|
/**
|
|
276
|
-
* Add the
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
398
|
+
* Add the product on screen to the cart. Clicks `#add_to_cart`, the
|
|
399
|
+
* "website_sale" add-to-cart control every product page renders. When the
|
|
400
|
+
* "sale_product_configurator" module is installed (a website_sale dependency
|
|
401
|
+
* as of Odoo 17), this opens an "Added to your cart" dialog instead of
|
|
402
|
+
* updating the cart directly; the step then dismisses it through its
|
|
403
|
+
* "Continue Shopping" action, which keeps the item in the cart without
|
|
404
|
+
* leaving the product page.
|
|
283
405
|
*
|
|
284
406
|
* Example #1: When I add the product to the Odoo cart
|
|
285
|
-
* Example #2: When we add this product to the
|
|
286
|
-
* Example #3:
|
|
287
|
-
* Example #4: When we add the product to the
|
|
288
|
-
* Example #5:
|
|
407
|
+
* Example #2: When we add this product to the cart
|
|
408
|
+
* Example #3: And add the current product to the Odoo cart
|
|
409
|
+
* Example #4: When we add the product to the cart
|
|
410
|
+
* Example #5: And I add this product to the cart
|
|
289
411
|
*
|
|
290
412
|
*/
|
|
291
|
-
When(/^(I |we )*add (?:the |this )?(?:current )?product to the Odoo cart$/, async function (pronounCase) {
|
|
292
|
-
const defaultTime = this.minWaitTime.page || 3000;
|
|
413
|
+
When(/^(I |we )*add (?:the |this )?(?:current )?product to the (?:Odoo )?cart$/, async function (pronounCase) {
|
|
293
414
|
const addToCart = this.page.locator('#add_to_cart').first();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
415
|
+
try {
|
|
416
|
+
await addToCart.waitFor({ state: 'visible', timeout: WAIT_TIMEOUT });
|
|
417
|
+
await addToCart.click();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw friendly({
|
|
420
|
+
action: 'click the add-to-cart button',
|
|
421
|
+
target: '#add_to_cart',
|
|
422
|
+
cause: error,
|
|
423
|
+
hint: 'Open a product page first; a product that is out of stock or not published has no add-to-cart button.',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
const continueShopping = this.page.getByRole('button', { name: /Continue Shopping/i }).or(this.page.getByText('Continue Shopping')).first();
|
|
297
427
|
if (await continueShopping.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
298
428
|
await continueShopping.click();
|
|
299
429
|
}
|
|
300
|
-
await waitForPageLoad(this.page,
|
|
430
|
+
await waitForPageLoad(this.page, budget(this));
|
|
301
431
|
});
|
|
302
432
|
|
|
303
433
|
/**
|
|
304
|
-
* Assert the
|
|
434
|
+
* Assert the cart badge shows, or does not show, the given item count. Reads
|
|
305
435
|
* `.my_cart_quantity`, the header cart-icon badge "website_sale" updates
|
|
306
|
-
* whenever the session cart changes.
|
|
436
|
+
* whenever the session cart changes, and waits for it to read that number.
|
|
307
437
|
*
|
|
308
438
|
* Example #1: Then I should see "1" item in the Odoo cart
|
|
309
|
-
* Example #2: And I should see "2" items in the
|
|
439
|
+
* Example #2: And I should see "2" items in the cart
|
|
310
440
|
* Example #3: Then we should see "1" item in the Odoo cart
|
|
311
|
-
* Example #4: And should see "3" items in the
|
|
312
|
-
* Example #5: Then I should see "0" items in the
|
|
441
|
+
* Example #4: And should not see "3" items in the cart
|
|
442
|
+
* Example #5: Then I should see "0" items in the cart
|
|
313
443
|
*
|
|
314
444
|
*/
|
|
315
|
-
Then(/^(I |we )*should see "([^"]*)?" items? in the Odoo cart$/, async function (pronounCase, expectedCount) {
|
|
445
|
+
Then(/^(I |we )*should( not)* see "([^"]*)?" items? in the (?:Odoo )?cart$/, async function (pronounCase, notCase, expectedCount) {
|
|
316
446
|
const badge = this.page.locator('.my_cart_quantity').first();
|
|
317
|
-
await
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
String(expectedCount),
|
|
325
|
-
`Expected the Odoo cart quantity badge to read "${expectedCount}" but found "${text}"`
|
|
326
|
-
);
|
|
447
|
+
await assertPresence(badge, undefined, 'the cart quantity badge (".my_cart_quantity")', {
|
|
448
|
+
hint: 'The badge sits on the header cart icon; a header without the cart link (an eCommerce option) has no badge.',
|
|
449
|
+
});
|
|
450
|
+
await assertText(badge, String(expectedCount), notCase, 'the cart quantity badge', {
|
|
451
|
+
exact: true,
|
|
452
|
+
hint: 'The badge is updated by the add-to-cart request; a configurator dialog left open keeps the old count.',
|
|
453
|
+
});
|
|
327
454
|
});
|
|
328
455
|
|
|
329
456
|
// ---------------------------------------------------------------------------
|
|
@@ -331,22 +458,21 @@ Then(/^(I |we )*should see "([^"]*)?" items? in the Odoo cart$/, async function
|
|
|
331
458
|
// ---------------------------------------------------------------------------
|
|
332
459
|
|
|
333
460
|
/**
|
|
334
|
-
* Assert the current page is the
|
|
461
|
+
* Assert the current page is, or is not, the Events index/listing template.
|
|
335
462
|
* Matches `.o_wevent_index`, rendered by the Events app's event-list view.
|
|
336
463
|
*
|
|
337
464
|
* Example #1: Then I should be on the Odoo events page
|
|
338
|
-
* Example #2: And I should be on the
|
|
465
|
+
* Example #2: And I should be on the event page
|
|
339
466
|
* Example #3: Then we should be on the Odoo events page
|
|
340
|
-
* Example #4: And should be on the
|
|
341
|
-
* Example #5: Then I should be on the
|
|
467
|
+
* Example #4: And should not be on the events index
|
|
468
|
+
* Example #5: Then I should be on the events listing page
|
|
342
469
|
*
|
|
343
470
|
*/
|
|
344
|
-
Then(/^(I |we )*should be on the Odoo events? (?:page|index|listing page)$/, async function (pronounCase) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
);
|
|
471
|
+
Then(/^(I |we )*should( not)* be on the (?:Odoo )?events? (?:page|index|listing page)$/, async function (pronounCase, notCase) {
|
|
472
|
+
await assertPresence(this.page.locator('.o_wevent_index, .o_wevent_events_list').first(), notCase, 'the Events index template (".o_wevent_index")', {
|
|
473
|
+
attached: true,
|
|
474
|
+
hint: 'The events list lives at /event once the website_event app is installed.',
|
|
475
|
+
});
|
|
350
476
|
});
|
|
351
477
|
|
|
352
478
|
// ---------------------------------------------------------------------------
|
|
@@ -354,42 +480,40 @@ Then(/^(I |we )*should be on the Odoo events? (?:page|index|listing page)$/, asy
|
|
|
354
480
|
// ---------------------------------------------------------------------------
|
|
355
481
|
|
|
356
482
|
/**
|
|
357
|
-
* Assert the current page is the
|
|
358
|
-
* Matches `#jobs_grid`, rendered by the Recruitment app's public job
|
|
483
|
+
* Assert the current page is, or is not, the Jobs (recruitment) listing
|
|
484
|
+
* template. Matches `#jobs_grid`, rendered by the Recruitment app's public job
|
|
485
|
+
* board.
|
|
359
486
|
*
|
|
360
487
|
* Example #1: Then I should be on the Odoo jobs page
|
|
361
|
-
* Example #2: And I should be on the
|
|
488
|
+
* Example #2: And I should be on the careers page
|
|
362
489
|
* Example #3: Then we should be on the Odoo jobs page
|
|
363
|
-
* Example #4: And should be on the
|
|
364
|
-
* Example #5: Then I should be on the
|
|
490
|
+
* Example #4: And should not be on the jobs listing page
|
|
491
|
+
* Example #5: Then I should be on the recruitment page
|
|
365
492
|
*
|
|
366
493
|
*/
|
|
367
|
-
Then(/^(I |we )*should be on the Odoo (?:jobs|careers|recruitment) (?:page|listing page)$/, async function (pronounCase) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
);
|
|
494
|
+
Then(/^(I |we )*should( not)* be on the (?:Odoo )?(?:jobs|careers|recruitment) (?:page|listing page)$/, async function (pronounCase, notCase) {
|
|
495
|
+
await assertPresence(this.page.locator('#jobs_grid'), notCase, 'the Jobs listing template ("#jobs_grid")', {
|
|
496
|
+
attached: true,
|
|
497
|
+
hint: 'The job board lives at /jobs once the hr_recruitment app is installed with its website module.',
|
|
498
|
+
});
|
|
373
499
|
});
|
|
374
500
|
|
|
375
501
|
/**
|
|
376
|
-
* Assert at least one job opening link is visible on the
|
|
377
|
-
* Matches links whose `href` contains `/jobs/`, the URL pattern every
|
|
378
|
-
* posting page uses.
|
|
502
|
+
* Assert at least one job opening link is, or none is, visible on the Jobs
|
|
503
|
+
* page. Matches links whose `href` contains `/jobs/`, the URL pattern every
|
|
504
|
+
* job posting page uses.
|
|
379
505
|
*
|
|
380
506
|
* Example #1: Then I should see Odoo job openings
|
|
381
|
-
* Example #2: And I should see
|
|
507
|
+
* Example #2: And I should see job listings
|
|
382
508
|
* Example #3: Then we should see Odoo job openings
|
|
383
|
-
* Example #4: And should see
|
|
384
|
-
* Example #5: Then I should see
|
|
509
|
+
* Example #4: And should not see job listings
|
|
510
|
+
* Example #5: Then I should see job postings
|
|
385
511
|
*
|
|
386
512
|
*/
|
|
387
|
-
Then(/^(I |we )*should see Odoo job (?:openings|listings|postings)$/, async function (pronounCase) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
'Expected at least one Odoo job opening link ("a[href*=\\"/jobs/\\"]") to be visible'
|
|
392
|
-
);
|
|
513
|
+
Then(/^(I |we )*should( not)* see (?:Odoo )?job (?:openings|listings|postings)$/, async function (pronounCase, notCase) {
|
|
514
|
+
await assertPresence(this.page.locator('a[href*="/jobs/"]').first(), notCase, 'a job opening link ("a[href*=\\"/jobs/\\"]")', {
|
|
515
|
+
hint: 'Only published job positions are listed; publish one from the Recruitment app or drop the scenario.',
|
|
516
|
+
});
|
|
393
517
|
});
|
|
394
518
|
|
|
395
519
|
// ---------------------------------------------------------------------------
|
|
@@ -397,106 +521,93 @@ Then(/^(I |we )*should see Odoo job (?:openings|listings|postings)$/, async func
|
|
|
397
521
|
// ---------------------------------------------------------------------------
|
|
398
522
|
|
|
399
523
|
/**
|
|
400
|
-
* Navigate to
|
|
524
|
+
* Navigate to the core "Contact us" page at `/contactus`.
|
|
401
525
|
*
|
|
402
526
|
* Example #1: Given I am on the Odoo contact us page
|
|
403
|
-
* Example #2: Given we are on the
|
|
527
|
+
* Example #2: Given we are on the contact page
|
|
404
528
|
* Example #3: Given I am on the Odoo "contact us" page
|
|
405
|
-
* Example #4:
|
|
406
|
-
* Example #5: Given I am on the
|
|
529
|
+
* Example #4: And on the contact-us page
|
|
530
|
+
* Example #5: Given I am on the contact page
|
|
407
531
|
*
|
|
408
532
|
*/
|
|
409
|
-
Given(/^(I am |we are )*on the Odoo (?:"contact us"|contact[ -]?us|contact) page$/, async function (pronounCase) {
|
|
410
|
-
|
|
411
|
-
await gotoUrl(this.page, this.launchUrl + '/contactus');
|
|
412
|
-
await this.page.waitForSelector('body', { state: 'attached', timeout: 45000 });
|
|
413
|
-
await waitForPageLoad(this.page, defaultTime);
|
|
533
|
+
Given(/^(I am |we are )*on the (?:Odoo )?(?:"contact us"|contact[ -]?us|contact) page$/, async function (pronounCase) {
|
|
534
|
+
await navigate(this, '/contactus');
|
|
414
535
|
});
|
|
415
536
|
|
|
416
537
|
/**
|
|
417
|
-
* Assert the
|
|
418
|
-
* the `s_website_form` snippet instance the Website app
|
|
419
|
-
* default contact-us page.
|
|
538
|
+
* Assert the "Contact us" form is, or is not, visible. Matches
|
|
539
|
+
* `#contactus_form`, the `s_website_form` snippet instance the Website app
|
|
540
|
+
* renders on the default contact-us page.
|
|
420
541
|
*
|
|
421
542
|
* Example #1: Then I should see the Odoo contact us form
|
|
422
|
-
* Example #2: And I should see the
|
|
543
|
+
* Example #2: And I should see the contact form
|
|
423
544
|
* Example #3: Then we should see the Odoo contact us form
|
|
424
|
-
* Example #4: And should see the
|
|
425
|
-
* Example #5: Then I should see the
|
|
545
|
+
* Example #4: And should not see the contact form
|
|
546
|
+
* Example #5: Then I should see the "contact us" form
|
|
426
547
|
*
|
|
427
548
|
*/
|
|
428
|
-
Then(/^(I |we )*should see the Odoo (?:"contact us"|contact[ -]?us|contact) form$/, async function (pronounCase) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
'Expected the Odoo contact-us form ("#contactus_form") to be visible'
|
|
433
|
-
);
|
|
549
|
+
Then(/^(I |we )*should( not)* see the (?:Odoo )?(?:"contact us"|contact[ -]?us|contact) form$/, async function (pronounCase, notCase) {
|
|
550
|
+
await assertPresence(this.page.locator('#contactus_form'), notCase, 'the contact-us form ("#contactus_form")', {
|
|
551
|
+
hint: 'The default /contactus page renders the form snippet with this id; a redesigned page may have dropped or renamed it.',
|
|
552
|
+
});
|
|
434
553
|
});
|
|
435
554
|
|
|
436
555
|
// ---------------------------------------------------------------------------
|
|
437
|
-
// /web/login
|
|
556
|
+
// /web/login: authentication
|
|
438
557
|
// ---------------------------------------------------------------------------
|
|
439
558
|
|
|
440
559
|
/**
|
|
441
|
-
* Navigate to
|
|
442
|
-
*
|
|
443
|
-
*
|
|
560
|
+
* Navigate to the core login screen at `/web/login`. This template is shared
|
|
561
|
+
* by every Odoo installation (Community and Enterprise alike): it ships in
|
|
562
|
+
* the base "web" module, not the Website builder.
|
|
444
563
|
*
|
|
445
564
|
* Example #1: Given I am on the Odoo login page
|
|
446
|
-
* Example #2: Given we are on the
|
|
447
|
-
* Example #3: Given I am on the
|
|
448
|
-
* Example #4:
|
|
449
|
-
* Example #5: Given I am on the
|
|
565
|
+
* Example #2: Given we are on the login page
|
|
566
|
+
* Example #3: Given I am on the sign in page
|
|
567
|
+
* Example #4: And on the Odoo sign-in page
|
|
568
|
+
* Example #5: Given I am on the web/login page
|
|
450
569
|
*
|
|
451
570
|
*/
|
|
452
|
-
Given(/^(I am |we are )*on the Odoo (?:sign
|
|
453
|
-
|
|
454
|
-
await gotoUrl(this.page, this.launchUrl + '/web/login');
|
|
455
|
-
await this.page.waitForSelector('body', { state: 'attached', timeout: 45000 });
|
|
456
|
-
await waitForPageLoad(this.page, defaultTime);
|
|
571
|
+
Given(/^(I am |we are )*on the (?:Odoo )?(?:sign[- ]?in|web\/login|login) page$/, async function (pronounCase) {
|
|
572
|
+
await navigate(this, '/web/login');
|
|
457
573
|
});
|
|
458
574
|
|
|
459
575
|
/**
|
|
460
|
-
* Assert
|
|
576
|
+
* Assert the core login form is, or is not, visible: the form posting to
|
|
461
577
|
* `/web/login`, with its `#login` and `#password` fields (`addons/web`'s
|
|
462
|
-
* `webclient_templates.xml`, unchanged across Odoo versions).
|
|
578
|
+
* `webclient_templates.xml`, unchanged across Odoo versions). Each part is
|
|
579
|
+
* checked separately so a failure names which one is missing.
|
|
463
580
|
*
|
|
464
581
|
* Example #1: Then I should see the Odoo login form
|
|
465
|
-
* Example #2: And I should see the
|
|
466
|
-
* Example #3: Then we should see the
|
|
467
|
-
* Example #4: And should see the
|
|
582
|
+
* Example #2: And I should see the sign in form
|
|
583
|
+
* Example #3: Then we should see the login form
|
|
584
|
+
* Example #4: And should not see the login form
|
|
468
585
|
* Example #5: Then I should see the Odoo sign-in form
|
|
469
586
|
*
|
|
470
587
|
*/
|
|
471
|
-
Then(/^(I |we )*should see the Odoo (?:sign
|
|
472
|
-
const
|
|
473
|
-
await
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
477
|
-
await assert.doesNotReject(
|
|
478
|
-
() => this.page.locator('#login').waitFor({ state: 'visible', timeout: 5000 }),
|
|
479
|
-
'Expected the Odoo login/email field ("#login") to be visible'
|
|
480
|
-
);
|
|
481
|
-
await assert.doesNotReject(
|
|
482
|
-
() => this.page.locator('#password').waitFor({ state: 'visible', timeout: 5000 }),
|
|
483
|
-
'Expected the Odoo password field ("#password") to be visible'
|
|
484
|
-
);
|
|
588
|
+
Then(/^(I |we )*should( not)* see the (?:Odoo )?(?:sign[- ]?in|login) form$/, async function (pronounCase, notCase) {
|
|
589
|
+
const hint = 'The core login screen is /web/login; an authenticated session is redirected away from it to /web.';
|
|
590
|
+
await assertPresence(this.page.locator('form[action="/web/login"]'), notCase, 'the login form ("form[action=\\"/web/login\\"]")', { hint });
|
|
591
|
+
if (notCase) return;
|
|
592
|
+
await assertPresence(this.page.locator('#login'), undefined, 'the login/email field ("#login")', { hint, timeout: 5000 });
|
|
593
|
+
await assertPresence(this.page.locator('#password'), undefined, 'the password field ("#password")', { hint, timeout: 5000 });
|
|
485
594
|
});
|
|
486
595
|
|
|
487
596
|
/**
|
|
488
|
-
* Fill and submit
|
|
489
|
-
* password (`#login`, `#password`, the form's submit button)
|
|
597
|
+
* Fill and submit the core login form with the given username/email and
|
|
598
|
+
* password (`#login`, `#password`, the form's submit button), for scenarios
|
|
599
|
+
* where the credentials are the subject. Requires the login screen to be
|
|
600
|
+
* open already.
|
|
490
601
|
*
|
|
491
602
|
* Example #1: When I log in to Odoo as "admin" with password "secret"
|
|
492
|
-
* Example #2: When we log in
|
|
493
|
-
* Example #3:
|
|
494
|
-
* Example #4: When we sign
|
|
495
|
-
* Example #5:
|
|
603
|
+
* Example #2: When we log in as "demo@example.com" with password "p@ssw0rd"
|
|
604
|
+
* Example #3: And I sign in to Odoo as "admin" with password "secret"
|
|
605
|
+
* Example #4: When we sign-in as "reviewer" with password "hunter2"
|
|
606
|
+
* Example #5: And log in as "portal_user" with password "letmein"
|
|
496
607
|
*
|
|
497
608
|
*/
|
|
498
|
-
When(/^(I |we )*(?:log in|sign in) to Odoo as "([^"]*)?" with password "([^"]*)?"$/, async function (pronounCase, username, password) {
|
|
499
|
-
await submitLogin(this
|
|
609
|
+
When(/^(I |we )*(?:log in|sign[- ]?in)(?: to Odoo)? as "([^"]*)?" with password "([^"]*)?"$/, async function (pronounCase, username, password) {
|
|
610
|
+
await submitLogin(this, username, password);
|
|
500
611
|
});
|
|
501
612
|
|
|
502
613
|
/**
|
|
@@ -516,16 +627,24 @@ Given(/^(I am |we are )*a logged in user with( the)*( username)* "([^"]*)?"( use
|
|
|
516
627
|
const users = (this.parameters && this.parameters.users) || {};
|
|
517
628
|
const entry = users[userKey];
|
|
518
629
|
if (!entry) {
|
|
519
|
-
throw
|
|
630
|
+
throw friendly({
|
|
631
|
+
message: `User "${userKey}" is not configured.`,
|
|
632
|
+
hint: `Add it to worldParameters.users in cucumber.js. Known users: ${Object.keys(users).join(', ') || '(none)'}.`,
|
|
633
|
+
});
|
|
520
634
|
}
|
|
521
635
|
const username = entry.username || userKey;
|
|
522
636
|
const password = typeof entry === 'string' ? entry : entry.password;
|
|
523
637
|
if (!password) {
|
|
524
|
-
throw
|
|
638
|
+
throw friendly({
|
|
639
|
+
message: `User "${userKey}" has no password configured.`,
|
|
640
|
+
hint: `Set worldParameters.users.${userKey}.password in cucumber.js, ideally from an environment variable.`,
|
|
641
|
+
});
|
|
525
642
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
await
|
|
643
|
+
// Start from a fresh session: an authenticated visitor is redirected away
|
|
644
|
+
// from /web/login and would never see the form.
|
|
645
|
+
await this.context.clearCookies();
|
|
646
|
+
await navigate(this, '/web/login');
|
|
647
|
+
await submitLogin(this, username, password);
|
|
529
648
|
});
|
|
530
649
|
|
|
531
650
|
/**
|
|
@@ -549,28 +668,36 @@ Given(/^(I am |we are )*not logged in$/, async function (pronounCase) {
|
|
|
549
668
|
try { sessionStorage.clear(); } catch { /* storage may be unavailable */ }
|
|
550
669
|
});
|
|
551
670
|
} catch { /* the page may not be on a real URL yet */ }
|
|
552
|
-
await
|
|
553
|
-
await this.page.locator('form[action="/web/login"]')
|
|
671
|
+
await navigate(this, '/web/login');
|
|
672
|
+
await assertPresence(this.page.locator('form[action="/web/login"]'), undefined, 'the login form (the session should be anonymous)', {
|
|
673
|
+
timeout: 10000,
|
|
674
|
+
hint: 'An authenticated session is redirected from /web/login to /web; clearing the cookies should have ended it.',
|
|
675
|
+
});
|
|
554
676
|
});
|
|
555
677
|
|
|
556
678
|
/**
|
|
557
|
-
* Assert the
|
|
558
|
-
* the `.alert-danger` message
|
|
559
|
-
* credentials.
|
|
679
|
+
* Assert the login screen is, or is not, showing an authentication error,
|
|
680
|
+
* optionally containing a text. Matches the `.alert-danger` message the login
|
|
681
|
+
* controller renders on invalid credentials.
|
|
560
682
|
*
|
|
561
683
|
* Example #1: Then I should see an Odoo login error
|
|
562
|
-
* Example #2: And I should see
|
|
563
|
-
* Example #3: Then we should see
|
|
564
|
-
* Example #4: And should see
|
|
565
|
-
* Example #5: Then I should see an
|
|
684
|
+
* Example #2: And I should see a sign in error
|
|
685
|
+
* Example #3: Then we should see a login error "Wrong login/password"
|
|
686
|
+
* Example #4: And should not see a login error
|
|
687
|
+
* Example #5: Then I should see an authentication error message
|
|
566
688
|
*
|
|
567
689
|
*/
|
|
568
|
-
Then(/^(I |we )*should see an Odoo (?:login|sign
|
|
569
|
-
const error = this.page.locator('.alert-danger');
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
690
|
+
Then(/^(I |we )*should( not)* see an? (?:Odoo )?(?:login|sign[- ]?in|authentication) error(?: message)?(?: "([^"]*)")?$/, async function (pronounCase, notCase, expectedText) {
|
|
691
|
+
const error = this.page.locator('form[action="/web/login"] .alert-danger, .alert-danger').first();
|
|
692
|
+
const hint = 'The core login screen renders the notice inside the form after a wrong login or password.';
|
|
693
|
+
if (notCase && !expectedText) {
|
|
694
|
+
await assertPresence(error, notCase, 'a login error message (".alert-danger")', { hint });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
await assertPresence(error, undefined, 'a login error message (".alert-danger")', { hint });
|
|
698
|
+
if (expectedText) {
|
|
699
|
+
await assertText(error, expectedText, notCase, 'the login error message', { hint });
|
|
700
|
+
}
|
|
574
701
|
});
|
|
575
702
|
|
|
576
703
|
// ---------------------------------------------------------------------------
|
|
@@ -578,21 +705,20 @@ Then(/^(I |we )*should see an Odoo (?:login|sign-?in|authentication) error$/, as
|
|
|
578
705
|
// ---------------------------------------------------------------------------
|
|
579
706
|
|
|
580
707
|
/**
|
|
581
|
-
* Assert the current page is
|
|
582
|
-
* page heading for "404" (rendered by `addons/http_routing`'s
|
|
708
|
+
* Assert the current page is, or is not, the core 404 template, by checking
|
|
709
|
+
* the page heading for "404" (rendered by `addons/http_routing`'s
|
|
583
710
|
* `http_routing.404` template on every Odoo site).
|
|
584
711
|
*
|
|
585
712
|
* Example #1: Then I should be on the Odoo 404 page
|
|
586
|
-
* Example #2: And I should be on the
|
|
713
|
+
* Example #2: And I should be on the not found page
|
|
587
714
|
* Example #3: Then we should be on the Odoo 404 page
|
|
588
|
-
* Example #4: And should be on the
|
|
715
|
+
* Example #4: And should not be on the 404 page
|
|
589
716
|
* Example #5: Then I should land on the Odoo 404 page
|
|
590
717
|
*
|
|
591
718
|
*/
|
|
592
|
-
Then(/^(I |we )*should (?:be on|land on) the Odoo (?:404|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
);
|
|
719
|
+
Then(/^(I |we )*should( not)* (?:be on|land on) the (?:Odoo )?(?:404|not found|page not found) page$/, async function (pronounCase, notCase) {
|
|
720
|
+
await assertPresence(this.page.locator('h1', { hasText: '404' }).first(), notCase, 'the 404 template (an <h1> containing "404")', {
|
|
721
|
+
attached: true,
|
|
722
|
+
hint: 'A site with a custom 404 page keeps the status code but may drop the "404" heading; assert the status code with the response step instead.',
|
|
723
|
+
});
|
|
598
724
|
});
|