@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,350 @@
1
+ /**
2
+ * PostsList - Blog Theme Posts List Page POM
3
+ *
4
+ * Handles posts listing with filters, views, and CRUD actions.
5
+ * Supports both table and grid view modes.
6
+ *
7
+ * Test cases: BLOG_POST_READ_001-005, BLOG_POST_DELETE_001-003
8
+ */
9
+ export class PostsList {
10
+ static selectors = {
11
+ // Container
12
+ container: '[data-cy="posts-list-container"]',
13
+ title: '[data-cy="posts-list-title"]',
14
+
15
+ // Stats/Filters
16
+ statAll: '[data-cy="posts-stat-all"]',
17
+ statPublished: '[data-cy="posts-stat-published"]',
18
+ statDraft: '[data-cy="posts-stat-draft"]',
19
+ statScheduled: '[data-cy="posts-stat-scheduled"]',
20
+
21
+ // Toolbar
22
+ toolbar: '[data-cy="posts-toolbar"]',
23
+ searchInput: '[data-cy="posts-search-input"]',
24
+ sortSelect: '[data-cy="posts-sort-select"]',
25
+
26
+ // View Toggles
27
+ viewTable: '[data-cy="posts-view-table"]',
28
+ viewGrid: '[data-cy="posts-view-grid"]',
29
+
30
+ // Bulk Actions
31
+ bulkActions: '[data-cy="posts-bulk-actions"]',
32
+ bulkPublish: '[data-cy="posts-bulk-publish"]',
33
+ bulkDelete: '[data-cy="posts-bulk-delete"]',
34
+
35
+ // Table View
36
+ tableContainer: '[data-cy="posts-table-container"]',
37
+ table: '[data-cy="posts-table"]',
38
+
39
+ // Grid View
40
+ gridContainer: '[data-cy="posts-grid-container"]',
41
+
42
+ // Create Button
43
+ createButton: '[data-cy="posts-create-button"]',
44
+
45
+ // States
46
+ loading: '[data-cy="posts-loading"]',
47
+ empty: '[data-cy="posts-empty"]',
48
+ emptyCreate: '[data-cy="posts-empty-create"]',
49
+
50
+ // Delete Dialog
51
+ deleteDialog: '[data-cy="posts-delete-dialog"]',
52
+ deleteConfirm: '[data-cy="posts-delete-confirm"]',
53
+ deleteCancel: '[data-cy="posts-delete-cancel"]',
54
+ }
55
+
56
+ /**
57
+ * Get dynamic selectors for a specific post by ID
58
+ */
59
+ static getPostSelectors(id) {
60
+ return {
61
+ // Table row
62
+ row: `[data-cy="posts-row-${id}"]`,
63
+ title: `[data-cy="posts-title-${id}"]`,
64
+ status: `[data-cy="posts-status-${id}"]`,
65
+ actions: `[data-cy="posts-actions-${id}"]`,
66
+ edit: `[data-cy="posts-edit-${id}"]`,
67
+ viewLive: `[data-cy="posts-view-live-${id}"]`,
68
+ publish: `[data-cy="posts-publish-${id}"]`,
69
+ unpublish: `[data-cy="posts-unpublish-${id}"]`,
70
+ delete: `[data-cy="posts-delete-${id}"]`,
71
+
72
+ // Grid card
73
+ card: `[data-cy="posts-card-${id}"]`,
74
+ cardTitle: `[data-cy="posts-card-title-${id}"]`,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validate that the posts list page is visible and loaded
80
+ */
81
+ validateListVisible() {
82
+ cy.get(PostsList.selectors.container).should('be.visible')
83
+ cy.get(PostsList.selectors.title).should('be.visible')
84
+ cy.url().should('include', '/dashboard/posts')
85
+ return this
86
+ }
87
+
88
+ /**
89
+ * Validate loading state
90
+ */
91
+ validateLoading() {
92
+ cy.get(PostsList.selectors.loading).should('be.visible')
93
+ return this
94
+ }
95
+
96
+ /**
97
+ * Wait for loading to complete
98
+ */
99
+ waitForLoadingComplete() {
100
+ cy.get(PostsList.selectors.loading).should('not.exist')
101
+ return this
102
+ }
103
+
104
+ /**
105
+ * Validate empty state is shown
106
+ */
107
+ validateEmptyState() {
108
+ cy.get(PostsList.selectors.empty).should('be.visible')
109
+ return this
110
+ }
111
+
112
+ /**
113
+ * Filter posts by status using stat buttons
114
+ * @param {string} status - 'all' | 'published' | 'draft' | 'scheduled'
115
+ */
116
+ filterByStatus(status) {
117
+ const statusMap = {
118
+ all: PostsList.selectors.statAll,
119
+ published: PostsList.selectors.statPublished,
120
+ draft: PostsList.selectors.statDraft,
121
+ scheduled: PostsList.selectors.statScheduled,
122
+ }
123
+ cy.get(statusMap[status]).click()
124
+ return this
125
+ }
126
+
127
+ /**
128
+ * Search posts by term
129
+ * @param {string} term - Search term
130
+ */
131
+ search(term) {
132
+ cy.get(PostsList.selectors.searchInput)
133
+ .clear()
134
+ .type(term)
135
+ return this
136
+ }
137
+
138
+ /**
139
+ * Clear search input
140
+ */
141
+ clearSearch() {
142
+ cy.get(PostsList.selectors.searchInput).clear()
143
+ return this
144
+ }
145
+
146
+ /**
147
+ * Set view mode
148
+ * @param {string} mode - 'table' | 'grid'
149
+ */
150
+ setViewMode(mode) {
151
+ if (mode === 'table') {
152
+ cy.get(PostsList.selectors.viewTable).click()
153
+ } else {
154
+ cy.get(PostsList.selectors.viewGrid).click()
155
+ }
156
+ return this
157
+ }
158
+
159
+ /**
160
+ * Validate current view mode
161
+ * @param {string} mode - 'table' | 'grid'
162
+ */
163
+ validateViewMode(mode) {
164
+ if (mode === 'table') {
165
+ cy.get(PostsList.selectors.tableContainer).should('be.visible')
166
+ } else {
167
+ cy.get(PostsList.selectors.gridContainer).should('be.visible')
168
+ }
169
+ return this
170
+ }
171
+
172
+ /**
173
+ * Sort posts by selecting option
174
+ * @param {string} option - Sort option value
175
+ */
176
+ sortBy(option) {
177
+ cy.get(PostsList.selectors.sortSelect).click()
178
+ cy.get(`[data-value="${option}"]`).click()
179
+ return this
180
+ }
181
+
182
+ /**
183
+ * Click create new post button
184
+ */
185
+ clickCreate() {
186
+ cy.get(PostsList.selectors.createButton).click()
187
+ return this
188
+ }
189
+
190
+ /**
191
+ * Get post row element (table view)
192
+ * @param {string} id - Post ID
193
+ */
194
+ getPostRow(id) {
195
+ return cy.get(PostsList.getPostSelectors(id).row)
196
+ }
197
+
198
+ /**
199
+ * Get post card element (grid view)
200
+ * @param {string} id - Post ID
201
+ */
202
+ getPostCard(id) {
203
+ return cy.get(PostsList.getPostSelectors(id).card)
204
+ }
205
+
206
+ /**
207
+ * Click on post title to navigate to edit
208
+ * @param {string} id - Post ID
209
+ */
210
+ clickPostTitle(id) {
211
+ cy.get(PostsList.getPostSelectors(id).title).click()
212
+ return this
213
+ }
214
+
215
+ /**
216
+ * Open actions menu for a post
217
+ * @param {string} id - Post ID
218
+ */
219
+ openPostActions(id) {
220
+ cy.get(PostsList.getPostSelectors(id).actions).click()
221
+ return this
222
+ }
223
+
224
+ /**
225
+ * Click edit action for a post
226
+ * @param {string} id - Post ID
227
+ */
228
+ clickEdit(id) {
229
+ this.openPostActions(id)
230
+ cy.get(PostsList.getPostSelectors(id).edit).click()
231
+ return this
232
+ }
233
+
234
+ /**
235
+ * Click publish action for a post
236
+ * @param {string} id - Post ID
237
+ */
238
+ clickPublish(id) {
239
+ this.openPostActions(id)
240
+ cy.get(PostsList.getPostSelectors(id).publish).click()
241
+ return this
242
+ }
243
+
244
+ /**
245
+ * Click unpublish action for a post
246
+ * @param {string} id - Post ID
247
+ */
248
+ clickUnpublish(id) {
249
+ this.openPostActions(id)
250
+ cy.get(PostsList.getPostSelectors(id).unpublish).click()
251
+ return this
252
+ }
253
+
254
+ /**
255
+ * Click delete action for a post (opens confirmation dialog)
256
+ * @param {string} id - Post ID
257
+ */
258
+ clickDelete(id) {
259
+ this.openPostActions(id)
260
+ cy.get(PostsList.getPostSelectors(id).delete).click()
261
+ return this
262
+ }
263
+
264
+ /**
265
+ * Confirm post deletion in dialog
266
+ */
267
+ confirmDelete() {
268
+ cy.get(PostsList.selectors.deleteDialog).should('be.visible')
269
+ cy.get(PostsList.selectors.deleteConfirm).click()
270
+ return this
271
+ }
272
+
273
+ /**
274
+ * Cancel post deletion in dialog
275
+ */
276
+ cancelDelete() {
277
+ cy.get(PostsList.selectors.deleteDialog).should('be.visible')
278
+ cy.get(PostsList.selectors.deleteCancel).click()
279
+ return this
280
+ }
281
+
282
+ /**
283
+ * Validate delete dialog is visible
284
+ */
285
+ validateDeleteDialogVisible() {
286
+ cy.get(PostsList.selectors.deleteDialog).should('be.visible')
287
+ return this
288
+ }
289
+
290
+ /**
291
+ * Validate a post exists in the list
292
+ * @param {string} id - Post ID
293
+ */
294
+ validatePostExists(id) {
295
+ cy.get(PostsList.getPostSelectors(id).row).should('exist')
296
+ return this
297
+ }
298
+
299
+ /**
300
+ * Validate a post does not exist in the list
301
+ * @param {string} id - Post ID
302
+ */
303
+ validatePostNotExists(id) {
304
+ cy.get(PostsList.getPostSelectors(id).row).should('not.exist')
305
+ return this
306
+ }
307
+
308
+ /**
309
+ * Validate post status badge using data-cy-status attribute
310
+ * @param {string} id - Post ID
311
+ * @param {string} status - Expected status code ('draft' | 'published')
312
+ */
313
+ validatePostStatus(id, status) {
314
+ cy.get(PostsList.getPostSelectors(id).status)
315
+ .should('be.visible')
316
+ .and('have.attr', 'data-cy-status', status)
317
+ return this
318
+ }
319
+
320
+ /**
321
+ * Validate post count in list
322
+ * @param {number} count - Expected number of posts
323
+ */
324
+ validatePostCount(count) {
325
+ if (count === 0) {
326
+ this.validateEmptyState()
327
+ } else {
328
+ cy.get('[data-cy^="posts-row-"]').should('have.length', count)
329
+ }
330
+ return this
331
+ }
332
+
333
+ /**
334
+ * Validate stat count
335
+ * @param {string} status - Stat type ('all' | 'published' | 'draft' | 'scheduled')
336
+ * @param {number} count - Expected count
337
+ */
338
+ validateStatCount(status, count) {
339
+ const statusMap = {
340
+ all: PostsList.selectors.statAll,
341
+ published: PostsList.selectors.statPublished,
342
+ draft: PostsList.selectors.statDraft,
343
+ scheduled: PostsList.selectors.statScheduled,
344
+ }
345
+ cy.get(statusMap[status]).should('contain.text', count.toString())
346
+ return this
347
+ }
348
+ }
349
+
350
+ export default PostsList
@@ -0,0 +1,373 @@
1
+ /**
2
+ * WysiwygEditor - Blog Theme Rich Text Editor POM
3
+ *
4
+ * Handles content editing with formatting toolbar.
5
+ * Based on native contentEditable with document.execCommand.
6
+ *
7
+ * Test cases: BLOG_EDITOR_001-006
8
+ */
9
+ export class WysiwygEditor {
10
+ static selectors = {
11
+ // Container
12
+ container: '[data-cy="wysiwyg-container"]',
13
+
14
+ // Toolbar
15
+ toolbar: '[data-cy="wysiwyg-toolbar"]',
16
+
17
+ // Undo/Redo
18
+ undo: '[data-cy="wysiwyg-undo"]',
19
+ redo: '[data-cy="wysiwyg-redo"]',
20
+
21
+ // Text Formatting
22
+ bold: '[data-cy="wysiwyg-bold"]',
23
+ italic: '[data-cy="wysiwyg-italic"]',
24
+ underline: '[data-cy="wysiwyg-underline"]',
25
+ strikeThrough: '[data-cy="wysiwyg-strikeThrough"]',
26
+
27
+ // Headings
28
+ h1: '[data-cy="wysiwyg-formatBlock-h1"]',
29
+ h2: '[data-cy="wysiwyg-formatBlock-h2"]',
30
+ h3: '[data-cy="wysiwyg-formatBlock-h3"]',
31
+
32
+ // Lists
33
+ bulletList: '[data-cy="wysiwyg-insertUnorderedList"]',
34
+ orderedList: '[data-cy="wysiwyg-insertOrderedList"]',
35
+
36
+ // Blocks
37
+ blockquote: '[data-cy="wysiwyg-formatBlock-blockquote"]',
38
+ codeBlock: '[data-cy="wysiwyg-formatBlock-pre"]',
39
+
40
+ // Media/Links
41
+ link: '[data-cy="wysiwyg-createLink"]',
42
+ image: '[data-cy="wysiwyg-insertImage"]',
43
+ horizontalRule: '[data-cy="wysiwyg-insertHorizontalRule"]',
44
+
45
+ // Preview
46
+ previewToggle: '[data-cy="wysiwyg-preview-toggle"]',
47
+ preview: '[data-cy="wysiwyg-preview"]',
48
+
49
+ // Editor
50
+ editorWrapper: '[data-cy="wysiwyg-editor-wrapper"]',
51
+ content: '[data-cy="wysiwyg-content"]',
52
+ placeholder: '[data-cy="wysiwyg-placeholder"]',
53
+
54
+ // Status Bar
55
+ statusbar: '[data-cy="wysiwyg-statusbar"]',
56
+ shortcuts: '[data-cy="wysiwyg-shortcuts"]',
57
+ wordcount: '[data-cy="wysiwyg-wordcount"]',
58
+ }
59
+
60
+ /**
61
+ * Validate editor is visible
62
+ */
63
+ validateVisible() {
64
+ cy.get(WysiwygEditor.selectors.container).should('be.visible')
65
+ cy.get(WysiwygEditor.selectors.toolbar).should('be.visible')
66
+ return this
67
+ }
68
+
69
+ /**
70
+ * Focus the editor content area
71
+ */
72
+ focus() {
73
+ cy.get(WysiwygEditor.selectors.content).focus()
74
+ return this
75
+ }
76
+
77
+ /**
78
+ * Type content into the editor
79
+ * @param {string} text - Text to type
80
+ */
81
+ typeContent(text) {
82
+ cy.get(WysiwygEditor.selectors.content)
83
+ .focus()
84
+ .type(text)
85
+ return this
86
+ }
87
+
88
+ /**
89
+ * Clear all content
90
+ */
91
+ clearContent() {
92
+ cy.get(WysiwygEditor.selectors.content)
93
+ .focus()
94
+ .clear()
95
+ return this
96
+ }
97
+
98
+ /**
99
+ * Get content HTML
100
+ */
101
+ getContent() {
102
+ return cy.get(WysiwygEditor.selectors.content).invoke('html')
103
+ }
104
+
105
+ /**
106
+ * Validate content contains text
107
+ * @param {string} text - Expected text
108
+ */
109
+ validateContentContains(text) {
110
+ cy.get(WysiwygEditor.selectors.content).should('contain.text', text)
111
+ return this
112
+ }
113
+
114
+ /**
115
+ * Select all content
116
+ */
117
+ selectAll() {
118
+ cy.get(WysiwygEditor.selectors.content)
119
+ .focus()
120
+ .type('{selectall}')
121
+ return this
122
+ }
123
+
124
+ /**
125
+ * Toggle bold formatting
126
+ */
127
+ toggleBold() {
128
+ cy.get(WysiwygEditor.selectors.bold).click()
129
+ return this
130
+ }
131
+
132
+ /**
133
+ * Toggle italic formatting
134
+ */
135
+ toggleItalic() {
136
+ cy.get(WysiwygEditor.selectors.italic).click()
137
+ return this
138
+ }
139
+
140
+ /**
141
+ * Toggle underline formatting
142
+ */
143
+ toggleUnderline() {
144
+ cy.get(WysiwygEditor.selectors.underline).click()
145
+ return this
146
+ }
147
+
148
+ /**
149
+ * Toggle strikethrough formatting
150
+ */
151
+ toggleStrikethrough() {
152
+ cy.get(WysiwygEditor.selectors.strikeThrough).click()
153
+ return this
154
+ }
155
+
156
+ /**
157
+ * Insert heading
158
+ * @param {number} level - Heading level (1, 2, or 3)
159
+ */
160
+ insertHeading(level) {
161
+ const headingMap = {
162
+ 1: WysiwygEditor.selectors.h1,
163
+ 2: WysiwygEditor.selectors.h2,
164
+ 3: WysiwygEditor.selectors.h3,
165
+ }
166
+ cy.get(headingMap[level]).click()
167
+ return this
168
+ }
169
+
170
+ /**
171
+ * Insert bullet list
172
+ */
173
+ insertBulletList() {
174
+ cy.get(WysiwygEditor.selectors.bulletList).click()
175
+ return this
176
+ }
177
+
178
+ /**
179
+ * Insert ordered list
180
+ */
181
+ insertOrderedList() {
182
+ cy.get(WysiwygEditor.selectors.orderedList).click()
183
+ return this
184
+ }
185
+
186
+ /**
187
+ * Insert blockquote
188
+ */
189
+ insertBlockquote() {
190
+ cy.get(WysiwygEditor.selectors.blockquote).click()
191
+ return this
192
+ }
193
+
194
+ /**
195
+ * Insert code block
196
+ */
197
+ insertCodeBlock() {
198
+ cy.get(WysiwygEditor.selectors.codeBlock).click()
199
+ return this
200
+ }
201
+
202
+ /**
203
+ * Insert horizontal rule
204
+ */
205
+ insertHorizontalRule() {
206
+ cy.get(WysiwygEditor.selectors.horizontalRule).click()
207
+ return this
208
+ }
209
+
210
+ /**
211
+ * Insert link (will trigger browser prompt)
212
+ * Note: In tests, you may need to stub window.prompt
213
+ * @param {string} url - Link URL
214
+ */
215
+ insertLink(url) {
216
+ cy.window().then(win => {
217
+ cy.stub(win, 'prompt').returns(url)
218
+ })
219
+ cy.get(WysiwygEditor.selectors.link).click()
220
+ return this
221
+ }
222
+
223
+ /**
224
+ * Insert image (will trigger browser prompt)
225
+ * Note: In tests, you may need to stub window.prompt
226
+ * @param {string} url - Image URL
227
+ */
228
+ insertImage(url) {
229
+ cy.window().then(win => {
230
+ cy.stub(win, 'prompt').returns(url)
231
+ })
232
+ cy.get(WysiwygEditor.selectors.image).click()
233
+ return this
234
+ }
235
+
236
+ /**
237
+ * Click undo button
238
+ */
239
+ undo() {
240
+ cy.get(WysiwygEditor.selectors.undo).click()
241
+ return this
242
+ }
243
+
244
+ /**
245
+ * Click redo button
246
+ */
247
+ redo() {
248
+ cy.get(WysiwygEditor.selectors.redo).click()
249
+ return this
250
+ }
251
+
252
+ /**
253
+ * Use keyboard shortcut for undo
254
+ */
255
+ undoKeyboard() {
256
+ cy.get(WysiwygEditor.selectors.content)
257
+ .focus()
258
+ .type('{ctrl+z}')
259
+ return this
260
+ }
261
+
262
+ /**
263
+ * Use keyboard shortcut for redo
264
+ */
265
+ redoKeyboard() {
266
+ cy.get(WysiwygEditor.selectors.content)
267
+ .focus()
268
+ .type('{ctrl+shift+z}')
269
+ return this
270
+ }
271
+
272
+ /**
273
+ * Toggle preview mode
274
+ */
275
+ togglePreview() {
276
+ cy.get(WysiwygEditor.selectors.previewToggle).click()
277
+ return this
278
+ }
279
+
280
+ /**
281
+ * Validate preview mode is active
282
+ */
283
+ validatePreviewMode() {
284
+ cy.get(WysiwygEditor.selectors.preview).should('be.visible')
285
+ cy.get(WysiwygEditor.selectors.content).should('not.exist')
286
+ return this
287
+ }
288
+
289
+ /**
290
+ * Validate edit mode is active
291
+ */
292
+ validateEditMode() {
293
+ cy.get(WysiwygEditor.selectors.content).should('be.visible')
294
+ cy.get(WysiwygEditor.selectors.preview).should('not.exist')
295
+ return this
296
+ }
297
+
298
+ /**
299
+ * Validate placeholder is visible
300
+ * @param {string} text - Expected placeholder text (optional)
301
+ */
302
+ validatePlaceholder(text) {
303
+ cy.get(WysiwygEditor.selectors.placeholder).should('be.visible')
304
+ if (text) {
305
+ cy.get(WysiwygEditor.selectors.placeholder).should('contain.text', text)
306
+ }
307
+ return this
308
+ }
309
+
310
+ /**
311
+ * Validate placeholder is hidden (content exists)
312
+ */
313
+ validatePlaceholderHidden() {
314
+ cy.get(WysiwygEditor.selectors.placeholder).should('not.exist')
315
+ return this
316
+ }
317
+
318
+ /**
319
+ * Validate word count
320
+ * @param {number} count - Expected word count
321
+ */
322
+ validateWordCount(count) {
323
+ cy.get(WysiwygEditor.selectors.wordcount)
324
+ .should('contain.text', `${count} words`)
325
+ return this
326
+ }
327
+
328
+ /**
329
+ * Validate content contains specific HTML element
330
+ * @param {string} tag - HTML tag name (e.g., 'h1', 'ul', 'blockquote')
331
+ */
332
+ validateContentHasElement(tag) {
333
+ cy.get(WysiwygEditor.selectors.content)
334
+ .find(tag)
335
+ .should('exist')
336
+ return this
337
+ }
338
+
339
+ /**
340
+ * Validate bold text is present
341
+ */
342
+ validateHasBoldText() {
343
+ cy.get(WysiwygEditor.selectors.content)
344
+ .find('b, strong')
345
+ .should('exist')
346
+ return this
347
+ }
348
+
349
+ /**
350
+ * Validate italic text is present
351
+ */
352
+ validateHasItalicText() {
353
+ cy.get(WysiwygEditor.selectors.content)
354
+ .find('i, em')
355
+ .should('exist')
356
+ return this
357
+ }
358
+
359
+ /**
360
+ * Validate link is present
361
+ * @param {string} url - Expected URL (optional)
362
+ */
363
+ validateHasLink(url) {
364
+ const link = cy.get(WysiwygEditor.selectors.content).find('a')
365
+ link.should('exist')
366
+ if (url) {
367
+ link.should('have.attr', 'href', url)
368
+ }
369
+ return this
370
+ }
371
+ }
372
+
373
+ export default WysiwygEditor