@nextsparkjs/theme-crm 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.
Files changed (70) hide show
  1. package/package.json +2 -2
  2. package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
  3. package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
  4. package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
  5. package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
  6. package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
  7. package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
  8. package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
  9. package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
  10. package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
  11. package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
  12. package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
  13. package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
  14. package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
  15. package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
  16. package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
  17. package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
  18. package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
  19. package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
  20. package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
  21. package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
  22. package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
  23. package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
  24. package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
  25. package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
  26. package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
  27. package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
  28. package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +240 -0
  31. package/tests/cypress/src/components/CRMDataTable.js +223 -0
  32. package/tests/cypress/src/components/CRMMobileNav.js +138 -0
  33. package/tests/cypress/src/components/CRMSidebar.js +145 -0
  34. package/tests/cypress/src/components/CRMTopBar.js +194 -0
  35. package/tests/cypress/src/components/DealCard.js +197 -0
  36. package/tests/cypress/src/components/EntityDetail.ts +290 -0
  37. package/tests/cypress/src/components/EntityForm.ts +357 -0
  38. package/tests/cypress/src/components/EntityList.ts +360 -0
  39. package/tests/cypress/src/components/PipelineKanban.js +204 -0
  40. package/tests/cypress/src/components/StageColumn.js +196 -0
  41. package/tests/cypress/src/components/index.js +13 -0
  42. package/tests/cypress/src/components/index.ts +22 -0
  43. package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
  44. package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
  45. package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
  46. package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
  47. package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
  48. package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
  49. package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
  50. package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
  51. package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
  52. package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
  53. package/tests/cypress/src/controllers/index.ts +35 -0
  54. package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
  55. package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
  56. package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
  57. package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
  58. package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
  59. package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
  60. package/tests/cypress/src/entities/index.ts +31 -0
  61. package/tests/cypress/src/forms/OpportunityForm.js +316 -0
  62. package/tests/cypress/src/forms/PipelineForm.js +243 -0
  63. package/tests/cypress/src/forms/index.js +8 -0
  64. package/tests/cypress/src/index.js +22 -0
  65. package/tests/cypress/src/index.ts +68 -0
  66. package/tests/cypress/src/selectors.ts +50 -0
  67. package/tests/cypress/src/session-helpers.ts +94 -0
  68. package/tests/cypress/support/e2e.ts +89 -0
  69. package/tests/cypress.config.ts +165 -0
  70. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Generic Entity Detail POM for CRM Theme
3
+ *
4
+ * Page Object Model for entity detail pages in CRM theme.
5
+ * Reusable for all entities by providing the entity slug and singular name.
6
+ *
7
+ * Usage:
8
+ * const contactDetail = new EntityDetail('contacts', 'contact')
9
+ * const opportunityDetail = new EntityDetail('opportunities', 'opportunity')
10
+ */
11
+
12
+ export interface EntityDetailConfig {
13
+ /** Plural entity slug for URL (e.g., 'contacts', 'opportunities') */
14
+ entitySlug: string
15
+ /** Singular entity name for selectors (e.g., 'contact', 'opportunity') */
16
+ singularName: string
17
+ /** Custom sections this entity has (e.g., ['activities', 'notes'] for contacts) */
18
+ sections?: string[]
19
+ }
20
+
21
+ export class EntityDetail {
22
+ private entitySlug: string
23
+ private singularName: string
24
+ private sections: string[]
25
+
26
+ /**
27
+ * Create a new EntityDetail POM instance
28
+ * @param entitySlug - Plural entity slug for URL (e.g., 'contacts', 'opportunities')
29
+ * @param singularName - Singular name for selectors (e.g., 'contact', 'opportunity')
30
+ * @param sections - Custom sections this entity has
31
+ */
32
+ constructor(entitySlug: string, singularName: string, sections: string[] = []) {
33
+ this.entitySlug = entitySlug
34
+ this.singularName = singularName
35
+ this.sections = sections
36
+ }
37
+
38
+ // ============================================
39
+ // DYNAMIC SELECTORS
40
+ // ============================================
41
+
42
+ /**
43
+ * Get selectors for this entity detail page
44
+ */
45
+ get selectors() {
46
+ return {
47
+ // Page container
48
+ page: `[data-cy="${this.singularName}-detail-page"]`,
49
+
50
+ // Header elements
51
+ header: `[data-cy="${this.singularName}-detail-header"]`,
52
+ title: `[data-cy="${this.singularName}-title"]`,
53
+ backButton: `[data-cy="${this.singularName}-back-btn"]`,
54
+ editButton: `[data-cy="${this.singularName}-edit-btn"]`,
55
+ deleteButton: `[data-cy="${this.singularName}-delete-btn"]`,
56
+
57
+ // Status
58
+ statusBadge: `[data-cy="${this.singularName}-status-badge"]`,
59
+
60
+ // Panels
61
+ fieldsPanel: `[data-cy="${this.singularName}-fields-panel"]`,
62
+ descriptionPanel: `[data-cy="${this.singularName}-description-panel"]`,
63
+
64
+ // Dynamic section selector
65
+ section: (sectionName: string) => `[data-cy="${this.singularName}-${sectionName}-section"]`,
66
+
67
+ // Related item in a section (e.g., contact-activity-{id})
68
+ relatedItem: (relatedType: string, id: string) => `[data-cy="${this.singularName}-${relatedType}-${id}"]`,
69
+
70
+ // Generic panels (stats, etc.)
71
+ statsCards: `[data-cy="${this.singularName}-stats-cards"]`,
72
+ }
73
+ }
74
+
75
+ // ============================================
76
+ // STATIC FACTORY METHODS
77
+ // ============================================
78
+
79
+ /**
80
+ * Pre-configured entity detail configurations for CRM entities
81
+ */
82
+ static configs: Record<string, EntityDetailConfig> = {
83
+ // Core CRM entities
84
+ leads: { entitySlug: 'leads', singularName: 'lead', sections: ['activities', 'notes'] },
85
+ contacts: { entitySlug: 'contacts', singularName: 'contact', sections: ['activities', 'notes', 'opportunities'] },
86
+ companies: { entitySlug: 'companies', singularName: 'company', sections: ['contacts', 'opportunities', 'notes'] },
87
+ opportunities: { entitySlug: 'opportunities', singularName: 'opportunity', sections: ['activities', 'notes'] },
88
+ activities: { entitySlug: 'activities', singularName: 'activity', sections: [] },
89
+ campaigns: { entitySlug: 'campaigns', singularName: 'campaign', sections: ['leads', 'activities'] },
90
+ pipelines: { entitySlug: 'pipelines', singularName: 'pipeline', sections: ['stages'] },
91
+ products: { entitySlug: 'products', singularName: 'product', sections: [] },
92
+ notes: { entitySlug: 'notes', singularName: 'note', sections: [] },
93
+ }
94
+
95
+ /**
96
+ * Create an EntityDetail from pre-configured entity name
97
+ */
98
+ static for(entityName: keyof typeof EntityDetail.configs): EntityDetail {
99
+ const config = EntityDetail.configs[entityName]
100
+ if (!config) {
101
+ throw new Error(`Unknown entity: ${entityName}. Available: ${Object.keys(EntityDetail.configs).join(', ')}`)
102
+ }
103
+ return new EntityDetail(config.entitySlug, config.singularName, config.sections)
104
+ }
105
+
106
+ // ============================================
107
+ // VALIDATION METHODS
108
+ // ============================================
109
+
110
+ /**
111
+ * Validate the detail page is visible
112
+ */
113
+ validatePageVisible() {
114
+ cy.get(this.selectors.page).should('be.visible')
115
+ return this
116
+ }
117
+
118
+ /**
119
+ * Validate the header is visible
120
+ */
121
+ validateHeaderVisible() {
122
+ cy.get(this.selectors.header).should('be.visible')
123
+ return this
124
+ }
125
+
126
+ /**
127
+ * Validate the title text
128
+ */
129
+ validateTitle(expectedTitle: string) {
130
+ cy.get(this.selectors.title).should('contain.text', expectedTitle)
131
+ return this
132
+ }
133
+
134
+ /**
135
+ * Validate the status badge has specific text
136
+ */
137
+ validateStatus(expectedStatus: string) {
138
+ cy.get(this.selectors.statusBadge).should('contain.text', expectedStatus)
139
+ return this
140
+ }
141
+
142
+ /**
143
+ * Validate the fields panel is visible
144
+ */
145
+ validateFieldsPanelVisible() {
146
+ cy.get(this.selectors.fieldsPanel).should('be.visible')
147
+ return this
148
+ }
149
+
150
+ /**
151
+ * Validate a custom section is visible
152
+ */
153
+ validateSectionVisible(sectionName: string) {
154
+ cy.get(this.selectors.section(sectionName)).should('be.visible')
155
+ return this
156
+ }
157
+
158
+ /**
159
+ * Validate edit button is visible
160
+ */
161
+ validateEditButtonVisible() {
162
+ cy.get(this.selectors.editButton).should('be.visible')
163
+ return this
164
+ }
165
+
166
+ /**
167
+ * Validate delete button is visible
168
+ */
169
+ validateDeleteButtonVisible() {
170
+ cy.get(this.selectors.deleteButton).should('be.visible')
171
+ return this
172
+ }
173
+
174
+ /**
175
+ * Validate edit button is not visible (user doesn't have permission)
176
+ */
177
+ validateEditButtonHidden() {
178
+ cy.get(this.selectors.editButton).should('not.exist')
179
+ return this
180
+ }
181
+
182
+ /**
183
+ * Validate delete button is not visible (user doesn't have permission)
184
+ */
185
+ validateDeleteButtonHidden() {
186
+ cy.get(this.selectors.deleteButton).should('not.exist')
187
+ return this
188
+ }
189
+
190
+ // ============================================
191
+ // INTERACTION METHODS
192
+ // ============================================
193
+
194
+ /**
195
+ * Click the back button to return to list
196
+ */
197
+ clickBack() {
198
+ cy.get(this.selectors.backButton).click()
199
+ cy.url().should('include', `/dashboard/${this.entitySlug}`)
200
+ return this
201
+ }
202
+
203
+ /**
204
+ * Click the edit button to open edit form
205
+ */
206
+ clickEdit() {
207
+ cy.get(this.selectors.editButton).click()
208
+ cy.url().should('include', '/edit')
209
+ return this
210
+ }
211
+
212
+ /**
213
+ * Click the delete button (will open confirmation dialog)
214
+ */
215
+ clickDelete() {
216
+ cy.get(this.selectors.deleteButton).click()
217
+ return this
218
+ }
219
+
220
+ /**
221
+ * Confirm deletion in the dialog
222
+ */
223
+ confirmDelete() {
224
+ cy.get('[data-cy="confirm-delete-btn"]').click()
225
+ return this
226
+ }
227
+
228
+ /**
229
+ * Cancel deletion in the dialog
230
+ */
231
+ cancelDelete() {
232
+ cy.get('[data-cy="cancel-delete-btn"]').click()
233
+ return this
234
+ }
235
+
236
+ /**
237
+ * Click on a related item in a section
238
+ */
239
+ clickRelatedItem(relatedType: string, id: string) {
240
+ cy.get(this.selectors.relatedItem(relatedType, id)).click()
241
+ return this
242
+ }
243
+
244
+ // ============================================
245
+ // WAIT METHODS
246
+ // ============================================
247
+
248
+ /**
249
+ * Wait for the detail page to load
250
+ */
251
+ waitForPageLoad() {
252
+ cy.get(this.selectors.page).should('be.visible')
253
+ return this
254
+ }
255
+
256
+ /**
257
+ * Visit the detail page directly
258
+ */
259
+ visit(id: string) {
260
+ cy.visit(`/dashboard/${this.entitySlug}/${id}`)
261
+ this.waitForPageLoad()
262
+ return this
263
+ }
264
+
265
+ // ============================================
266
+ // FIELD VALUE ASSERTIONS
267
+ // ============================================
268
+
269
+ /**
270
+ * Assert a field value in the fields panel
271
+ * Uses FieldDisplay component pattern
272
+ */
273
+ assertFieldValue(fieldLabel: string, expectedValue: string) {
274
+ cy.get(this.selectors.fieldsPanel)
275
+ .contains(fieldLabel)
276
+ .parent()
277
+ .should('contain.text', expectedValue)
278
+ return this
279
+ }
280
+
281
+ /**
282
+ * Assert the description panel contains text
283
+ */
284
+ assertDescriptionContains(text: string) {
285
+ cy.get(this.selectors.descriptionPanel).should('contain.text', text)
286
+ return this
287
+ }
288
+ }
289
+
290
+ export default EntityDetail
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Generic Entity Form POM for CRM Theme
3
+ *
4
+ * Page Object Model for entity create/edit forms in CRM theme.
5
+ * Uses standardized data-cy selectors from entities.json.
6
+ *
7
+ * Convention: {slug}-{component}-{detail}
8
+ * Examples: leads-form, contacts-field-email, opportunities-form-submit
9
+ *
10
+ * Usage:
11
+ * const leadForm = EntityForm.for('leads')
12
+ * const contactForm = EntityForm.for('contacts')
13
+ */
14
+
15
+ // Import entity configs from CRM 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
+ private config: EntityConfig
30
+ private 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