@webship-js/django-cms-steps 2.0.0-alpha1 → 2.0.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -12
- package/package.json +2 -1
- package/tests/step-definitions/django-cms.steps.js +196 -83
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ module.exports = {
|
|
|
39
39
|
paths: ['tests/features/**/*.feature'],
|
|
40
40
|
worldParameters: {
|
|
41
41
|
launchUrl: process.env.LAUNCH_URL || 'http://localhost',
|
|
42
|
-
// Accounts for "a logged in user with the
|
|
42
|
+
// Accounts for "a logged in user with the "<key>" user". Keep the
|
|
43
43
|
// password out of the repo: read it from an environment variable.
|
|
44
44
|
users: {
|
|
45
45
|
admin: {
|
|
@@ -58,7 +58,7 @@ Then write a feature using the new steps:
|
|
|
58
58
|
Feature: CMS toolbar
|
|
59
59
|
|
|
60
60
|
Scenario: Admin sees the CMS toolbar and opens the page tree
|
|
61
|
-
Given I am a logged in user with the
|
|
61
|
+
Given I am a logged in user with the "admin" user
|
|
62
62
|
Then I should see the CMS toolbar
|
|
63
63
|
When I click "Pages" in the CMS toolbar menu
|
|
64
64
|
Then the CMS sideframe should be showing "/admin/cms/pagecontent/"
|
|
@@ -72,19 +72,29 @@ npx cucumber-js --config cucumber.js
|
|
|
72
72
|
|
|
73
73
|
| Step | Type |
|
|
74
74
|
| --- | --- |
|
|
75
|
-
| `I am a logged in user with the
|
|
75
|
+
| `I am a logged in user with the "<key>" user` (reads `worldParameters.users`) | Given |
|
|
76
76
|
| `I am not logged in` (clears cookies and storage, asserts the admin login form) | Given |
|
|
77
77
|
| `I log out` | When |
|
|
78
|
-
| `I should
|
|
78
|
+
| `I should( not) see the CMS toolbar` | Then |
|
|
79
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
|
|
82
|
-
| `I should see the page tree` | Then |
|
|
83
|
-
| `I should see "<title>" in the page tree` | Then |
|
|
84
|
-
| `I should see the
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
| `the CMS sideframe should( not) be showing "<url part>"` | Then |
|
|
81
|
+
| `I am on the page tree` | Given |
|
|
82
|
+
| `I should( not) see the page tree` | Then |
|
|
83
|
+
| `I should( not) see "<title>" in the page tree` | Then |
|
|
84
|
+
| `I should( not) see the welcome screen` | Then |
|
|
85
|
+
|
|
86
|
+
The product word is optional in every sentence: "the CMS toolbar", "the
|
|
87
|
+
django CMS toolbar" and "the toolbar" all match, so a feature written for
|
|
88
|
+
another pack reads here too. Every Then step takes `should` or `should not`,
|
|
89
|
+
every When/Then step takes `I`, `we` or no pronoun, and every failure is a
|
|
90
|
+
friendly message with a hint (no Playwright stack). Assertions wait on their
|
|
91
|
+
locator for the `minWaitTime.page` budget (at least 10 s) instead of reading
|
|
92
|
+
the page once.
|
|
93
|
+
|
|
94
|
+
Every step's regex and five Gherkin examples are in the JSDoc block above it in
|
|
95
|
+
[`tests/step-definitions/django-cms.steps.js`](tests/step-definitions/django-cms.steps.js);
|
|
96
|
+
`npm run test:examples` checks that each example matches its own step (and no
|
|
97
|
+
webship-js core step) and that every feature step is defined exactly once.
|
|
88
98
|
|
|
89
99
|
## Testing this package itself
|
|
90
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship-js/django-cms-steps",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-rc1",
|
|
4
4
|
"description": "Custom webship-js step definitions for testing django CMS sites (admin login, the CMS toolbar, toolbar navigation, and the page tree).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"test:chromium": "BROWSER=chromium cucumber-js --config cucumber.js",
|
|
15
15
|
"test:firefox": "BROWSER=firefox cucumber-js --config cucumber.js",
|
|
16
16
|
"test:webkit": "BROWSER=webkit cucumber-js --config cucumber.js",
|
|
17
|
+
"test:examples": "node scripts/check-examples.js",
|
|
17
18
|
"report": "generate-reports"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
* Selectors below were captured against a real django CMS 5 install
|
|
15
15
|
* (Django admin login form, the `#cms-top` / `.cms-toolbar` toolbar, and
|
|
16
16
|
* the `.cms-pagetree` page-tree admin). Every step documents its Gherkin
|
|
17
|
-
* phrasing in the JSDoc block right above it.
|
|
17
|
+
* phrasing in the JSDoc block right above it. The product word is optional
|
|
18
|
+
* in every sentence ("the CMS toolbar", "the page tree", "the welcome
|
|
19
|
+
* screen"), so a feature written for another pack reads here too.
|
|
20
|
+
*
|
|
21
|
+
* Every assertion waits on its locator (the assertion is the wait), and
|
|
22
|
+
* every failure is a friendly error with a hint instead of a raw Playwright
|
|
23
|
+
* timeout stack.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
21
|
-
const
|
|
22
|
-
const { gotoUrl, smartSettle } = require('webship-js/tests/step-definitions/webship');
|
|
27
|
+
const { gotoUrl, smartSettle, friendly } = require('webship-js/tests/step-definitions/webship');
|
|
23
28
|
|
|
24
29
|
// ---------------------------------------------------------------------------
|
|
25
30
|
// Shared selectors (django CMS toolbar / Django admin)
|
|
@@ -41,37 +46,73 @@ const SELECTORS = {
|
|
|
41
46
|
pageTreeEmpty: '.cms-pagetree-empty',
|
|
42
47
|
// The "no pages yet" welcome screen shown on a fresh install.
|
|
43
48
|
welcomeScreen: '.cms-welcome-heading, .cms-welcome-section, .cms-welcome, #cms-welcome-config',
|
|
49
|
+
welcomePart: '.cms-welcome-heading, .cms-welcome-section, #cms-welcome-config',
|
|
44
50
|
};
|
|
45
51
|
|
|
52
|
+
const TOOLBAR_HINT = 'The CMS toolbar (`#cms-top`) only renders for a logged-in staff user on a page served through the CMS; log in first and check the toolbar markup.';
|
|
53
|
+
|
|
46
54
|
function siteRoot(launchUrl) {
|
|
47
55
|
return String(launchUrl).replace(/\/$/, '');
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
/** The per-assertion wait budget: `minWaitTime.page` from cucumber.js, else 10 s. */
|
|
59
|
+
function budget(world) {
|
|
60
|
+
return Math.max((world.minWaitTime && world.minWaitTime.page) || 0, 10000);
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
async function settle(world, fallback) {
|
|
51
64
|
await smartSettle(world.page, (world.minWaitTime && world.minWaitTime.page) || fallback || 3000);
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Run a locator action and rethrow as a friendly error (action, target,
|
|
69
|
+
* cause and a hint) instead of Playwright's raw timeout stack.
|
|
70
|
+
*/
|
|
71
|
+
async function actOrExplain(label, target, fn, hint) {
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw friendly({ action: label, target, cause: err, hint });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wait until `locator` has (or, with `notCase`, has no) match in the given
|
|
81
|
+
* state, then throw a friendly error otherwise: the wait and the assertion
|
|
82
|
+
* are one call.
|
|
83
|
+
*/
|
|
84
|
+
async function assertPresence(locator, notCase, { label, hint, timeout, state = 'visible' }) {
|
|
85
|
+
try {
|
|
86
|
+
await locator.first().waitFor({ state: notCase ? (state === 'attached' ? 'detached' : 'hidden') : state, timeout });
|
|
87
|
+
} catch {
|
|
88
|
+
throw friendly({
|
|
89
|
+
message: notCase ? `Expected not to see ${label}, but it is still on the page.` : `Expected ${label}, but it did not appear within ${timeout} ms.`,
|
|
90
|
+
hint,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
/**
|
|
55
96
|
* Look up a named user from cucumber.js worldParameters.users
|
|
56
|
-
* (`{ <key>: { username, password } }`), throwing a
|
|
57
|
-
* not configured.
|
|
97
|
+
* (`{ <key>: { username, password } }`), throwing a friendly error with a
|
|
98
|
+
* hint if it is not configured.
|
|
58
99
|
*/
|
|
59
100
|
function resolveUser(parameters, userKey) {
|
|
60
101
|
const users = (parameters && parameters.users) || {};
|
|
61
102
|
const entry = users[userKey];
|
|
62
103
|
if (!entry) {
|
|
63
|
-
throw
|
|
64
|
-
`User "${userKey}" is not configured.
|
|
65
|
-
|
|
66
|
-
);
|
|
104
|
+
throw friendly({
|
|
105
|
+
message: `User "${userKey}" is not configured. Known users: ${Object.keys(users).join(', ') || '(none)'}.`,
|
|
106
|
+
hint: `Add it to worldParameters.users in cucumber.js: users: { ${userKey}: { username: '...', password: process.env.DJANGOCMS_ADMIN_PASSWORD } }.`,
|
|
107
|
+
});
|
|
67
108
|
}
|
|
68
109
|
const username = entry.username || userKey;
|
|
69
110
|
const password = typeof entry === 'string' ? entry : entry.password;
|
|
70
111
|
if (!password) {
|
|
71
|
-
throw
|
|
72
|
-
`User "${userKey}" has no password configured
|
|
73
|
-
|
|
74
|
-
);
|
|
112
|
+
throw friendly({
|
|
113
|
+
message: `User "${userKey}" has no password configured.`,
|
|
114
|
+
hint: `Set worldParameters.users.${userKey}.password in cucumber.js, ideally from an environment variable such as DJANGOCMS_ADMIN_PASSWORD.`,
|
|
115
|
+
});
|
|
75
116
|
}
|
|
76
117
|
return { username, password };
|
|
77
118
|
}
|
|
@@ -80,10 +121,13 @@ function resolveUser(parameters, userKey) {
|
|
|
80
121
|
* Open the toolbar's site-name dropdown. It opens on click, toggling a
|
|
81
122
|
* "-hover" class via JS; it is not a plain CSS :hover menu.
|
|
82
123
|
*/
|
|
83
|
-
async function openToolbarSiteMenu(
|
|
84
|
-
|
|
85
|
-
const siteMenu = page.locator(`${SELECTORS.toolbarNav} > li`).first();
|
|
86
|
-
await
|
|
124
|
+
async function openToolbarSiteMenu(world) {
|
|
125
|
+
const nav = world.page.locator(SELECTORS.toolbarNav);
|
|
126
|
+
const siteMenu = world.page.locator(`${SELECTORS.toolbarNav} > li`).first();
|
|
127
|
+
await actOrExplain('open', 'the CMS toolbar site menu', async () => {
|
|
128
|
+
await nav.waitFor({ state: 'visible', timeout: budget(world) });
|
|
129
|
+
await siteMenu.click();
|
|
130
|
+
}, TOOLBAR_HINT);
|
|
87
131
|
}
|
|
88
132
|
|
|
89
133
|
// ---------------------------------------------------------------------------
|
|
@@ -102,33 +146,44 @@ async function openToolbarSiteMenu(page) {
|
|
|
102
146
|
* installer) serves a single, unprefixed language, so URLs here are bare:
|
|
103
147
|
* no "/en/" (or other language code) segment.
|
|
104
148
|
*
|
|
105
|
-
* Example #1: Given I am a logged in user with the
|
|
106
|
-
* Example #2: Given we are a logged in user with username "admin"
|
|
149
|
+
* Example #1: Given I am a logged in user with the "admin" user
|
|
150
|
+
* Example #2: Given we are a logged in user with the username "admin"
|
|
107
151
|
* Example #3: Given I am a logged in user with "admin"
|
|
108
|
-
* Example #4: And a logged in user with
|
|
152
|
+
* Example #4: And a logged in user with username "admin"
|
|
109
153
|
* Example #5: Given I am a logged in user with the username "admin" user
|
|
110
154
|
*
|
|
111
155
|
*/
|
|
112
156
|
Given(/^(I am |we are )*a logged in user with( the)*( username)* "([^"]*)?"( user)*$/, async function (pronounCase, theCase, usernameCase, userKey, userCase) {
|
|
113
157
|
const { username, password } = resolveUser(this.parameters, userKey);
|
|
114
158
|
const page = this.page;
|
|
159
|
+
const timeout = budget(this);
|
|
115
160
|
await gotoUrl(page, `${siteRoot(this.launchUrl)}/admin/login/?next=/`);
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
await actOrExplain('log in as', username, async () => {
|
|
162
|
+
await page.locator(SELECTORS.loginUsername).waitFor({ state: 'visible', timeout });
|
|
163
|
+
await page.fill(SELECTORS.loginUsername, username);
|
|
164
|
+
await page.fill(SELECTORS.loginPassword, password);
|
|
165
|
+
await Promise.all([
|
|
166
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
|
|
167
|
+
page.evaluate((formSelector) => {
|
|
168
|
+
const submit = document.querySelector(`${formSelector} input[type="submit"], ${formSelector} button[type="submit"]`);
|
|
169
|
+
if (!submit) throw new Error(`The login form "${formSelector}" has no submit button.`);
|
|
170
|
+
submit.click();
|
|
171
|
+
}, SELECTORS.loginForm),
|
|
172
|
+
]);
|
|
173
|
+
}, 'The Django admin login form (`#login-form` with `#id_username` / `#id_password`) must be served at /admin/login/ under `launchUrl`.');
|
|
127
174
|
await settle(this);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
175
|
+
// The login has two outcomes: the front end (no login form) or the form
|
|
176
|
+
// again with Django's `.errornote`. Wait for whichever comes first.
|
|
177
|
+
const errorNote = page.locator(`${SELECTORS.loginForm} .errornote`);
|
|
178
|
+
await Promise.race([
|
|
179
|
+
page.locator(SELECTORS.loginForm).waitFor({ state: 'detached', timeout }),
|
|
180
|
+
errorNote.waitFor({ state: 'visible', timeout }),
|
|
181
|
+
]).catch(() => {});
|
|
182
|
+
if (await errorNote.isVisible()) {
|
|
183
|
+
throw friendly({
|
|
184
|
+
message: `Login as "${username}" was rejected: ${(await errorNote.innerText()).trim()}`,
|
|
185
|
+
hint: `Check worldParameters.users.${userKey} against the site's admin account (DJANGOCMS_ADMIN_USER / DJANGOCMS_ADMIN_PASSWORD).`,
|
|
186
|
+
});
|
|
132
187
|
}
|
|
133
188
|
});
|
|
134
189
|
|
|
@@ -156,7 +211,11 @@ Given(/^(I am |we are )*not logged in$/, async function (pronounCase) {
|
|
|
156
211
|
} else {
|
|
157
212
|
await gotoUrl(this.page, loginUrl);
|
|
158
213
|
}
|
|
159
|
-
await this.page.
|
|
214
|
+
await assertPresence(this.page.locator(SELECTORS.loginForm), null, {
|
|
215
|
+
label: 'the Django admin login form (`#login-form`)',
|
|
216
|
+
hint: 'Check that `launchUrl` points at the site root (the step opens /admin/login/ under it).',
|
|
217
|
+
timeout: budget(this),
|
|
218
|
+
});
|
|
160
219
|
await settle(this, 1000);
|
|
161
220
|
});
|
|
162
221
|
|
|
@@ -171,15 +230,20 @@ Given(/^(I am |we are )*not logged in$/, async function (pronounCase) {
|
|
|
171
230
|
* Example #1: When I log out
|
|
172
231
|
* Example #2: When we log out
|
|
173
232
|
* Example #3: And I log out
|
|
233
|
+
* Example #4: When log out
|
|
234
|
+
* Example #5: And we log out
|
|
174
235
|
*
|
|
175
236
|
*/
|
|
176
237
|
When(/^(I |we )*log out$/, async function (pronounCase) {
|
|
177
|
-
await openToolbarSiteMenu(this
|
|
178
|
-
const logoutLink = this.page.locator(`${SELECTORS.toolbarNav} a`).filter({ hasText: /Logout/i });
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
238
|
+
await openToolbarSiteMenu(this);
|
|
239
|
+
const logoutLink = this.page.locator(`${SELECTORS.toolbarNav} a`).filter({ hasText: /Logout/i }).first();
|
|
240
|
+
await actOrExplain('click', 'the toolbar "Logout" link', async () => {
|
|
241
|
+
await logoutLink.waitFor({ state: 'visible', timeout: budget(this) });
|
|
242
|
+
await Promise.all([
|
|
243
|
+
this.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
|
|
244
|
+
logoutLink.click(),
|
|
245
|
+
]);
|
|
246
|
+
}, 'The "Logout <username>" entry lives in the toolbar site-name menu; you must be logged in first.');
|
|
183
247
|
await settle(this);
|
|
184
248
|
});
|
|
185
249
|
|
|
@@ -193,17 +257,28 @@ When(/^(I |we )*log out$/, async function (pronounCase) {
|
|
|
193
257
|
*
|
|
194
258
|
* Example #1: Then I should see the CMS toolbar
|
|
195
259
|
* Example #2: Then I should not see the CMS toolbar
|
|
196
|
-
* Example #3: And we should see the CMS toolbar
|
|
260
|
+
* Example #3: And we should see the django CMS toolbar
|
|
197
261
|
* Example #4: But we should not see the CMS toolbar
|
|
262
|
+
* Example #5: Then should see the toolbar
|
|
198
263
|
*
|
|
199
264
|
*/
|
|
200
|
-
Then(/^(I |we )*should( not)* see the CMS toolbar$/, async function (pronounCase, notCase) {
|
|
265
|
+
Then(/^(I |we )*should( not)* see the (?:(?:django )?CMS )?toolbar$/, async function (pronounCase, notCase) {
|
|
201
266
|
if (notCase) {
|
|
202
|
-
|
|
203
|
-
|
|
267
|
+
// The toolbar is never rendered for an anonymous visitor, so the
|
|
268
|
+
// negative case waits for the element to be gone from the DOM.
|
|
269
|
+
await assertPresence(this.page.locator(SELECTORS.toolbar), notCase, {
|
|
270
|
+
label: 'the CMS toolbar',
|
|
271
|
+
hint: 'The toolbar is still in the DOM: the session was not cleared, or the logout did not complete.',
|
|
272
|
+
timeout: budget(this),
|
|
273
|
+
state: 'attached',
|
|
274
|
+
});
|
|
204
275
|
return;
|
|
205
276
|
}
|
|
206
|
-
await this.page.
|
|
277
|
+
await assertPresence(this.page.locator(SELECTORS.toolbarBar), null, {
|
|
278
|
+
label: 'the CMS toolbar',
|
|
279
|
+
hint: TOOLBAR_HINT,
|
|
280
|
+
timeout: budget(this),
|
|
281
|
+
});
|
|
207
282
|
});
|
|
208
283
|
|
|
209
284
|
/**
|
|
@@ -219,37 +294,52 @@ Then(/^(I |we )*should( not)* see the CMS toolbar$/, async function (pronounCase
|
|
|
219
294
|
*
|
|
220
295
|
* Example #1: When I click "Pages" in the CMS toolbar menu
|
|
221
296
|
* Example #2: When I click "Users" in the CMS toolbar menu
|
|
222
|
-
* Example #3: And we click "Administration" in the CMS toolbar menu
|
|
297
|
+
* Example #3: And we click "Administration" in the django CMS toolbar menu
|
|
298
|
+
* Example #4: When I click "User settings" in the toolbar menu
|
|
299
|
+
* Example #5: And click "Aliases" in the CMS toolbar menu
|
|
223
300
|
*
|
|
224
301
|
*/
|
|
225
|
-
When(/^(I |we )*click "([^"]*)" in the CMS toolbar menu$/, async function (pronounCase, linkText) {
|
|
226
|
-
await openToolbarSiteMenu(this
|
|
302
|
+
When(/^(I |we )*click "([^"]*)" in the (?:(?:django )?CMS )?toolbar menu$/, async function (pronounCase, linkText) {
|
|
303
|
+
await openToolbarSiteMenu(this);
|
|
227
304
|
const label = linkText.trim().replace(/[.…]+$/, '');
|
|
228
305
|
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
229
306
|
const link = this.page
|
|
230
307
|
.locator(`${SELECTORS.toolbarNav} a`)
|
|
231
|
-
.filter({ hasText: new RegExp(`^\\s*${escaped}(?:\\.\\.\\.|…)?\\s*$`, 'i') })
|
|
232
|
-
|
|
308
|
+
.filter({ hasText: new RegExp(`^\\s*${escaped}(?:\\.\\.\\.|…)?\\s*$`, 'i') })
|
|
309
|
+
.first();
|
|
310
|
+
await actOrExplain('click', `"${linkText}" in the CMS toolbar menu`, async () => {
|
|
311
|
+
await link.waitFor({ state: 'visible', timeout: budget(this) });
|
|
312
|
+
await link.click();
|
|
313
|
+
}, 'The label must match a toolbar site-menu entry (e.g. "Pages", "Users", "Administration"); a trailing "..." is optional.');
|
|
233
314
|
await settle(this, 1000);
|
|
234
315
|
});
|
|
235
316
|
|
|
236
317
|
/**
|
|
237
|
-
* Assert the CMS sideframe overlay is open
|
|
238
|
-
* URL containing the given substring.
|
|
318
|
+
* Assert the CMS sideframe overlay is (or is not) open with its iframe
|
|
319
|
+
* showing a URL containing the given substring.
|
|
239
320
|
*
|
|
240
321
|
* Example #1: Then the CMS sideframe should be showing "/admin/cms/pagecontent/"
|
|
241
322
|
* Example #2: Then the CMS sideframe should be showing "/admin/auth/user/"
|
|
242
|
-
* Example #3: And the CMS sideframe should be showing "/admin/"
|
|
323
|
+
* Example #3: And the django CMS sideframe should be showing "/admin/"
|
|
324
|
+
* Example #4: Then the sideframe should not be showing "/admin/auth/user/"
|
|
325
|
+
* Example #5: And the CMS sideframe should not be showing "/admin/cms/pagecontent/"
|
|
243
326
|
*
|
|
244
327
|
*/
|
|
245
|
-
Then(/^the CMS sideframe should be showing "([^"]*)"$/, async function (urlSubstring) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
328
|
+
Then(/^the (?:(?:django )?CMS )?sideframe should( not)* be showing "([^"]*)"$/, async function (notCase, urlSubstring) {
|
|
329
|
+
const frames = this.page.locator('.cms-sideframe-frame iframe');
|
|
330
|
+
const matching = frames.and(this.page.locator(`[src*="${urlSubstring.replace(/["\\]/g, '\\$&')}"]`));
|
|
331
|
+
const timeout = budget(this);
|
|
332
|
+
try {
|
|
333
|
+
await matching.first().waitFor({ state: notCase ? 'detached' : 'attached', timeout });
|
|
334
|
+
} catch {
|
|
335
|
+
const src = await frames.first().getAttribute('src').catch(() => null);
|
|
336
|
+
throw friendly({
|
|
337
|
+
message: notCase
|
|
338
|
+
? `Expected the CMS sideframe not to be showing a URL containing "${urlSubstring}", but it is ("${src}").`
|
|
339
|
+
: `Expected the CMS sideframe to be showing a URL containing "${urlSubstring}", but it ${src ? `is showing "${src}"` : 'is not open'}.`,
|
|
340
|
+
hint: 'Toolbar entries with data-rel="sideframe" open in the `.cms-sideframe` overlay; click one with "click \\"...\\" in the CMS toolbar menu" first.',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
253
343
|
});
|
|
254
344
|
|
|
255
345
|
// ---------------------------------------------------------------------------
|
|
@@ -263,38 +353,55 @@ Then(/^the CMS sideframe should be showing "([^"]*)"$/, async function (urlSubst
|
|
|
263
353
|
* Example #1: Given I am on the django CMS page tree
|
|
264
354
|
* Example #2: Given we are on the django CMS page tree
|
|
265
355
|
* Example #3: And on the django CMS page tree
|
|
356
|
+
* Example #4: Given I am on the CMS page tree
|
|
357
|
+
* Example #5: Given I am on the page tree
|
|
266
358
|
*
|
|
267
359
|
*/
|
|
268
|
-
Given(/^(I am |we are )*on the django CMS page tree$/, async function (pronounCase) {
|
|
360
|
+
Given(/^(I am |we are )*on the (?:(?:django )?CMS )?page tree$/, async function (pronounCase) {
|
|
269
361
|
await gotoUrl(this.page, `${siteRoot(this.launchUrl)}/admin/cms/pagecontent/`);
|
|
270
362
|
await settle(this);
|
|
271
363
|
});
|
|
272
364
|
|
|
273
365
|
/**
|
|
274
|
-
* Assert the page tree admin has rendered (either populated
|
|
275
|
-
* "no pages yet" empty state).
|
|
366
|
+
* Assert the page tree admin has (or has not) rendered (either populated
|
|
367
|
+
* or the "no pages yet" empty state).
|
|
276
368
|
*
|
|
277
369
|
* Example #1: Then I should see the page tree
|
|
278
370
|
* Example #2: Then we should see the page tree
|
|
279
|
-
* Example #3: And should see the page tree
|
|
371
|
+
* Example #3: And should see the CMS page tree
|
|
372
|
+
* Example #4: Then I should not see the page tree
|
|
373
|
+
* Example #5: And we should see the django CMS page tree
|
|
280
374
|
*
|
|
281
375
|
*/
|
|
282
|
-
Then(/^(I |we )*should see the page tree$/, async function (pronounCase) {
|
|
283
|
-
await this.page.
|
|
376
|
+
Then(/^(I |we )*should( not)* see the (?:(?:django )?CMS )?page tree$/, async function (pronounCase, notCase) {
|
|
377
|
+
await assertPresence(this.page.locator(SELECTORS.pageTree), notCase, {
|
|
378
|
+
label: 'the page tree (`.cms-pagetree`)',
|
|
379
|
+
hint: 'The page tree is the change list at /admin/cms/pagecontent/ and needs a logged-in staff user; open it with "I am on the page tree".',
|
|
380
|
+
timeout: budget(this),
|
|
381
|
+
});
|
|
284
382
|
});
|
|
285
383
|
|
|
286
384
|
/**
|
|
287
|
-
* Assert a page with the given title is listed in the page tree.
|
|
385
|
+
* Assert a page with the given title is (or is not) listed in the page tree.
|
|
288
386
|
*
|
|
289
387
|
* Example #1: Then I should see "Home" in the page tree
|
|
290
388
|
* Example #2: Then we should see "About" in the page tree
|
|
291
389
|
* Example #3: And should see "Contact" in the page tree
|
|
390
|
+
* Example #4: Then I should not see "Draft" in the page tree
|
|
391
|
+
* Example #5: And we should see "Home" in the CMS page tree
|
|
292
392
|
*
|
|
293
393
|
*/
|
|
294
|
-
Then(/^(I |we )*should see "([^"]*)" in the page tree$/, async function (pronounCase, title) {
|
|
295
|
-
await this.page.
|
|
296
|
-
|
|
297
|
-
|
|
394
|
+
Then(/^(I |we )*should( not)* see "([^"]*)" in the (?:(?:django )?CMS )?page tree$/, async function (pronounCase, notCase, title) {
|
|
395
|
+
await assertPresence(this.page.locator(SELECTORS.pageTree), null, {
|
|
396
|
+
label: 'the page tree (`.cms-pagetree`)',
|
|
397
|
+
hint: 'Open the page tree first with "I am on the page tree".',
|
|
398
|
+
timeout: budget(this),
|
|
399
|
+
});
|
|
400
|
+
await assertPresence(this.page.locator('.cms-pagetree-jstree').getByText(title, { exact: false }), notCase, {
|
|
401
|
+
label: `a page titled "${title}" in the page tree`,
|
|
402
|
+
hint: 'Page titles come from the pages the site really has; create the page first or check the spelling.',
|
|
403
|
+
timeout: budget(this),
|
|
404
|
+
});
|
|
298
405
|
});
|
|
299
406
|
|
|
300
407
|
// ---------------------------------------------------------------------------
|
|
@@ -302,20 +409,26 @@ Then(/^(I |we )*should see "([^"]*)" in the page tree$/, async function (pronoun
|
|
|
302
409
|
// ---------------------------------------------------------------------------
|
|
303
410
|
|
|
304
411
|
/**
|
|
305
|
-
* Assert the django CMS "no pages yet" welcome screen is
|
|
306
|
-
* renders it for the site root while the site has no
|
|
307
|
-
* signed-in user: its script sends an anonymous visitor
|
|
308
|
-
* log in first.
|
|
412
|
+
* Assert the django CMS "no pages yet" welcome screen is (or is not)
|
|
413
|
+
* showing. django CMS renders it for the site root while the site has no
|
|
414
|
+
* pages at all, for a signed-in user: its script sends an anonymous visitor
|
|
415
|
+
* to the login form, so log in first.
|
|
309
416
|
*
|
|
310
417
|
* Example #1: Then I should see the django CMS welcome screen
|
|
311
418
|
* Example #2: Then we should see the django CMS welcome screen
|
|
312
|
-
* Example #3: And should see the
|
|
419
|
+
* Example #3: And should see the CMS welcome screen
|
|
420
|
+
* Example #4: Then I should see the welcome screen
|
|
421
|
+
* Example #5: Then I should not see the welcome screen
|
|
313
422
|
*
|
|
314
423
|
*/
|
|
315
|
-
Then(/^(I |we )*should see the django CMS welcome screen$/, async function (pronounCase) {
|
|
424
|
+
Then(/^(I |we )*should( not)* see the (?:(?:django )?CMS )?welcome screen$/, async function (pronounCase, notCase) {
|
|
316
425
|
// The welcome wrapper itself can report no box of its own (its parts are positioned), so the
|
|
317
426
|
// assertion is on a part a visitor reads: the heading or a section.
|
|
318
|
-
await this.page.
|
|
319
|
-
|
|
320
|
-
|
|
427
|
+
await assertPresence(this.page.locator(SELECTORS.welcomePart), notCase, {
|
|
428
|
+
label: 'the "no pages yet" welcome screen',
|
|
429
|
+
hint: notCase
|
|
430
|
+
? 'The welcome screen shows while the site has no pages at all; create a page to replace it.'
|
|
431
|
+
: 'The welcome screen only renders on the site root of a site with no pages, for a signed-in user; log in first.',
|
|
432
|
+
timeout: budget(this),
|
|
433
|
+
});
|
|
321
434
|
});
|