@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,386 @@
1
+ /**
2
+ * PostEditor - Blog Theme Post Create/Edit Page POM
3
+ *
4
+ * Handles post creation and editing with WYSIWYG editor.
5
+ * Supports both create and edit modes with dynamic selector prefixes.
6
+ *
7
+ * Test cases: BLOG_POST_CREATE_001-004, BLOG_POST_UPDATE_001-004
8
+ */
9
+ export class PostEditor {
10
+ /**
11
+ * Create a PostEditor instance
12
+ * @param {string} mode - 'create' | 'edit'
13
+ */
14
+ constructor(mode = 'create') {
15
+ this.mode = mode
16
+ this.prefix = mode === 'create' ? 'post-create' : 'post-edit'
17
+ }
18
+
19
+ /**
20
+ * Get selectors based on current mode
21
+ */
22
+ get selectors() {
23
+ const prefix = this.prefix
24
+ return {
25
+ // Container
26
+ container: `[data-cy="${prefix}-container"]`,
27
+ header: `[data-cy="${prefix}-header"]`,
28
+
29
+ // Navigation
30
+ back: `[data-cy="${prefix}-back"]`,
31
+
32
+ // Status
33
+ status: `[data-cy="${prefix}-status"]`,
34
+ autosaved: `[data-cy="${prefix}-autosaved"]`,
35
+
36
+ // Actions
37
+ settingsToggle: `[data-cy="${prefix}-settings-toggle"]`,
38
+ save: `[data-cy="${prefix}-save"]`,
39
+ publish: `[data-cy="${prefix}-publish"]`,
40
+ unpublish: `[data-cy="${prefix}-unpublish"]`, // Edit mode only
41
+ viewLive: `[data-cy="${prefix}-view-live"]`, // Edit mode only
42
+
43
+ // Error
44
+ error: `[data-cy="${prefix}-error"]`,
45
+ errorDismiss: `[data-cy="${prefix}-error-dismiss"]`,
46
+
47
+ // Editor
48
+ title: `[data-cy="${prefix}-title"]`,
49
+ content: `[data-cy="${prefix}-content"]`,
50
+
51
+ // Settings Panel
52
+ settings: `[data-cy="${prefix}-settings"]`,
53
+ statusSelect: `[data-cy="${prefix}-status-select"]`,
54
+ slug: `[data-cy="${prefix}-slug"]`,
55
+ excerpt: `[data-cy="${prefix}-excerpt"]`,
56
+ featuredImage: `[data-cy="${prefix}-featured-image"]`,
57
+ featuredToggle: `[data-cy="${prefix}-featured-toggle"]`,
58
+
59
+ // Delete Dialog (Edit mode only)
60
+ delete: `[data-cy="${prefix}-delete"]`,
61
+ deleteDialog: `[data-cy="${prefix}-delete-dialog"]`,
62
+ deleteConfirm: `[data-cy="${prefix}-delete-confirm"]`,
63
+ deleteCancel: `[data-cy="${prefix}-delete-cancel"]`,
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Validate page is visible and loaded
69
+ */
70
+ validatePageVisible() {
71
+ cy.get(this.selectors.container).should('be.visible')
72
+ cy.get(this.selectors.header).should('be.visible')
73
+
74
+ const urlPattern = this.mode === 'create'
75
+ ? '/dashboard/posts/create'
76
+ : '/dashboard/posts/.+/edit'
77
+ cy.url().should('match', new RegExp(urlPattern))
78
+
79
+ return this
80
+ }
81
+
82
+ /**
83
+ * Click back button to return to posts list
84
+ */
85
+ clickBack() {
86
+ cy.get(this.selectors.back).click()
87
+ return this
88
+ }
89
+
90
+ /**
91
+ * Fill post title
92
+ * @param {string} title - Post title
93
+ */
94
+ fillTitle(title) {
95
+ cy.get(this.selectors.title)
96
+ .clear()
97
+ .type(title)
98
+ return this
99
+ }
100
+
101
+ /**
102
+ * Get current title value
103
+ */
104
+ getTitle() {
105
+ return cy.get(this.selectors.title).invoke('val')
106
+ }
107
+
108
+ /**
109
+ * Validate title value
110
+ * @param {string} expectedTitle - Expected title
111
+ */
112
+ validateTitle(expectedTitle) {
113
+ cy.get(this.selectors.title).should('have.value', expectedTitle)
114
+ return this
115
+ }
116
+
117
+ /**
118
+ * Focus on content editor
119
+ */
120
+ focusContent() {
121
+ cy.get(this.selectors.content).find('[data-cy="wysiwyg-content"]').focus()
122
+ return this
123
+ }
124
+
125
+ /**
126
+ * Type content in the WYSIWYG editor
127
+ * @param {string} content - Content to type
128
+ */
129
+ typeContent(content) {
130
+ cy.get(this.selectors.content)
131
+ .find('[data-cy="wysiwyg-content"]')
132
+ .focus()
133
+ .type(content)
134
+ return this
135
+ }
136
+
137
+ /**
138
+ * Toggle settings panel visibility
139
+ */
140
+ toggleSettings() {
141
+ cy.get(this.selectors.settingsToggle).click()
142
+ return this
143
+ }
144
+
145
+ /**
146
+ * Validate settings panel is visible
147
+ */
148
+ validateSettingsVisible() {
149
+ cy.get(this.selectors.settings).should('be.visible')
150
+ return this
151
+ }
152
+
153
+ /**
154
+ * Validate settings panel is hidden
155
+ */
156
+ validateSettingsHidden() {
157
+ cy.get(this.selectors.settings).should('not.exist')
158
+ return this
159
+ }
160
+
161
+ /**
162
+ * Set post status via dropdown
163
+ * @param {string} status - 'draft' | 'published'
164
+ */
165
+ setStatus(status) {
166
+ cy.get(this.selectors.statusSelect).click()
167
+ cy.get(`[data-value="${status}"]`).click()
168
+ return this
169
+ }
170
+
171
+ /**
172
+ * Fill URL slug
173
+ * @param {string} slug - URL slug
174
+ */
175
+ fillSlug(slug) {
176
+ cy.get(this.selectors.slug)
177
+ .clear()
178
+ .type(slug)
179
+ return this
180
+ }
181
+
182
+ /**
183
+ * Validate slug value
184
+ * @param {string} expectedSlug - Expected slug
185
+ */
186
+ validateSlug(expectedSlug) {
187
+ cy.get(this.selectors.slug).should('have.value', expectedSlug)
188
+ return this
189
+ }
190
+
191
+ /**
192
+ * Fill excerpt
193
+ * @param {string} excerpt - Post excerpt
194
+ */
195
+ fillExcerpt(excerpt) {
196
+ cy.get(this.selectors.excerpt)
197
+ .clear()
198
+ .type(excerpt)
199
+ return this
200
+ }
201
+
202
+ /**
203
+ * Toggle featured post switch
204
+ * @param {boolean} enabled - Whether to enable featured
205
+ */
206
+ toggleFeatured(enabled) {
207
+ const expectedState = enabled ? 'checked' : 'unchecked'
208
+
209
+ cy.get(this.selectors.featuredToggle).then($switch => {
210
+ const currentState = $switch.attr('data-state')
211
+ if (currentState !== expectedState) {
212
+ // Click the switch element directly (it's already a button)
213
+ cy.wrap($switch).click({ force: true })
214
+ }
215
+ })
216
+
217
+ // Wait for state update and verify
218
+ cy.get(this.selectors.featuredToggle, { timeout: 5000 })
219
+ .should('have.attr', 'data-state', expectedState)
220
+
221
+ return this
222
+ }
223
+
224
+ /**
225
+ * Validate featured toggle state
226
+ * @param {boolean} enabled - Expected state
227
+ */
228
+ validateFeaturedState(enabled) {
229
+ const expectedState = enabled ? 'checked' : 'unchecked'
230
+ cy.get(this.selectors.featuredToggle, { timeout: 5000 })
231
+ .should('have.attr', 'data-state', expectedState)
232
+ return this
233
+ }
234
+
235
+ /**
236
+ * Click save draft button
237
+ */
238
+ saveDraft() {
239
+ cy.get(this.selectors.save).click()
240
+ return this
241
+ }
242
+
243
+ /**
244
+ * Click publish button
245
+ */
246
+ publish() {
247
+ cy.get(this.selectors.publish).click()
248
+ return this
249
+ }
250
+
251
+ /**
252
+ * Click unpublish button (edit mode only)
253
+ */
254
+ unpublish() {
255
+ if (this.mode !== 'edit') {
256
+ throw new Error('unpublish is only available in edit mode')
257
+ }
258
+ cy.get(this.selectors.unpublish).click()
259
+ return this
260
+ }
261
+
262
+ /**
263
+ * Click view live button (edit mode only)
264
+ */
265
+ clickViewLive() {
266
+ if (this.mode !== 'edit') {
267
+ throw new Error('viewLive is only available in edit mode')
268
+ }
269
+ cy.get(this.selectors.viewLive).click()
270
+ return this
271
+ }
272
+
273
+ /**
274
+ * Click delete button to open dialog (edit mode only)
275
+ */
276
+ clickDelete() {
277
+ if (this.mode !== 'edit') {
278
+ throw new Error('delete is only available in edit mode')
279
+ }
280
+ cy.get(this.selectors.delete).click()
281
+ return this
282
+ }
283
+
284
+ /**
285
+ * Confirm deletion in dialog (edit mode only)
286
+ */
287
+ confirmDelete() {
288
+ cy.get(this.selectors.deleteDialog).should('be.visible')
289
+ cy.get(this.selectors.deleteConfirm).click()
290
+ return this
291
+ }
292
+
293
+ /**
294
+ * Cancel deletion in dialog (edit mode only)
295
+ */
296
+ cancelDelete() {
297
+ cy.get(this.selectors.deleteDialog).should('be.visible')
298
+ cy.get(this.selectors.deleteCancel).click()
299
+ return this
300
+ }
301
+
302
+ /**
303
+ * Validate delete dialog is visible
304
+ */
305
+ validateDeleteDialogVisible() {
306
+ cy.get(this.selectors.deleteDialog).should('be.visible')
307
+ return this
308
+ }
309
+
310
+ /**
311
+ * Validate auto-saved indicator is shown
312
+ */
313
+ validateAutoSaved() {
314
+ cy.get(this.selectors.autosaved).should('be.visible')
315
+ return this
316
+ }
317
+
318
+ /**
319
+ * Validate error message is shown
320
+ * @param {string} errorType - Error type code (e.g., 'title-required') for data-cy-error matching
321
+ */
322
+ validateError(errorType) {
323
+ cy.get(this.selectors.error).should('be.visible')
324
+ if (errorType) {
325
+ cy.get(this.selectors.error).should('have.attr', 'data-cy-error', errorType)
326
+ }
327
+ return this
328
+ }
329
+
330
+ /**
331
+ * Dismiss error message
332
+ */
333
+ dismissError() {
334
+ cy.get(this.selectors.errorDismiss).click()
335
+ return this
336
+ }
337
+
338
+ /**
339
+ * Validate error is not visible
340
+ */
341
+ validateNoError() {
342
+ cy.get(this.selectors.error).should('not.exist')
343
+ return this
344
+ }
345
+
346
+ /**
347
+ * Validate status badge shows correct status using data-cy-status attribute
348
+ * @param {string} status - Status code: 'draft' | 'published' | 'new-draft' | 'new-post'
349
+ */
350
+ validateStatusBadge(status) {
351
+ cy.get(this.selectors.status).should('have.attr', 'data-cy-status', status)
352
+ return this
353
+ }
354
+
355
+ /**
356
+ * Validate unsaved changes indicator is visible (edit mode only)
357
+ */
358
+ validateUnsavedChanges() {
359
+ cy.get('[data-cy="post-unsaved-indicator"]').should('be.visible')
360
+ return this
361
+ }
362
+
363
+ /**
364
+ * Validate unsaved changes indicator is not visible
365
+ */
366
+ validateNoUnsavedChanges() {
367
+ cy.get('[data-cy="post-unsaved-indicator"]').should('not.exist')
368
+ return this
369
+ }
370
+
371
+ /**
372
+ * Fill complete post form
373
+ * @param {object} post - Post data
374
+ */
375
+ fillPost(post) {
376
+ if (post.title) this.fillTitle(post.title)
377
+ if (post.content) this.typeContent(post.content)
378
+ if (post.slug) this.fillSlug(post.slug)
379
+ if (post.excerpt) this.fillExcerpt(post.excerpt)
380
+ if (post.featured !== undefined) this.toggleFeatured(post.featured)
381
+ if (post.status) this.setStatus(post.status)
382
+ return this
383
+ }
384
+ }
385
+
386
+ export default PostEditor