cdd-cli 4.0.0 → 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 (130) hide show
  1. package/README.es.md +29 -5
  2. package/README.md +29 -5
  3. package/dist/components/UsageMenu.js +1 -1
  4. package/dist/helpers/appState.js +26 -0
  5. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  6. package/dist/hooks/creation/useContainerCreation.js +25 -6
  7. package/dist/hooks/useContainerCommandRouter.js +12 -2
  8. package/dist/hooks/useControls.js +5 -0
  9. package/dist/hooks/useShellMode.js +101 -0
  10. package/dist/index.js +3 -1
  11. package/package.json +4 -1
  12. package/.atl/skill-registry.md +0 -30
  13. package/.editorconfig +0 -12
  14. package/.eslintrc.json +0 -30
  15. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  17. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  18. package/.github/workflows/ci.yml +0 -32
  19. package/.github/workflows/docs.yml +0 -42
  20. package/.prettierignore +0 -4
  21. package/.prettierrc +0 -8
  22. package/.~lock.ROADMAP.md# +0 -1
  23. package/CHANGELOG.md +0 -216
  24. package/CODE_OF_CONDUCT.md +0 -12
  25. package/CONTRIBUTING.md +0 -35
  26. package/ROADMAP.md +0 -647
  27. package/SECURITY.md +0 -5
  28. package/__mocks__/ink.cjs +0 -17
  29. package/_config.yml +0 -13
  30. package/babel.config.cjs +0 -23
  31. package/coverage/clover.xml +0 -575
  32. package/coverage/coverage-final.json +0 -17
  33. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
  34. package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
  35. package/coverage/lcov-report/base.css +0 -224
  36. package/coverage/lcov-report/block-navigation.js +0 -87
  37. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -394
  38. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  39. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
  40. package/coverage/lcov-report/components/index.html +0 -146
  41. package/coverage/lcov-report/favicon.png +0 -0
  42. package/coverage/lcov-report/helpers/constants.js.html +0 -352
  43. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  44. package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
  45. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  46. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  47. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  48. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  49. package/coverage/lcov-report/helpers/index.html +0 -206
  50. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  51. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  52. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  53. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  54. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  55. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -982
  56. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  57. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  58. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  59. package/coverage/lcov-report/hooks/index.html +0 -161
  60. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  61. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  62. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  63. package/coverage/lcov-report/hooks/useControls.js.html +0 -868
  64. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  65. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  66. package/coverage/lcov-report/index.html +0 -176
  67. package/coverage/lcov-report/prettify.css +0 -1
  68. package/coverage/lcov-report/prettify.js +0 -2
  69. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  70. package/coverage/lcov-report/sorter.js +0 -210
  71. package/coverage/lcov.info +0 -1163
  72. package/fix-imports.cjs +0 -54
  73. package/jest.config.cjs +0 -14
  74. package/src/App.jsx +0 -102
  75. package/src/components/ContainerCreationPrompt.jsx +0 -107
  76. package/src/components/ContainerList.jsx +0 -32
  77. package/src/components/ContainerRow.jsx +0 -84
  78. package/src/components/ContainerSection.jsx +0 -24
  79. package/src/components/ControlsHUD.jsx +0 -66
  80. package/src/components/Footer.jsx +0 -15
  81. package/src/components/Header.jsx +0 -41
  82. package/src/components/LogViewer.jsx +0 -36
  83. package/src/components/MessageFeedback.jsx +0 -25
  84. package/src/components/PromptField.jsx +0 -48
  85. package/src/components/StatsBar.jsx +0 -70
  86. package/src/components/SuggestionPanel.jsx +0 -61
  87. package/src/components/UsageMenu.jsx +0 -52
  88. package/src/helpers/appInfo.js +0 -23
  89. package/src/helpers/constants.js +0 -89
  90. package/src/helpers/containerOptionsBuilder.js +0 -53
  91. package/src/helpers/dockerHubService.js +0 -75
  92. package/src/helpers/dockerService/dockerService.js +0 -28
  93. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  94. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  95. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  96. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  97. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  98. package/src/helpers/exitWithMessage.js +0 -32
  99. package/src/helpers/imageNameUtils.js +0 -32
  100. package/src/helpers/logger.js +0 -112
  101. package/src/helpers/safeCall.js +0 -18
  102. package/src/helpers/validationHelpers.js +0 -87
  103. package/src/hooks/creation/useContainerActions.js +0 -89
  104. package/src/hooks/creation/useContainerCreation.js +0 -377
  105. package/src/hooks/creation/useLogsViewer.js +0 -51
  106. package/src/hooks/debug/useDebugLogs.js +0 -34
  107. package/src/hooks/navigation/useContainerSelection.js +0 -28
  108. package/src/hooks/useContainerCommandRouter.js +0 -123
  109. package/src/hooks/useContainerStats.js +0 -44
  110. package/src/hooks/useContainers.js +0 -26
  111. package/src/hooks/useControls.js +0 -263
  112. package/src/hooks/useEraseConfirmation.js +0 -39
  113. package/src/hooks/useExitHandler.js +0 -69
  114. package/src/index.js +0 -20
  115. package/test/ContainerCreationPrompt.dom.test.js +0 -313
  116. package/test/ControlsHUD.dom.test.js +0 -125
  117. package/test/SuggestionPanel.dom.test.js +0 -83
  118. package/test/constants.test.js +0 -41
  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/dockerHubService.test.js +0 -213
  123. package/test/jest.setup.js +0 -7
  124. package/test/logger.test.js +0 -58
  125. package/test/resolveImageTag.test.js +0 -26
  126. package/test/safeCall.test.js +0 -25
  127. package/test/useContainerCreation.dom.test.js +0 -567
  128. package/test/useContainerCreationHub.dom.test.js +0 -184
  129. package/test/useControls.dom.test.js +0 -519
  130. 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,213 +0,0 @@
1
- /**
2
- * @jest-environment node
3
- */
4
- import { jest } from '@jest/globals';
5
- import { formatHubResult } from '../src/helpers/dockerHubService.js';
6
-
7
- // We mock fetch at the global level using jest.spyOn
8
- // The module under test is imported dynamically after spy setup
9
-
10
- describe('searchDockerHub — unit tests', () => {
11
- let searchDockerHub;
12
- let fetchSpy;
13
-
14
- beforeEach(async () => {
15
- // Reset modules so each test gets fresh state
16
- jest.resetModules();
17
- // Dynamically import after setting up spy
18
- ({ searchDockerHub } = await import('../src/helpers/dockerHubService.js'));
19
- });
20
-
21
- afterEach(() => {
22
- if (fetchSpy) fetchSpy.mockRestore();
23
- fetchSpy = null;
24
- });
25
-
26
- // ── 1. Query trim & encode ──────────────────────────────────────────────
27
-
28
- test('trims whitespace from query before encoding', async () => {
29
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
30
- ok: true,
31
- json: async () => ({ results: [] }),
32
- });
33
-
34
- await searchDockerHub(' nginx ');
35
-
36
- const calledUrl = fetchSpy.mock.calls[0][0];
37
- expect(calledUrl).toContain('query=nginx');
38
- expect(calledUrl).not.toContain('query=+nginx');
39
- expect(calledUrl).not.toContain(' ');
40
- });
41
-
42
- test('encodes special characters in query', async () => {
43
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
44
- ok: true,
45
- json: async () => ({ results: [] }),
46
- });
47
-
48
- await searchDockerHub('hello world');
49
-
50
- const calledUrl = fetchSpy.mock.calls[0][0];
51
- expect(calledUrl).toContain('query=hello%20world');
52
- });
53
-
54
- test('returns [] immediately for empty query (no fetch call)', async () => {
55
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
56
- ok: true,
57
- json: async () => ({ results: [] }),
58
- });
59
-
60
- const result = await searchDockerHub(' ');
61
-
62
- expect(result).toEqual([]);
63
- expect(fetchSpy).not.toHaveBeenCalled();
64
- });
65
-
66
- // ── 2. AbortSignal.timeout(5000) default ──────────────────────────────
67
-
68
- test('uses AbortSignal.timeout(5000) by default when no signal provided', async () => {
69
- const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
70
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
71
- ok: true,
72
- json: async () => ({ results: [] }),
73
- });
74
-
75
- await searchDockerHub('nginx');
76
-
77
- expect(timeoutSignalSpy).toHaveBeenCalledWith(5000);
78
- timeoutSignalSpy.mockRestore();
79
- });
80
-
81
- test('does NOT call AbortSignal.timeout when signal is injected', async () => {
82
- const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
83
- const controller = new AbortController();
84
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
85
- ok: true,
86
- json: async () => ({ results: [] }),
87
- });
88
-
89
- await searchDockerHub('nginx', { signal: controller.signal });
90
-
91
- expect(timeoutSignalSpy).not.toHaveBeenCalled();
92
- timeoutSignalSpy.mockRestore();
93
- });
94
-
95
- // ── 3. Injected signal is passed to fetch ──────────────────────────────
96
-
97
- test('passes injected signal to fetch', async () => {
98
- const controller = new AbortController();
99
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
100
- ok: true,
101
- json: async () => ({ results: [] }),
102
- });
103
-
104
- await searchDockerHub('nginx', { signal: controller.signal });
105
-
106
- const fetchOptions = fetchSpy.mock.calls[0][1];
107
- expect(fetchOptions.signal).toBe(controller.signal);
108
- });
109
-
110
- // ── 4. Result mapping ──────────────────────────────────────────────────
111
-
112
- test('maps results to normalized objects with name, isOfficial, pullCount, description', async () => {
113
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
114
- ok: true,
115
- json: async () => ({
116
- results: [
117
- {
118
- repo_name: 'library/nginx',
119
- is_official: true,
120
- pull_count: 1000000000,
121
- short_description: 'Official nginx image',
122
- },
123
- {
124
- repo_name: 'bitnami/nginx',
125
- is_official: false,
126
- pull_count: 50000000,
127
- short_description: 'Bitnami nginx image',
128
- },
129
- ],
130
- }),
131
- });
132
-
133
- const results = await searchDockerHub('nginx');
134
-
135
- expect(results).toHaveLength(2);
136
- expect(results[0]).toEqual({
137
- name: 'library/nginx',
138
- isOfficial: true,
139
- pullCount: 1000000000,
140
- description: 'Official nginx image',
141
- });
142
- expect(results[1]).toEqual({
143
- name: 'bitnami/nginx',
144
- isOfficial: false,
145
- pullCount: 50000000,
146
- description: 'Bitnami nginx image',
147
- });
148
- });
149
-
150
- test('returns empty array when results is missing from response', async () => {
151
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
152
- ok: true,
153
- json: async () => ({}),
154
- });
155
-
156
- const results = await searchDockerHub('nginx');
157
- expect(results).toEqual([]);
158
- });
159
-
160
- // ── 5. Non-OK rejection ────────────────────────────────────────────────
161
-
162
- test('throws when response.ok is false', async () => {
163
- fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
164
- ok: false,
165
- status: 429,
166
- json: async () => ({}),
167
- });
168
-
169
- await expect(searchDockerHub('nginx')).rejects.toThrow(
170
- 'Docker Hub search failed: 429'
171
- );
172
- });
173
-
174
- test('re-throws AbortError from fetch', async () => {
175
- const abortErr = new DOMException('Aborted', 'AbortError');
176
- fetchSpy = jest.spyOn(global, 'fetch').mockRejectedValue(abortErr);
177
-
178
- await expect(searchDockerHub('nginx')).rejects.toThrow('Aborted');
179
- });
180
- });
181
-
182
- describe('formatHubResult — unit tests', () => {
183
- test('official repo includes [★ official]', () => {
184
- const result = formatHubResult({ name: 'library/nginx', isOfficial: true, pullCount: 0 });
185
- expect(result).toContain('[★ official]');
186
- expect(result).toContain('library/nginx');
187
- });
188
-
189
- test('high pull count (≥1B) is formatted as 1B+', () => {
190
- const result = formatHubResult({ name: 'nginx', isOfficial: true, pullCount: 1e9 });
191
- expect(result).toContain('[1B+ pulls]');
192
- });
193
-
194
- test('pull count ≥1M is formatted as NM+', () => {
195
- const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e6 });
196
- expect(result).toContain('[500M+ pulls]');
197
- });
198
-
199
- test('pull count ≥1K is formatted as NK+', () => {
200
- const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e3 });
201
- expect(result).toContain('[500K+ pulls]');
202
- });
203
-
204
- test('no pull count → just name (no suffix)', () => {
205
- const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 0 });
206
- expect(result).toBe('myimage');
207
- });
208
-
209
- test('unofficial + low pull count → just name with raw count', () => {
210
- const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 42 });
211
- expect(result).toBe('myimage [42 pulls]');
212
- });
213
- });
@@ -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,58 +0,0 @@
1
- /**
2
- * Tests for src/helpers/logger.js
3
- * Verifies that console output is suppressed when Ink listeners are active.
4
- */
5
- import { jest } from '@jest/globals';
6
-
7
- const { logger } = await import('../src/helpers/logger.js');
8
-
9
- describe('logger — console output suppression', () => {
10
- let consoleSpy;
11
- let warnSpy;
12
-
13
- beforeEach(() => {
14
- consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
15
- warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
16
- });
17
-
18
- afterEach(() => {
19
- consoleSpy.mockRestore();
20
- warnSpy.mockRestore();
21
- });
22
-
23
- test('when NO listeners are registered, logger.error calls console.error (fallback)', () => {
24
- logger.error('test error no listener');
25
- expect(consoleSpy).toHaveBeenCalledTimes(1);
26
- });
27
-
28
- test('when a listener IS registered, logger.error does NOT call console.error', () => {
29
- const unsubscribe = logger.subscribe(() => {});
30
- logger.error('test error with listener');
31
- expect(consoleSpy).not.toHaveBeenCalled();
32
- unsubscribe();
33
- });
34
-
35
- test('when a listener IS registered, logger.warn does NOT call console.warn', () => {
36
- const unsubscribe = logger.subscribe(() => {});
37
- logger.warn('test warn with listener');
38
- expect(warnSpy).not.toHaveBeenCalled();
39
- unsubscribe();
40
- });
41
-
42
- test('after unsubscribing last listener, logger.error resumes calling console.error', () => {
43
- const unsubscribe = logger.subscribe(() => {});
44
- unsubscribe();
45
- logger.error('after unsubscribe');
46
- expect(consoleSpy).toHaveBeenCalledTimes(1);
47
- });
48
-
49
- test('listener is invoked with the log entry even when console output is suppressed', () => {
50
- const received = [];
51
- const unsubscribe = logger.subscribe((entry) => received.push(entry));
52
- logger.error('entry delivered', 'extra-arg');
53
- expect(received).toHaveLength(1);
54
- expect(received[0].level).toBe('error');
55
- expect(received[0].message).toBe('entry delivered');
56
- unsubscribe();
57
- });
58
- });
@@ -1,26 +0,0 @@
1
- /**
2
- * @jest-environment node
3
- */
4
- import { resolveImageTag, IMAGE_PROFILES } from '../src/helpers/constants.js';
5
-
6
- describe('resolveImageTag', () => {
7
- test('appends defaultTag when image has no tag', () => {
8
- expect(resolveImageTag('postgres', IMAGE_PROFILES)).toBe('postgres:17-alpine');
9
- });
10
-
11
- test('preserves explicit tag when image already has one', () => {
12
- expect(resolveImageTag('postgres:15', IMAGE_PROFILES)).toBe('postgres:15');
13
- });
14
-
15
- test('returns image name unchanged when no matching profile', () => {
16
- expect(resolveImageTag('unknownimage', IMAGE_PROFILES)).toBe('unknownimage');
17
- });
18
-
19
- test('appends defaultTag for nginx', () => {
20
- expect(resolveImageTag('nginx', IMAGE_PROFILES)).toBe('nginx:1.27-alpine');
21
- });
22
-
23
- test('passes through empty string unchanged', () => {
24
- expect(resolveImageTag('', IMAGE_PROFILES)).toBe('');
25
- });
26
- });
@@ -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
- });