@savvagent/angular 1.0.1
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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/module.ts.html +289 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/service.ts.html +1846 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +242 -0
- package/coverage/module.ts.html +289 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/service.ts.html +1846 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/README.md +484 -0
- package/dist/esm2022/index.mjs +15 -0
- package/dist/esm2022/module.mjs +75 -0
- package/dist/esm2022/savvagent-angular.mjs +5 -0
- package/dist/esm2022/service.mjs +473 -0
- package/dist/fesm2022/savvagent-angular.mjs +563 -0
- package/dist/fesm2022/savvagent-angular.mjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/module.d.ts +57 -0
- package/dist/service.d.ts +319 -0
- package/jest.config.js +40 -0
- package/ng-package.json +8 -0
- package/package.json +73 -0
- package/setup-jest.ts +2 -0
- package/src/index.spec.ts +144 -0
- package/src/index.ts +38 -0
- package/src/module.spec.ts +283 -0
- package/src/module.ts +68 -0
- package/src/service.spec.ts +945 -0
- package/src/service.ts +587 -0
- package/test-utils/angular-core-mock.ts +28 -0
- package/test-utils/angular-testing-mock.ts +87 -0
- package/tsconfig.json +33 -0
- package/tsconfig.spec.json +11 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SavvagentModule } from './module';
|
|
3
|
+
import { SavvagentService, SAVVAGENT_CONFIG, SavvagentConfig } from './service';
|
|
4
|
+
|
|
5
|
+
describe('SavvagentModule', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
TestBed.resetTestingModule();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Module Configuration', () => {
|
|
11
|
+
it('should create module', () => {
|
|
12
|
+
const module = new SavvagentModule();
|
|
13
|
+
expect(module).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should provide SavvagentService by default', () => {
|
|
17
|
+
TestBed.configureTestingModule({
|
|
18
|
+
imports: [SavvagentModule],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const service = TestBed.inject(SavvagentService);
|
|
22
|
+
expect(service).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('forRoot Configuration', () => {
|
|
27
|
+
const testConfig: SavvagentConfig = {
|
|
28
|
+
config: {
|
|
29
|
+
apiKey: 'sdk_test_api_key',
|
|
30
|
+
baseUrl: 'https://api.test.com',
|
|
31
|
+
},
|
|
32
|
+
defaultContext: {
|
|
33
|
+
applicationId: 'test-app',
|
|
34
|
+
environment: 'test',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
it('should return ModuleWithProviders', () => {
|
|
39
|
+
const moduleWithProviders = SavvagentModule.forRoot(testConfig);
|
|
40
|
+
|
|
41
|
+
expect(moduleWithProviders).toEqual({
|
|
42
|
+
ngModule: SavvagentModule,
|
|
43
|
+
providers: [
|
|
44
|
+
{
|
|
45
|
+
provide: SAVVAGENT_CONFIG,
|
|
46
|
+
useValue: testConfig,
|
|
47
|
+
},
|
|
48
|
+
SavvagentService,
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should provide SAVVAGENT_CONFIG token', () => {
|
|
54
|
+
TestBed.configureTestingModule({
|
|
55
|
+
imports: [SavvagentModule.forRoot(testConfig)],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const config = TestBed.inject(SAVVAGENT_CONFIG);
|
|
59
|
+
expect(config).toEqual(testConfig);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should initialize SavvagentService with config', () => {
|
|
63
|
+
TestBed.configureTestingModule({
|
|
64
|
+
imports: [SavvagentModule.forRoot(testConfig)],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const service = TestBed.inject(SavvagentService);
|
|
68
|
+
expect(service).toBeTruthy();
|
|
69
|
+
expect(service.isReady).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should work with minimal config', () => {
|
|
73
|
+
const minimalConfig: SavvagentConfig = {
|
|
74
|
+
config: {
|
|
75
|
+
apiKey: 'sdk_test_key',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
TestBed.configureTestingModule({
|
|
80
|
+
imports: [SavvagentModule.forRoot(minimalConfig)],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const service = TestBed.inject(SavvagentService);
|
|
84
|
+
expect(service).toBeTruthy();
|
|
85
|
+
expect(service.isReady).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should work with full config including all context fields', () => {
|
|
89
|
+
const fullConfig: SavvagentConfig = {
|
|
90
|
+
config: {
|
|
91
|
+
apiKey: 'sdk_test_api_key',
|
|
92
|
+
baseUrl: 'https://api.test.com',
|
|
93
|
+
enableRealtime: true,
|
|
94
|
+
cacheTtl: 60000,
|
|
95
|
+
onError: (error) => console.error(error),
|
|
96
|
+
},
|
|
97
|
+
defaultContext: {
|
|
98
|
+
applicationId: 'test-app',
|
|
99
|
+
environment: 'production',
|
|
100
|
+
organizationId: 'org-123',
|
|
101
|
+
userId: 'user-456',
|
|
102
|
+
anonymousId: 'anon-789',
|
|
103
|
+
sessionId: 'session-abc',
|
|
104
|
+
language: 'en',
|
|
105
|
+
attributes: {
|
|
106
|
+
plan: 'pro',
|
|
107
|
+
region: 'us-west',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
TestBed.configureTestingModule({
|
|
113
|
+
imports: [SavvagentModule.forRoot(fullConfig)],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const config = TestBed.inject(SAVVAGENT_CONFIG);
|
|
117
|
+
expect(config).toEqual(fullConfig);
|
|
118
|
+
|
|
119
|
+
const service = TestBed.inject(SavvagentService);
|
|
120
|
+
expect(service).toBeTruthy();
|
|
121
|
+
expect(service.isReady).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('Dependency Injection', () => {
|
|
126
|
+
it('should provide same service instance within module scope', () => {
|
|
127
|
+
TestBed.configureTestingModule({
|
|
128
|
+
imports: [
|
|
129
|
+
SavvagentModule.forRoot({
|
|
130
|
+
config: { apiKey: 'test' },
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const service1 = TestBed.inject(SavvagentService);
|
|
136
|
+
const service2 = TestBed.inject(SavvagentService);
|
|
137
|
+
|
|
138
|
+
expect(service1).toBe(service2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should work without forRoot (service providedIn: root)', () => {
|
|
142
|
+
TestBed.configureTestingModule({
|
|
143
|
+
imports: [SavvagentModule],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const service = TestBed.inject(SavvagentService);
|
|
147
|
+
expect(service).toBeTruthy();
|
|
148
|
+
// Service won't be initialized without config
|
|
149
|
+
expect(service.isReady).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Multiple Imports', () => {
|
|
154
|
+
it('should handle multiple module imports', () => {
|
|
155
|
+
const config1: SavvagentConfig = {
|
|
156
|
+
config: { apiKey: 'key1' },
|
|
157
|
+
};
|
|
158
|
+
const config2: SavvagentConfig = {
|
|
159
|
+
config: { apiKey: 'key2' },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// The last imported config should win
|
|
163
|
+
TestBed.configureTestingModule({
|
|
164
|
+
imports: [
|
|
165
|
+
SavvagentModule.forRoot(config1),
|
|
166
|
+
SavvagentModule.forRoot(config2),
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const config = TestBed.inject(SAVVAGENT_CONFIG);
|
|
171
|
+
// Due to Angular's DI, the first provider typically wins,
|
|
172
|
+
// but this tests that the setup doesn't break
|
|
173
|
+
expect(config).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Integration Tests', () => {
|
|
178
|
+
it('should allow service to evaluate flags after module setup', async () => {
|
|
179
|
+
TestBed.configureTestingModule({
|
|
180
|
+
imports: [
|
|
181
|
+
SavvagentModule.forRoot({
|
|
182
|
+
config: { apiKey: 'sdk_test' },
|
|
183
|
+
defaultContext: {
|
|
184
|
+
applicationId: 'test-app',
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const service = TestBed.inject(SavvagentService);
|
|
191
|
+
expect(service).toBeTruthy();
|
|
192
|
+
expect(service.isReady).toBe(true);
|
|
193
|
+
expect(service.flagClient).not.toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should support standalone component pattern (Angular 14+)', () => {
|
|
197
|
+
// Simulate standalone component setup
|
|
198
|
+
const providers = SavvagentModule.forRoot({
|
|
199
|
+
config: { apiKey: 'sdk_test' },
|
|
200
|
+
}).providers || [];
|
|
201
|
+
|
|
202
|
+
TestBed.configureTestingModule({
|
|
203
|
+
providers,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const service = TestBed.inject(SavvagentService);
|
|
207
|
+
expect(service).toBeTruthy();
|
|
208
|
+
expect(service.isReady).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Error Handling', () => {
|
|
213
|
+
it('should handle invalid config gracefully', () => {
|
|
214
|
+
const invalidConfig = {
|
|
215
|
+
config: {} as any, // Missing apiKey
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
expect(() => {
|
|
219
|
+
TestBed.configureTestingModule({
|
|
220
|
+
imports: [SavvagentModule.forRoot(invalidConfig)],
|
|
221
|
+
});
|
|
222
|
+
}).not.toThrow();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle null/undefined values in config', () => {
|
|
226
|
+
const configWithNulls: SavvagentConfig = {
|
|
227
|
+
config: {
|
|
228
|
+
apiKey: 'test',
|
|
229
|
+
baseUrl: undefined,
|
|
230
|
+
},
|
|
231
|
+
defaultContext: {
|
|
232
|
+
applicationId: undefined,
|
|
233
|
+
environment: undefined,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
TestBed.configureTestingModule({
|
|
238
|
+
imports: [SavvagentModule.forRoot(configWithNulls)],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const service = TestBed.inject(SavvagentService);
|
|
242
|
+
expect(service).toBeTruthy();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Type Safety', () => {
|
|
247
|
+
it('should enforce correct config structure', () => {
|
|
248
|
+
const validConfig: SavvagentConfig = {
|
|
249
|
+
config: {
|
|
250
|
+
apiKey: 'test_api_key',
|
|
251
|
+
},
|
|
252
|
+
defaultContext: {
|
|
253
|
+
userId: 'user-123',
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const moduleWithProviders = SavvagentModule.forRoot(validConfig);
|
|
258
|
+
expect(moduleWithProviders.ngModule).toBe(SavvagentModule);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should accept all valid defaultContext properties', () => {
|
|
262
|
+
const config: SavvagentConfig = {
|
|
263
|
+
config: { apiKey: 'test' },
|
|
264
|
+
defaultContext: {
|
|
265
|
+
applicationId: 'app',
|
|
266
|
+
environment: 'prod',
|
|
267
|
+
organizationId: 'org',
|
|
268
|
+
userId: 'user',
|
|
269
|
+
anonymousId: 'anon',
|
|
270
|
+
sessionId: 'session',
|
|
271
|
+
language: 'en',
|
|
272
|
+
attributes: { key: 'value' },
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
expect(() => {
|
|
277
|
+
TestBed.configureTestingModule({
|
|
278
|
+
imports: [SavvagentModule.forRoot(config)],
|
|
279
|
+
});
|
|
280
|
+
}).not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NgModule, ModuleWithProviders } from '@angular/core';
|
|
2
|
+
import { SavvagentService, SavvagentConfig, SAVVAGENT_CONFIG } from './service';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Angular module for Savvagent feature flags.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // app.module.ts
|
|
10
|
+
* import { SavvagentModule } from '@savvagent/angular';
|
|
11
|
+
*
|
|
12
|
+
* @NgModule({
|
|
13
|
+
* imports: [
|
|
14
|
+
* SavvagentModule.forRoot({
|
|
15
|
+
* config: {
|
|
16
|
+
* apiKey: 'sdk_your_api_key',
|
|
17
|
+
* baseUrl: 'https://api.savvagent.com'
|
|
18
|
+
* },
|
|
19
|
+
* defaultContext: {
|
|
20
|
+
* applicationId: 'my-app',
|
|
21
|
+
* environment: 'production',
|
|
22
|
+
* userId: 'user-123'
|
|
23
|
+
* }
|
|
24
|
+
* })
|
|
25
|
+
* ]
|
|
26
|
+
* })
|
|
27
|
+
* export class AppModule {}
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // For standalone components (Angular 14+)
|
|
33
|
+
* import { SavvagentModule } from '@savvagent/angular';
|
|
34
|
+
*
|
|
35
|
+
* bootstrapApplication(AppComponent, {
|
|
36
|
+
* providers: [
|
|
37
|
+
* importProvidersFrom(
|
|
38
|
+
* SavvagentModule.forRoot({
|
|
39
|
+
* config: { apiKey: 'sdk_...' }
|
|
40
|
+
* })
|
|
41
|
+
* )
|
|
42
|
+
* ]
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
@NgModule({
|
|
47
|
+
providers: [SavvagentService]
|
|
48
|
+
})
|
|
49
|
+
export class SavvagentModule {
|
|
50
|
+
/**
|
|
51
|
+
* Configure the Savvagent module with API key and default context.
|
|
52
|
+
*
|
|
53
|
+
* @param savvagentConfig - Configuration including API key and optional default context
|
|
54
|
+
* @returns Module with providers
|
|
55
|
+
*/
|
|
56
|
+
static forRoot(savvagentConfig: SavvagentConfig): ModuleWithProviders<SavvagentModule> {
|
|
57
|
+
return {
|
|
58
|
+
ngModule: SavvagentModule,
|
|
59
|
+
providers: [
|
|
60
|
+
{
|
|
61
|
+
provide: SAVVAGENT_CONFIG,
|
|
62
|
+
useValue: savvagentConfig
|
|
63
|
+
},
|
|
64
|
+
SavvagentService
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|