@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.
- package/api/authors/[username]/route.ts +5 -2
- package/api/authors/docs.md +135 -0
- package/api/authors/presets.ts +45 -0
- package/api/authors/route.ts +4 -1
- package/api/posts/public/docs.md +124 -0
- package/api/posts/public/presets.ts +65 -0
- package/api/posts/public/route.ts +4 -1
- package/components/ExportPostsButton.tsx +2 -1
- package/components/ImportPostsDialog.tsx +2 -1
- package/components/editor/FeaturedImageUpload.tsx +2 -1
- package/components/editor/WysiwygEditor.tsx +56 -7
- package/components/public/AuthorBio.tsx +3 -2
- package/components/public/AuthorCard.tsx +3 -2
- package/components/public/BlogFooter.tsx +2 -1
- package/components/public/PostCard.tsx +8 -7
- package/components/public/RelatedPosts.tsx +2 -1
- package/config/app.config.ts +8 -15
- package/config/billing.config.ts +4 -7
- package/config/dashboard.config.ts +13 -0
- package/config/permissions.config.ts +11 -0
- package/entities/categories/api/docs.md +119 -0
- package/entities/categories/api/presets.ts +67 -0
- package/entities/posts/api/docs.md +174 -0
- package/entities/posts/api/presets.ts +137 -0
- package/lib/sanitize-post-html.ts +52 -0
- package/lib/selectors.ts +2 -3
- package/migrations/090_billing_sample_data.sql +99 -0
- package/migrations/999_theme_sample_data.sql +13 -13
- package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
- package/package.json +13 -5
- package/styles/globals.css +58 -0
- package/templates/(public)/author/[username]/page.tsx +6 -5
- package/templates/(public)/authors/page.tsx +3 -2
- package/templates/(public)/page.tsx +2 -1
- package/templates/(public)/posts/[slug]/page.tsx +7 -5
- package/templates/dashboard/(main)/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +4 -3
- package/templates/dashboard/(main)/posts/create/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/page.tsx +7 -6
- 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 +51 -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 +378 -0
- package/tests/cypress/src/components/EntityList.ts +378 -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 +151 -0
- package/tests/cypress/support/e2e.ts +90 -0
- package/tests/cypress.config.ts +154 -0
- package/tests/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/components/WysiwygEditor.test.tsx +117 -0
- package/tests/jest/jest.config.cjs +148 -0
- package/tests/jest/lib/sanitize-post-html.test.ts +47 -0
- package/tests/jest/setup.ts +170 -0
- package/tests/jest/tsconfig.jest.json +6 -0
- 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
|
+
})
|