@nextsparkjs/theme-blog 0.1.0-beta.19 → 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,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,89 @@
1
+ /**
2
+ * Blog Theme Session Helpers
3
+ *
4
+ * Isolated login helpers for blog theme tests.
5
+ * Uses cy.session() for cached authentication sessions.
6
+ *
7
+ * Theme Mode: single-user (isolated blogs, no team collaboration)
8
+ */
9
+
10
+ import { DevKeyring } from '../../../../../../test/cypress/src/classes/components/auth/DevKeyring.js'
11
+
12
+ /**
13
+ * Blog theme test users
14
+ * Each user owns their own individual blog
15
+ */
16
+ export const BLOG_USERS = {
17
+ MARCOS: {
18
+ email: 'blog_author_marcos@nextspark.dev',
19
+ password: 'Test1234',
20
+ name: 'Marcos Tech'
21
+ },
22
+ LUCIA: {
23
+ email: 'blog_author_lucia@nextspark.dev',
24
+ password: 'Test1234',
25
+ name: 'Lucia Lifestyle'
26
+ },
27
+ CARLOS: {
28
+ email: 'blog_author_carlos@nextspark.dev',
29
+ password: 'Test1234',
30
+ name: 'Carlos Finance'
31
+ }
32
+ } as const
33
+
34
+ export type BlogAuthor = keyof typeof BLOG_USERS
35
+
36
+ /**
37
+ * Login as a blog author
38
+ * Session is cached and reused across tests for optimal performance
39
+ *
40
+ * @param author - Author key (MARCOS, LUCIA, or CARLOS)
41
+ */
42
+ export function loginAsBlogAuthor(author: BlogAuthor = 'MARCOS') {
43
+ const user = BLOG_USERS[author]
44
+
45
+ cy.session(`blog-author-${author.toLowerCase()}`, () => {
46
+ cy.visit('/login')
47
+ const devKeyring = new DevKeyring()
48
+ devKeyring.validateVisible()
49
+ devKeyring.quickLoginByEmail(user.email)
50
+ cy.url().should('include', '/dashboard')
51
+ }, {
52
+ validate: () => {
53
+ cy.visit('/dashboard')
54
+ cy.url().should('include', '/dashboard')
55
+ }
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Alias for loginAsBlogAuthor('MARCOS')
61
+ * Default owner for blog theme tests
62
+ */
63
+ export function loginAsOwner() {
64
+ return loginAsBlogAuthor('MARCOS')
65
+ }
66
+
67
+ /**
68
+ * Login with a specific blog user email
69
+ * Useful for custom test scenarios
70
+ *
71
+ * @param email - User email to login with
72
+ * @param sessionName - Unique session name for caching
73
+ */
74
+ export function loginWithBlogEmail(email: string, sessionName?: string) {
75
+ const name = sessionName || `blog-session-${email.split('@')[0]}`
76
+
77
+ cy.session(name, () => {
78
+ cy.visit('/login')
79
+ const devKeyring = new DevKeyring()
80
+ devKeyring.validateVisible()
81
+ devKeyring.quickLoginByEmail(email)
82
+ cy.url().should('include', '/dashboard')
83
+ }, {
84
+ validate: () => {
85
+ cy.visit('/dashboard')
86
+ cy.url().should('include', '/dashboard')
87
+ }
88
+ })
89
+ }
@@ -0,0 +1,89 @@
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
+ import registerCypressGrep from '@cypress/grep'
16
+ registerCypressGrep()
17
+
18
+ // Doc commands are optional (require cypress-slow-down)
19
+ // Uncomment if you have cypress-slow-down installed:
20
+ // import './doc-commands'
21
+
22
+ // Global error handling
23
+ Cypress.on('uncaught:exception', (err) => {
24
+ // Ignore React hydration errors
25
+ if (err.message.includes('Hydration')) {
26
+ return false
27
+ }
28
+ // Ignore ResizeObserver errors
29
+ if (err.message.includes('ResizeObserver')) {
30
+ return false
31
+ }
32
+ return true
33
+ })
34
+
35
+ // Global before hook
36
+ beforeEach(() => {
37
+ cy.clearCookies()
38
+ cy.clearLocalStorage()
39
+ })
40
+
41
+ // Type declarations for @cypress/grep and custom commands
42
+ declare global {
43
+ namespace Cypress {
44
+ interface Chainable {
45
+ /**
46
+ * Custom command to make API requests with better error handling
47
+ */
48
+ apiRequest(options: Partial<Cypress.RequestOptions>): Chainable<Cypress.Response<any>>
49
+
50
+ /**
51
+ * Login command for authenticated tests
52
+ */
53
+ login(email: string, password: string): Chainable<void>
54
+ }
55
+ interface SuiteConfigOverrides {
56
+ tags?: string | string[]
57
+ }
58
+ interface TestConfigOverrides {
59
+ tags?: string | string[]
60
+ }
61
+ }
62
+ }
63
+
64
+ // Custom API request command
65
+ Cypress.Commands.add('apiRequest', (options) => {
66
+ const defaultOptions = {
67
+ failOnStatusCode: false,
68
+ timeout: 15000,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ ...options.headers
72
+ }
73
+ }
74
+ return cy.request({
75
+ ...defaultOptions,
76
+ ...options
77
+ })
78
+ })
79
+
80
+ // Login command for authenticated tests
81
+ Cypress.Commands.add('login', (email: string, password: string) => {
82
+ cy.session([email, password], () => {
83
+ cy.visit('/login')
84
+ cy.get('[data-testid="email-input"], input[name="email"]').type(email)
85
+ cy.get('[data-testid="password-input"], input[name="password"]').type(password)
86
+ cy.get('[data-testid="submit-button"], button[type="submit"]').click()
87
+ cy.url().should('include', '/dashboard')
88
+ })
89
+ })