@nextsparkjs/core 0.1.0-beta.23 → 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.
Files changed (32) hide show
  1. package/dist/styles/classes.json +1 -1
  2. package/dist/styles/ui.css +1 -1
  3. package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/auth.cy.ts +199 -0
  4. package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +104 -0
  5. package/dist/templates/contents/themes/starter/tests/cypress/e2e/_selectors/tasks.cy.ts +274 -0
  6. package/dist/templates/contents/themes/starter/tests/cypress/support/e2e.ts +36 -4
  7. package/dist/templates/contents/themes/starter/tests/cypress.config.ts +18 -15
  8. package/dist/templates/contents/themes/starter/tests/jest/__mocks__/jose.js +22 -0
  9. package/dist/templates/contents/themes/starter/tests/jest/__mocks__/next-server.js +56 -0
  10. package/dist/templates/contents/themes/starter/tests/jest/example.test.ts +87 -0
  11. package/dist/templates/contents/themes/starter/tests/jest/jest.config.cjs +90 -0
  12. package/dist/templates/contents/themes/starter/tests/jest/services/tasks.service.test.ts +547 -0
  13. package/dist/templates/contents/themes/starter/tests/jest/setup.ts +170 -0
  14. package/package.json +12 -12
  15. package/scripts/build/docs-registry.mjs +0 -0
  16. package/scripts/create-theme.mjs +0 -0
  17. package/scripts/deploy/release-version.mjs +0 -0
  18. package/scripts/deploy/vercel-deploy.mjs +0 -0
  19. package/scripts/dev/watch-plugins.mjs +0 -0
  20. package/scripts/maintenance/update-core.mjs +0 -0
  21. package/scripts/setup/npm-postinstall.mjs +0 -0
  22. package/scripts/setup/setup-claude.mjs +0 -0
  23. package/scripts/validation/check-imports.sh +0 -0
  24. package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/auth.cy.ts +199 -0
  25. package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +104 -0
  26. package/templates/contents/themes/starter/tests/cypress/e2e/_selectors/tasks.cy.ts +274 -0
  27. package/templates/contents/themes/starter/tests/jest/__mocks__/jose.js +22 -0
  28. package/templates/contents/themes/starter/tests/jest/__mocks__/next-server.js +56 -0
  29. package/templates/contents/themes/starter/tests/jest/example.test.ts +87 -0
  30. package/templates/contents/themes/starter/tests/jest/jest.config.cjs +90 -0
  31. package/templates/contents/themes/starter/tests/jest/services/tasks.service.test.ts +547 -0
  32. package/templates/contents/themes/starter/tests/jest/setup.ts +170 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * UI Selectors Validation: Tasks Entity
3
+ *
4
+ * This test validates that Tasks entity selectors exist in the DOM.
5
+ * This is a lightweight test that ONLY checks selector presence, not functionality.
6
+ *
7
+ * Purpose:
8
+ * - Validate TasksPOM selectors work correctly
9
+ * - Ensure dynamic selector generation produces valid CSS selectors
10
+ * - Catch missing data-cy attributes early
11
+ *
12
+ * Scope:
13
+ * - Login and navigate to tasks pages
14
+ * - Assert elements exist in DOM (no full CRUD operations)
15
+ * - Fast execution (< 30 seconds)
16
+ */
17
+
18
+ import { TasksPOM } from '../tasks/TasksPOM'
19
+
20
+ describe('Tasks Entity Selectors Validation', { tags: ['@ui-selectors'] }, () => {
21
+ const tasks = TasksPOM.create()
22
+
23
+ beforeEach(() => {
24
+ // Login as owner before each test
25
+ cy.loginAsOwner()
26
+ })
27
+
28
+ // ============================================
29
+ // LIST PAGE SELECTORS
30
+ // ============================================
31
+ describe('List Page Selectors', () => {
32
+ beforeEach(() => {
33
+ tasks.visitList()
34
+ tasks.waitForList()
35
+ })
36
+
37
+ it('should find table container element', () => {
38
+ cy.get(tasks.selectors.tableContainer).should('exist')
39
+ })
40
+
41
+ it('should find add button', () => {
42
+ cy.get(tasks.selectors.addButton).should('exist')
43
+ })
44
+
45
+ it('should find search input', () => {
46
+ cy.get(tasks.selectors.search).should('exist')
47
+ })
48
+
49
+ it('should find search container', () => {
50
+ cy.get(tasks.selectors.searchContainer).should('exist')
51
+ })
52
+
53
+ it('should find select all checkbox', () => {
54
+ cy.get(tasks.selectors.selectAll).should('exist')
55
+ })
56
+
57
+ it('should find pagination container', () => {
58
+ cy.get(tasks.selectors.pagination).should('exist')
59
+ })
60
+
61
+ it('should find pagination controls', () => {
62
+ cy.get(tasks.selectors.pageFirst).should('exist')
63
+ cy.get(tasks.selectors.pagePrev).should('exist')
64
+ cy.get(tasks.selectors.pageNext).should('exist')
65
+ cy.get(tasks.selectors.pageLast).should('exist')
66
+ })
67
+
68
+ it('should find page size selector', () => {
69
+ cy.get(tasks.selectors.pageSize).should('exist')
70
+ })
71
+
72
+ it('should find page info', () => {
73
+ cy.get(tasks.selectors.pageInfo).should('exist')
74
+ })
75
+
76
+ it('should find at least one row with dynamic selector', () => {
77
+ cy.get(tasks.selectors.rowGeneric).should('have.length.at.least', 1)
78
+ })
79
+ })
80
+
81
+ // ============================================
82
+ // FILTER SELECTORS
83
+ // ============================================
84
+ describe('Filter Selectors', () => {
85
+ beforeEach(() => {
86
+ tasks.visitList()
87
+ tasks.waitForList()
88
+ })
89
+
90
+ it('should find status filter trigger', () => {
91
+ cy.get(tasks.selectors.filterTrigger('status')).should('exist')
92
+ })
93
+
94
+ it('should find priority filter trigger', () => {
95
+ cy.get(tasks.selectors.filterTrigger('priority')).should('exist')
96
+ })
97
+
98
+ it('should find filter options when opened', () => {
99
+ tasks.openFilter('status')
100
+ cy.get(tasks.selectors.filterContent('status')).should('be.visible')
101
+ })
102
+ })
103
+
104
+ // ============================================
105
+ // ROW DYNAMIC SELECTORS
106
+ // ============================================
107
+ describe('Row Dynamic Selectors', () => {
108
+ beforeEach(() => {
109
+ tasks.visitList()
110
+ tasks.waitForList()
111
+ })
112
+
113
+ it('should find row elements with dynamic ID', () => {
114
+ // Get any row and extract its ID to test dynamic selectors
115
+ cy.get(tasks.selectors.rowGeneric)
116
+ .first()
117
+ .invoke('attr', 'data-cy')
118
+ .then((dataCy) => {
119
+ // Extract ID from data-cy="tasks-row-{id}"
120
+ const id = dataCy?.replace('tasks-row-', '') || ''
121
+ expect(id).to.not.be.empty
122
+
123
+ // Test dynamic selector functions work
124
+ cy.get(tasks.selectors.row(id)).should('exist')
125
+ cy.get(tasks.selectors.rowSelect(id)).should('exist')
126
+ cy.get(tasks.selectors.rowMenu(id)).should('exist')
127
+ })
128
+ })
129
+ })
130
+
131
+ // ============================================
132
+ // CREATE PAGE SELECTORS
133
+ // ============================================
134
+ describe('Create Page Selectors', () => {
135
+ beforeEach(() => {
136
+ tasks.visitCreate()
137
+ tasks.waitForForm()
138
+ })
139
+
140
+ it('should find form container', () => {
141
+ cy.get(tasks.selectors.form).should('exist')
142
+ })
143
+
144
+ it('should find submit button', () => {
145
+ cy.get(tasks.selectors.submitButton).should('exist')
146
+ })
147
+
148
+ it('should find create header', () => {
149
+ cy.get(tasks.selectors.createHeader).should('exist')
150
+ })
151
+
152
+ it('should find back button', () => {
153
+ cy.get(tasks.selectors.backButton).should('exist')
154
+ })
155
+
156
+ it('should find title field', () => {
157
+ cy.get(tasks.selectors.field('title')).should('exist')
158
+ })
159
+
160
+ it('should find description field', () => {
161
+ cy.get(tasks.selectors.field('description')).should('exist')
162
+ })
163
+
164
+ it('should find status field', () => {
165
+ cy.get(tasks.selectors.field('status')).should('exist')
166
+ })
167
+
168
+ it('should find priority field', () => {
169
+ cy.get(tasks.selectors.field('priority')).should('exist')
170
+ })
171
+ })
172
+
173
+ // ============================================
174
+ // EDIT PAGE SELECTORS
175
+ // ============================================
176
+ describe('Edit Page Selectors', () => {
177
+ let testTaskId: string
178
+
179
+ beforeEach(() => {
180
+ // Create a task to edit
181
+ cy.createTask({
182
+ title: `Edit Test ${Date.now()}`,
183
+ status: 'todo',
184
+ priority: 'medium'
185
+ }).then((response) => {
186
+ if (response.status === 201) {
187
+ testTaskId = response.body.data.id
188
+ tasks.visitEdit(testTaskId)
189
+ tasks.waitForForm()
190
+ }
191
+ })
192
+ })
193
+
194
+ afterEach(() => {
195
+ // Clean up
196
+ if (testTaskId) {
197
+ cy.window().then((win) => {
198
+ const teamId = win.localStorage.getItem('activeTeamId')
199
+ cy.request({
200
+ method: 'DELETE',
201
+ url: `/api/v1/tasks/${testTaskId}`,
202
+ headers: { 'x-team-id': teamId || '' },
203
+ failOnStatusCode: false
204
+ })
205
+ })
206
+ }
207
+ })
208
+
209
+ it('should find form container', () => {
210
+ cy.get(tasks.selectors.form).should('exist')
211
+ })
212
+
213
+ it('should find edit header', () => {
214
+ cy.get(tasks.selectors.editHeader).should('exist')
215
+ })
216
+
217
+ it('should find submit button', () => {
218
+ cy.get(tasks.selectors.submitButton).should('exist')
219
+ })
220
+ })
221
+
222
+ // ============================================
223
+ // DETAIL PAGE SELECTORS
224
+ // ============================================
225
+ describe('Detail Page Selectors', () => {
226
+ let testTaskId: string
227
+
228
+ beforeEach(() => {
229
+ // Create a task to view
230
+ cy.createTask({
231
+ title: `Detail Test ${Date.now()}`,
232
+ status: 'todo',
233
+ priority: 'medium'
234
+ }).then((response) => {
235
+ if (response.status === 201) {
236
+ testTaskId = response.body.data.id
237
+ tasks.visitDetail(testTaskId)
238
+ tasks.waitForDetail()
239
+ }
240
+ })
241
+ })
242
+
243
+ afterEach(() => {
244
+ // Clean up
245
+ if (testTaskId) {
246
+ cy.window().then((win) => {
247
+ const teamId = win.localStorage.getItem('activeTeamId')
248
+ cy.request({
249
+ method: 'DELETE',
250
+ url: `/api/v1/tasks/${testTaskId}`,
251
+ headers: { 'x-team-id': teamId || '' },
252
+ failOnStatusCode: false
253
+ })
254
+ })
255
+ }
256
+ })
257
+
258
+ it('should find detail container', () => {
259
+ cy.get(tasks.selectors.detailContainer).should('exist')
260
+ })
261
+
262
+ it('should find detail header', () => {
263
+ cy.get(tasks.selectors.detailHeader).should('exist')
264
+ })
265
+
266
+ it('should find edit button', () => {
267
+ cy.get(tasks.selectors.editButton).should('exist')
268
+ })
269
+
270
+ it('should find delete button', () => {
271
+ cy.get(tasks.selectors.deleteButton).should('exist')
272
+ })
273
+ })
274
+ })
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mock for jose package
3
+ * Resolves ES module import issues in Jest tests
4
+ */
5
+
6
+ module.exports = {
7
+ jwtVerify: jest.fn(),
8
+ SignJWT: jest.fn().mockImplementation(() => ({
9
+ setProtectedHeader: jest.fn().mockReturnThis(),
10
+ setIssuedAt: jest.fn().mockReturnThis(),
11
+ setExpirationTime: jest.fn().mockReturnThis(),
12
+ sign: jest.fn().mockResolvedValue('mock-jwt-token')
13
+ })),
14
+ importJWK: jest.fn(),
15
+ generateSecret: jest.fn(),
16
+ createRemoteJWKSet: jest.fn(),
17
+ errors: {
18
+ JWTExpired: class JWTExpired extends Error {},
19
+ JWTInvalid: class JWTInvalid extends Error {},
20
+ JWTClaimValidationFailed: class JWTClaimValidationFailed extends Error {}
21
+ }
22
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Mock for next/server
3
+ * Provides NextRequest and NextResponse mocks for API testing
4
+ */
5
+
6
+ class MockNextRequest {
7
+ constructor(url, options = {}) {
8
+ this.url = url
9
+ this.method = options.method || 'GET'
10
+ this.headers = new Map(Object.entries(options.headers || {}))
11
+ this._body = options.body
12
+ }
13
+
14
+ async json() {
15
+ if (!this._body) return {}
16
+ try {
17
+ return typeof this._body === 'string' ? JSON.parse(this._body) : this._body
18
+ } catch {
19
+ throw new SyntaxError('Invalid JSON')
20
+ }
21
+ }
22
+
23
+ async text() {
24
+ return this._body || ''
25
+ }
26
+ }
27
+
28
+ class MockNextResponse {
29
+ constructor(body, options = {}) {
30
+ this.body = body
31
+ this.status = options.status || 200
32
+ this.statusText = options.statusText || 'OK'
33
+ this.headers = new Map(Object.entries(options.headers || {}))
34
+ }
35
+
36
+ async json() {
37
+ if (!this.body) return {}
38
+ try {
39
+ return typeof this.body === 'string' ? JSON.parse(this.body) : this.body
40
+ } catch {
41
+ throw new SyntaxError('Invalid JSON')
42
+ }
43
+ }
44
+
45
+ static json(data, options = {}) {
46
+ return new MockNextResponse(data, {
47
+ ...options,
48
+ headers: { 'Content-Type': 'application/json', ...options.headers }
49
+ })
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ NextRequest: MockNextRequest,
55
+ NextResponse: MockNextResponse
56
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Example Jest Test
3
+ *
4
+ * This file demonstrates how to write Jest tests for your theme.
5
+ * Run tests with: pnpm test:theme
6
+ *
7
+ * Test Organization:
8
+ * - Create test files with .test.ts or .spec.ts extension
9
+ * - Group related tests in describe blocks
10
+ * - Use meaningful test names that describe the expected behavior
11
+ */
12
+
13
+ import { render, screen } from '@testing-library/react'
14
+
15
+ // Example: Testing a simple utility function
16
+ describe('Example Utility Tests', () => {
17
+ it('should demonstrate a simple passing test', () => {
18
+ const sum = (a: number, b: number) => a + b
19
+ expect(sum(2, 3)).toBe(5)
20
+ })
21
+
22
+ it('should work with async/await', async () => {
23
+ const fetchData = async () => ({ status: 'ok' })
24
+ const result = await fetchData()
25
+ expect(result.status).toBe('ok')
26
+ })
27
+ })
28
+
29
+ // Example: Testing with mocked fetch
30
+ describe('API Mock Tests', () => {
31
+ beforeEach(() => {
32
+ // Reset fetch mock before each test
33
+ ;(global.fetch as jest.Mock).mockReset()
34
+ })
35
+
36
+ it('should mock fetch responses', async () => {
37
+ const mockData = { users: ['Alice', 'Bob'] }
38
+
39
+ ;(global.fetch as jest.Mock).mockResolvedValueOnce({
40
+ ok: true,
41
+ status: 200,
42
+ json: async () => mockData,
43
+ })
44
+
45
+ const response = await fetch('/api/users')
46
+ const data = await response.json()
47
+
48
+ expect(data.users).toHaveLength(2)
49
+ expect(data.users).toContain('Alice')
50
+ })
51
+ })
52
+
53
+ // Example: Testing React components
54
+ describe('React Component Tests', () => {
55
+ it('should render a simple component', () => {
56
+ const SimpleComponent = () => <div data-testid="greeting">Hello World</div>
57
+
58
+ render(<SimpleComponent />)
59
+
60
+ expect(screen.getByTestId('greeting')).toHaveTextContent('Hello World')
61
+ })
62
+
63
+ it('should use jest-dom matchers', () => {
64
+ const Button = ({ disabled }: { disabled: boolean }) => (
65
+ <button disabled={disabled}>Click me</button>
66
+ )
67
+
68
+ render(<Button disabled={true} />)
69
+
70
+ const button = screen.getByRole('button')
71
+ expect(button).toBeDisabled()
72
+ expect(button).toHaveTextContent('Click me')
73
+ })
74
+ })
75
+
76
+ // Example: Testing with theme imports
77
+ describe('Theme Integration Tests', () => {
78
+ // You can import theme utilities and test them
79
+ // import { formatDate } from '../../lib/utils'
80
+
81
+ it('should work with theme utilities', () => {
82
+ // Example test placeholder
83
+ // const formatted = formatDate(new Date('2024-01-15'))
84
+ // expect(formatted).toBe('January 15, 2024')
85
+ expect(true).toBe(true)
86
+ })
87
+ })
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Jest Configuration for Starter Theme
3
+ *
4
+ * This config is for npm mode (projects created via nextspark init).
5
+ * Run with: pnpm test:theme
6
+ */
7
+
8
+ const path = require('path')
9
+
10
+ // Paths relative to this config file
11
+ const themeTestsRoot = __dirname
12
+ const themeRoot = path.resolve(__dirname, '../..')
13
+
14
+ // In npm mode: contents/themes/starter/tests/jest -> project root (5 levels up)
15
+ const projectRoot = path.resolve(__dirname, '../../../../..')
16
+
17
+ /** @type {import('jest').Config} */
18
+ module.exports = {
19
+ displayName: 'theme-starter',
20
+ rootDir: projectRoot,
21
+
22
+ // Use roots to explicitly set test location
23
+ roots: [themeTestsRoot],
24
+
25
+ // Test file patterns
26
+ testMatch: [
27
+ '**/*.{test,spec}.{js,ts,tsx}',
28
+ ],
29
+ testPathIgnorePatterns: [
30
+ '<rootDir>/node_modules/',
31
+ '<rootDir>/.next/',
32
+ ],
33
+
34
+ // Preset and environment
35
+ preset: 'ts-jest',
36
+ testEnvironment: 'jsdom',
37
+
38
+ // Module resolution for npm mode
39
+ moduleNameMapper: {
40
+ // Resolve from node_modules
41
+ '^@nextsparkjs/core/(.*)$': '@nextsparkjs/core/$1',
42
+ '^@nextsparkjs/core$': '@nextsparkjs/core',
43
+ '^@/contents/(.*)$': '<rootDir>/contents/$1',
44
+ '^@/entities/(.*)$': '<rootDir>/contents/entities/$1',
45
+ '^@/plugins/(.*)$': '<rootDir>/contents/plugins/$1',
46
+ '^@/themes/(.*)$': '<rootDir>/contents/themes/$1',
47
+ '^@/(.*)$': '<rootDir>/$1',
48
+ // Mocks from theme-local folder
49
+ 'next/server': path.join(themeTestsRoot, '__mocks__/next-server.js'),
50
+ '^jose$': path.join(themeTestsRoot, '__mocks__/jose.js'),
51
+ '^jose/(.*)$': path.join(themeTestsRoot, '__mocks__/jose.js'),
52
+ },
53
+
54
+ // Setup files
55
+ setupFilesAfterEnv: [
56
+ path.join(themeTestsRoot, 'setup.ts'),
57
+ ],
58
+
59
+ // Transform configuration
60
+ transform: {
61
+ '^.+\\.(ts|tsx)$': ['ts-jest', {
62
+ tsconfig: path.join(projectRoot, 'tsconfig.json'),
63
+ }],
64
+ },
65
+
66
+ // Transform ignore patterns
67
+ transformIgnorePatterns: [
68
+ 'node_modules/(?!(uncrypto|better-auth|@noble|.*jose.*|remark.*|unified.*|@nextsparkjs/core/tests|.*\\.mjs$))',
69
+ 'node_modules/\\.pnpm/(?!(.*uncrypto.*|.*better-auth.*|.*@noble.*|.*jose.*|.*remark.*|.*unified.*|@nextsparkjs.*core.*tests|.*\\.mjs$))',
70
+ ],
71
+
72
+ // File extensions
73
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
74
+
75
+ // Test timeout
76
+ testTimeout: 10000,
77
+
78
+ // Verbose output
79
+ verbose: true,
80
+
81
+ // Force exit after tests complete
82
+ forceExit: true,
83
+
84
+ // Disable watchman for symlink support
85
+ watchman: false,
86
+
87
+ // Coverage output directory
88
+ coverageDirectory: path.join(themeTestsRoot, 'coverage'),
89
+ coverageReporters: ['text', 'lcov', 'html'],
90
+ }