@nextsparkjs/theme-crm 0.1.0-beta.1 → 0.1.0-beta.100

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/config/app.config.ts +4 -5
  3. package/config/dashboard.config.ts +13 -0
  4. package/config/permissions.config.ts +11 -0
  5. package/entities/activities/api/docs.md +146 -0
  6. package/entities/activities/api/presets.ts +132 -0
  7. package/entities/campaigns/api/docs.md +140 -0
  8. package/entities/campaigns/api/presets.ts +147 -0
  9. package/entities/companies/api/docs.md +139 -0
  10. package/entities/companies/api/presets.ts +107 -0
  11. package/entities/contacts/api/docs.md +139 -0
  12. package/entities/contacts/api/presets.ts +98 -0
  13. package/entities/leads/api/docs.md +132 -0
  14. package/entities/leads/api/presets.ts +109 -0
  15. package/entities/notes/api/docs.md +135 -0
  16. package/entities/notes/api/presets.ts +115 -0
  17. package/entities/opportunities/api/docs.md +151 -0
  18. package/entities/opportunities/api/presets.ts +151 -0
  19. package/entities/pipelines/api/docs.md +145 -0
  20. package/entities/pipelines/api/presets.ts +118 -0
  21. package/entities/products/api/docs.md +137 -0
  22. package/entities/products/api/presets.ts +132 -0
  23. package/lib/selectors.ts +2 -3
  24. package/package.json +8 -3
  25. package/styles/globals.css +37 -1
  26. package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
  27. package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
  28. package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
  29. package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
  30. package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
  31. package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
  32. package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
  33. package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
  34. package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
  35. package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
  36. package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
  37. package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
  38. package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
  39. package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
  40. package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
  41. package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
  42. package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
  43. package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
  44. package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
  45. package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
  46. package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
  47. package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
  48. package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
  49. package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
  50. package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
  51. package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
  52. package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
  53. package/tests/cypress/fixtures/blocks.json +9 -0
  54. package/tests/cypress/fixtures/entities.json +240 -0
  55. package/tests/cypress/src/components/CRMDataTable.js +223 -0
  56. package/tests/cypress/src/components/CRMMobileNav.js +138 -0
  57. package/tests/cypress/src/components/CRMSidebar.js +145 -0
  58. package/tests/cypress/src/components/CRMTopBar.js +194 -0
  59. package/tests/cypress/src/components/DealCard.js +197 -0
  60. package/tests/cypress/src/components/EntityDetail.ts +290 -0
  61. package/tests/cypress/src/components/EntityForm.ts +357 -0
  62. package/tests/cypress/src/components/EntityList.ts +360 -0
  63. package/tests/cypress/src/components/PipelineKanban.js +204 -0
  64. package/tests/cypress/src/components/StageColumn.js +196 -0
  65. package/tests/cypress/src/components/index.js +13 -0
  66. package/tests/cypress/src/components/index.ts +22 -0
  67. package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
  68. package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
  69. package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
  70. package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
  71. package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
  72. package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
  73. package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
  74. package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
  75. package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
  76. package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
  77. package/tests/cypress/src/controllers/index.ts +35 -0
  78. package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
  79. package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
  80. package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
  81. package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
  82. package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
  83. package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
  84. package/tests/cypress/src/entities/index.ts +31 -0
  85. package/tests/cypress/src/forms/OpportunityForm.js +316 -0
  86. package/tests/cypress/src/forms/PipelineForm.js +243 -0
  87. package/tests/cypress/src/forms/index.js +8 -0
  88. package/tests/cypress/src/index.js +22 -0
  89. package/tests/cypress/src/index.ts +68 -0
  90. package/tests/cypress/src/selectors.ts +50 -0
  91. package/tests/cypress/src/session-helpers.ts +176 -0
  92. package/tests/cypress/support/e2e.ts +90 -0
  93. package/tests/cypress.config.ts +154 -0
  94. package/tests/jest/__mocks__/jose.js +22 -0
  95. package/tests/jest/__mocks__/next-server.js +56 -0
  96. package/tests/jest/jest.config.cjs +131 -0
  97. package/tests/jest/setup.ts +170 -0
  98. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * CRM Theme Session Helpers
3
+ *
4
+ * Direct login functions for CRM theme tests using CRM-specific users.
5
+ * Uses API-based login for faster and more stable authentication.
6
+ */
7
+
8
+ /**
9
+ * CRM Test Users - hardcoded for CRM theme tests
10
+ */
11
+ export const CRM_USERS = {
12
+ OWNER: 'crm_owner_roberto@nextspark.dev', // CEO
13
+ ADMIN: 'crm_admin_sofia@nextspark.dev', // Sales Manager
14
+ MEMBER: 'crm_member_miguel@nextspark.dev', // Sales Rep
15
+ LAURA: 'crm_member_laura@nextspark.dev', // Marketing
16
+ } as const
17
+
18
+ // Default test password
19
+ const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
20
+ const API_TIMEOUT = 60000
21
+
22
+ /**
23
+ * API login helper
24
+ */
25
+ function apiLogin(email: string, password: string = TEST_PASSWORD): Cypress.Chainable<boolean> {
26
+ return cy.request({
27
+ method: 'POST',
28
+ url: '/api/auth/sign-in/email',
29
+ body: { email, password },
30
+ timeout: API_TIMEOUT,
31
+ failOnStatusCode: false
32
+ }).then((response) => {
33
+ if (response.status === 200) {
34
+ return true
35
+ } else {
36
+ cy.log(`⚠️ API login failed with status ${response.status}`)
37
+ return false
38
+ }
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Setup team context after login
44
+ */
45
+ function setupTeamContext(preferredRole?: string) {
46
+ cy.request({
47
+ method: 'GET',
48
+ url: '/api/v1/teams',
49
+ timeout: API_TIMEOUT,
50
+ failOnStatusCode: false
51
+ }).then((teamsResponse) => {
52
+ if (teamsResponse.status === 200 && teamsResponse.body?.data?.length > 0) {
53
+ const teams = teamsResponse.body.data
54
+ let selectedTeam = teams[0]
55
+ if (preferredRole) {
56
+ const teamWithRole = teams.find((t: { role: string }) => t.role === preferredRole)
57
+ if (teamWithRole) {
58
+ selectedTeam = teamWithRole
59
+ }
60
+ }
61
+ const teamId = selectedTeam.id
62
+ cy.log(`✅ Setting active team: ${selectedTeam.name} (${teamId})`)
63
+ cy.window().then((win) => {
64
+ win.localStorage.setItem('activeTeamId', teamId)
65
+ })
66
+ cy.request({
67
+ method: 'POST',
68
+ url: '/api/v1/teams/switch',
69
+ body: { teamId },
70
+ timeout: API_TIMEOUT,
71
+ failOnStatusCode: false
72
+ })
73
+ }
74
+ })
75
+ }
76
+
77
+ /**
78
+ * Login as CRM Owner (CEO)
79
+ * Session is cached and reused across tests
80
+ */
81
+ export function loginAsCrmOwner() {
82
+ cy.session('crm-owner-session', () => {
83
+ apiLogin(CRM_USERS.OWNER).then((success) => {
84
+ if (success) {
85
+ cy.visit('/dashboard', { timeout: 60000 })
86
+ }
87
+ cy.url().should('include', '/dashboard')
88
+ setupTeamContext()
89
+ })
90
+ }, {
91
+ validate: () => {
92
+ cy.request({
93
+ url: '/api/auth/get-session',
94
+ timeout: API_TIMEOUT,
95
+ failOnStatusCode: false
96
+ }).its('status').should('eq', 200)
97
+ }
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Login as CRM Admin (Sales Manager)
103
+ * Session is cached and reused across tests
104
+ */
105
+ export function loginAsCrmAdmin() {
106
+ cy.session('crm-admin-session', () => {
107
+ apiLogin(CRM_USERS.ADMIN).then((success) => {
108
+ if (success) {
109
+ cy.visit('/dashboard', { timeout: 60000 })
110
+ }
111
+ cy.url().should('include', '/dashboard')
112
+ setupTeamContext()
113
+ })
114
+ }, {
115
+ validate: () => {
116
+ cy.request({
117
+ url: '/api/auth/get-session',
118
+ timeout: API_TIMEOUT,
119
+ failOnStatusCode: false
120
+ }).its('status').should('eq', 200)
121
+ }
122
+ })
123
+ }
124
+
125
+ /**
126
+ * Login as CRM Member (Sales Rep)
127
+ * Session is cached and reused across tests
128
+ */
129
+ export function loginAsCrmMember() {
130
+ cy.session('crm-member-session', () => {
131
+ apiLogin(CRM_USERS.MEMBER).then((success) => {
132
+ if (success) {
133
+ cy.visit('/dashboard', { timeout: 60000 })
134
+ }
135
+ cy.url().should('include', '/dashboard')
136
+ setupTeamContext('member')
137
+ })
138
+ }, {
139
+ validate: () => {
140
+ cy.request({
141
+ url: '/api/auth/get-session',
142
+ timeout: API_TIMEOUT,
143
+ failOnStatusCode: false
144
+ }).its('status').should('eq', 200)
145
+ }
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Login as CRM Laura (Marketing)
151
+ * Session is cached and reused across tests
152
+ */
153
+ export function loginAsCrmLaura() {
154
+ cy.session('crm-laura-session', () => {
155
+ apiLogin(CRM_USERS.LAURA).then((success) => {
156
+ if (success) {
157
+ cy.visit('/dashboard', { timeout: 60000 })
158
+ }
159
+ cy.url().should('include', '/dashboard')
160
+ setupTeamContext('member')
161
+ })
162
+ }, {
163
+ validate: () => {
164
+ cy.request({
165
+ url: '/api/auth/get-session',
166
+ timeout: API_TIMEOUT,
167
+ failOnStatusCode: false
168
+ }).its('status').should('eq', 200)
169
+ }
170
+ })
171
+ }
172
+
173
+ // Aliases for convenience
174
+ export const loginAsOwner = loginAsCrmOwner
175
+ export const loginAsAdmin = loginAsCrmAdmin
176
+ export const loginAsMember = loginAsCrmMember
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Cypress E2E Support File for Theme
3
+ *
4
+ * This file is used when running tests in npm mode (outside monorepo).
5
+ * In monorepo mode, the core support file is used instead via cypress.config.ts.
6
+ *
7
+ * For documentation video commands, install cypress-slow-down:
8
+ * pnpm add -D cypress-slow-down
9
+ * Then uncomment the doc-commands import below.
10
+ */
11
+
12
+ import '@testing-library/cypress/add-commands'
13
+
14
+ // Import @cypress/grep for test filtering by tags
15
+ // v5.x uses named export { register }
16
+ import { register as registerCypressGrep } from '@cypress/grep'
17
+ registerCypressGrep()
18
+
19
+ // Doc commands are optional (require cypress-slow-down)
20
+ // Uncomment if you have cypress-slow-down installed:
21
+ // import './doc-commands'
22
+
23
+ // Global error handling
24
+ Cypress.on('uncaught:exception', (err) => {
25
+ // Ignore React hydration errors
26
+ if (err.message.includes('Hydration')) {
27
+ return false
28
+ }
29
+ // Ignore ResizeObserver errors
30
+ if (err.message.includes('ResizeObserver')) {
31
+ return false
32
+ }
33
+ return true
34
+ })
35
+
36
+ // Global before hook
37
+ beforeEach(() => {
38
+ cy.clearCookies()
39
+ cy.clearLocalStorage()
40
+ })
41
+
42
+ // Type declarations for @cypress/grep and custom commands
43
+ declare global {
44
+ namespace Cypress {
45
+ interface Chainable {
46
+ /**
47
+ * Custom command to make API requests with better error handling
48
+ */
49
+ apiRequest(options: Partial<Cypress.RequestOptions>): Chainable<Cypress.Response<any>>
50
+
51
+ /**
52
+ * Login command for authenticated tests
53
+ */
54
+ login(email: string, password: string): Chainable<void>
55
+ }
56
+ interface SuiteConfigOverrides {
57
+ tags?: string | string[]
58
+ }
59
+ interface TestConfigOverrides {
60
+ tags?: string | string[]
61
+ }
62
+ }
63
+ }
64
+
65
+ // Custom API request command
66
+ Cypress.Commands.add('apiRequest', (options) => {
67
+ const defaultOptions = {
68
+ failOnStatusCode: false,
69
+ timeout: 15000,
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...options.headers
73
+ }
74
+ }
75
+ return cy.request({
76
+ ...defaultOptions,
77
+ ...options
78
+ })
79
+ })
80
+
81
+ // Login command for authenticated tests
82
+ Cypress.Commands.add('login', (email: string, password: string) => {
83
+ cy.session([email, password], () => {
84
+ cy.visit('/login')
85
+ cy.get('[data-testid="email-input"], input[name="email"]').type(email)
86
+ cy.get('[data-testid="password-input"], input[name="password"]').type(password)
87
+ cy.get('[data-testid="submit-button"], button[type="submit"]').click()
88
+ cy.url().should('include', '/dashboard')
89
+ })
90
+ })
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Cypress Configuration for crm Theme
3
+ *
4
+ * This config works in both monorepo and npm mode.
5
+ * Run with: NEXT_PUBLIC_ACTIVE_THEME=default pnpm cy:open
6
+ */
7
+
8
+ import { defineConfig } from 'cypress'
9
+ import { fileURLToPath } from 'url'
10
+ import path from 'path'
11
+ import fs from 'fs'
12
+
13
+ // ESM-compatible __dirname
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = path.dirname(__filename)
16
+
17
+ // Paths relative to this config file
18
+ const themeRoot = path.resolve(__dirname, '..')
19
+ const projectRoot = path.resolve(__dirname, '../../../..')
20
+ const narrationsOutputDir = path.join(__dirname, 'cypress/videos/narrations')
21
+
22
+ // Load environment variables
23
+ import dotenv from 'dotenv'
24
+ dotenv.config({ path: path.join(projectRoot, '.env') })
25
+
26
+ // Server port (from .env or default 3000)
27
+ const port = process.env.PORT || 3000
28
+
29
+ export default defineConfig({
30
+ e2e: {
31
+ // Base URL for the application
32
+ baseUrl: `http://localhost:${port}`,
33
+
34
+ // Spec patterns: theme tests only
35
+ specPattern: [
36
+ path.join(__dirname, 'cypress/e2e/**/*.cy.{js,ts}'),
37
+ ],
38
+
39
+ // Support file (always theme-local)
40
+ supportFile: path.join(__dirname, 'cypress/support/e2e.ts'),
41
+
42
+ // Fixtures folder (theme-specific)
43
+ fixturesFolder: path.join(__dirname, 'cypress/fixtures'),
44
+
45
+ // Output folders (theme-specific)
46
+ downloadsFolder: path.join(__dirname, 'cypress/downloads'),
47
+ screenshotsFolder: path.join(__dirname, 'cypress/screenshots'),
48
+ videosFolder: path.join(__dirname, 'cypress/videos'),
49
+
50
+ // Viewport settings
51
+ viewportWidth: 1280,
52
+ viewportHeight: 720,
53
+
54
+ // Video and screenshot settings
55
+ video: true,
56
+ screenshotOnRunFailure: true,
57
+
58
+ // Timeouts
59
+ defaultCommandTimeout: 10000,
60
+ requestTimeout: 10000,
61
+ responseTimeout: 10000,
62
+ pageLoadTimeout: 30000,
63
+
64
+ // Browser settings
65
+ chromeWebSecurity: false,
66
+
67
+ // Test isolation
68
+ testIsolation: true,
69
+
70
+ // Retry settings
71
+ retries: {
72
+ runMode: 1,
73
+ openMode: 0,
74
+ },
75
+
76
+ // Environment variables
77
+ env: {
78
+ // Theme info
79
+ ACTIVE_THEME: 'crm',
80
+ THEME_PATH: themeRoot,
81
+
82
+ // Test user credentials
83
+ TEST_USER_EMAIL: 'user@example.com',
84
+ TEST_USER_PASSWORD: 'Testing1234',
85
+
86
+ // Feature flags
87
+ ENABLE_ALLURE: true,
88
+
89
+ // Allure reporting
90
+ allureResultsPath: path.join(__dirname, 'cypress/allure-results'),
91
+
92
+ // API settings
93
+ API_URL: `http://localhost:${port}/api`,
94
+ API_BASE_URL: `http://localhost:${port}`,
95
+
96
+ // @cypress/grep - filter specs by tags
97
+ grepFilterSpecs: true,
98
+ grepOmitFiltered: true,
99
+ },
100
+
101
+ async setupNodeEvents(on, config) {
102
+ // Allure plugin setup (allure-cypress)
103
+ const { allureCypress } = await import('allure-cypress/reporter')
104
+ allureCypress(on, config, {
105
+ resultsDir: path.join(__dirname, 'cypress/allure-results'),
106
+ })
107
+
108
+ // @cypress/grep plugin for test filtering by tags
109
+ // v5.x uses named export { plugin } from '@cypress/grep/plugin'
110
+ const { plugin: grepPlugin } = await import('@cypress/grep/plugin')
111
+ grepPlugin(config)
112
+
113
+ // Documentation video tasks
114
+ on('task', {
115
+ /**
116
+ * Save narrations to JSON file for post-processing
117
+ */
118
+ saveNarrations({ specName, narrations }: { specName: string; narrations: unknown[] }) {
119
+ // Ensure output directory exists
120
+ if (!fs.existsSync(narrationsOutputDir)) {
121
+ fs.mkdirSync(narrationsOutputDir, { recursive: true })
122
+ }
123
+
124
+ const filename = `${specName}-narrations.json`
125
+ const filepath = path.join(narrationsOutputDir, filename)
126
+
127
+ fs.writeFileSync(filepath, JSON.stringify(narrations, null, 2))
128
+ console.log(`📝 Narrations saved to: ${filepath}`)
129
+
130
+ return null
131
+ },
132
+
133
+ /**
134
+ * Add narration entry (called per narration)
135
+ */
136
+ addNarration(narration: unknown) {
137
+ // This could be used for real-time streaming to a narration service
138
+ console.log('🎙️ Narration:', narration)
139
+ return null
140
+ },
141
+ })
142
+
143
+ return config
144
+ },
145
+ },
146
+
147
+ // Component testing (future use)
148
+ component: {
149
+ devServer: {
150
+ framework: 'next',
151
+ bundler: 'webpack',
152
+ },
153
+ },
154
+ })
@@ -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,131 @@
1
+ /**
2
+ * Jest Configuration for CRM Theme
3
+ *
4
+ * This config works in both monorepo and npm mode.
5
+ * Run with: pnpm test:theme
6
+ */
7
+
8
+ const path = require('path')
9
+ const fs = require('fs')
10
+
11
+ // Paths relative to this config file
12
+ const themeTestsRoot = __dirname
13
+ const themeRoot = path.resolve(__dirname, '../..')
14
+
15
+ // In monorepo: themes/crm/tests/jest -> apps/dev (via symlink in contents/)
16
+ // In npm mode: contents/themes/crm/tests/jest -> project root (5 levels up)
17
+ const monorepoRepoRoot = path.resolve(__dirname, '../../../..')
18
+ const npmModeRoot = path.resolve(__dirname, '../../../../..')
19
+
20
+ // Detect if running in npm mode (no packages/core folder) vs monorepo
21
+ const isNpmMode = !fs.existsSync(path.join(monorepoRepoRoot, 'packages/core'))
22
+
23
+ // In monorepo, use apps/dev as rootDir since contents/ symlinks to themes/
24
+ const monorepoAppRoot = path.join(monorepoRepoRoot, 'apps/dev')
25
+ const projectRoot = isNpmMode ? npmModeRoot : monorepoAppRoot
26
+
27
+ // Module name mapper based on mode
28
+ const moduleNameMapper = isNpmMode
29
+ ? {
30
+ // NPM mode: explicitly resolve @nextsparkjs/core subpaths to dist directory
31
+ // Jest doesn't respect package.json exports, so we map directly to dist files
32
+ '^@nextsparkjs/core/lib/(.*)$': '<rootDir>/node_modules/@nextsparkjs/core/dist/lib/$1',
33
+ '^@nextsparkjs/core/hooks/(.*)$': '<rootDir>/node_modules/@nextsparkjs/core/dist/hooks/$1',
34
+ '^@nextsparkjs/core/components/(.*)$': '<rootDir>/node_modules/@nextsparkjs/core/dist/components/$1',
35
+ '^@nextsparkjs/core/(.*)$': '<rootDir>/node_modules/@nextsparkjs/core/dist/$1',
36
+ '^@nextsparkjs/core$': '<rootDir>/node_modules/@nextsparkjs/core/dist/index.js',
37
+ '^@/contents/(.*)$': '<rootDir>/contents/$1',
38
+ '^@/entities/(.*)$': '<rootDir>/contents/entities/$1',
39
+ '^@/plugins/(.*)$': '<rootDir>/contents/plugins/$1',
40
+ '^@/themes/(.*)$': '<rootDir>/contents/themes/$1',
41
+ '^@/(.*)$': '<rootDir>/$1',
42
+ // Mocks from theme-local folder
43
+ 'next/server': path.join(themeTestsRoot, '__mocks__/next-server.js'),
44
+ '^jose$': path.join(themeTestsRoot, '__mocks__/jose.js'),
45
+ '^jose/(.*)$': path.join(themeTestsRoot, '__mocks__/jose.js'),
46
+ }
47
+ : {
48
+ // Monorepo mode: resolve from packages/core/src (rootDir is apps/dev)
49
+ '^@nextsparkjs/core/(.*)$': '<rootDir>/../../packages/core/src/$1',
50
+ '^@nextsparkjs/core$': '<rootDir>/../../packages/core/src',
51
+ '^@/contents/(.*)$': '<rootDir>/contents/$1',
52
+ '^@/entities/(.*)$': '<rootDir>/contents/entities/$1',
53
+ '^@/plugins/(.*)$': '<rootDir>/contents/plugins/$1',
54
+ '^@/themes/(.*)$': '<rootDir>/contents/themes/$1',
55
+ '^@/(.*)$': '<rootDir>/$1',
56
+ // Mocks from core
57
+ 'next/server': '<rootDir>/../../packages/core/tests/jest/__mocks__/next-server.js',
58
+ '^jose$': '<rootDir>/../../packages/core/tests/jest/__mocks__/jose.js',
59
+ '^jose/(.*)$': '<rootDir>/../../packages/core/tests/jest/__mocks__/jose.js',
60
+ }
61
+
62
+ // Setup files based on mode
63
+ const setupFilesAfterEnv = isNpmMode
64
+ ? [
65
+ // NPM mode: use theme's local setup only (it includes everything needed)
66
+ path.join(themeTestsRoot, 'setup.ts'),
67
+ ]
68
+ : [
69
+ // Monorepo mode: use local core setup (rootDir is apps/dev)
70
+ '<rootDir>/../../packages/core/tests/jest/setup.ts',
71
+ ]
72
+
73
+ /** @type {import('jest').Config} */
74
+ module.exports = {
75
+ displayName: 'theme-crm',
76
+ rootDir: projectRoot,
77
+
78
+ // Use roots to explicitly set test location (handles symlinks better)
79
+ roots: [themeTestsRoot],
80
+
81
+ // Test file patterns
82
+ testMatch: [
83
+ '**/*.{test,spec}.{js,ts,tsx}',
84
+ ],
85
+ testPathIgnorePatterns: [
86
+ '<rootDir>/node_modules/',
87
+ '<rootDir>/.next/',
88
+ ],
89
+
90
+ // Preset and environment
91
+ preset: 'ts-jest',
92
+ testEnvironment: 'jsdom',
93
+
94
+ // Module resolution
95
+ moduleNameMapper,
96
+
97
+ // Setup files
98
+ setupFilesAfterEnv,
99
+
100
+ // Transform configuration
101
+ transform: {
102
+ '^.+\\.(ts|tsx)$': ['ts-jest', {
103
+ tsconfig: path.join(projectRoot, 'tsconfig.json'),
104
+ }],
105
+ },
106
+
107
+ // Transform ignore patterns - allow TypeScript from core's jest-setup
108
+ transformIgnorePatterns: [
109
+ 'node_modules/(?!(uncrypto|better-auth|@noble|.*jose.*|remark.*|unified.*|@nextsparkjs/core/tests|.*\\.mjs$))',
110
+ 'node_modules/\\.pnpm/(?!(.*uncrypto.*|.*better-auth.*|.*@noble.*|.*jose.*|.*remark.*|.*unified.*|@nextsparkjs.*core.*tests|.*\\.mjs$))',
111
+ ],
112
+
113
+ // File extensions
114
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
115
+
116
+ // Test timeout
117
+ testTimeout: 10000,
118
+
119
+ // Verbose output
120
+ verbose: true,
121
+
122
+ // Force exit after tests complete
123
+ forceExit: true,
124
+
125
+ // Disable watchman for symlink support
126
+ watchman: false,
127
+
128
+ // Coverage output directory
129
+ coverageDirectory: path.join(themeTestsRoot, 'coverage'),
130
+ coverageReporters: ['text', 'lcov', 'html'],
131
+ }