@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.
- package/package.json +3 -3
- package/tests/cypress/e2e/ui/boards/boards-owner.cy.ts +349 -0
- package/tests/cypress/e2e/ui/cards/cards-modal.cy.ts +369 -0
- package/tests/cypress/e2e/ui/kanban/kanban-cards.cy.ts +280 -0
- package/tests/cypress/e2e/ui/kanban/kanban-columns.cy.ts +243 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +60 -0
- package/tests/cypress/src/components/BoardsPOM.ts +353 -0
- package/tests/cypress/src/components/CardsPOM.ts +383 -0
- package/tests/cypress/src/components/KanbanPOM.ts +399 -0
- package/tests/cypress/src/components/index.ts +9 -0
- package/tests/cypress/src/controllers/BoardsAPIController.js +302 -0
- package/tests/cypress/src/controllers/CardsAPIController.js +406 -0
- package/tests/cypress/src/controllers/ListsAPIController.js +299 -0
- package/tests/cypress/src/index.ts +25 -0
- package/tests/cypress/src/selectors.ts +50 -0
- package/tests/cypress/src/session-helpers.ts +105 -0
- package/tests/cypress/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -0
- package/tests/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/jest.config.cjs +127 -0
- package/tests/jest/setup.ts +170 -0
- package/tests/tsconfig.json +15 -0
|
@@ -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
|
+
}
|