@nextsparkjs/theme-blog 0.1.0-beta.17 → 0.1.0-beta.170

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.
Files changed (49) hide show
  1. package/api/authors/[username]/route.ts +5 -2
  2. package/api/authors/docs.md +135 -0
  3. package/api/authors/presets.ts +45 -0
  4. package/api/authors/route.ts +4 -1
  5. package/api/posts/public/docs.md +124 -0
  6. package/api/posts/public/presets.ts +65 -0
  7. package/api/posts/public/route.ts +4 -1
  8. package/config/app.config.ts +4 -5
  9. package/config/billing.config.ts +4 -7
  10. package/config/dashboard.config.ts +13 -0
  11. package/config/permissions.config.ts +11 -0
  12. package/entities/categories/api/docs.md +119 -0
  13. package/entities/categories/api/presets.ts +67 -0
  14. package/entities/posts/api/docs.md +174 -0
  15. package/entities/posts/api/presets.ts +137 -0
  16. package/lib/selectors.ts +2 -3
  17. package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
  18. package/package.json +4 -3
  19. package/styles/globals.css +45 -0
  20. package/tests/cypress/e2e/README.md +170 -0
  21. package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
  22. package/tests/cypress/e2e/categories/categories-crud.md +73 -0
  23. package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
  24. package/tests/cypress/e2e/posts/posts-crud.md +115 -0
  25. package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
  26. package/tests/cypress/e2e/posts/posts-editor.md +139 -0
  27. package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
  28. package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +42 -0
  31. package/tests/cypress/src/FeaturedImageUpload.js +131 -0
  32. package/tests/cypress/src/PostEditor.js +386 -0
  33. package/tests/cypress/src/PostsList.js +350 -0
  34. package/tests/cypress/src/WysiwygEditor.js +373 -0
  35. package/tests/cypress/src/components/EntityForm.ts +378 -0
  36. package/tests/cypress/src/components/EntityList.ts +378 -0
  37. package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
  38. package/tests/cypress/src/components/PostsPOM.ts +362 -0
  39. package/tests/cypress/src/components/index.ts +18 -0
  40. package/tests/cypress/src/index.js +33 -0
  41. package/tests/cypress/src/selectors.ts +49 -0
  42. package/tests/cypress/src/session-helpers.ts +151 -0
  43. package/tests/cypress/support/e2e.ts +90 -0
  44. package/tests/cypress.config.ts +154 -0
  45. package/tests/jest/__mocks__/jose.js +22 -0
  46. package/tests/jest/__mocks__/next-server.js +56 -0
  47. package/tests/jest/jest.config.cjs +131 -0
  48. package/tests/jest/setup.ts +170 -0
  49. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,378 @@
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
+ * Fill a field (auto-detects input or textarea)
244
+ * Alias for typeInField that also handles textareas
245
+ */
246
+ fillField(fieldName: string, value: string) {
247
+ cy.get(this.selectors.field(fieldName)).then($field => {
248
+ const $input = $field.find('input')
249
+ const $textarea = $field.find('textarea')
250
+
251
+ if ($input.length > 0) {
252
+ cy.wrap($input).clear().type(value)
253
+ } else if ($textarea.length > 0) {
254
+ cy.wrap($textarea).clear().type(value)
255
+ } else {
256
+ // Fallback to trying input selector directly
257
+ cy.get(this.selectors.fieldInput(fieldName)).clear().type(value)
258
+ }
259
+ })
260
+ return this
261
+ }
262
+
263
+ // ============================================
264
+ // FORM SUBMISSION METHODS
265
+ // ============================================
266
+
267
+ /**
268
+ * Click the submit button
269
+ */
270
+ submit() {
271
+ cy.get(this.selectors.submitButton).click()
272
+ return this
273
+ }
274
+
275
+ /**
276
+ * Click the cancel button
277
+ */
278
+ cancel() {
279
+ cy.get(this.selectors.cancelButton).click()
280
+ return this
281
+ }
282
+
283
+ // ============================================
284
+ // BULK FILL METHODS
285
+ // ============================================
286
+
287
+ /**
288
+ * Fill multiple fields at once
289
+ */
290
+ fillForm(data: Record<string, string | boolean>) {
291
+ Object.entries(data).forEach(([fieldName, value]) => {
292
+ if (typeof value === 'boolean') {
293
+ if (value) {
294
+ this.checkField(fieldName)
295
+ } else {
296
+ this.uncheckField(fieldName)
297
+ }
298
+ } else {
299
+ // Try input first, then textarea
300
+ cy.get(this.selectors.field(fieldName)).then($field => {
301
+ const hasInput = $field.find('input:not([type="checkbox"])').length > 0
302
+ const hasTextarea = $field.find('textarea').length > 0
303
+ const hasSelect = $field.find('[role="combobox"]').length > 0
304
+
305
+ if (hasInput) {
306
+ this.typeInField(fieldName, value)
307
+ } else if (hasTextarea) {
308
+ this.typeInTextarea(fieldName, value)
309
+ } else if (hasSelect) {
310
+ this.selectOption(fieldName, value)
311
+ }
312
+ })
313
+ }
314
+ })
315
+ return this
316
+ }
317
+
318
+ // ============================================
319
+ // NAVIGATION METHODS
320
+ // ============================================
321
+
322
+ /**
323
+ * Visit the create form page
324
+ */
325
+ visitCreate() {
326
+ cy.visit(`/dashboard/${this.slug}/create`)
327
+ this.validatePageVisible()
328
+ return this
329
+ }
330
+
331
+ /**
332
+ * Visit the edit form page
333
+ */
334
+ visitEdit(id: string) {
335
+ cy.visit(`/dashboard/${this.slug}/${id}/edit`)
336
+ this.validatePageVisible()
337
+ return this
338
+ }
339
+
340
+ /**
341
+ * Wait for form to be ready (all fields loaded)
342
+ */
343
+ waitForFormReady() {
344
+ cy.get(this.selectors.form).should('be.visible')
345
+ cy.get(this.selectors.submitButton).should('be.visible')
346
+ return this
347
+ }
348
+
349
+ // ============================================
350
+ // ASSERTION HELPERS
351
+ // ============================================
352
+
353
+ /**
354
+ * Assert field has specific value
355
+ */
356
+ assertFieldValue(fieldName: string, expectedValue: string) {
357
+ cy.get(this.selectors.fieldInput(fieldName)).should('have.value', expectedValue)
358
+ return this
359
+ }
360
+
361
+ /**
362
+ * Assert textarea has specific value
363
+ */
364
+ assertTextareaValue(fieldName: string, expectedValue: string) {
365
+ cy.get(this.selectors.fieldTextarea(fieldName)).should('have.value', expectedValue)
366
+ return this
367
+ }
368
+
369
+ /**
370
+ * Assert select displays specific option
371
+ */
372
+ assertSelectValue(fieldName: string, expectedText: string) {
373
+ cy.get(this.selectors.fieldSelect(fieldName)).should('contain.text', expectedText)
374
+ return this
375
+ }
376
+ }
377
+
378
+ export default EntityForm