@nextsparkjs/theme-blog 0.1.0-beta.19 → 0.1.0-beta.24
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 +3 -3
- 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/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/jest.config.cjs +127 -0
- package/tests/jest/setup.ts +170 -0
- package/tests/tsconfig.json +15 -0
|
@@ -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
|
+
})
|