@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,357 @@
1
+ /**
2
+ * Generic Entity Form POM
3
+ *
4
+ * Page Object Model for entity create/edit forms in Blog theme.
5
+ * Uses standardized data-cy selectors from entities.json.
6
+ *
7
+ * Convention: {slug}-{component}-{detail}
8
+ * Examples: posts-form, categories-field-name, posts-form-submit
9
+ *
10
+ * Usage:
11
+ * const postForm = EntityForm.for('posts')
12
+ * const categoryForm = EntityForm.for('categories')
13
+ */
14
+
15
+ // Import entity configs from theme
16
+ import entitiesConfig from '../../fixtures/entities.json'
17
+
18
+ export interface EntityConfig {
19
+ slug: string
20
+ singular: string
21
+ plural: string
22
+ tableName: string
23
+ fields: string[]
24
+ sections: string[]
25
+ filters: string[]
26
+ }
27
+
28
+ export class EntityForm {
29
+ protected config: EntityConfig
30
+ protected slug: string
31
+
32
+ /**
33
+ * Create a new EntityForm POM instance from entity config
34
+ */
35
+ constructor(entityKey: string) {
36
+ const config = entitiesConfig.entities[entityKey as keyof typeof entitiesConfig.entities]
37
+ if (!config) {
38
+ throw new Error(`Unknown entity: ${entityKey}. Available: ${Object.keys(entitiesConfig.entities).join(', ')}`)
39
+ }
40
+ this.config = config as EntityConfig
41
+ this.slug = config.slug
42
+ }
43
+
44
+ // ============================================
45
+ // STATIC FACTORY METHOD
46
+ // ============================================
47
+
48
+ /**
49
+ * Create an EntityForm from entity key
50
+ */
51
+ static for(entityKey: string): EntityForm {
52
+ return new EntityForm(entityKey)
53
+ }
54
+
55
+ // ============================================
56
+ // DYNAMIC SELECTORS (from entities.json convention)
57
+ // ============================================
58
+
59
+ /**
60
+ * Get selectors for this entity form following the standard convention
61
+ */
62
+ get selectors() {
63
+ const slug = this.slug
64
+ return {
65
+ // Page and form containers
66
+ page: `[data-cy="${slug}-form-page"]`,
67
+ form: `[data-cy="${slug}-form"]`,
68
+ pageTitle: '[data-cy="page-title"]',
69
+
70
+ // Buttons
71
+ submitButton: `[data-cy="${slug}-form-submit"]`,
72
+ cancelButton: `[data-cy="${slug}-form-cancel"]`,
73
+
74
+ // Sections
75
+ section: (sectionName: string) => `[data-cy="${slug}-section-${sectionName}"]`,
76
+
77
+ // Fields
78
+ field: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}"]`,
79
+ fieldInput: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}"] input`,
80
+ fieldTextarea: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}"] textarea`,
81
+ fieldSelect: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}"] [role="combobox"]`,
82
+ fieldCheckbox: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}"] input[type="checkbox"]`,
83
+ fieldOption: (fieldName: string, value: string) => `[data-cy="${slug}-field-${fieldName}-option-${value}"]`,
84
+ fieldError: (fieldName: string) => `[data-cy="${slug}-field-${fieldName}-error"]`,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get the entity config
90
+ */
91
+ get entityConfig(): EntityConfig {
92
+ return this.config
93
+ }
94
+
95
+ /**
96
+ * Get available fields for this entity
97
+ */
98
+ get fields(): string[] {
99
+ return this.config.fields
100
+ }
101
+
102
+ /**
103
+ * Get available sections for this entity
104
+ */
105
+ get sections(): string[] {
106
+ return this.config.sections
107
+ }
108
+
109
+ // ============================================
110
+ // VALIDATION METHODS
111
+ // ============================================
112
+
113
+ /**
114
+ * Validate the form page is visible
115
+ */
116
+ validatePageVisible() {
117
+ cy.get(this.selectors.page).should('be.visible')
118
+ return this
119
+ }
120
+
121
+ /**
122
+ * Validate the form is visible
123
+ */
124
+ validateFormVisible() {
125
+ cy.get(this.selectors.form).should('be.visible')
126
+ return this
127
+ }
128
+
129
+ /**
130
+ * Validate the page title text
131
+ */
132
+ validatePageTitle(expectedTitle: string) {
133
+ cy.get(this.selectors.pageTitle).should('contain.text', expectedTitle)
134
+ return this
135
+ }
136
+
137
+ /**
138
+ * Validate a section is visible
139
+ */
140
+ validateSectionVisible(sectionName: string) {
141
+ cy.get(this.selectors.section(sectionName)).should('be.visible')
142
+ return this
143
+ }
144
+
145
+ /**
146
+ * Validate a field is visible
147
+ */
148
+ validateFieldVisible(fieldName: string) {
149
+ cy.get(this.selectors.field(fieldName)).should('be.visible')
150
+ return this
151
+ }
152
+
153
+ /**
154
+ * Validate a field has an error message
155
+ */
156
+ validateFieldHasError(fieldName: string, errorMessage?: string) {
157
+ const errorSelector = this.selectors.fieldError(fieldName)
158
+ cy.get(errorSelector).should('be.visible')
159
+ if (errorMessage) {
160
+ cy.get(errorSelector).should('contain.text', errorMessage)
161
+ }
162
+ return this
163
+ }
164
+
165
+ /**
166
+ * Validate submit button is enabled
167
+ */
168
+ validateSubmitEnabled() {
169
+ cy.get(this.selectors.submitButton).should('not.be.disabled')
170
+ return this
171
+ }
172
+
173
+ /**
174
+ * Validate submit button is disabled
175
+ */
176
+ validateSubmitDisabled() {
177
+ cy.get(this.selectors.submitButton).should('be.disabled')
178
+ return this
179
+ }
180
+
181
+ // ============================================
182
+ // INPUT METHODS
183
+ // ============================================
184
+
185
+ /**
186
+ * Type into a text input field
187
+ */
188
+ typeInField(fieldName: string, value: string) {
189
+ cy.get(this.selectors.fieldInput(fieldName)).clear().type(value)
190
+ return this
191
+ }
192
+
193
+ /**
194
+ * Type into a textarea field
195
+ */
196
+ typeInTextarea(fieldName: string, value: string) {
197
+ cy.get(this.selectors.fieldTextarea(fieldName)).clear().type(value)
198
+ return this
199
+ }
200
+
201
+ /**
202
+ * Clear a text input field
203
+ */
204
+ clearField(fieldName: string) {
205
+ cy.get(this.selectors.fieldInput(fieldName)).clear()
206
+ return this
207
+ }
208
+
209
+ /**
210
+ * Select an option from a select/combobox field
211
+ */
212
+ selectOption(fieldName: string, optionValue: string) {
213
+ cy.get(this.selectors.fieldSelect(fieldName)).click()
214
+ cy.get(this.selectors.fieldOption(fieldName, optionValue)).click()
215
+ return this
216
+ }
217
+
218
+ /**
219
+ * Check a checkbox field
220
+ */
221
+ checkField(fieldName: string) {
222
+ cy.get(this.selectors.fieldCheckbox(fieldName)).check()
223
+ return this
224
+ }
225
+
226
+ /**
227
+ * Uncheck a checkbox field
228
+ */
229
+ uncheckField(fieldName: string) {
230
+ cy.get(this.selectors.fieldCheckbox(fieldName)).uncheck()
231
+ return this
232
+ }
233
+
234
+ /**
235
+ * Fill a date input field
236
+ */
237
+ fillDate(fieldName: string, dateString: string) {
238
+ cy.get(this.selectors.fieldInput(fieldName)).clear().type(dateString)
239
+ return this
240
+ }
241
+
242
+ // ============================================
243
+ // FORM SUBMISSION METHODS
244
+ // ============================================
245
+
246
+ /**
247
+ * Click the submit button
248
+ */
249
+ submit() {
250
+ cy.get(this.selectors.submitButton).click()
251
+ return this
252
+ }
253
+
254
+ /**
255
+ * Click the cancel button
256
+ */
257
+ cancel() {
258
+ cy.get(this.selectors.cancelButton).click()
259
+ return this
260
+ }
261
+
262
+ // ============================================
263
+ // BULK FILL METHODS
264
+ // ============================================
265
+
266
+ /**
267
+ * Fill multiple fields at once
268
+ */
269
+ fillForm(data: Record<string, string | boolean>) {
270
+ Object.entries(data).forEach(([fieldName, value]) => {
271
+ if (typeof value === 'boolean') {
272
+ if (value) {
273
+ this.checkField(fieldName)
274
+ } else {
275
+ this.uncheckField(fieldName)
276
+ }
277
+ } else {
278
+ // Try input first, then textarea
279
+ cy.get(this.selectors.field(fieldName)).then($field => {
280
+ const hasInput = $field.find('input:not([type="checkbox"])').length > 0
281
+ const hasTextarea = $field.find('textarea').length > 0
282
+ const hasSelect = $field.find('[role="combobox"]').length > 0
283
+
284
+ if (hasInput) {
285
+ this.typeInField(fieldName, value)
286
+ } else if (hasTextarea) {
287
+ this.typeInTextarea(fieldName, value)
288
+ } else if (hasSelect) {
289
+ this.selectOption(fieldName, value)
290
+ }
291
+ })
292
+ }
293
+ })
294
+ return this
295
+ }
296
+
297
+ // ============================================
298
+ // NAVIGATION METHODS
299
+ // ============================================
300
+
301
+ /**
302
+ * Visit the create form page
303
+ */
304
+ visitCreate() {
305
+ cy.visit(`/dashboard/${this.slug}/create`)
306
+ this.validatePageVisible()
307
+ return this
308
+ }
309
+
310
+ /**
311
+ * Visit the edit form page
312
+ */
313
+ visitEdit(id: string) {
314
+ cy.visit(`/dashboard/${this.slug}/${id}/edit`)
315
+ this.validatePageVisible()
316
+ return this
317
+ }
318
+
319
+ /**
320
+ * Wait for form to be ready (all fields loaded)
321
+ */
322
+ waitForFormReady() {
323
+ cy.get(this.selectors.form).should('be.visible')
324
+ cy.get(this.selectors.submitButton).should('be.visible')
325
+ return this
326
+ }
327
+
328
+ // ============================================
329
+ // ASSERTION HELPERS
330
+ // ============================================
331
+
332
+ /**
333
+ * Assert field has specific value
334
+ */
335
+ assertFieldValue(fieldName: string, expectedValue: string) {
336
+ cy.get(this.selectors.fieldInput(fieldName)).should('have.value', expectedValue)
337
+ return this
338
+ }
339
+
340
+ /**
341
+ * Assert textarea has specific value
342
+ */
343
+ assertTextareaValue(fieldName: string, expectedValue: string) {
344
+ cy.get(this.selectors.fieldTextarea(fieldName)).should('have.value', expectedValue)
345
+ return this
346
+ }
347
+
348
+ /**
349
+ * Assert select displays specific option
350
+ */
351
+ assertSelectValue(fieldName: string, expectedText: string) {
352
+ cy.get(this.selectors.fieldSelect(fieldName)).should('contain.text', expectedText)
353
+ return this
354
+ }
355
+ }
356
+
357
+ export default EntityForm
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Generic Entity List POM
3
+ *
4
+ * Page Object Model for entity list pages in Blog theme.
5
+ * Uses standardized data-cy selectors from entities.json.
6
+ *
7
+ * Convention: {slug}-{component}-{detail}
8
+ * Examples: posts-table, categories-create-btn, posts-row-{id}
9
+ *
10
+ * Usage:
11
+ * const postsList = EntityList.for('posts')
12
+ * const categoriesList = EntityList.for('categories')
13
+ */
14
+
15
+ // Import entity configs from theme
16
+ import entitiesConfig from '../../fixtures/entities.json'
17
+
18
+ export interface EntityConfig {
19
+ slug: string
20
+ singular: string
21
+ plural: string
22
+ tableName: string
23
+ fields: string[]
24
+ sections: string[]
25
+ filters: string[]
26
+ }
27
+
28
+ export class EntityList {
29
+ protected config: EntityConfig
30
+ protected slug: string
31
+
32
+ /**
33
+ * Create a new EntityList POM instance from entity config
34
+ */
35
+ constructor(entityKey: string) {
36
+ const config = entitiesConfig.entities[entityKey as keyof typeof entitiesConfig.entities]
37
+ if (!config) {
38
+ throw new Error(`Unknown entity: ${entityKey}. Available: ${Object.keys(entitiesConfig.entities).join(', ')}`)
39
+ }
40
+ this.config = config as EntityConfig
41
+ this.slug = config.slug
42
+ }
43
+
44
+ // ============================================
45
+ // STATIC FACTORY METHOD
46
+ // ============================================
47
+
48
+ /**
49
+ * Create an EntityList from entity key
50
+ */
51
+ static for(entityKey: string): EntityList {
52
+ return new EntityList(entityKey)
53
+ }
54
+
55
+ // ============================================
56
+ // DYNAMIC SELECTORS (from entities.json convention)
57
+ // ============================================
58
+
59
+ /**
60
+ * Get selectors for this entity following the standard convention
61
+ */
62
+ get selectors() {
63
+ const slug = this.slug
64
+ return {
65
+ // Page elements
66
+ page: `[data-cy="${slug}-page"]`,
67
+ pageTitle: '[data-cy="page-title"]',
68
+
69
+ // Table
70
+ table: `[data-cy="${slug}-table"]`,
71
+
72
+ // Create button
73
+ createButton: `[data-cy="${slug}-create-btn"]`,
74
+
75
+ // Search
76
+ search: `[data-cy="${slug}-search"]`,
77
+ searchInput: `[data-cy="${slug}-search-input"]`,
78
+
79
+ // Filters
80
+ filter: (fieldName: string) => `[data-cy="${slug}-filter-${fieldName}"]`,
81
+ filterTrigger: (fieldName: string) => `[data-cy="${slug}-filter-${fieldName}-trigger"]`,
82
+ filterOption: (fieldName: string, value: string) => `[data-cy="${slug}-filter-${fieldName}-option-${value}"]`,
83
+
84
+ // Rows
85
+ row: (id: string) => `[data-cy="${slug}-row-${id}"]`,
86
+ rowGeneric: `[data-cy^="${slug}-row-"]`,
87
+
88
+ // Cards (for grid view)
89
+ card: (id: string) => `[data-cy="${slug}-card-${id}"]`,
90
+ cardGeneric: `[data-cy^="${slug}-card-"]`,
91
+
92
+ // Actions
93
+ actionEdit: (id: string) => `[data-cy="${slug}-action-edit-${id}"]`,
94
+ actionDelete: (id: string) => `[data-cy="${slug}-action-delete-${id}"]`,
95
+ actionView: (id: string) => `[data-cy="${slug}-action-view-${id}"]`,
96
+ actionsDropdown: (id: string) => `[data-cy="${slug}-actions-${id}"]`,
97
+ actionsTrigger: (id: string) => `[data-cy="${slug}-actions-trigger-${id}"]`,
98
+
99
+ // Pagination
100
+ pagination: `[data-cy="${slug}-pagination"]`,
101
+ paginationPrev: `[data-cy="${slug}-pagination-prev"]`,
102
+ paginationNext: `[data-cy="${slug}-pagination-next"]`,
103
+
104
+ // Bulk actions
105
+ bulkActions: `[data-cy="${slug}-bulk-actions"]`,
106
+
107
+ // Empty state
108
+ emptyState: `[data-cy="${slug}-empty"]`,
109
+
110
+ // Dialogs
111
+ confirmDelete: `[data-cy="${slug}-confirm-delete"]`,
112
+ confirmDeleteBtn: `[data-cy="${slug}-confirm-delete-btn"]`,
113
+ cancelDeleteBtn: `[data-cy="${slug}-cancel-delete-btn"]`,
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Get the entity config
119
+ */
120
+ get entityConfig(): EntityConfig {
121
+ return this.config
122
+ }
123
+
124
+ // ============================================
125
+ // VALIDATION METHODS
126
+ // ============================================
127
+
128
+ /**
129
+ * Validate the list page is visible
130
+ */
131
+ validatePageVisible() {
132
+ cy.get(this.selectors.page).should('be.visible')
133
+ return this
134
+ }
135
+
136
+ /**
137
+ * Validate the table is visible
138
+ */
139
+ validateTableVisible() {
140
+ cy.get(this.selectors.table).should('be.visible')
141
+ return this
142
+ }
143
+
144
+ /**
145
+ * Validate the page title text
146
+ */
147
+ validatePageTitle(expectedTitle: string) {
148
+ cy.get(this.selectors.pageTitle).should('contain.text', expectedTitle)
149
+ return this
150
+ }
151
+
152
+ /**
153
+ * Validate the create button is visible
154
+ */
155
+ validateCreateButtonVisible() {
156
+ cy.get(this.selectors.createButton).should('be.visible')
157
+ return this
158
+ }
159
+
160
+ /**
161
+ * Validate the table has rows
162
+ */
163
+ validateTableHasRows() {
164
+ cy.get(this.selectors.table).find('tbody tr').should('have.length.at.least', 1)
165
+ return this
166
+ }
167
+
168
+ /**
169
+ * Validate the table is empty
170
+ */
171
+ validateTableEmpty() {
172
+ cy.get(this.selectors.table).find('tbody tr').should('have.length', 0)
173
+ return this
174
+ }
175
+
176
+ // ============================================
177
+ // INTERACTION METHODS
178
+ // ============================================
179
+
180
+ /**
181
+ * Click the create button
182
+ */
183
+ clickCreate() {
184
+ cy.get(this.selectors.createButton).click()
185
+ return this
186
+ }
187
+
188
+ /**
189
+ * Search for a term
190
+ */
191
+ search(term: string) {
192
+ cy.get(this.selectors.searchInput).clear().type(term)
193
+ return this
194
+ }
195
+
196
+ /**
197
+ * Clear the search input
198
+ */
199
+ clearSearch() {
200
+ cy.get(this.selectors.searchInput).clear()
201
+ return this
202
+ }
203
+
204
+ /**
205
+ * Select a filter option
206
+ */
207
+ selectFilter(fieldName: string, value: string) {
208
+ cy.get(this.selectors.filterTrigger(fieldName)).click()
209
+ cy.get(this.selectors.filterOption(fieldName, value)).click()
210
+ return this
211
+ }
212
+
213
+ /**
214
+ * Click on a table row by index (0-based)
215
+ */
216
+ clickRowByIndex(index: number) {
217
+ cy.get(this.selectors.table).find('tbody tr').eq(index).click()
218
+ return this
219
+ }
220
+
221
+ /**
222
+ * Click on a table row by text content
223
+ */
224
+ clickRowByText(text: string) {
225
+ cy.get(this.selectors.table).find('tbody tr').contains(text).click()
226
+ return this
227
+ }
228
+
229
+ /**
230
+ * Click on a specific row by ID
231
+ */
232
+ clickRowById(id: string) {
233
+ cy.get(this.selectors.row(id)).click()
234
+ return this
235
+ }
236
+
237
+ /**
238
+ * Click edit action for a row
239
+ */
240
+ clickEditAction(id: string) {
241
+ cy.get(this.selectors.actionEdit(id)).click()
242
+ return this
243
+ }
244
+
245
+ /**
246
+ * Click delete action for a row
247
+ */
248
+ clickDeleteAction(id: string) {
249
+ cy.get(this.selectors.actionDelete(id)).click()
250
+ return this
251
+ }
252
+
253
+ /**
254
+ * Open actions dropdown for a row
255
+ */
256
+ openActionsDropdown(id: string) {
257
+ cy.get(this.selectors.actionsTrigger(id)).click()
258
+ return this
259
+ }
260
+
261
+ // ============================================
262
+ // PAGINATION METHODS
263
+ // ============================================
264
+
265
+ /**
266
+ * Go to next page
267
+ */
268
+ nextPage() {
269
+ cy.get(this.selectors.paginationNext).click()
270
+ return this
271
+ }
272
+
273
+ /**
274
+ * Go to previous page
275
+ */
276
+ previousPage() {
277
+ cy.get(this.selectors.paginationPrev).click()
278
+ return this
279
+ }
280
+
281
+ // ============================================
282
+ // BULK ACTIONS METHODS
283
+ // ============================================
284
+
285
+ /**
286
+ * Select all rows using the header checkbox
287
+ */
288
+ selectAll() {
289
+ cy.get(this.selectors.table).find('thead input[type="checkbox"]').check()
290
+ return this
291
+ }
292
+
293
+ /**
294
+ * Deselect all rows
295
+ */
296
+ deselectAll() {
297
+ cy.get(this.selectors.table).find('thead input[type="checkbox"]').uncheck()
298
+ return this
299
+ }
300
+
301
+ /**
302
+ * Select a row by index
303
+ */
304
+ selectRowByIndex(index: number) {
305
+ cy.get(this.selectors.table).find('tbody tr').eq(index).find('input[type="checkbox"]').check()
306
+ return this
307
+ }
308
+
309
+ /**
310
+ * Validate bulk actions panel is visible
311
+ */
312
+ validateBulkActionsVisible() {
313
+ cy.get(this.selectors.bulkActions).should('be.visible')
314
+ return this
315
+ }
316
+
317
+ // ============================================
318
+ // DELETE CONFIRMATION
319
+ // ============================================
320
+
321
+ /**
322
+ * Confirm deletion in dialog
323
+ */
324
+ confirmDelete() {
325
+ cy.get(this.selectors.confirmDeleteBtn).click()
326
+ return this
327
+ }
328
+
329
+ /**
330
+ * Cancel deletion in dialog
331
+ */
332
+ cancelDelete() {
333
+ cy.get(this.selectors.cancelDeleteBtn).click()
334
+ return this
335
+ }
336
+
337
+ // ============================================
338
+ // WAIT METHODS
339
+ // ============================================
340
+
341
+ /**
342
+ * Wait for the table to load
343
+ */
344
+ waitForTableLoad() {
345
+ cy.get(this.selectors.table).should('exist')
346
+ cy.get(this.selectors.page).find('[data-loading]').should('not.exist')
347
+ return this
348
+ }
349
+
350
+ /**
351
+ * Wait for page load
352
+ */
353
+ waitForPageLoad() {
354
+ cy.url().should('include', `/dashboard/${this.slug}`)
355
+ cy.get(this.selectors.page).should('be.visible')
356
+ return this
357
+ }
358
+ }
359
+
360
+ export default EntityList