@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 +21 -0
- package/README.md +102 -2
- package/package.json +56 -4
- package/tests/step-definitions/README.md +1 -0
- package/tests/step-definitions/odoo.steps.js +530 -0
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
|
-
#
|
|
1
|
+
# odoo-steps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
});
|