@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,210 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Leads CRUD - Owner Role (Full Access)
5
+ *
6
+ * Uses CRM theme-specific POMs with Entity Testing Convention.
7
+ * Selectors follow the pattern: {slug}-{component}-{detail}
8
+ */
9
+
10
+ import { LeadsPOM } from '../../../../src/entities/LeadsPOM'
11
+ import { loginAsCrmOwner } from '../../../../src/session-helpers'
12
+
13
+ describe('Leads CRUD - Owner Role (Full Access)', () => {
14
+ const leads = new LeadsPOM()
15
+
16
+ beforeEach(() => {
17
+ loginAsCrmOwner()
18
+ cy.visit('/dashboard/leads')
19
+ leads.list.validatePageVisible()
20
+ })
21
+
22
+ describe('CREATE - Owner can create leads', () => {
23
+ it('OWNER_LEAD_CREATE_001: should create new lead with required fields', () => {
24
+ const timestamp = Date.now()
25
+ const firstName = `Test`
26
+ const lastName = `Lead ${timestamp}`
27
+ const email = `lead${timestamp}@example.com`
28
+
29
+ leads.list.clickCreate()
30
+ leads.form.validateFormVisible()
31
+
32
+ leads.fillLeadForm({
33
+ firstName,
34
+ lastName,
35
+ email,
36
+ status: 'new'
37
+ })
38
+
39
+ leads.submitForm()
40
+
41
+ cy.url().should('include', '/dashboard/leads')
42
+ cy.contains(lastName).should('be.visible')
43
+ })
44
+
45
+ it('OWNER_LEAD_CREATE_002: should create lead with optional fields', () => {
46
+ const timestamp = Date.now()
47
+ const firstName = `Complete`
48
+ const lastName = `Lead ${timestamp}`
49
+ const email = `complete${timestamp}@example.com`
50
+
51
+ leads.list.clickCreate()
52
+ leads.form.validateFormVisible()
53
+
54
+ leads.fillLeadForm({
55
+ firstName,
56
+ lastName,
57
+ email,
58
+ phone: '+1 555 123 4567',
59
+ company: 'Test Company Inc',
60
+ source: 'website',
61
+ status: 'new'
62
+ })
63
+
64
+ leads.submitForm()
65
+
66
+ cy.url().should('include', '/dashboard/leads')
67
+ cy.contains(lastName).should('be.visible')
68
+ })
69
+
70
+ it('OWNER_LEAD_CREATE_003: should validate required fields', () => {
71
+ leads.list.clickCreate()
72
+ leads.form.validateFormVisible()
73
+
74
+ // Try to submit without filling required fields
75
+ leads.submitForm()
76
+
77
+ // Form should still be visible (validation failed)
78
+ leads.form.validateFormVisible()
79
+ })
80
+ })
81
+
82
+ describe('READ - Owner can read leads', () => {
83
+ it('OWNER_LEAD_READ_001: should view lead list', () => {
84
+ leads.list.validatePageVisible()
85
+ leads.list.validateTableVisible()
86
+ })
87
+
88
+ it('OWNER_LEAD_READ_002: should view lead details', () => {
89
+ cy.get(leads.list.selectors.rowGeneric).then($rows => {
90
+ if ($rows.length > 0) {
91
+ leads.list.clickRowByIndex(0)
92
+ cy.url().should('match', /\/dashboard\/leads\/[a-z0-9-]+/)
93
+ }
94
+ })
95
+ })
96
+
97
+ it('OWNER_LEAD_READ_003: should search and filter leads', () => {
98
+ leads.list.search('test')
99
+ cy.wait(500)
100
+ leads.list.clearSearch()
101
+ })
102
+ })
103
+
104
+ describe('UPDATE - Owner can update leads', () => {
105
+ it('OWNER_LEAD_UPDATE_001: should edit lead basic information', () => {
106
+ cy.get(leads.list.selectors.rowGeneric).then($rows => {
107
+ if ($rows.length > 0) {
108
+ leads.list.clickRowByIndex(0)
109
+
110
+ cy.url().then(url => {
111
+ const leadId = url.split('/').pop()
112
+ leads.form.visitEdit(leadId!)
113
+ leads.form.validateFormVisible()
114
+
115
+ const updatedLastName = `Updated Lead ${Date.now()}`
116
+ leads.form.typeInField('lastName', updatedLastName)
117
+
118
+ leads.submitForm()
119
+
120
+ cy.url().should('include', '/dashboard/leads')
121
+ cy.contains(updatedLastName).should('be.visible')
122
+ })
123
+ }
124
+ })
125
+ })
126
+
127
+ it('OWNER_LEAD_UPDATE_002: should update lead status', () => {
128
+ cy.get(leads.list.selectors.rowGeneric).then($rows => {
129
+ if ($rows.length > 0) {
130
+ leads.list.clickRowByIndex(0)
131
+
132
+ cy.url().then(url => {
133
+ const leadId = url.split('/').pop()
134
+ leads.form.visitEdit(leadId!)
135
+ leads.form.validateFormVisible()
136
+
137
+ leads.form.selectOption('status', 'contacted')
138
+
139
+ leads.submitForm()
140
+ cy.url().should('include', '/dashboard/leads')
141
+ })
142
+ }
143
+ })
144
+ })
145
+
146
+ it('OWNER_LEAD_UPDATE_003: should cancel update without saving', () => {
147
+ cy.get(leads.list.selectors.rowGeneric).then($rows => {
148
+ if ($rows.length > 0) {
149
+ leads.list.clickRowByIndex(0)
150
+
151
+ cy.url().then(url => {
152
+ const leadId = url.split('/').pop()
153
+ leads.form.visitEdit(leadId!)
154
+ leads.form.validateFormVisible()
155
+
156
+ leads.form.typeInField('lastName', 'ShouldNotSave')
157
+
158
+ leads.form.cancel()
159
+ cy.url().should('not.include', '/edit')
160
+ })
161
+ }
162
+ })
163
+ })
164
+ })
165
+
166
+ describe('DELETE - Owner can delete leads', () => {
167
+ it('OWNER_LEAD_DELETE_001: should delete lead', () => {
168
+ const timestamp = Date.now()
169
+ const firstName = `Delete`
170
+ const lastName = `Test ${timestamp}`
171
+ const email = `delete${timestamp}@example.com`
172
+
173
+ leads.list.clickCreate()
174
+ leads.fillLeadForm({
175
+ firstName,
176
+ lastName,
177
+ email,
178
+ status: 'new'
179
+ })
180
+ leads.submitForm()
181
+
182
+ cy.contains(lastName).should('be.visible')
183
+
184
+ leads.list.search(lastName)
185
+
186
+ cy.get(leads.list.selectors.rowGeneric).first().within(() => {
187
+ cy.get('[data-cy*="delete"]').click()
188
+ })
189
+
190
+ leads.list.confirmDelete()
191
+
192
+ cy.wait(1000)
193
+ leads.list.search(lastName)
194
+ cy.get(leads.list.selectors.emptyState).should('be.visible')
195
+ })
196
+
197
+ it('OWNER_LEAD_DELETE_002: should cancel deletion', () => {
198
+ cy.get(leads.list.selectors.rowGeneric).then($rows => {
199
+ if ($rows.length > 0) {
200
+ cy.get(leads.list.selectors.rowGeneric).first().within(() => {
201
+ cy.get('[data-cy*="delete"]').click()
202
+ })
203
+
204
+ leads.list.cancelDelete()
205
+ leads.list.validateTableVisible()
206
+ }
207
+ })
208
+ })
209
+ })
210
+ })
@@ -0,0 +1,197 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Opportunities CRUD - Admin Role (Full Access)
5
+ *
6
+ * Uses CRM theme-specific POMs with Entity Testing Convention.
7
+ * Selectors follow the pattern: {slug}-{component}-{detail}
8
+ */
9
+
10
+ import { OpportunitiesPOM } from '../../../src/entities/OpportunitiesPOM'
11
+ import { loginAsCrmAdmin } from '../../../src/session-helpers'
12
+
13
+ describe('Opportunities CRUD - Admin Role (Full Access)', () => {
14
+ const opportunities = new OpportunitiesPOM()
15
+
16
+ beforeEach(() => {
17
+ loginAsCrmAdmin()
18
+ cy.visit('/dashboard/opportunities')
19
+ opportunities.list.validatePageVisible()
20
+ })
21
+
22
+ describe('CREATE - Admin can create opportunities', () => {
23
+ it('ADMIN_OPPO_CREATE_001: should create new opportunity with required fields', () => {
24
+ const timestamp = Date.now()
25
+ const opportunityName = `Admin Test Opportunity ${timestamp}`
26
+
27
+ opportunities.list.clickCreate()
28
+ opportunities.form.validateFormVisible()
29
+
30
+ opportunities.fillOpportunityForm({
31
+ name: opportunityName,
32
+ amount: '45000'
33
+ })
34
+
35
+ opportunities.submitForm()
36
+
37
+ cy.url().should('include', '/dashboard/opportunities')
38
+ cy.contains(opportunityName).should('be.visible')
39
+ })
40
+
41
+ it('ADMIN_OPPO_CREATE_002: should create opportunity with high value', () => {
42
+ const timestamp = Date.now()
43
+ const opportunityName = `High Value Deal ${timestamp}`
44
+
45
+ opportunities.list.clickCreate()
46
+ opportunities.form.validateFormVisible()
47
+
48
+ opportunities.fillOpportunityForm({
49
+ name: opportunityName,
50
+ amount: '150000',
51
+ probability: '75'
52
+ })
53
+
54
+ opportunities.submitForm()
55
+
56
+ cy.url().should('include', '/dashboard/opportunities')
57
+ cy.contains(opportunityName).should('be.visible')
58
+ })
59
+
60
+ it('ADMIN_OPPO_CREATE_003: should validate required fields', () => {
61
+ opportunities.list.clickCreate()
62
+ opportunities.form.validateFormVisible()
63
+
64
+ // Try to submit without filling required fields
65
+ opportunities.submitForm()
66
+
67
+ // Form should still be visible (validation failed)
68
+ opportunities.form.validateFormVisible()
69
+ })
70
+ })
71
+
72
+ describe('READ - Admin can read opportunities', () => {
73
+ it('ADMIN_OPPO_READ_001: should view opportunity list', () => {
74
+ opportunities.list.validatePageVisible()
75
+ opportunities.list.validateTableVisible()
76
+ })
77
+
78
+ it('ADMIN_OPPO_READ_002: should view opportunity details', () => {
79
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
80
+ if ($rows.length > 0) {
81
+ opportunities.list.clickRowByIndex(0)
82
+ cy.url().should('match', /\/dashboard\/opportunities\/[a-z0-9-]+/)
83
+ }
84
+ })
85
+ })
86
+
87
+ it('ADMIN_OPPO_READ_003: should search opportunities', () => {
88
+ opportunities.list.search('test')
89
+ cy.wait(500)
90
+ opportunities.list.clearSearch()
91
+ })
92
+ })
93
+
94
+ describe('UPDATE - Admin can update opportunities', () => {
95
+ it('ADMIN_OPPO_UPDATE_001: should edit opportunity details', () => {
96
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
97
+ if ($rows.length > 0) {
98
+ opportunities.list.clickRowByIndex(0)
99
+
100
+ cy.url().then(url => {
101
+ const opportunityId = url.split('/').pop()
102
+ opportunities.form.visitEdit(opportunityId!)
103
+ opportunities.form.validateFormVisible()
104
+
105
+ const updatedName = `Admin Updated ${Date.now()}`
106
+ opportunities.form.typeInField('name', updatedName)
107
+
108
+ opportunities.submitForm()
109
+
110
+ cy.url().should('include', '/dashboard/opportunities')
111
+ cy.contains(updatedName).should('be.visible')
112
+ })
113
+ }
114
+ })
115
+ })
116
+
117
+ it('ADMIN_OPPO_UPDATE_002: should update probability and value', () => {
118
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
119
+ if ($rows.length > 0) {
120
+ opportunities.list.clickRowByIndex(0)
121
+
122
+ cy.url().then(url => {
123
+ const opportunityId = url.split('/').pop()
124
+ opportunities.form.visitEdit(opportunityId!)
125
+ opportunities.form.validateFormVisible()
126
+
127
+ opportunities.form.typeInField('probability', '90')
128
+ opportunities.form.typeInField('amount', '120000')
129
+
130
+ opportunities.submitForm()
131
+ cy.url().should('include', '/dashboard/opportunities')
132
+ })
133
+ }
134
+ })
135
+ })
136
+
137
+ it('ADMIN_OPPO_UPDATE_003: should cancel update without saving', () => {
138
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
139
+ if ($rows.length > 0) {
140
+ opportunities.list.clickRowByIndex(0)
141
+
142
+ cy.url().then(url => {
143
+ const opportunityId = url.split('/').pop()
144
+ opportunities.form.visitEdit(opportunityId!)
145
+ opportunities.form.validateFormVisible()
146
+
147
+ opportunities.form.typeInField('name', 'ShouldNotSave')
148
+
149
+ opportunities.form.cancel()
150
+ cy.url().should('not.include', '/edit')
151
+ })
152
+ }
153
+ })
154
+ })
155
+ })
156
+
157
+ describe('DELETE - Admin can delete opportunities', () => {
158
+ it('ADMIN_OPPO_DELETE_001: should delete opportunity successfully', () => {
159
+ const timestamp = Date.now()
160
+ const opportunityName = `Admin Delete Test ${timestamp}`
161
+
162
+ opportunities.list.clickCreate()
163
+ opportunities.fillOpportunityForm({
164
+ name: opportunityName,
165
+ amount: '35000'
166
+ })
167
+ opportunities.submitForm()
168
+
169
+ cy.contains(opportunityName).should('be.visible')
170
+
171
+ opportunities.list.search(opportunityName)
172
+
173
+ cy.get(opportunities.list.selectors.rowGeneric).first().within(() => {
174
+ cy.get('[data-cy*="delete"]').click()
175
+ })
176
+
177
+ opportunities.list.confirmDelete()
178
+
179
+ cy.wait(1000)
180
+ opportunities.list.search(opportunityName)
181
+ cy.get(opportunities.list.selectors.emptyState).should('be.visible')
182
+ })
183
+
184
+ it('ADMIN_OPPO_DELETE_002: should cancel deletion', () => {
185
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
186
+ if ($rows.length > 0) {
187
+ cy.get(opportunities.list.selectors.rowGeneric).first().within(() => {
188
+ cy.get('[data-cy*="delete"]').click()
189
+ })
190
+
191
+ opportunities.list.cancelDelete()
192
+ opportunities.list.validateTableVisible()
193
+ }
194
+ })
195
+ })
196
+ })
197
+ })
@@ -0,0 +1,229 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Opportunities CRUD - Member Role (Restricted: View + Edit Only)
5
+ *
6
+ * Uses CRM theme-specific POMs with Entity Testing Convention.
7
+ * Selectors follow the pattern: {slug}-{component}-{detail}
8
+ */
9
+
10
+ import { OpportunitiesPOM } from '../../../src/entities/OpportunitiesPOM'
11
+ import { loginAsCrmMember } from '../../../src/session-helpers'
12
+
13
+ describe('Opportunities CRUD - Member Role (Restricted: View + Edit Only)', () => {
14
+ const opportunities = new OpportunitiesPOM()
15
+
16
+ beforeEach(() => {
17
+ loginAsCrmMember()
18
+ cy.visit('/dashboard/opportunities')
19
+ opportunities.list.validatePageVisible()
20
+ })
21
+
22
+ describe('READ - Member CAN view opportunities', () => {
23
+ it('MEMBER_OPPO_READ_001: should view opportunity list', () => {
24
+ opportunities.list.validatePageVisible()
25
+ opportunities.list.validateTableVisible()
26
+ })
27
+
28
+ it('MEMBER_OPPO_READ_002: should view opportunity details', () => {
29
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
30
+ if ($rows.length > 0) {
31
+ opportunities.list.clickRowByIndex(0)
32
+ cy.url().should('match', /\/dashboard\/opportunities\/[a-z0-9-]+/)
33
+ }
34
+ })
35
+ })
36
+
37
+ it('MEMBER_OPPO_READ_003: should search opportunities', () => {
38
+ opportunities.list.search('test')
39
+ cy.wait(500)
40
+ opportunities.list.clearSearch()
41
+ })
42
+ })
43
+
44
+ describe('UPDATE - Member CAN edit opportunities', () => {
45
+ it('MEMBER_OPPO_UPDATE_001: should edit existing opportunity', () => {
46
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
47
+ if ($rows.length > 0) {
48
+ opportunities.list.clickRowByIndex(0)
49
+
50
+ cy.url().then(url => {
51
+ const opportunityId = url.split('/').pop()
52
+ opportunities.form.visitEdit(opportunityId!)
53
+ opportunities.form.validateFormVisible()
54
+
55
+ // Verify fields are enabled (Member can edit)
56
+ cy.get(opportunities.form.selectors.field('name')).within(() => {
57
+ cy.get('input, textarea').should('not.be.disabled')
58
+ })
59
+ })
60
+ }
61
+ })
62
+ })
63
+
64
+ it('MEMBER_OPPO_UPDATE_002: should update opportunity name and value', () => {
65
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
66
+ if ($rows.length > 0) {
67
+ opportunities.list.clickRowByIndex(0)
68
+
69
+ cy.url().then(url => {
70
+ const opportunityId = url.split('/').pop()
71
+ opportunities.form.visitEdit(opportunityId!)
72
+ opportunities.form.validateFormVisible()
73
+
74
+ const updatedName = `Member Updated ${Date.now()}`
75
+ opportunities.form.typeInField('name', updatedName)
76
+ opportunities.form.typeInField('amount', '55000')
77
+
78
+ opportunities.submitForm()
79
+ cy.url().should('include', '/dashboard/opportunities')
80
+ cy.contains(updatedName).should('be.visible')
81
+ })
82
+ }
83
+ })
84
+ })
85
+
86
+ it('MEMBER_OPPO_UPDATE_003: should update probability', () => {
87
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
88
+ if ($rows.length > 0) {
89
+ opportunities.list.clickRowByIndex(0)
90
+
91
+ cy.url().then(url => {
92
+ const opportunityId = url.split('/').pop()
93
+ opportunities.form.visitEdit(opportunityId!)
94
+ opportunities.form.validateFormVisible()
95
+
96
+ opportunities.form.typeInField('probability', '80')
97
+
98
+ opportunities.submitForm()
99
+ cy.url().should('include', '/dashboard/opportunities')
100
+ })
101
+ }
102
+ })
103
+ })
104
+
105
+ it('MEMBER_OPPO_UPDATE_004: should cancel update without saving', () => {
106
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
107
+ if ($rows.length > 0) {
108
+ opportunities.list.clickRowByIndex(0)
109
+
110
+ cy.url().then(url => {
111
+ const opportunityId = url.split('/').pop()
112
+ opportunities.form.visitEdit(opportunityId!)
113
+ opportunities.form.validateFormVisible()
114
+
115
+ opportunities.form.typeInField('name', 'ShouldNotSave')
116
+
117
+ opportunities.form.cancel()
118
+ cy.url().should('not.include', '/edit')
119
+ })
120
+ }
121
+ })
122
+ })
123
+ })
124
+
125
+ describe('CREATE - Member CANNOT create opportunities', () => {
126
+ it('MEMBER_OPPO_CREATE_001: create button should be hidden', () => {
127
+ cy.get(opportunities.list.selectors.createButton).should('not.exist')
128
+ })
129
+
130
+ it('MEMBER_OPPO_CREATE_002: should not be able to access create form via UI', () => {
131
+ // Verify no visible way to access create form
132
+ cy.get('[data-cy*="create"], [data-cy*="add"]').should('not.exist')
133
+ })
134
+
135
+ it('MEMBER_OPPO_CREATE_003: direct navigation to create form should be blocked', () => {
136
+ cy.visit('/dashboard/opportunities/create', { failOnStatusCode: false })
137
+
138
+ // Should either redirect or show access denied
139
+ cy.url().then(url => {
140
+ if (url.includes('/dashboard/opportunities/create')) {
141
+ // If still on create page, should show error
142
+ cy.get('body').should('not.contain', opportunities.form.selectors.form)
143
+ } else {
144
+ // Should redirect away from create page
145
+ cy.url().should('not.include', '/create')
146
+ }
147
+ })
148
+ })
149
+ })
150
+
151
+ describe('DELETE - Member CANNOT delete opportunities', () => {
152
+ it('MEMBER_OPPO_DELETE_001: delete buttons should be hidden', () => {
153
+ cy.get('[data-cy*="delete"]').should('not.exist')
154
+ })
155
+
156
+ it('MEMBER_OPPO_DELETE_002: should not see delete action in opportunity list', () => {
157
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
158
+ if ($rows.length > 0) {
159
+ cy.get(opportunities.list.selectors.rowGeneric).first().within(() => {
160
+ cy.get('[data-cy*="delete"]').should('not.exist')
161
+ })
162
+ }
163
+ })
164
+ })
165
+
166
+ it('MEMBER_OPPO_DELETE_003: should not see delete action in detail view', () => {
167
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
168
+ if ($rows.length > 0) {
169
+ opportunities.list.clickRowByIndex(0)
170
+ cy.url().should('match', /\/dashboard\/opportunities\/[a-z0-9-]+/)
171
+
172
+ cy.get('[data-cy*="delete"]').should('not.exist')
173
+ }
174
+ })
175
+ })
176
+
177
+ it('MEMBER_OPPO_DELETE_004: should NOT delete opportunity via API', () => {
178
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
179
+ if ($rows.length > 0) {
180
+ cy.get(opportunities.list.selectors.rowGeneric).first().invoke('attr', 'data-cy').then(dataCy => {
181
+ if (dataCy) {
182
+ const opportunityId = dataCy.replace('opportunities-row-', '')
183
+
184
+ cy.request({
185
+ method: 'DELETE',
186
+ url: `/api/v1/opportunities/${opportunityId}`,
187
+ failOnStatusCode: false,
188
+ }).then(response => {
189
+ expect(response.status).to.be.oneOf([401, 403, 404])
190
+ })
191
+ }
192
+ })
193
+ }
194
+ })
195
+ })
196
+ })
197
+
198
+ describe('PERMISSIONS - Member role capabilities', () => {
199
+ it('MEMBER_OPPO_PERMISSIONS_001: should have view and edit access only', () => {
200
+ opportunities.list.validatePageVisible()
201
+
202
+ // Check for no create button
203
+ cy.get(opportunities.list.selectors.createButton).should('not.exist')
204
+
205
+ // Check for no delete buttons
206
+ cy.get('[data-cy*="delete"]').should('not.exist')
207
+ })
208
+
209
+ it('MEMBER_OPPO_PERMISSIONS_002: should see all organization opportunities', () => {
210
+ opportunities.list.validatePageVisible()
211
+
212
+ cy.get(opportunities.list.selectors.rowGeneric).then($rows => {
213
+ if ($rows.length > 0) {
214
+ cy.get(opportunities.list.selectors.rowGeneric).should('have.length.at.least', 1)
215
+ }
216
+ })
217
+ })
218
+
219
+ it('MEMBER_OPPO_PERMISSIONS_003: should maintain restrictions after page refresh', () => {
220
+ cy.reload()
221
+
222
+ opportunities.list.validatePageVisible()
223
+
224
+ // Verify restrictions persist
225
+ cy.get(opportunities.list.selectors.createButton).should('not.exist')
226
+ cy.get('[data-cy*="delete"]').should('not.exist')
227
+ })
228
+ })
229
+ })