@nextsparkjs/theme-crm 0.1.0-beta.20 → 0.1.0-beta.30
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/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/jest.config.cjs +131 -0
- package/tests/jest/setup.ts +170 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextsparkjs/theme-crm",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./config/theme.config.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"react": "^19.0.0",
|
|
14
14
|
"react-dom": "^19.0.0",
|
|
15
15
|
"zod": "^4.0.0",
|
|
16
|
-
"@nextsparkjs/core": "0.1.0-beta.
|
|
16
|
+
"@nextsparkjs/core": "0.1.0-beta.30"
|
|
17
17
|
},
|
|
18
18
|
"nextspark": {
|
|
19
19
|
"type": "theme",
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Setup for CRM Theme
|
|
3
|
+
*
|
|
4
|
+
* This file provides complete Jest setup for npm mode.
|
|
5
|
+
* It includes all necessary polyfills and mocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import '@testing-library/jest-dom'
|
|
9
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
10
|
+
|
|
11
|
+
// Polyfill TextEncoder/TextDecoder for jsdom
|
|
12
|
+
global.TextEncoder = TextEncoder
|
|
13
|
+
global.TextDecoder = TextDecoder as typeof global.TextDecoder
|
|
14
|
+
|
|
15
|
+
// Polyfill Web APIs for Next.js server components testing
|
|
16
|
+
class MockHeaders {
|
|
17
|
+
private _headers: Map<string, string> = new Map()
|
|
18
|
+
|
|
19
|
+
constructor(init?: HeadersInit) {
|
|
20
|
+
if (init) {
|
|
21
|
+
if (init instanceof MockHeaders) {
|
|
22
|
+
init._headers.forEach((value, key) => this._headers.set(key, value))
|
|
23
|
+
} else if (Array.isArray(init)) {
|
|
24
|
+
init.forEach(([key, value]) => this._headers.set(key.toLowerCase(), value))
|
|
25
|
+
} else {
|
|
26
|
+
Object.entries(init).forEach(([key, value]) => this._headers.set(key.toLowerCase(), value))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(name: string) { return this._headers.get(name.toLowerCase()) || null }
|
|
32
|
+
set(name: string, value: string) { this._headers.set(name.toLowerCase(), value) }
|
|
33
|
+
has(name: string) { return this._headers.has(name.toLowerCase()) }
|
|
34
|
+
delete(name: string) { this._headers.delete(name.toLowerCase()) }
|
|
35
|
+
forEach(cb: (value: string, key: string) => void) { this._headers.forEach(cb) }
|
|
36
|
+
entries() { return this._headers.entries() }
|
|
37
|
+
keys() { return this._headers.keys() }
|
|
38
|
+
values() { return this._headers.values() }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class MockRequest {
|
|
42
|
+
url: string
|
|
43
|
+
method: string
|
|
44
|
+
headers: MockHeaders
|
|
45
|
+
private _body: any
|
|
46
|
+
|
|
47
|
+
constructor(input: string | URL, init?: RequestInit) {
|
|
48
|
+
this.url = typeof input === 'string' ? input : input.toString()
|
|
49
|
+
this.method = init?.method || 'GET'
|
|
50
|
+
this.headers = new MockHeaders(init?.headers as HeadersInit)
|
|
51
|
+
this._body = init?.body
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async json() {
|
|
55
|
+
if (!this._body) return {}
|
|
56
|
+
return typeof this._body === 'string' ? JSON.parse(this._body) : this._body
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async text() {
|
|
60
|
+
return this._body?.toString() || ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clone() {
|
|
64
|
+
return new MockRequest(this.url, {
|
|
65
|
+
method: this.method,
|
|
66
|
+
headers: Object.fromEntries(this.headers.entries()),
|
|
67
|
+
body: this._body,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class MockResponse {
|
|
73
|
+
body: any
|
|
74
|
+
status: number
|
|
75
|
+
statusText: string
|
|
76
|
+
headers: MockHeaders
|
|
77
|
+
ok: boolean
|
|
78
|
+
|
|
79
|
+
constructor(body?: any, init?: ResponseInit) {
|
|
80
|
+
this.body = body
|
|
81
|
+
this.status = init?.status || 200
|
|
82
|
+
this.statusText = init?.statusText || 'OK'
|
|
83
|
+
this.headers = new MockHeaders(init?.headers as HeadersInit)
|
|
84
|
+
this.ok = this.status >= 200 && this.status < 300
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async json() {
|
|
88
|
+
if (!this.body) return {}
|
|
89
|
+
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async text() {
|
|
93
|
+
return this.body?.toString() || ''
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clone() {
|
|
97
|
+
return new MockResponse(this.body, {
|
|
98
|
+
status: this.status,
|
|
99
|
+
statusText: this.statusText,
|
|
100
|
+
headers: Object.fromEntries(this.headers.entries()),
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static json(data: any, init?: ResponseInit) {
|
|
105
|
+
return new MockResponse(JSON.stringify(data), {
|
|
106
|
+
...init,
|
|
107
|
+
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) as object },
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Assign to global scope
|
|
113
|
+
global.Headers = MockHeaders as any
|
|
114
|
+
global.Request = MockRequest as any
|
|
115
|
+
global.Response = MockResponse as any
|
|
116
|
+
|
|
117
|
+
// Mock fetch
|
|
118
|
+
global.fetch = jest.fn().mockResolvedValue(new MockResponse('{}', { status: 200 }))
|
|
119
|
+
|
|
120
|
+
// Mock Web Crypto API
|
|
121
|
+
const crypto = require('crypto')
|
|
122
|
+
Object.defineProperty(global, 'crypto', {
|
|
123
|
+
value: {
|
|
124
|
+
getRandomValues: (arr: Uint8Array) => crypto.randomFillSync(arr),
|
|
125
|
+
randomUUID: () => crypto.randomUUID(),
|
|
126
|
+
subtle: {
|
|
127
|
+
digest: async (algorithm: string, data: BufferSource) => {
|
|
128
|
+
const hash = crypto.createHash(algorithm.toLowerCase().replace('-', ''))
|
|
129
|
+
hash.update(Buffer.from(data as ArrayBuffer))
|
|
130
|
+
return hash.digest().buffer
|
|
131
|
+
},
|
|
132
|
+
encrypt: jest.fn(),
|
|
133
|
+
decrypt: jest.fn(),
|
|
134
|
+
sign: jest.fn(),
|
|
135
|
+
verify: jest.fn(),
|
|
136
|
+
generateKey: jest.fn(),
|
|
137
|
+
importKey: jest.fn(),
|
|
138
|
+
exportKey: jest.fn(),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Mock matchMedia for component tests with media queries
|
|
144
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
145
|
+
writable: true,
|
|
146
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
147
|
+
matches: false,
|
|
148
|
+
media: query,
|
|
149
|
+
onchange: null,
|
|
150
|
+
addListener: jest.fn(),
|
|
151
|
+
removeListener: jest.fn(),
|
|
152
|
+
addEventListener: jest.fn(),
|
|
153
|
+
removeEventListener: jest.fn(),
|
|
154
|
+
dispatchEvent: jest.fn(),
|
|
155
|
+
})),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Mock ResizeObserver
|
|
159
|
+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
|
160
|
+
observe: jest.fn(),
|
|
161
|
+
unobserve: jest.fn(),
|
|
162
|
+
disconnect: jest.fn(),
|
|
163
|
+
}))
|
|
164
|
+
|
|
165
|
+
// Mock IntersectionObserver
|
|
166
|
+
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
|
167
|
+
observe: jest.fn(),
|
|
168
|
+
unobserve: jest.fn(),
|
|
169
|
+
disconnect: jest.fn(),
|
|
170
|
+
}))
|