@nextsparkjs/theme-blog 0.1.0-beta.17 → 0.1.0-beta.171

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 (49) 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/config/app.config.ts +4 -5
  9. package/config/billing.config.ts +4 -7
  10. package/config/dashboard.config.ts +13 -0
  11. package/config/permissions.config.ts +11 -0
  12. package/entities/categories/api/docs.md +119 -0
  13. package/entities/categories/api/presets.ts +67 -0
  14. package/entities/posts/api/docs.md +174 -0
  15. package/entities/posts/api/presets.ts +137 -0
  16. package/lib/selectors.ts +2 -3
  17. package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
  18. package/package.json +4 -3
  19. package/styles/globals.css +45 -0
  20. package/tests/cypress/e2e/README.md +170 -0
  21. package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
  22. package/tests/cypress/e2e/categories/categories-crud.md +73 -0
  23. package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
  24. package/tests/cypress/e2e/posts/posts-crud.md +115 -0
  25. package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
  26. package/tests/cypress/e2e/posts/posts-editor.md +139 -0
  27. package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
  28. package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +42 -0
  31. package/tests/cypress/src/FeaturedImageUpload.js +131 -0
  32. package/tests/cypress/src/PostEditor.js +386 -0
  33. package/tests/cypress/src/PostsList.js +350 -0
  34. package/tests/cypress/src/WysiwygEditor.js +373 -0
  35. package/tests/cypress/src/components/EntityForm.ts +378 -0
  36. package/tests/cypress/src/components/EntityList.ts +378 -0
  37. package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
  38. package/tests/cypress/src/components/PostsPOM.ts +362 -0
  39. package/tests/cypress/src/components/index.ts +18 -0
  40. package/tests/cypress/src/index.js +33 -0
  41. package/tests/cypress/src/selectors.ts +49 -0
  42. package/tests/cypress/src/session-helpers.ts +151 -0
  43. package/tests/cypress/support/e2e.ts +90 -0
  44. package/tests/cypress.config.ts +154 -0
  45. package/tests/jest/__mocks__/jose.js +22 -0
  46. package/tests/jest/__mocks__/next-server.js +56 -0
  47. package/tests/jest/jest.config.cjs +131 -0
  48. package/tests/jest/setup.ts +170 -0
  49. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Posts POM - Blog Theme Posts List Page
3
+ *
4
+ * Entity-specific POM for posts listing with selector mapping.
5
+ * Maps current blog theme selectors to the entity testing convention.
6
+ *
7
+ * Current selectors (blog theme):
8
+ * posts-list-container, posts-create-button, posts-table-container
9
+ *
10
+ * Convention selectors (target):
11
+ * posts-page, posts-create-btn, posts-table
12
+ *
13
+ * This POM uses the CURRENT selectors from the blog theme components
14
+ * while providing the same API as the generic EntityList.
15
+ *
16
+ * Test cases: BLOG_POST_READ_001-005, BLOG_POST_DELETE_001-003
17
+ */
18
+
19
+ import { EntityList, EntityConfig } from './EntityList'
20
+ import entitiesConfig from '../../fixtures/entities.json'
21
+
22
+ export class PostsPOM extends EntityList {
23
+ // Static entity config from entities.json
24
+ static entityConfig = entitiesConfig.entities.posts as EntityConfig
25
+ static slug = PostsPOM.entityConfig.slug
26
+ static fields = PostsPOM.entityConfig.fields
27
+
28
+ constructor() {
29
+ super('posts')
30
+ }
31
+
32
+ // ============================================
33
+ // STATIC FACTORY METHOD
34
+ // ============================================
35
+
36
+ static for(_entityKey: string): PostsPOM {
37
+ return new PostsPOM()
38
+ }
39
+
40
+ // ============================================
41
+ // SELECTOR MAPPING (Current -> Convention)
42
+ // Blog theme uses slightly different selectors than convention
43
+ // ============================================
44
+
45
+ /**
46
+ * Override selectors to use current blog theme selectors
47
+ * This maintains backwards compatibility with existing components
48
+ */
49
+ get selectors() {
50
+ const slug = this.slug
51
+ return {
52
+ // Page elements - MAPPED to current blog selectors
53
+ page: '[data-cy="posts-list-container"]', // Convention: posts-page
54
+ pageTitle: '[data-cy="posts-list-title"]',
55
+
56
+ // Table - MAPPED
57
+ table: '[data-cy="posts-table"]',
58
+ tableContainer: '[data-cy="posts-table-container"]', // Convention: posts-table
59
+
60
+ // Create button - MAPPED
61
+ createButton: '[data-cy="posts-create-button"]', // Convention: posts-create-btn
62
+
63
+ // Search
64
+ search: '[data-cy="posts-search"]',
65
+ searchInput: '[data-cy="posts-search-input"]',
66
+
67
+ // Sort
68
+ sortSelect: '[data-cy="posts-sort-select"]',
69
+
70
+ // Filters (inherited from EntityList)
71
+ filter: (fieldName: string) => `[data-cy="${slug}-filter-${fieldName}"]`,
72
+ filterTrigger: (fieldName: string) => `[data-cy="${slug}-filter-${fieldName}-trigger"]`,
73
+ filterOption: (fieldName: string, value: string) => `[data-cy="${slug}-filter-${fieldName}-option-${value}"]`,
74
+
75
+ // Rows
76
+ row: (id: string) => `[data-cy="${slug}-row-${id}"]`,
77
+ rowGeneric: `[data-cy^="${slug}-row-"]`,
78
+
79
+ // Cards (for grid view)
80
+ card: (id: string) => `[data-cy="${slug}-card-${id}"]`,
81
+ cardGeneric: `[data-cy^="${slug}-card-"]`,
82
+ cardTitle: (id: string) => `[data-cy="${slug}-card-title-${id}"]`,
83
+
84
+ // Row-specific elements
85
+ rowTitle: (id: string) => `[data-cy="${slug}-title-${id}"]`,
86
+ rowStatus: (id: string) => `[data-cy="${slug}-status-${id}"]`,
87
+
88
+ // Actions - MAPPED (current uses posts-actions-{id}, convention: posts-actions-trigger-{id})
89
+ actionEdit: (id: string) => `[data-cy="${slug}-edit-${id}"]`,
90
+ actionDelete: (id: string) => `[data-cy="${slug}-delete-${id}"]`,
91
+ actionView: (id: string) => `[data-cy="${slug}-view-live-${id}"]`,
92
+ actionPublish: (id: string) => `[data-cy="${slug}-publish-${id}"]`,
93
+ actionUnpublish: (id: string) => `[data-cy="${slug}-unpublish-${id}"]`,
94
+ actionsDropdown: (id: string) => `[data-cy="${slug}-actions-${id}"]`,
95
+ actionsTrigger: (id: string) => `[data-cy="${slug}-actions-${id}"]`, // Same as dropdown in blog
96
+
97
+ // Pagination
98
+ pagination: `[data-cy="${slug}-pagination"]`,
99
+ paginationPrev: `[data-cy="${slug}-pagination-prev"]`,
100
+ paginationNext: `[data-cy="${slug}-pagination-next"]`,
101
+
102
+ // Bulk actions
103
+ bulkActions: '[data-cy="posts-bulk-actions"]',
104
+ bulkPublish: '[data-cy="posts-bulk-publish"]',
105
+ bulkDelete: '[data-cy="posts-bulk-delete"]',
106
+
107
+ // States
108
+ loading: '[data-cy="posts-loading"]',
109
+ emptyState: '[data-cy="posts-empty"]',
110
+ emptyCreate: '[data-cy="posts-empty-create"]',
111
+
112
+ // Dialogs - MAPPED
113
+ deleteDialog: '[data-cy="posts-delete-dialog"]', // Convention: posts-confirm-delete
114
+ confirmDelete: '[data-cy="posts-delete-dialog"]',
115
+ confirmDeleteBtn: '[data-cy="posts-delete-confirm"]',
116
+ cancelDeleteBtn: '[data-cy="posts-delete-cancel"]',
117
+ }
118
+ }
119
+
120
+ // ============================================
121
+ // POSTS-SPECIFIC SELECTORS
122
+ // ============================================
123
+
124
+ get postSelectors() {
125
+ return {
126
+ // Stats/Filters
127
+ statAll: '[data-cy="posts-stat-all"]',
128
+ statPublished: '[data-cy="posts-stat-published"]',
129
+ statDraft: '[data-cy="posts-stat-draft"]',
130
+ statScheduled: '[data-cy="posts-stat-scheduled"]',
131
+
132
+ // View Toggles
133
+ viewTable: '[data-cy="posts-view-table"]',
134
+ viewGrid: '[data-cy="posts-view-grid"]',
135
+
136
+ // View Containers
137
+ tableContainer: '[data-cy="posts-table-container"]',
138
+ gridContainer: '[data-cy="posts-grid-container"]',
139
+
140
+ // Toolbar
141
+ toolbar: '[data-cy="posts-toolbar"]',
142
+ }
143
+ }
144
+
145
+ // ============================================
146
+ // POSTS-SPECIFIC METHODS
147
+ // ============================================
148
+
149
+ /**
150
+ * Validate the posts list page is visible and loaded
151
+ */
152
+ validateListVisible() {
153
+ cy.get(this.selectors.page).should('be.visible')
154
+ cy.get(this.selectors.pageTitle).should('be.visible')
155
+ cy.url().should('include', '/dashboard/posts')
156
+ return this
157
+ }
158
+
159
+ /**
160
+ * Validate loading state
161
+ */
162
+ validateLoading() {
163
+ cy.get(this.selectors.loading).should('be.visible')
164
+ return this
165
+ }
166
+
167
+ /**
168
+ * Wait for loading to complete
169
+ */
170
+ waitForLoadingComplete() {
171
+ cy.get(this.selectors.loading).should('not.exist')
172
+ return this
173
+ }
174
+
175
+ /**
176
+ * Filter posts by status using stat buttons
177
+ */
178
+ filterByStatus(status: 'all' | 'published' | 'draft' | 'scheduled') {
179
+ const statusMap = {
180
+ all: this.postSelectors.statAll,
181
+ published: this.postSelectors.statPublished,
182
+ draft: this.postSelectors.statDraft,
183
+ scheduled: this.postSelectors.statScheduled,
184
+ }
185
+ cy.get(statusMap[status]).click()
186
+ return this
187
+ }
188
+
189
+ /**
190
+ * Set view mode
191
+ */
192
+ setViewMode(mode: 'table' | 'grid') {
193
+ if (mode === 'table') {
194
+ cy.get(this.postSelectors.viewTable).click()
195
+ } else {
196
+ cy.get(this.postSelectors.viewGrid).click()
197
+ }
198
+ return this
199
+ }
200
+
201
+ /**
202
+ * Validate current view mode
203
+ */
204
+ validateViewMode(mode: 'table' | 'grid') {
205
+ if (mode === 'table') {
206
+ cy.get(this.postSelectors.tableContainer).should('be.visible')
207
+ } else {
208
+ cy.get(this.postSelectors.gridContainer).should('be.visible')
209
+ }
210
+ return this
211
+ }
212
+
213
+ /**
214
+ * Sort posts by selecting option
215
+ */
216
+ sortBy(option: string) {
217
+ cy.get(this.selectors.sortSelect).click()
218
+ cy.get(`[data-value="${option}"]`).click()
219
+ return this
220
+ }
221
+
222
+ /**
223
+ * Get post row element (table view)
224
+ */
225
+ getPostRow(id: string) {
226
+ return cy.get(this.selectors.row(id))
227
+ }
228
+
229
+ /**
230
+ * Get post card element (grid view)
231
+ */
232
+ getPostCard(id: string) {
233
+ return cy.get(this.selectors.card(id))
234
+ }
235
+
236
+ /**
237
+ * Click on post title to navigate to edit
238
+ */
239
+ clickPostTitle(id: string) {
240
+ cy.get(this.selectors.rowTitle(id)).click()
241
+ return this
242
+ }
243
+
244
+ /**
245
+ * Open actions menu for a post
246
+ */
247
+ openPostActions(id: string) {
248
+ cy.get(this.selectors.actionsDropdown(id)).click()
249
+ return this
250
+ }
251
+
252
+ /**
253
+ * Click edit action for a post
254
+ */
255
+ clickEdit(id: string) {
256
+ this.openPostActions(id)
257
+ cy.get(this.selectors.actionEdit(id)).click()
258
+ return this
259
+ }
260
+
261
+ /**
262
+ * Click publish action for a post
263
+ */
264
+ clickPublish(id: string) {
265
+ this.openPostActions(id)
266
+ cy.get(this.selectors.actionPublish(id)).click()
267
+ return this
268
+ }
269
+
270
+ /**
271
+ * Click unpublish action for a post
272
+ */
273
+ clickUnpublish(id: string) {
274
+ this.openPostActions(id)
275
+ cy.get(this.selectors.actionUnpublish(id)).click()
276
+ return this
277
+ }
278
+
279
+ /**
280
+ * Click delete action for a post (opens confirmation dialog)
281
+ */
282
+ clickDelete(id: string) {
283
+ this.openPostActions(id)
284
+ cy.get(this.selectors.actionDelete(id)).click()
285
+ return this
286
+ }
287
+
288
+ /**
289
+ * Validate delete dialog is visible
290
+ */
291
+ validateDeleteDialogVisible() {
292
+ cy.get(this.selectors.deleteDialog).should('be.visible')
293
+ return this
294
+ }
295
+
296
+ /**
297
+ * Validate a post exists in the list
298
+ */
299
+ validatePostExists(id: string) {
300
+ cy.get(this.selectors.row(id)).should('exist')
301
+ return this
302
+ }
303
+
304
+ /**
305
+ * Validate a post does not exist in the list
306
+ */
307
+ validatePostNotExists(id: string) {
308
+ cy.get(this.selectors.row(id)).should('not.exist')
309
+ return this
310
+ }
311
+
312
+ /**
313
+ * Validate post status badge using data-cy-status attribute
314
+ */
315
+ validatePostStatus(id: string, status: 'draft' | 'published' | 'scheduled') {
316
+ cy.get(this.selectors.rowStatus(id))
317
+ .should('be.visible')
318
+ .and('have.attr', 'data-cy-status', status)
319
+ return this
320
+ }
321
+
322
+ /**
323
+ * Validate post count in list
324
+ */
325
+ validatePostCount(count: number) {
326
+ if (count === 0) {
327
+ cy.get(this.selectors.emptyState).should('be.visible')
328
+ } else {
329
+ cy.get(this.selectors.rowGeneric).should('have.length', count)
330
+ }
331
+ return this
332
+ }
333
+
334
+ /**
335
+ * Validate stat count
336
+ */
337
+ validateStatCount(status: 'all' | 'published' | 'draft' | 'scheduled', count: number) {
338
+ const statusMap = {
339
+ all: this.postSelectors.statAll,
340
+ published: this.postSelectors.statPublished,
341
+ draft: this.postSelectors.statDraft,
342
+ scheduled: this.postSelectors.statScheduled,
343
+ }
344
+ cy.get(statusMap[status]).should('contain.text', count.toString())
345
+ return this
346
+ }
347
+
348
+ // ============================================
349
+ // NAVIGATION
350
+ // ============================================
351
+
352
+ /**
353
+ * Visit the posts list page
354
+ */
355
+ visit() {
356
+ cy.visit('/dashboard/posts')
357
+ this.validateListVisible()
358
+ return this
359
+ }
360
+ }
361
+
362
+ export default PostsPOM
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Blog Theme - Component POMs Index
3
+ *
4
+ * Exports all POM classes and types for blog theme E2E tests.
5
+ * Follows the standardized Entity Testing Convention pattern.
6
+ *
7
+ * Usage:
8
+ * import { EntityList, PostsPOM, PostEditorPOM } from '@/classes/themes/blog/components'
9
+ * import type { EntityConfig, EditorMode } from '@/classes/themes/blog/components'
10
+ */
11
+
12
+ // Generic POMs
13
+ export { EntityList, type EntityConfig } from './EntityList'
14
+ export { EntityForm } from './EntityForm'
15
+
16
+ // Entity-specific POMs
17
+ export { PostsPOM } from './PostsPOM'
18
+ export { PostEditorPOM, type EditorMode, type PostData } from './PostEditorPOM'
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Blog Theme - Cypress POM Classes
3
+ *
4
+ * Page Object Model classes for testing the Blog theme.
5
+ *
6
+ * Usage:
7
+ * import { EntityList, PostsPOM, PostEditorPOM } from '@/classes/themes/blog'
8
+ *
9
+ * Legacy POMs (backwards compatibility):
10
+ * import { PostsList, PostEditor } from '@/classes/themes/blog'
11
+ */
12
+
13
+ // ============================================
14
+ // NEW: Entity Testing Convention POMs (TypeScript)
15
+ // ============================================
16
+
17
+ // Generic POMs
18
+ export { EntityList } from './components/EntityList.ts'
19
+ export { EntityForm } from './components/EntityForm.ts'
20
+
21
+ // Entity-specific POMs
22
+ export { PostsPOM } from './components/PostsPOM.ts'
23
+ export { PostEditorPOM } from './components/PostEditorPOM.ts'
24
+
25
+ // ============================================
26
+ // LEGACY: Original POMs (JavaScript)
27
+ // Kept for backwards compatibility with existing tests
28
+ // ============================================
29
+
30
+ export { PostsList } from './PostsList.js'
31
+ export { PostEditor } from './PostEditor.js'
32
+ export { WysiwygEditor } from './WysiwygEditor.js'
33
+ export { FeaturedImageUpload } from './FeaturedImageUpload.js'
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Theme Selectors for Cypress Tests
3
+ *
4
+ * This file re-exports from the main selectors in lib/.
5
+ * The lib/selectors.ts is the source of truth, placed there so
6
+ * block components can import it (tests/ is excluded from TypeScript).
7
+ *
8
+ * Architecture:
9
+ * - Core selectors: `core/lib/test/core-selectors.ts`
10
+ * - Theme selectors (source): `lib/selectors.ts`
11
+ * - Theme selectors (tests): This file (re-exports)
12
+ *
13
+ * @example POM usage:
14
+ * ```typescript
15
+ * import { cySelector, sel, SELECTORS } from '../selectors'
16
+ *
17
+ * class MyPOM extends BasePOM {
18
+ * get elements() {
19
+ * return {
20
+ * loginForm: cySelector('auth.login.form'),
21
+ * submitButton: cySelector('auth.login.submit'),
22
+ * postsList: cySelector('entities.posts.list'),
23
+ * }
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+
29
+ // Re-export everything from the lib selectors
30
+ export {
31
+ BLOCK_SELECTORS,
32
+ ENTITY_SELECTORS,
33
+ BLOG_SELECTORS,
34
+ THEME_SELECTORS,
35
+ SELECTORS,
36
+ sel,
37
+ s,
38
+ selDev,
39
+ cySelector,
40
+ entitySelectors,
41
+ CORE_SELECTORS,
42
+ } from '../../../lib/selectors'
43
+
44
+ export type {
45
+ ThemeSelectorsType,
46
+ BlockSelectorsType,
47
+ EntitySelectorsType,
48
+ Replacements,
49
+ } from '../../../lib/selectors'
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Blog Theme Session Helpers
3
+ *
4
+ * Isolated login helpers for blog theme tests.
5
+ * Uses cy.session() for cached authentication sessions.
6
+ * Uses API-based login for faster and more stable authentication.
7
+ *
8
+ * Theme Mode: single-user (isolated blogs, no team collaboration)
9
+ */
10
+
11
+ /**
12
+ * Blog theme test users
13
+ * Each user owns their own individual blog
14
+ */
15
+ export const BLOG_USERS = {
16
+ MARCOS: {
17
+ email: 'blog_author_marcos@nextspark.dev',
18
+ password: 'Test1234',
19
+ name: 'Marcos Tech'
20
+ },
21
+ LUCIA: {
22
+ email: 'blog_author_lucia@nextspark.dev',
23
+ password: 'Test1234',
24
+ name: 'Lucia Lifestyle'
25
+ },
26
+ CARLOS: {
27
+ email: 'blog_author_carlos@nextspark.dev',
28
+ password: 'Test1234',
29
+ name: 'Carlos Finance'
30
+ }
31
+ } as const
32
+
33
+ export type BlogAuthor = keyof typeof BLOG_USERS
34
+
35
+ // Default test password
36
+ const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
37
+ const API_TIMEOUT = 60000
38
+
39
+ /**
40
+ * API login helper
41
+ */
42
+ function apiLogin(email: string, password: string = TEST_PASSWORD): Cypress.Chainable<boolean> {
43
+ return cy.request({
44
+ method: 'POST',
45
+ url: '/api/auth/sign-in/email',
46
+ body: { email, password },
47
+ timeout: API_TIMEOUT,
48
+ failOnStatusCode: false
49
+ }).then((response) => {
50
+ if (response.status === 200) {
51
+ return true
52
+ } else {
53
+ cy.log(`⚠️ API login failed with status ${response.status}`)
54
+ return false
55
+ }
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Setup team context after login (blog theme uses personal teams)
61
+ */
62
+ function setupTeamContext() {
63
+ cy.request({
64
+ method: 'GET',
65
+ url: '/api/v1/teams',
66
+ timeout: API_TIMEOUT,
67
+ failOnStatusCode: false
68
+ }).then((teamsResponse) => {
69
+ if (teamsResponse.status === 200 && teamsResponse.body?.data?.length > 0) {
70
+ const teams = teamsResponse.body.data
71
+ const selectedTeam = teams[0]
72
+ const teamId = selectedTeam.id
73
+ cy.log(`✅ Setting active team: ${selectedTeam.name} (${teamId})`)
74
+ cy.window().then((win) => {
75
+ win.localStorage.setItem('activeTeamId', teamId)
76
+ })
77
+ cy.request({
78
+ method: 'POST',
79
+ url: '/api/v1/teams/switch',
80
+ body: { teamId },
81
+ timeout: API_TIMEOUT,
82
+ failOnStatusCode: false
83
+ })
84
+ }
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Login as a blog author
90
+ * Session is cached and reused across tests for optimal performance
91
+ *
92
+ * @param author - Author key (MARCOS, LUCIA, or CARLOS)
93
+ */
94
+ export function loginAsBlogAuthor(author: BlogAuthor = 'MARCOS') {
95
+ const user = BLOG_USERS[author]
96
+
97
+ cy.session(`blog-author-${author.toLowerCase()}`, () => {
98
+ apiLogin(user.email, user.password).then((success) => {
99
+ if (success) {
100
+ cy.visit('/dashboard', { timeout: 60000 })
101
+ }
102
+ cy.url().should('include', '/dashboard')
103
+ setupTeamContext()
104
+ })
105
+ }, {
106
+ validate: () => {
107
+ cy.request({
108
+ url: '/api/auth/get-session',
109
+ timeout: API_TIMEOUT,
110
+ failOnStatusCode: false
111
+ }).its('status').should('eq', 200)
112
+ }
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Alias for loginAsBlogAuthor('MARCOS')
118
+ * Default owner for blog theme tests
119
+ */
120
+ export function loginAsOwner() {
121
+ return loginAsBlogAuthor('MARCOS')
122
+ }
123
+
124
+ /**
125
+ * Login with a specific blog user email
126
+ * Useful for custom test scenarios
127
+ *
128
+ * @param email - User email to login with
129
+ * @param sessionName - Unique session name for caching
130
+ */
131
+ export function loginWithBlogEmail(email: string, sessionName?: string) {
132
+ const name = sessionName || `blog-session-${email.split('@')[0]}`
133
+
134
+ cy.session(name, () => {
135
+ apiLogin(email).then((success) => {
136
+ if (success) {
137
+ cy.visit('/dashboard', { timeout: 60000 })
138
+ }
139
+ cy.url().should('include', '/dashboard')
140
+ setupTeamContext()
141
+ })
142
+ }, {
143
+ validate: () => {
144
+ cy.request({
145
+ url: '/api/auth/get-session',
146
+ timeout: API_TIMEOUT,
147
+ failOnStatusCode: false
148
+ }).its('status').should('eq', 200)
149
+ }
150
+ })
151
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Cypress E2E Support File for Theme
3
+ *
4
+ * This file is used when running tests in npm mode (outside monorepo).
5
+ * In monorepo mode, the core support file is used instead via cypress.config.ts.
6
+ *
7
+ * For documentation video commands, install cypress-slow-down:
8
+ * pnpm add -D cypress-slow-down
9
+ * Then uncomment the doc-commands import below.
10
+ */
11
+
12
+ import '@testing-library/cypress/add-commands'
13
+
14
+ // Import @cypress/grep for test filtering by tags
15
+ // v5.x uses named export { register }
16
+ import { register as registerCypressGrep } from '@cypress/grep'
17
+ registerCypressGrep()
18
+
19
+ // Doc commands are optional (require cypress-slow-down)
20
+ // Uncomment if you have cypress-slow-down installed:
21
+ // import './doc-commands'
22
+
23
+ // Global error handling
24
+ Cypress.on('uncaught:exception', (err) => {
25
+ // Ignore React hydration errors
26
+ if (err.message.includes('Hydration')) {
27
+ return false
28
+ }
29
+ // Ignore ResizeObserver errors
30
+ if (err.message.includes('ResizeObserver')) {
31
+ return false
32
+ }
33
+ return true
34
+ })
35
+
36
+ // Global before hook
37
+ beforeEach(() => {
38
+ cy.clearCookies()
39
+ cy.clearLocalStorage()
40
+ })
41
+
42
+ // Type declarations for @cypress/grep and custom commands
43
+ declare global {
44
+ namespace Cypress {
45
+ interface Chainable {
46
+ /**
47
+ * Custom command to make API requests with better error handling
48
+ */
49
+ apiRequest(options: Partial<Cypress.RequestOptions>): Chainable<Cypress.Response<any>>
50
+
51
+ /**
52
+ * Login command for authenticated tests
53
+ */
54
+ login(email: string, password: string): Chainable<void>
55
+ }
56
+ interface SuiteConfigOverrides {
57
+ tags?: string | string[]
58
+ }
59
+ interface TestConfigOverrides {
60
+ tags?: string | string[]
61
+ }
62
+ }
63
+ }
64
+
65
+ // Custom API request command
66
+ Cypress.Commands.add('apiRequest', (options) => {
67
+ const defaultOptions = {
68
+ failOnStatusCode: false,
69
+ timeout: 15000,
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...options.headers
73
+ }
74
+ }
75
+ return cy.request({
76
+ ...defaultOptions,
77
+ ...options
78
+ })
79
+ })
80
+
81
+ // Login command for authenticated tests
82
+ Cypress.Commands.add('login', (email: string, password: string) => {
83
+ cy.session([email, password], () => {
84
+ cy.visit('/login')
85
+ cy.get('[data-testid="email-input"], input[name="email"]').type(email)
86
+ cy.get('[data-testid="password-input"], input[name="password"]').type(password)
87
+ cy.get('[data-testid="submit-button"], button[type="submit"]').click()
88
+ cy.url().should('include', '/dashboard')
89
+ })
90
+ })