@webship-js/odoo-steps 0.0.0-stage → 2.0.0-alpha1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Webship.co
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,103 @@
1
- # Temporary Holding Version
1
+ # odoo-steps
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Test](https://github.com/webship/odoo-steps/actions/workflows/test.yml/badge.svg)](https://github.com/webship/odoo-steps/actions/workflows/test.yml)
4
+
5
+ Odoo-specific Cucumber step definitions for [webship-js](https://www.npmjs.com/package/webship-js) — a Playwright + Cucumber BDD testing framework. `@webship-js/odoo-steps` adds an `odoo.steps.js` step-pack that understands the markup the Odoo Website builder (and the eCommerce, Blog, Events, Jobs and core login modules bundled with it) render, so you can write readable Gherkin scenarios against the main pages of any Odoo site without hand-rolling CSS selectors.
6
+
7
+ It is a **plugin for webship-js** — it does not run standalone. It ships one file of step definitions; you drop it into an existing (or new) webship-js test project.
8
+
9
+ ## What's covered
10
+
11
+ - Homepage / core Website layout (`#wrapwrap`, top menu, footer)
12
+ - Blog index and single post
13
+ - Shop index, search, product page, add-to-cart, cart badge
14
+ - Events index
15
+ - Jobs listing
16
+ - `/contactus` form
17
+ - `/web/login` login form + authentication (and the invalid-login error path)
18
+ - 404 page
19
+
20
+ See [tests/step-definitions/odoo.steps.js](tests/step-definitions/odoo.steps.js) for the full list of steps with Gherkin examples.
21
+
22
+ ## Requirements
23
+
24
+ - Node.js >= 20
25
+ - An existing [webship-js](https://www.npmjs.com/package/webship-js) 2.x test project (or run `npx init-webship-js` to create one)
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install --save-dev webship-js @webship-js/odoo-steps
31
+ ```
32
+
33
+ Add both step-definition paths to your project's `cucumber.js`:
34
+
35
+ ```javascript
36
+ module.exports = {
37
+ default: {
38
+ require: [
39
+ 'node_modules/webship-js/tests/step-definitions/**/*.js', // webship-js core steps
40
+ 'node_modules/@webship-js/odoo-steps/tests/step-definitions/**/*.js', // this plugin
41
+ 'tests/step-definitions/**/*.js', // your own custom steps, if any
42
+ ],
43
+ paths: ['tests/features/**/*.feature'],
44
+ worldParameters: {
45
+ launchUrl: process.env.LAUNCH_URL || 'https://your-odoo-site.example',
46
+ // ...
47
+ },
48
+ },
49
+ };
50
+ ```
51
+
52
+ This repo's own `tests/features/*.feature` files double as a working reference project.
53
+
54
+ ## Quick example
55
+
56
+ ```gherkin
57
+ Feature: Odoo homepage
58
+
59
+ Scenario: Homepage renders the Odoo Website layout
60
+ Given I am an anonymous user
61
+ And I am on the homepage
62
+ Then I should be on an Odoo website page
63
+ And the response status code should be 200
64
+ And I should see the Odoo top menu
65
+ And I should see the Odoo footer
66
+ ```
67
+
68
+ More runnable examples: [tests/features/](tests/features/).
69
+
70
+ ## Running this repo's own test suite
71
+
72
+ This repository is itself a working webship-js project — its `tests/features/*.feature` files run by default against a **self-hosted Odoo 17 instance managed by [DDEV](https://ddev.com/)** (`.ddev/`), seeded with Odoo's own demo data for Website, Blog, eCommerce, Events and Jobs. Public Odoo-built sites' bot-detection blocks and rate-limits automated CI traffic, so this repo never targets one.
73
+
74
+ ```bash
75
+ ddev start
76
+ ddev exec -s odoo -- odoo -d demo --db_host=db --db_user=odoo --db_password=odoo \
77
+ --stop-after-init -i website,website_blog,website_sale,website_event,hr_recruitment
78
+ docker restart ddev-odoo-steps-odoo # pick up the demo database
79
+
80
+ npm install
81
+ npx playwright install --with-deps chromium
82
+ npm test # runs against http://localhost:8069
83
+ LAUNCH_URL=https://your-site.example npm test # runs against your own site instead
84
+
85
+ ddev delete -Oy odoo-steps # tear down when done
86
+ ```
87
+
88
+ Other scripts:
89
+
90
+ | Script | Effect |
91
+ | --- | --- |
92
+ | `npm test` | Full suite, pretty output |
93
+ | `npm run test:dry` | `--dry-run` — checks for undefined/ambiguous steps only |
94
+ | `npm run test:headed` | Headed browser |
95
+ | `npm run test:chromium` / `test:firefox` / `test:webkit` | Pick a browser |
96
+
97
+ ## Contributing
98
+
99
+ Issues and pull requests are tracked on GitHub. `2.0.x` is the repository's single active branch.
100
+
101
+ ## License
102
+
103
+ MIT © Webship.co — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@webship-js/odoo-steps",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0-alpha1",
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
+ "main": "tests/step-definitions/odoo.steps.js",
6
+ "files": [
7
+ "tests/step-definitions/odoo.steps.js",
8
+ "README.md",
9
+ "LICENSE"
10
+ ],
11
+ "scripts": {
12
+ "test": "cucumber-js --config cucumber.js",
13
+ "test:dry": "cucumber-js --config cucumber.js --dry-run",
14
+ "test:chromium": "BROWSER=chromium cucumber-js --config cucumber.js",
15
+ "test:firefox": "BROWSER=firefox cucumber-js --config cucumber.js",
16
+ "test:webkit": "BROWSER=webkit cucumber-js --config cucumber.js",
17
+ "test:headed": "HEADLESS=false cucumber-js --config cucumber.js",
18
+ "generate-reports": "generate-reports"
19
+ },
20
+ "keywords": [
21
+ "webship-js",
22
+ "odoo",
23
+ "cucumber",
24
+ "cucumber-js",
25
+ "playwright",
26
+ "bdd",
27
+ "gherkin",
28
+ "step-definitions",
29
+ "odoo-testing",
30
+ "e2e-testing"
31
+ ],
32
+ "author": "Webship.co",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/webship/odoo-steps.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/webship/odoo-steps/issues"
40
+ },
41
+ "homepage": "https://github.com/webship/odoo-steps#readme",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">= 20.0"
47
+ },
48
+ "peerDependencies": {
49
+ "webship-js": "^2.0.4"
50
+ },
51
+ "devDependencies": {
52
+ "tsx": "^4.19.2",
53
+ "webship-js": "^2.0.4"
54
+ },
55
+ "overrides": {
56
+ "@cucumber/cucumber": "^12"
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ # Step Definitions
@@ -0,0 +1,530 @@
1
+ 'use strict';
2
+
3
+ // Odoo-specific Cucumber step definitions for webship-js. These steps
4
+ // understand the markup the Odoo Website builder (and the eCommerce, Blog,
5
+ // Events, Jobs and core web-login modules bundled with it) renders on every
6
+ // Odoo site — #wrapwrap/#top_menu layout, o_wblog_* blog templates,
7
+ // o_wsale_* eCommerce templates, o_wevent_* events templates, o_jobs
8
+ // recruitment templates, s_website_form snippet forms, and the core
9
+ // /web/login screen — so scenarios can assert against the main pages of an
10
+ // Odoo site without hand-rolling CSS selectors in every feature file.
11
+ //
12
+ // Selectors were verified against the live markup of https://www.odoo.com
13
+ // (an Odoo-built site) rather than guessed, since Odoo (unlike WordPress)
14
+ // has no single generic "body class per page type" convention — each
15
+ // front-end app (Website, eCommerce, Blog, Events, Jobs, ...) has its own
16
+ // stable class/id conventions instead.
17
+ //
18
+ // Composes with webship-js's built-in steps (navigation.steps.js,
19
+ // assertion.steps.js, form.steps.js, response.steps.js, …) rather than
20
+ // duplicating them — use those directly in feature files wherever they
21
+ // already cover the need (e.g. "the response status code should be 200",
22
+ // "I should be on the homepage").
23
+
24
+ const { Given, When, Then } = require('@cucumber/cucumber');
25
+ const assert = require('assert');
26
+ const { gotoUrl, waitForPageLoad, fillField } = require('webship-js/tests/step-definitions/webship');
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Core layout: #wrapwrap, top menu, footer
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Assert the current page was rendered by the Odoo Website builder, i.e. it
34
+ * has the `#wrapwrap` element every Odoo Website page wraps its header/
35
+ * main/footer in, regardless of which front-end app (Website, eCommerce,
36
+ * Blog, Events, Jobs, ...) rendered the page.
37
+ *
38
+ * Example #1: Then I should be on an Odoo website page
39
+ * Example #2: And I should be on an Odoo website page
40
+ * Example #3: Then we should be on an Odoo website page
41
+ * Example #4: And should be on an Odoo website page
42
+ * Example #5: Then I should be on a Odoo website page
43
+ *
44
+ */
45
+ Then(/^(I |we )*should be on an? Odoo website page$/, async function (pronounCase) {
46
+ const wrapwrap = this.page.locator('#wrapwrap');
47
+ await assert.doesNotReject(
48
+ () => wrapwrap.waitFor({ state: 'attached', timeout: 45000 }),
49
+ 'Expected the Odoo Website wrapper ("#wrapwrap") to be present'
50
+ );
51
+ });
52
+
53
+ /**
54
+ * Assert the Odoo Website top menu (main navigation) is visible. Matches
55
+ * `#top_menu`, the `<ul>` every Odoo Website header renders its menu items
56
+ * into.
57
+ *
58
+ * Example #1: Then I should see the Odoo top menu
59
+ * Example #2: And I should see the Odoo main menu
60
+ * Example #3: Then we should see the Odoo menu
61
+ * Example #4: And should see the Odoo top menu
62
+ * Example #5: Then I should see the Odoo main navigation menu
63
+ *
64
+ */
65
+ Then(/^(I |we )*should see the Odoo (?:top |main )?(?:menu|navigation menu)$/, async function (pronounCase) {
66
+ const menu = this.page.locator('#top_menu');
67
+ await assert.doesNotReject(
68
+ () => menu.waitFor({ state: 'visible', timeout: 45000 }),
69
+ 'Expected the Odoo Website top menu ("#top_menu") to be visible'
70
+ );
71
+ });
72
+
73
+ /**
74
+ * Assert the Odoo Website footer is visible. Matches `footer#bottom`, the
75
+ * element every Odoo Website footer snippet renders into.
76
+ *
77
+ * Example #1: Then I should see the Odoo footer
78
+ * Example #2: And I should see the Odoo site footer
79
+ * Example #3: Then we should see the Odoo page footer
80
+ * Example #4: And should see the Odoo footer
81
+ * Example #5: Then I should see the Odoo site footer
82
+ *
83
+ */
84
+ Then(/^(I |we )*should see the Odoo (?:site |page )?footer$/, async function (pronounCase) {
85
+ const footer = this.page.locator('footer#bottom, #footer').first();
86
+ await assert.doesNotReject(
87
+ () => footer.waitFor({ state: 'visible', timeout: 45000 }),
88
+ 'Expected the Odoo Website footer ("footer#bottom") to be visible'
89
+ );
90
+ });
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Blog
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Assert the current page is the Odoo Blog index/listing template. Matches
98
+ * `#o_wblog_index_content`, rendered by the Blog app's post-list view.
99
+ *
100
+ * Example #1: Then I should be on the Odoo blog page
101
+ * Example #2: And I should be on the Odoo blog index
102
+ * Example #3: Then we should be on the Odoo blog page
103
+ * Example #4: And should be on the Odoo blog index
104
+ * Example #5: Then I should be on the Odoo blog listing page
105
+ *
106
+ */
107
+ Then(/^(I |we )*should be on the Odoo blog (?:index|page|listing page)$/, async function (pronounCase) {
108
+ const index = this.page.locator('#o_wblog_index_content');
109
+ await assert.doesNotReject(
110
+ () => index.waitFor({ state: 'attached', timeout: 45000 }),
111
+ 'Expected the Odoo Blog index template ("#o_wblog_index_content") to be present'
112
+ );
113
+ });
114
+
115
+ /**
116
+ * Assert the current page is a single Odoo Blog post template. Matches
117
+ * `.o_wblog_post_title`, an element only the single-post view renders (the
118
+ * index view's post teaser cards do not include it).
119
+ *
120
+ * Example #1: Then I should be on an Odoo blog post page
121
+ * Example #2: And I should be on a Odoo blog post page
122
+ * Example #3: Then we should be on an Odoo blog post page
123
+ * Example #4: And should be on an Odoo blog post page
124
+ * Example #5: Then I should be on the Odoo single blog post page
125
+ *
126
+ */
127
+ Then(/^(I |we )*should be on an? Odoo (?:single )?blog post page$/, async function (pronounCase) {
128
+ // .first(): the single-post template can also render a "related posts"
129
+ // teaser section further down the page reusing the same title class.
130
+ const title = this.page.locator('.o_wblog_post_title').first();
131
+ await assert.doesNotReject(
132
+ () => title.waitFor({ state: 'attached', timeout: 45000 }),
133
+ 'Expected the Odoo single blog post template (".o_wblog_post_title") to be present'
134
+ );
135
+ });
136
+
137
+ /**
138
+ * Assert the current Odoo blog post's title matches the given text. Reads
139
+ * `.o_wblog_post_title`, the single-post view's title element.
140
+ *
141
+ * Example #1: Then I should see the Odoo blog post titled "Customer Reviews"
142
+ * Example #2: And I should see the Odoo blog post titled "Employee Testimonials"
143
+ * Example #3: Then we should see the Odoo blog post titled "Release Notes"
144
+ * Example #4: And should see the Odoo blog post titled "Product Update"
145
+ * Example #5: Then I should see the Odoo blog post titled "Company News"
146
+ *
147
+ */
148
+ Then(/^(I |we )*should see the Odoo blog post titled "([^"]*)?"$/, async function (pronounCase, expectedTitle) {
149
+ const title = this.page.locator('.o_wblog_post_title').first();
150
+ const text = (await title.textContent()) || '';
151
+ assert.ok(
152
+ text.includes(expectedTitle),
153
+ `Expected the Odoo blog post title to contain "${expectedTitle}" but found "${text.trim()}"`
154
+ );
155
+ });
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Shop (eCommerce)
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Assert the current page is the Odoo eCommerce shop/catalog template.
163
+ * Matches `#products_grid`, the container the "website_sale" module renders
164
+ * the product listing into.
165
+ *
166
+ * Example #1: Then I should be on the Odoo shop page
167
+ * Example #2: And I should be on the Odoo shop index
168
+ * Example #3: Then we should be on the Odoo shop page
169
+ * Example #4: And should be on the Odoo shop index
170
+ * Example #5: Then I should be on the Odoo shop catalog page
171
+ *
172
+ */
173
+ Then(/^(I |we )*should be on the Odoo shop (?:page|index|catalog page)$/, async function (pronounCase) {
174
+ const grid = this.page.locator('#products_grid');
175
+ await assert.doesNotReject(
176
+ () => grid.waitFor({ state: 'attached', timeout: 45000 }),
177
+ 'Expected the Odoo shop product grid ("#products_grid") to be present'
178
+ );
179
+ });
180
+
181
+ /**
182
+ * Assert the current page is an Odoo eCommerce product page. Matches
183
+ * `.o_wsale_product_page`, rendered by "website_sale" for every product
184
+ * detail page.
185
+ *
186
+ * Example #1: Then I should be on an Odoo product page
187
+ * Example #2: And I should be on a Odoo shop product page
188
+ * Example #3: Then we should be on an Odoo product page
189
+ * Example #4: And should be on an Odoo shop product page
190
+ * Example #5: Then I should be on the Odoo product detail page
191
+ *
192
+ */
193
+ Then(/^(I |we )*should be on an? Odoo (?:shop )?product(?: detail)? page$/, async function (pronounCase) {
194
+ const product = this.page.locator('.o_wsale_product_page');
195
+ await assert.doesNotReject(
196
+ () => product.waitFor({ state: 'attached', timeout: 45000 }),
197
+ 'Expected the Odoo product page (".o_wsale_product_page") to be present'
198
+ );
199
+ });
200
+
201
+ /**
202
+ * Submit the Odoo shop's search form with the given term. Fills the
203
+ * `input[name="search"]` scoped to `.o_wsale_products_searchbar_form` — the
204
+ * shop listing's own search box, as opposed to the (hidden until toggled)
205
+ * search field "website_sale" also renders in the page header — and presses
206
+ * Enter.
207
+ *
208
+ * Example #1: When I search the Odoo shop for "mug"
209
+ * Example #2: When we search the Odoo shop for "polo shirt"
210
+ * Example #3: When I search the Odoo shop for "t-shirt"
211
+ * Example #4: When we search the Odoo shop for "backpack"
212
+ * Example #5: When I search the Odoo shop for "notebook"
213
+ *
214
+ */
215
+ When(/^(I |we )*search the Odoo shop for "([^"]*)?"$/, async function (pronounCase, term) {
216
+ const defaultTime = this.minWaitTime.page || 3000;
217
+ const searchField = this.page
218
+ .locator('.o_wsale_products_searchbar_form input[name="search"]')
219
+ .first();
220
+ await searchField.waitFor({ state: 'visible', timeout: 45000 });
221
+ await searchField.fill(term);
222
+ await searchField.press('Enter');
223
+ await waitForPageLoad(this.page, defaultTime);
224
+ });
225
+
226
+ /**
227
+ * Assert the Odoo shop search returned results for the given term: the
228
+ * product grid is present and the URL's `search` query parameter matches.
229
+ *
230
+ * Example #1: Then I should see Odoo shop search results for "mug"
231
+ * Example #2: And I should see Odoo shop search results for "polo shirt"
232
+ * Example #3: Then we should see Odoo shop search results for "t-shirt"
233
+ * Example #4: And should see Odoo shop search results for "backpack"
234
+ * Example #5: Then I should see Odoo shop search results for "notebook"
235
+ *
236
+ */
237
+ Then(/^(I |we )*should see Odoo shop search results for "([^"]*)?"$/, async function (pronounCase, term) {
238
+ const grid = this.page.locator('#products_grid');
239
+ await assert.doesNotReject(
240
+ () => grid.waitFor({ state: 'attached', timeout: 45000 }),
241
+ 'Expected the Odoo shop product grid ("#products_grid") to be present'
242
+ );
243
+ const currentUrl = this.page.url();
244
+ const encoded = encodeURIComponent(term).replace(/%20/g, '+');
245
+ assert.ok(
246
+ currentUrl.includes(`search=${encoded}`) || currentUrl.includes(`search=${encodeURIComponent(term)}`),
247
+ `Expected the shop search URL to contain the term "${term}" but URL was: ${currentUrl}`
248
+ );
249
+ });
250
+
251
+ /**
252
+ * Add the current Odoo product page's product to the cart. Clicks
253
+ * `#add_to_cart`, the "website_sale" add-to-cart control every product
254
+ * page renders. When the "sale_product_configurator" module is installed
255
+ * (a website_sale dependency as of Odoo 17), this opens an "Added to your
256
+ * cart" modal instead of updating the cart directly — in that case this
257
+ * step dismisses it via its "Continue Shopping" action, which keeps the
258
+ * item in the cart without leaving the product page.
259
+ *
260
+ * Example #1: When I add the product to the Odoo cart
261
+ * Example #2: When we add this product to the Odoo cart
262
+ * Example #3: When I add the current product to the Odoo cart
263
+ * Example #4: When we add the product to the Odoo cart
264
+ * Example #5: When I add this product to the Odoo cart
265
+ *
266
+ */
267
+ When(/^(I |we )*add (?:the |this )?(?:current )?product to the Odoo cart$/, async function (pronounCase) {
268
+ const defaultTime = this.minWaitTime.page || 3000;
269
+ const addToCart = this.page.locator('#add_to_cart').first();
270
+ await addToCart.waitFor({ state: 'visible', timeout: 45000 });
271
+ await addToCart.click();
272
+ const continueShopping = this.page.getByText('Continue Shopping').first();
273
+ if (await continueShopping.isVisible({ timeout: 5000 }).catch(() => false)) {
274
+ await continueShopping.click();
275
+ }
276
+ await waitForPageLoad(this.page, defaultTime);
277
+ });
278
+
279
+ /**
280
+ * Assert the Odoo cart badge shows the given item count. Reads
281
+ * `.my_cart_quantity`, the header cart-icon badge "website_sale" updates
282
+ * whenever the session cart changes.
283
+ *
284
+ * Example #1: Then I should see "1" item in the Odoo cart
285
+ * Example #2: And I should see "2" items in the Odoo cart
286
+ * Example #3: Then we should see "1" item in the Odoo cart
287
+ * Example #4: And should see "3" items in the Odoo cart
288
+ * Example #5: Then I should see "0" items in the Odoo cart
289
+ *
290
+ */
291
+ Then(/^(I |we )*should see "([^"]*)?" items? in the Odoo cart$/, async function (pronounCase, expectedCount) {
292
+ const badge = this.page.locator('.my_cart_quantity').first();
293
+ await assert.doesNotReject(
294
+ () => badge.waitFor({ state: 'visible', timeout: 45000 }),
295
+ 'Expected the Odoo cart quantity badge (".my_cart_quantity") to be visible'
296
+ );
297
+ const text = ((await badge.textContent()) || '').trim();
298
+ assert.strictEqual(
299
+ text,
300
+ String(expectedCount),
301
+ `Expected the Odoo cart quantity badge to read "${expectedCount}" but found "${text}"`
302
+ );
303
+ });
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Events
307
+ // ---------------------------------------------------------------------------
308
+
309
+ /**
310
+ * Assert the current page is the Odoo Events index/listing template.
311
+ * Matches `.o_wevent_index`, rendered by the Events app's event-list view.
312
+ *
313
+ * Example #1: Then I should be on the Odoo events page
314
+ * Example #2: And I should be on the Odoo event page
315
+ * Example #3: Then we should be on the Odoo events page
316
+ * Example #4: And should be on the Odoo events index
317
+ * Example #5: Then I should be on the Odoo events listing page
318
+ *
319
+ */
320
+ Then(/^(I |we )*should be on the Odoo events? (?:page|index|listing page)$/, async function (pronounCase) {
321
+ const index = this.page.locator('.o_wevent_index, .o_wevent_events_list').first();
322
+ await assert.doesNotReject(
323
+ () => index.waitFor({ state: 'attached', timeout: 45000 }),
324
+ 'Expected the Odoo Events index template (".o_wevent_index") to be present'
325
+ );
326
+ });
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // Jobs
330
+ // ---------------------------------------------------------------------------
331
+
332
+ /**
333
+ * Assert the current page is the Odoo Jobs (recruitment) listing template.
334
+ * Matches `#jobs_grid`, rendered by the Recruitment app's public job board.
335
+ *
336
+ * Example #1: Then I should be on the Odoo jobs page
337
+ * Example #2: And I should be on the Odoo careers page
338
+ * Example #3: Then we should be on the Odoo jobs page
339
+ * Example #4: And should be on the Odoo jobs listing page
340
+ * Example #5: Then I should be on the Odoo recruitment page
341
+ *
342
+ */
343
+ Then(/^(I |we )*should be on the Odoo (?:jobs|careers|recruitment) (?:page|listing page)$/, async function (pronounCase) {
344
+ const jobs = this.page.locator('#jobs_grid');
345
+ await assert.doesNotReject(
346
+ () => jobs.waitFor({ state: 'attached', timeout: 45000 }),
347
+ 'Expected the Odoo Jobs listing template ("#jobs_grid") to be present'
348
+ );
349
+ });
350
+
351
+ /**
352
+ * Assert at least one job opening link is visible on the Odoo Jobs page.
353
+ * Matches links whose `href` contains `/jobs/`, the URL pattern every job
354
+ * posting page uses.
355
+ *
356
+ * Example #1: Then I should see Odoo job openings
357
+ * Example #2: And I should see Odoo job listings
358
+ * Example #3: Then we should see Odoo job openings
359
+ * Example #4: And should see Odoo job listings
360
+ * Example #5: Then I should see Odoo job postings
361
+ *
362
+ */
363
+ Then(/^(I |we )*should see Odoo job (?:openings|listings|postings)$/, async function (pronounCase) {
364
+ const jobLink = this.page.locator('a[href*="/jobs/"]').first();
365
+ await assert.doesNotReject(
366
+ () => jobLink.waitFor({ state: 'visible', timeout: 45000 }),
367
+ 'Expected at least one Odoo job opening link ("a[href*=\\"/jobs/\\"]") to be visible'
368
+ );
369
+ });
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Contact form
373
+ // ---------------------------------------------------------------------------
374
+
375
+ /**
376
+ * Navigate to Odoo's core "Contact us" page at `/contactus`.
377
+ *
378
+ * Example #1: Given I am on the Odoo contact us page
379
+ * Example #2: Given we are on the Odoo contact page
380
+ * Example #3: Given I am on the Odoo "contact us" page
381
+ * Example #4: Given we are on the Odoo contact-us page
382
+ * Example #5: Given I am on the Odoo contact page
383
+ *
384
+ */
385
+ Given(/^(I am |we are )*on the Odoo (?:"contact us"|contact[ -]?us|contact) page$/, async function (pronounCase) {
386
+ const defaultTime = this.minWaitTime.page || 3000;
387
+ await gotoUrl(this.page, this.launchUrl + '/contactus');
388
+ await this.page.waitForSelector('body', { state: 'attached', timeout: 45000 });
389
+ await waitForPageLoad(this.page, defaultTime);
390
+ });
391
+
392
+ /**
393
+ * Assert the Odoo "Contact us" form is visible. Matches `#contactus_form`,
394
+ * the `s_website_form` snippet instance the Website app renders on the
395
+ * default contact-us page.
396
+ *
397
+ * Example #1: Then I should see the Odoo contact us form
398
+ * Example #2: And I should see the Odoo contact form
399
+ * Example #3: Then we should see the Odoo contact us form
400
+ * Example #4: And should see the Odoo contact form
401
+ * Example #5: Then I should see the Odoo "contact us" form
402
+ *
403
+ */
404
+ Then(/^(I |we )*should see the Odoo (?:"contact us"|contact[ -]?us|contact) form$/, async function (pronounCase) {
405
+ const form = this.page.locator('#contactus_form');
406
+ await assert.doesNotReject(
407
+ () => form.waitFor({ state: 'visible', timeout: 45000 }),
408
+ 'Expected the Odoo contact-us form ("#contactus_form") to be visible'
409
+ );
410
+ });
411
+
412
+ // ---------------------------------------------------------------------------
413
+ // /web/login — authentication
414
+ // ---------------------------------------------------------------------------
415
+
416
+ /**
417
+ * Navigate to Odoo's core login screen at `/web/login`. This template is
418
+ * shared by every Odoo installation (Community and Enterprise alike) —
419
+ * it ships in the base "web" module, not the Website builder.
420
+ *
421
+ * Example #1: Given I am on the Odoo login page
422
+ * Example #2: Given we are on the Odoo login page
423
+ * Example #3: Given I am on the Odoo sign in page
424
+ * Example #4: Given we are on the Odoo sign-in page
425
+ * Example #5: Given I am on the Odoo "web/login" page
426
+ *
427
+ */
428
+ Given(/^(I am |we are )*on the Odoo (?:sign-?in|"web\/login"|login) page$/, async function (pronounCase) {
429
+ const defaultTime = this.minWaitTime.page || 3000;
430
+ await gotoUrl(this.page, this.launchUrl + '/web/login');
431
+ await this.page.waitForSelector('body', { state: 'attached', timeout: 45000 });
432
+ await waitForPageLoad(this.page, defaultTime);
433
+ });
434
+
435
+ /**
436
+ * Assert Odoo's core login form is visible: the form posting to
437
+ * `/web/login`, with its `#login` and `#password` fields (`addons/web`'s
438
+ * `webclient_templates.xml`, unchanged across Odoo versions).
439
+ *
440
+ * Example #1: Then I should see the Odoo login form
441
+ * Example #2: And I should see the Odoo sign in form
442
+ * Example #3: Then we should see the Odoo login form
443
+ * Example #4: And should see the Odoo login form
444
+ * Example #5: Then I should see the Odoo sign-in form
445
+ *
446
+ */
447
+ Then(/^(I |we )*should see the Odoo (?:sign-?in|login) form$/, async function (pronounCase) {
448
+ const form = this.page.locator('form[action="/web/login"]');
449
+ await assert.doesNotReject(
450
+ () => form.waitFor({ state: 'visible', timeout: 45000 }),
451
+ 'Expected the Odoo login form ("form[action=\\"/web/login\\"]") to be visible'
452
+ );
453
+ await assert.doesNotReject(
454
+ () => this.page.locator('#login').waitFor({ state: 'visible', timeout: 5000 }),
455
+ 'Expected the Odoo login/email field ("#login") to be visible'
456
+ );
457
+ await assert.doesNotReject(
458
+ () => this.page.locator('#password').waitFor({ state: 'visible', timeout: 5000 }),
459
+ 'Expected the Odoo password field ("#password") to be visible'
460
+ );
461
+ });
462
+
463
+ /**
464
+ * Fill and submit Odoo's core login form with the given username/email and
465
+ * password (`#login`, `#password`, the form's submit button).
466
+ *
467
+ * Example #1: When I log in to Odoo as "admin" with password "secret"
468
+ * Example #2: When we log in to Odoo as "demo@example.com" with password "p@ssw0rd"
469
+ * Example #3: When I sign in to Odoo as "admin" with password "secret"
470
+ * Example #4: When we sign in to Odoo as "reviewer" with password "hunter2"
471
+ * Example #5: When I log in to Odoo as "portal_user" with password "letmein"
472
+ *
473
+ */
474
+ When(/^(I |we )*(?:log in|sign in) to Odoo as "([^"]*)?" with password "([^"]*)?"$/, async function (pronounCase, username, password) {
475
+ await fillField(this.page, 'login', username);
476
+ await fillField(this.page, 'password', password);
477
+ await this.page.locator('form[action="/web/login"] button[type="submit"]').first().click();
478
+ // A successful login redirects to the backend webclient (/web), which
479
+ // keeps a permanent longpoll connection open (Discuss/bus) — so
480
+ // networkidle never fires there and the shared waitForPageLoad()/
481
+ // smartSettle() helper (built around network-idle + ajax-quiet
482
+ // detection) will not settle. Wait for DOM content only instead; an
483
+ // invalid login stays on /web/login without a full navigation, so this
484
+ // resolves quickly either way.
485
+ await this.page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
486
+ });
487
+
488
+ /**
489
+ * Assert the Odoo login screen is showing an authentication error. Matches
490
+ * the `.alert-danger` message Odoo's login controller renders on invalid
491
+ * credentials.
492
+ *
493
+ * Example #1: Then I should see an Odoo login error
494
+ * Example #2: And I should see an Odoo sign in error
495
+ * Example #3: Then we should see an Odoo login error
496
+ * Example #4: And should see an Odoo login error
497
+ * Example #5: Then I should see an Odoo authentication error
498
+ *
499
+ */
500
+ Then(/^(I |we )*should see an Odoo (?:login|sign-?in|authentication) error$/, async function (pronounCase) {
501
+ const error = this.page.locator('.alert-danger');
502
+ await assert.doesNotReject(
503
+ () => error.waitFor({ state: 'visible', timeout: 45000 }),
504
+ 'Expected an Odoo login error message (".alert-danger") to be visible'
505
+ );
506
+ });
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // 404
510
+ // ---------------------------------------------------------------------------
511
+
512
+ /**
513
+ * Assert the current page is Odoo's core 404 template, by checking the
514
+ * page heading for "404" (rendered by `addons/http_routing`'s
515
+ * `http_routing.404` template on every Odoo site).
516
+ *
517
+ * Example #1: Then I should be on the Odoo 404 page
518
+ * Example #2: And I should be on the Odoo "not found" page
519
+ * Example #3: Then we should be on the Odoo 404 page
520
+ * Example #4: And should be on the Odoo 404 page
521
+ * Example #5: Then I should land on the Odoo 404 page
522
+ *
523
+ */
524
+ Then(/^(I |we )*should (?:be on|land on) the Odoo (?:404|"not found") page$/, async function (pronounCase) {
525
+ const heading = this.page.locator('h1', { hasText: '404' }).first();
526
+ await assert.doesNotReject(
527
+ () => heading.waitFor({ state: 'attached', timeout: 45000 }),
528
+ 'Expected the Odoo 404 template (an <h1> containing "404") to be present'
529
+ );
530
+ });