@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.
- package/package.json +2 -2
- package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
- package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
- package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
- package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
- package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
- package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
- package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
- package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
- package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
- package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
- package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
- package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
- package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
- package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
- package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
- package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
- package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
- package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
- package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
- package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
- package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +240 -0
- package/tests/cypress/src/components/CRMDataTable.js +223 -0
- package/tests/cypress/src/components/CRMMobileNav.js +138 -0
- package/tests/cypress/src/components/CRMSidebar.js +145 -0
- package/tests/cypress/src/components/CRMTopBar.js +194 -0
- package/tests/cypress/src/components/DealCard.js +197 -0
- package/tests/cypress/src/components/EntityDetail.ts +290 -0
- package/tests/cypress/src/components/EntityForm.ts +357 -0
- package/tests/cypress/src/components/EntityList.ts +360 -0
- package/tests/cypress/src/components/PipelineKanban.js +204 -0
- package/tests/cypress/src/components/StageColumn.js +196 -0
- package/tests/cypress/src/components/index.js +13 -0
- package/tests/cypress/src/components/index.ts +22 -0
- package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
- package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
- package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
- package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
- package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
- package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
- package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
- package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
- package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
- package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
- package/tests/cypress/src/controllers/index.ts +35 -0
- package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
- package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
- package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
- package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
- package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
- package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
- package/tests/cypress/src/entities/index.ts +31 -0
- package/tests/cypress/src/forms/OpportunityForm.js +316 -0
- package/tests/cypress/src/forms/PipelineForm.js +243 -0
- package/tests/cypress/src/forms/index.js +8 -0
- package/tests/cypress/src/index.js +22 -0
- package/tests/cypress/src/index.ts +68 -0
- package/tests/cypress/src/selectors.ts +50 -0
- package/tests/cypress/src/session-helpers.ts +94 -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,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
|