cdd-cli 3.2.2 → 4.1.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/README.es.md +222 -0
  2. package/README.md +108 -26
  3. package/dist/App.js +7 -1
  4. package/dist/components/ContainerCreationPrompt.js +26 -10
  5. package/dist/components/ControlsHUD.js +106 -0
  6. package/dist/components/SuggestionPanel.js +16 -7
  7. package/dist/components/UsageMenu.js +1 -1
  8. package/dist/helpers/appState.js +26 -0
  9. package/dist/helpers/constants.js +60 -21
  10. package/dist/helpers/dockerHubService.js +111 -0
  11. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  12. package/dist/helpers/logger.js +1 -1
  13. package/dist/hooks/creation/useContainerActions.js +22 -5
  14. package/dist/hooks/creation/useContainerCreation.js +414 -107
  15. package/dist/hooks/useContainerCommandRouter.js +13 -3
  16. package/dist/hooks/useControls.js +28 -7
  17. package/dist/hooks/useShellMode.js +101 -0
  18. package/dist/index.js +10 -1
  19. package/package.json +4 -1
  20. package/.atl/skill-registry.md +0 -30
  21. package/.editorconfig +0 -12
  22. package/.eslintrc.json +0 -30
  23. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  24. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  25. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  26. package/.github/workflows/ci.yml +0 -32
  27. package/.github/workflows/docs.yml +0 -42
  28. package/.prettierignore +0 -4
  29. package/.prettierrc +0 -8
  30. package/CHANGELOG.md +0 -165
  31. package/CODE_OF_CONDUCT.md +0 -12
  32. package/CONTRIBUTING.md +0 -35
  33. package/SECURITY.md +0 -5
  34. package/__mocks__/ink.cjs +0 -17
  35. package/_config.yml +0 -13
  36. package/babel.config.cjs +0 -23
  37. package/coverage/clover.xml +0 -694
  38. package/coverage/coverage-final.json +0 -21
  39. package/coverage/lcov-report/base.css +0 -224
  40. package/coverage/lcov-report/block-navigation.js +0 -87
  41. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -376
  42. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  43. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -244
  44. package/coverage/lcov-report/components/index.html +0 -146
  45. package/coverage/lcov-report/favicon.png +0 -0
  46. package/coverage/lcov-report/helpers/constants.js.html +0 -298
  47. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  48. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  49. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  50. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  51. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  52. package/coverage/lcov-report/helpers/index.html +0 -191
  53. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  54. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  55. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  56. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  57. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  58. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -805
  59. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  60. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  61. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  62. package/coverage/lcov-report/hooks/index.html +0 -161
  63. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  64. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  65. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  66. package/coverage/lcov-report/hooks/useControls.js.html +0 -826
  67. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  68. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  69. package/coverage/lcov-report/index.html +0 -206
  70. package/coverage/lcov-report/prettify.css +0 -1
  71. package/coverage/lcov-report/prettify.js +0 -2
  72. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  73. package/coverage/lcov-report/sorter.js +0 -210
  74. package/coverage/lcov.info +0 -1402
  75. package/fix-imports.cjs +0 -54
  76. package/jest.config.cjs +0 -14
  77. package/src/App.jsx +0 -96
  78. package/src/components/ContainerCreationPrompt.jsx +0 -97
  79. package/src/components/ContainerList.jsx +0 -32
  80. package/src/components/ContainerRow.jsx +0 -84
  81. package/src/components/ContainerSection.jsx +0 -24
  82. package/src/components/Footer.jsx +0 -15
  83. package/src/components/Header.jsx +0 -41
  84. package/src/components/LogViewer.jsx +0 -36
  85. package/src/components/MessageFeedback.jsx +0 -25
  86. package/src/components/PromptField.jsx +0 -48
  87. package/src/components/StatsBar.jsx +0 -70
  88. package/src/components/SuggestionPanel.jsx +0 -53
  89. package/src/components/UsageMenu.jsx +0 -52
  90. package/src/helpers/appInfo.js +0 -23
  91. package/src/helpers/constants.js +0 -71
  92. package/src/helpers/containerOptionsBuilder.js +0 -53
  93. package/src/helpers/dockerService/dockerService.js +0 -28
  94. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  95. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  96. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  97. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  98. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  99. package/src/helpers/exitWithMessage.js +0 -32
  100. package/src/helpers/imageNameUtils.js +0 -32
  101. package/src/helpers/logger.js +0 -113
  102. package/src/helpers/safeCall.js +0 -18
  103. package/src/helpers/validationHelpers.js +0 -87
  104. package/src/hooks/creation/useContainerActions.js +0 -76
  105. package/src/hooks/creation/useContainerCreation.js +0 -240
  106. package/src/hooks/creation/useLogsViewer.js +0 -51
  107. package/src/hooks/debug/useDebugLogs.js +0 -34
  108. package/src/hooks/navigation/useContainerSelection.js +0 -28
  109. package/src/hooks/useContainerCommandRouter.js +0 -123
  110. package/src/hooks/useContainerStats.js +0 -44
  111. package/src/hooks/useContainers.js +0 -26
  112. package/src/hooks/useControls.js +0 -247
  113. package/src/hooks/useEraseConfirmation.js +0 -39
  114. package/src/hooks/useExitHandler.js +0 -69
  115. package/src/index.js +0 -16
  116. package/test/ContainerCreationPrompt.dom.test.js +0 -68
  117. package/test/SuggestionPanel.dom.test.js +0 -53
  118. package/test/constants.test.js +0 -34
  119. package/test/containerActions.test.js +0 -229
  120. package/test/containerList.test.js +0 -79
  121. package/test/containerOptionsBuilder.test.js +0 -36
  122. package/test/jest.setup.js +0 -7
  123. package/test/safeCall.test.js +0 -25
  124. package/test/useContainerCreation.dom.test.js +0 -264
  125. package/test/useControls.dom.test.js +0 -313
  126. package/test/validationHelpers.test.js +0 -111
@@ -1,229 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- describe('containerActions service functions (mocked ESM imports)', () => {
4
- afterEach(() => jest.resetModules());
5
-
6
- test('createContainer returns { id, ports } when image exists', async () => {
7
- const imageUtilsMock = {
8
- imageExists: jest.fn().mockResolvedValue(true),
9
- pullImage: jest.fn()
10
- };
11
-
12
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '3306/tcp': {} } } });
13
- const dockerMock = {
14
- createContainer: jest.fn().mockResolvedValue({ id: 'cid-123' }),
15
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
16
- listContainers: jest.fn().mockResolvedValue([])
17
- };
18
-
19
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
20
- ...imageUtilsMock
21
- }));
22
-
23
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
24
- docker: dockerMock
25
- }));
26
-
27
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
28
-
29
- const result = await mod.createContainer('mysql:8', { Name: 'test' });
30
- expect(result).toHaveProperty('id', 'cid-123');
31
- expect(result).toHaveProperty('ports');
32
- expect(Array.isArray(result.ports)).toBe(true);
33
- });
34
-
35
- test('createContainer ports contain containerPort, hostPort, protocol, source fields', async () => {
36
- const imageUtilsMock = {
37
- imageExists: jest.fn().mockResolvedValue(true),
38
- pullImage: jest.fn()
39
- };
40
-
41
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
42
- const dockerMock = {
43
- createContainer: jest.fn().mockResolvedValue({ id: 'cid-ports' }),
44
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
45
- listContainers: jest.fn().mockResolvedValue([])
46
- };
47
-
48
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
49
- ...imageUtilsMock
50
- }));
51
-
52
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
53
- docker: dockerMock
54
- }));
55
-
56
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
57
- const result = await mod.createContainer('nginx:latest', {});
58
- expect(result.ports.length).toBeGreaterThan(0);
59
- const port = result.ports[0];
60
- expect(port).toHaveProperty('containerPort');
61
- expect(port).toHaveProperty('hostPort');
62
- expect(port).toHaveProperty('protocol');
63
- expect(port).toHaveProperty('source');
64
- });
65
-
66
- test('createContainer uses IMAGE_PROFILES defaultPort as fallback when ExposedPorts empty', async () => {
67
- const imageUtilsMock = {
68
- imageExists: jest.fn().mockResolvedValue(true),
69
- pullImage: jest.fn()
70
- };
71
-
72
- // inspect returns empty ExposedPorts
73
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
74
- const dockerMock = {
75
- createContainer: jest.fn().mockResolvedValue({ id: 'cid-fallback' }),
76
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
77
- listContainers: jest.fn().mockResolvedValue([])
78
- };
79
-
80
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
81
- ...imageUtilsMock
82
- }));
83
-
84
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
85
- docker: dockerMock
86
- }));
87
-
88
- const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
89
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
90
- const result = await mod.createContainer('mysql:8', {}, IMAGE_PROFILES);
91
-
92
- expect(result.id).toBe('cid-fallback');
93
- // Should have auto port from defaultPort fallback
94
- const mysqlDefault = IMAGE_PROFILES.mysql.defaultPort;
95
- const portEntry = result.ports.find(p => p.containerPort === mysqlDefault);
96
- expect(portEntry).toBeDefined();
97
- expect(portEntry.source).toBe('auto');
98
- });
99
-
100
- test('createContainer returns { id, ports: [] } when no ports and image not in profiles', async () => {
101
- const imageUtilsMock = {
102
- imageExists: jest.fn().mockResolvedValue(true),
103
- pullImage: jest.fn()
104
- };
105
-
106
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
107
- const dockerMock = {
108
- createContainer: jest.fn().mockResolvedValue({ id: 'cid-noport' }),
109
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
110
- listContainers: jest.fn().mockResolvedValue([])
111
- };
112
-
113
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
114
- ...imageUtilsMock
115
- }));
116
-
117
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
118
- docker: dockerMock
119
- }));
120
-
121
- const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
122
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
123
- const result = await mod.createContainer('unknownimage:latest', {}, IMAGE_PROFILES);
124
-
125
- expect(result.id).toBe('cid-noport');
126
- expect(result.ports).toEqual([]);
127
- });
128
-
129
- test('createContainer pulls image when not present and returns { id, ports }', async () => {
130
- const imageUtilsMock = {
131
- imageExists: jest.fn().mockResolvedValue(false),
132
- pullImage: jest.fn().mockResolvedValue(true)
133
- };
134
-
135
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
136
- const dockerMock = {
137
- createContainer: jest.fn().mockResolvedValue({ Id: 'CID-456' }),
138
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
139
- listContainers: jest.fn().mockResolvedValue([])
140
- };
141
-
142
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
143
- ...imageUtilsMock
144
- }));
145
-
146
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
147
- docker: dockerMock
148
- }));
149
-
150
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
151
- const result = await mod.createContainer('busybox:1.0');
152
- expect(result.id).toBe('CID-456');
153
- expect(result).toHaveProperty('ports');
154
- const imageUtils = await import('../src/helpers/dockerService/serviceComponents/imageUtils.js');
155
- expect(imageUtils.pullImage).toHaveBeenCalledWith('busybox:1.0');
156
- });
157
-
158
- test('createContainer auto maps exposed ports when ports not provided', async () => {
159
- const imageUtilsMock = {
160
- imageExists: jest.fn().mockResolvedValue(true),
161
- pullImage: jest.fn()
162
- };
163
-
164
- const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
165
- const createContainerMock = jest.fn().mockResolvedValue({ id: 'cid-auto' });
166
- const dockerMock = {
167
- createContainer: createContainerMock,
168
- getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
169
- listContainers: jest.fn().mockResolvedValue([
170
- { Ports: [{ PublicPort: 80, PrivatePort: 80 }] }
171
- ])
172
- };
173
-
174
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
175
- ...imageUtilsMock
176
- }));
177
-
178
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
179
- docker: dockerMock
180
- }));
181
-
182
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
183
- await mod.createContainer('nginx:alpine');
184
-
185
- const [[createArgs]] = createContainerMock.mock.calls;
186
- expect(createArgs.ExposedPorts).toEqual({ '80/tcp': {} });
187
- expect(createArgs.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '81' }]);
188
- expect(dockerMock.listContainers).toHaveBeenCalled();
189
- });
190
-
191
- test('removeContainer calls docker.remove with force', async () => {
192
- const removeMock = jest.fn().mockResolvedValue(undefined);
193
- const dockerMock = {
194
- getContainer: jest.fn().mockReturnValue({ remove: removeMock })
195
- };
196
-
197
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
198
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
199
-
200
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
201
- await mod.removeContainer('abc');
202
- const ds = await import('../src/helpers/dockerService/dockerService.js');
203
- expect(ds.docker.getContainer).toHaveBeenCalledWith('abc');
204
- expect(removeMock).toHaveBeenCalledWith({ force: true });
205
- });
206
-
207
- test('start/stop/restart call respective container methods', async () => {
208
- const startMock = jest.fn().mockResolvedValue(undefined);
209
- const stopMock = jest.fn().mockResolvedValue(undefined);
210
- const restartMock = jest.fn().mockResolvedValue(undefined);
211
-
212
- const dockerMock = {
213
- getContainer: jest.fn().mockReturnValue({ start: startMock, stop: stopMock, restart: restartMock })
214
- };
215
-
216
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
217
- await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
218
-
219
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
220
-
221
- await mod.startContainer('c1');
222
- await mod.stopContainer('c1');
223
- await mod.restartContainer('c1');
224
-
225
- expect(startMock).toHaveBeenCalled();
226
- expect(stopMock).toHaveBeenCalled();
227
- expect(restartMock).toHaveBeenCalled();
228
- });
229
- });
@@ -1,79 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- describe('containerList.getContainers', () => {
4
- afterEach(() => jest.resetModules());
5
-
6
- const baseContainer = {
7
- Id: 'abc123',
8
- Image: 'nginx:latest',
9
- State: 'running',
10
- Status: 'Up 5 minutes',
11
- Ports: []
12
- };
13
-
14
- test('falls back to container.Name when Names array is missing', async () => {
15
- const dockerMock = {
16
- listContainers: jest.fn().mockResolvedValue([
17
- {
18
- ...baseContainer,
19
- Names: undefined,
20
- Name: '/custom-container'
21
- }
22
- ])
23
- };
24
-
25
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
26
- docker: dockerMock
27
- }));
28
-
29
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
30
- const containers = await mod.getContainers();
31
-
32
- expect(containers).toHaveLength(1);
33
- expect(containers[0].name).toBe('custom-container');
34
- });
35
-
36
- test('returns Unknown when no usable name data is present', async () => {
37
- const dockerMock = {
38
- listContainers: jest.fn().mockResolvedValue([
39
- {
40
- ...baseContainer,
41
- Names: [],
42
- Name: '',
43
- Id: 'def456'
44
- }
45
- ])
46
- };
47
-
48
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
49
- docker: dockerMock
50
- }));
51
-
52
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
53
- const containers = await mod.getContainers();
54
-
55
- expect(containers).toHaveLength(1);
56
- expect(containers[0].name).toBe('Unknown');
57
- });
58
-
59
- test('normalizes first entry in Names array', async () => {
60
- const dockerMock = {
61
- listContainers: jest.fn().mockResolvedValue([
62
- {
63
- ...baseContainer,
64
- Names: ['///leading/slash', '/ignored-second']
65
- }
66
- ])
67
- };
68
-
69
- await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
70
- docker: dockerMock
71
- }));
72
-
73
- const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
74
- const containers = await mod.getContainers();
75
-
76
- expect(containers).toHaveLength(1);
77
- expect(containers[0].name).toBe('leading/slash');
78
- });
79
- });
@@ -1,36 +0,0 @@
1
- let buildContainerOptions;
2
-
3
- beforeAll(async () => {
4
- const mod = await import('../src/helpers/containerOptionsBuilder.js');
5
- buildContainerOptions = mod.buildContainerOptions;
6
- });
7
-
8
- describe('buildContainerOptions', () => {
9
- test('basic image only sets Tty', () => {
10
- const opts = buildContainerOptions({ imageName: 'nginx' });
11
- expect(opts.Tty).toBe(true);
12
- expect(opts.Env).toBeUndefined();
13
- });
14
-
15
- test('env var with value containing = is preserved intact', () => {
16
- const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b' });
17
- expect(opts.Env).toEqual(['TOKEN=a=b']);
18
- });
19
-
20
- test('multiple env vars with = in values are all preserved', () => {
21
- const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b,DSN=user:pass@host/db?opt=1' });
22
- expect(opts.Env).toContain('TOKEN=a=b');
23
- expect(opts.Env).toContain('DSN=user:pass@host/db?opt=1');
24
- });
25
-
26
- test('port input builds ExposedPorts and PortBindings', () => {
27
- const opts = buildContainerOptions({ imageName: 'nginx', portInput: '8080:80' });
28
- expect(opts.ExposedPorts).toEqual({ '80/tcp': {} });
29
- expect(opts.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '8080' }]);
30
- });
31
-
32
- test('container name is included when provided', () => {
33
- const opts = buildContainerOptions({ imageName: 'nginx', containerName: 'my-nginx' });
34
- expect(opts.name).toBe('my-nginx');
35
- });
36
- });
@@ -1,7 +0,0 @@
1
- // Minimal test setup for React/DOM environment
2
- // Could add more global matchers here
3
- import '@testing-library/jest-dom';
4
-
5
- // Silence act() warnings in environments where receiver isn't concurrent
6
- // (react-test-renderer/act warnings are printed, but tests can proceed)
7
- // If needed, configure global helpers here.
@@ -1,25 +0,0 @@
1
- import { safeCall } from '../src/helpers/safeCall.js';
2
-
3
- describe('safeCall', () => {
4
- test('calls function and returns value', () => {
5
- function fn(a, b) {
6
- fn.called = true;
7
- fn.args = [a, b];
8
- return a + b;
9
- }
10
- const res = safeCall(fn, 2, 3);
11
- expect(fn.called).toBe(true);
12
- expect(fn.args).toEqual([2, 3]);
13
- expect(res).toBe(5);
14
- });
15
-
16
- test('returns undefined when fn is not a function', () => {
17
- const res = safeCall(undefined, 1, 2);
18
- expect(res).toBeUndefined();
19
- });
20
-
21
- test('catches errors thrown by function and does not rethrow', () => {
22
- const bad = () => { throw new Error('boom'); };
23
- expect(() => safeCall(bad)).not.toThrow();
24
- });
25
- });
@@ -1,264 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
- import React, { useEffect } from 'react';
5
- import { render, act } from '@testing-library/react';
6
- import { useContainerCreation } from '../src/hooks/creation/useContainerCreation.js';
7
- import { IMAGE_PROFILES } from '../src/helpers/constants.js';
8
-
9
- function HookTester({ onCreate, onCancel, dbImages, imageProfiles, expose }) {
10
- const hook = useContainerCreation({ onCreate, onCancel, dbImages, imageProfiles });
11
- // keep exposing the latest hook values on every render
12
- useEffect(() => {
13
- if (expose) expose.current = hook;
14
- });
15
- return null;
16
- }
17
-
18
- describe('useContainerCreation (DOM render)', () => {
19
- test('image empty shows error and does not advance', () => {
20
- const expose = { current: null };
21
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} expose={expose} />);
22
-
23
- expect(expose.current.step).toBe(0);
24
-
25
- act(() => {
26
- expose.current.nextStep();
27
- });
28
-
29
- expect(expose.current.message).toBe('Image name cannot be empty.');
30
- expect(expose.current.step).toBe(0);
31
- });
32
-
33
- test('ports validation: invalid ports shows error', () => {
34
- const expose = { current: null };
35
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} expose={expose} />);
36
-
37
- act(() => {
38
- expose.current.setImageName('nginx');
39
- });
40
-
41
- act(() => {
42
- expose.current.nextStep(); // to name
43
- });
44
-
45
- act(() => {
46
- expose.current.nextStep(); // to ports
47
- });
48
-
49
- expect(expose.current.step).toBe(2);
50
-
51
- act(() => {
52
- expose.current.setPortInput('notaport');
53
- });
54
-
55
- act(() => {
56
- expose.current.nextStep();
57
- });
58
-
59
- expect(expose.current.message).toBe('Port format must be host:container and both must be numbers (e.g. 8080:80)');
60
- expect(expose.current.step).toBe(2);
61
- });
62
-
63
- test('complete flow calls onCreate with data', () => {
64
- const created = [];
65
- const onCreate = (data) => created.push(data);
66
- const expose = { current: null };
67
-
68
- render(<HookTester onCreate={onCreate} onCancel={() => {}} dbImages={[]} expose={expose} />);
69
-
70
- act(() => {
71
- expose.current.setImageName('redis');
72
- });
73
-
74
- act(() => {
75
- expose.current.nextStep(); // to name
76
- });
77
-
78
- act(() => {
79
- expose.current.nextStep(); // to ports
80
- });
81
-
82
- act(() => {
83
- expose.current.setPortInput('');
84
- });
85
-
86
- act(() => {
87
- expose.current.nextStep(); // to env
88
- });
89
-
90
- act(() => {
91
- expose.current.setEnvInput('FOO=bar');
92
- });
93
-
94
- act(() => {
95
- expose.current.nextStep(); // final create
96
- });
97
-
98
- expect(created.length).toBe(1);
99
- expect(created[0]).toEqual({ imageName: 'redis', containerName: '', portInput: '', envInput: 'FOO=bar' });
100
- });
101
-
102
- test('step 3 with mysql image: missing MYSQL_ROOT_PASSWORD blocks creation', () => {
103
- const created = [];
104
- const expose = { current: null };
105
-
106
- render(<HookTester onCreate={(d) => created.push(d)} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
107
-
108
- // advance to step 3 (env) with mysql image
109
- act(() => { expose.current.setImageName('mysql:8'); });
110
- act(() => { expose.current.nextStep(); }); // step 0 → 1
111
- act(() => { expose.current.nextStep(); }); // step 1 → 2
112
- act(() => { expose.current.nextStep(); }); // step 2 → 3
113
-
114
- expect(expose.current.step).toBe(3);
115
-
116
- // try to advance without required env var
117
- act(() => { expose.current.nextStep(); });
118
-
119
- expect(expose.current.step).toBe(3); // still blocked
120
- expect(expose.current.message).toMatch(/MYSQL_ROOT_PASSWORD/i);
121
- expect(created.length).toBe(0);
122
- });
123
-
124
- test('step 3 with mysql image: valid env advances and calls onCreate', () => {
125
- const created = [];
126
- const expose = { current: null };
127
-
128
- render(<HookTester onCreate={(d) => created.push(d)} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
129
-
130
- act(() => { expose.current.setImageName('mysql:8'); });
131
- act(() => { expose.current.nextStep(); });
132
- act(() => { expose.current.nextStep(); });
133
- act(() => { expose.current.nextStep(); }); // at step 3
134
-
135
- act(() => { expose.current.setEnvInput('MYSQL_ROOT_PASSWORD=secret'); });
136
- act(() => { expose.current.nextStep(); }); // should call onCreate
137
-
138
- expect(created.length).toBe(1);
139
- expect(created[0].imageName).toBe('mysql:8');
140
- });
141
- });
142
-
143
- describe('useContainerCreation — suggestions & updateImageInput()', () => {
144
- test('hook exposes suggestions, selectedSuggestionIndex, visibleOffset', () => {
145
- const expose = { current: null };
146
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
147
- expect(Array.isArray(expose.current.suggestions)).toBe(true);
148
- expect(typeof expose.current.selectedSuggestionIndex).toBe('number');
149
- expect(typeof expose.current.visibleOffset).toBe('number');
150
- });
151
-
152
- test('updateImageInput() with "ng" filters IMAGE_PROFILES keys containing "ng"', () => {
153
- const expose = { current: null };
154
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
155
-
156
- act(() => { expose.current.updateImageInput('ng'); });
157
-
158
- expect(expose.current.imageName).toBe('ng');
159
- expect(expose.current.suggestions).toContain('nginx');
160
- });
161
-
162
- test('updateImageInput() with empty string clears suggestions', () => {
163
- const expose = { current: null };
164
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
165
-
166
- act(() => { expose.current.updateImageInput('ng'); });
167
- act(() => { expose.current.updateImageInput(''); });
168
-
169
- expect(expose.current.suggestions).toHaveLength(0);
170
- });
171
-
172
- test('updateImageInput() resets selectedSuggestionIndex to -1', () => {
173
- const expose = { current: null };
174
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
175
-
176
- act(() => { expose.current.updateImageInput('ng'); });
177
-
178
- expect(expose.current.selectedSuggestionIndex).toBe(-1);
179
- });
180
-
181
- test('step-3 message includes suggestedEnv hints when profile has them', () => {
182
- const expose = { current: null };
183
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
184
-
185
- act(() => { expose.current.setImageName('postgres'); });
186
- act(() => { expose.current.nextStep(); }); // → step 1
187
- act(() => { expose.current.nextStep(); }); // → step 2
188
- act(() => { expose.current.nextStep(); }); // → step 3
189
-
190
- expect(expose.current.message).toMatch(/POSTGRES_PASSWORD/i);
191
- expect(expose.current.message).toMatch(/POSTGRES_USER/i);
192
- });
193
- });
194
-
195
- describe('useContainerCreation — moveSuggestionSelection() & applyFocusedSuggestion()', () => {
196
- test('moveSuggestionSelection(1) increases selectedSuggestionIndex', () => {
197
- const expose = { current: null };
198
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
199
-
200
- act(() => { expose.current.updateImageInput('m'); });
201
- // There should be several suggestions with "m"
202
- act(() => { expose.current.moveSuggestionSelection(1); });
203
-
204
- expect(expose.current.selectedSuggestionIndex).toBe(0);
205
- });
206
-
207
- test('moveSuggestionSelection() clamps at last item', () => {
208
- const expose = { current: null };
209
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
210
-
211
- act(() => { expose.current.updateImageInput('m'); });
212
- const count = expose.current.suggestions.length;
213
-
214
- // Move past end
215
- for (let i = 0; i <= count + 2; i++) {
216
- act(() => { expose.current.moveSuggestionSelection(1); });
217
- }
218
-
219
- expect(expose.current.selectedSuggestionIndex).toBe(count - 1);
220
- });
221
-
222
- test('moveSuggestionSelection(-1) clamps at -1 (no selection)', () => {
223
- const expose = { current: null };
224
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
225
-
226
- act(() => { expose.current.updateImageInput('m'); });
227
- act(() => { expose.current.moveSuggestionSelection(-1); });
228
-
229
- expect(expose.current.selectedSuggestionIndex).toBe(-1);
230
- });
231
-
232
- test('applyFocusedSuggestion() fills imageName with focused suggestion', () => {
233
- const expose = { current: null };
234
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
235
-
236
- act(() => { expose.current.updateImageInput('ng'); });
237
- act(() => { expose.current.moveSuggestionSelection(1); }); // select first
238
- act(() => { expose.current.applyFocusedSuggestion(); });
239
-
240
- expect(expose.current.imageName).toBe('nginx');
241
- });
242
-
243
- test('applyFocusedSuggestion() does NOT advance step', () => {
244
- const expose = { current: null };
245
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
246
-
247
- act(() => { expose.current.updateImageInput('ng'); });
248
- act(() => { expose.current.moveSuggestionSelection(1); });
249
- act(() => { expose.current.applyFocusedSuggestion(); });
250
-
251
- expect(expose.current.step).toBe(0);
252
- });
253
-
254
- test('applyFocusedSuggestion() clears suggestions after applying', () => {
255
- const expose = { current: null };
256
- render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
257
-
258
- act(() => { expose.current.updateImageInput('ng'); });
259
- act(() => { expose.current.moveSuggestionSelection(1); });
260
- act(() => { expose.current.applyFocusedSuggestion(); });
261
-
262
- expect(expose.current.suggestions).toHaveLength(0);
263
- });
264
- });