@nextsparkjs/theme-blog 0.1.0-beta.18 → 0.1.0-beta.20
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/package.json +2 -2
- package/tests/cypress/e2e/README.md +170 -0
- package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
- package/tests/cypress/e2e/categories/categories-crud.md +73 -0
- package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
- package/tests/cypress/e2e/posts/posts-crud.md +115 -0
- package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
- package/tests/cypress/e2e/posts/posts-editor.md +139 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +42 -0
- package/tests/cypress/src/FeaturedImageUpload.js +131 -0
- package/tests/cypress/src/PostEditor.js +386 -0
- package/tests/cypress/src/PostsList.js +350 -0
- package/tests/cypress/src/WysiwygEditor.js +373 -0
- package/tests/cypress/src/components/EntityForm.ts +357 -0
- package/tests/cypress/src/components/EntityList.ts +360 -0
- package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
- package/tests/cypress/src/components/PostsPOM.ts +362 -0
- package/tests/cypress/src/components/index.ts +18 -0
- package/tests/cypress/src/index.js +33 -0
- package/tests/cypress/src/selectors.ts +49 -0
- package/tests/cypress/src/session-helpers.ts +89 -0
- package/tests/cypress/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -0
- package/tests/tsconfig.json +15 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostEditorPOM - Blog Theme Post Create/Edit Page
|
|
3
|
+
*
|
|
4
|
+
* Handles post creation and editing with WYSIWYG editor.
|
|
5
|
+
* Supports both create and edit modes with dynamic selector prefixes.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This POM uses the blog's custom editor pattern (post-create-* / post-edit-*)
|
|
8
|
+
* which is intentionally different from the standard entity convention.
|
|
9
|
+
* The editor is a complex, unique UI that benefits from mode-specific selectors.
|
|
10
|
+
*
|
|
11
|
+
* Test cases: BLOG_POST_CREATE_001-004, BLOG_POST_UPDATE_001-004
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type EditorMode = 'create' | 'edit'
|
|
15
|
+
|
|
16
|
+
export interface PostData {
|
|
17
|
+
title?: string
|
|
18
|
+
content?: string
|
|
19
|
+
slug?: string
|
|
20
|
+
excerpt?: string
|
|
21
|
+
featured?: boolean
|
|
22
|
+
status?: 'draft' | 'published' | 'scheduled'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class PostEditorPOM {
|
|
26
|
+
private mode: EditorMode
|
|
27
|
+
private prefix: string
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a PostEditorPOM instance
|
|
31
|
+
*/
|
|
32
|
+
constructor(mode: EditorMode = 'create') {
|
|
33
|
+
this.mode = mode
|
|
34
|
+
this.prefix = mode === 'create' ? 'post-create' : 'post-edit'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// STATIC FACTORY METHODS
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
static forCreate(): PostEditorPOM {
|
|
42
|
+
return new PostEditorPOM('create')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static forEdit(): PostEditorPOM {
|
|
46
|
+
return new PostEditorPOM('edit')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// DYNAMIC SELECTORS (Mode-aware)
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get selectors based on current mode
|
|
55
|
+
*/
|
|
56
|
+
get selectors() {
|
|
57
|
+
const prefix = this.prefix
|
|
58
|
+
return {
|
|
59
|
+
// Container
|
|
60
|
+
container: `[data-cy="${prefix}-container"]`,
|
|
61
|
+
header: `[data-cy="${prefix}-header"]`,
|
|
62
|
+
|
|
63
|
+
// Navigation
|
|
64
|
+
back: `[data-cy="${prefix}-back"]`,
|
|
65
|
+
|
|
66
|
+
// Status
|
|
67
|
+
status: `[data-cy="${prefix}-status"]`,
|
|
68
|
+
autosaved: `[data-cy="${prefix}-autosaved"]`,
|
|
69
|
+
|
|
70
|
+
// Actions
|
|
71
|
+
settingsToggle: `[data-cy="${prefix}-settings-toggle"]`,
|
|
72
|
+
save: `[data-cy="${prefix}-save"]`,
|
|
73
|
+
publish: `[data-cy="${prefix}-publish"]`,
|
|
74
|
+
unpublish: `[data-cy="${prefix}-unpublish"]`, // Edit mode only
|
|
75
|
+
viewLive: `[data-cy="${prefix}-view-live"]`, // Edit mode only
|
|
76
|
+
|
|
77
|
+
// Error
|
|
78
|
+
error: `[data-cy="${prefix}-error"]`,
|
|
79
|
+
errorDismiss: `[data-cy="${prefix}-error-dismiss"]`,
|
|
80
|
+
|
|
81
|
+
// Editor
|
|
82
|
+
title: `[data-cy="${prefix}-title"]`,
|
|
83
|
+
content: `[data-cy="${prefix}-content"]`,
|
|
84
|
+
|
|
85
|
+
// Settings Panel
|
|
86
|
+
settings: `[data-cy="${prefix}-settings"]`,
|
|
87
|
+
statusSelect: `[data-cy="${prefix}-status-select"]`,
|
|
88
|
+
slug: `[data-cy="${prefix}-slug"]`,
|
|
89
|
+
excerpt: `[data-cy="${prefix}-excerpt"]`,
|
|
90
|
+
featuredImage: `[data-cy="${prefix}-featured-image"]`,
|
|
91
|
+
featuredToggle: `[data-cy="${prefix}-featured-toggle"]`,
|
|
92
|
+
|
|
93
|
+
// Delete Dialog (Edit mode only)
|
|
94
|
+
delete: `[data-cy="${prefix}-delete"]`,
|
|
95
|
+
deleteDialog: `[data-cy="${prefix}-delete-dialog"]`,
|
|
96
|
+
deleteConfirm: `[data-cy="${prefix}-delete-confirm"]`,
|
|
97
|
+
deleteCancel: `[data-cy="${prefix}-delete-cancel"]`,
|
|
98
|
+
|
|
99
|
+
// WYSIWYG Editor (nested)
|
|
100
|
+
wysiwygContent: '[data-cy="wysiwyg-content"]',
|
|
101
|
+
wysiwygToolbar: '[data-cy="wysiwyg-toolbar"]',
|
|
102
|
+
|
|
103
|
+
// Unsaved indicator
|
|
104
|
+
unsavedIndicator: '[data-cy="post-unsaved-indicator"]',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================
|
|
109
|
+
// VALIDATION METHODS
|
|
110
|
+
// ============================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate page is visible and loaded
|
|
114
|
+
*/
|
|
115
|
+
validatePageVisible() {
|
|
116
|
+
cy.get(this.selectors.container).should('be.visible')
|
|
117
|
+
cy.get(this.selectors.header).should('be.visible')
|
|
118
|
+
|
|
119
|
+
const urlPattern = this.mode === 'create'
|
|
120
|
+
? '/dashboard/posts/create'
|
|
121
|
+
: '/dashboard/posts/.+/edit'
|
|
122
|
+
cy.url().should('match', new RegExp(urlPattern))
|
|
123
|
+
|
|
124
|
+
return this
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate settings panel is visible
|
|
129
|
+
*/
|
|
130
|
+
validateSettingsVisible() {
|
|
131
|
+
cy.get(this.selectors.settings).should('be.visible')
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate settings panel is hidden
|
|
137
|
+
*/
|
|
138
|
+
validateSettingsHidden() {
|
|
139
|
+
cy.get(this.selectors.settings).should('not.exist')
|
|
140
|
+
return this
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate title value
|
|
145
|
+
*/
|
|
146
|
+
validateTitle(expectedTitle: string) {
|
|
147
|
+
cy.get(this.selectors.title).should('have.value', expectedTitle)
|
|
148
|
+
return this
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate slug value
|
|
153
|
+
*/
|
|
154
|
+
validateSlug(expectedSlug: string) {
|
|
155
|
+
cy.get(this.selectors.slug).should('have.value', expectedSlug)
|
|
156
|
+
return this
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate featured toggle state
|
|
161
|
+
*/
|
|
162
|
+
validateFeaturedState(enabled: boolean) {
|
|
163
|
+
const expectedState = enabled ? 'checked' : 'unchecked'
|
|
164
|
+
cy.get(this.selectors.featuredToggle, { timeout: 5000 })
|
|
165
|
+
.should('have.attr', 'data-state', expectedState)
|
|
166
|
+
return this
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate auto-saved indicator is shown
|
|
171
|
+
*/
|
|
172
|
+
validateAutoSaved() {
|
|
173
|
+
cy.get(this.selectors.autosaved).should('be.visible')
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate error message is shown
|
|
179
|
+
*/
|
|
180
|
+
validateError(errorType?: string) {
|
|
181
|
+
cy.get(this.selectors.error).should('be.visible')
|
|
182
|
+
if (errorType) {
|
|
183
|
+
cy.get(this.selectors.error).should('have.attr', 'data-cy-error', errorType)
|
|
184
|
+
}
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate error is not visible
|
|
190
|
+
*/
|
|
191
|
+
validateNoError() {
|
|
192
|
+
cy.get(this.selectors.error).should('not.exist')
|
|
193
|
+
return this
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validate status badge shows correct status
|
|
198
|
+
*/
|
|
199
|
+
validateStatusBadge(status: 'draft' | 'published' | 'new-draft' | 'new-post') {
|
|
200
|
+
cy.get(this.selectors.status).should('have.attr', 'data-cy-status', status)
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate unsaved changes indicator is visible
|
|
206
|
+
*/
|
|
207
|
+
validateUnsavedChanges() {
|
|
208
|
+
cy.get(this.selectors.unsavedIndicator).should('be.visible')
|
|
209
|
+
return this
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate unsaved changes indicator is not visible
|
|
214
|
+
*/
|
|
215
|
+
validateNoUnsavedChanges() {
|
|
216
|
+
cy.get(this.selectors.unsavedIndicator).should('not.exist')
|
|
217
|
+
return this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate delete dialog is visible
|
|
222
|
+
*/
|
|
223
|
+
validateDeleteDialogVisible() {
|
|
224
|
+
cy.get(this.selectors.deleteDialog).should('be.visible')
|
|
225
|
+
return this
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================
|
|
229
|
+
// NAVIGATION METHODS
|
|
230
|
+
// ============================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Click back button to return to posts list
|
|
234
|
+
*/
|
|
235
|
+
clickBack() {
|
|
236
|
+
cy.get(this.selectors.back).click()
|
|
237
|
+
return this
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Toggle settings panel visibility
|
|
242
|
+
*/
|
|
243
|
+
toggleSettings() {
|
|
244
|
+
cy.get(this.selectors.settingsToggle).click()
|
|
245
|
+
return this
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Visit create page
|
|
250
|
+
*/
|
|
251
|
+
visitCreate() {
|
|
252
|
+
cy.visit('/dashboard/posts/create')
|
|
253
|
+
this.validatePageVisible()
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Visit edit page
|
|
259
|
+
*/
|
|
260
|
+
visitEdit(postId: string) {
|
|
261
|
+
cy.visit(`/dashboard/posts/${postId}/edit`)
|
|
262
|
+
this.validatePageVisible()
|
|
263
|
+
return this
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================
|
|
267
|
+
// INPUT METHODS
|
|
268
|
+
// ============================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Fill post title
|
|
272
|
+
*/
|
|
273
|
+
fillTitle(title: string) {
|
|
274
|
+
cy.get(this.selectors.title)
|
|
275
|
+
.clear()
|
|
276
|
+
.type(title)
|
|
277
|
+
return this
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get current title value
|
|
282
|
+
*/
|
|
283
|
+
getTitle() {
|
|
284
|
+
return cy.get(this.selectors.title).invoke('val')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Focus on content editor
|
|
289
|
+
*/
|
|
290
|
+
focusContent() {
|
|
291
|
+
cy.get(this.selectors.content).find(this.selectors.wysiwygContent).focus()
|
|
292
|
+
return this
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type content in the WYSIWYG editor
|
|
297
|
+
*/
|
|
298
|
+
typeContent(content: string) {
|
|
299
|
+
cy.get(this.selectors.content)
|
|
300
|
+
.find(this.selectors.wysiwygContent)
|
|
301
|
+
.focus()
|
|
302
|
+
.type(content)
|
|
303
|
+
return this
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Set post status via dropdown
|
|
308
|
+
*/
|
|
309
|
+
setStatus(status: 'draft' | 'published' | 'scheduled') {
|
|
310
|
+
cy.get(this.selectors.statusSelect).click()
|
|
311
|
+
cy.get(`[data-value="${status}"]`).click()
|
|
312
|
+
return this
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fill URL slug
|
|
317
|
+
*/
|
|
318
|
+
fillSlug(slug: string) {
|
|
319
|
+
cy.get(this.selectors.slug)
|
|
320
|
+
.clear()
|
|
321
|
+
.type(slug)
|
|
322
|
+
return this
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Fill excerpt
|
|
327
|
+
*/
|
|
328
|
+
fillExcerpt(excerpt: string) {
|
|
329
|
+
cy.get(this.selectors.excerpt)
|
|
330
|
+
.clear()
|
|
331
|
+
.type(excerpt)
|
|
332
|
+
return this
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Toggle featured post switch
|
|
337
|
+
*/
|
|
338
|
+
toggleFeatured(enabled: boolean) {
|
|
339
|
+
const expectedState = enabled ? 'checked' : 'unchecked'
|
|
340
|
+
|
|
341
|
+
cy.get(this.selectors.featuredToggle).then($switch => {
|
|
342
|
+
const currentState = $switch.attr('data-state')
|
|
343
|
+
if (currentState !== expectedState) {
|
|
344
|
+
cy.wrap($switch).click({ force: true })
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
cy.get(this.selectors.featuredToggle, { timeout: 5000 })
|
|
349
|
+
.should('have.attr', 'data-state', expectedState)
|
|
350
|
+
|
|
351
|
+
return this
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Fill complete post form
|
|
356
|
+
*/
|
|
357
|
+
fillPost(post: PostData) {
|
|
358
|
+
if (post.title) this.fillTitle(post.title)
|
|
359
|
+
if (post.content) this.typeContent(post.content)
|
|
360
|
+
if (post.slug) this.fillSlug(post.slug)
|
|
361
|
+
if (post.excerpt) this.fillExcerpt(post.excerpt)
|
|
362
|
+
if (post.featured !== undefined) this.toggleFeatured(post.featured)
|
|
363
|
+
if (post.status) this.setStatus(post.status)
|
|
364
|
+
return this
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================
|
|
368
|
+
// ACTION METHODS
|
|
369
|
+
// ============================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Click save draft button
|
|
373
|
+
*/
|
|
374
|
+
saveDraft() {
|
|
375
|
+
cy.get(this.selectors.save).click()
|
|
376
|
+
return this
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Click publish button
|
|
381
|
+
*/
|
|
382
|
+
publish() {
|
|
383
|
+
cy.get(this.selectors.publish).click()
|
|
384
|
+
return this
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Click unpublish button (edit mode only)
|
|
389
|
+
*/
|
|
390
|
+
unpublish() {
|
|
391
|
+
if (this.mode !== 'edit') {
|
|
392
|
+
throw new Error('unpublish is only available in edit mode')
|
|
393
|
+
}
|
|
394
|
+
cy.get(this.selectors.unpublish).click()
|
|
395
|
+
return this
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Click view live button (edit mode only)
|
|
400
|
+
*/
|
|
401
|
+
clickViewLive() {
|
|
402
|
+
if (this.mode !== 'edit') {
|
|
403
|
+
throw new Error('viewLive is only available in edit mode')
|
|
404
|
+
}
|
|
405
|
+
cy.get(this.selectors.viewLive).click()
|
|
406
|
+
return this
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Click delete button to open dialog (edit mode only)
|
|
411
|
+
*/
|
|
412
|
+
clickDelete() {
|
|
413
|
+
if (this.mode !== 'edit') {
|
|
414
|
+
throw new Error('delete is only available in edit mode')
|
|
415
|
+
}
|
|
416
|
+
cy.get(this.selectors.delete).click()
|
|
417
|
+
return this
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Confirm deletion in dialog (edit mode only)
|
|
422
|
+
*/
|
|
423
|
+
confirmDelete() {
|
|
424
|
+
cy.get(this.selectors.deleteDialog).should('be.visible')
|
|
425
|
+
cy.get(this.selectors.deleteConfirm).click()
|
|
426
|
+
return this
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Cancel deletion in dialog (edit mode only)
|
|
431
|
+
*/
|
|
432
|
+
cancelDelete() {
|
|
433
|
+
cy.get(this.selectors.deleteDialog).should('be.visible')
|
|
434
|
+
cy.get(this.selectors.deleteCancel).click()
|
|
435
|
+
return this
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Dismiss error message
|
|
440
|
+
*/
|
|
441
|
+
dismissError() {
|
|
442
|
+
cy.get(this.selectors.errorDismiss).click()
|
|
443
|
+
return this
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export default PostEditorPOM
|