@webship-js/django-cms-steps 0.0.0-stage → 2.0.0-beta1
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 +99 -2
- package/index.js +29 -0
- package/package.json +54 -4
- package/tests/step-definitions/django-cms.steps.js +321 -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,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# django-cms-steps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Custom [webship-js](https://www.npmjs.com/package/webship-js) step definitions
|
|
4
|
+
for testing [django CMS](https://www.django-cms.org/) sites: Django admin
|
|
5
|
+
login/logout (as a user from the `users` registry), the CMS toolbar, toolbar
|
|
6
|
+
navigation (including the sideframe overlay), the page tree, and the
|
|
7
|
+
"no pages yet" welcome screen.
|
|
8
|
+
|
|
9
|
+
`@webship-js/django-cms-steps` is a **plugin** for webship-js — it does not replace it. It
|
|
10
|
+
ships one file, `tests/step-definitions/django-cms.steps.js`, of extra `Given`/`When`/`Then`
|
|
11
|
+
steps that you load alongside webship-js's own step library in your project's
|
|
12
|
+
`cucumber.js`. There is no `custom.js`; every step this package provides
|
|
13
|
+
lives in `django-cms.steps.js`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev webship-js @webship-js/django-cms-steps
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both packages are devDependencies of your test project. `webship-js` is a
|
|
22
|
+
peer dependency of `@webship-js/django-cms-steps`.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Add this package's steps to your `cucumber.js` `require` array, next to
|
|
27
|
+
webship-js's own:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// cucumber.js
|
|
31
|
+
module.exports = {
|
|
32
|
+
default: {
|
|
33
|
+
requireModule: ['ts-node/register'],
|
|
34
|
+
require: [
|
|
35
|
+
'node_modules/webship-js/tests/step-definitions/**/*.js',
|
|
36
|
+
'node_modules/@webship-js/django-cms-steps/tests/step-definitions/*.steps.js',
|
|
37
|
+
'tests/step-definitions/**/*.js', // your own project-specific steps, if any
|
|
38
|
+
],
|
|
39
|
+
paths: ['tests/features/**/*.feature'],
|
|
40
|
+
worldParameters: {
|
|
41
|
+
launchUrl: process.env.LAUNCH_URL || 'http://localhost',
|
|
42
|
+
// Accounts for "a logged in user with the username "<key>"". Keep the
|
|
43
|
+
// password out of the repo: read it from an environment variable.
|
|
44
|
+
users: {
|
|
45
|
+
admin: {
|
|
46
|
+
username: process.env.DJANGOCMS_ADMIN_USER || 'admin',
|
|
47
|
+
password: process.env.DJANGOCMS_ADMIN_PASS || 'admin',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then write a feature using the new steps:
|
|
56
|
+
|
|
57
|
+
```gherkin
|
|
58
|
+
Feature: CMS toolbar
|
|
59
|
+
|
|
60
|
+
Scenario: Admin sees the CMS toolbar and opens the page tree
|
|
61
|
+
Given I am a logged in user with the username "admin"
|
|
62
|
+
Then I should see the CMS toolbar
|
|
63
|
+
When I click "Pages" in the CMS toolbar menu
|
|
64
|
+
Then the CMS sideframe should be showing "/admin/cms/pagecontent/"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx cucumber-js --config cucumber.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Steps
|
|
72
|
+
|
|
73
|
+
| Step | Type |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `I am a logged in user with the username "<key>"` (reads `worldParameters.users`) | Given |
|
|
76
|
+
| `I am not logged in` (clears cookies and storage, asserts the admin login form) | Given |
|
|
77
|
+
| `I log out` | When |
|
|
78
|
+
| `I should see the CMS toolbar` / `I should not see the CMS toolbar` | Then |
|
|
79
|
+
| `I click "<label>" in the CMS toolbar menu` | When |
|
|
80
|
+
| `the CMS sideframe should be showing "<url part>"` | Then |
|
|
81
|
+
| `I am on the django CMS page tree` | Given |
|
|
82
|
+
| `I should see the page tree` | Then |
|
|
83
|
+
| `I should see "<title>" in the page tree` | Then |
|
|
84
|
+
| `I should see the django CMS welcome screen` | Then |
|
|
85
|
+
|
|
86
|
+
Every step's regex and Gherkin examples are in the JSDoc block above it in
|
|
87
|
+
[`tests/step-definitions/django-cms.steps.js`](tests/step-definitions/django-cms.steps.js).
|
|
88
|
+
|
|
89
|
+
## Testing this package itself
|
|
90
|
+
|
|
91
|
+
This repository also contains its own test suite (`tests/features/`) that
|
|
92
|
+
exercises the steps in `tests/step-definitions/django-cms.steps.js`. It
|
|
93
|
+
doesn't bundle a demo site — point it at any django CMS install you have via
|
|
94
|
+
`LAUNCH_URL`. CI builds a throwaway django CMS site with
|
|
95
|
+
[ddev](https://ddev.com/) at run time to test against on every push/PR — see
|
|
96
|
+
[.github/workflows/test.yml](.github/workflows/test.yml).
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
[MIT](LICENSE) © Webship.co
|
package/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* django-cms-steps
|
|
5
|
+
*
|
|
6
|
+
* Custom webship-js step definitions for testing django CMS sites.
|
|
7
|
+
*
|
|
8
|
+
* This module has no exported functions of its own — requiring
|
|
9
|
+
* `tests/step-definitions/django-cms.steps.js` (directly, or via the glob
|
|
10
|
+
* below) is what registers the Given/When/Then steps with @cucumber/cucumber.
|
|
11
|
+
*
|
|
12
|
+
* Consumers wire this package into their cucumber.js `require` array,
|
|
13
|
+
* see README.md for details.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const STEP_DEFINITIONS_GLOB = path.join(
|
|
19
|
+
'node_modules',
|
|
20
|
+
'@webship-js/django-cms-steps',
|
|
21
|
+
'tests',
|
|
22
|
+
'step-definitions',
|
|
23
|
+
'*.steps.js'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
stepDefinitionsGlob: STEP_DEFINITIONS_GLOB,
|
|
28
|
+
stepDefinitionsPath: path.join(__dirname, 'tests', 'step-definitions', 'django-cms.steps.js'),
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship-js/django-cms-steps",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0-beta1",
|
|
4
|
+
"description": "Custom webship-js step definitions for testing django CMS sites (admin login, the CMS toolbar, toolbar navigation, and the page tree).",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"tests/step-definitions",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "BROWSER=chromium cucumber-js --config cucumber.js",
|
|
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
|
+
"report": "generate-reports"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"webship-js",
|
|
21
|
+
"django",
|
|
22
|
+
"django-cms",
|
|
23
|
+
"cucumber",
|
|
24
|
+
"playwright",
|
|
25
|
+
"bdd",
|
|
26
|
+
"testing",
|
|
27
|
+
"step-definitions"
|
|
28
|
+
],
|
|
29
|
+
"author": "Webship.co",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/webship/django-cms-steps.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/webship/django-cms-steps/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/webship/django-cms-steps#readme",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">= 20.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"webship-js": "^2.0.6"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"webship-js": "^2.0.6",
|
|
50
|
+
"ts-node": "^10",
|
|
51
|
+
"typescript": "~5.4.0"
|
|
52
|
+
},
|
|
53
|
+
"overrides": {
|
|
54
|
+
"@cucumber/cucumber": "^12"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* django-cms-steps / tests/step-definitions/django-cms.steps.js
|
|
5
|
+
*
|
|
6
|
+
* Custom webship-js step definitions for testing django CMS sites:
|
|
7
|
+
* Django admin authentication, the CMS toolbar and its navigation menu,
|
|
8
|
+
* the page tree, and the "no pages yet" welcome screen.
|
|
9
|
+
*
|
|
10
|
+
* These steps assume the standard webship-js World: `this.page`
|
|
11
|
+
* (Playwright Page), `this.context` (BrowserContext), `this.launchUrl`,
|
|
12
|
+
* and `this.parameters` (cucumber.js worldParameters).
|
|
13
|
+
*
|
|
14
|
+
* Selectors below were captured against a real django CMS 5 install
|
|
15
|
+
* (Django admin login form, the `#cms-top` / `.cms-toolbar` toolbar, and
|
|
16
|
+
* the `.cms-pagetree` page-tree admin). Every step documents its Gherkin
|
|
17
|
+
* phrasing in the JSDoc block right above it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
const { gotoUrl, smartSettle } = require('webship-js/tests/step-definitions/webship');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shared selectors (django CMS toolbar / Django admin)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const SELECTORS = {
|
|
29
|
+
// The CMS toolbar is mounted as #cms-top and always carries the
|
|
30
|
+
// .cms-toolbar class on its inner bar, whether or not there are any
|
|
31
|
+
// pages yet.
|
|
32
|
+
toolbar: '#cms-top',
|
|
33
|
+
toolbarBar: '#cms-top .cms-toolbar',
|
|
34
|
+
toolbarNav: '#cms-top .cms-toolbar-item-navigation',
|
|
35
|
+
// Django's own admin login form (served at /admin/login/).
|
|
36
|
+
loginForm: '#login-form',
|
|
37
|
+
loginUsername: '#id_username',
|
|
38
|
+
loginPassword: '#id_password',
|
|
39
|
+
// The page-tree change list at /admin/cms/pagecontent/.
|
|
40
|
+
pageTree: '.cms-pagetree-jstree, .cms-pagetree',
|
|
41
|
+
pageTreeEmpty: '.cms-pagetree-empty',
|
|
42
|
+
// The "no pages yet" welcome screen shown on a fresh install.
|
|
43
|
+
welcomeScreen: '.cms-welcome-heading, .cms-welcome-section, .cms-welcome, #cms-welcome-config',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function siteRoot(launchUrl) {
|
|
47
|
+
return String(launchUrl).replace(/\/$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function settle(world, fallback) {
|
|
51
|
+
await smartSettle(world.page, (world.minWaitTime && world.minWaitTime.page) || fallback || 3000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Look up a named user from cucumber.js worldParameters.users
|
|
56
|
+
* (`{ <key>: { username, password } }`), throwing a helpful error if it is
|
|
57
|
+
* not configured.
|
|
58
|
+
*/
|
|
59
|
+
function resolveUser(parameters, userKey) {
|
|
60
|
+
const users = (parameters && parameters.users) || {};
|
|
61
|
+
const entry = users[userKey];
|
|
62
|
+
if (!entry) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`User "${userKey}" is not configured. Add it to worldParameters.users in cucumber.js. ` +
|
|
65
|
+
`Known users: ${Object.keys(users).join(', ') || '(none)'}.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const username = entry.username || userKey;
|
|
69
|
+
const password = typeof entry === 'string' ? entry : entry.password;
|
|
70
|
+
if (!password) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`User "${userKey}" has no password configured. Set worldParameters.users.${userKey}.password ` +
|
|
73
|
+
'in cucumber.js, ideally from an environment variable.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return { username, password };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Open the toolbar's site-name dropdown. It opens on click, toggling a
|
|
81
|
+
* "-hover" class via JS; it is not a plain CSS :hover menu.
|
|
82
|
+
*/
|
|
83
|
+
async function openToolbarSiteMenu(page) {
|
|
84
|
+
await page.waitForSelector(SELECTORS.toolbarNav, { state: 'visible', timeout: 10000 });
|
|
85
|
+
const siteMenu = page.locator(`${SELECTORS.toolbarNav} > li`).first();
|
|
86
|
+
await siteMenu.click();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Authentication (Django admin login)
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Log in through the Django admin login form as a user from the `users`
|
|
95
|
+
* registry in cucumber.js worldParameters (`{ <key>: { username, password } }`,
|
|
96
|
+
* with the password read from an environment variable so no credential has
|
|
97
|
+
* to live in a feature file).
|
|
98
|
+
*
|
|
99
|
+
* The CMS toolbar is only injected on pages served through the CMS, not on
|
|
100
|
+
* the raw Django admin index, so the login redirects back to the front end
|
|
101
|
+
* ("/"). A default django CMS project (as scaffolded by the `python -m cms`
|
|
102
|
+
* installer) serves a single, unprefixed language, so URLs here are bare:
|
|
103
|
+
* no "/en/" (or other language code) segment.
|
|
104
|
+
*
|
|
105
|
+
* Example #1: Given I am a logged in user with the username "admin"
|
|
106
|
+
* Example #2: Given we are a logged in user with username "admin"
|
|
107
|
+
* Example #3: Given I am a logged in user with "admin"
|
|
108
|
+
* Example #4: And a logged in user with the username "admin"
|
|
109
|
+
* Example #5: Given I am a logged in user with the username "admin" user
|
|
110
|
+
*
|
|
111
|
+
*/
|
|
112
|
+
Given(/^(I am |we are )*a logged in user with( the)*( username)* "([^"]*)?"( user)*$/, async function (pronounCase, theCase, usernameCase, userKey, userCase) {
|
|
113
|
+
const { username, password } = resolveUser(this.parameters, userKey);
|
|
114
|
+
const page = this.page;
|
|
115
|
+
await gotoUrl(page, `${siteRoot(this.launchUrl)}/admin/login/?next=/`);
|
|
116
|
+
await page.waitForSelector(SELECTORS.loginUsername, { state: 'visible', timeout: 10000 });
|
|
117
|
+
await page.fill(SELECTORS.loginUsername, username);
|
|
118
|
+
await page.fill(SELECTORS.loginPassword, password);
|
|
119
|
+
await Promise.all([
|
|
120
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
|
|
121
|
+
page.evaluate((formSelector) => {
|
|
122
|
+
const submit = document.querySelector(`${formSelector} input[type="submit"], ${formSelector} button[type="submit"]`);
|
|
123
|
+
if (!submit) throw new Error(`The login form "${formSelector}" has no submit button.`);
|
|
124
|
+
submit.click();
|
|
125
|
+
}, SELECTORS.loginForm),
|
|
126
|
+
]);
|
|
127
|
+
await settle(this);
|
|
128
|
+
const stillOnLogin = await page.locator(`${SELECTORS.loginForm} .errornote`).count();
|
|
129
|
+
if (stillOnLogin) {
|
|
130
|
+
const text = (await page.locator(`${SELECTORS.loginForm} .errornote`).innerText()).trim();
|
|
131
|
+
throw new Error(`Login as "${username}" was rejected: ${text}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Make sure nobody is logged in: clear the browser's cookies and web
|
|
137
|
+
* storage, load the Django admin login page again, and assert its login
|
|
138
|
+
* form (`#login-form`) is showing.
|
|
139
|
+
*
|
|
140
|
+
* Example #1: Given I am not logged in
|
|
141
|
+
* Example #2: Given we are not logged in
|
|
142
|
+
* Example #3: And not logged in
|
|
143
|
+
* Example #4: Given I am not logged in
|
|
144
|
+
* Example #5: And we are not logged in
|
|
145
|
+
*
|
|
146
|
+
*/
|
|
147
|
+
Given(/^(I am |we are )*not logged in$/, async function (pronounCase) {
|
|
148
|
+
await this.context.clearCookies();
|
|
149
|
+
await this.page.evaluate(() => {
|
|
150
|
+
try { window.localStorage.clear(); } catch (e) { /* storage may be unavailable */ }
|
|
151
|
+
try { window.sessionStorage.clear(); } catch (e) { /* storage may be unavailable */ }
|
|
152
|
+
}).catch(() => {});
|
|
153
|
+
const loginUrl = `${siteRoot(this.launchUrl)}/admin/login/`;
|
|
154
|
+
if (this.page.url().startsWith(loginUrl)) {
|
|
155
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
156
|
+
} else {
|
|
157
|
+
await gotoUrl(this.page, loginUrl);
|
|
158
|
+
}
|
|
159
|
+
await this.page.waitForSelector(SELECTORS.loginForm, { state: 'visible', timeout: 10000 });
|
|
160
|
+
await settle(this, 1000);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Log out of the Django admin / CMS toolbar.
|
|
165
|
+
*
|
|
166
|
+
* django CMS's "Logout <username>" toolbar link submits an AJAX POST
|
|
167
|
+
* (Django's admin logout view rejects a bare GET), so this step opens the
|
|
168
|
+
* toolbar's site-name menu and clicks through it rather than navigating
|
|
169
|
+
* to /admin/logout/ directly.
|
|
170
|
+
*
|
|
171
|
+
* Example #1: When I log out
|
|
172
|
+
* Example #2: When we log out
|
|
173
|
+
* Example #3: And I log out
|
|
174
|
+
*
|
|
175
|
+
*/
|
|
176
|
+
When(/^(I |we )*log out$/, async function (pronounCase) {
|
|
177
|
+
await openToolbarSiteMenu(this.page);
|
|
178
|
+
const logoutLink = this.page.locator(`${SELECTORS.toolbarNav} a`).filter({ hasText: /Logout/i });
|
|
179
|
+
await Promise.all([
|
|
180
|
+
this.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
|
|
181
|
+
logoutLink.first().click(),
|
|
182
|
+
]);
|
|
183
|
+
await settle(this);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// CMS toolbar
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Assert the django CMS toolbar is (or is not) visible. It renders for
|
|
192
|
+
* logged-in staff users and never for anonymous visitors.
|
|
193
|
+
*
|
|
194
|
+
* Example #1: Then I should see the CMS toolbar
|
|
195
|
+
* Example #2: Then I should not see the CMS toolbar
|
|
196
|
+
* Example #3: And we should see the CMS toolbar
|
|
197
|
+
* Example #4: But we should not see the CMS toolbar
|
|
198
|
+
*
|
|
199
|
+
*/
|
|
200
|
+
Then(/^(I |we )*should( not)* see the CMS toolbar$/, async function (pronounCase, notCase) {
|
|
201
|
+
if (notCase) {
|
|
202
|
+
const count = await this.page.locator(SELECTORS.toolbar).count();
|
|
203
|
+
assert.strictEqual(count, 0, 'CMS toolbar should not be visible but it is.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await this.page.waitForSelector(SELECTORS.toolbarBar, { state: 'visible', timeout: 10000 });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Open the toolbar's site-name menu and click a link by its visible text
|
|
211
|
+
* (e.g. the "Pages...", "Users...", "Administration...", "Aliases...", or
|
|
212
|
+
* "User settings..." entries). Matching is case-insensitive and ignores a
|
|
213
|
+
* trailing ellipsis, so "Pages" matches "Pages...".
|
|
214
|
+
*
|
|
215
|
+
* Most of these links carry `data-rel="sideframe"`: django CMS opens them
|
|
216
|
+
* in an in-page iframe overlay (`.cms-sideframe`) rather than navigating
|
|
217
|
+
* the main page. Assert on the sideframe with the step below, not with
|
|
218
|
+
* webship-js's generic "I should be on ..." page-URL steps.
|
|
219
|
+
*
|
|
220
|
+
* Example #1: When I click "Pages" in the CMS toolbar menu
|
|
221
|
+
* Example #2: When I click "Users" in the CMS toolbar menu
|
|
222
|
+
* Example #3: And we click "Administration" in the CMS toolbar menu
|
|
223
|
+
*
|
|
224
|
+
*/
|
|
225
|
+
When(/^(I |we )*click "([^"]*)" in the CMS toolbar menu$/, async function (pronounCase, linkText) {
|
|
226
|
+
await openToolbarSiteMenu(this.page);
|
|
227
|
+
const label = linkText.trim().replace(/[.…]+$/, '');
|
|
228
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
229
|
+
const link = this.page
|
|
230
|
+
.locator(`${SELECTORS.toolbarNav} a`)
|
|
231
|
+
.filter({ hasText: new RegExp(`^\\s*${escaped}(?:\\.\\.\\.|…)?\\s*$`, 'i') });
|
|
232
|
+
await link.first().click();
|
|
233
|
+
await settle(this, 1000);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Assert the CMS sideframe overlay is open and its iframe is showing a
|
|
238
|
+
* URL containing the given substring.
|
|
239
|
+
*
|
|
240
|
+
* Example #1: Then the CMS sideframe should be showing "/admin/cms/pagecontent/"
|
|
241
|
+
* Example #2: Then the CMS sideframe should be showing "/admin/auth/user/"
|
|
242
|
+
* Example #3: And the CMS sideframe should be showing "/admin/"
|
|
243
|
+
*
|
|
244
|
+
*/
|
|
245
|
+
Then(/^the CMS sideframe should be showing "([^"]*)"$/, async function (urlSubstring) {
|
|
246
|
+
const iframe = this.page.locator('.cms-sideframe-frame iframe').first();
|
|
247
|
+
await iframe.waitFor({ state: 'attached', timeout: 10000 });
|
|
248
|
+
const src = await iframe.getAttribute('src');
|
|
249
|
+
assert.ok(
|
|
250
|
+
src && src.includes(urlSubstring),
|
|
251
|
+
`Expected the CMS sideframe to be showing a URL containing "${urlSubstring}" but it was showing "${src}".`
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Page tree
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Navigate directly to the django CMS page-tree admin
|
|
261
|
+
* (/admin/cms/pagecontent/).
|
|
262
|
+
*
|
|
263
|
+
* Example #1: Given I am on the django CMS page tree
|
|
264
|
+
* Example #2: Given we are on the django CMS page tree
|
|
265
|
+
* Example #3: And on the django CMS page tree
|
|
266
|
+
*
|
|
267
|
+
*/
|
|
268
|
+
Given(/^(I am |we are )*on the django CMS page tree$/, async function (pronounCase) {
|
|
269
|
+
await gotoUrl(this.page, `${siteRoot(this.launchUrl)}/admin/cms/pagecontent/`);
|
|
270
|
+
await settle(this);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Assert the page tree admin has rendered (either populated or the
|
|
275
|
+
* "no pages yet" empty state).
|
|
276
|
+
*
|
|
277
|
+
* Example #1: Then I should see the page tree
|
|
278
|
+
* Example #2: Then we should see the page tree
|
|
279
|
+
* Example #3: And should see the page tree
|
|
280
|
+
*
|
|
281
|
+
*/
|
|
282
|
+
Then(/^(I |we )*should see the page tree$/, async function (pronounCase) {
|
|
283
|
+
await this.page.waitForSelector(SELECTORS.pageTree, { state: 'visible', timeout: 10000 });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Assert a page with the given title is listed in the page tree.
|
|
288
|
+
*
|
|
289
|
+
* Example #1: Then I should see "Home" in the page tree
|
|
290
|
+
* Example #2: Then we should see "About" in the page tree
|
|
291
|
+
* Example #3: And should see "Contact" in the page tree
|
|
292
|
+
*
|
|
293
|
+
*/
|
|
294
|
+
Then(/^(I |we )*should see "([^"]*)" in the page tree$/, async function (pronounCase, title) {
|
|
295
|
+
await this.page.waitForSelector(SELECTORS.pageTree, { state: 'visible', timeout: 10000 });
|
|
296
|
+
const entry = this.page.locator('.cms-pagetree-jstree').getByText(title, { exact: false });
|
|
297
|
+
await entry.first().waitFor({ state: 'visible', timeout: 10000 });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Welcome screen (fresh install, no pages yet)
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Assert the django CMS "no pages yet" welcome screen is showing. django CMS
|
|
306
|
+
* renders it for the site root while the site has no pages at all, for a
|
|
307
|
+
* signed-in user: its script sends an anonymous visitor to the login form, so
|
|
308
|
+
* log in first.
|
|
309
|
+
*
|
|
310
|
+
* Example #1: Then I should see the django CMS welcome screen
|
|
311
|
+
* Example #2: Then we should see the django CMS welcome screen
|
|
312
|
+
* Example #3: And should see the django CMS welcome screen
|
|
313
|
+
*
|
|
314
|
+
*/
|
|
315
|
+
Then(/^(I |we )*should see the django CMS welcome screen$/, async function (pronounCase) {
|
|
316
|
+
// The welcome wrapper itself can report no box of its own (its parts are positioned), so the
|
|
317
|
+
// assertion is on a part a visitor reads: the heading or a section.
|
|
318
|
+
await this.page.waitForSelector(SELECTORS.welcomeScreen, { state: 'attached', timeout: 10000 });
|
|
319
|
+
const part = this.page.locator('.cms-welcome-heading, .cms-welcome-section, #cms-welcome-config').first();
|
|
320
|
+
await part.waitFor({ state: 'visible', timeout: 10000 });
|
|
321
|
+
});
|