@linagora/linid-im-front-corelib 0.0.4 → 0.0.6

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 (35) hide show
  1. package/.github/workflows/pull-request.yml +26 -1
  2. package/.github/workflows/release.yml +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/CONTRIBUTING.md +7 -2
  5. package/dist/core-lib.es.js +921 -851
  6. package/dist/core-lib.umd.js +9 -9
  7. package/dist/package.json +20 -12
  8. package/dist/types/src/index.d.ts +3 -0
  9. package/dist/types/src/services/httpClientService.d.ts +13 -0
  10. package/dist/types/src/services/linIdConfigurationService.d.ts +21 -0
  11. package/dist/types/src/stores/linIdConfigurationStore.d.ts +79 -0
  12. package/dist/types/src/types/linidConfiguration.d.ts +42 -0
  13. package/docs/services.md +39 -0
  14. package/docs/types-and-interfaces.md +56 -9
  15. package/eslint.config.js +2 -1
  16. package/package.json +19 -11
  17. package/src/index.ts +11 -0
  18. package/src/services/httpClientService.ts +61 -0
  19. package/src/services/linIdConfigurationService.ts +73 -0
  20. package/src/stores/linIdConfigurationStore.ts +116 -0
  21. package/src/types/linidConfiguration.ts +70 -0
  22. package/tests/unit/components/LinidZoneRenderer.spec.js +135 -0
  23. package/tests/unit/lifecycle/skeleton.spec.js +138 -0
  24. package/tests/unit/services/federationService.spec.js +146 -0
  25. package/tests/unit/services/httpClientService.spec.js +49 -0
  26. package/tests/unit/services/linIdConfigurationService.spec.js +113 -0
  27. package/tests/unit/stores/linIdConfigurationStore.spec.js +171 -0
  28. package/tests/unit/stores/linidZoneStore.spec.js +94 -0
  29. package/tsconfig.json +11 -27
  30. package/tsconfig.lib.json +20 -0
  31. package/tsconfig.node.json +9 -0
  32. package/tsconfig.spec.json +16 -0
  33. package/vite.config.ts +11 -16
  34. package/vitest.config.ts +19 -0
  35. package/dist/types/vite.config.d.ts +0 -2
@@ -0,0 +1,135 @@
1
+ import { flushPromises, shallowMount } from '@vue/test-utils';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import LinidZoneRenderer from 'src/components/LinidZoneRenderer.vue';
4
+ import * as federationService from 'src/services/federationService';
5
+ import { useLinidZoneStore } from 'src/stores/linidZoneStore';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { nextTick, watch } from 'vue';
8
+
9
+ vi.mock('src/services/federationService', () => ({
10
+ loadAsyncComponent: vi.fn(),
11
+ }));
12
+
13
+ describe('Test component: LinidZoneRenderer', () => {
14
+ let pinia;
15
+ let store;
16
+ let wrapper;
17
+
18
+ beforeEach(() => {
19
+ pinia = createPinia();
20
+ setActivePinia(pinia);
21
+ store = useLinidZoneStore();
22
+ wrapper = shallowMount(LinidZoneRenderer, {
23
+ props: { zone: '' },
24
+ global: { plugins: [pinia] },
25
+ });
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ describe('Test watchEffect', () => {
30
+ it('should initialize isLoadingComplete to false when mounted', async () => {
31
+ const values = [];
32
+ const stopWatch = watch(
33
+ () => wrapper.vm.isLoadingComplete,
34
+ (newVal) => {
35
+ values.push(newVal);
36
+ },
37
+ { flush: 'sync' }
38
+ );
39
+
40
+ await wrapper.setProps({ zone: 'zone-test' });
41
+
42
+ await flushPromises();
43
+ await nextTick();
44
+ stopWatch();
45
+
46
+ expect(values).toEqual([false, true]);
47
+ });
48
+
49
+ it('should set isLoadingComplete to true after initialization', async () => {
50
+ wrapper.setProps({ zone: 'any-zone' });
51
+
52
+ await flushPromises();
53
+ await nextTick();
54
+
55
+ expect(wrapper.vm.isLoadingComplete).toBe(true);
56
+ });
57
+
58
+ it('should not call loadAsyncComponent when the zone is not registered in store', async () => {
59
+ wrapper.setProps({ zone: 'unregistered-zone' });
60
+
61
+ await flushPromises();
62
+ await nextTick();
63
+
64
+ expect(federationService.loadAsyncComponent).not.toHaveBeenCalled();
65
+ expect(wrapper.vm.components).toEqual([]);
66
+ });
67
+
68
+ it('should not call loadAsyncComponent when the zone is registered has no entries', async () => {
69
+ wrapper.vm.linidZoneStore.zones['empty-zone'] = [];
70
+
71
+ wrapper.setProps({ zone: 'empty-zone' });
72
+
73
+ await flushPromises();
74
+ await nextTick();
75
+
76
+ expect(federationService.loadAsyncComponent).not.toHaveBeenCalled();
77
+ expect(wrapper.vm.components).toEqual([]);
78
+ });
79
+
80
+ it('should load components from store when zone is registered and has entries', async () => {
81
+ const MockComponentA = {
82
+ name: 'MockComponent',
83
+ template: '<div>Mock</div>',
84
+ };
85
+ const MockComponentB = {
86
+ name: 'MockComponent',
87
+ template: '<div>Mock</div>',
88
+ };
89
+ vi.mocked(federationService.loadAsyncComponent)
90
+ .mockReturnValueOnce(MockComponentA)
91
+ .mockReturnValueOnce(MockComponentB);
92
+
93
+ store.register('test-zone', {
94
+ plugin: 'test-plugin/MockComponentA',
95
+ props: { title: 'Test' },
96
+ });
97
+
98
+ store.register('test-zone', {
99
+ plugin: 'test-plugin/MockComponentB',
100
+ props: {},
101
+ });
102
+
103
+ wrapper.setProps({ zone: 'test-zone' });
104
+
105
+ await flushPromises();
106
+ await nextTick();
107
+
108
+ expect(federationService.loadAsyncComponent).toHaveBeenCalledTimes(2);
109
+ expect(federationService.loadAsyncComponent).toHaveBeenCalledWith(
110
+ 'test-plugin/MockComponentA'
111
+ );
112
+ expect(federationService.loadAsyncComponent).toHaveBeenCalledWith(
113
+ 'test-plugin/MockComponentB'
114
+ );
115
+
116
+ expect(wrapper.vm.components).toHaveLength(2);
117
+
118
+ expect(wrapper.vm.components[0].plugin).toBe(
119
+ 'test-plugin/MockComponentA'
120
+ );
121
+ expect(wrapper.vm.components[0].props).toEqual({ title: 'Test' });
122
+ expect(JSON.stringify(wrapper.vm.components[0].component)).toEqual(
123
+ JSON.stringify(MockComponentA)
124
+ );
125
+
126
+ expect(wrapper.vm.components[1].plugin).toBe(
127
+ 'test-plugin/MockComponentB'
128
+ );
129
+ expect(wrapper.vm.components[1].props).toEqual({});
130
+ expect(JSON.stringify(wrapper.vm.components[1].component)).toEqual(
131
+ JSON.stringify(MockComponentB)
132
+ );
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,138 @@
1
+ import { BasicRemoteModule } from 'src/lifecycle/skeleton';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ describe('Test class: BasicRemoteModule', () => {
5
+ describe('Test constructor', () => {
6
+ it('should create a module with required parameters', () => {
7
+ const module = new BasicRemoteModule(
8
+ 'test-module',
9
+ 'Test Module',
10
+ '1.0.0'
11
+ );
12
+
13
+ expect(module.id).toBe('test-module');
14
+ expect(module.name).toBe('Test Module');
15
+ expect(module.version).toBe('1.0.0');
16
+ expect(module.description).toBeUndefined();
17
+ });
18
+
19
+ it('should create a module with description', () => {
20
+ const module = new BasicRemoteModule(
21
+ 'test-module',
22
+ 'Test Module',
23
+ '1.0.0',
24
+ 'A test module for unit testing'
25
+ );
26
+
27
+ expect(module.id).toBe('test-module');
28
+ expect(module.name).toBe('Test Module');
29
+ expect(module.version).toBe('1.0.0');
30
+ expect(module.description).toBe('A test module for unit testing');
31
+ });
32
+ });
33
+
34
+ describe('Test lifecycle hooks', () => {
35
+ let module;
36
+
37
+ beforeEach(() => {
38
+ module = new BasicRemoteModule(
39
+ 'test-module',
40
+ 'Test Module',
41
+ '1.0.0',
42
+ 'Test description'
43
+ );
44
+ });
45
+
46
+ describe('Test hook: setup', () => {
47
+ it('should return success by default', async () => {
48
+ const result = await module.setup();
49
+
50
+ expect(result).toEqual({ success: true });
51
+ });
52
+ });
53
+
54
+ describe('Test hook: configure', () => {
55
+ it('should return success with empty config', async () => {
56
+ const result = await module.configure({});
57
+
58
+ expect(result).toEqual({ success: true });
59
+ });
60
+
61
+ it('should return success with valid config', async () => {
62
+ const config = {
63
+ id: 'test-module',
64
+ remoteName: 'testModule',
65
+ };
66
+
67
+ const result = await module.configure(config);
68
+
69
+ expect(result).toEqual({ success: true });
70
+ });
71
+ });
72
+
73
+ describe('Test hook: initialize', () => {
74
+ it('should return success by default', async () => {
75
+ const result = await module.initialize();
76
+
77
+ expect(result).toEqual({ success: true });
78
+ });
79
+ });
80
+
81
+ describe('Test hook: ready', () => {
82
+ it('should return success by default', async () => {
83
+ const result = await module.ready();
84
+
85
+ expect(result).toEqual({ success: true });
86
+ });
87
+ });
88
+
89
+ describe('Test hook: postInit', () => {
90
+ it('should return success by default', async () => {
91
+ const result = await module.postInit();
92
+
93
+ expect(result).toEqual({ success: true });
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('Test module extension', () => {
99
+ it('should allow extending the class', () => {
100
+ class CustomModule extends BasicRemoteModule {
101
+ constructor() {
102
+ super('custom-module', 'Custom Module', '1.0.0');
103
+ }
104
+ }
105
+
106
+ const module = new CustomModule();
107
+
108
+ expect(module).toBeInstanceOf(BasicRemoteModule);
109
+ expect(module.id).toBe('custom-module');
110
+ expect(module.name).toBe('Custom Module');
111
+ expect(module.version).toBe('1.0.0');
112
+ });
113
+
114
+ it('should allow overriding lifecycle methods', async () => {
115
+ class OverriddenModule extends BasicRemoteModule {
116
+ constructor() {
117
+ super('override-module', 'Override Module', '1.0.0');
118
+ }
119
+
120
+ async configure(config) {
121
+ if (!config.required) {
122
+ return { success: false, error: 'Missing required config' };
123
+ }
124
+ return { success: true };
125
+ }
126
+ }
127
+
128
+ const module = new OverriddenModule();
129
+
130
+ const failResult = await module.configure({});
131
+ expect(failResult.success).toBe(false);
132
+ expect(failResult.error).toBe('Missing required config');
133
+
134
+ const successResult = await module.configure({ required: true });
135
+ expect(successResult.success).toBe(true);
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,146 @@
1
+ import { loadRemote } from '@module-federation/enhanced/runtime';
2
+ import { loadAsyncComponent } from 'src/services/federationService';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { defineAsyncComponent } from 'vue';
5
+
6
+ vi.mock('@module-federation/enhanced/runtime');
7
+ vi.mock('vue', async () => {
8
+ const actual = await vi.importActual('vue');
9
+ return {
10
+ ...actual,
11
+ defineAsyncComponent: vi.fn((loader) => loader),
12
+ };
13
+ });
14
+
15
+ describe('Test service: federationService', () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ describe('Test function: loadAsyncComponent', () => {
21
+ it('should load a remote component successfully', async () => {
22
+ const testComponent = { default: 'RemoteTestComponent' };
23
+ const remoteModule = { './TestComponent': testComponent };
24
+ vi.mocked(loadRemote).mockResolvedValue(remoteModule['./TestComponent']);
25
+
26
+ const loader = loadAsyncComponent('test-plugin/TestComponent');
27
+ const result = await loader();
28
+
29
+ expect(loadRemote).toHaveBeenCalledTimes(1);
30
+ expect(loadRemote).toHaveBeenCalledWith('test-plugin/TestComponent');
31
+ expect(defineAsyncComponent).toHaveBeenCalled();
32
+ expect(result).toEqual('RemoteTestComponent');
33
+ });
34
+
35
+ it('should throw an error if remote component export a null default', async () => {
36
+ const testComponent = { default: null };
37
+ const remoteModule = { './TestComponent': testComponent };
38
+ vi.mocked(loadRemote).mockResolvedValue(remoteModule['./TestComponent']);
39
+
40
+ const loader = loadAsyncComponent('invalid-plugin/TestComponent');
41
+
42
+ await expect(loader()).rejects.toThrow(
43
+ 'Failed to load component from invalid-plugin'
44
+ );
45
+ expect(loadRemote).toHaveBeenCalledWith('invalid-plugin/TestComponent');
46
+ });
47
+
48
+ it('should throw an error if remote component export an undefined default', async () => {
49
+ const testComponent = { default: undefined };
50
+ const remoteModule = { './TestComponent': testComponent };
51
+ vi.mocked(loadRemote).mockResolvedValue(remoteModule['./TestComponent']);
52
+
53
+ const loader = loadAsyncComponent('invalid-plugin/TestComponent');
54
+
55
+ await expect(loader()).rejects.toThrow(
56
+ 'Failed to load component from invalid-plugin'
57
+ );
58
+ expect(loadRemote).toHaveBeenCalledWith('invalid-plugin/TestComponent');
59
+ });
60
+
61
+ it('should throw an error if remote component does not have a default export', async () => {
62
+ const testComponent = { toto: vi.fn() };
63
+ const remoteModule = { './TestComponent': testComponent };
64
+ vi.mocked(loadRemote).mockResolvedValue(remoteModule['./TestComponent']);
65
+
66
+ const loader = loadAsyncComponent('invalid-plugin/TestComponent');
67
+
68
+ await expect(loader()).rejects.toThrow(
69
+ 'Failed to load component from invalid-plugin'
70
+ );
71
+ expect(loadRemote).toHaveBeenCalledWith('invalid-plugin/TestComponent');
72
+ });
73
+
74
+ it('should throw an error if module is null', async () => {
75
+ vi.mocked(loadRemote).mockResolvedValue(null);
76
+
77
+ const loader = loadAsyncComponent('null-plugin/TestComponent');
78
+
79
+ await expect(loader()).rejects.toThrow(
80
+ 'Failed to load component from null-plugin'
81
+ );
82
+ });
83
+
84
+ it('should throw an error if module is undefined', async () => {
85
+ vi.mocked(loadRemote).mockResolvedValue(undefined);
86
+
87
+ const loader = loadAsyncComponent('undefined-plugin/TestComponent');
88
+
89
+ await expect(loader()).rejects.toThrow(
90
+ 'Failed to load component from undefined-plugin'
91
+ );
92
+ });
93
+
94
+ it('should throw an error if loadRemote rejects', async () => {
95
+ const error = new Error('Network error');
96
+
97
+ vi.mocked(loadRemote).mockRejectedValue(error);
98
+
99
+ const loader = loadAsyncComponent('failing-plugin/TestComponent');
100
+
101
+ await expect(loader()).rejects.toThrow('Network error');
102
+ expect(loadRemote).toHaveBeenCalledWith('failing-plugin/TestComponent');
103
+ });
104
+
105
+ it('should handle different plugin names', async () => {
106
+ const plugins = ['plugin-a', 'plugin-b', 'plugin-c'];
107
+ const testComponentA = { default: 'RemoteTestComponentA' };
108
+ const remoteModuleA = { './TestComponent': testComponentA };
109
+ const testComponentB = { default: 'RemoteTestComponentB' };
110
+ const remoteModuleB = { './TestComponent': testComponentB };
111
+ const testComponentC = { default: 'RemoteTestComponentC' };
112
+ const remoteModuleC = { './TestComponent': testComponentC };
113
+
114
+ vi.mocked(loadRemote)
115
+ .mockResolvedValueOnce(remoteModuleA['./TestComponent'])
116
+ .mockResolvedValueOnce(remoteModuleB['./TestComponent'])
117
+ .mockResolvedValueOnce(remoteModuleC['./TestComponent']);
118
+
119
+ for (const plugin of plugins) {
120
+ const loader = loadAsyncComponent(`${plugin}/TestComponent`);
121
+ const result = await loader();
122
+ const lastChar = plugin.charAt(plugin.length - 1).toUpperCase();
123
+
124
+ expect(loadRemote).toHaveBeenCalledWith(`${plugin}/TestComponent`);
125
+ expect(result).toEqual(`RemoteTestComponent${lastChar}`);
126
+ }
127
+
128
+ expect(loadRemote).toHaveBeenCalledTimes(3);
129
+ });
130
+
131
+ it('should handle module with additional exports', async () => {
132
+ const testComponent = {
133
+ default: 'RemoteTestComponent',
134
+ namedExport1: 'value1',
135
+ namedExport2: 'value2',
136
+ };
137
+ const remoteModule = { './TestComponent': testComponent };
138
+ vi.mocked(loadRemote).mockResolvedValue(remoteModule['./TestComponent']);
139
+
140
+ const loader = loadAsyncComponent('multi-export-plugin');
141
+ const result = await loader();
142
+
143
+ expect(result).toEqual('RemoteTestComponent');
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ // We need to reset the module between tests to clear the singleton
4
+ let getHttpClient, setHttpClient;
5
+
6
+ describe('Test service: httpClientService', () => {
7
+ beforeEach(async () => {
8
+ vi.resetModules();
9
+ const module = await import('src/services/httpClientService');
10
+ getHttpClient = module.getHttpClient;
11
+ setHttpClient = module.setHttpClient;
12
+ });
13
+
14
+ describe('Test function: setHttpClient', () => {
15
+ it('should set the HTTP client successfully', () => {
16
+ const mockClient = { get: vi.fn(), post: vi.fn() };
17
+
18
+ setHttpClient(mockClient);
19
+
20
+ expect(getHttpClient()).toBe(mockClient);
21
+ });
22
+
23
+ it('should warn and ignore re-initialization', () => {
24
+ const consoleWarnSpy = vi
25
+ .spyOn(globalThis.console, 'warn')
26
+ .mockImplementation(() => {});
27
+ const firstClient = { get: vi.fn() };
28
+ const secondClient = { post: vi.fn() };
29
+
30
+ setHttpClient(firstClient);
31
+ setHttpClient(secondClient);
32
+
33
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
34
+ '[LinID CoreLib] HTTP client has already been initialized. Re-initialization is ignored.'
35
+ );
36
+ expect(getHttpClient()).toBe(firstClient);
37
+
38
+ consoleWarnSpy.mockRestore();
39
+ });
40
+ });
41
+
42
+ describe('Test function: getHttpClient', () => {
43
+ it('should throw an error if client is not initialized', () => {
44
+ expect(() => getHttpClient()).toThrow(
45
+ '[LinID CoreLib] HTTP client is not initialized. Call setHttpClient() first.'
46
+ );
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,113 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ getEntitiesConfiguration,
4
+ getEntityConfiguration,
5
+ getRoutesConfiguration,
6
+ } from 'src/services/linIdConfigurationService';
7
+ import * as httpClientService from 'src/services/httpClientService';
8
+
9
+ vi.mock('src/services/httpClientService', () => ({
10
+ getHttpClient: vi.fn(),
11
+ }));
12
+
13
+ describe('Test service: linIdConfigurationService', () => {
14
+ let mockHttpClient;
15
+
16
+ beforeEach(() => {
17
+ mockHttpClient = {
18
+ get: vi.fn(),
19
+ };
20
+ vi.mocked(httpClientService.getHttpClient).mockReturnValue(mockHttpClient);
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ describe('Test function: getEntitiesConfiguration', () => {
25
+ it('should fetch all entities from /metadata/entities', async () => {
26
+ const mockEntities = [
27
+ { name: 'user', attributes: [] },
28
+ { name: 'group', attributes: [] },
29
+ ];
30
+ mockHttpClient.get.mockResolvedValue({ data: mockEntities });
31
+
32
+ const result = await getEntitiesConfiguration();
33
+
34
+ expect(httpClientService.getHttpClient).toHaveBeenCalled();
35
+ expect(mockHttpClient.get).toHaveBeenCalledWith('/metadata/entities');
36
+ expect(result).toEqual(mockEntities);
37
+ });
38
+
39
+ it('should propagate errors from HTTP client', async () => {
40
+ const error = new Error('Network error');
41
+ mockHttpClient.get.mockRejectedValue(error);
42
+
43
+ await expect(getEntitiesConfiguration()).rejects.toThrow('Network error');
44
+ });
45
+ });
46
+
47
+ describe('Test function: getEntityConfiguration', () => {
48
+ it('should fetch a specific entity from /metadata/entities/:entity', async () => {
49
+ const mockEntity = {
50
+ name: 'user',
51
+ attributes: [
52
+ {
53
+ name: 'email',
54
+ type: 'string',
55
+ required: true,
56
+ hasValidations: false,
57
+ input: 'text',
58
+ inputSettings: {},
59
+ },
60
+ ],
61
+ };
62
+ mockHttpClient.get.mockResolvedValue({ data: mockEntity });
63
+
64
+ const result = await getEntityConfiguration('user');
65
+
66
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
67
+ '/metadata/entities/user'
68
+ );
69
+ expect(result).toEqual(mockEntity);
70
+ });
71
+
72
+ it('should handle special characters in entity ID', async () => {
73
+ const mockEntity = { name: 'my-entity', attributes: [] };
74
+ mockHttpClient.get.mockResolvedValue({ data: mockEntity });
75
+
76
+ const result = await getEntityConfiguration('my-entity');
77
+
78
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
79
+ '/metadata/entities/my-entity'
80
+ );
81
+ expect(result).toEqual(mockEntity);
82
+ });
83
+
84
+ it('should propagate errors from HTTP client', async () => {
85
+ mockHttpClient.get.mockRejectedValue(new Error('Not found'));
86
+
87
+ await expect(getEntityConfiguration('unknown')).rejects.toThrow(
88
+ 'Not found'
89
+ );
90
+ });
91
+ });
92
+
93
+ describe('Test function: getRoutesConfiguration', () => {
94
+ it('should fetch all routes from /metadata/routes', async () => {
95
+ const mockRoutes = [
96
+ { method: 'GET', path: '/users', entity: 'user', variables: [] },
97
+ { method: 'POST', path: '/users', entity: 'user', variables: [] },
98
+ ];
99
+ mockHttpClient.get.mockResolvedValue({ data: mockRoutes });
100
+
101
+ const result = await getRoutesConfiguration();
102
+
103
+ expect(mockHttpClient.get).toHaveBeenCalledWith('/metadata/routes');
104
+ expect(result).toEqual(mockRoutes);
105
+ });
106
+
107
+ it('should propagate errors from HTTP client', async () => {
108
+ mockHttpClient.get.mockRejectedValue(new Error('Server error'));
109
+
110
+ await expect(getRoutesConfiguration()).rejects.toThrow('Server error');
111
+ });
112
+ });
113
+ });