@nextsparkjs/theme-productivity 0.1.0-beta.19 → 0.1.0-beta.24

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.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * CardsAPIController - Controller for interacting with the Cards API
3
+ * Encapsulates all CRUD operations for /api/v1/cards endpoints
4
+ * Cards are tasks within a Kanban list (column)
5
+ *
6
+ * Requires:
7
+ * - API Key with cards:read, cards:write scopes (or superadmin with *)
8
+ * - x-team-id header for team context
9
+ */
10
+ class CardsAPIController {
11
+ constructor(baseUrl = 'http://localhost:5173', apiKey = null, teamId = null) {
12
+ this.baseUrl = baseUrl;
13
+ this.apiKey = apiKey;
14
+ this.teamId = teamId;
15
+ this.endpoints = {
16
+ cards: '/api/v1/cards',
17
+ cardById: (id) => `/api/v1/cards/${id}`
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Set the API key for requests
23
+ * @param {string} apiKey - Valid API key
24
+ */
25
+ setApiKey(apiKey) {
26
+ this.apiKey = apiKey;
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Set the team ID for requests
32
+ * @param {string} teamId - Valid team ID
33
+ */
34
+ setTeamId(teamId) {
35
+ this.teamId = teamId;
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Get default headers for requests
41
+ * @param {Object} additionalHeaders - Additional headers
42
+ * @returns {Object} Complete headers
43
+ */
44
+ getHeaders(additionalHeaders = {}) {
45
+ const headers = {
46
+ 'Content-Type': 'application/json',
47
+ ...additionalHeaders
48
+ };
49
+
50
+ if (this.apiKey) {
51
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
52
+ }
53
+
54
+ if (this.teamId) {
55
+ headers['x-team-id'] = this.teamId;
56
+ }
57
+
58
+ return headers;
59
+ }
60
+
61
+ /**
62
+ * GET /api/v1/cards - Get list of cards
63
+ * @param {Object} options - Query options
64
+ * @param {string} options.boardId - Filter by board ID
65
+ * @param {string} options.listId - Filter by list ID
66
+ * @param {string} options.assigneeId - Filter by assignee
67
+ * @param {string} options.priority - Filter by priority (low, medium, high, urgent)
68
+ * @param {number} options.page - Page number
69
+ * @param {number} options.limit - Results per page
70
+ * @param {string} options.search - Search in title/description
71
+ * @param {Object} options.headers - Additional headers
72
+ * @returns {Cypress.Chainable} Cypress response
73
+ */
74
+ getAll(options = {}) {
75
+ const { boardId, listId, assigneeId, priority, page, limit, search, headers = {} } = options;
76
+
77
+ const queryParams = new URLSearchParams();
78
+ if (boardId) queryParams.append('boardId', boardId);
79
+ if (listId) queryParams.append('listId', listId);
80
+ if (assigneeId) queryParams.append('assigneeId', assigneeId);
81
+ if (priority) queryParams.append('priority', priority);
82
+ if (page) queryParams.append('page', page);
83
+ if (limit) queryParams.append('limit', limit);
84
+ if (search) queryParams.append('search', search);
85
+
86
+ const url = `${this.baseUrl}${this.endpoints.cards}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
87
+
88
+ return cy.request({
89
+ method: 'GET',
90
+ url: url,
91
+ headers: this.getHeaders(headers),
92
+ failOnStatusCode: false
93
+ });
94
+ }
95
+
96
+ /**
97
+ * POST /api/v1/cards - Create new card
98
+ * @param {Object} cardData - Card data
99
+ * @param {string} cardData.title - Card title (required)
100
+ * @param {string} cardData.listId - Parent list ID (required)
101
+ * @param {string} cardData.boardId - Parent board ID (required)
102
+ * @param {string} cardData.description - Description (optional)
103
+ * @param {string} cardData.priority - Priority: low, medium, high, urgent (optional)
104
+ * @param {string} cardData.dueDate - Due date ISO string (optional)
105
+ * @param {Array<string>} cardData.labels - Labels array (optional)
106
+ * @param {string} cardData.assigneeId - Assignee user ID (optional)
107
+ * @param {number} cardData.position - Position in list (optional)
108
+ * @param {Object} options - Additional options
109
+ * @param {Object} options.headers - Additional headers
110
+ * @returns {Cypress.Chainable} Cypress response
111
+ */
112
+ create(cardData, options = {}) {
113
+ const { headers = {} } = options;
114
+
115
+ return cy.request({
116
+ method: 'POST',
117
+ url: `${this.baseUrl}${this.endpoints.cards}`,
118
+ headers: this.getHeaders(headers),
119
+ body: cardData,
120
+ failOnStatusCode: false
121
+ });
122
+ }
123
+
124
+ /**
125
+ * GET /api/v1/cards/{id} - Get specific card by ID
126
+ * @param {string} id - Card ID
127
+ * @param {Object} options - Additional options
128
+ * @param {Object} options.headers - Additional headers
129
+ * @returns {Cypress.Chainable} Cypress response
130
+ */
131
+ getById(id, options = {}) {
132
+ const { headers = {} } = options;
133
+
134
+ return cy.request({
135
+ method: 'GET',
136
+ url: `${this.baseUrl}${this.endpoints.cardById(id)}`,
137
+ headers: this.getHeaders(headers),
138
+ failOnStatusCode: false
139
+ });
140
+ }
141
+
142
+ /**
143
+ * PATCH /api/v1/cards/{id} - Update card
144
+ * @param {string} id - Card ID
145
+ * @param {Object} updateData - Data to update
146
+ * @param {string} updateData.title - Card title (optional)
147
+ * @param {string} updateData.description - Description (optional)
148
+ * @param {string} updateData.priority - Priority (optional)
149
+ * @param {string} updateData.dueDate - Due date (optional)
150
+ * @param {Array<string>} updateData.labels - Labels (optional)
151
+ * @param {string} updateData.assigneeId - Assignee (optional)
152
+ * @param {string} updateData.listId - Move to different list (optional)
153
+ * @param {number} updateData.position - Position in list (optional)
154
+ * @param {Object} options - Additional options
155
+ * @param {Object} options.headers - Additional headers
156
+ * @returns {Cypress.Chainable} Cypress response
157
+ */
158
+ update(id, updateData, options = {}) {
159
+ const { headers = {} } = options;
160
+
161
+ return cy.request({
162
+ method: 'PATCH',
163
+ url: `${this.baseUrl}${this.endpoints.cardById(id)}`,
164
+ headers: this.getHeaders(headers),
165
+ body: updateData,
166
+ failOnStatusCode: false
167
+ });
168
+ }
169
+
170
+ /**
171
+ * DELETE /api/v1/cards/{id} - Delete card
172
+ * @param {string} id - Card ID
173
+ * @param {Object} options - Additional options
174
+ * @param {Object} options.headers - Additional headers
175
+ * @returns {Cypress.Chainable} Cypress response
176
+ */
177
+ delete(id, options = {}) {
178
+ const { headers = {} } = options;
179
+
180
+ return cy.request({
181
+ method: 'DELETE',
182
+ url: `${this.baseUrl}${this.endpoints.cardById(id)}`,
183
+ headers: this.getHeaders(headers),
184
+ failOnStatusCode: false
185
+ });
186
+ }
187
+
188
+ // ========== SPECIAL METHODS ==========
189
+
190
+ /**
191
+ * Move card to a different list
192
+ * @param {string} id - Card ID
193
+ * @param {string} targetListId - Target list ID
194
+ * @param {number} position - Position in target list (optional)
195
+ * @param {Object} options - Additional options
196
+ * @returns {Cypress.Chainable} Cypress response
197
+ */
198
+ move(id, targetListId, position = 0, options = {}) {
199
+ return this.update(id, { listId: targetListId, position }, options);
200
+ }
201
+
202
+ /**
203
+ * Assign card to a user
204
+ * @param {string} id - Card ID
205
+ * @param {string} assigneeId - User ID to assign
206
+ * @param {Object} options - Additional options
207
+ * @returns {Cypress.Chainable} Cypress response
208
+ */
209
+ assign(id, assigneeId, options = {}) {
210
+ return this.update(id, { assigneeId }, options);
211
+ }
212
+
213
+ /**
214
+ * Unassign card
215
+ * @param {string} id - Card ID
216
+ * @param {Object} options - Additional options
217
+ * @returns {Cypress.Chainable} Cypress response
218
+ */
219
+ unassign(id, options = {}) {
220
+ return this.update(id, { assigneeId: null }, options);
221
+ }
222
+
223
+ /**
224
+ * Set card priority
225
+ * @param {string} id - Card ID
226
+ * @param {string} priority - Priority: low, medium, high, urgent
227
+ * @param {Object} options - Additional options
228
+ * @returns {Cypress.Chainable} Cypress response
229
+ */
230
+ setPriority(id, priority, options = {}) {
231
+ return this.update(id, { priority }, options);
232
+ }
233
+
234
+ /**
235
+ * Set card due date
236
+ * @param {string} id - Card ID
237
+ * @param {string} dueDate - Due date ISO string
238
+ * @param {Object} options - Additional options
239
+ * @returns {Cypress.Chainable} Cypress response
240
+ */
241
+ setDueDate(id, dueDate, options = {}) {
242
+ return this.update(id, { dueDate }, options);
243
+ }
244
+
245
+ /**
246
+ * Get all cards for a specific board
247
+ * @param {string} boardId - Board ID
248
+ * @param {Object} options - Additional options
249
+ * @returns {Cypress.Chainable} Cypress response
250
+ */
251
+ getByBoard(boardId, options = {}) {
252
+ return this.getAll({ ...options, boardId, limit: 500 });
253
+ }
254
+
255
+ /**
256
+ * Get all cards for a specific list
257
+ * @param {string} listId - List ID
258
+ * @param {Object} options - Additional options
259
+ * @returns {Cypress.Chainable} Cypress response
260
+ */
261
+ getByList(listId, options = {}) {
262
+ return this.getAll({ ...options, listId, limit: 100 });
263
+ }
264
+
265
+ // ========== UTILITY METHODS ==========
266
+
267
+ /**
268
+ * Generate random card data for testing
269
+ * @param {Object} overrides - Specific data to override
270
+ * @returns {Object} Generated card data
271
+ */
272
+ generateRandomData(overrides = {}) {
273
+ const randomId = Math.random().toString(36).substring(2, 8);
274
+ const priorities = ['low', 'medium', 'high', 'urgent'];
275
+ const labels = ['bug', 'feature', 'enhancement', 'documentation', 'urgent', 'important'];
276
+ const cardTitles = [
277
+ 'Fix login bug',
278
+ 'Implement search feature',
279
+ 'Update documentation',
280
+ 'Refactor API',
281
+ 'Add unit tests',
282
+ 'Review pull request',
283
+ 'Deploy to staging',
284
+ 'Performance optimization'
285
+ ];
286
+
287
+ // Generate random due date (1-30 days from now)
288
+ const futureDate = new Date();
289
+ futureDate.setDate(futureDate.getDate() + Math.floor(Math.random() * 30) + 1);
290
+
291
+ return {
292
+ title: `${cardTitles[Math.floor(Math.random() * cardTitles.length)]} ${randomId}`,
293
+ description: `Test card description for task ${randomId}`,
294
+ priority: priorities[Math.floor(Math.random() * priorities.length)],
295
+ dueDate: futureDate.toISOString().split('T')[0],
296
+ labels: [labels[Math.floor(Math.random() * labels.length)]],
297
+ ...overrides
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Create a test card and return its data
303
+ * @param {string} boardId - Parent board ID
304
+ * @param {string} listId - Parent list ID
305
+ * @param {Object} cardData - Card data (optional)
306
+ * @returns {Cypress.Chainable} Promise resolving with created card data
307
+ */
308
+ createTestRecord(boardId, listId, cardData = {}) {
309
+ const testCardData = this.generateRandomData({ ...cardData, boardId, listId });
310
+
311
+ return this.create(testCardData).then((response) => {
312
+ if (response.status === 201) {
313
+ return { ...testCardData, ...response.body.data };
314
+ }
315
+ throw new Error(`Failed to create test card: ${response.body?.error || 'Unknown error'}`);
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Clean up a test card (delete it)
321
+ * @param {string} id - Card ID
322
+ * @returns {Cypress.Chainable} Delete response
323
+ */
324
+ cleanupTestRecord(id) {
325
+ return this.delete(id);
326
+ }
327
+
328
+ // ========== VALIDATION METHODS ==========
329
+
330
+ /**
331
+ * Validate success response structure
332
+ * @param {Object} response - API response
333
+ * @param {number} expectedStatus - Expected status code
334
+ */
335
+ validateSuccessResponse(response, expectedStatus = 200) {
336
+ expect(response.status).to.eq(expectedStatus);
337
+ expect(response.body).to.have.property('success', true);
338
+ expect(response.body).to.have.property('data');
339
+ }
340
+
341
+ /**
342
+ * Validate error response structure
343
+ * @param {Object} response - API response
344
+ * @param {number} expectedStatus - Expected status code
345
+ * @param {string} expectedErrorCode - Expected error code (optional)
346
+ */
347
+ validateErrorResponse(response, expectedStatus, expectedErrorCode = null) {
348
+ expect(response.status).to.eq(expectedStatus);
349
+ expect(response.body).to.have.property('success', false);
350
+ expect(response.body).to.have.property('error');
351
+
352
+ if (expectedErrorCode) {
353
+ expect(response.body).to.have.property('code', expectedErrorCode);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Validate card object structure
359
+ * @param {Object} card - Card object
360
+ */
361
+ validateObject(card) {
362
+ // Required system fields
363
+ expect(card).to.have.property('id');
364
+ expect(card).to.have.property('createdAt');
365
+ expect(card).to.have.property('updatedAt');
366
+
367
+ // Required entity fields
368
+ expect(card).to.have.property('title');
369
+ expect(card.title).to.be.a('string');
370
+
371
+ expect(card).to.have.property('listId');
372
+ expect(card.listId).to.be.a('string');
373
+
374
+ expect(card).to.have.property('boardId');
375
+ expect(card.boardId).to.be.a('string');
376
+
377
+ // Optional fields
378
+ if (card.description !== null && card.description !== undefined) {
379
+ expect(card.description).to.be.a('string');
380
+ }
381
+
382
+ if (card.priority !== null && card.priority !== undefined) {
383
+ expect(card.priority).to.be.oneOf(['low', 'medium', 'high', 'urgent']);
384
+ }
385
+
386
+ if (card.dueDate !== null && card.dueDate !== undefined) {
387
+ expect(card.dueDate).to.be.a('string');
388
+ }
389
+
390
+ if (card.labels !== null && card.labels !== undefined) {
391
+ expect(card.labels).to.be.an('array');
392
+ }
393
+
394
+ if (card.position !== null && card.position !== undefined) {
395
+ expect(card.position).to.be.a('number');
396
+ }
397
+ }
398
+ }
399
+
400
+ // Export class for use in tests
401
+ module.exports = CardsAPIController;
402
+
403
+ // For global use in Cypress
404
+ if (typeof window !== 'undefined') {
405
+ window.CardsAPIController = CardsAPIController;
406
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * ListsAPIController - Controller for interacting with the Lists API
3
+ * Encapsulates all CRUD operations for /api/v1/lists endpoints
4
+ * Lists are columns within a Kanban board
5
+ *
6
+ * Requires:
7
+ * - API Key with lists:read, lists:write scopes (or superadmin with *)
8
+ * - x-team-id header for team context
9
+ */
10
+ class ListsAPIController {
11
+ constructor(baseUrl = 'http://localhost:5173', apiKey = null, teamId = null) {
12
+ this.baseUrl = baseUrl;
13
+ this.apiKey = apiKey;
14
+ this.teamId = teamId;
15
+ this.endpoints = {
16
+ lists: '/api/v1/lists',
17
+ listById: (id) => `/api/v1/lists/${id}`
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Set the API key for requests
23
+ * @param {string} apiKey - Valid API key
24
+ */
25
+ setApiKey(apiKey) {
26
+ this.apiKey = apiKey;
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Set the team ID for requests
32
+ * @param {string} teamId - Valid team ID
33
+ */
34
+ setTeamId(teamId) {
35
+ this.teamId = teamId;
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Get default headers for requests
41
+ * @param {Object} additionalHeaders - Additional headers
42
+ * @returns {Object} Complete headers
43
+ */
44
+ getHeaders(additionalHeaders = {}) {
45
+ const headers = {
46
+ 'Content-Type': 'application/json',
47
+ ...additionalHeaders
48
+ };
49
+
50
+ if (this.apiKey) {
51
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
52
+ }
53
+
54
+ if (this.teamId) {
55
+ headers['x-team-id'] = this.teamId;
56
+ }
57
+
58
+ return headers;
59
+ }
60
+
61
+ /**
62
+ * GET /api/v1/lists - Get list of lists (columns)
63
+ * @param {Object} options - Query options
64
+ * @param {string} options.boardId - Filter by board ID (required for most use cases)
65
+ * @param {number} options.page - Page number
66
+ * @param {number} options.limit - Results per page
67
+ * @param {Object} options.headers - Additional headers
68
+ * @returns {Cypress.Chainable} Cypress response
69
+ */
70
+ getAll(options = {}) {
71
+ const { boardId, page, limit, headers = {} } = options;
72
+
73
+ const queryParams = new URLSearchParams();
74
+ if (boardId) queryParams.append('boardId', boardId);
75
+ if (page) queryParams.append('page', page);
76
+ if (limit) queryParams.append('limit', limit);
77
+
78
+ const url = `${this.baseUrl}${this.endpoints.lists}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
79
+
80
+ return cy.request({
81
+ method: 'GET',
82
+ url: url,
83
+ headers: this.getHeaders(headers),
84
+ failOnStatusCode: false
85
+ });
86
+ }
87
+
88
+ /**
89
+ * POST /api/v1/lists - Create new list (column)
90
+ * @param {Object} listData - List data
91
+ * @param {string} listData.name - List name (required)
92
+ * @param {string} listData.boardId - Parent board ID (required)
93
+ * @param {number} listData.position - Position in board (optional)
94
+ * @param {Object} options - Additional options
95
+ * @param {Object} options.headers - Additional headers
96
+ * @returns {Cypress.Chainable} Cypress response
97
+ */
98
+ create(listData, options = {}) {
99
+ const { headers = {} } = options;
100
+
101
+ return cy.request({
102
+ method: 'POST',
103
+ url: `${this.baseUrl}${this.endpoints.lists}`,
104
+ headers: this.getHeaders(headers),
105
+ body: listData,
106
+ failOnStatusCode: false
107
+ });
108
+ }
109
+
110
+ /**
111
+ * GET /api/v1/lists/{id} - Get specific list by ID
112
+ * @param {string} id - List ID
113
+ * @param {Object} options - Additional options
114
+ * @param {Object} options.headers - Additional headers
115
+ * @returns {Cypress.Chainable} Cypress response
116
+ */
117
+ getById(id, options = {}) {
118
+ const { headers = {} } = options;
119
+
120
+ return cy.request({
121
+ method: 'GET',
122
+ url: `${this.baseUrl}${this.endpoints.listById(id)}`,
123
+ headers: this.getHeaders(headers),
124
+ failOnStatusCode: false
125
+ });
126
+ }
127
+
128
+ /**
129
+ * PATCH /api/v1/lists/{id} - Update list
130
+ * @param {string} id - List ID
131
+ * @param {Object} updateData - Data to update
132
+ * @param {string} updateData.name - List name (optional)
133
+ * @param {number} updateData.position - Position (optional)
134
+ * @param {Object} options - Additional options
135
+ * @param {Object} options.headers - Additional headers
136
+ * @returns {Cypress.Chainable} Cypress response
137
+ */
138
+ update(id, updateData, options = {}) {
139
+ const { headers = {} } = options;
140
+
141
+ return cy.request({
142
+ method: 'PATCH',
143
+ url: `${this.baseUrl}${this.endpoints.listById(id)}`,
144
+ headers: this.getHeaders(headers),
145
+ body: updateData,
146
+ failOnStatusCode: false
147
+ });
148
+ }
149
+
150
+ /**
151
+ * DELETE /api/v1/lists/{id} - Delete list
152
+ * @param {string} id - List ID
153
+ * @param {Object} options - Additional options
154
+ * @param {Object} options.headers - Additional headers
155
+ * @returns {Cypress.Chainable} Cypress response
156
+ */
157
+ delete(id, options = {}) {
158
+ const { headers = {} } = options;
159
+
160
+ return cy.request({
161
+ method: 'DELETE',
162
+ url: `${this.baseUrl}${this.endpoints.listById(id)}`,
163
+ headers: this.getHeaders(headers),
164
+ failOnStatusCode: false
165
+ });
166
+ }
167
+
168
+ // ========== SPECIAL METHODS ==========
169
+
170
+ /**
171
+ * Reorder lists within a board
172
+ * @param {string} boardId - Board ID
173
+ * @param {Array<{id: string, position: number}>} listPositions - Array of list IDs with new positions
174
+ * @param {Object} options - Additional options
175
+ * @returns {Cypress.Chainable} Cypress response chain
176
+ */
177
+ reorder(boardId, listPositions, options = {}) {
178
+ // Update each list's position
179
+ const updates = listPositions.map(({ id, position }) =>
180
+ this.update(id, { position }, options)
181
+ );
182
+
183
+ return cy.wrap(updates);
184
+ }
185
+
186
+ /**
187
+ * Get all lists for a specific board
188
+ * @param {string} boardId - Board ID
189
+ * @param {Object} options - Additional options
190
+ * @returns {Cypress.Chainable} Cypress response
191
+ */
192
+ getByBoard(boardId, options = {}) {
193
+ return this.getAll({ ...options, boardId, limit: 100 });
194
+ }
195
+
196
+ // ========== UTILITY METHODS ==========
197
+
198
+ /**
199
+ * Generate random list data for testing
200
+ * @param {Object} overrides - Specific data to override
201
+ * @returns {Object} Generated list data
202
+ */
203
+ generateRandomData(overrides = {}) {
204
+ const randomId = Math.random().toString(36).substring(2, 8);
205
+ const listNames = ['To Do', 'In Progress', 'Review', 'Done', 'Backlog', 'Blocked'];
206
+
207
+ return {
208
+ name: `${listNames[Math.floor(Math.random() * listNames.length)]} ${randomId}`,
209
+ position: Math.floor(Math.random() * 10),
210
+ ...overrides
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Create a test list and return its data
216
+ * @param {string} boardId - Parent board ID
217
+ * @param {Object} listData - List data (optional)
218
+ * @returns {Cypress.Chainable} Promise resolving with created list data
219
+ */
220
+ createTestRecord(boardId, listData = {}) {
221
+ const testListData = this.generateRandomData({ ...listData, boardId });
222
+
223
+ return this.create(testListData).then((response) => {
224
+ if (response.status === 201) {
225
+ return { ...testListData, ...response.body.data };
226
+ }
227
+ throw new Error(`Failed to create test list: ${response.body?.error || 'Unknown error'}`);
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Clean up a test list (delete it)
233
+ * @param {string} id - List ID
234
+ * @returns {Cypress.Chainable} Delete response
235
+ */
236
+ cleanupTestRecord(id) {
237
+ return this.delete(id);
238
+ }
239
+
240
+ // ========== VALIDATION METHODS ==========
241
+
242
+ /**
243
+ * Validate success response structure
244
+ * @param {Object} response - API response
245
+ * @param {number} expectedStatus - Expected status code
246
+ */
247
+ validateSuccessResponse(response, expectedStatus = 200) {
248
+ expect(response.status).to.eq(expectedStatus);
249
+ expect(response.body).to.have.property('success', true);
250
+ expect(response.body).to.have.property('data');
251
+ }
252
+
253
+ /**
254
+ * Validate error response structure
255
+ * @param {Object} response - API response
256
+ * @param {number} expectedStatus - Expected status code
257
+ * @param {string} expectedErrorCode - Expected error code (optional)
258
+ */
259
+ validateErrorResponse(response, expectedStatus, expectedErrorCode = null) {
260
+ expect(response.status).to.eq(expectedStatus);
261
+ expect(response.body).to.have.property('success', false);
262
+ expect(response.body).to.have.property('error');
263
+
264
+ if (expectedErrorCode) {
265
+ expect(response.body).to.have.property('code', expectedErrorCode);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Validate list object structure
271
+ * @param {Object} list - List object
272
+ */
273
+ validateObject(list) {
274
+ // Required system fields
275
+ expect(list).to.have.property('id');
276
+ expect(list).to.have.property('createdAt');
277
+ expect(list).to.have.property('updatedAt');
278
+
279
+ // Required entity fields
280
+ expect(list).to.have.property('name');
281
+ expect(list.name).to.be.a('string');
282
+
283
+ expect(list).to.have.property('boardId');
284
+ expect(list.boardId).to.be.a('string');
285
+
286
+ // Optional fields
287
+ if (list.position !== null && list.position !== undefined) {
288
+ expect(list.position).to.be.a('number');
289
+ }
290
+ }
291
+ }
292
+
293
+ // Export class for use in tests
294
+ module.exports = ListsAPIController;
295
+
296
+ // For global use in Cypress
297
+ if (typeof window !== 'undefined') {
298
+ window.ListsAPIController = ListsAPIController;
299
+ }