@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.
- package/package.json +2 -2
- package/tests/cypress/e2e/README.md +170 -0
- package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
- package/tests/cypress/e2e/categories/categories-crud.md +73 -0
- package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
- package/tests/cypress/e2e/posts/posts-crud.md +115 -0
- package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
- package/tests/cypress/e2e/posts/posts-editor.md +139 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +42 -0
- package/tests/cypress/src/FeaturedImageUpload.js +131 -0
- package/tests/cypress/src/PostEditor.js +386 -0
- package/tests/cypress/src/PostsList.js +350 -0
- package/tests/cypress/src/WysiwygEditor.js +373 -0
- package/tests/cypress/src/components/EntityForm.ts +357 -0
- package/tests/cypress/src/components/EntityList.ts +360 -0
- package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
- package/tests/cypress/src/components/PostsPOM.ts +362 -0
- package/tests/cypress/src/components/index.ts +18 -0
- package/tests/cypress/src/index.js +33 -0
- package/tests/cypress/src/selectors.ts +49 -0
- package/tests/cypress/src/session-helpers.ts +89 -0
- package/tests/cypress/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -0
- package/tests/tsconfig.json +15 -0
|
@@ -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
|