@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.
@@ -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,9 @@
1
+ /**
2
+ * Productivity Theme - POM Components Index
3
+ *
4
+ * Exports all Page Object Models for the productivity theme.
5
+ */
6
+
7
+ export { BoardsPOM } from './BoardsPOM'
8
+ export { KanbanPOM } from './KanbanPOM'
9
+ export { CardsPOM } from './CardsPOM'
@@ -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
+ }