@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,243 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Kanban Columns (Lists) - Owner Role Tests
5
+ *
6
+ * Tests for managing columns/lists on the Kanban board.
7
+ * All selectors follow the pattern: lists-{component}-{detail}
8
+ *
9
+ * @see test/cypress/src/classes/themes/productivity/components/KanbanPOM.ts
10
+ */
11
+
12
+ import { KanbanPOM } from '../../../src/components/KanbanPOM'
13
+ import { BoardsPOM } from '../../../src/components/BoardsPOM'
14
+ import { loginAsProductivityOwner } from '../../../src/session-helpers'
15
+
16
+ describe('Kanban Columns (Lists) - Owner Role', () => {
17
+ let testBoardId: string
18
+
19
+ before(() => {
20
+ // Create a board for all kanban tests
21
+ loginAsProductivityOwner()
22
+ BoardsPOM.visitList()
23
+ BoardsPOM.waitForListLoad()
24
+
25
+ const timestamp = Date.now()
26
+ const boardName = `Kanban Test Board ${timestamp}`
27
+
28
+ BoardsPOM.clickCreate()
29
+ BoardsPOM.waitForFormLoad()
30
+ BoardsPOM.fillName(boardName)
31
+ BoardsPOM.submitForm()
32
+
33
+ // Extract board ID from URL
34
+ cy.url().then((url) => {
35
+ const match = url.match(/\/boards\/([a-z0-9_-]+)/)
36
+ if (match) {
37
+ testBoardId = match[1]
38
+ }
39
+ })
40
+ })
41
+
42
+ beforeEach(() => {
43
+ loginAsProductivityOwner()
44
+ cy.then(() => {
45
+ KanbanPOM.visitBoard(testBoardId)
46
+ KanbanPOM.waitForBoardLoad()
47
+ })
48
+ })
49
+
50
+ after(() => {
51
+ // Cleanup: Delete the test board
52
+ loginAsProductivityOwner()
53
+ cy.then(() => {
54
+ if (testBoardId) {
55
+ BoardsPOM.visitEdit(testBoardId)
56
+ BoardsPOM.waitForFormLoad()
57
+ BoardsPOM.confirmDelete()
58
+ BoardsPOM.deleteFromEdit()
59
+ }
60
+ })
61
+ })
62
+
63
+ describe('CREATE - Add new columns', () => {
64
+ it('KANBAN_COLUMNS_CREATE_001: should add a new column with name', () => {
65
+ const timestamp = Date.now()
66
+ const columnName = `New Column ${timestamp}`
67
+
68
+ // Click add column button
69
+ KanbanPOM.clickAddColumn()
70
+
71
+ // Fill column name
72
+ KanbanPOM.fillColumnName(columnName)
73
+
74
+ // Submit
75
+ KanbanPOM.submitAddColumn()
76
+
77
+ // Verify column was created
78
+ KanbanPOM.assertBoardContains(columnName)
79
+
80
+ cy.log('✅ Created new column successfully')
81
+ })
82
+
83
+ it('KANBAN_COLUMNS_CREATE_002: should create multiple columns', () => {
84
+ const timestamp = Date.now()
85
+ const columns = ['To Do', 'In Progress', 'Done'].map((name) => `${name} ${timestamp}`)
86
+
87
+ // Create each column
88
+ columns.forEach((columnName) => {
89
+ KanbanPOM.clickAddColumn()
90
+ KanbanPOM.fillColumnName(columnName)
91
+ KanbanPOM.submitAddColumn()
92
+ cy.wait(500) // Wait for creation to complete
93
+ })
94
+
95
+ // Verify all columns exist
96
+ columns.forEach((columnName) => {
97
+ KanbanPOM.assertBoardContains(columnName)
98
+ })
99
+
100
+ cy.log('✅ Created multiple columns successfully')
101
+ })
102
+
103
+ it('KANBAN_COLUMNS_CREATE_003: should cancel column creation', () => {
104
+ const timestamp = Date.now()
105
+ const columnName = `Cancelled Column ${timestamp}`
106
+
107
+ // Click add column button
108
+ KanbanPOM.clickAddColumn()
109
+
110
+ // Fill column name
111
+ KanbanPOM.fillColumnName(columnName)
112
+
113
+ // Press Escape to cancel
114
+ cy.get(KanbanPOM.selectors.columnFieldName).type('{esc}')
115
+
116
+ // Wait a moment
117
+ cy.wait(500)
118
+
119
+ // Column should not exist
120
+ cy.contains(columnName).should('not.exist')
121
+
122
+ cy.log('✅ Column creation cancelled successfully')
123
+ })
124
+ })
125
+
126
+ describe('READ - View columns', () => {
127
+ it('KANBAN_COLUMNS_READ_001: should display all columns', () => {
128
+ // Board should be visible
129
+ KanbanPOM.validateBoardVisible()
130
+
131
+ // Should have at least the add column button
132
+ cy.get(KanbanPOM.selectors.addColumn).should('be.visible')
133
+
134
+ cy.log('✅ Kanban board displays correctly')
135
+ })
136
+
137
+ it('KANBAN_COLUMNS_READ_002: should show column card count', () => {
138
+ // Check if columns have card counts displayed
139
+ cy.get('[data-cy^="lists-column-"]').first().then(($column) => {
140
+ // The column header should show count
141
+ cy.wrap($column).find('.text-xs').should('exist')
142
+ })
143
+
144
+ cy.log('✅ Column card counts are visible')
145
+ })
146
+ })
147
+
148
+ describe('UPDATE - Rename columns', () => {
149
+ let testColumnId: string
150
+
151
+ beforeEach(() => {
152
+ // Create a column to update
153
+ const timestamp = Date.now()
154
+ const columnName = `Update Test ${timestamp}`
155
+
156
+ KanbanPOM.createColumn(columnName)
157
+ cy.wait(500)
158
+
159
+ // Get the column ID
160
+ cy.contains('[data-cy^="lists-column-"]', columnName).then(($column) => {
161
+ const dataCy = $column.attr('data-cy')
162
+ if (dataCy) {
163
+ testColumnId = dataCy.replace('lists-column-', '')
164
+ }
165
+ })
166
+ })
167
+
168
+ it('KANBAN_COLUMNS_UPDATE_001: should rename column by clicking title', () => {
169
+ const updatedName = `Renamed Column ${Date.now()}`
170
+
171
+ cy.then(() => {
172
+ // Click on column title to edit
173
+ KanbanPOM.clickColumnTitle(testColumnId)
174
+
175
+ // Wait for input to appear
176
+ cy.wait(300)
177
+
178
+ // Type new name and submit
179
+ cy.get('input').last().clear().type(`${updatedName}{enter}`)
180
+
181
+ // Verify name changed
182
+ cy.wait(500)
183
+ cy.contains(updatedName).should('be.visible')
184
+ })
185
+
186
+ cy.log('✅ Column renamed successfully')
187
+ })
188
+
189
+ it('KANBAN_COLUMNS_UPDATE_002: should rename column via menu', () => {
190
+ const updatedName = `Menu Renamed ${Date.now()}`
191
+
192
+ cy.then(() => {
193
+ // Open column menu
194
+ KanbanPOM.openColumnMenu(testColumnId)
195
+
196
+ // Click rename option
197
+ cy.get('[role="menuitem"]').contains('Rename').click()
198
+
199
+ // Wait for input to appear
200
+ cy.wait(300)
201
+
202
+ // Type new name and submit
203
+ cy.get('input').last().clear().type(`${updatedName}{enter}`)
204
+
205
+ // Verify name changed
206
+ cy.wait(500)
207
+ cy.contains(updatedName).should('be.visible')
208
+ })
209
+
210
+ cy.log('✅ Column renamed via menu successfully')
211
+ })
212
+ })
213
+
214
+ describe('DELETE - Remove columns', () => {
215
+ it('KANBAN_COLUMNS_DELETE_001: should delete column via menu', () => {
216
+ // Create a column to delete
217
+ const timestamp = Date.now()
218
+ const columnName = `Delete Test ${timestamp}`
219
+
220
+ KanbanPOM.createColumn(columnName)
221
+ cy.wait(500)
222
+
223
+ // Get the column ID
224
+ cy.contains('[data-cy^="lists-column-"]', columnName).then(($column) => {
225
+ const dataCy = $column.attr('data-cy')
226
+ if (dataCy) {
227
+ const columnId = dataCy.replace('lists-column-', '')
228
+
229
+ // Delete the column
230
+ KanbanPOM.clickColumnDelete(columnId)
231
+
232
+ // Wait for deletion
233
+ cy.wait(500)
234
+
235
+ // Verify column is gone
236
+ cy.contains(columnName).should('not.exist')
237
+ }
238
+ })
239
+
240
+ cy.log('✅ Column deleted successfully')
241
+ })
242
+ })
243
+ })
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./blocks.schema.json",
3
+ "_warning": "AUTO-GENERATED by build-registry.mjs - DO NOT EDIT MANUALLY",
4
+ "_theme": "productivity",
5
+ "_totalBlocks": 0,
6
+ "available": [],
7
+ "categories": [],
8
+ "byCategory": {}
9
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "./entities.schema.json",
3
+ "_warning": "AUTO-GENERATED by build-registry.mjs - DO NOT EDIT MANUALLY",
4
+ "_theme": "productivity",
5
+ "entities": {
6
+ "boards": {
7
+ "slug": "boards",
8
+ "singular": "board",
9
+ "plural": "Boards",
10
+ "tableName": "boards",
11
+ "fields": [
12
+ "name",
13
+ "description",
14
+ "color",
15
+ "archived",
16
+ "position",
17
+ "createdAt",
18
+ "updatedAt"
19
+ ],
20
+ "filters": [],
21
+ "source": "theme"
22
+ },
23
+ "cards": {
24
+ "slug": "cards",
25
+ "singular": "card",
26
+ "plural": "Cards",
27
+ "tableName": "cards",
28
+ "fields": [
29
+ "title",
30
+ "description",
31
+ "position",
32
+ "dueDate",
33
+ "priority",
34
+ "labels",
35
+ "assigneeId",
36
+ "listId",
37
+ "boardId",
38
+ "createdAt",
39
+ "updatedAt"
40
+ ],
41
+ "filters": [],
42
+ "source": "theme"
43
+ },
44
+ "lists": {
45
+ "slug": "lists",
46
+ "singular": "list",
47
+ "plural": "Lists",
48
+ "tableName": "lists",
49
+ "fields": [
50
+ "name",
51
+ "position",
52
+ "boardId",
53
+ "createdAt",
54
+ "updatedAt"
55
+ ],
56
+ "filters": [],
57
+ "source": "theme"
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Boards Page Object Model - Entity Testing Convention
3
+ *
4
+ * POM for managing boards in the productivity theme.
5
+ * Follows the pattern: {slug}-{component}-{detail}
6
+ *
7
+ * Convention: boards-{component}-{detail}
8
+ * Examples:
9
+ * - boards-page (list page container)
10
+ * - boards-create-btn (create button)
11
+ * - boards-card-{id} (board card)
12
+ * - boards-form (form container)
13
+ * - boards-field-name (name input)
14
+ *
15
+ * @see test/cypress/fixtures/themes/productivity/entities.json
16
+ */
17
+
18
+ import entitiesConfig from '../../fixtures/entities.json'
19
+
20
+ // Get boards entity config from JSON
21
+ const boardsEntity = entitiesConfig.entities.boards
22
+ const slug = boardsEntity.slug
23
+
24
+ /**
25
+ * Boards Page Object Model
26
+ *
27
+ * Uses the entity testing convention for consistent, maintainable selectors.
28
+ */
29
+ export class BoardsPOM {
30
+ // ============================================
31
+ // ENTITY METADATA (from entities.json)
32
+ // ============================================
33
+
34
+ static get entityConfig() {
35
+ return boardsEntity
36
+ }
37
+
38
+ static get slug() {
39
+ return slug
40
+ }
41
+
42
+ static get fields() {
43
+ return boardsEntity.fields
44
+ }
45
+
46
+ // ============================================
47
+ // SELECTORS
48
+ // ============================================
49
+
50
+ static get selectors() {
51
+ return {
52
+ // List Page
53
+ page: `[data-cy="${slug}-page"]`,
54
+ createBtn: `[data-cy="${slug}-create-btn"]`,
55
+ createCard: `[data-cy="${slug}-create-card"]`,
56
+
57
+ // Board Cards (dynamic)
58
+ card: (id: string) => `[data-cy="${slug}-card-${id}"]`,
59
+ cardMenu: (id: string) => `[data-cy="${slug}-card-menu-${id}"]`,
60
+ cardEdit: (id: string) => `[data-cy="${slug}-card-edit-${id}"]`,
61
+ cardArchive: (id: string) => `[data-cy="${slug}-card-archive-${id}"]`,
62
+ cardDelete: (id: string) => `[data-cy="${slug}-card-delete-${id}"]`,
63
+
64
+ // Create Page
65
+ createPage: `[data-cy="${slug}-create-page"]`,
66
+
67
+ // Edit Page
68
+ editPage: `[data-cy="${slug}-edit-page"]`,
69
+
70
+ // Form (shared between create/edit)
71
+ form: `[data-cy="${slug}-form"]`,
72
+ fieldName: `[data-cy="${slug}-field-name"]`,
73
+ fieldDescription: `[data-cy="${slug}-field-description"]`,
74
+ fieldColor: `[data-cy="${slug}-field-color"]`,
75
+ fieldArchived: `[data-cy="${slug}-field-archived"]`,
76
+ formSubmit: `[data-cy="${slug}-form-submit"]`,
77
+ formCancel: `[data-cy="${slug}-form-cancel"]`,
78
+ formDelete: `[data-cy="${slug}-form-delete"]`,
79
+ }
80
+ }
81
+
82
+ // ============================================
83
+ // LIST PAGE ACTIONS
84
+ // ============================================
85
+
86
+ /**
87
+ * Visit the boards list page
88
+ */
89
+ static visitList() {
90
+ cy.visit('/dashboard/boards')
91
+ return this
92
+ }
93
+
94
+ /**
95
+ * Wait for list page to load
96
+ */
97
+ static waitForListLoad() {
98
+ cy.url().should('include', '/dashboard/boards')
99
+ cy.get(this.selectors.page, { timeout: 15000 }).should('be.visible')
100
+ return this
101
+ }
102
+
103
+ /**
104
+ * Validate list page is visible
105
+ */
106
+ static validateListPageVisible() {
107
+ cy.get(this.selectors.page).should('be.visible')
108
+ return this
109
+ }
110
+
111
+ /**
112
+ * Click create button
113
+ */
114
+ static clickCreate() {
115
+ cy.get(this.selectors.createBtn).click()
116
+ return this
117
+ }
118
+
119
+ /**
120
+ * Click on a board card by ID
121
+ */
122
+ static clickCard(id: string) {
123
+ cy.get(this.selectors.card(id)).click()
124
+ return this
125
+ }
126
+
127
+ /**
128
+ * Open board card menu
129
+ */
130
+ static openCardMenu(id: string) {
131
+ cy.get(this.selectors.cardMenu(id)).click({ force: true })
132
+ return this
133
+ }
134
+
135
+ /**
136
+ * Click edit from card menu
137
+ */
138
+ static clickCardEdit(id: string) {
139
+ this.openCardMenu(id)
140
+ cy.get(this.selectors.cardEdit(id)).click()
141
+ return this
142
+ }
143
+
144
+ /**
145
+ * Click archive from card menu
146
+ */
147
+ static clickCardArchive(id: string) {
148
+ this.openCardMenu(id)
149
+ cy.get(this.selectors.cardArchive(id)).click()
150
+ return this
151
+ }
152
+
153
+ /**
154
+ * Click delete from card menu
155
+ */
156
+ static clickCardDelete(id: string) {
157
+ this.openCardMenu(id)
158
+ cy.get(this.selectors.cardDelete(id)).click()
159
+ return this
160
+ }
161
+
162
+ /**
163
+ * Get board card count
164
+ */
165
+ static getBoardCount(): Cypress.Chainable<number> {
166
+ return cy.get(`[data-cy^="${slug}-card-"]`).its('length')
167
+ }
168
+
169
+ // ============================================
170
+ // FORM PAGE ACTIONS
171
+ // ============================================
172
+
173
+ /**
174
+ * Visit create form
175
+ */
176
+ static visitCreate() {
177
+ cy.visit('/dashboard/boards/create')
178
+ return this
179
+ }
180
+
181
+ /**
182
+ * Visit edit form
183
+ */
184
+ static visitEdit(id: string) {
185
+ cy.visit(`/dashboard/boards/${id}/edit`)
186
+ return this
187
+ }
188
+
189
+ /**
190
+ * Wait for form to load
191
+ */
192
+ static waitForFormLoad() {
193
+ cy.get(this.selectors.form, { timeout: 10000 }).should('be.visible')
194
+ return this
195
+ }
196
+
197
+ /**
198
+ * Validate form is visible
199
+ */
200
+ static validateFormVisible() {
201
+ cy.get(this.selectors.form).should('be.visible')
202
+ return this
203
+ }
204
+
205
+ /**
206
+ * Fill the name field
207
+ */
208
+ static fillName(name: string) {
209
+ cy.get(this.selectors.fieldName).clear().type(name)
210
+ return this
211
+ }
212
+
213
+ /**
214
+ * Fill the description field
215
+ */
216
+ static fillDescription(description: string) {
217
+ cy.get(this.selectors.fieldDescription).clear().type(description)
218
+ return this
219
+ }
220
+
221
+ /**
222
+ * Select a color
223
+ */
224
+ static selectColor(color: string) {
225
+ cy.get(this.selectors.fieldColor).click()
226
+ cy.get(`[data-value="${color}"]`).click()
227
+ return this
228
+ }
229
+
230
+ /**
231
+ * Toggle archived status
232
+ */
233
+ static toggleArchived() {
234
+ cy.get(this.selectors.fieldArchived).click()
235
+ return this
236
+ }
237
+
238
+ /**
239
+ * Submit the form
240
+ */
241
+ static submitForm() {
242
+ cy.get(this.selectors.formSubmit).click()
243
+ return this
244
+ }
245
+
246
+ /**
247
+ * Cancel the form
248
+ */
249
+ static cancelForm() {
250
+ cy.get(this.selectors.formCancel).click()
251
+ return this
252
+ }
253
+
254
+ /**
255
+ * Delete from edit page
256
+ */
257
+ static deleteFromEdit() {
258
+ cy.get(this.selectors.formDelete).click()
259
+ return this
260
+ }
261
+
262
+ /**
263
+ * Fill board form with data
264
+ */
265
+ static fillBoardForm(data: { name?: string; description?: string; color?: string }) {
266
+ if (data.name) {
267
+ this.fillName(data.name)
268
+ }
269
+ if (data.description) {
270
+ this.fillDescription(data.description)
271
+ }
272
+ if (data.color) {
273
+ this.selectColor(data.color)
274
+ }
275
+ return this
276
+ }
277
+
278
+ /**
279
+ * Create a board with given data
280
+ */
281
+ static createBoard(data: { name: string; description?: string; color?: string }) {
282
+ this.visitCreate()
283
+ this.waitForFormLoad()
284
+ this.fillBoardForm(data)
285
+ this.submitForm()
286
+ return this
287
+ }
288
+
289
+ // ============================================
290
+ // ASSERTIONS
291
+ // ============================================
292
+
293
+ /**
294
+ * Assert board exists in list by name
295
+ */
296
+ static assertBoardInList(name: string) {
297
+ cy.contains(name).should('be.visible')
298
+ return this
299
+ }
300
+
301
+ /**
302
+ * Assert board does not exist in list
303
+ */
304
+ static assertBoardNotInList(name: string) {
305
+ cy.contains(name).should('not.exist')
306
+ return this
307
+ }
308
+
309
+ /**
310
+ * Assert current URL matches boards list
311
+ */
312
+ static assertOnListPage() {
313
+ cy.url().should('include', '/dashboard/boards')
314
+ cy.url().should('not.include', '/create')
315
+ cy.url().should('not.include', '/edit')
316
+ return this
317
+ }
318
+
319
+ /**
320
+ * Assert current URL is create page
321
+ */
322
+ static assertOnCreatePage() {
323
+ cy.url().should('include', '/dashboard/boards/create')
324
+ return this
325
+ }
326
+
327
+ /**
328
+ * Assert current URL is edit page
329
+ */
330
+ static assertOnEditPage(id: string) {
331
+ cy.url().should('include', `/dashboard/boards/${id}/edit`)
332
+ return this
333
+ }
334
+
335
+ /**
336
+ * Assert form field has value
337
+ */
338
+ static assertFieldValue(field: 'name' | 'description', expectedValue: string) {
339
+ const selector = field === 'name' ? this.selectors.fieldName : this.selectors.fieldDescription
340
+ cy.get(selector).should('have.value', expectedValue)
341
+ return this
342
+ }
343
+
344
+ /**
345
+ * Confirm deletion dialog
346
+ */
347
+ static confirmDelete() {
348
+ cy.on('window:confirm', () => true)
349
+ return this
350
+ }
351
+ }
352
+
353
+ export default BoardsPOM