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