@openmrs/esm-extensions 8.0.1-pre.3518 → 8.0.1-pre.3525
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/.turbo/turbo-build.log +1 -1
- package/package.json +10 -8
- package/src/extensions.test.ts +472 -6
package/.turbo/turbo-build.log
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-extensions",
|
|
3
|
-
"version": "8.0.1-pre.
|
|
3
|
+
"version": "8.0.1-pre.3525",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Coordinates extensions and extension points in the OpenMRS Frontend",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "cross-env TZ=UTC vitest run --passWithNoTests",
|
|
26
26
|
"test:watch": "cross-env TZ=UTC vitest watch --passWithNoTests",
|
|
27
|
+
"coverage": "cross-env TZ=UTC vitest run --coverage --passWithNoTests",
|
|
27
28
|
"build": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
|
|
28
29
|
"build:development": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
|
|
29
30
|
"typescript": "tsc --project tsconfig.build.json",
|
|
@@ -64,20 +65,21 @@
|
|
|
64
65
|
"single-spa": "6.x"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
|
-
"@openmrs/esm-api": "8.0.1-pre.
|
|
68
|
-
"@openmrs/esm-config": "8.0.1-pre.
|
|
69
|
-
"@openmrs/esm-expression-evaluator": "8.0.1-pre.
|
|
70
|
-
"@openmrs/esm-feature-flags": "8.0.1-pre.
|
|
71
|
-
"@openmrs/esm-state": "8.0.1-pre.
|
|
72
|
-
"@openmrs/esm-utils": "8.0.1-pre.
|
|
68
|
+
"@openmrs/esm-api": "8.0.1-pre.3525",
|
|
69
|
+
"@openmrs/esm-config": "8.0.1-pre.3525",
|
|
70
|
+
"@openmrs/esm-expression-evaluator": "8.0.1-pre.3525",
|
|
71
|
+
"@openmrs/esm-feature-flags": "8.0.1-pre.3525",
|
|
72
|
+
"@openmrs/esm-state": "8.0.1-pre.3525",
|
|
73
|
+
"@openmrs/esm-utils": "8.0.1-pre.3525",
|
|
73
74
|
"@swc/cli": "^0.7.7",
|
|
74
75
|
"@swc/core": "^1.11.29",
|
|
76
|
+
"@vitest/coverage-v8": "^4.0.7",
|
|
75
77
|
"concurrently": "^9.1.2",
|
|
76
78
|
"cross-env": "^7.0.3",
|
|
77
79
|
"happy-dom": "^17.4.7",
|
|
78
80
|
"rimraf": "^6.0.1",
|
|
79
81
|
"single-spa": "^6.0.3",
|
|
80
|
-
"vitest": "^
|
|
82
|
+
"vitest": "^4.0.7"
|
|
81
83
|
},
|
|
82
84
|
"stableVersion": "8.0.0"
|
|
83
85
|
}
|
package/src/extensions.test.ts
CHANGED
|
@@ -1,17 +1,483 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { createGlobalStore } from '@openmrs/esm-state';
|
|
3
|
-
import {
|
|
3
|
+
import type { Session } from '@openmrs/esm-api';
|
|
4
|
+
import {
|
|
5
|
+
attach,
|
|
6
|
+
detach,
|
|
7
|
+
detachAll,
|
|
8
|
+
getAssignedExtensions,
|
|
9
|
+
getExtensionNameFromId,
|
|
10
|
+
getExtensionRegistration,
|
|
11
|
+
getExtensionRegistrationFrom,
|
|
12
|
+
registerExtension,
|
|
13
|
+
registerExtensionSlot,
|
|
14
|
+
updateExtensionSlotState,
|
|
15
|
+
} from './extensions';
|
|
16
|
+
import type { ExtensionInfo, ExtensionInternalStore, ExtensionRegistration } from './store';
|
|
17
|
+
import { getExtensionInternalStore } from './store';
|
|
4
18
|
|
|
19
|
+
// Minimal mocking - only what we need for fine-grained control
|
|
5
20
|
vi.mock('@openmrs/esm-api', () => ({
|
|
6
21
|
sessionStore: createGlobalStore('mock-session-store', {
|
|
7
22
|
loaded: false,
|
|
8
23
|
session: null,
|
|
9
24
|
}),
|
|
25
|
+
userHasAccess: vi.fn(() => true),
|
|
10
26
|
}));
|
|
11
27
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
vi.mock('@openmrs/esm-utils', async (importOriginal) => {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
30
|
+
const actual = await importOriginal<typeof import('@openmrs/esm-utils')>();
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
isOnline: vi.fn(() => true),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
vi.mock('@openmrs/esm-globals', async (importOriginal) => {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
39
|
+
const actual = await importOriginal<typeof import('@openmrs/esm-globals')>();
|
|
40
|
+
return {
|
|
41
|
+
...actual,
|
|
42
|
+
subscribeConnectivityChanged: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Helper to create unique names for test isolation
|
|
47
|
+
let nameCounter = 0;
|
|
48
|
+
function getUniqueName(prefix: string = 'test'): string {
|
|
49
|
+
return `${prefix}-${++nameCounter}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Helper to create a mock extension registration
|
|
53
|
+
function createMockExtension(name: string, overrides: Partial<ExtensionRegistration> = {}): ExtensionInfo {
|
|
54
|
+
return {
|
|
55
|
+
name,
|
|
56
|
+
load: vi.fn(async () => ({ bootstrap: vi.fn(), mount: vi.fn(), unmount: vi.fn() })),
|
|
57
|
+
moduleName: `${name}-module`,
|
|
58
|
+
meta: {},
|
|
59
|
+
instances: [],
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('getExtensionNameFromId', () => {
|
|
65
|
+
it('should extract the extension name from a simple ID', () => {
|
|
66
|
+
expect(getExtensionNameFromId('foo')).toBe('foo');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should extract the extension name from an ID with # separator', () => {
|
|
70
|
+
expect(getExtensionNameFromId('foo#bar')).toBe('foo');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should extract the extension name from an ID with multiple # separators', () => {
|
|
74
|
+
expect(getExtensionNameFromId('foo#bar#baz')).toBe('foo');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle empty string', () => {
|
|
78
|
+
expect(getExtensionNameFromId('')).toBe('');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('getExtensionRegistrationFrom', () => {
|
|
83
|
+
it('should return the extension registration if it exists', () => {
|
|
84
|
+
const mockExtension = createMockExtension('test-extension');
|
|
85
|
+
|
|
86
|
+
const state: ExtensionInternalStore = {
|
|
87
|
+
slots: {},
|
|
88
|
+
extensions: {
|
|
89
|
+
'test-extension': mockExtension,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(getExtensionRegistrationFrom(state, 'test-extension')).toBe(mockExtension);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return undefined if the extension does not exist', () => {
|
|
97
|
+
const state: ExtensionInternalStore = {
|
|
98
|
+
slots: {},
|
|
99
|
+
extensions: {},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
expect(getExtensionRegistrationFrom(state, 'non-existent')).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle extension IDs with # separator', () => {
|
|
106
|
+
const mockExtension = createMockExtension('test-extension');
|
|
107
|
+
|
|
108
|
+
const state: ExtensionInternalStore = {
|
|
109
|
+
slots: {},
|
|
110
|
+
extensions: {
|
|
111
|
+
'test-extension': mockExtension,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect(getExtensionRegistrationFrom(state, 'test-extension#instance1')).toBe(mockExtension);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getExtensionRegistration', () => {
|
|
120
|
+
it('should return undefined for non-existent extension', () => {
|
|
121
|
+
const result = getExtensionRegistration('non-existent-extension-xyz');
|
|
122
|
+
expect(result).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return the extension registration for a registered extension', () => {
|
|
126
|
+
const extensionName = getUniqueName('registered-extension');
|
|
127
|
+
const mockExtension = createMockExtension(extensionName);
|
|
128
|
+
|
|
129
|
+
registerExtension(mockExtension);
|
|
130
|
+
|
|
131
|
+
const result = getExtensionRegistration(extensionName);
|
|
132
|
+
expect(result?.name).toBe(extensionName);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle extension IDs with # separator', () => {
|
|
136
|
+
const extensionName = getUniqueName('extension-with-hash');
|
|
137
|
+
const mockExtension = createMockExtension(extensionName);
|
|
138
|
+
|
|
139
|
+
registerExtension(mockExtension);
|
|
140
|
+
|
|
141
|
+
const result = getExtensionRegistration(`${extensionName}#instance1`);
|
|
142
|
+
expect(result?.name).toBe(extensionName);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('attach', () => {
|
|
147
|
+
it('should attach an extension to a non-existent slot', () => {
|
|
148
|
+
const slotName = getUniqueName('test-slot');
|
|
149
|
+
const extensionId = 'test-extension';
|
|
150
|
+
|
|
151
|
+
attach(slotName, extensionId);
|
|
152
|
+
|
|
153
|
+
const store = getExtensionInternalStore();
|
|
154
|
+
const state = store.getState();
|
|
155
|
+
|
|
156
|
+
expect(state.slots[slotName]).toBeDefined();
|
|
157
|
+
expect(state.slots[slotName].attachedIds).toContain(extensionId);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should attach an extension to an existing slot', () => {
|
|
161
|
+
const slotName = getUniqueName('test-slot-existing');
|
|
162
|
+
const extensionId1 = 'extension-1';
|
|
163
|
+
const extensionId2 = 'extension-2';
|
|
164
|
+
|
|
165
|
+
attach(slotName, extensionId1);
|
|
166
|
+
attach(slotName, extensionId2);
|
|
167
|
+
|
|
168
|
+
const store = getExtensionInternalStore();
|
|
169
|
+
const state = store.getState();
|
|
170
|
+
|
|
171
|
+
expect(state.slots[slotName].attachedIds).toContain(extensionId1);
|
|
172
|
+
expect(state.slots[slotName].attachedIds).toContain(extensionId2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should allow attaching the same extension multiple times', () => {
|
|
176
|
+
const slotName = getUniqueName('test-slot-duplicate');
|
|
177
|
+
const extensionId = 'duplicate-extension';
|
|
178
|
+
|
|
179
|
+
attach(slotName, extensionId);
|
|
180
|
+
attach(slotName, extensionId);
|
|
181
|
+
|
|
182
|
+
const store = getExtensionInternalStore();
|
|
183
|
+
const state = store.getState();
|
|
184
|
+
|
|
185
|
+
// Both instances should be in the attachedIds array
|
|
186
|
+
const count = state.slots[slotName].attachedIds.filter((id) => id === extensionId).length;
|
|
187
|
+
expect(count).toBe(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle extension IDs with # separator', () => {
|
|
191
|
+
const slotName = getUniqueName('test-slot-with-hash');
|
|
192
|
+
const extensionId = 'extension#instance1';
|
|
193
|
+
|
|
194
|
+
attach(slotName, extensionId);
|
|
195
|
+
|
|
196
|
+
const store = getExtensionInternalStore();
|
|
197
|
+
const state = store.getState();
|
|
198
|
+
|
|
199
|
+
expect(state.slots[slotName].attachedIds).toContain(extensionId);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should create a slot with the correct initial structure', () => {
|
|
203
|
+
const slotName = getUniqueName('test-slot-structure');
|
|
204
|
+
const extensionId = 'test-extension';
|
|
205
|
+
|
|
206
|
+
attach(slotName, extensionId);
|
|
207
|
+
|
|
208
|
+
const store = getExtensionInternalStore();
|
|
209
|
+
const state = store.getState();
|
|
210
|
+
const slot = state.slots[slotName];
|
|
211
|
+
|
|
212
|
+
expect(slot).toEqual({
|
|
213
|
+
moduleName: undefined,
|
|
214
|
+
name: slotName,
|
|
215
|
+
attachedIds: [extensionId],
|
|
216
|
+
config: null,
|
|
217
|
+
state: undefined,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('detach', () => {
|
|
223
|
+
it('should detach an extension from a slot', () => {
|
|
224
|
+
const slotName = getUniqueName('test-slot-detach');
|
|
225
|
+
const extensionId = 'extension-to-detach';
|
|
226
|
+
|
|
227
|
+
attach(slotName, extensionId);
|
|
228
|
+
detach(slotName, extensionId);
|
|
229
|
+
|
|
230
|
+
const store = getExtensionInternalStore();
|
|
231
|
+
const state = store.getState();
|
|
232
|
+
|
|
233
|
+
expect(state.slots[slotName].attachedIds).not.toContain(extensionId);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not throw when detaching from a non-existent slot', () => {
|
|
237
|
+
const slotName = getUniqueName('non-existent-slot');
|
|
238
|
+
|
|
239
|
+
expect(() => detach(slotName, 'some-extension')).not.toThrow();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should not throw when detaching a non-attached extension', () => {
|
|
243
|
+
const slotName = getUniqueName('test-slot-no-ext');
|
|
244
|
+
|
|
245
|
+
attach(slotName, 'other-extension');
|
|
246
|
+
|
|
247
|
+
expect(() => detach(slotName, 'non-attached-extension')).not.toThrow();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should only detach the specified extension', () => {
|
|
251
|
+
const slotName = getUniqueName('test-slot-multi');
|
|
252
|
+
const ext1 = 'extension-1';
|
|
253
|
+
const ext2 = 'extension-2';
|
|
254
|
+
|
|
255
|
+
attach(slotName, ext1);
|
|
256
|
+
attach(slotName, ext2);
|
|
257
|
+
detach(slotName, ext1);
|
|
258
|
+
|
|
259
|
+
const store = getExtensionInternalStore();
|
|
260
|
+
const state = store.getState();
|
|
261
|
+
|
|
262
|
+
expect(state.slots[slotName].attachedIds).not.toContain(ext1);
|
|
263
|
+
expect(state.slots[slotName].attachedIds).toContain(ext2);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should not modify state when detaching from non-existent slot', () => {
|
|
267
|
+
const slotName = getUniqueName('non-existent-detach');
|
|
268
|
+
const store = getExtensionInternalStore();
|
|
269
|
+
const stateBefore = store.getState();
|
|
270
|
+
|
|
271
|
+
detach(slotName, 'some-extension');
|
|
272
|
+
|
|
273
|
+
const stateAfter = store.getState();
|
|
274
|
+
expect(stateAfter).toBe(stateBefore);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should not modify state when detaching non-attached extension', () => {
|
|
278
|
+
const slotName = getUniqueName('detach-non-attached');
|
|
279
|
+
|
|
280
|
+
attach(slotName, 'existing-extension');
|
|
281
|
+
|
|
282
|
+
const store = getExtensionInternalStore();
|
|
283
|
+
const stateBefore = store.getState();
|
|
284
|
+
|
|
285
|
+
detach(slotName, 'non-existent-extension');
|
|
286
|
+
|
|
287
|
+
const stateAfter = store.getState();
|
|
288
|
+
expect(stateAfter).toBe(stateBefore);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('detachAll', () => {
|
|
293
|
+
it('should detach all extensions from a slot', () => {
|
|
294
|
+
const slotName = getUniqueName('test-slot-detach-all');
|
|
295
|
+
|
|
296
|
+
attach(slotName, 'extension-1');
|
|
297
|
+
attach(slotName, 'extension-2');
|
|
298
|
+
attach(slotName, 'extension-3');
|
|
299
|
+
|
|
300
|
+
detachAll(slotName);
|
|
301
|
+
|
|
302
|
+
const store = getExtensionInternalStore();
|
|
303
|
+
const state = store.getState();
|
|
304
|
+
|
|
305
|
+
expect(state.slots[slotName].attachedIds).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should not throw when detaching all from a non-existent slot', () => {
|
|
309
|
+
const slotName = getUniqueName('non-existent-slot-all');
|
|
310
|
+
|
|
311
|
+
expect(() => detachAll(slotName)).not.toThrow();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle detaching all from an empty slot', () => {
|
|
315
|
+
const slotName = getUniqueName('test-slot-empty');
|
|
316
|
+
|
|
317
|
+
attach(slotName, 'some-extension');
|
|
318
|
+
detachAll(slotName);
|
|
319
|
+
detachAll(slotName);
|
|
320
|
+
|
|
321
|
+
const store = getExtensionInternalStore();
|
|
322
|
+
const state = store.getState();
|
|
323
|
+
|
|
324
|
+
expect(state.slots[slotName].attachedIds).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should not modify state when detaching all from non-existent slot', () => {
|
|
328
|
+
const slotName = getUniqueName('non-existent-all');
|
|
329
|
+
const store = getExtensionInternalStore();
|
|
330
|
+
const stateBefore = store.getState();
|
|
331
|
+
|
|
332
|
+
detachAll(slotName);
|
|
333
|
+
|
|
334
|
+
const stateAfter = store.getState();
|
|
335
|
+
expect(stateAfter).toBe(stateBefore);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('registerExtensionSlot', () => {
|
|
340
|
+
it('should not crash when a slot is registered before the extensions that go in it', () => {
|
|
341
|
+
const slotName = getUniqueName('mario-slot');
|
|
342
|
+
|
|
343
|
+
attach(slotName, 'mario-hat');
|
|
344
|
+
expect(() => registerExtensionSlot('mario-module', slotName)).not.toThrow();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should register a slot with module name', () => {
|
|
348
|
+
const slotName = getUniqueName('slot-with-module');
|
|
349
|
+
const moduleName = 'test-module';
|
|
350
|
+
|
|
351
|
+
registerExtensionSlot(moduleName, slotName);
|
|
352
|
+
|
|
353
|
+
const store = getExtensionInternalStore();
|
|
354
|
+
const state = store.getState();
|
|
355
|
+
|
|
356
|
+
expect(state.slots[slotName]).toBeDefined();
|
|
357
|
+
expect(state.slots[slotName].moduleName).toBe(moduleName);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should register a slot with custom state', () => {
|
|
361
|
+
const slotName = getUniqueName('slot-with-state');
|
|
362
|
+
const customState = { foo: 'bar', count: 42 };
|
|
363
|
+
|
|
364
|
+
registerExtensionSlot('test-module', slotName, customState);
|
|
365
|
+
|
|
366
|
+
const store = getExtensionInternalStore();
|
|
367
|
+
const state = store.getState();
|
|
368
|
+
|
|
369
|
+
expect(state.slots[slotName].state).toEqual(customState);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should preserve attachedIds when registering an existing slot', () => {
|
|
373
|
+
const slotName = getUniqueName('preserve-attached');
|
|
374
|
+
const extensionId = 'test-extension';
|
|
375
|
+
|
|
376
|
+
attach(slotName, extensionId);
|
|
377
|
+
registerExtensionSlot('test-module', slotName);
|
|
378
|
+
|
|
379
|
+
const store = getExtensionInternalStore();
|
|
380
|
+
const state = store.getState();
|
|
381
|
+
|
|
382
|
+
expect(state.slots[slotName].attachedIds).toContain(extensionId);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('updateExtensionSlotState', () => {
|
|
387
|
+
it('should update the full state when partial is false', () => {
|
|
388
|
+
const slotName = getUniqueName('test-slot-state');
|
|
389
|
+
|
|
390
|
+
registerExtensionSlot('test-module', slotName, { initial: 'state' });
|
|
391
|
+
|
|
392
|
+
const newState = { updated: 'state', count: 1 };
|
|
393
|
+
updateExtensionSlotState(slotName, newState, false);
|
|
394
|
+
|
|
395
|
+
const store = getExtensionInternalStore();
|
|
396
|
+
const state = store.getState();
|
|
397
|
+
|
|
398
|
+
expect(state.slots[slotName].state).toEqual(newState);
|
|
399
|
+
expect(state.slots[slotName].state).not.toHaveProperty('initial');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should merge state when partial is true', () => {
|
|
403
|
+
const slotName = getUniqueName('test-slot-partial');
|
|
404
|
+
|
|
405
|
+
registerExtensionSlot('test-module', slotName, { foo: 'bar', count: 1 });
|
|
406
|
+
|
|
407
|
+
const partialState = { count: 2, newProp: 'value' };
|
|
408
|
+
updateExtensionSlotState(slotName, partialState, true);
|
|
409
|
+
|
|
410
|
+
const store = getExtensionInternalStore();
|
|
411
|
+
const state = store.getState();
|
|
412
|
+
|
|
413
|
+
expect(state.slots[slotName].state).toEqual({
|
|
414
|
+
foo: 'bar',
|
|
415
|
+
count: 2,
|
|
416
|
+
newProp: 'value',
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should default partial to false when not specified', () => {
|
|
421
|
+
const slotName = getUniqueName('test-slot-default');
|
|
422
|
+
|
|
423
|
+
registerExtensionSlot('test-module', slotName, { foo: 'bar' });
|
|
424
|
+
|
|
425
|
+
const newState = { new: 'state' };
|
|
426
|
+
updateExtensionSlotState(slotName, newState);
|
|
427
|
+
|
|
428
|
+
const store = getExtensionInternalStore();
|
|
429
|
+
const state = store.getState();
|
|
430
|
+
|
|
431
|
+
expect(state.slots[slotName].state).toEqual(newState);
|
|
432
|
+
expect(state.slots[slotName].state).not.toHaveProperty('foo');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('getAssignedExtensions', () => {
|
|
437
|
+
it('should return an empty array for a slot with no registered extensions', () => {
|
|
438
|
+
const slotName = getUniqueName('empty-slot');
|
|
439
|
+
|
|
440
|
+
attach(slotName, 'non-registered-extension');
|
|
441
|
+
|
|
442
|
+
const result = getAssignedExtensions(slotName);
|
|
443
|
+
expect(result).toEqual([]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should return assigned extensions for a slot with registered extensions', () => {
|
|
447
|
+
const slotName = getUniqueName('slot-with-extensions');
|
|
448
|
+
const extensionName = getUniqueName('extension');
|
|
449
|
+
|
|
450
|
+
const mockExtension = createMockExtension(extensionName);
|
|
451
|
+
registerExtension(mockExtension);
|
|
452
|
+
attach(slotName, extensionName);
|
|
453
|
+
|
|
454
|
+
const result = getAssignedExtensions(slotName);
|
|
455
|
+
|
|
456
|
+
expect(result).toHaveLength(1);
|
|
457
|
+
expect(result[0].name).toBe(extensionName);
|
|
458
|
+
expect(result[0].id).toBe(extensionName);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should return empty array for slot with no attached extensions', () => {
|
|
462
|
+
const slotName = getUniqueName('empty-registered-slot');
|
|
463
|
+
|
|
464
|
+
registerExtensionSlot('test-module', slotName);
|
|
465
|
+
|
|
466
|
+
const result = getAssignedExtensions(slotName);
|
|
467
|
+
expect(result).toEqual([]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should include extension metadata', () => {
|
|
471
|
+
const slotName = getUniqueName('slot-with-meta');
|
|
472
|
+
const extensionName = getUniqueName('extension-meta');
|
|
473
|
+
const meta = { version: '1.0', author: 'test' };
|
|
474
|
+
|
|
475
|
+
const mockExtension = createMockExtension(extensionName, { meta });
|
|
476
|
+
registerExtension(mockExtension);
|
|
477
|
+
attach(slotName, extensionName);
|
|
478
|
+
|
|
479
|
+
const result = getAssignedExtensions(slotName);
|
|
480
|
+
|
|
481
|
+
expect(result[0].meta).toEqual(meta);
|
|
16
482
|
});
|
|
17
483
|
});
|