@nextsparkjs/theme-default 0.1.0-beta.44 → 0.1.0-beta.45

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 (48) hide show
  1. package/components/ai-chat/ChatPanel.tsx +7 -7
  2. package/components/ai-chat/Message.tsx +2 -2
  3. package/components/ai-chat/MessageInput.tsx +3 -3
  4. package/components/ai-chat/MessageList.tsx +3 -3
  5. package/components/ai-chat/TypingIndicator.tsx +2 -2
  6. package/entities/customers/api/docs.md +107 -0
  7. package/entities/customers/api/presets.ts +80 -0
  8. package/entities/pages/api/docs.md +114 -0
  9. package/entities/pages/api/presets.ts +72 -0
  10. package/entities/posts/api/docs.md +120 -0
  11. package/entities/posts/api/presets.ts +74 -0
  12. package/entities/tasks/api/docs.md +126 -0
  13. package/entities/tasks/api/presets.ts +84 -0
  14. package/lib/selectors.ts +2 -2
  15. package/messages/de/admin.json +45 -0
  16. package/messages/en/admin.json +45 -0
  17. package/messages/es/admin.json +45 -0
  18. package/messages/fr/admin.json +45 -0
  19. package/messages/it/admin.json +45 -0
  20. package/messages/pt/admin.json +45 -0
  21. package/package.json +3 -3
  22. package/styles/globals.css +24 -0
  23. package/tests/cypress/e2e/_utils/selectors/block-editor.bdd.md +491 -0
  24. package/tests/cypress/e2e/_utils/selectors/block-editor.cy.ts +475 -0
  25. package/tests/cypress/e2e/_utils/selectors/dashboard-container.cy.ts +52 -0
  26. package/tests/cypress/e2e/_utils/selectors/dashboard-mobile.cy.ts +14 -14
  27. package/tests/cypress/e2e/_utils/selectors/dashboard-navigation.cy.ts +3 -3
  28. package/tests/cypress/e2e/_utils/selectors/dashboard-sidebar.bdd.md +38 -73
  29. package/tests/cypress/e2e/_utils/selectors/dashboard-sidebar.cy.ts +21 -42
  30. package/tests/cypress/e2e/_utils/selectors/dashboard-topnav.bdd.md +117 -38
  31. package/tests/cypress/e2e/_utils/selectors/dashboard-topnav.cy.ts +35 -12
  32. package/tests/cypress/e2e/_utils/selectors/settings-layout.bdd.md +50 -59
  33. package/tests/cypress/e2e/_utils/selectors/settings-layout.cy.ts +15 -23
  34. package/tests/cypress/e2e/_utils/selectors/tasks.bdd.md +395 -155
  35. package/tests/cypress/e2e/_utils/selectors/tasks.cy.ts +795 -174
  36. package/tests/cypress/e2e/api/_core/teams/teams-security.cy.ts +415 -0
  37. package/tests/cypress/e2e/uat/_core/teams/inline-edit.cy.ts +278 -0
  38. package/tests/cypress/src/core/BlockEditorBasePOM.ts +269 -99
  39. package/tests/cypress/src/core/DashboardEntityPOM.ts +1 -1
  40. package/tests/cypress/src/features/DashboardPOM.ts +49 -28
  41. package/tests/cypress/src/features/PageBuilderPOM.ts +20 -0
  42. package/tests/cypress/src/features/SettingsPOM.ts +511 -166
  43. package/tests/cypress/src/features/SuperadminPOM.ts +679 -159
  44. package/tests/cypress/src/features/index.ts +10 -10
  45. package/tests/cypress/e2e/_utils/selectors/pages-editor.bdd.md +0 -207
  46. package/tests/cypress/e2e/_utils/selectors/pages-editor.cy.ts +0 -211
  47. package/tests/cypress/e2e/_utils/selectors/posts-editor.bdd.md +0 -184
  48. package/tests/cypress/e2e/_utils/selectors/posts-editor.cy.ts +0 -350
@@ -1,23 +1,43 @@
1
1
  /**
2
- * POC Test: Tasks Entity Selectors Validation
2
+ * UI Selectors Validation: Tasks Entity
3
3
  *
4
- * This test validates that the new POM architecture with dynamic selectors
5
- * works correctly for entity CRUD operations.
4
+ * This test validates that all entity selectors exist in the DOM.
5
+ * Organized by the 6 first-level keys in ENTITIES_SELECTORS:
6
6
  *
7
- * Purpose:
8
- * - Validate selectors from DashboardEntityPOM work correctly
9
- * - Ensure dynamic selector generation produces valid CSS selectors
10
- * - Test before migrating existing tests to new architecture
7
+ * ═══════════════════════════════════════════════════════════════════════════════
8
+ * STRUCTURE (matches entities.selectors.ts)
9
+ * ═══════════════════════════════════════════════════════════════════════════════
11
10
  *
12
- * Scope:
13
- * - Only login and navigate
14
- * - Assert elements exist in DOM (no full CRUD operations)
11
+ * 1. PAGE - Page-level container
12
+ * 2. LIST - List view (search, filters, table, pagination, bulk, confirm)
13
+ * 3. HEADER - Entity detail header (view/edit/create modes)
14
+ * 4. DETAIL - Detail view container
15
+ * 5. FORM - Form container, fields, and actions
16
+ * 6. CHILD - Child entity management (not applicable for tasks)
17
+ *
18
+ * ═══════════════════════════════════════════════════════════════════════════════
19
+ *
20
+ * Test IDs:
21
+ * - SEL_TASK_PAGE_001: Page Container
22
+ * - SEL_TASK_LIST_001: Search Selectors
23
+ * - SEL_TASK_LIST_002: Table Structure Selectors
24
+ * - SEL_TASK_LIST_003: Row Dynamic Selectors
25
+ * - SEL_TASK_LIST_004: Pagination Selectors
26
+ * - SEL_TASK_LIST_005: Filter Selectors
27
+ * - SEL_TASK_LIST_006: Bulk Action Selectors
28
+ * - SEL_TASK_LIST_007: Confirm Dialog Selectors
29
+ * - SEL_TASK_HEADER_001: Header Selectors (all modes)
30
+ * - SEL_TASK_DETAIL_001: Detail Container
31
+ * - SEL_TASK_FORM_001: Form Selectors
32
+ *
33
+ * POM: TasksPOM extends DashboardEntityPOM
34
+ * Selectors: ENTITIES_SELECTORS (6 first-level keys)
15
35
  */
16
36
 
17
37
  import { TasksPOM } from '../../../src/entities/TasksPOM'
18
38
  import { loginAsDefaultOwner } from '../../../src/session-helpers'
19
39
 
20
- describe('Tasks Entity Selectors Validation', { tags: ['@ui-selectors', '@tasks'] }, () => {
40
+ describe('Tasks Entity Selectors Validation', { tags: ['@ui-selectors', '@entities', '@tasks'] }, () => {
21
41
  const tasks = TasksPOM.create()
22
42
 
23
43
  beforeEach(() => {
@@ -25,219 +45,820 @@ describe('Tasks Entity Selectors Validation', { tags: ['@ui-selectors', '@tasks'
25
45
  loginAsDefaultOwner()
26
46
  })
27
47
 
28
- describe('SEL_TASK_001: List Page Selectors', { tags: '@SEL_TASK_001' }, () => {
48
+ // ═══════════════════════════════════════════════════════════════════════════════
49
+ // 1. PAGE - Page-level container
50
+ // ═══════════════════════════════════════════════════════════════════════════════
51
+ describe('SEL_TASK_PAGE_001: Page Container', { tags: '@SEL_TASK_PAGE_001' }, () => {
29
52
  beforeEach(() => {
30
53
  tasks.visitList()
31
54
  tasks.waitForList()
32
55
  })
33
56
 
34
- it('should find table container element', () => {
35
- cy.get(tasks.selectors.tableContainer).should('exist')
36
- })
37
-
38
- // NOTE: entities.table.element selector not implemented in EntityTable component
39
- // The component uses {slug}-table-container on wrapper, not {slug}-table on <table>
40
- // Test removed as redundant with tableContainer test above
41
-
42
- it('should find add button', () => {
43
- cy.get(tasks.selectors.addButton).should('exist')
44
- })
45
-
46
- it('should find search input', () => {
47
- cy.get(tasks.selectors.search).should('exist')
57
+ /**
58
+ * Selector: entities.page.container
59
+ * POM: tasks.selectors.page
60
+ * data-cy: tasks-page
61
+ */
62
+ it.skip('should find page container (not implemented in EntityList)', () => {
63
+ // NOTE: page.container selector not currently implemented in EntityList component
64
+ // The page container would wrap the entire entity list view
65
+ cy.get(tasks.selectors.page).should('exist')
48
66
  })
67
+ })
49
68
 
50
- it('should find search container', () => {
51
- cy.get(tasks.selectors.searchContainer).should('exist')
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+ // 2. LIST - List view selectors
71
+ // ═══════════════════════════════════════════════════════════════════════════════
72
+ describe('LIST Selectors', { tags: '@entities-list' }, () => {
73
+ // ---------------------------------------------------------------------------
74
+ // 2.1 SEARCH - Search input and controls
75
+ // ---------------------------------------------------------------------------
76
+ describe('SEL_TASK_LIST_001: Search Selectors', { tags: '@SEL_TASK_LIST_001' }, () => {
77
+ beforeEach(() => {
78
+ tasks.visitList()
79
+ tasks.waitForList()
80
+ })
81
+
82
+ /**
83
+ * Selector: entities.list.search.input
84
+ * POM: tasks.selectors.search
85
+ * data-cy: tasks-search-input
86
+ */
87
+ it('should find search input', () => {
88
+ cy.get(tasks.selectors.search).should('exist')
89
+ })
90
+
91
+ /**
92
+ * Selector: entities.list.search.container
93
+ * POM: tasks.selectors.searchContainer
94
+ * data-cy: tasks-search
95
+ */
96
+ it('should find search container', () => {
97
+ cy.get(tasks.selectors.searchContainer).should('exist')
98
+ })
99
+
100
+ /**
101
+ * Selector: entities.list.search.clear
102
+ * POM: tasks.selectors.searchClear
103
+ * data-cy: tasks-search-clear
104
+ */
105
+ it.skip('should find search clear button (not implemented)', () => {
106
+ // NOTE: searchClear selector not currently implemented in EntityTable
107
+ cy.get(tasks.selectors.searchClear).should('exist')
108
+ })
52
109
  })
53
110
 
54
- it('should find select all checkbox', () => {
55
- cy.get(tasks.selectors.selectAll).should('exist')
111
+ // ---------------------------------------------------------------------------
112
+ // 2.2 TABLE - Table structure selectors
113
+ // ---------------------------------------------------------------------------
114
+ describe('SEL_TASK_LIST_002: Table Structure Selectors', { tags: '@SEL_TASK_LIST_002' }, () => {
115
+ beforeEach(() => {
116
+ tasks.visitList()
117
+ tasks.waitForList()
118
+ })
119
+
120
+ /**
121
+ * Selector: entities.list.table.container
122
+ * POM: tasks.selectors.tableContainer
123
+ * data-cy: tasks-table-container
124
+ */
125
+ it('should find table container', () => {
126
+ cy.get(tasks.selectors.tableContainer).should('exist')
127
+ })
128
+
129
+ /**
130
+ * Selector: entities.list.table.element
131
+ * POM: tasks.selectors.table
132
+ * data-cy: tasks-table
133
+ * NOTE: Table element only renders when there's data (not empty state)
134
+ */
135
+ it('should find table element (requires data)', () => {
136
+ // Skip if no rows exist (empty state)
137
+ cy.get('body').then(($body) => {
138
+ if ($body.find(tasks.selectors.rowGeneric).length > 0) {
139
+ cy.get(tasks.selectors.table).should('exist')
140
+ } else {
141
+ cy.log('⚠️ Skipping: No data - empty state showing')
142
+ }
143
+ })
144
+ })
145
+
146
+ /**
147
+ * Selector: entities.list.addButton
148
+ * POM: tasks.selectors.addButton
149
+ * data-cy: tasks-add
150
+ */
151
+ it('should find add button', () => {
152
+ cy.get(tasks.selectors.addButton).should('exist')
153
+ })
154
+
155
+ /**
156
+ * Selector: entities.list.table.selectAll
157
+ * POM: tasks.selectors.selectAll
158
+ * data-cy: tasks-select-all
159
+ * NOTE: SelectAll checkbox only renders when there's data and selectable=true
160
+ */
161
+ it('should find select all checkbox (requires data)', () => {
162
+ // Skip if no rows exist (empty state)
163
+ cy.get('body').then(($body) => {
164
+ if ($body.find(tasks.selectors.rowGeneric).length > 0) {
165
+ cy.get(tasks.selectors.selectAll).should('exist')
166
+ } else {
167
+ cy.log('⚠️ Skipping: No data - empty state showing')
168
+ }
169
+ })
170
+ })
171
+
172
+ /**
173
+ * Selector: entities.list.selectionCount
174
+ * POM: tasks.selectors.selectionCount
175
+ * data-cy: tasks-selection-count
176
+ * NOTE: Only visible when items are selected
177
+ */
178
+ it('should find selection count after selecting items (requires data)', () => {
179
+ cy.get('body').then(($body) => {
180
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
181
+ cy.log('⚠️ Skipping: No data - empty state showing')
182
+ return
183
+ }
184
+
185
+ // Select the first row to make selectionCount appear
186
+ cy.get(tasks.selectors.rowGeneric)
187
+ .first()
188
+ .invoke('attr', 'data-cy')
189
+ .then((dataCy) => {
190
+ const id = dataCy?.replace('tasks-row-', '') || ''
191
+ cy.get(tasks.selectors.rowSelect(id)).click()
192
+
193
+ // Now selectionCount should be visible
194
+ cy.get(tasks.selectors.selectionCount).should('exist')
195
+
196
+ // Clear selection
197
+ cy.get(tasks.selectors.rowSelect(id)).click()
198
+ })
199
+ })
200
+ })
56
201
  })
57
202
 
58
- it('should find pagination container', () => {
59
- cy.get(tasks.selectors.pagination).should('exist')
203
+ // ---------------------------------------------------------------------------
204
+ // 2.3 ROW - Dynamic row selectors
205
+ // ---------------------------------------------------------------------------
206
+ describe('SEL_TASK_LIST_003: Row Dynamic Selectors', { tags: '@SEL_TASK_LIST_003' }, () => {
207
+ beforeEach(() => {
208
+ tasks.visitList()
209
+ tasks.waitForList()
210
+ })
211
+
212
+ /**
213
+ * Selector: entities.list.table.row.element (dynamic)
214
+ * POM: tasks.selectors.row(id)
215
+ * data-cy: tasks-row-{id}
216
+ * NOTE: Rows only render when there's data
217
+ */
218
+ it('should find row elements with rowGeneric pattern (requires data)', () => {
219
+ cy.get('body').then(($body) => {
220
+ if ($body.find(tasks.selectors.rowGeneric).length > 0) {
221
+ cy.get(tasks.selectors.rowGeneric).should('have.length.at.least', 1)
222
+ } else {
223
+ cy.log('⚠️ Skipping: No data - empty state showing')
224
+ }
225
+ })
226
+ })
227
+
228
+ /**
229
+ * Tests dynamic selectors: row, rowSelect, rowMenu
230
+ * NOTE: Requires data to exist
231
+ */
232
+ it('should find row with dynamic ID selectors (requires data)', () => {
233
+ cy.get('body').then(($body) => {
234
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
235
+ cy.log('⚠️ Skipping: No data - empty state showing')
236
+ return
237
+ }
238
+
239
+ cy.get(tasks.selectors.rowGeneric)
240
+ .first()
241
+ .invoke('attr', 'data-cy')
242
+ .then((dataCy) => {
243
+ const id = dataCy?.replace('tasks-row-', '') || ''
244
+ expect(id).to.not.be.empty
245
+
246
+ // Selector: entities.list.table.row.element
247
+ cy.get(tasks.selectors.row(id)).should('exist')
248
+
249
+ // Selector: entities.list.table.row.checkbox
250
+ cy.get(tasks.selectors.rowSelect(id)).should('exist')
251
+
252
+ // Selector: entities.list.table.row.menu
253
+ cy.get(tasks.selectors.rowMenu(id)).should('exist')
254
+ })
255
+ })
256
+ })
257
+
258
+ /**
259
+ * Selector: entities.list.table.row.action (dynamic)
260
+ * POM: tasks.selectors.rowAction(action, id)
261
+ * data-cy: tasks-action-{action}-{id}
262
+ * NOTE: Requires data to exist
263
+ */
264
+ it('should find row action selectors in dropdown (requires data)', () => {
265
+ cy.get('body').then(($body) => {
266
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
267
+ cy.log('⚠️ Skipping: No data - empty state showing')
268
+ return
269
+ }
270
+
271
+ cy.get(tasks.selectors.rowGeneric)
272
+ .first()
273
+ .invoke('attr', 'data-cy')
274
+ .then((dataCy) => {
275
+ const id = dataCy?.replace('tasks-row-', '') || ''
276
+
277
+ // Open row menu
278
+ cy.get(tasks.selectors.rowMenu(id)).click()
279
+
280
+ // Check action selectors exist
281
+ cy.get(tasks.selectors.rowAction('view', id)).should('exist')
282
+ cy.get(tasks.selectors.rowAction('edit', id)).should('exist')
283
+ cy.get(tasks.selectors.rowAction('delete', id)).should('exist')
284
+
285
+ // Close menu by pressing Escape
286
+ cy.get('body').type('{esc}')
287
+ })
288
+ })
289
+ })
60
290
  })
61
291
 
62
- it('should find pagination controls', () => {
63
- cy.get(tasks.selectors.pageFirst).should('exist')
64
- cy.get(tasks.selectors.pagePrev).should('exist')
65
- cy.get(tasks.selectors.pageNext).should('exist')
66
- cy.get(tasks.selectors.pageLast).should('exist')
292
+ // ---------------------------------------------------------------------------
293
+ // 2.4 PAGINATION - Pagination controls
294
+ // NOTE: Pagination only renders when there's data (not in empty state)
295
+ // ---------------------------------------------------------------------------
296
+ describe('SEL_TASK_LIST_004: Pagination Selectors', { tags: '@SEL_TASK_LIST_004' }, () => {
297
+ beforeEach(() => {
298
+ tasks.visitList()
299
+ tasks.waitForList()
300
+ })
301
+
302
+ /**
303
+ * Selector: entities.list.pagination.container
304
+ * POM: tasks.selectors.pagination
305
+ * data-cy: tasks-pagination
306
+ * NOTE: Requires data to exist
307
+ */
308
+ it('should find pagination container (requires data)', () => {
309
+ cy.get('body').then(($body) => {
310
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
311
+ cy.log('⚠️ Skipping: No data - pagination not rendered in empty state')
312
+ return
313
+ }
314
+ cy.get(tasks.selectors.pagination).should('exist')
315
+ })
316
+ })
317
+
318
+ /**
319
+ * Selector: entities.list.pagination.info
320
+ * POM: tasks.selectors.pageInfo
321
+ * data-cy: tasks-page-info
322
+ * NOTE: Requires data to exist
323
+ */
324
+ it('should find page info (requires data)', () => {
325
+ cy.get('body').then(($body) => {
326
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
327
+ cy.log('⚠️ Skipping: No data - pagination not rendered in empty state')
328
+ return
329
+ }
330
+ cy.get(tasks.selectors.pageInfo).should('exist')
331
+ })
332
+ })
333
+
334
+ /**
335
+ * Selector: entities.list.pagination.pageSize
336
+ * POM: tasks.selectors.pageSize
337
+ * data-cy: tasks-page-size
338
+ * NOTE: Requires data to exist
339
+ */
340
+ it('should find page size selector (requires data)', () => {
341
+ cy.get('body').then(($body) => {
342
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
343
+ cy.log('⚠️ Skipping: No data - pagination not rendered in empty state')
344
+ return
345
+ }
346
+ cy.get(tasks.selectors.pageSize).should('exist')
347
+ })
348
+ })
349
+
350
+ /**
351
+ * Selectors: entities.list.pagination.{first,prev,next,last}
352
+ * NOTE: Requires data to exist
353
+ */
354
+ it('should find pagination navigation controls (requires data)', () => {
355
+ cy.get('body').then(($body) => {
356
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
357
+ cy.log('⚠️ Skipping: No data - pagination not rendered in empty state')
358
+ return
359
+ }
360
+ cy.get(tasks.selectors.pageFirst).should('exist')
361
+ cy.get(tasks.selectors.pagePrev).should('exist')
362
+ cy.get(tasks.selectors.pageNext).should('exist')
363
+ cy.get(tasks.selectors.pageLast).should('exist')
364
+ })
365
+ })
366
+
367
+ /**
368
+ * Selector: entities.list.pagination.pageSizeOption (dynamic)
369
+ * POM: tasks.selectors.pageSizeOption(size)
370
+ * data-cy: tasks-page-size-{size}
371
+ * NOTE: Requires data to exist
372
+ */
373
+ it('should find page size options when dropdown opened (requires data)', () => {
374
+ cy.get('body').then(($body) => {
375
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
376
+ cy.log('⚠️ Skipping: No data - pagination not rendered in empty state')
377
+ return
378
+ }
379
+ cy.get(tasks.selectors.pageSize).click()
380
+ cy.get(tasks.selectors.pageSizeOption('10')).should('exist')
381
+ cy.get(tasks.selectors.pageSizeOption('20')).should('exist')
382
+ cy.get('body').type('{esc}')
383
+ })
384
+ })
67
385
  })
68
386
 
69
- it('should find page size selector', () => {
70
- cy.get(tasks.selectors.pageSize).should('exist')
387
+ // ---------------------------------------------------------------------------
388
+ // 2.5 FILTERS - Filter controls
389
+ // ---------------------------------------------------------------------------
390
+ describe('SEL_TASK_LIST_005: Filter Selectors', { tags: '@SEL_TASK_LIST_005' }, () => {
391
+ beforeEach(() => {
392
+ tasks.visitList()
393
+ tasks.waitForList()
394
+ })
395
+
396
+ /**
397
+ * Selector: entities.list.filters.trigger
398
+ * POM: tasks.selectors.filterTrigger(field)
399
+ * data-cy: tasks-filter-{field}
400
+ */
401
+ it('should find status filter trigger', () => {
402
+ cy.get(tasks.selectors.filterTrigger('status')).should('exist')
403
+ })
404
+
405
+ it('should find priority filter trigger', () => {
406
+ cy.get(tasks.selectors.filterTrigger('priority')).should('exist')
407
+ })
408
+
409
+ /**
410
+ * Selector: entities.list.filters.content
411
+ * POM: tasks.selectors.filterContent(field)
412
+ * data-cy: tasks-filter-{field}-content
413
+ */
414
+ it('should find filter content when opened', () => {
415
+ tasks.openFilter('status')
416
+ cy.get(tasks.selectors.filterContent('status')).should('be.visible')
417
+ cy.get('body').type('{esc}')
418
+ })
419
+
420
+ /**
421
+ * Selector: entities.list.filters.option
422
+ * POM: tasks.selectors.filterOption(field, value)
423
+ * data-cy: tasks-filter-{field}-{value}
424
+ * NOTE: Status values are: todo, in-progress, review, done, blocked
425
+ */
426
+ it('should find filter options', () => {
427
+ tasks.openFilter('status')
428
+ cy.get(tasks.selectors.filterOption('status', 'todo')).should('exist')
429
+ cy.get('body').type('{esc}')
430
+ })
431
+
432
+ /**
433
+ * Selector: entities.list.filters.clearAll
434
+ * POM: tasks.selectors.filterClearAll(field)
435
+ * data-cy: tasks-filter-{field}-clear-all
436
+ * NOTE: Clear button only appears when >1 option is selected
437
+ */
438
+ it('should find filter clear button when multiple options selected', () => {
439
+ // Open status filter
440
+ tasks.openFilter('status')
441
+
442
+ // Select 2 options to make clear button appear
443
+ cy.get(tasks.selectors.filterOption('status', 'todo')).click()
444
+ cy.get(tasks.selectors.filterOption('status', 'in-progress')).click()
445
+
446
+ // Close dropdown
447
+ cy.get('body').type('{esc}')
448
+
449
+ // Clear button should now exist
450
+ cy.get(tasks.selectors.filterClearAll('status')).should('exist')
451
+
452
+ // Click to clear and verify it disappears
453
+ cy.get(tasks.selectors.filterClearAll('status')).click()
454
+ cy.get(tasks.selectors.filterClearAll('status')).should('not.exist')
455
+ })
71
456
  })
72
457
 
73
- it('should find page info', () => {
74
- cy.get(tasks.selectors.pageInfo).should('exist')
458
+ // ---------------------------------------------------------------------------
459
+ // 2.6 BULK - Bulk action selectors
460
+ // ---------------------------------------------------------------------------
461
+ describe('SEL_TASK_LIST_006: Bulk Action Selectors', { tags: '@SEL_TASK_LIST_006' }, () => {
462
+ beforeEach(() => {
463
+ tasks.visitList()
464
+ tasks.waitForList()
465
+ })
466
+
467
+ /**
468
+ * Selectors: entities.list.bulk.{bar,count,delete,clear}
469
+ * Bulk bar appears after selecting items
470
+ * NOTE: Requires data to exist
471
+ */
472
+ it('should show bulk bar selectors after selecting rows (requires data)', () => {
473
+ cy.get('body').then(($body) => {
474
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
475
+ cy.log('⚠️ Skipping: No data - empty state showing')
476
+ return
477
+ }
478
+
479
+ cy.get(tasks.selectors.rowGeneric)
480
+ .first()
481
+ .invoke('attr', 'data-cy')
482
+ .then((dataCy) => {
483
+ const id = dataCy?.replace('tasks-row-', '') || ''
484
+ cy.get(tasks.selectors.rowSelect(id)).click()
485
+
486
+ // Selector: entities.list.bulk.bar
487
+ cy.get(tasks.selectors.bulkBar).should('be.visible')
488
+
489
+ // Selector: entities.list.bulk.count
490
+ cy.get(tasks.selectors.bulkCount).should('exist')
491
+
492
+ // Selector: entities.list.bulk.deleteButton
493
+ cy.get(tasks.selectors.bulkDelete).should('exist')
494
+
495
+ // Selector: entities.list.bulk.clearButton
496
+ cy.get(tasks.selectors.bulkClear).should('exist')
497
+ })
498
+ })
499
+ })
500
+
501
+ /**
502
+ * Selector: entities.list.bulk.statusButton
503
+ * POM: tasks.selectors.bulkStatus
504
+ * NOTE: enableChangeStatus not enabled in EntityListWrapper for tasks
505
+ */
506
+ it.skip('should find bulk status button (enableChangeStatus not enabled)', () => {
507
+ cy.get(tasks.selectors.bulkStatus).should('exist')
508
+ })
509
+
510
+ /**
511
+ * Selectors: entities.list.bulk.deleteDialog, deleteCancel, deleteConfirm
512
+ * NOTE: Requires data to exist
513
+ */
514
+ it('should show bulk delete dialog selectors (requires data)', () => {
515
+ cy.get('body').then(($body) => {
516
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
517
+ cy.log('⚠️ Skipping: No data - empty state showing')
518
+ return
519
+ }
520
+
521
+ cy.get(tasks.selectors.rowGeneric)
522
+ .first()
523
+ .invoke('attr', 'data-cy')
524
+ .then((dataCy) => {
525
+ const id = dataCy?.replace('tasks-row-', '') || ''
526
+ cy.get(tasks.selectors.rowSelect(id)).click()
527
+
528
+ // Open bulk delete dialog
529
+ cy.get(tasks.selectors.bulkDelete).click()
530
+
531
+ // Selector: entities.list.bulk.deleteDialog
532
+ cy.get(tasks.selectors.bulkDeleteDialog).should('be.visible')
533
+
534
+ // Selector: entities.list.bulk.deleteCancel
535
+ cy.get(tasks.selectors.bulkDeleteCancel).should('exist')
536
+
537
+ // Selector: entities.list.bulk.deleteConfirm
538
+ cy.get(tasks.selectors.bulkDeleteConfirm).should('exist')
539
+
540
+ // Cancel without deleting
541
+ cy.get(tasks.selectors.bulkDeleteCancel).click()
542
+ })
543
+ })
544
+ })
75
545
  })
76
546
 
77
- it('should find at least one row with dynamic selector', () => {
78
- cy.get(tasks.selectors.rowGeneric).should('have.length.at.least', 1)
547
+ // ---------------------------------------------------------------------------
548
+ // 2.7 CONFIRM - Row action confirm dialogs
549
+ // ---------------------------------------------------------------------------
550
+ describe('SEL_TASK_LIST_007: Confirm Dialog Selectors', { tags: '@SEL_TASK_LIST_007' }, () => {
551
+ /**
552
+ * Selectors: entities.list.confirm.{dialog,cancel,action}
553
+ * Confirm dialog appears for row delete action
554
+ * NOTE: Requires data to exist
555
+ */
556
+ it('should show confirm dialog selectors for row delete (requires data)', () => {
557
+ tasks.visitList()
558
+ tasks.waitForList()
559
+
560
+ cy.get('body').then(($body) => {
561
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
562
+ cy.log('⚠️ Skipping: No data - empty state showing')
563
+ return
564
+ }
565
+
566
+ cy.get(tasks.selectors.rowGeneric)
567
+ .first()
568
+ .invoke('attr', 'data-cy')
569
+ .then((dataCy) => {
570
+ const id = dataCy?.replace('tasks-row-', '') || ''
571
+
572
+ // Open row menu and click delete
573
+ cy.get(tasks.selectors.rowMenu(id)).click()
574
+ cy.get(tasks.selectors.rowAction('delete', id)).click()
575
+
576
+ // Selector: entities.list.confirm.dialog
577
+ cy.get(tasks.selectors.confirmDialog).should('be.visible')
578
+
579
+ // Selector: entities.list.confirm.cancel
580
+ cy.get(tasks.selectors.confirmCancel).should('exist')
581
+
582
+ // Selector: entities.list.confirm.action
583
+ cy.get(tasks.selectors.confirmAction).should('exist')
584
+
585
+ // Cancel without deleting
586
+ cy.get(tasks.selectors.confirmCancel).click()
587
+ })
588
+ })
589
+ })
79
590
  })
80
591
  })
81
592
 
82
- describe('SEL_TASK_002: Filter Selectors', { tags: '@SEL_TASK_002' }, () => {
83
- beforeEach(() => {
84
- tasks.visitList()
85
- tasks.waitForList()
86
- })
87
-
88
- it('should find status filter trigger', () => {
89
- cy.get(tasks.selectors.filterTrigger('status')).should('exist')
593
+ // ═══════════════════════════════════════════════════════════════════════════════
594
+ // 3. HEADER - Entity detail header
595
+ // ═══════════════════════════════════════════════════════════════════════════════
596
+ describe('SEL_TASK_HEADER_001: Header Selectors', { tags: '@SEL_TASK_HEADER_001' }, () => {
597
+ /**
598
+ * Header selectors for CREATE mode
599
+ * Selectors: entities.header.container (mode: create), backButton
600
+ */
601
+ describe('Create Mode', () => {
602
+ beforeEach(() => {
603
+ tasks.visitCreate()
604
+ tasks.waitForForm()
605
+ })
606
+
607
+ /**
608
+ * Selector: entities.header.container (mode: create)
609
+ * POM: tasks.selectors.createHeader
610
+ * data-cy: tasks-create-header
611
+ */
612
+ it('should find create header', () => {
613
+ cy.get(tasks.selectors.createHeader).should('exist')
614
+ })
615
+
616
+ /**
617
+ * Selector: entities.header.backButton
618
+ * POM: tasks.selectors.backButton
619
+ * data-cy: tasks-back
620
+ */
621
+ it('should find back button', () => {
622
+ cy.get(tasks.selectors.backButton).should('exist')
623
+ })
624
+
625
+ /**
626
+ * Selector: entities.header.title
627
+ * POM: tasks.selectors.title
628
+ * data-cy: tasks-header-title
629
+ */
630
+ it('should find header title', () => {
631
+ cy.get(tasks.selectors.title).should('exist')
632
+ })
90
633
  })
91
634
 
92
- it('should find priority filter trigger', () => {
93
- cy.get(tasks.selectors.filterTrigger('priority')).should('exist')
635
+ /**
636
+ * Header selectors for VIEW mode
637
+ * Selectors: entities.header.container (mode: view), backButton, editButton, deleteButton
638
+ * NOTE: Requires data to exist
639
+ */
640
+ describe('View Mode', () => {
641
+ it('should find view header selectors (requires data)', () => {
642
+ tasks.visitList()
643
+ tasks.waitForList()
644
+
645
+ cy.get('body').then(($body) => {
646
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
647
+ cy.log('⚠️ Skipping: No data - empty state showing')
648
+ return
649
+ }
650
+
651
+ cy.get(tasks.selectors.rowGeneric)
652
+ .first()
653
+ .invoke('attr', 'data-cy')
654
+ .then((dataCy) => {
655
+ const id = dataCy?.replace('tasks-row-', '') || ''
656
+
657
+ tasks.visitDetail(id)
658
+ tasks.waitForDetail()
659
+
660
+ // Selector: entities.header.container (mode: view)
661
+ cy.get(tasks.selectors.viewHeader).should('exist')
662
+
663
+ // Selector: entities.header.backButton
664
+ cy.get(tasks.selectors.backButton).should('exist')
665
+
666
+ // Selector: entities.header.editButton
667
+ cy.get(tasks.selectors.editButton).should('exist')
668
+
669
+ // Selector: entities.header.deleteButton
670
+ cy.get(tasks.selectors.deleteButton).should('exist')
671
+ })
672
+ })
673
+ })
674
+
675
+ /**
676
+ * Selectors: entities.header.{deleteDialog,deleteCancel,deleteConfirm}
677
+ * NOTE: Requires data to exist
678
+ */
679
+ it('should find header delete dialog selectors (requires data)', () => {
680
+ tasks.visitList()
681
+ tasks.waitForList()
682
+
683
+ cy.get('body').then(($body) => {
684
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
685
+ cy.log('⚠️ Skipping: No data - empty state showing')
686
+ return
687
+ }
688
+
689
+ cy.get(tasks.selectors.rowGeneric)
690
+ .first()
691
+ .invoke('attr', 'data-cy')
692
+ .then((dataCy) => {
693
+ const id = dataCy?.replace('tasks-row-', '') || ''
694
+
695
+ tasks.visitDetail(id)
696
+ tasks.waitForDetail()
697
+
698
+ // Click delete to open dialog
699
+ tasks.clickDelete()
700
+
701
+ // Selector: entities.header.deleteDialog
702
+ cy.get(tasks.selectors.deleteDialog).should('be.visible')
703
+
704
+ // Selector: entities.header.deleteConfirm
705
+ cy.get(tasks.selectors.deleteConfirm).should('exist')
706
+
707
+ // Selector: entities.header.deleteCancel
708
+ cy.get(tasks.selectors.deleteCancel).should('exist')
709
+
710
+ // Cancel without deleting
711
+ tasks.cancelDelete()
712
+ })
713
+ })
714
+ })
94
715
  })
95
716
 
96
- it('should find filter options when opened', () => {
97
- tasks.openFilter('status')
98
- cy.get(tasks.selectors.filterContent('status')).should('be.visible')
717
+ /**
718
+ * Header selectors for EDIT mode
719
+ * Selectors: entities.header.container (mode: edit), backButton
720
+ * NOTE: Requires data to exist
721
+ */
722
+ describe('Edit Mode', () => {
723
+ it('should find edit header selectors (requires data)', () => {
724
+ tasks.visitList()
725
+ tasks.waitForList()
726
+
727
+ cy.get('body').then(($body) => {
728
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
729
+ cy.log('⚠️ Skipping: No data - empty state showing')
730
+ return
731
+ }
732
+
733
+ cy.get(tasks.selectors.rowGeneric)
734
+ .first()
735
+ .invoke('attr', 'data-cy')
736
+ .then((dataCy) => {
737
+ const id = dataCy?.replace('tasks-row-', '') || ''
738
+
739
+ tasks.visitEdit(id)
740
+ tasks.waitForForm()
741
+
742
+ // Selector: entities.header.container (mode: edit)
743
+ cy.get(tasks.selectors.editHeader).should('exist')
744
+
745
+ // Selector: entities.header.backButton
746
+ cy.get(tasks.selectors.backButton).should('exist')
747
+ })
748
+ })
749
+ })
99
750
  })
100
751
  })
101
752
 
102
- describe('SEL_TASK_003: Row Dynamic Selectors', { tags: '@SEL_TASK_003' }, () => {
103
- beforeEach(() => {
753
+ // ═══════════════════════════════════════════════════════════════════════════════
754
+ // 4. DETAIL - Detail view container
755
+ // ═══════════════════════════════════════════════════════════════════════════════
756
+ describe('SEL_TASK_DETAIL_001: Detail Container', { tags: '@SEL_TASK_DETAIL_001' }, () => {
757
+ /**
758
+ * Selector: entities.detail.container
759
+ * POM: tasks.selectors.detail
760
+ * data-cy: tasks-detail
761
+ * NOTE: Requires data to exist
762
+ */
763
+ it('should find detail container (requires data)', () => {
104
764
  tasks.visitList()
105
765
  tasks.waitForList()
106
- })
107
766
 
108
- it('should find row elements with dynamic ID', () => {
109
- // Get any row and extract its ID to test dynamic selectors
110
- cy.get(tasks.selectors.rowGeneric)
111
- .first()
112
- .invoke('attr', 'data-cy')
113
- .then((dataCy) => {
114
- // Extract ID from data-cy="tasks-row-{id}"
115
- const id = dataCy?.replace('tasks-row-', '') || ''
116
- expect(id).to.not.be.empty
767
+ cy.get('body').then(($body) => {
768
+ if ($body.find(tasks.selectors.rowGeneric).length === 0) {
769
+ cy.log('⚠️ Skipping: No data - empty state showing')
770
+ return
771
+ }
117
772
 
118
- // Test dynamic selector functions work
119
- cy.get(tasks.selectors.row(id)).should('exist')
120
- cy.get(tasks.selectors.rowSelect(id)).should('exist')
121
- cy.get(tasks.selectors.rowMenu(id)).should('exist')
122
- })
773
+ cy.get(tasks.selectors.rowGeneric)
774
+ .first()
775
+ .invoke('attr', 'data-cy')
776
+ .then((dataCy) => {
777
+ const id = dataCy?.replace('tasks-row-', '') || ''
778
+
779
+ tasks.visitDetail(id)
780
+ tasks.waitForDetail()
781
+
782
+ cy.get(tasks.selectors.detail).should('exist')
783
+ })
784
+ })
123
785
  })
124
786
  })
125
787
 
126
- describe('SEL_TASK_004: Create Page Selectors', { tags: '@SEL_TASK_004' }, () => {
788
+ // ═══════════════════════════════════════════════════════════════════════════════
789
+ // 5. FORM - Form selectors
790
+ // ═══════════════════════════════════════════════════════════════════════════════
791
+ describe('SEL_TASK_FORM_001: Form Selectors', { tags: '@SEL_TASK_FORM_001' }, () => {
127
792
  beforeEach(() => {
128
793
  tasks.visitCreate()
129
794
  tasks.waitForForm()
130
795
  })
131
796
 
797
+ /**
798
+ * Selector: entities.form.container
799
+ * POM: tasks.selectors.form
800
+ * data-cy: tasks-form
801
+ */
132
802
  it('should find form container', () => {
133
803
  cy.get(tasks.selectors.form).should('exist')
134
804
  })
135
805
 
806
+ /**
807
+ * Selector: entities.form.submitButton
808
+ * POM: tasks.selectors.submitButton
809
+ * data-cy: tasks-form-submit
810
+ */
136
811
  it('should find submit button', () => {
137
812
  cy.get(tasks.selectors.submitButton).should('exist')
138
813
  })
139
814
 
140
- it('should find create header', () => {
141
- cy.get(tasks.selectors.createHeader).should('exist')
142
- })
143
-
144
- it('should find back button', () => {
145
- cy.get(tasks.selectors.backButton).should('exist')
146
- })
147
-
148
- it('should find title field', () => {
149
- cy.get(tasks.selectors.field('title')).should('exist')
150
- })
151
-
152
- it('should find description field', () => {
153
- cy.get(tasks.selectors.field('description')).should('exist')
154
- })
155
-
156
- it('should find status field', () => {
157
- cy.get(tasks.selectors.field('status')).should('exist')
158
- })
159
-
160
- it('should find priority field', () => {
161
- cy.get(tasks.selectors.field('priority')).should('exist')
815
+ /**
816
+ * Selector: entities.form.field (dynamic)
817
+ * POM: tasks.selectors.field(name)
818
+ * data-cy: tasks-field-{name}
819
+ */
820
+ describe('Field Selectors', () => {
821
+ it('should find title field', () => {
822
+ cy.get(tasks.selectors.field('title')).should('exist')
823
+ })
824
+
825
+ it('should find description field', () => {
826
+ cy.get(tasks.selectors.field('description')).should('exist')
827
+ })
828
+
829
+ it('should find status field', () => {
830
+ cy.get(tasks.selectors.field('status')).should('exist')
831
+ })
832
+
833
+ it('should find priority field', () => {
834
+ cy.get(tasks.selectors.field('priority')).should('exist')
835
+ })
162
836
  })
163
837
  })
164
838
 
165
- describe('SEL_TASK_005: Detail Page Selectors', { tags: '@SEL_TASK_005' }, () => {
166
- it('should find detail page elements after navigating to a task', () => {
167
- // First get a task ID from the list
168
- tasks.visitList()
169
- tasks.waitForList()
170
-
171
- cy.get(tasks.selectors.rowGeneric)
172
- .first()
173
- .invoke('attr', 'data-cy')
174
- .then((dataCy) => {
175
- const id = dataCy?.replace('tasks-row-', '') || ''
176
-
177
- // Navigate to detail page
178
- tasks.visitDetail(id)
179
- tasks.waitForDetail()
180
-
181
- // Validate detail page selectors
182
- cy.get(tasks.selectors.viewHeader).should('exist')
183
- cy.get(tasks.selectors.editButton).should('exist')
184
- cy.get(tasks.selectors.deleteButton).should('exist')
185
- cy.get(tasks.selectors.backButton).should('exist')
186
- })
187
- })
188
- })
189
-
190
- describe('SEL_TASK_006: Bulk Actions Selectors', { tags: '@SEL_TASK_006' }, () => {
191
- beforeEach(() => {
192
- tasks.visitList()
193
- tasks.waitForList()
194
- })
195
-
196
- it('should show bulk bar after selecting rows', () => {
197
- // Select first row
198
- cy.get(tasks.selectors.rowGeneric)
199
- .first()
200
- .invoke('attr', 'data-cy')
201
- .then((dataCy) => {
202
- const id = dataCy?.replace('tasks-row-', '') || ''
203
- cy.get(tasks.selectors.rowSelect(id)).click()
204
-
205
- // Bulk bar should appear
206
- cy.get(tasks.selectors.bulkBar).should('be.visible')
207
- cy.get(tasks.selectors.bulkCount).should('exist')
208
- cy.get(tasks.selectors.bulkDelete).should('exist')
209
- cy.get(tasks.selectors.bulkClear).should('exist')
210
- // Note: bulkStatus not tested - enableChangeStatus not enabled in EntityListWrapper
211
- })
212
- })
213
- })
214
-
215
- describe('SEL_TASK_007: Delete Dialog Selectors', { tags: '@SEL_TASK_007' }, () => {
216
- it('should find delete dialog elements', () => {
217
- // Navigate to a task detail
218
- tasks.visitList()
219
- tasks.waitForList()
220
-
221
- cy.get(tasks.selectors.rowGeneric)
222
- .first()
223
- .invoke('attr', 'data-cy')
224
- .then((dataCy) => {
225
- const id = dataCy?.replace('tasks-row-', '') || ''
226
-
227
- tasks.visitDetail(id)
228
- tasks.waitForDetail()
229
-
230
- // Click delete to open dialog
231
- tasks.clickDelete()
232
-
233
- // Validate dialog selectors
234
- cy.get(tasks.selectors.deleteDialog).should('be.visible')
235
- cy.get(tasks.selectors.deleteConfirm).should('exist')
236
- cy.get(tasks.selectors.deleteCancel).should('exist')
237
-
238
- // Close without deleting
239
- tasks.cancelDelete()
240
- })
241
- })
242
- })
839
+ // ═══════════════════════════════════════════════════════════════════════════════
840
+ // 6. CHILD ENTITY - Child entity management (not applicable for tasks)
841
+ // ═══════════════════════════════════════════════════════════════════════════════
842
+ // NOTE: Tasks entity does not have child entities.
843
+ // If it did, tests would follow this pattern:
844
+ //
845
+ // describe('SEL_TASK_CHILD_001: Child Entity Selectors', { tags: '@SEL_TASK_CHILD_001' }, () => {
846
+ // /**
847
+ // * Selector: entities.childEntity.container
848
+ // * POM: tasks.selectors.childEntityContainer(childName)
849
+ // * data-cy: tasks-{childName}-section
850
+ // */
851
+ // it('should find child entity container', () => {
852
+ // cy.get(tasks.selectors.childEntityContainer('subtasks')).should('exist')
853
+ // })
854
+ //
855
+ // /**
856
+ // * Selector: entities.childEntity.addButton
857
+ // * POM: tasks.selectors.childEntityAddButton(childName)
858
+ // * data-cy: tasks-{childName}-add
859
+ // */
860
+ // it('should find child entity add button', () => {
861
+ // cy.get(tasks.selectors.childEntityAddButton('subtasks')).should('exist')
862
+ // })
863
+ // })
243
864
  })