@nextsparkjs/theme-blog 0.1.0-beta.19 → 0.1.0-beta.190

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.
Files changed (72) hide show
  1. package/api/authors/[username]/route.ts +5 -2
  2. package/api/authors/docs.md +135 -0
  3. package/api/authors/presets.ts +45 -0
  4. package/api/authors/route.ts +4 -1
  5. package/api/posts/public/docs.md +124 -0
  6. package/api/posts/public/presets.ts +65 -0
  7. package/api/posts/public/route.ts +4 -1
  8. package/components/ExportPostsButton.tsx +2 -1
  9. package/components/ImportPostsDialog.tsx +2 -1
  10. package/components/editor/FeaturedImageUpload.tsx +2 -1
  11. package/components/editor/WysiwygEditor.tsx +56 -7
  12. package/components/public/AuthorBio.tsx +3 -2
  13. package/components/public/AuthorCard.tsx +3 -2
  14. package/components/public/BlogFooter.tsx +2 -1
  15. package/components/public/PostCard.tsx +8 -7
  16. package/components/public/RelatedPosts.tsx +2 -1
  17. package/config/app.config.ts +8 -15
  18. package/config/billing.config.ts +4 -7
  19. package/config/dashboard.config.ts +13 -0
  20. package/config/permissions.config.ts +11 -0
  21. package/entities/categories/api/docs.md +119 -0
  22. package/entities/categories/api/presets.ts +67 -0
  23. package/entities/posts/api/docs.md +174 -0
  24. package/entities/posts/api/presets.ts +137 -0
  25. package/lib/sanitize-post-html.ts +52 -0
  26. package/lib/selectors.ts +2 -3
  27. package/migrations/090_billing_sample_data.sql +99 -0
  28. package/migrations/999_theme_sample_data.sql +13 -13
  29. package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
  30. package/package.json +13 -5
  31. package/styles/globals.css +58 -0
  32. package/templates/(public)/author/[username]/page.tsx +6 -5
  33. package/templates/(public)/authors/page.tsx +3 -2
  34. package/templates/(public)/page.tsx +2 -1
  35. package/templates/(public)/posts/[slug]/page.tsx +7 -5
  36. package/templates/dashboard/(main)/page.tsx +2 -1
  37. package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +4 -3
  38. package/templates/dashboard/(main)/posts/create/page.tsx +2 -1
  39. package/templates/dashboard/(main)/posts/page.tsx +7 -6
  40. package/tests/cypress/e2e/README.md +170 -0
  41. package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
  42. package/tests/cypress/e2e/categories/categories-crud.md +73 -0
  43. package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
  44. package/tests/cypress/e2e/posts/posts-crud.md +115 -0
  45. package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
  46. package/tests/cypress/e2e/posts/posts-editor.md +139 -0
  47. package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
  48. package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
  49. package/tests/cypress/fixtures/blocks.json +9 -0
  50. package/tests/cypress/fixtures/entities.json +51 -0
  51. package/tests/cypress/src/FeaturedImageUpload.js +131 -0
  52. package/tests/cypress/src/PostEditor.js +386 -0
  53. package/tests/cypress/src/PostsList.js +350 -0
  54. package/tests/cypress/src/WysiwygEditor.js +373 -0
  55. package/tests/cypress/src/components/EntityForm.ts +378 -0
  56. package/tests/cypress/src/components/EntityList.ts +378 -0
  57. package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
  58. package/tests/cypress/src/components/PostsPOM.ts +362 -0
  59. package/tests/cypress/src/components/index.ts +18 -0
  60. package/tests/cypress/src/index.js +33 -0
  61. package/tests/cypress/src/selectors.ts +49 -0
  62. package/tests/cypress/src/session-helpers.ts +151 -0
  63. package/tests/cypress/support/e2e.ts +90 -0
  64. package/tests/cypress.config.ts +154 -0
  65. package/tests/jest/__mocks__/jose.js +22 -0
  66. package/tests/jest/__mocks__/next-server.js +56 -0
  67. package/tests/jest/components/WysiwygEditor.test.tsx +117 -0
  68. package/tests/jest/jest.config.cjs +148 -0
  69. package/tests/jest/lib/sanitize-post-html.test.ts +47 -0
  70. package/tests/jest/setup.ts +170 -0
  71. package/tests/jest/tsconfig.jest.json +6 -0
  72. package/tests/tsconfig.json +15 -0
@@ -29,6 +29,7 @@ import {
29
29
  Eye,
30
30
  MoreHorizontal
31
31
  } from 'lucide-react'
32
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
32
33
 
33
34
  interface PostStats {
34
35
  total: number
@@ -65,7 +66,7 @@ async function fetchPosts(): Promise<{ stats: PostStats; recent: RecentPost[] }>
65
66
  const headers = buildHeaders()
66
67
 
67
68
  // Fetch all posts to get stats
68
- const response = await fetch('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc', {
69
+ const response = await fetch(withBasePath('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc'), {
69
70
  credentials: 'include',
70
71
  headers,
71
72
  })
@@ -39,6 +39,7 @@ import {
39
39
  import { Switch } from '@nextsparkjs/core/components/ui/switch'
40
40
  import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
41
41
  import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
42
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
42
43
 
43
44
  interface PostData {
44
45
  id: string
@@ -102,7 +103,7 @@ export default function EditPostPage({ params }: PageProps) {
102
103
  async function fetchPost() {
103
104
  try {
104
105
  const headers = buildHeaders()
105
- const response = await fetch(`/api/v1/posts/${id}`, {
106
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
106
107
  credentials: 'include',
107
108
  headers,
108
109
  })
@@ -178,7 +179,7 @@ export default function EditPostPage({ params }: PageProps) {
178
179
 
179
180
  try {
180
181
  const headers = buildHeaders()
181
- const response = await fetch(`/api/v1/posts/${id}`, {
182
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
182
183
  method: 'PATCH',
183
184
  headers,
184
185
  credentials: 'include',
@@ -232,7 +233,7 @@ export default function EditPostPage({ params }: PageProps) {
232
233
 
233
234
  try {
234
235
  const headers = buildHeaders()
235
- const response = await fetch(`/api/v1/posts/${id}`, {
236
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
236
237
  method: 'DELETE',
237
238
  headers,
238
239
  credentials: 'include',
@@ -29,6 +29,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/comp
29
29
  import { Switch } from '@nextsparkjs/core/components/ui/switch'
30
30
  import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
31
31
  import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
32
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
32
33
 
33
34
  interface PostData {
34
35
  title: string
@@ -122,7 +123,7 @@ export default function CreatePostPage() {
122
123
 
123
124
  try {
124
125
  const headers = buildHeaders()
125
- const response = await fetch('/api/v1/posts', {
126
+ const response = await fetch(withBasePath('/api/v1/posts'), {
126
127
  method: 'POST',
127
128
  headers,
128
129
  credentials: 'include',
@@ -63,6 +63,7 @@ import {
63
63
  Send,
64
64
  ImageIcon,
65
65
  } from 'lucide-react'
66
+ import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
66
67
 
67
68
  interface Post {
68
69
  id: string
@@ -174,7 +175,7 @@ export default function PostsListPage() {
174
175
  sortOrder,
175
176
  })
176
177
 
177
- const response = await fetch(`/api/v1/posts?${params}`, {
178
+ const response = await fetch(withBasePath(`/api/v1/posts?${params}`), {
178
179
  credentials: 'include',
179
180
  headers,
180
181
  })
@@ -246,7 +247,7 @@ export default function PostsListPage() {
246
247
  setActionLoading(post.id)
247
248
  try {
248
249
  const headers = buildHeaders()
249
- const response = await fetch(`/api/v1/posts/${post.id}`, {
250
+ const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
250
251
  method: 'PATCH',
251
252
  headers,
252
253
  credentials: 'include',
@@ -269,7 +270,7 @@ export default function PostsListPage() {
269
270
  setActionLoading(post.id)
270
271
  try {
271
272
  const headers = buildHeaders()
272
- const response = await fetch(`/api/v1/posts/${post.id}`, {
273
+ const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
273
274
  method: 'PATCH',
274
275
  headers,
275
276
  credentials: 'include',
@@ -291,7 +292,7 @@ export default function PostsListPage() {
291
292
  setActionLoading(postToDelete.id)
292
293
  try {
293
294
  const headers = buildHeaders()
294
- const response = await fetch(`/api/v1/posts/${postToDelete.id}`, {
295
+ const response = await fetch(withBasePath(`/api/v1/posts/${postToDelete.id}`), {
295
296
  method: 'DELETE',
296
297
  headers,
297
298
  credentials: 'include',
@@ -555,7 +556,7 @@ export default function PostsListPage() {
555
556
  <div className="hidden sm:block flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-muted">
556
557
  {post.featuredImage ? (
557
558
  <Image
558
- src={post.featuredImage}
559
+ src={withBasePathIfInApp(post.featuredImage)}
559
560
  alt=""
560
561
  width={64}
561
562
  height={48}
@@ -690,7 +691,7 @@ export default function PostsListPage() {
690
691
  <div className="aspect-video relative bg-muted">
691
692
  {post.featuredImage ? (
692
693
  <Image
693
- src={post.featuredImage}
694
+ src={withBasePathIfInApp(post.featuredImage)}
694
695
  alt=""
695
696
  fill
696
697
  className="object-cover"
@@ -0,0 +1,170 @@
1
+ # Blog Theme - E2E Testing Suite
2
+
3
+ ## Overview
4
+
5
+ Complete E2E testing infrastructure for the Blog theme. The blog theme operates in **single-user mode** where each user owns their own isolated blog without team collaboration.
6
+
7
+ ## Test Status: ✅ All Tests Passing (52/52)
8
+
9
+ | Test File | Tests | Status |
10
+ |-----------|-------|--------|
11
+ | `categories/categories-crud.cy.ts` | 9 | ✅ Passing |
12
+ | `posts/posts-crud.cy.ts` | 16 | ✅ Passing |
13
+ | `posts/posts-editor.cy.ts` | 19 | ✅ Passing |
14
+ | `posts/posts-status-workflow.cy.ts` | 8 | ✅ Passing |
15
+ | **Total** | **52** | **100%** |
16
+
17
+ ## Test Structure
18
+
19
+ ```
20
+ test/cypress/e2e/themes/blog/
21
+ ├── posts/
22
+ │ ├── posts-crud.cy.ts # Full CRUD operations (16 tests)
23
+ │ ├── posts-status-workflow.cy.ts # Publish/Unpublish workflow (8 tests)
24
+ │ └── posts-editor.cy.ts # WYSIWYG editor tests (19 tests)
25
+ ├── categories/
26
+ │ └── categories-crud.cy.ts # Standard CRUD (9 tests)
27
+ └── README.md # This file
28
+ ```
29
+
30
+ ## Test Users
31
+
32
+ All blog theme tests use isolated blog author accounts:
33
+
34
+ | Author | Email | Description |
35
+ |--------|-------|-------------|
36
+ | **Marcos** (Primary) | `blog_author_marcos@nextspark.dev` | Main test author |
37
+ | Lucia | `blog_author_lucia@nextspark.dev` | Lifestyle blog author |
38
+ | Carlos | `blog_author_carlos@nextspark.dev` | Finance blog author |
39
+
40
+ **Password for all users:** `Test1234`
41
+
42
+ ## POM Classes
43
+
44
+ Blog theme POM classes are located at `test/cypress/src/classes/themes/blog/`:
45
+
46
+ | Class | Description |
47
+ |-------|-------------|
48
+ | `PostsList.js` | Posts list page interactions (filters, views, actions) |
49
+ | `PostEditor.js` | Create/Edit post pages (supports both modes) |
50
+ | `WysiwygEditor.js` | Rich text editor component |
51
+ | `FeaturedImageUpload.js` | Image upload component |
52
+ | `session-helpers.ts` | Blog-specific login helpers |
53
+
54
+ ## Session Helpers
55
+
56
+ Blog theme has its own session helpers for isolated login:
57
+
58
+ ```typescript
59
+ import { loginAsBlogAuthor, BLOG_USERS } from '../../../src/session-helpers'
60
+
61
+ // Login as default author (Marcos)
62
+ loginAsBlogAuthor('MARCOS')
63
+
64
+ // Login as specific author
65
+ loginAsBlogAuthor('LUCIA')
66
+ loginAsBlogAuthor('CARLOS')
67
+ ```
68
+
69
+ ## Running Tests
70
+
71
+ ```bash
72
+ # Run all blog theme tests
73
+ npx cypress run --spec "test/cypress/e2e/themes/blog/**/*.cy.ts"
74
+
75
+ # Run only posts tests
76
+ npx cypress run --spec "test/cypress/e2e/themes/blog/posts/**/*.cy.ts"
77
+
78
+ # Run only categories tests
79
+ npx cypress run --spec "test/cypress/e2e/themes/blog/categories/**/*.cy.ts"
80
+
81
+ # Run with blog theme active
82
+ NEXT_PUBLIC_ACTIVE_THEME=blog npx cypress run --spec "test/cypress/e2e/themes/blog/**/*.cy.ts"
83
+ ```
84
+
85
+ ## Data-cy Selectors
86
+
87
+ ### Posts List Page
88
+
89
+ ```
90
+ [data-cy="posts-list-container"]
91
+ [data-cy="posts-stat-all|published|draft|scheduled"]
92
+ [data-cy="posts-search-input"]
93
+ [data-cy="posts-sort-select"]
94
+ [data-cy="posts-view-table|grid"]
95
+ [data-cy="posts-row-{id}"]
96
+ [data-cy="posts-title-{id}"]
97
+ [data-cy="posts-status-{id}"]
98
+ [data-cy="posts-actions-{id}"]
99
+ [data-cy="posts-edit-{id}"]
100
+ [data-cy="posts-publish-{id}"]
101
+ [data-cy="posts-delete-{id}"]
102
+ [data-cy="posts-create-button"]
103
+ [data-cy="posts-delete-dialog"]
104
+ [data-cy="posts-delete-confirm|cancel"]
105
+ ```
106
+
107
+ ### Post Editor (Create/Edit)
108
+
109
+ ```
110
+ [data-cy="post-{mode}-container"] # mode = create|edit
111
+ [data-cy="post-{mode}-header"]
112
+ [data-cy="post-{mode}-back"]
113
+ [data-cy="post-{mode}-status"]
114
+ [data-cy="post-{mode}-autosaved"]
115
+ [data-cy="post-{mode}-save"]
116
+ [data-cy="post-{mode}-publish"]
117
+ [data-cy="post-{mode}-unpublish"] # edit only
118
+ [data-cy="post-{mode}-title"]
119
+ [data-cy="post-{mode}-content"]
120
+ [data-cy="post-{mode}-settings"]
121
+ [data-cy="post-{mode}-status-select"]
122
+ [data-cy="post-{mode}-slug"]
123
+ [data-cy="post-{mode}-excerpt"]
124
+ [data-cy="post-{mode}-featured-image"]
125
+ [data-cy="post-{mode}-featured-toggle"]
126
+ [data-cy="post-{mode}-delete"] # edit only
127
+ [data-cy="post-{mode}-delete-dialog"]
128
+ ```
129
+
130
+ ### WYSIWYG Editor
131
+
132
+ ```
133
+ [data-cy="wysiwyg-container"]
134
+ [data-cy="wysiwyg-toolbar"]
135
+ [data-cy="wysiwyg-content"]
136
+ [data-cy="wysiwyg-preview"]
137
+ [data-cy="wysiwyg-preview-toggle"]
138
+ [data-cy="wysiwyg-{command}"] # undo, redo, bold, italic, etc.
139
+ [data-cy="wysiwyg-formatBlock-{tag}"] # h1, h2, h3, blockquote, pre
140
+ [data-cy="wysiwyg-wordcount"]
141
+ ```
142
+
143
+ ### Featured Image Upload
144
+
145
+ ```
146
+ [data-cy="featured-image-container"]
147
+ [data-cy="featured-image-dropzone"]
148
+ [data-cy="featured-image-input"]
149
+ [data-cy="featured-image-preview"]
150
+ [data-cy="featured-image-remove"]
151
+ [data-cy="featured-image-loading"]
152
+ [data-cy="featured-image-error"]
153
+ ```
154
+
155
+ ## Dependencies
156
+
157
+ - Generic POM classes from `test/cypress/src/classes/components/entities/`
158
+ - DevKeyring for authentication: `test/cypress/src/classes/components/auth/DevKeyring.js`
159
+ - Core session helpers (NOT modified): `test/cypress/src/helpers/session-helpers.ts`
160
+
161
+ ## Notes
162
+
163
+ - Blog theme uses **custom UI** for posts (PostsList, PostEditor) unlike generic EntityList/EntityForm
164
+ - Categories use the **generic EntityList/EntityForm** pattern
165
+ - All tests use **cy.session()** for cached authentication (3-5x faster)
166
+ - Tests are designed to be **self-contained** and create their own test data
167
+
168
+ ---
169
+
170
+ **Last Updated:** 2025-12-04
@@ -0,0 +1,322 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Categories CRUD Tests - Blog Theme
5
+ *
6
+ * Tests for Categories entity CRUD operations.
7
+ * Uses generic EntityList and EntityForm POM classes.
8
+ *
9
+ * Theme Mode: single-user (isolated blogs, no team collaboration)
10
+ */
11
+
12
+ import { EntityList } from '../../src/components/EntityList'
13
+ import { EntityForm } from '../../src/components/EntityForm'
14
+ import { loginAsBlogAuthor } from '../../src/session-helpers'
15
+
16
+ describe('Categories CRUD - Blog Author (Full Access)', () => {
17
+ const categoryList = new EntityList('categories')
18
+ const categoryForm = new EntityForm('categories')
19
+
20
+ beforeEach(() => {
21
+ loginAsBlogAuthor('MARCOS')
22
+ cy.visit('/dashboard/categories')
23
+ categoryList.validateListVisible()
24
+ })
25
+
26
+ // =========================================================================
27
+ // CREATE - Author can create categories
28
+ // =========================================================================
29
+ describe('CREATE - Author can create categories', () => {
30
+ it('BLOG_CAT_CREATE_001: should create new category successfully', () => {
31
+ const timestamp = Date.now()
32
+ const categoryName = `Category ${timestamp}`
33
+ const categorySlug = `category-${timestamp}`
34
+
35
+ // Click create button
36
+ categoryList.clickAdd()
37
+
38
+ // Validate form is visible
39
+ categoryForm.validateFormVisible()
40
+
41
+ // Fill required fields (name and slug are both required)
42
+ categoryForm.fillField('name', categoryName)
43
+ categoryForm.fillField('slug', categorySlug)
44
+
45
+ // Submit form
46
+ categoryForm.submit()
47
+
48
+ // Validate redirect to list
49
+ cy.url().should('include', '/dashboard/categories')
50
+
51
+ // Validate category appears in list
52
+ cy.contains(categoryName).should('be.visible')
53
+
54
+ cy.log('✅ Author created category successfully')
55
+ })
56
+
57
+ it('BLOG_CAT_CREATE_002: should create category with description', () => {
58
+ const timestamp = Date.now()
59
+ const categoryName = `Full Category ${timestamp}`
60
+ const categorySlug = `full-category-${timestamp}`
61
+ const categoryDescription = 'This is a category with a description'
62
+
63
+ // Click create button
64
+ categoryList.clickAdd()
65
+
66
+ // Fill all fields (name and slug are required)
67
+ categoryForm.fillField('name', categoryName)
68
+ categoryForm.fillField('slug', categorySlug)
69
+ categoryForm.fillField('description', categoryDescription)
70
+
71
+ // Submit form
72
+ categoryForm.submit()
73
+
74
+ // Validate category appears in list
75
+ cy.url().should('include', '/dashboard/categories')
76
+ cy.contains(categoryName).should('be.visible')
77
+
78
+ cy.log('✅ Author created category with description successfully')
79
+ })
80
+
81
+ it('BLOG_CAT_CREATE_003: should show validation error for empty name', () => {
82
+ // Click create button
83
+ categoryList.clickAdd()
84
+
85
+ // Try to submit without filling name
86
+ categoryForm.submit()
87
+
88
+ // Validate error is shown (form should have validation)
89
+ cy.get('body').then($body => {
90
+ // Check for validation message or form stays on same page
91
+ const isOnFormPage = $body.find('[data-cy="categories-form"]').length > 0 ||
92
+ $body.find('[data-cy="entity-form"]').length > 0
93
+
94
+ if (isOnFormPage) {
95
+ cy.log('✅ Form validation prevented submission')
96
+ } else {
97
+ // Check for error message
98
+ cy.get('[role="alert"], .text-destructive, [data-cy*="error"]')
99
+ .should('be.visible')
100
+ cy.log('✅ Validation error shown for empty name')
101
+ }
102
+ })
103
+ })
104
+ })
105
+
106
+ // =========================================================================
107
+ // READ - Author can view categories
108
+ // =========================================================================
109
+ describe('READ - Author can view categories', () => {
110
+ it('BLOG_CAT_READ_001: should view categories list', () => {
111
+ // Validate list is visible
112
+ categoryList.validateListVisible()
113
+
114
+ cy.log('✅ Author can view categories list')
115
+ })
116
+
117
+ it('BLOG_CAT_READ_002: should search categories', () => {
118
+ // Create a category with unique name first
119
+ const timestamp = Date.now()
120
+ const uniqueName = `Searchable Cat ${timestamp}`
121
+ const uniqueSlug = `searchable-cat-${timestamp}`
122
+
123
+ categoryList.clickAdd()
124
+ categoryForm.fillField('name', uniqueName)
125
+ categoryForm.fillField('slug', uniqueSlug)
126
+ categoryForm.submit()
127
+
128
+ // Go back to list
129
+ cy.visit('/dashboard/categories')
130
+ categoryList.validateListVisible()
131
+
132
+ // Search for the category
133
+ categoryList.search(uniqueName.substring(0, 10))
134
+ cy.wait(500) // Wait for search
135
+
136
+ // Validate search results
137
+ cy.contains(uniqueName).should('be.visible')
138
+
139
+ // Clear search
140
+ categoryList.clearSearch()
141
+
142
+ cy.log('✅ Author can search categories')
143
+ })
144
+ })
145
+
146
+ // =========================================================================
147
+ // UPDATE - Author can update categories
148
+ // =========================================================================
149
+ describe('UPDATE - Author can update categories', () => {
150
+ beforeEach(() => {
151
+ // Create a test category for update tests
152
+ const timestamp = Date.now()
153
+ const testName = `Update Cat ${timestamp}`
154
+ const testSlug = `update-cat-${timestamp}`
155
+
156
+ categoryList.clickAdd()
157
+ categoryForm.fillField('name', testName)
158
+ categoryForm.fillField('slug', testSlug)
159
+ categoryForm.submit()
160
+
161
+ cy.visit('/dashboard/categories')
162
+ categoryList.validateListVisible()
163
+ })
164
+
165
+ it('BLOG_CAT_UPDATE_001: should edit category name', () => {
166
+ // Find and edit the first category
167
+ cy.get('body').then($body => {
168
+ const editSelector = '[data-cy*="edit"]'
169
+
170
+ if ($body.find(editSelector).length > 0) {
171
+ cy.get(editSelector).first().click()
172
+
173
+ // Validate form is visible
174
+ categoryForm.validateFormVisible()
175
+
176
+ // Update name
177
+ const updatedName = `Updated Category ${Date.now()}`
178
+ categoryForm.fillField('name', updatedName)
179
+
180
+ // Submit form
181
+ categoryForm.submit()
182
+
183
+ // Validate update
184
+ cy.url().should('include', '/dashboard/categories')
185
+ cy.contains(updatedName).should('be.visible')
186
+
187
+ cy.log('✅ Author updated category name successfully')
188
+ } else {
189
+ cy.log('⚠️ No categories available to edit')
190
+ }
191
+ })
192
+ })
193
+
194
+ it('BLOG_CAT_UPDATE_002: should update category description', () => {
195
+ cy.get('body').then($body => {
196
+ const editSelector = '[data-cy*="edit"]'
197
+
198
+ if ($body.find(editSelector).length > 0) {
199
+ cy.get(editSelector).first().click()
200
+
201
+ categoryForm.validateFormVisible()
202
+
203
+ // Update description
204
+ const updatedDescription = `Updated description ${Date.now()}`
205
+ categoryForm.fillField('description', updatedDescription)
206
+
207
+ // Submit form
208
+ categoryForm.submit()
209
+
210
+ cy.url().should('include', '/dashboard/categories')
211
+
212
+ cy.log('✅ Author updated category description successfully')
213
+ } else {
214
+ cy.log('⚠️ No categories available to edit')
215
+ }
216
+ })
217
+ })
218
+ })
219
+
220
+ // =========================================================================
221
+ // DELETE - Author can delete categories
222
+ // =========================================================================
223
+ describe('DELETE - Author can delete categories', () => {
224
+ it('BLOG_CAT_DELETE_001: should delete category successfully', () => {
225
+ // Create a category to delete
226
+ const timestamp = Date.now()
227
+ const categoryName = `Delete Cat ${timestamp}`
228
+ const categorySlug = `delete-cat-${timestamp}`
229
+
230
+ categoryList.clickAdd()
231
+ categoryForm.fillField('name', categoryName)
232
+ categoryForm.fillField('slug', categorySlug)
233
+ categoryForm.submit()
234
+
235
+ // Wait for category to appear in list
236
+ cy.visit('/dashboard/categories')
237
+ cy.contains(categoryName).should('be.visible')
238
+
239
+ // Find and delete
240
+ cy.get('body').then($body => {
241
+ const deleteSelector = '[data-cy*="delete"]'
242
+
243
+ if ($body.find(deleteSelector).length > 0) {
244
+ cy.get(deleteSelector).first().click()
245
+
246
+ // Confirm deletion if modal appears
247
+ cy.get('body').then($body2 => {
248
+ if ($body2.find('[data-cy="confirm-delete"]').length > 0) {
249
+ cy.get('[data-cy="confirm-delete"]').click()
250
+ } else if ($body2.find('[data-cy*="delete-confirm"]').length > 0) {
251
+ cy.get('[data-cy*="delete-confirm"]').click()
252
+ } else if ($body2.find('[role="dialog"] button[data-cy="confirm-delete"]').length > 0) {
253
+ // Click the delete button in the modal (not the trigger)
254
+ cy.get('[role="dialog"] button[data-cy="confirm-delete"]').click()
255
+ } else if ($body2.find('[role="dialog"]').length > 0) {
256
+ // Fallback: click any button that looks like confirm delete in the dialog
257
+ cy.get('[role="dialog"] button').last().click()
258
+ }
259
+ })
260
+
261
+ // Validate deletion
262
+ cy.wait(500)
263
+ cy.contains(categoryName).should('not.exist')
264
+
265
+ cy.log('✅ Author deleted category successfully')
266
+ } else {
267
+ cy.log('⚠️ Delete button not found')
268
+ }
269
+ })
270
+ })
271
+
272
+ it('BLOG_CAT_DELETE_002: should cancel delete operation', () => {
273
+ // Create a category
274
+ const timestamp = Date.now()
275
+ const categoryName = `Cancel Delete Cat ${timestamp}`
276
+ const categorySlug = `cancel-delete-cat-${timestamp}`
277
+
278
+ categoryList.clickAdd()
279
+ categoryForm.fillField('name', categoryName)
280
+ categoryForm.fillField('slug', categorySlug)
281
+ categoryForm.submit()
282
+
283
+ // Wait for category to appear
284
+ cy.visit('/dashboard/categories')
285
+ cy.contains(categoryName).should('be.visible')
286
+
287
+ // Find and try to delete, then cancel
288
+ cy.get('body').then($body => {
289
+ const deleteSelector = '[data-cy*="delete"]'
290
+
291
+ if ($body.find(deleteSelector).length > 0) {
292
+ cy.get(deleteSelector).first().click()
293
+
294
+ // Cancel deletion if modal appears
295
+ cy.get('body').then($body2 => {
296
+ if ($body2.find('[data-cy="cancel-delete"]').length > 0) {
297
+ cy.get('[data-cy="cancel-delete"]').click()
298
+ } else if ($body2.find('[data-cy*="delete-cancel"]').length > 0) {
299
+ cy.get('[data-cy*="delete-cancel"]').click()
300
+ } else if ($body2.find('[role="dialog"] button[data-cy="cancel-delete"]').length > 0) {
301
+ cy.get('[role="dialog"] button[data-cy="cancel-delete"]').click()
302
+ } else if ($body2.find('[role="dialog"]').length > 0) {
303
+ // Fallback: click the first button (usually cancel) in the dialog
304
+ cy.get('[role="dialog"] button').first().click()
305
+ }
306
+ })
307
+
308
+ // Validate category still exists
309
+ cy.contains(categoryName).should('be.visible')
310
+
311
+ cy.log('✅ Author cancelled delete operation successfully')
312
+ } else {
313
+ cy.log('⚠️ Delete button not found')
314
+ }
315
+ })
316
+ })
317
+ })
318
+
319
+ after(() => {
320
+ cy.log('✅ Categories CRUD tests completed')
321
+ })
322
+ })