@api-client/core 0.15.0 → 0.16.0

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 (126) hide show
  1. package/TESTING_READY.md +114 -0
  2. package/TESTING_SETUP.md +198 -0
  3. package/build/src/modeling/Semantics.d.ts +126 -2
  4. package/build/src/modeling/Semantics.d.ts.map +1 -1
  5. package/build/src/modeling/Semantics.js +281 -13
  6. package/build/src/modeling/Semantics.js.map +1 -1
  7. package/build/src/modeling/definitions/Calculated.d.ts +54 -0
  8. package/build/src/modeling/definitions/Calculated.d.ts.map +1 -0
  9. package/build/src/modeling/definitions/Calculated.js +31 -0
  10. package/build/src/modeling/definitions/Calculated.js.map +1 -0
  11. package/build/src/modeling/definitions/Categories.d.ts +60 -0
  12. package/build/src/modeling/definitions/Categories.d.ts.map +1 -0
  13. package/build/src/modeling/definitions/Categories.js +33 -0
  14. package/build/src/modeling/definitions/Categories.js.map +1 -0
  15. package/build/src/modeling/definitions/Derived.d.ts +54 -0
  16. package/build/src/modeling/definitions/Derived.d.ts.map +1 -0
  17. package/build/src/modeling/definitions/Derived.js +31 -0
  18. package/build/src/modeling/definitions/Derived.js.map +1 -0
  19. package/build/src/modeling/definitions/Description.d.ts +36 -0
  20. package/build/src/modeling/definitions/Description.d.ts.map +1 -0
  21. package/build/src/modeling/definitions/Description.js +28 -0
  22. package/build/src/modeling/definitions/Description.js.map +1 -0
  23. package/build/src/modeling/definitions/Email.d.ts +66 -0
  24. package/build/src/modeling/definitions/Email.d.ts.map +1 -0
  25. package/build/src/modeling/definitions/Email.js +33 -0
  26. package/build/src/modeling/definitions/Email.js.map +1 -0
  27. package/build/src/modeling/definitions/GeospatialCoordinates.d.ts +212 -0
  28. package/build/src/modeling/definitions/GeospatialCoordinates.d.ts.map +1 -0
  29. package/build/src/modeling/definitions/GeospatialCoordinates.js +129 -0
  30. package/build/src/modeling/definitions/GeospatialCoordinates.js.map +1 -0
  31. package/build/src/modeling/definitions/HTML.d.ts +88 -0
  32. package/build/src/modeling/definitions/HTML.d.ts.map +1 -0
  33. package/build/src/modeling/definitions/HTML.js +42 -0
  34. package/build/src/modeling/definitions/HTML.js.map +1 -0
  35. package/build/src/modeling/definitions/Markdown.d.ts +84 -0
  36. package/build/src/modeling/definitions/Markdown.d.ts.map +1 -0
  37. package/build/src/modeling/definitions/Markdown.js +41 -0
  38. package/build/src/modeling/definitions/Markdown.js.map +1 -0
  39. package/build/src/modeling/definitions/Password.d.ts +112 -0
  40. package/build/src/modeling/definitions/Password.d.ts.map +1 -0
  41. package/build/src/modeling/definitions/Password.js +57 -0
  42. package/build/src/modeling/definitions/Password.js.map +1 -0
  43. package/build/src/modeling/definitions/Phone.d.ts +83 -0
  44. package/build/src/modeling/definitions/Phone.d.ts.map +1 -0
  45. package/build/src/modeling/definitions/Phone.js +39 -0
  46. package/build/src/modeling/definitions/Phone.js.map +1 -0
  47. package/build/src/modeling/definitions/Price.d.ts +102 -0
  48. package/build/src/modeling/definitions/Price.d.ts.map +1 -0
  49. package/build/src/modeling/definitions/Price.js +99 -0
  50. package/build/src/modeling/definitions/Price.js.map +1 -0
  51. package/build/src/modeling/definitions/PublicUniqueName.d.ts +69 -0
  52. package/build/src/modeling/definitions/PublicUniqueName.d.ts.map +1 -0
  53. package/build/src/modeling/definitions/PublicUniqueName.js +34 -0
  54. package/build/src/modeling/definitions/PublicUniqueName.js.map +1 -0
  55. package/build/src/modeling/definitions/SKU.d.ts +127 -0
  56. package/build/src/modeling/definitions/SKU.d.ts.map +1 -0
  57. package/build/src/modeling/definitions/SKU.js +142 -0
  58. package/build/src/modeling/definitions/SKU.js.map +1 -0
  59. package/build/src/modeling/definitions/Status.d.ts +150 -0
  60. package/build/src/modeling/definitions/Status.d.ts.map +1 -0
  61. package/build/src/modeling/definitions/Status.js +60 -0
  62. package/build/src/modeling/definitions/Status.js.map +1 -0
  63. package/build/src/modeling/definitions/Summary.d.ts +53 -0
  64. package/build/src/modeling/definitions/Summary.d.ts.map +1 -0
  65. package/build/src/modeling/definitions/Summary.js +50 -0
  66. package/build/src/modeling/definitions/Summary.js.map +1 -0
  67. package/build/src/modeling/definitions/Tags.d.ts +52 -0
  68. package/build/src/modeling/definitions/Tags.d.ts.map +1 -0
  69. package/build/src/modeling/definitions/Tags.js +32 -0
  70. package/build/src/modeling/definitions/Tags.js.map +1 -0
  71. package/build/src/modeling/definitions/URL.d.ts +68 -0
  72. package/build/src/modeling/definitions/URL.d.ts.map +1 -0
  73. package/build/src/modeling/definitions/URL.js +37 -0
  74. package/build/src/modeling/definitions/URL.js.map +1 -0
  75. package/build/src/modeling/validation/semantic_validation.d.ts +4 -0
  76. package/build/src/modeling/validation/semantic_validation.d.ts.map +1 -1
  77. package/build/src/modeling/validation/semantic_validation.js +32 -1
  78. package/build/src/modeling/validation/semantic_validation.js.map +1 -1
  79. package/build/tsconfig.tsbuildinfo +1 -1
  80. package/data/models/example-generator-api.json +9 -9
  81. package/package.json +1 -1
  82. package/src/modeling/Semantics.ts +297 -14
  83. package/src/modeling/definitions/Calculated.ts +76 -0
  84. package/src/modeling/definitions/Categories.ts +84 -0
  85. package/src/modeling/definitions/Derived.ts +76 -0
  86. package/src/modeling/definitions/Description.ts +55 -0
  87. package/src/modeling/definitions/Email.ts +90 -0
  88. package/src/modeling/definitions/GeospatialCoordinates.ts +274 -0
  89. package/src/modeling/definitions/HTML.ts +121 -0
  90. package/src/modeling/definitions/Markdown.ts +116 -0
  91. package/src/modeling/definitions/Password.ts +156 -0
  92. package/src/modeling/definitions/Phone.ts +116 -0
  93. package/src/modeling/definitions/Price.examples.md +158 -0
  94. package/src/modeling/definitions/Price.ts +180 -0
  95. package/src/modeling/definitions/PublicUniqueName.ts +98 -0
  96. package/src/modeling/definitions/SKU.examples.md +230 -0
  97. package/src/modeling/definitions/SKU.ts +254 -0
  98. package/src/modeling/definitions/Status.ts +227 -0
  99. package/src/modeling/definitions/Summary.ts +73 -0
  100. package/src/modeling/definitions/Tags.ts +75 -0
  101. package/src/modeling/definitions/URL.ts +96 -0
  102. package/src/modeling/validation/semantic_validation.ts +35 -1
  103. package/tests/example-test-setup.ts +133 -0
  104. package/tests/template-node.spec.ts +75 -0
  105. package/tests/test-utils.ts +293 -0
  106. package/tests/unit/modeling/definitions/calculated.spec.ts +33 -0
  107. package/tests/unit/modeling/definitions/categories.spec.ts +38 -0
  108. package/tests/unit/modeling/definitions/derived.spec.ts +34 -0
  109. package/tests/unit/modeling/definitions/description.spec.ts +38 -0
  110. package/tests/unit/modeling/definitions/email.spec.ts +38 -0
  111. package/tests/unit/modeling/definitions/geospatial-coordinates.spec.ts +41 -0
  112. package/tests/unit/modeling/definitions/html.spec.ts +38 -0
  113. package/tests/unit/modeling/definitions/markdown.spec.ts +38 -0
  114. package/tests/unit/modeling/definitions/password.spec.ts +347 -0
  115. package/tests/unit/modeling/definitions/phone.spec.ts +38 -0
  116. package/tests/unit/modeling/definitions/price.spec.ts +465 -0
  117. package/tests/unit/modeling/definitions/public-unique-name.spec.ts +38 -0
  118. package/tests/unit/modeling/definitions/sku.spec.ts +240 -0
  119. package/tests/unit/modeling/definitions/status.spec.ts +37 -0
  120. package/tests/unit/modeling/definitions/summary.spec.ts +36 -0
  121. package/tests/unit/modeling/definitions/tags.spec.ts +38 -0
  122. package/tests/unit/modeling/definitions/url.spec.ts +38 -0
  123. package/tests/unit/modeling/domain_property.spec.ts +106 -0
  124. package/tests/unit/modeling/domain_validation.spec.ts +5 -5
  125. package/tests/unit/modeling/semantic-configs.spec.ts +569 -0
  126. package/tests/unit/modeling/semantics.spec.ts +52 -0
@@ -0,0 +1,96 @@
1
+ import type { AppliedDataSemantic } from '../Semantics.js'
2
+ import { SemanticType } from '../Semantics.js'
3
+
4
+ /**
5
+ * Configuration options for the URL semantic.
6
+ * Controls validation, allowed protocols, and formatting.
7
+ */
8
+ export interface URLConfig {
9
+ /**
10
+ * List of allowed protocols (e.g., ['http', 'https']).
11
+ */
12
+ allowedProtocols?: string[]
13
+ /**
14
+ * Whether to require HTTPS protocol (default: false).
15
+ */
16
+ requireHttps?: boolean
17
+ /**
18
+ * Whether to allow query parameters in the URL.
19
+ */
20
+ allowQueryParams?: boolean
21
+ /**
22
+ * Whether to allow URL fragments (e.g., #section).
23
+ */
24
+ allowFragments?: boolean
25
+ /**
26
+ * Whether to allow internationalized domain names.
27
+ */
28
+ allowInternational?: boolean
29
+ /**
30
+ * Whether to allow IP addresses as hostnames.
31
+ */
32
+ allowIP?: boolean
33
+ /**
34
+ * Whether to allow port numbers in the URL.
35
+ */
36
+ allowPort?: boolean
37
+ /**
38
+ * Whether to allow authentication info (user:pass@host).
39
+ */
40
+ allowAuthentication?: boolean
41
+ /**
42
+ * Custom metadata for the URL field.
43
+ */
44
+ metadata?: Record<string, unknown>
45
+ /**
46
+ * Index signature to allow additional properties.
47
+ */
48
+ [key: string]: unknown
49
+ }
50
+
51
+ /**
52
+ * Type-safe configuration for URL semantic.
53
+ */
54
+ export interface AppliedURLSemantic extends AppliedDataSemantic {
55
+ id: SemanticType.URL
56
+ config?: URLConfig
57
+ }
58
+
59
+ /**
60
+ * Type guard to check if a semantic is a URL semantic.
61
+ */
62
+ export const isURLSemantic = (semantic: AppliedDataSemantic): semantic is AppliedURLSemantic => {
63
+ return semantic.id === SemanticType.URL
64
+ }
65
+
66
+ /**
67
+ * Helper function to create a URL semantic with configuration.
68
+ */
69
+ export const createURLSemantic = (config: URLConfig = {}): AppliedURLSemantic => {
70
+ const mergedConfig = {
71
+ ...DEFAULT_URL_CONFIG,
72
+ ...config,
73
+ }
74
+ if (config.metadata) {
75
+ mergedConfig.metadata = { ...config.metadata }
76
+ }
77
+
78
+ return {
79
+ id: SemanticType.URL,
80
+ config: mergedConfig,
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Default configuration for URL semantic.
86
+ */
87
+ export const DEFAULT_URL_CONFIG: URLConfig = {
88
+ allowedProtocols: ['http', 'https'],
89
+ requireHttps: false,
90
+ allowQueryParams: true,
91
+ allowFragments: true,
92
+ allowInternational: true,
93
+ allowIP: true,
94
+ allowPort: true,
95
+ allowAuthentication: false,
96
+ }
@@ -1,6 +1,6 @@
1
1
  import { DomainEntityKind } from '../../models/kinds.js'
2
2
  import type { DataDomain } from '../DataDomain.js'
3
- import { SemanticType } from '../Semantics.js'
3
+ import { DataSemantics, isPropertySemantic, SemanticType } from '../Semantics.js'
4
4
  import type { DomainValidation } from './rules.js'
5
5
 
6
6
  /**
@@ -29,6 +29,10 @@ export class SemanticValidation {
29
29
  const softDeleteSemantics = this.validateSoftDeleteSemantics()
30
30
  results.push(...softDeleteSemantics)
31
31
 
32
+ // Validate property semantics data types
33
+ const propertySemanticsDataTypes = this.validatePropertySemanticsDataTypes()
34
+ results.push(...propertySemanticsDataTypes)
35
+
32
36
  return results
33
37
  }
34
38
 
@@ -142,4 +146,34 @@ export class SemanticValidation {
142
146
 
143
147
  return results
144
148
  }
149
+
150
+ /**
151
+ * Validates if property semantics are applied to properties with compatible data types.
152
+ */
153
+ private validatePropertySemanticsDataTypes(): DomainValidation[] {
154
+ const results: DomainValidation[] = []
155
+
156
+ for (const entity of this.domain.listEntities()) {
157
+ for (const property of entity.listProperties()) {
158
+ for (const appliedSemantic of property.semantics) {
159
+ const semanticDefinition = DataSemantics[appliedSemantic.id]
160
+ if (isPropertySemantic(semanticDefinition) && semanticDefinition.applicableDataTypes) {
161
+ if (!semanticDefinition.applicableDataTypes.includes(property.type)) {
162
+ results.push({
163
+ field: 'semantics',
164
+ rule: 'type_mismatch',
165
+ message: `The "${property.info.getLabel()}" property has the "${semanticDefinition.displayName}" semantic applied, but its type "${property.type}" is not compatible.`,
166
+ help: `The "${semanticDefinition.displayName}" semantic can only be applied to properties of type(s): ${semanticDefinition.applicableDataTypes.join(', ')}.`,
167
+ severity: 'error',
168
+ key: property.key,
169
+ kind: property.kind,
170
+ parent: entity.key,
171
+ })
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return results
178
+ }
145
179
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Example test setup and usage guide for API Client Core
3
+ *
4
+ * This file demonstrates how to use the testing infrastructure
5
+ * and provides working examples for both Node.js and browser tests.
6
+ */
7
+
8
+ // Node.js test example using Japa
9
+ import { test } from '@japa/runner'
10
+ import { TestUtils } from './test-utils.js'
11
+
12
+ // Example: Testing a utility function
13
+ test.group('Example Node.js Tests', (group) => {
14
+ group.each.setup(() => {
15
+ // Setup before each test
16
+ // eslint-disable-next-line no-console
17
+ console.log('Setting up test...')
18
+ })
19
+
20
+ test('should demonstrate basic testing', ({ assert }) => {
21
+ const result = 'hello world'
22
+ assert.equal(result, 'hello world')
23
+ assert.typeOf(result, 'string')
24
+ })
25
+
26
+ test('should test async operations', async ({ assert }) => {
27
+ const data = TestUtils.generateTestData()
28
+ assert.typeOf(data.id, 'string')
29
+ assert.typeOf(data.timestamp, 'number')
30
+ assert.typeOf(data.randomBoolean, 'boolean')
31
+ })
32
+
33
+ test('should test HTTP mocking', async ({ assert }) => {
34
+ const mockResponse = TestUtils.createMockResponse(200, { success: true })
35
+ const cleanup = TestUtils.mockFetch(mockResponse)
36
+
37
+ try {
38
+ const response = await fetch('https://api.example.com/test')
39
+ const data = await response.json()
40
+ assert.isTrue(data.success)
41
+ assert.equal(response.status, 200)
42
+ } finally {
43
+ cleanup()
44
+ }
45
+ })
46
+
47
+ test('should test error handling', async ({ assert }) => {
48
+ const error = await TestUtils.assertThrows(() => {
49
+ throw new Error('Test error')
50
+ }, 'Test error')
51
+ assert.equal(error.message, 'Test error')
52
+ })
53
+
54
+ test('should validate object properties', ({ assert }) => {
55
+ const testObj = { name: 'test', value: 42 }
56
+
57
+ // This should not throw
58
+ TestUtils.validateRequiredProperties(testObj, ['name', 'value'])
59
+
60
+ // This should throw
61
+ assert.throws(() => {
62
+ // @ts-expect-error: Intentionally missing property
63
+ TestUtils.validateRequiredProperties(testObj, ['name', 'missing'])
64
+ })
65
+ })
66
+
67
+ test('should create test URLs with parameters', ({ assert }) => {
68
+ const url = TestUtils.createTestUrl('/api/users', {
69
+ page: '1',
70
+ limit: '10',
71
+ })
72
+
73
+ assert.equal(url.pathname, '/api/users')
74
+ assert.equal(url.searchParams.get('page'), '1')
75
+ assert.equal(url.searchParams.get('limit'), '10')
76
+ })
77
+
78
+ test('should create test requests', ({ assert }) => {
79
+ const request = TestUtils.createTestRequest(
80
+ 'POST',
81
+ 'https://api.example.com/users',
82
+ JSON.stringify({ name: 'John' }),
83
+ { Authorization: 'Bearer token123' }
84
+ )
85
+
86
+ assert.equal(request.method, 'POST')
87
+ assert.equal(request.url, 'https://api.example.com/users')
88
+ })
89
+
90
+ test('should create mock auth config', ({ assert }) => {
91
+ const config = TestUtils.createMockAuthConfig()
92
+ assert.typeOf(config.clientId, 'string')
93
+ assert.typeOf(config.clientSecret, 'string')
94
+ assert.isArray(config.scopes)
95
+ assert.include(config.scopes, 'read')
96
+ assert.include(config.scopes, 'write')
97
+ })
98
+
99
+ test('should test with sinon mocks', ({ sinon, assert }) => {
100
+ const mockObj = {
101
+ getValue: () => 'original',
102
+ }
103
+
104
+ const stub = sinon.stub(mockObj, 'getValue')
105
+ stub.returns('mocked')
106
+
107
+ assert.equal(mockObj.getValue(), 'mocked')
108
+
109
+ stub.restore()
110
+ assert.equal(mockObj.getValue(), 'original')
111
+ })
112
+
113
+ test('should test timeout behavior', async ({ assert }) => {
114
+ const start = Date.now()
115
+ await TestUtils.sleep(100)
116
+ const elapsed = Date.now() - start
117
+
118
+ assert.isAtLeast(elapsed, 90) // Allow some variance
119
+ assert.isAtMost(elapsed, 150)
120
+ })
121
+ })
122
+
123
+ export default {
124
+ setupNodeTests: () => {
125
+ // eslint-disable-next-line no-console
126
+ console.log('Node.js test environment initialized')
127
+ },
128
+
129
+ setupBrowserTests: () => {
130
+ // eslint-disable-next-line no-console
131
+ console.log('Browser test environment initialized')
132
+ },
133
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Template for Node.js tests using Japa
3
+ * Copy this file and rename it to create new test suites.
4
+ */
5
+ import { test } from '@japa/runner'
6
+
7
+ test.group('YourModuleName', (group) => {
8
+ // Setup runs before each test in the group
9
+ group.each.setup(() => {
10
+ // Initialize test data, mocks, etc.
11
+ })
12
+
13
+ // Teardown runs after each test in the group
14
+ group.each.teardown(() => {
15
+ // Clean up resources, restore mocks, etc.
16
+ })
17
+
18
+ test('should describe what this test does', ({ assert }) => {
19
+ // Arrange
20
+ const input = 'test-input'
21
+
22
+ // Act
23
+ const result = input.toUpperCase()
24
+
25
+ // Assert
26
+ assert.equal(result, 'TEST-INPUT')
27
+ })
28
+
29
+ test('should test async operations', async ({ assert }) => {
30
+ // Arrange
31
+ const promise = Promise.resolve('async-result')
32
+
33
+ // Act
34
+ const result = await promise
35
+
36
+ // Assert
37
+ assert.equal(result, 'async-result')
38
+ })
39
+
40
+ test('should test error cases', async ({ assert }) => {
41
+ // Assert that function throws
42
+ await assert.rejects(() => Promise.reject(new Error('Expected error')), 'Expected error')
43
+ })
44
+
45
+ test('should use sinon for mocking', ({ sinon, assert }) => {
46
+ // Arrange
47
+ const mockObject = {
48
+ method: () => 'original',
49
+ }
50
+ const stub = sinon.stub(mockObject, 'method')
51
+ stub.returns('mocked')
52
+
53
+ // Act
54
+ const result = mockObject.method()
55
+
56
+ // Assert
57
+ assert.equal(result, 'mocked')
58
+
59
+ // Cleanup (optional - sinon auto-restores)
60
+ stub.restore()
61
+ })
62
+
63
+ test('should test with custom timeout', async ({ assert }) => {
64
+ // This test has a longer timeout
65
+ const start = Date.now()
66
+ await new Promise((resolve) => setTimeout(resolve, 100))
67
+ const elapsed = Date.now() - start
68
+ assert.isAtLeast(elapsed, 90)
69
+ }).timeout(5000) // 5 second timeout
70
+
71
+ test('should be skipped', ({ assert }) => {
72
+ // This test is skipped
73
+ assert.fail('This should not run')
74
+ }).skip(true)
75
+ })
@@ -0,0 +1,293 @@
1
+ import { TestContext } from '@japa/runner/core'
2
+
3
+ /**
4
+ * Common test utilities for the API Client Core
5
+ */
6
+ export const TestUtils = {
7
+ /**
8
+ * Creates a mock HTTP response
9
+ */
10
+ createMockResponse(status = 200, body: unknown = {}, headers: Record<string, string> = {}): Response {
11
+ return new Response(JSON.stringify(body), {
12
+ status,
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ ...headers,
16
+ },
17
+ })
18
+ },
19
+
20
+ /**
21
+ * Creates a test URL with optional query parameters
22
+ */
23
+ createTestUrl(path = '/', params: Record<string, string> = {}): URL {
24
+ const url = new URL(path, 'https://api.example.com')
25
+ Object.entries(params).forEach(([key, value]) => {
26
+ url.searchParams.set(key, value)
27
+ })
28
+ return url
29
+ },
30
+
31
+ /**
32
+ * Waits for a specified amount of time
33
+ */
34
+ async sleep(ms: number): Promise<void> {
35
+ return new Promise((resolve) => setTimeout(resolve, ms))
36
+ },
37
+
38
+ /**
39
+ * Creates a test request object
40
+ */
41
+ createTestRequest(
42
+ method = 'GET',
43
+ url: string | URL = 'https://api.example.com',
44
+ body?: BodyInit,
45
+ headers: Record<string, string> = {}
46
+ ): Request {
47
+ return new Request(url, {
48
+ method,
49
+ body,
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ ...headers,
53
+ },
54
+ })
55
+ },
56
+
57
+ /**
58
+ * Generates random test data
59
+ */
60
+ generateTestData() {
61
+ return {
62
+ id: Math.random().toString(36).substr(2, 9),
63
+ timestamp: Date.now(),
64
+ randomString: Math.random().toString(36).substr(2, 10),
65
+ randomNumber: Math.floor(Math.random() * 1000),
66
+ randomBoolean: Math.random() > 0.5,
67
+ }
68
+ },
69
+
70
+ /**
71
+ * Creates a mock authorization configuration
72
+ */
73
+ createMockAuthConfig() {
74
+ return {
75
+ clientId: 'test-client-id',
76
+ clientSecret: 'test-client-secret',
77
+ authorizationUri: 'https://auth.example.com/oauth/authorize',
78
+ accessTokenUri: 'https://auth.example.com/oauth/token',
79
+ scopes: ['read', 'write'],
80
+ }
81
+ },
82
+
83
+ /**
84
+ * Creates a mock API project structure
85
+ */
86
+ createMockProject() {
87
+ return {
88
+ kind: 'Project',
89
+ key: 'test-project',
90
+ info: {
91
+ name: 'Test Project',
92
+ version: '1.0.0',
93
+ description: 'A test API project',
94
+ },
95
+ definitions: {},
96
+ environments: [],
97
+ requests: [],
98
+ folders: [],
99
+ }
100
+ },
101
+
102
+ /**
103
+ * Asserts that an error is thrown with specific message
104
+ */
105
+ async assertThrows(fn: () => Promise<unknown> | unknown, expectedMessage?: string | RegExp): Promise<Error> {
106
+ try {
107
+ await fn()
108
+ throw new Error('Expected function to throw an error')
109
+ } catch (error) {
110
+ const typedError = error as Error
111
+ if (expectedMessage) {
112
+ if (typeof expectedMessage === 'string') {
113
+ if (!typedError.message.includes(expectedMessage)) {
114
+ throw new Error(`Expected error message to include "${expectedMessage}", got "${typedError.message}"`)
115
+ }
116
+ } else if (expectedMessage instanceof RegExp) {
117
+ if (!expectedMessage.test(typedError.message)) {
118
+ throw new Error(`Expected error message to match ${expectedMessage}, got "${typedError.message}"`)
119
+ }
120
+ }
121
+ }
122
+ return typedError
123
+ }
124
+ },
125
+
126
+ /**
127
+ * Validates that an object has required properties
128
+ */
129
+ validateRequiredProperties<T extends Record<string, unknown>>(obj: T, requiredProps: (keyof T)[]): void {
130
+ for (const prop of requiredProps) {
131
+ if (!(prop in obj)) {
132
+ throw new Error(`Missing required property: ${String(prop)}`)
133
+ }
134
+ if (obj[prop] === undefined || obj[prop] === null) {
135
+ throw new Error(`Property ${String(prop)} cannot be null or undefined`)
136
+ }
137
+ }
138
+ },
139
+
140
+ /**
141
+ * Creates a test environment configuration
142
+ */
143
+ createTestEnvironment() {
144
+ return {
145
+ variables: [
146
+ { name: 'baseUrl', value: 'https://api.example.com' },
147
+ { name: 'apiKey', value: 'test-api-key' },
148
+ { name: 'version', value: 'v1' },
149
+ ],
150
+ oauth2: {
151
+ port: 8080,
152
+ issuer: 'https://oauth.example.com',
153
+ },
154
+ }
155
+ },
156
+
157
+ /**
158
+ * Mocks the fetch function with a custom response
159
+ */
160
+ mockFetch(response: Response | ((input: RequestInfo | URL, init?: RequestInit) => Response)): () => void {
161
+ const originalFetch = globalThis.fetch
162
+
163
+ globalThis.fetch =
164
+ typeof response === 'function'
165
+ ? (input: RequestInfo | URL, init?: RequestInit) =>
166
+ Promise.resolve((response as (input: RequestInfo | URL, init?: RequestInit) => Response)(input, init))
167
+ : () => Promise.resolve(response)
168
+
169
+ // Return cleanup function
170
+ return () => {
171
+ globalThis.fetch = originalFetch
172
+ }
173
+ },
174
+ }
175
+
176
+ /**
177
+ * Test decorators and helpers for Japa tests
178
+ */
179
+ export const JapaTestHelpers = {
180
+ /**
181
+ * Timeout decorator for tests that may take longer
182
+ */
183
+ timeout(ms: number) {
184
+ return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
185
+ const originalMethod = descriptor.value
186
+ descriptor.value = function (...args: unknown[]) {
187
+ const [context] = args
188
+ // Set timeout on test context (implementation may vary)
189
+ if (context && typeof context === 'object' && 'timeout' in context) {
190
+ ;(context as { timeout: number }).timeout = ms
191
+ }
192
+ return originalMethod.apply(this, args)
193
+ }
194
+ }
195
+ },
196
+
197
+ /**
198
+ * Skip test conditionally
199
+ */
200
+ skipIf(condition: boolean, reason?: string) {
201
+ return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
202
+ if (condition) {
203
+ descriptor.value = function (context: TestContext) {
204
+ context.test.skip(true, reason || 'Skipped conditionally')
205
+ }
206
+ }
207
+ }
208
+ },
209
+
210
+ /**
211
+ * Run test only in specific environment
212
+ */
213
+ onlyInEnvironment(env: 'node' | 'browser') {
214
+ return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
215
+ // eslint-disable-next-line no-restricted-globals
216
+ const isNode = typeof window === 'undefined'
217
+ const shouldRun = (env === 'node' && isNode) || (env === 'browser' && !isNode)
218
+
219
+ if (!shouldRun) {
220
+ descriptor.value = function (context: TestContext) {
221
+ context.test.skip(true, `Only runs in ${env} environment`)
222
+ }
223
+ }
224
+ }
225
+ },
226
+ }
227
+
228
+ /**
229
+ * Browser-specific test utilities
230
+ */
231
+ export const BrowserTestUtils = {
232
+ /**
233
+ * Simulates a browser event
234
+ */
235
+ dispatchEvent(element: Element, eventType: string, options: EventInit = {}): Event {
236
+ const event = new Event(eventType, { bubbles: true, cancelable: true, ...options })
237
+ element.dispatchEvent(event)
238
+ return event
239
+ },
240
+
241
+ /**
242
+ * Creates a mock localStorage implementation
243
+ */
244
+ createMockLocalStorage(): Storage {
245
+ const storage: Record<string, string> = {}
246
+
247
+ return {
248
+ getItem: (key: string) => storage[key] || null,
249
+ setItem: (key: string, value: string) => {
250
+ storage[key] = value
251
+ },
252
+ removeItem: (key: string) => {
253
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
254
+ delete storage[key]
255
+ },
256
+ clear: () => {
257
+ Object.keys(storage).forEach((key) => {
258
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
259
+ delete storage[key]
260
+ })
261
+ },
262
+ key: (index: number) => Object.keys(storage)[index] || null,
263
+ get length() {
264
+ return Object.keys(storage).length
265
+ },
266
+ }
267
+ },
268
+
269
+ /**
270
+ * Waits for DOM element to appear
271
+ */
272
+ async waitForElement(selector: string, timeout = 5000): Promise<Element> {
273
+ const startTime = Date.now()
274
+
275
+ while (Date.now() - startTime < timeout) {
276
+ // eslint-disable-next-line no-restricted-globals
277
+ const element = document.querySelector(selector)
278
+ if (element) {
279
+ return element
280
+ }
281
+ await TestUtils.sleep(50)
282
+ }
283
+
284
+ throw new Error(`Element with selector "${selector}" not found within ${timeout}ms`)
285
+ },
286
+
287
+ /**
288
+ * Creates a mock file for file upload testing
289
+ */
290
+ createMockFile(name = 'test.txt', content = 'test content', type = 'text/plain'): File {
291
+ return new File([content], name, { type })
292
+ },
293
+ }
@@ -0,0 +1,33 @@
1
+ import { test } from '@japa/runner'
2
+ import {
3
+ createCalculatedSemantic,
4
+ isCalculatedSemantic,
5
+ type CalculatedConfig,
6
+ } from '../../../../src/modeling/definitions/Calculated.js'
7
+ import { SemanticType, type AppliedDataSemantic } from '../../../../src/modeling/Semantics.js'
8
+
9
+ test.group('Calculated Semantic Configuration', () => {
10
+ test('should create semantic with custom config', ({ assert }) => {
11
+ const config: CalculatedConfig = {
12
+ formula: 'price * quantity',
13
+ dependencies: ['price', 'quantity'],
14
+ recalculateOnUpdate: true,
15
+ allowManualOverride: true,
16
+ }
17
+ const semantic = createCalculatedSemantic(config)
18
+ assert.equal(semantic.id, SemanticType.Calculated)
19
+ assert.deepEqual(semantic.config, config)
20
+ })
21
+
22
+ test('should identify calculated semantic', ({ assert }) => {
23
+ const config: CalculatedConfig = { formula: 'price * quantity' }
24
+ const semantic = createCalculatedSemantic(config)
25
+ assert.isTrue(isCalculatedSemantic(semantic))
26
+ })
27
+
28
+ test('should not identify non-calculated semantic', ({ assert }) => {
29
+ // Simulate a different semantic
30
+ const fakeSemantic: AppliedDataSemantic = { id: SemanticType.Email, config: {} }
31
+ assert.isFalse(isCalculatedSemantic(fakeSemantic))
32
+ })
33
+ })