@webship-js/django-cms-steps 2.0.0-beta1 → 2.0.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 username "<key>"". Keep 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 username "admin"
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 username "<key>"` (reads `worldParameters.users`) | Given |
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 see the CMS toolbar` / `I should not see the CMS toolbar` | Then |
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 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).
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-beta1",
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 assert = require('assert');
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 helpful error if it is
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 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
- );
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 new Error(
72
- `User "${userKey}" has no password configured. Set worldParameters.users.${userKey}.password ` +
73
- 'in cucumber.js, ideally from an environment variable.'
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(page) {
84
- await page.waitForSelector(SELECTORS.toolbarNav, { state: 'visible', timeout: 10000 });
85
- const siteMenu = page.locator(`${SELECTORS.toolbarNav} > li`).first();
86
- await siteMenu.click();
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 username "admin"
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 the username "admin"
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 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
- ]);
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
- 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}`);
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.waitForSelector(SELECTORS.loginForm, { state: 'visible', timeout: 10000 });
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.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
- ]);
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
- const count = await this.page.locator(SELECTORS.toolbar).count();
203
- assert.strictEqual(count, 0, 'CMS toolbar should not be visible but it is.');
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.waitForSelector(SELECTORS.toolbarBar, { state: 'visible', timeout: 10000 });
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.page);
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
- await link.first().click();
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 and its iframe is showing a
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 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
- );
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 or the
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.waitForSelector(SELECTORS.pageTree, { state: 'visible', timeout: 10000 });
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.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 });
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 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.
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 django CMS welcome screen
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.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 });
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
  });