@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.
@@ -0,0 +1,460 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Posts CRUD Tests - Blog Theme
5
+ *
6
+ * Tests for Posts entity CRUD operations.
7
+ * Uses custom PostsList and PostEditor POM classes.
8
+ *
9
+ * Theme Mode: single-user (isolated blogs, no team collaboration)
10
+ */
11
+
12
+ import { PostsList } from '../../../src/PostsList.js'
13
+ import { PostEditor } from '../../../src/PostEditor.js'
14
+ import { loginAsBlogAuthor, BLOG_USERS } from '../../../src/session-helpers'
15
+
16
+ describe('Posts CRUD - Blog Author (Full Access)', () => {
17
+ const postsList = new PostsList()
18
+
19
+ beforeEach(() => {
20
+ loginAsBlogAuthor('MARCOS')
21
+ cy.visit('/dashboard/posts')
22
+ postsList.validateListVisible()
23
+ })
24
+
25
+ // =========================================================================
26
+ // CREATE - Author can create posts
27
+ // =========================================================================
28
+ describe('CREATE - Author can create posts', () => {
29
+ it('BLOG_POST_CREATE_001: should create draft post', () => {
30
+ const postTitle = `Draft Post ${Date.now()}`
31
+ const postEditor = new PostEditor('create')
32
+
33
+ // Click create button
34
+ postsList.clickCreate()
35
+
36
+ // Validate create page
37
+ postEditor.validatePageVisible()
38
+ postEditor.validateStatusBadge('new-draft')
39
+
40
+ // Fill post data
41
+ postEditor.fillTitle(postTitle)
42
+ postEditor.typeContent('This is a draft post content.')
43
+
44
+ // Save as draft
45
+ postEditor.saveDraft()
46
+
47
+ // Validate redirect to edit page
48
+ cy.url().should('match', /\/dashboard\/posts\/[a-z0-9-]+\/edit/)
49
+
50
+ // After redirect, we're now on edit page - use edit mode editor
51
+ const editEditor = new PostEditor('edit')
52
+ editEditor.validateStatusBadge('draft')
53
+
54
+ cy.log('✅ Author created draft post successfully')
55
+ })
56
+
57
+ it('BLOG_POST_CREATE_002: should create and publish post immediately', () => {
58
+ const postTitle = `Published Post ${Date.now()}`
59
+ const postEditor = new PostEditor('create')
60
+
61
+ // Click create button
62
+ postsList.clickCreate()
63
+
64
+ // Fill post data
65
+ postEditor.fillTitle(postTitle)
66
+ postEditor.typeContent('This is a published post content.')
67
+
68
+ // Publish immediately
69
+ postEditor.publish()
70
+
71
+ // Validate redirect to edit page
72
+ cy.url().should('match', /\/dashboard\/posts\/[a-z0-9-]+\/edit/)
73
+
74
+ // After redirect, we're now on edit page - use edit mode editor
75
+ const editEditor = new PostEditor('edit')
76
+
77
+ // Go back to list using edit mode editor
78
+ editEditor.clickBack()
79
+
80
+ // Validate post appears with published status
81
+ cy.contains(postTitle).should('be.visible')
82
+
83
+ cy.log('✅ Author created and published post successfully')
84
+ })
85
+
86
+ it('BLOG_POST_CREATE_003: should create post with all metadata', () => {
87
+ const postTitle = `Full Post ${Date.now()}`
88
+ const postSlug = `full-post-${Date.now()}`
89
+ const postExcerpt = 'This is a brief excerpt for the post.'
90
+ const postEditor = new PostEditor('create')
91
+
92
+ // Click create button
93
+ postsList.clickCreate()
94
+
95
+ // Fill all fields
96
+ postEditor.fillTitle(postTitle)
97
+ postEditor.typeContent('This is a complete post with all metadata filled.')
98
+ postEditor.fillSlug(postSlug)
99
+ postEditor.fillExcerpt(postExcerpt)
100
+ postEditor.toggleFeatured(true)
101
+
102
+ // Save
103
+ postEditor.saveDraft()
104
+
105
+ // Validate slug was saved
106
+ const editEditor = new PostEditor('edit')
107
+ editEditor.validateSlug(postSlug)
108
+ editEditor.validateFeaturedState(true)
109
+
110
+ cy.log('✅ Author created post with all metadata successfully')
111
+ })
112
+
113
+ it('BLOG_POST_CREATE_004: should show validation error for empty title', () => {
114
+ const postEditor = new PostEditor('create')
115
+
116
+ // Click create button
117
+ postsList.clickCreate()
118
+
119
+ // Try to save without title
120
+ postEditor.typeContent('Content without a title')
121
+ postEditor.saveDraft()
122
+
123
+ // Validate error is shown
124
+ postEditor.validateError('title-required')
125
+
126
+ cy.log('✅ Validation error shown for empty title')
127
+ })
128
+ })
129
+
130
+ // =========================================================================
131
+ // READ - Author can view posts
132
+ // =========================================================================
133
+ describe('READ - Author can view posts', () => {
134
+ it('BLOG_POST_READ_001: should view posts list in table mode', () => {
135
+ // Set table view
136
+ postsList.setViewMode('table')
137
+
138
+ // Validate table view
139
+ postsList.validateViewMode('table')
140
+
141
+ cy.log('✅ Author can view posts in table mode')
142
+ })
143
+
144
+ it('BLOG_POST_READ_002: should view posts list in grid mode', () => {
145
+ // Set grid view
146
+ postsList.setViewMode('grid')
147
+
148
+ // Validate grid view
149
+ postsList.validateViewMode('grid')
150
+
151
+ cy.log('✅ Author can view posts in grid mode')
152
+ })
153
+
154
+ it('BLOG_POST_READ_003: should filter posts by status', () => {
155
+ // Filter by published
156
+ postsList.filterByStatus('published')
157
+ cy.wait(500) // Wait for filter to apply
158
+
159
+ // Filter by draft
160
+ postsList.filterByStatus('draft')
161
+ cy.wait(500)
162
+
163
+ // Show all
164
+ postsList.filterByStatus('all')
165
+
166
+ cy.log('✅ Author can filter posts by status')
167
+ })
168
+
169
+ it('BLOG_POST_READ_004: should search posts by title', () => {
170
+ // Create a post with unique title first
171
+ const uniqueTitle = `Searchable Post ${Date.now()}`
172
+ const postEditor = new PostEditor('create')
173
+
174
+ postsList.clickCreate()
175
+ postEditor.fillTitle(uniqueTitle)
176
+ postEditor.typeContent('Content for search test')
177
+ postEditor.saveDraft()
178
+ postEditor.clickBack()
179
+
180
+ // Search for the post
181
+ postsList.search(uniqueTitle.substring(0, 10))
182
+ cy.wait(500) // Wait for search
183
+
184
+ // Validate search results
185
+ cy.contains(uniqueTitle).should('be.visible')
186
+
187
+ // Clear search
188
+ postsList.clearSearch()
189
+
190
+ cy.log('✅ Author can search posts')
191
+ })
192
+
193
+ it('BLOG_POST_READ_005: should view post details via edit', () => {
194
+ // Check if there are posts to view
195
+ cy.get('body').then($body => {
196
+ const rowSelector = '[data-cy^="posts-row-"]'
197
+
198
+ if ($body.find(rowSelector).length > 0) {
199
+ // Get the first post ID and click edit
200
+ cy.get(rowSelector).first().invoke('attr', 'data-cy').then(dataCy => {
201
+ const postId = dataCy?.replace('posts-row-', '')
202
+ if (postId) {
203
+ postsList.clickEdit(postId)
204
+
205
+ // Validate edit page
206
+ const postEditor = new PostEditor('edit')
207
+ postEditor.validatePageVisible()
208
+
209
+ cy.log('✅ Author can view post details')
210
+ }
211
+ })
212
+ } else {
213
+ cy.log('⚠️ No posts available to view details')
214
+ }
215
+ })
216
+ })
217
+ })
218
+
219
+ // =========================================================================
220
+ // UPDATE - Author can update posts
221
+ // =========================================================================
222
+ describe('UPDATE - Author can update posts', () => {
223
+ beforeEach(() => {
224
+ // Create a test post for update tests
225
+ const postEditor = new PostEditor('create')
226
+ const testTitle = `Update Test ${Date.now()}`
227
+
228
+ postsList.clickCreate()
229
+ postEditor.fillTitle(testTitle)
230
+ postEditor.typeContent('Original content')
231
+ postEditor.saveDraft()
232
+ postEditor.clickBack()
233
+ postsList.validateListVisible()
234
+ })
235
+
236
+ it('BLOG_POST_UPDATE_001: should edit post title and content', () => {
237
+ // Find and edit the first post
238
+ cy.get('[data-cy^="posts-row-"]').first().invoke('attr', 'data-cy').then(dataCy => {
239
+ const postId = dataCy?.replace('posts-row-', '')
240
+ if (postId) {
241
+ postsList.clickEdit(postId)
242
+
243
+ const postEditor = new PostEditor('edit')
244
+ postEditor.validatePageVisible()
245
+
246
+ // Update title
247
+ const updatedTitle = `Updated Title ${Date.now()}`
248
+ postEditor.fillTitle(updatedTitle)
249
+
250
+ // Save
251
+ postEditor.saveDraft()
252
+ postEditor.validateAutoSaved()
253
+
254
+ // Go back and verify
255
+ postEditor.clickBack()
256
+ cy.contains(updatedTitle).should('be.visible')
257
+
258
+ cy.log('✅ Author updated post title successfully')
259
+ }
260
+ })
261
+ })
262
+
263
+ it('BLOG_POST_UPDATE_002: should change post status from draft to published', () => {
264
+ // Find and edit the first draft post
265
+ postsList.filterByStatus('draft')
266
+
267
+ cy.get('body').then($body => {
268
+ const rowSelector = '[data-cy^="posts-row-"]'
269
+
270
+ if ($body.find(rowSelector).length > 0) {
271
+ cy.get(rowSelector).first().invoke('attr', 'data-cy').then(dataCy => {
272
+ const postId = dataCy?.replace('posts-row-', '')
273
+ if (postId) {
274
+ postsList.clickEdit(postId)
275
+
276
+ const postEditor = new PostEditor('edit')
277
+ postEditor.validatePageVisible()
278
+
279
+ // Publish the post
280
+ postEditor.publish()
281
+
282
+ // Wait for save operation to complete
283
+ cy.wait(1500)
284
+
285
+ // Reload page to verify status was persisted
286
+ cy.reload()
287
+
288
+ // Wait for page to load
289
+ cy.get('[data-cy="post-edit-container"]').should('be.visible')
290
+
291
+ // After reload, status should show Published and Publish button should be replaced with Unpublish
292
+ cy.get('[data-cy="post-edit-unpublish"]').should('be.visible')
293
+
294
+ cy.log('✅ Author changed post status to published')
295
+ }
296
+ })
297
+ } else {
298
+ cy.log('⚠️ No draft posts available to publish')
299
+ }
300
+ })
301
+ })
302
+
303
+ it('BLOG_POST_UPDATE_003: should toggle featured flag', () => {
304
+ cy.get('[data-cy^="posts-row-"]').first().invoke('attr', 'data-cy').then(dataCy => {
305
+ const postId = dataCy?.replace('posts-row-', '')
306
+ if (postId) {
307
+ postsList.clickEdit(postId)
308
+
309
+ const postEditor = new PostEditor('edit')
310
+ postEditor.validatePageVisible()
311
+
312
+ // Click on the featured toggle to change its state
313
+ cy.get('[data-cy="post-edit-featured-toggle"]').click({ force: true })
314
+
315
+ // Wait for state to update
316
+ cy.wait(300)
317
+
318
+ // Save changes
319
+ postEditor.saveDraft()
320
+
321
+ // Wait for save
322
+ cy.wait(500)
323
+
324
+ // Toggle again (back to original state)
325
+ cy.get('[data-cy="post-edit-featured-toggle"]').click({ force: true })
326
+ cy.wait(300)
327
+ postEditor.saveDraft()
328
+
329
+ cy.log('✅ Author toggled featured flag successfully')
330
+ }
331
+ })
332
+ })
333
+
334
+ it('BLOG_POST_UPDATE_004: should update URL slug', () => {
335
+ cy.get('[data-cy^="posts-row-"]').first().invoke('attr', 'data-cy').then(dataCy => {
336
+ const postId = dataCy?.replace('posts-row-', '')
337
+ if (postId) {
338
+ postsList.clickEdit(postId)
339
+
340
+ const postEditor = new PostEditor('edit')
341
+ postEditor.validatePageVisible()
342
+
343
+ // Update slug
344
+ const newSlug = `custom-slug-${Date.now()}`
345
+ postEditor.fillSlug(newSlug)
346
+ postEditor.saveDraft()
347
+
348
+ // Validate slug was saved
349
+ postEditor.validateSlug(newSlug)
350
+
351
+ cy.log('✅ Author updated URL slug successfully')
352
+ }
353
+ })
354
+ })
355
+ })
356
+
357
+ // =========================================================================
358
+ // DELETE - Author can delete posts
359
+ // =========================================================================
360
+ describe('DELETE - Author can delete posts', () => {
361
+ it('BLOG_POST_DELETE_001: should delete draft post', () => {
362
+ // Create a post to delete
363
+ const postTitle = `Delete Test ${Date.now()}`
364
+ const postEditor = new PostEditor('create')
365
+
366
+ postsList.clickCreate()
367
+ postEditor.fillTitle(postTitle)
368
+ postEditor.typeContent('Post to be deleted')
369
+ postEditor.saveDraft()
370
+ postEditor.clickBack()
371
+
372
+ // Wait for post to appear
373
+ cy.contains(postTitle).should('be.visible')
374
+
375
+ // Find the post row and get its ID
376
+ cy.contains('[data-cy^="posts-row-"]', postTitle)
377
+ .invoke('attr', 'data-cy')
378
+ .then(dataCy => {
379
+ const postId = dataCy?.replace('posts-row-', '')
380
+ if (postId) {
381
+ // Click delete
382
+ postsList.clickDelete(postId)
383
+
384
+ // Confirm delete
385
+ postsList.confirmDelete()
386
+
387
+ // Validate post is removed
388
+ cy.wait(500)
389
+ cy.contains(postTitle).should('not.exist')
390
+
391
+ cy.log('✅ Author deleted draft post successfully')
392
+ }
393
+ })
394
+ })
395
+
396
+ it('BLOG_POST_DELETE_002: should delete post from edit page', () => {
397
+ // Create a post to delete
398
+ const postTitle = `Delete From Edit ${Date.now()}`
399
+ const postEditor = new PostEditor('create')
400
+
401
+ postsList.clickCreate()
402
+ postEditor.fillTitle(postTitle)
403
+ postEditor.typeContent('Post to be deleted from edit page')
404
+ postEditor.saveDraft()
405
+
406
+ // Now we're on edit page
407
+ const editEditor = new PostEditor('edit')
408
+ editEditor.validatePageVisible()
409
+
410
+ // Click delete button
411
+ editEditor.clickDelete()
412
+
413
+ // Confirm deletion
414
+ editEditor.confirmDelete()
415
+
416
+ // Should redirect to posts list
417
+ cy.url().should('include', '/dashboard/posts')
418
+ cy.contains(postTitle).should('not.exist')
419
+
420
+ cy.log('✅ Author deleted post from edit page successfully')
421
+ })
422
+
423
+ it('BLOG_POST_DELETE_003: should cancel delete operation', () => {
424
+ // Create a post
425
+ const postTitle = `Cancel Delete ${Date.now()}`
426
+ const postEditor = new PostEditor('create')
427
+
428
+ postsList.clickCreate()
429
+ postEditor.fillTitle(postTitle)
430
+ postEditor.typeContent('Post that should not be deleted')
431
+ postEditor.saveDraft()
432
+ postEditor.clickBack()
433
+
434
+ // Wait for post to appear
435
+ cy.contains(postTitle).should('be.visible')
436
+
437
+ // Find the post and click delete
438
+ cy.contains('[data-cy^="posts-row-"]', postTitle)
439
+ .invoke('attr', 'data-cy')
440
+ .then(dataCy => {
441
+ const postId = dataCy?.replace('posts-row-', '')
442
+ if (postId) {
443
+ postsList.clickDelete(postId)
444
+
445
+ // Cancel delete
446
+ postsList.cancelDelete()
447
+
448
+ // Validate post still exists
449
+ cy.contains(postTitle).should('be.visible')
450
+
451
+ cy.log('✅ Author cancelled delete operation successfully')
452
+ }
453
+ })
454
+ })
455
+ })
456
+
457
+ after(() => {
458
+ cy.log('✅ Posts CRUD tests completed')
459
+ })
460
+ })
@@ -0,0 +1,115 @@
1
+ # Posts CRUD - E2E Tests
2
+
3
+ ## Overview
4
+
5
+ Tests for Posts entity CRUD operations in the Blog theme.
6
+
7
+ **Test File:** `test/cypress/e2e/themes/blog/posts/posts-crud.cy.ts`
8
+
9
+ ## Entity Characteristics
10
+
11
+ | Property | Value |
12
+ |----------|-------|
13
+ | **Entity** | Posts |
14
+ | **UI** | Custom (PostsList, PostEditor) |
15
+ | **Team Mode** | Single-user (isolated) |
16
+ | **Status** | Draft/Published workflow |
17
+ | **Features** | Featured toggle, WYSIWYG editor |
18
+
19
+ ## Test Users
20
+
21
+ | Author | Email | Role |
22
+ |--------|-------|------|
23
+ | Marcos Tech | blog_author_marcos@nextspark.dev | Owner |
24
+ | Lucia Lifestyle | blog_author_lucia@nextspark.dev | Owner |
25
+ | Carlos Finance | blog_author_carlos@nextspark.dev | Owner |
26
+
27
+ **Password:** `Test1234`
28
+
29
+ ## Test Coverage
30
+
31
+ ### 1. CREATE (4 tests)
32
+
33
+ | ID | Test Case | Description | Status |
34
+ |----|-----------|-------------|--------|
35
+ | BLOG_POST_CREATE_001 | Create draft post | Create a new post as draft | ✅ Passing |
36
+ | BLOG_POST_CREATE_002 | Create and publish immediately | Create post and publish in one action | ✅ Passing |
37
+ | BLOG_POST_CREATE_003 | Create with all metadata | Create post with slug, excerpt, featured | ✅ Passing |
38
+ | BLOG_POST_CREATE_004 | Validation error empty title | Show error when title is empty | ✅ Passing |
39
+
40
+ ### 2. READ (5 tests)
41
+
42
+ | ID | Test Case | Description | Status |
43
+ |----|-----------|-------------|--------|
44
+ | BLOG_POST_READ_001 | View posts in table mode | Display posts in table layout | ✅ Passing |
45
+ | BLOG_POST_READ_002 | View posts in grid mode | Display posts in grid layout | ✅ Passing |
46
+ | BLOG_POST_READ_003 | Filter by status | Filter posts by published/draft | ✅ Passing |
47
+ | BLOG_POST_READ_004 | Search posts | Search posts by title | ✅ Passing |
48
+ | BLOG_POST_READ_005 | View post details | Navigate to edit page | ✅ Passing |
49
+
50
+ ### 3. UPDATE (4 tests)
51
+
52
+ | ID | Test Case | Description | Status |
53
+ |----|-----------|-------------|--------|
54
+ | BLOG_POST_UPDATE_001 | Edit title and content | Update post title and content | ✅ Passing |
55
+ | BLOG_POST_UPDATE_002 | Change status | Publish draft post | ✅ Passing |
56
+ | BLOG_POST_UPDATE_003 | Toggle featured flag | Mark post as featured | ✅ Passing |
57
+ | BLOG_POST_UPDATE_004 | Update URL slug | Change post slug | ✅ Passing |
58
+
59
+ ### 4. DELETE (3 tests)
60
+
61
+ | ID | Test Case | Description | Status |
62
+ |----|-----------|-------------|--------|
63
+ | BLOG_POST_DELETE_001 | Delete draft post | Delete post from list actions | ✅ Passing |
64
+ | BLOG_POST_DELETE_002 | Delete from edit page | Delete post from editor | ✅ Passing |
65
+ | BLOG_POST_DELETE_003 | Cancel delete | Cancel delete operation | ✅ Passing |
66
+
67
+ ## Summary
68
+
69
+ | Category | Total | Passing | Pending | Failing |
70
+ |----------|-------|---------|---------|---------|
71
+ | CREATE | 4 | 4 | 0 | 0 |
72
+ | READ | 5 | 5 | 0 | 0 |
73
+ | UPDATE | 4 | 4 | 0 | 0 |
74
+ | DELETE | 3 | 3 | 0 | 0 |
75
+ | **Total** | **16** | **16** | **0** | **0** |
76
+
77
+ ## Data-cy Selectors Used
78
+
79
+ ### PostsList
80
+
81
+ ```
82
+ [data-cy="posts-list-container"]
83
+ [data-cy="posts-stat-{status}"]
84
+ [data-cy="posts-search-input"]
85
+ [data-cy="posts-view-{mode}"]
86
+ [data-cy="posts-row-{id}"]
87
+ [data-cy="posts-actions-{id}"]
88
+ [data-cy="posts-create-button"]
89
+ ```
90
+
91
+ ### PostEditor
92
+
93
+ ```
94
+ [data-cy="post-{mode}-container"]
95
+ [data-cy="post-{mode}-title"]
96
+ [data-cy="post-{mode}-content"]
97
+ [data-cy="post-{mode}-save"]
98
+ [data-cy="post-{mode}-publish"]
99
+ ```
100
+
101
+ ## POM Classes
102
+
103
+ - **PostsList:** `test/cypress/src/classes/themes/blog/PostsList.js`
104
+ - **PostEditor:** `test/cypress/src/classes/themes/blog/PostEditor.js`
105
+ - **WysiwygEditor:** `test/cypress/src/classes/themes/blog/WysiwygEditor.js`
106
+
107
+ ## Running Tests
108
+
109
+ ```bash
110
+ npx cypress run --spec "test/cypress/e2e/themes/blog/posts/posts-crud.cy.ts"
111
+ ```
112
+
113
+ ---
114
+
115
+ **Last Updated:** 2025-12-04