@nextsparkjs/theme-productivity 0.1.0-beta.19 → 0.1.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- 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/tsconfig.json +15 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kanban Board Page Object Model - Entity Testing Convention
|
|
3
|
+
*
|
|
4
|
+
* POM for the Kanban board view with columns (lists) and cards.
|
|
5
|
+
* Follows the pattern: {slug}-{component}-{detail}
|
|
6
|
+
*
|
|
7
|
+
* Convention:
|
|
8
|
+
* - lists-{component}-{detail} for columns
|
|
9
|
+
* - cards-{component}-{detail} for cards
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* - kanban-board (board container)
|
|
13
|
+
* - lists-column-{id} (column)
|
|
14
|
+
* - lists-add-column (add column button)
|
|
15
|
+
* - cards-item-{id} (card)
|
|
16
|
+
*
|
|
17
|
+
* @see test/cypress/fixtures/themes/productivity/entities.json
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import entitiesConfig from '../../fixtures/entities.json'
|
|
21
|
+
|
|
22
|
+
// Get lists and cards entity configs
|
|
23
|
+
const listsEntity = entitiesConfig.entities.lists
|
|
24
|
+
const cardsEntity = entitiesConfig.entities.cards
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kanban Board Page Object Model
|
|
28
|
+
*
|
|
29
|
+
* Handles interactions with the Kanban board, columns (lists), and cards.
|
|
30
|
+
*/
|
|
31
|
+
export class KanbanPOM {
|
|
32
|
+
// ============================================
|
|
33
|
+
// ENTITY METADATA
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
static get listsConfig() {
|
|
37
|
+
return listsEntity
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static get cardsConfig() {
|
|
41
|
+
return cardsEntity
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// SELECTORS
|
|
46
|
+
// ============================================
|
|
47
|
+
|
|
48
|
+
static get selectors() {
|
|
49
|
+
return {
|
|
50
|
+
// Board Container
|
|
51
|
+
board: '[data-cy="kanban-board"]',
|
|
52
|
+
|
|
53
|
+
// Column (List) Selectors
|
|
54
|
+
addColumn: '[data-cy="lists-add-column"]',
|
|
55
|
+
addColumnForm: '[data-cy="lists-add-form"]',
|
|
56
|
+
columnFieldName: '[data-cy="lists-field-name"]',
|
|
57
|
+
columnFormSubmit: '[data-cy="lists-form-submit"]',
|
|
58
|
+
|
|
59
|
+
// Dynamic column selectors
|
|
60
|
+
column: (id: string) => `[data-cy="lists-column-${id}"]`,
|
|
61
|
+
columnHeader: (id: string) => `[data-cy="lists-column-header-${id}"]`,
|
|
62
|
+
columnTitle: (id: string) => `[data-cy="lists-column-title-${id}"]`,
|
|
63
|
+
columnMenuTrigger: (id: string) => `[data-cy="lists-column-menu-trigger-${id}"]`,
|
|
64
|
+
columnMenu: (id: string) => `[data-cy="lists-column-menu-${id}"]`,
|
|
65
|
+
|
|
66
|
+
// Column card actions
|
|
67
|
+
addCard: (listId: string) => `[data-cy="lists-add-card-${listId}"]`,
|
|
68
|
+
addCardForm: (listId: string) => `[data-cy="cards-add-form-${listId}"]`,
|
|
69
|
+
cardFieldTitle: (listId: string) => `[data-cy="cards-field-title-${listId}"]`,
|
|
70
|
+
cardFormSubmit: (listId: string) => `[data-cy="cards-form-submit-${listId}"]`,
|
|
71
|
+
|
|
72
|
+
// Card Selectors
|
|
73
|
+
card: (id: string) => `[data-cy="cards-item-${id}"]`,
|
|
74
|
+
allCards: '[data-cy^="cards-item-"]',
|
|
75
|
+
|
|
76
|
+
// Generic column selector
|
|
77
|
+
allColumns: '[data-cy^="lists-column-"]',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================
|
|
82
|
+
// BOARD NAVIGATION
|
|
83
|
+
// ============================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Visit a specific board by ID
|
|
87
|
+
*/
|
|
88
|
+
static visitBoard(boardId: string) {
|
|
89
|
+
cy.visit(`/dashboard/boards/${boardId}`)
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wait for board to load
|
|
95
|
+
*/
|
|
96
|
+
static waitForBoardLoad() {
|
|
97
|
+
cy.get(this.selectors.board, { timeout: 15000 }).should('be.visible')
|
|
98
|
+
return this
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate board is visible
|
|
103
|
+
*/
|
|
104
|
+
static validateBoardVisible() {
|
|
105
|
+
cy.get(this.selectors.board).should('be.visible')
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// COLUMN (LIST) ACTIONS
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Click add column button
|
|
115
|
+
*/
|
|
116
|
+
static clickAddColumn() {
|
|
117
|
+
cy.get(this.selectors.addColumn).click()
|
|
118
|
+
return this
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fill column name in add form
|
|
123
|
+
*/
|
|
124
|
+
static fillColumnName(name: string) {
|
|
125
|
+
cy.get(this.selectors.columnFieldName).clear().type(name)
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Submit add column form
|
|
131
|
+
*/
|
|
132
|
+
static submitAddColumn() {
|
|
133
|
+
cy.get(this.selectors.columnFormSubmit).click()
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a new column with given name
|
|
139
|
+
*/
|
|
140
|
+
static createColumn(name: string) {
|
|
141
|
+
this.clickAddColumn()
|
|
142
|
+
this.fillColumnName(name)
|
|
143
|
+
this.submitAddColumn()
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get column by ID
|
|
149
|
+
*/
|
|
150
|
+
static getColumn(id: string): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
151
|
+
return cy.get(this.selectors.column(id))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get column header
|
|
156
|
+
*/
|
|
157
|
+
static getColumnHeader(id: string): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
158
|
+
return cy.get(this.selectors.columnHeader(id))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Click on column title to rename
|
|
163
|
+
*/
|
|
164
|
+
static clickColumnTitle(id: string) {
|
|
165
|
+
cy.get(this.selectors.columnTitle(id)).click()
|
|
166
|
+
return this
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Open column menu
|
|
171
|
+
*/
|
|
172
|
+
static openColumnMenu(id: string) {
|
|
173
|
+
cy.get(this.selectors.columnMenuTrigger(id)).click()
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Click rename in column menu
|
|
179
|
+
*/
|
|
180
|
+
static clickColumnRename(id: string) {
|
|
181
|
+
this.openColumnMenu(id)
|
|
182
|
+
cy.get('[role="menuitem"]').contains('Rename').click()
|
|
183
|
+
return this
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Click delete in column menu
|
|
188
|
+
*/
|
|
189
|
+
static clickColumnDelete(id: string) {
|
|
190
|
+
this.openColumnMenu(id)
|
|
191
|
+
cy.get('[role="menuitem"]').contains('Delete').click()
|
|
192
|
+
return this
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get all columns
|
|
197
|
+
*/
|
|
198
|
+
static getAllColumns(): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
199
|
+
return cy.get(this.selectors.allColumns)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get column count
|
|
204
|
+
*/
|
|
205
|
+
static getColumnCount(): Cypress.Chainable<number> {
|
|
206
|
+
return cy.get(this.selectors.allColumns).its('length')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// CARD ACTIONS
|
|
211
|
+
// ============================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Click add card button in a column
|
|
215
|
+
*/
|
|
216
|
+
static clickAddCard(listId: string) {
|
|
217
|
+
cy.get(this.selectors.addCard(listId)).click()
|
|
218
|
+
return this
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fill card title in add form
|
|
223
|
+
*/
|
|
224
|
+
static fillCardTitle(listId: string, title: string) {
|
|
225
|
+
cy.get(this.selectors.cardFieldTitle(listId)).clear().type(title)
|
|
226
|
+
return this
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Submit add card form
|
|
231
|
+
*/
|
|
232
|
+
static submitAddCard(listId: string) {
|
|
233
|
+
cy.get(this.selectors.cardFormSubmit(listId)).click()
|
|
234
|
+
return this
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a new card in a column
|
|
239
|
+
*/
|
|
240
|
+
static createCard(listId: string, title: string) {
|
|
241
|
+
this.clickAddCard(listId)
|
|
242
|
+
this.fillCardTitle(listId, title)
|
|
243
|
+
this.submitAddCard(listId)
|
|
244
|
+
return this
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get card by ID
|
|
249
|
+
*/
|
|
250
|
+
static getCard(id: string): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
251
|
+
return cy.get(this.selectors.card(id))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Click on a card to open modal
|
|
256
|
+
*/
|
|
257
|
+
static clickCard(id: string) {
|
|
258
|
+
cy.get(this.selectors.card(id)).click()
|
|
259
|
+
return this
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get all cards
|
|
264
|
+
*/
|
|
265
|
+
static getAllCards(): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
266
|
+
return cy.get(this.selectors.allCards)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get card count
|
|
271
|
+
*/
|
|
272
|
+
static getCardCount(): Cypress.Chainable<number> {
|
|
273
|
+
return cy.get(this.selectors.allCards).its('length')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get cards in a specific column
|
|
278
|
+
*/
|
|
279
|
+
static getCardsInColumn(listId: string): Cypress.Chainable<JQuery<HTMLElement>> {
|
|
280
|
+
return cy.get(this.selectors.column(listId)).find('[data-cy^="cards-item-"]')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get card count in a specific column
|
|
285
|
+
*/
|
|
286
|
+
static getCardCountInColumn(listId: string): Cypress.Chainable<number> {
|
|
287
|
+
return this.getCardsInColumn(listId).its('length')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================
|
|
291
|
+
// DRAG & DROP (Basic support)
|
|
292
|
+
// ============================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Drag a card to a different column
|
|
296
|
+
* Note: This is a basic implementation. For complex drag-drop, use specialized plugins.
|
|
297
|
+
*/
|
|
298
|
+
static dragCardToColumn(cardId: string, targetColumnId: string) {
|
|
299
|
+
const card = this.selectors.card(cardId)
|
|
300
|
+
const targetColumn = this.selectors.column(targetColumnId)
|
|
301
|
+
|
|
302
|
+
cy.get(card).then(($card) => {
|
|
303
|
+
cy.get(targetColumn).then(($column) => {
|
|
304
|
+
const cardRect = $card[0].getBoundingClientRect()
|
|
305
|
+
const columnRect = $column[0].getBoundingClientRect()
|
|
306
|
+
|
|
307
|
+
// Use native drag events
|
|
308
|
+
cy.get(card)
|
|
309
|
+
.trigger('mousedown', { which: 1, force: true })
|
|
310
|
+
.trigger('mousemove', {
|
|
311
|
+
clientX: columnRect.left + columnRect.width / 2,
|
|
312
|
+
clientY: columnRect.top + 100,
|
|
313
|
+
force: true,
|
|
314
|
+
})
|
|
315
|
+
.trigger('mouseup', { force: true })
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
return this
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================
|
|
323
|
+
// ASSERTIONS
|
|
324
|
+
// ============================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Assert column exists
|
|
328
|
+
*/
|
|
329
|
+
static assertColumnExists(id: string) {
|
|
330
|
+
cy.get(this.selectors.column(id)).should('exist')
|
|
331
|
+
return this
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Assert column does not exist
|
|
336
|
+
*/
|
|
337
|
+
static assertColumnNotExists(id: string) {
|
|
338
|
+
cy.get(this.selectors.column(id)).should('not.exist')
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Assert column has title
|
|
344
|
+
*/
|
|
345
|
+
static assertColumnTitle(id: string, title: string) {
|
|
346
|
+
cy.get(this.selectors.columnTitle(id)).should('contain.text', title)
|
|
347
|
+
return this
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Assert card exists
|
|
352
|
+
*/
|
|
353
|
+
static assertCardExists(id: string) {
|
|
354
|
+
cy.get(this.selectors.card(id)).should('exist')
|
|
355
|
+
return this
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Assert card does not exist
|
|
360
|
+
*/
|
|
361
|
+
static assertCardNotExists(id: string) {
|
|
362
|
+
cy.get(this.selectors.card(id)).should('not.exist')
|
|
363
|
+
return this
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Assert card is in column
|
|
368
|
+
*/
|
|
369
|
+
static assertCardInColumn(cardId: string, columnId: string) {
|
|
370
|
+
cy.get(this.selectors.column(columnId)).find(this.selectors.card(cardId)).should('exist')
|
|
371
|
+
return this
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Assert column count
|
|
376
|
+
*/
|
|
377
|
+
static assertColumnCount(count: number) {
|
|
378
|
+
cy.get(this.selectors.allColumns).should('have.length', count)
|
|
379
|
+
return this
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Assert card count in column
|
|
384
|
+
*/
|
|
385
|
+
static assertCardCountInColumn(columnId: string, count: number) {
|
|
386
|
+
this.getCardsInColumn(columnId).should('have.length', count)
|
|
387
|
+
return this
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Assert board contains text
|
|
392
|
+
*/
|
|
393
|
+
static assertBoardContains(text: string) {
|
|
394
|
+
cy.get(this.selectors.board).should('contain.text', text)
|
|
395
|
+
return this
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export default KanbanPOM
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoardsAPIController - Controller for interacting with the Boards API
|
|
3
|
+
* Encapsulates all CRUD operations for /api/v1/boards endpoints
|
|
4
|
+
*
|
|
5
|
+
* Requires:
|
|
6
|
+
* - API Key with boards:read, boards:write scopes (or superadmin with *)
|
|
7
|
+
* - x-team-id header for team context
|
|
8
|
+
*/
|
|
9
|
+
class BoardsAPIController {
|
|
10
|
+
constructor(baseUrl = 'http://localhost:5173', apiKey = null, teamId = null) {
|
|
11
|
+
this.baseUrl = baseUrl;
|
|
12
|
+
this.apiKey = apiKey;
|
|
13
|
+
this.teamId = teamId;
|
|
14
|
+
this.endpoints = {
|
|
15
|
+
boards: '/api/v1/boards',
|
|
16
|
+
boardById: (id) => `/api/v1/boards/${id}`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set the API key for requests
|
|
22
|
+
* @param {string} apiKey - Valid API key
|
|
23
|
+
*/
|
|
24
|
+
setApiKey(apiKey) {
|
|
25
|
+
this.apiKey = apiKey;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set the team ID for requests
|
|
31
|
+
* @param {string} teamId - Valid team ID
|
|
32
|
+
*/
|
|
33
|
+
setTeamId(teamId) {
|
|
34
|
+
this.teamId = teamId;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get default headers for requests
|
|
40
|
+
* @param {Object} additionalHeaders - Additional headers
|
|
41
|
+
* @returns {Object} Complete headers
|
|
42
|
+
*/
|
|
43
|
+
getHeaders(additionalHeaders = {}) {
|
|
44
|
+
const headers = {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
...additionalHeaders
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (this.apiKey) {
|
|
50
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.teamId) {
|
|
54
|
+
headers['x-team-id'] = this.teamId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return headers;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* GET /api/v1/boards - Get list of boards
|
|
62
|
+
* @param {Object} options - Query options
|
|
63
|
+
* @param {number} options.page - Page number
|
|
64
|
+
* @param {number} options.limit - Results per page
|
|
65
|
+
* @param {boolean} options.archived - Filter by archived status
|
|
66
|
+
* @param {string} options.search - Search in name/description
|
|
67
|
+
* @param {Object} options.headers - Additional headers
|
|
68
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
69
|
+
*/
|
|
70
|
+
getAll(options = {}) {
|
|
71
|
+
const { page, limit, archived, search, headers = {} } = options;
|
|
72
|
+
|
|
73
|
+
const queryParams = new URLSearchParams();
|
|
74
|
+
if (page) queryParams.append('page', page);
|
|
75
|
+
if (limit) queryParams.append('limit', limit);
|
|
76
|
+
if (archived !== undefined) queryParams.append('archived', archived);
|
|
77
|
+
if (search) queryParams.append('search', search);
|
|
78
|
+
|
|
79
|
+
const url = `${this.baseUrl}${this.endpoints.boards}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
80
|
+
|
|
81
|
+
return cy.request({
|
|
82
|
+
method: 'GET',
|
|
83
|
+
url: url,
|
|
84
|
+
headers: this.getHeaders(headers),
|
|
85
|
+
failOnStatusCode: false
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* POST /api/v1/boards - Create new board
|
|
91
|
+
* @param {Object} boardData - Board data
|
|
92
|
+
* @param {string} boardData.name - Board name (required)
|
|
93
|
+
* @param {string} boardData.description - Description (optional)
|
|
94
|
+
* @param {string} boardData.color - Color: blue, green, purple, orange, red, pink, gray (optional)
|
|
95
|
+
* @param {Object} options - Additional options
|
|
96
|
+
* @param {Object} options.headers - Additional headers
|
|
97
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
98
|
+
*/
|
|
99
|
+
create(boardData, options = {}) {
|
|
100
|
+
const { headers = {} } = options;
|
|
101
|
+
|
|
102
|
+
return cy.request({
|
|
103
|
+
method: 'POST',
|
|
104
|
+
url: `${this.baseUrl}${this.endpoints.boards}`,
|
|
105
|
+
headers: this.getHeaders(headers),
|
|
106
|
+
body: boardData,
|
|
107
|
+
failOnStatusCode: false
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GET /api/v1/boards/{id} - Get specific board by ID
|
|
113
|
+
* @param {string} id - Board ID
|
|
114
|
+
* @param {Object} options - Additional options
|
|
115
|
+
* @param {Object} options.headers - Additional headers
|
|
116
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
117
|
+
*/
|
|
118
|
+
getById(id, options = {}) {
|
|
119
|
+
const { headers = {} } = options;
|
|
120
|
+
|
|
121
|
+
return cy.request({
|
|
122
|
+
method: 'GET',
|
|
123
|
+
url: `${this.baseUrl}${this.endpoints.boardById(id)}`,
|
|
124
|
+
headers: this.getHeaders(headers),
|
|
125
|
+
failOnStatusCode: false
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* PATCH /api/v1/boards/{id} - Update board
|
|
131
|
+
* @param {string} id - Board ID
|
|
132
|
+
* @param {Object} updateData - Data to update
|
|
133
|
+
* @param {string} updateData.name - Board name (optional)
|
|
134
|
+
* @param {string} updateData.description - Description (optional)
|
|
135
|
+
* @param {string} updateData.color - Color (optional)
|
|
136
|
+
* @param {boolean} updateData.archived - Archived status (optional)
|
|
137
|
+
* @param {Object} options - Additional options
|
|
138
|
+
* @param {Object} options.headers - Additional headers
|
|
139
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
140
|
+
*/
|
|
141
|
+
update(id, updateData, options = {}) {
|
|
142
|
+
const { headers = {} } = options;
|
|
143
|
+
|
|
144
|
+
return cy.request({
|
|
145
|
+
method: 'PATCH',
|
|
146
|
+
url: `${this.baseUrl}${this.endpoints.boardById(id)}`,
|
|
147
|
+
headers: this.getHeaders(headers),
|
|
148
|
+
body: updateData,
|
|
149
|
+
failOnStatusCode: false
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* DELETE /api/v1/boards/{id} - Delete board
|
|
155
|
+
* @param {string} id - Board ID
|
|
156
|
+
* @param {Object} options - Additional options
|
|
157
|
+
* @param {Object} options.headers - Additional headers
|
|
158
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
159
|
+
*/
|
|
160
|
+
delete(id, options = {}) {
|
|
161
|
+
const { headers = {} } = options;
|
|
162
|
+
|
|
163
|
+
return cy.request({
|
|
164
|
+
method: 'DELETE',
|
|
165
|
+
url: `${this.baseUrl}${this.endpoints.boardById(id)}`,
|
|
166
|
+
headers: this.getHeaders(headers),
|
|
167
|
+
failOnStatusCode: false
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ========== SPECIAL METHODS ==========
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* PATCH /api/v1/boards/{id} - Archive board
|
|
175
|
+
* @param {string} id - Board ID
|
|
176
|
+
* @param {Object} options - Additional options
|
|
177
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
178
|
+
*/
|
|
179
|
+
archive(id, options = {}) {
|
|
180
|
+
return this.update(id, { archived: true }, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* PATCH /api/v1/boards/{id} - Unarchive board
|
|
185
|
+
* @param {string} id - Board ID
|
|
186
|
+
* @param {Object} options - Additional options
|
|
187
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
188
|
+
*/
|
|
189
|
+
unarchive(id, options = {}) {
|
|
190
|
+
return this.update(id, { archived: false }, options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ========== UTILITY METHODS ==========
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate random board data for testing
|
|
197
|
+
* @param {Object} overrides - Specific data to override
|
|
198
|
+
* @returns {Object} Generated board data
|
|
199
|
+
*/
|
|
200
|
+
generateRandomData(overrides = {}) {
|
|
201
|
+
const randomId = Math.random().toString(36).substring(2, 8);
|
|
202
|
+
const colors = ['blue', 'green', 'purple', 'orange', 'red', 'pink', 'gray'];
|
|
203
|
+
const boardNames = ['Project Alpha', 'Marketing Campaign', 'Product Launch', 'Sprint Board', 'Roadmap', 'Bug Tracker'];
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
name: `${boardNames[Math.floor(Math.random() * boardNames.length)]} ${randomId}`,
|
|
207
|
+
description: `Test board description ${randomId}`,
|
|
208
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
209
|
+
...overrides
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a test board and return its data
|
|
215
|
+
* @param {Object} boardData - Board data (optional)
|
|
216
|
+
* @returns {Cypress.Chainable} Promise resolving with created board data
|
|
217
|
+
*/
|
|
218
|
+
createTestRecord(boardData = {}) {
|
|
219
|
+
const testBoardData = this.generateRandomData(boardData);
|
|
220
|
+
|
|
221
|
+
return this.create(testBoardData).then((response) => {
|
|
222
|
+
if (response.status === 201) {
|
|
223
|
+
return { ...testBoardData, ...response.body.data };
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Failed to create test board: ${response.body?.error || 'Unknown error'}`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up a test board (delete it)
|
|
231
|
+
* @param {string} id - Board ID
|
|
232
|
+
* @returns {Cypress.Chainable} Delete response
|
|
233
|
+
*/
|
|
234
|
+
cleanupTestRecord(id) {
|
|
235
|
+
return this.delete(id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ========== VALIDATION METHODS ==========
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate success response structure
|
|
242
|
+
* @param {Object} response - API response
|
|
243
|
+
* @param {number} expectedStatus - Expected status code
|
|
244
|
+
*/
|
|
245
|
+
validateSuccessResponse(response, expectedStatus = 200) {
|
|
246
|
+
expect(response.status).to.eq(expectedStatus);
|
|
247
|
+
expect(response.body).to.have.property('success', true);
|
|
248
|
+
expect(response.body).to.have.property('data');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Validate error response structure
|
|
253
|
+
* @param {Object} response - API response
|
|
254
|
+
* @param {number} expectedStatus - Expected status code
|
|
255
|
+
* @param {string} expectedErrorCode - Expected error code (optional)
|
|
256
|
+
*/
|
|
257
|
+
validateErrorResponse(response, expectedStatus, expectedErrorCode = null) {
|
|
258
|
+
expect(response.status).to.eq(expectedStatus);
|
|
259
|
+
expect(response.body).to.have.property('success', false);
|
|
260
|
+
expect(response.body).to.have.property('error');
|
|
261
|
+
|
|
262
|
+
if (expectedErrorCode) {
|
|
263
|
+
expect(response.body).to.have.property('code', expectedErrorCode);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Validate board object structure
|
|
269
|
+
* @param {Object} board - Board object
|
|
270
|
+
*/
|
|
271
|
+
validateObject(board) {
|
|
272
|
+
// Required system fields
|
|
273
|
+
expect(board).to.have.property('id');
|
|
274
|
+
expect(board).to.have.property('createdAt');
|
|
275
|
+
expect(board).to.have.property('updatedAt');
|
|
276
|
+
|
|
277
|
+
// Required entity fields
|
|
278
|
+
expect(board).to.have.property('name');
|
|
279
|
+
expect(board.name).to.be.a('string');
|
|
280
|
+
|
|
281
|
+
// Optional fields
|
|
282
|
+
if (board.description !== null && board.description !== undefined) {
|
|
283
|
+
expect(board.description).to.be.a('string');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (board.color !== null && board.color !== undefined) {
|
|
287
|
+
expect(board.color).to.be.oneOf(['blue', 'green', 'purple', 'orange', 'red', 'pink', 'gray']);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (board.archived !== null && board.archived !== undefined) {
|
|
291
|
+
expect(board.archived).to.be.a('boolean');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Export class for use in tests
|
|
297
|
+
module.exports = BoardsAPIController;
|
|
298
|
+
|
|
299
|
+
// For global use in Cypress
|
|
300
|
+
if (typeof window !== 'undefined') {
|
|
301
|
+
window.BoardsAPIController = BoardsAPIController;
|
|
302
|
+
}
|