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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/config/app.config.ts +4 -5
  2. package/config/billing.config.ts +4 -7
  3. package/config/dashboard.config.ts +13 -0
  4. package/config/permissions.config.ts +11 -0
  5. package/entities/boards/api/docs.md +131 -0
  6. package/entities/boards/api/presets.ts +113 -0
  7. package/entities/cards/api/docs.md +162 -0
  8. package/entities/cards/api/presets.ts +172 -0
  9. package/entities/lists/api/docs.md +143 -0
  10. package/entities/lists/api/presets.ts +81 -0
  11. package/lib/selectors.ts +2 -3
  12. package/nextsparkjs-theme-productivity-0.1.0-beta.137.tgz +0 -0
  13. package/package.json +4 -3
  14. package/styles/globals.css +37 -1
  15. package/tests/cypress/e2e/ui/boards/boards-owner.cy.ts +349 -0
  16. package/tests/cypress/e2e/ui/cards/cards-modal.cy.ts +369 -0
  17. package/tests/cypress/e2e/ui/kanban/kanban-cards.cy.ts +280 -0
  18. package/tests/cypress/e2e/ui/kanban/kanban-columns.cy.ts +243 -0
  19. package/tests/cypress/fixtures/blocks.json +9 -0
  20. package/tests/cypress/fixtures/entities.json +60 -0
  21. package/tests/cypress/src/components/BoardsPOM.ts +353 -0
  22. package/tests/cypress/src/components/CardsPOM.ts +383 -0
  23. package/tests/cypress/src/components/KanbanPOM.ts +399 -0
  24. package/tests/cypress/src/components/index.ts +9 -0
  25. package/tests/cypress/src/controllers/BoardsAPIController.js +302 -0
  26. package/tests/cypress/src/controllers/CardsAPIController.js +406 -0
  27. package/tests/cypress/src/controllers/ListsAPIController.js +299 -0
  28. package/tests/cypress/src/index.ts +25 -0
  29. package/tests/cypress/src/selectors.ts +50 -0
  30. package/tests/cypress/src/session-helpers.ts +176 -0
  31. package/tests/cypress/support/e2e.ts +90 -0
  32. package/tests/cypress.config.ts +154 -0
  33. package/tests/jest/__mocks__/jose.js +22 -0
  34. package/tests/jest/__mocks__/next-server.js +56 -0
  35. package/tests/jest/jest.config.cjs +131 -0
  36. package/tests/jest/setup.ts +170 -0
  37. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,143 @@
1
+ # Lists API
2
+
3
+ Manage columns within boards (To Do, In Progress, Done, etc.).
4
+
5
+ ## Overview
6
+
7
+ The Lists API allows you to create, read, update, and delete list records. Lists are the columns within a board that organize cards in a Kanban workflow.
8
+
9
+ ## Authentication
10
+
11
+ All endpoints require authentication via:
12
+ - **Session cookie** (for browser-based requests)
13
+ - **API Key** header (for server-to-server requests)
14
+
15
+ ## Endpoints
16
+
17
+ ### List Lists
18
+ `GET /api/v1/lists`
19
+
20
+ Returns a paginated list of lists.
21
+
22
+ **Query Parameters:**
23
+ - `limit` (number, optional): Maximum records to return. Default: 20
24
+ - `offset` (number, optional): Number of records to skip. Default: 0
25
+ - `boardId` (string, optional): Filter by parent board
26
+ - `search` (string, optional): Search by name
27
+ - `sortBy` (string, optional): Field to sort by. Default: position
28
+ - `sortOrder` (string, optional): Sort direction (asc, desc)
29
+
30
+ **Example Response:**
31
+ ```json
32
+ {
33
+ "data": [
34
+ {
35
+ "id": "list_abc123",
36
+ "name": "To Do",
37
+ "position": 1,
38
+ "boardId": "board_xyz789",
39
+ "createdAt": "2024-01-15T10:30:00Z",
40
+ "updatedAt": "2024-01-15T10:30:00Z"
41
+ },
42
+ {
43
+ "id": "list_def456",
44
+ "name": "In Progress",
45
+ "position": 2,
46
+ "boardId": "board_xyz789",
47
+ "createdAt": "2024-01-15T10:30:00Z",
48
+ "updatedAt": "2024-01-15T10:30:00Z"
49
+ },
50
+ {
51
+ "id": "list_ghi789",
52
+ "name": "Done",
53
+ "position": 3,
54
+ "boardId": "board_xyz789",
55
+ "createdAt": "2024-01-15T10:30:00Z",
56
+ "updatedAt": "2024-01-15T10:30:00Z"
57
+ }
58
+ ],
59
+ "pagination": {
60
+ "total": 3,
61
+ "limit": 20,
62
+ "offset": 0
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Get Single List
68
+ `GET /api/v1/lists/[id]`
69
+
70
+ Returns a single list by ID.
71
+
72
+ ### Create List
73
+ `POST /api/v1/lists`
74
+
75
+ Create a new list.
76
+
77
+ **Request Body:**
78
+ ```json
79
+ {
80
+ "name": "Review",
81
+ "boardId": "board_xyz789",
82
+ "position": 4
83
+ }
84
+ ```
85
+
86
+ ### Update List
87
+ `PATCH /api/v1/lists/[id]`
88
+
89
+ Update an existing list. Supports partial updates.
90
+
91
+ ### Reorder Lists
92
+ `PATCH /api/v1/lists/[id]`
93
+
94
+ Update list position for drag & drop reordering.
95
+
96
+ **Request Body:**
97
+ ```json
98
+ {
99
+ "position": 2
100
+ }
101
+ ```
102
+
103
+ ### Delete List
104
+ `DELETE /api/v1/lists/[id]`
105
+
106
+ Delete a list. This will also delete all cards within the list.
107
+
108
+ ## Fields
109
+
110
+ | Field | Type | Required | Default | Description |
111
+ |-------|------|----------|---------|-------------|
112
+ | name | text | Yes | - | List name |
113
+ | position | number | No | 0 | Display order within board |
114
+ | boardId | reference | Yes | - | Parent board ID |
115
+ | createdAt | datetime | Auto | - | Creation timestamp |
116
+ | updatedAt | datetime | Auto | - | Last update timestamp |
117
+
118
+ ## Features
119
+
120
+ - **Searchable**: name
121
+ - **Sortable**: name, position, createdAt
122
+ - **Drag & Drop**: Update position for reordering
123
+ - **Cascade Delete**: Deleting a list removes all its cards
124
+
125
+ ## Permissions
126
+
127
+ - **Create/Update/Delete**: Owner, Admin, Member
128
+ - **Delete**: Owner, Admin
129
+
130
+ ## Error Responses
131
+
132
+ | Status | Description |
133
+ |--------|-------------|
134
+ | 400 | Bad Request - Invalid parameters |
135
+ | 401 | Unauthorized - Missing or invalid auth |
136
+ | 403 | Forbidden - Insufficient permissions |
137
+ | 404 | Not Found - List or Board doesn't exist |
138
+ | 422 | Validation Error - Invalid data |
139
+
140
+ ## Related APIs
141
+
142
+ - **[Boards](/api/v1/boards)** - Parent boards
143
+ - **[Cards](/api/v1/cards)** - Cards within lists
@@ -0,0 +1,81 @@
1
+ /**
2
+ * API Presets for Lists Entity
3
+ */
4
+
5
+ import { defineApiEndpoint } from '@nextsparkjs/core/types/api-presets'
6
+
7
+ export default defineApiEndpoint({
8
+ summary: 'Manage kanban lists (columns) within boards',
9
+ presets: [
10
+ {
11
+ id: 'list-all',
12
+ title: 'List All Lists',
13
+ description: 'Get all lists with pagination',
14
+ method: 'GET',
15
+ params: {
16
+ limit: 50
17
+ },
18
+ tags: ['read', 'list']
19
+ },
20
+ {
21
+ id: 'list-by-board',
22
+ title: 'List by Board',
23
+ description: 'Get all lists for a specific board',
24
+ method: 'GET',
25
+ params: {
26
+ boardId: '{{boardId}}',
27
+ sortBy: 'position',
28
+ sortOrder: 'asc'
29
+ },
30
+ tags: ['read', 'filter']
31
+ },
32
+ {
33
+ id: 'create-list',
34
+ title: 'Create List',
35
+ description: 'Create a new list in a board',
36
+ method: 'POST',
37
+ payload: {
38
+ name: 'New List',
39
+ boardId: '{{boardId}}',
40
+ position: 0
41
+ },
42
+ tags: ['write', 'create']
43
+ },
44
+ {
45
+ id: 'rename-list',
46
+ title: 'Rename List',
47
+ description: 'Rename an existing list',
48
+ method: 'PATCH',
49
+ pathParams: {
50
+ id: '{{id}}'
51
+ },
52
+ payload: {
53
+ name: '{{name}}'
54
+ },
55
+ tags: ['write', 'update']
56
+ },
57
+ {
58
+ id: 'reorder-list',
59
+ title: 'Reorder List',
60
+ description: 'Update list position within board',
61
+ method: 'PATCH',
62
+ pathParams: {
63
+ id: '{{id}}'
64
+ },
65
+ payload: {
66
+ position: '{{position}}'
67
+ },
68
+ tags: ['write', 'update']
69
+ },
70
+ {
71
+ id: 'delete-list',
72
+ title: 'Delete List',
73
+ description: 'Delete a list and all its cards',
74
+ method: 'DELETE',
75
+ pathParams: {
76
+ id: '{{id}}'
77
+ },
78
+ tags: ['write', 'delete']
79
+ }
80
+ ]
81
+ })
package/lib/selectors.ts CHANGED
@@ -9,8 +9,7 @@
9
9
  * - Cypress tests (via tests/cypress/src/selectors.ts)
10
10
  */
11
11
 
12
- import { createSelectorHelpers } from '@nextsparkjs/core/lib/test/selector-factory'
13
- import { CORE_SELECTORS } from '@nextsparkjs/core/lib/test/core-selectors'
12
+ import { createSelectorHelpers, CORE_SELECTORS } from '@nextsparkjs/core/selectors'
14
13
 
15
14
  // =============================================================================
16
15
  // BLOCK SELECTORS
@@ -202,5 +201,5 @@ export type ThemeSelectorsType = typeof THEME_SELECTORS
202
201
  export type BlockSelectorsType = typeof BLOCK_SELECTORS
203
202
  export type EntitySelectorsType = typeof ENTITY_SELECTORS
204
203
  export type KanbanSelectorsType = typeof KANBAN_SELECTORS
205
- export type { Replacements } from '@nextsparkjs/core/lib/test/selector-factory'
204
+ export type { Replacements } from '@nextsparkjs/core/selectors'
206
205
  export { CORE_SELECTORS }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@nextsparkjs/theme-productivity",
3
- "version": "0.1.0-beta.17",
3
+ "version": "0.1.0-beta.170",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./config/theme.config.ts",
7
7
  "requiredPlugins": [],
8
8
  "dependencies": {},
9
9
  "peerDependencies": {
10
+ "@nextsparkjs/core": "*",
11
+ "@nextsparkjs/testing": "*",
10
12
  "lucide-react": "^0.539.0",
11
13
  "next": "^15.0.0",
12
14
  "react": "^19.0.0",
13
15
  "react-dom": "^19.0.0",
14
- "zod": "^4.0.0",
15
- "@nextsparkjs/core": "0.1.0-beta.17"
16
+ "zod": "^4.0.0"
16
17
  },
17
18
  "nextspark": {
18
19
  "type": "theme",
@@ -3,10 +3,40 @@
3
3
  *
4
4
  * Clean, modern design focused on productivity.
5
5
  * Blue-tinted palette with clear visual hierarchy.
6
+ *
7
+ * ÚNICA FUENTE DE VERDAD para estilos del theme.
8
+ * Editar este archivo para customizar el design system.
6
9
  */
7
10
 
11
+ /* =============================================
12
+ IMPORTS
13
+ ============================================= */
14
+ @import "tailwindcss";
15
+
16
+ /* =============================================
17
+ TAILWIND v4 DARK MODE CONFIGURATION
18
+
19
+ Por defecto Tailwind v4 usa @media (prefers-color-scheme: dark)
20
+ para las variantes dark:. Esto causa problemas porque:
21
+ - next-themes usa la clase .dark en el HTML
22
+ - El sistema operativo puede tener dark mode aunque la app use light
23
+
24
+ Esta configuración hace que Tailwind use el selector de clase .dark
25
+ en lugar de la media query, sincronizándose con next-themes.
26
+ ============================================= */
27
+ @custom-variant dark (&:where(.dark, .dark *));
28
+
29
+ @plugin "@tailwindcss/container-queries";
30
+ @import "@nextsparkjs/core/styles/ui.css";
31
+ @import "@nextsparkjs/core/styles/utilities.css";
32
+ @import "@nextsparkjs/core/styles/docs.css";
33
+ @source "../../../**/*.{js,ts,jsx,tsx}";
34
+ /* Core package: monorepo (short path) + npm projects (long path) */
35
+ @source "../node_modules/@nextsparkjs/core/dist/**/*.js";
36
+ @source "../../../../node_modules/@nextsparkjs/core/dist/**/*.js";
37
+
8
38
  /* ============================================================================
9
- Base Theme Variables - Blue Color Scheme
39
+ PRODUCTIVITY THEME - LIGHT MODE
10
40
  ============================================================================ */
11
41
 
12
42
  :root {
@@ -249,7 +279,13 @@
249
279
  Base Styles
250
280
  ============================================================================ */
251
281
 
282
+ * {
283
+ border-color: var(--border);
284
+ }
285
+
252
286
  body {
287
+ background-color: var(--background);
288
+ color: var(--foreground);
253
289
  font-feature-settings: "cv02", "cv03", "cv04", "cv11";
254
290
  }
255
291
 
@@ -0,0 +1,349 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Boards CRUD - Owner Role (Full Access)
5
+ *
6
+ * Tests for the productivity theme boards using the Entity Testing Convention.
7
+ * All selectors follow the pattern: {slug}-{component}-{detail}
8
+ *
9
+ * @see test/cypress/src/classes/themes/productivity/components/BoardsPOM.ts
10
+ */
11
+
12
+ import { BoardsPOM } from '../../../src/components/BoardsPOM'
13
+ import { loginAsProductivityOwner } from '../../../src/session-helpers'
14
+
15
+ describe('Boards CRUD - Owner Role (Full Access)', () => {
16
+ beforeEach(() => {
17
+ loginAsProductivityOwner()
18
+ BoardsPOM.visitList()
19
+ BoardsPOM.waitForListLoad()
20
+ })
21
+
22
+ describe('CREATE - Owner can create boards', () => {
23
+ it('OWNER_BOARDS_CREATE_001: should create new board with required fields', () => {
24
+ const timestamp = Date.now()
25
+ const name = `Test Board ${timestamp}`
26
+ const description = `Test description for board ${timestamp}`
27
+
28
+ // Click create button
29
+ BoardsPOM.clickCreate()
30
+
31
+ // Validate form is visible
32
+ BoardsPOM.waitForFormLoad()
33
+ BoardsPOM.validateFormVisible()
34
+
35
+ // Fill required board fields
36
+ BoardsPOM.fillBoardForm({
37
+ name,
38
+ description,
39
+ })
40
+
41
+ // Submit form
42
+ BoardsPOM.submitForm()
43
+
44
+ // Should redirect to the new board's kanban view
45
+ cy.url().should('match', /\/dashboard\/boards\/[a-z0-9_-]+$/)
46
+
47
+ cy.log('✅ Owner created board successfully')
48
+ })
49
+
50
+ it('OWNER_BOARDS_CREATE_002: should create board with color', () => {
51
+ const timestamp = Date.now()
52
+ const name = `Colored Board ${timestamp}`
53
+
54
+ // Click create button
55
+ BoardsPOM.clickCreate()
56
+ BoardsPOM.waitForFormLoad()
57
+
58
+ // Fill fields with color selection
59
+ BoardsPOM.fillBoardForm({
60
+ name,
61
+ color: 'purple',
62
+ })
63
+
64
+ // Submit form
65
+ BoardsPOM.submitForm()
66
+
67
+ // Should redirect to board
68
+ cy.url().should('match', /\/dashboard\/boards\/[a-z0-9_-]+$/)
69
+
70
+ cy.log('✅ Owner created board with color successfully')
71
+ })
72
+
73
+ it('OWNER_BOARDS_CREATE_003: should validate required fields', () => {
74
+ // Click create button
75
+ BoardsPOM.clickCreate()
76
+ BoardsPOM.waitForFormLoad()
77
+
78
+ // Try to submit without filling required fields
79
+ BoardsPOM.submitForm()
80
+
81
+ // Form should still be visible (validation failed)
82
+ BoardsPOM.validateFormVisible()
83
+
84
+ cy.log('✅ Form validation working correctly')
85
+ })
86
+
87
+ it('OWNER_BOARDS_CREATE_004: should cancel board creation', () => {
88
+ const timestamp = Date.now()
89
+ const name = `Cancelled Board ${timestamp}`
90
+
91
+ // Click create button
92
+ BoardsPOM.clickCreate()
93
+ BoardsPOM.waitForFormLoad()
94
+
95
+ // Fill some fields
96
+ BoardsPOM.fillName(name)
97
+
98
+ // Cancel form
99
+ BoardsPOM.cancelForm()
100
+
101
+ // Should return to list
102
+ BoardsPOM.assertOnListPage()
103
+
104
+ // Board should not exist
105
+ BoardsPOM.assertBoardNotInList(name)
106
+
107
+ cy.log('✅ Board creation cancelled successfully')
108
+ })
109
+ })
110
+
111
+ describe('READ - Owner can view boards', () => {
112
+ it('OWNER_BOARDS_READ_001: should view boards list page', () => {
113
+ // Validate list page is visible
114
+ BoardsPOM.validateListPageVisible()
115
+
116
+ cy.log('✅ Owner can view boards list')
117
+ })
118
+
119
+ it('OWNER_BOARDS_READ_002: should see create button', () => {
120
+ // Create button should be visible for owner
121
+ cy.get(BoardsPOM.selectors.createBtn).should('be.visible')
122
+
123
+ cy.log('✅ Create button is visible for owner')
124
+ })
125
+
126
+ it('OWNER_BOARDS_READ_003: should click on board to open it', () => {
127
+ // Check if there are boards
128
+ cy.get('body').then(($body) => {
129
+ const boardCards = $body.find('[data-cy^="boards-card-"]')
130
+ if (boardCards.length > 0) {
131
+ // Click on first board card
132
+ cy.get('[data-cy^="boards-card-"]').first().find('a').first().click()
133
+
134
+ // Should navigate to board view
135
+ cy.url().should('match', /\/dashboard\/boards\/[a-z0-9_-]+$/)
136
+
137
+ cy.log('✅ Owner can open board')
138
+ } else {
139
+ cy.log('⚠️ No boards available to click')
140
+ }
141
+ })
142
+ })
143
+ })
144
+
145
+ describe('UPDATE - Owner can update boards', () => {
146
+ let testBoardId: string
147
+
148
+ beforeEach(() => {
149
+ // Create a board for update tests
150
+ const timestamp = Date.now()
151
+ const name = `Update Test ${timestamp}`
152
+
153
+ BoardsPOM.clickCreate()
154
+ BoardsPOM.waitForFormLoad()
155
+ BoardsPOM.fillName(name)
156
+ BoardsPOM.submitForm()
157
+
158
+ // Get board ID from URL
159
+ cy.url().then((url) => {
160
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
161
+ if (match) {
162
+ testBoardId = match[1]
163
+ }
164
+ })
165
+ })
166
+
167
+ it('OWNER_BOARDS_UPDATE_001: should edit board name', () => {
168
+ const updatedName = `Updated Board ${Date.now()}`
169
+
170
+ // Visit edit page
171
+ cy.url().then((url) => {
172
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
173
+ if (match) {
174
+ const boardId = match[1]
175
+ BoardsPOM.visitEdit(boardId)
176
+ BoardsPOM.waitForFormLoad()
177
+
178
+ // Update name
179
+ BoardsPOM.fillName(updatedName)
180
+
181
+ // Submit form
182
+ BoardsPOM.submitForm()
183
+
184
+ // Should redirect to board
185
+ cy.url().should('include', `/dashboard/boards/${boardId}`)
186
+
187
+ cy.log('✅ Owner updated board name successfully')
188
+ }
189
+ })
190
+ })
191
+
192
+ it('OWNER_BOARDS_UPDATE_002: should update board description', () => {
193
+ const updatedDescription = `Updated description ${Date.now()}`
194
+
195
+ cy.url().then((url) => {
196
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
197
+ if (match) {
198
+ const boardId = match[1]
199
+ BoardsPOM.visitEdit(boardId)
200
+ BoardsPOM.waitForFormLoad()
201
+
202
+ // Update description
203
+ BoardsPOM.fillDescription(updatedDescription)
204
+
205
+ // Submit form
206
+ BoardsPOM.submitForm()
207
+
208
+ // Should redirect to board
209
+ cy.url().should('include', `/dashboard/boards/${boardId}`)
210
+
211
+ cy.log('✅ Owner updated board description successfully')
212
+ }
213
+ })
214
+ })
215
+ })
216
+
217
+ describe('DELETE - Owner can delete boards', () => {
218
+ it('OWNER_BOARDS_DELETE_001: should delete board from edit page', () => {
219
+ // Create a board to delete
220
+ const timestamp = Date.now()
221
+ const name = `Delete Test ${timestamp}`
222
+
223
+ BoardsPOM.clickCreate()
224
+ BoardsPOM.waitForFormLoad()
225
+ BoardsPOM.fillName(name)
226
+ BoardsPOM.submitForm()
227
+
228
+ // Get board ID and go to edit page
229
+ cy.url().then((url) => {
230
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
231
+ if (match) {
232
+ const boardId = match[1]
233
+ BoardsPOM.visitEdit(boardId)
234
+ BoardsPOM.waitForFormLoad()
235
+
236
+ // Confirm deletion
237
+ BoardsPOM.confirmDelete()
238
+
239
+ // Click delete button
240
+ BoardsPOM.deleteFromEdit()
241
+
242
+ // Should redirect to list
243
+ BoardsPOM.waitForListLoad()
244
+ BoardsPOM.assertOnListPage()
245
+
246
+ // Board should not exist
247
+ BoardsPOM.assertBoardNotInList(name)
248
+
249
+ cy.log('✅ Owner deleted board successfully')
250
+ }
251
+ })
252
+ })
253
+
254
+ it('OWNER_BOARDS_DELETE_002: should delete board from list page menu', () => {
255
+ // Create a board to delete
256
+ const timestamp = Date.now()
257
+ const name = `Delete Menu Test ${timestamp}`
258
+
259
+ BoardsPOM.clickCreate()
260
+ BoardsPOM.waitForFormLoad()
261
+ BoardsPOM.fillName(name)
262
+ BoardsPOM.submitForm()
263
+
264
+ // Go back to list
265
+ BoardsPOM.visitList()
266
+ BoardsPOM.waitForListLoad()
267
+
268
+ // Find the board and open its menu
269
+ cy.get('body').then(($body) => {
270
+ // Find the board card containing our name
271
+ cy.contains('[data-cy^="boards-card-"]', name).then(($card) => {
272
+ // Extract ID from data-cy attribute
273
+ const dataCy = $card.attr('data-cy')
274
+ if (dataCy) {
275
+ const boardId = dataCy.replace('boards-card-', '')
276
+
277
+ // Confirm deletion
278
+ BoardsPOM.confirmDelete()
279
+
280
+ // Click delete from menu
281
+ BoardsPOM.clickCardDelete(boardId)
282
+
283
+ // Board should no longer exist
284
+ cy.contains('[data-cy^="boards-card-"]', name).should('not.exist')
285
+
286
+ cy.log('✅ Owner deleted board from list menu')
287
+ }
288
+ })
289
+ })
290
+ })
291
+ })
292
+
293
+ describe('INTEGRATION - Complete board lifecycle', () => {
294
+ it('OWNER_BOARDS_LIFECYCLE_001: should perform full CRUD lifecycle', () => {
295
+ const timestamp = Date.now()
296
+ const name = `Lifecycle Board ${timestamp}`
297
+ const description = `Lifecycle test description ${timestamp}`
298
+ const updatedName = `Updated Lifecycle ${timestamp}`
299
+
300
+ // CREATE
301
+ cy.log('🔄 Step 1: Create board')
302
+ BoardsPOM.clickCreate()
303
+ BoardsPOM.waitForFormLoad()
304
+ BoardsPOM.fillBoardForm({ name, description })
305
+ BoardsPOM.submitForm()
306
+
307
+ // Get board ID
308
+ cy.url().then((url) => {
309
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
310
+ expect(match).to.not.be.null
311
+
312
+ if (match) {
313
+ const boardId = match[1]
314
+
315
+ // READ
316
+ cy.log('🔄 Step 2: Verify board was created')
317
+ BoardsPOM.visitList()
318
+ BoardsPOM.waitForListLoad()
319
+ BoardsPOM.assertBoardInList(name)
320
+
321
+ // UPDATE
322
+ cy.log('🔄 Step 3: Update board')
323
+ BoardsPOM.visitEdit(boardId)
324
+ BoardsPOM.waitForFormLoad()
325
+ BoardsPOM.fillName(updatedName)
326
+ BoardsPOM.submitForm()
327
+
328
+ // Verify update
329
+ BoardsPOM.visitList()
330
+ BoardsPOM.waitForListLoad()
331
+ BoardsPOM.assertBoardInList(updatedName)
332
+
333
+ // DELETE
334
+ cy.log('🔄 Step 4: Delete board')
335
+ BoardsPOM.visitEdit(boardId)
336
+ BoardsPOM.waitForFormLoad()
337
+ BoardsPOM.confirmDelete()
338
+ BoardsPOM.deleteFromEdit()
339
+
340
+ // Verify deletion
341
+ BoardsPOM.waitForListLoad()
342
+ BoardsPOM.assertBoardNotInList(updatedName)
343
+
344
+ cy.log('✅ Full board lifecycle completed successfully')
345
+ }
346
+ })
347
+ })
348
+ })
349
+ })