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.
- package/README.es.md +222 -0
- package/README.md +108 -26
- package/dist/App.js +7 -1
- package/dist/components/ContainerCreationPrompt.js +26 -10
- package/dist/components/ControlsHUD.js +106 -0
- package/dist/components/SuggestionPanel.js +16 -7
- package/dist/components/UsageMenu.js +1 -1
- package/dist/helpers/appState.js +26 -0
- package/dist/helpers/constants.js +60 -21
- package/dist/helpers/dockerHubService.js +111 -0
- package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
- package/dist/helpers/logger.js +1 -1
- package/dist/hooks/creation/useContainerActions.js +22 -5
- package/dist/hooks/creation/useContainerCreation.js +414 -107
- package/dist/hooks/useContainerCommandRouter.js +13 -3
- package/dist/hooks/useControls.js +28 -7
- package/dist/hooks/useShellMode.js +101 -0
- package/dist/index.js +10 -1
- package/package.json +4 -1
- package/.atl/skill-registry.md +0 -30
- package/.editorconfig +0 -12
- package/.eslintrc.json +0 -30
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/docs.yml +0 -42
- package/.prettierignore +0 -4
- package/.prettierrc +0 -8
- package/CHANGELOG.md +0 -165
- package/CODE_OF_CONDUCT.md +0 -12
- package/CONTRIBUTING.md +0 -35
- package/SECURITY.md +0 -5
- package/__mocks__/ink.cjs +0 -17
- package/_config.yml +0 -13
- package/babel.config.cjs +0 -23
- package/coverage/clover.xml +0 -694
- package/coverage/coverage-final.json +0 -21
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -376
- package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -244
- package/coverage/lcov-report/components/index.html +0 -146
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +0 -298
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
- package/coverage/lcov-report/helpers/index.html +0 -191
- package/coverage/lcov-report/helpers/logger.js.html +0 -424
- package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
- package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
- package/coverage/lcov-report/hooks/creation/index.html +0 -146
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -805
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
- package/coverage/lcov-report/hooks/debug/index.html +0 -116
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
- package/coverage/lcov-report/hooks/index.html +0 -161
- package/coverage/lcov-report/hooks/navigation/index.html +0 -116
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
- package/coverage/lcov-report/hooks/useControls.js.html +0 -826
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
- package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
- package/coverage/lcov-report/index.html +0 -206
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -1402
- package/fix-imports.cjs +0 -54
- package/jest.config.cjs +0 -14
- package/src/App.jsx +0 -96
- package/src/components/ContainerCreationPrompt.jsx +0 -97
- package/src/components/ContainerList.jsx +0 -32
- package/src/components/ContainerRow.jsx +0 -84
- package/src/components/ContainerSection.jsx +0 -24
- package/src/components/Footer.jsx +0 -15
- package/src/components/Header.jsx +0 -41
- package/src/components/LogViewer.jsx +0 -36
- package/src/components/MessageFeedback.jsx +0 -25
- package/src/components/PromptField.jsx +0 -48
- package/src/components/StatsBar.jsx +0 -70
- package/src/components/SuggestionPanel.jsx +0 -53
- package/src/components/UsageMenu.jsx +0 -52
- package/src/helpers/appInfo.js +0 -23
- package/src/helpers/constants.js +0 -71
- package/src/helpers/containerOptionsBuilder.js +0 -53
- package/src/helpers/dockerService/dockerService.js +0 -28
- package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
- package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
- package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
- package/src/helpers/exitWithMessage.js +0 -32
- package/src/helpers/imageNameUtils.js +0 -32
- package/src/helpers/logger.js +0 -113
- package/src/helpers/safeCall.js +0 -18
- package/src/helpers/validationHelpers.js +0 -87
- package/src/hooks/creation/useContainerActions.js +0 -76
- package/src/hooks/creation/useContainerCreation.js +0 -240
- package/src/hooks/creation/useLogsViewer.js +0 -51
- package/src/hooks/debug/useDebugLogs.js +0 -34
- package/src/hooks/navigation/useContainerSelection.js +0 -28
- package/src/hooks/useContainerCommandRouter.js +0 -123
- package/src/hooks/useContainerStats.js +0 -44
- package/src/hooks/useContainers.js +0 -26
- package/src/hooks/useControls.js +0 -247
- package/src/hooks/useEraseConfirmation.js +0 -39
- package/src/hooks/useExitHandler.js +0 -69
- package/src/index.js +0 -16
- package/test/ContainerCreationPrompt.dom.test.js +0 -68
- package/test/SuggestionPanel.dom.test.js +0 -53
- package/test/constants.test.js +0 -34
- package/test/containerActions.test.js +0 -229
- package/test/containerList.test.js +0 -79
- package/test/containerOptionsBuilder.test.js +0 -36
- package/test/jest.setup.js +0 -7
- package/test/safeCall.test.js +0 -25
- package/test/useContainerCreation.dom.test.js +0 -264
- package/test/useControls.dom.test.js +0 -313
- 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
|
-
});
|
package/test/jest.setup.js
DELETED
|
@@ -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.
|
package/test/safeCall.test.js
DELETED
|
@@ -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
|
-
});
|