cdd-cli 3.1.9 → 3.2.2

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/.atl/skill-registry.md +30 -0
  2. package/.editorconfig +12 -0
  3. package/.eslintrc.json +1 -1
  4. package/.prettierignore +4 -0
  5. package/.prettierrc +8 -0
  6. package/CHANGELOG.md +46 -0
  7. package/SECURITY.md +5 -0
  8. package/__mocks__/ink.cjs +17 -0
  9. package/coverage/clover.xml +694 -0
  10. package/coverage/coverage-final.json +21 -0
  11. package/coverage/lcov-report/base.css +224 -0
  12. package/coverage/lcov-report/block-navigation.js +87 -0
  13. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +376 -0
  14. package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
  15. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +244 -0
  16. package/coverage/lcov-report/components/index.html +146 -0
  17. package/coverage/lcov-report/favicon.png +0 -0
  18. package/coverage/lcov-report/helpers/constants.js.html +298 -0
  19. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +244 -0
  20. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +931 -0
  21. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +307 -0
  22. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +131 -0
  23. package/coverage/lcov-report/helpers/imageNameUtils.js.html +181 -0
  24. package/coverage/lcov-report/helpers/index.html +191 -0
  25. package/coverage/lcov-report/helpers/logger.js.html +424 -0
  26. package/coverage/lcov-report/helpers/safeCall.js.html +139 -0
  27. package/coverage/lcov-report/helpers/validationHelpers.js.html +346 -0
  28. package/coverage/lcov-report/hooks/creation/index.html +146 -0
  29. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +313 -0
  30. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +805 -0
  31. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +238 -0
  32. package/coverage/lcov-report/hooks/debug/index.html +116 -0
  33. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +187 -0
  34. package/coverage/lcov-report/hooks/index.html +161 -0
  35. package/coverage/lcov-report/hooks/navigation/index.html +116 -0
  36. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +169 -0
  37. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +454 -0
  38. package/coverage/lcov-report/hooks/useControls.js.html +826 -0
  39. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +202 -0
  40. package/coverage/lcov-report/hooks/useExitHandler.js.html +292 -0
  41. package/coverage/lcov-report/index.html +206 -0
  42. package/coverage/lcov-report/prettify.css +1 -0
  43. package/coverage/lcov-report/prettify.js +2 -0
  44. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  45. package/coverage/lcov-report/sorter.js +210 -0
  46. package/coverage/lcov.info +1402 -0
  47. package/dist/App.js +11 -11
  48. package/dist/components/ContainerCreationPrompt.js +31 -10
  49. package/dist/components/ContainerList.js +3 -3
  50. package/dist/components/ContainerRow.js +20 -20
  51. package/dist/components/ContainerSection.js +3 -3
  52. package/dist/components/Footer.js +2 -2
  53. package/dist/components/Header.js +7 -7
  54. package/dist/components/LogViewer.js +3 -3
  55. package/dist/components/MessageFeedback.js +2 -2
  56. package/dist/components/PromptField.js +4 -4
  57. package/dist/components/StatsBar.js +5 -5
  58. package/dist/components/SuggestionPanel.js +44 -0
  59. package/dist/components/UsageMenu.js +3 -3
  60. package/dist/helpers/appInfo.js +7 -7
  61. package/dist/helpers/constants.js +114 -0
  62. package/dist/helpers/containerOptionsBuilder.js +3 -3
  63. package/dist/helpers/dockerService/dockerService.js +1 -1
  64. package/dist/helpers/dockerService/serviceComponents/containerActions.js +99 -40
  65. package/dist/helpers/dockerService/serviceComponents/containerList.js +11 -11
  66. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +9 -9
  67. package/dist/helpers/dockerService/serviceComponents/containerStats.js +5 -5
  68. package/dist/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  69. package/dist/helpers/exitWithMessage.js +10 -10
  70. package/dist/helpers/imageNameUtils.js +32 -0
  71. package/dist/helpers/logger.js +13 -13
  72. package/dist/helpers/safeCall.js +2 -2
  73. package/dist/helpers/validationHelpers.js +76 -21
  74. package/dist/hooks/creation/useContainerActions.js +9 -9
  75. package/dist/hooks/creation/useContainerCreation.js +138 -36
  76. package/dist/hooks/creation/useLogsViewer.js +2 -2
  77. package/dist/hooks/debug/useDebugLogs.js +4 -4
  78. package/dist/hooks/useContainerCommandRouter.js +13 -13
  79. package/dist/hooks/useContainers.js +3 -3
  80. package/dist/hooks/useControls.js +66 -35
  81. package/dist/hooks/useEraseConfirmation.js +3 -3
  82. package/dist/index.js +2 -2
  83. package/jest.config.cjs +7 -0
  84. package/package.json +7 -3
  85. package/src/App.jsx +33 -16
  86. package/src/components/ContainerCreationPrompt.jsx +43 -9
  87. package/src/components/ContainerList.jsx +3 -3
  88. package/src/components/ContainerRow.jsx +19 -16
  89. package/src/components/ContainerSection.jsx +3 -3
  90. package/src/components/Footer.jsx +2 -2
  91. package/src/components/Header.jsx +7 -7
  92. package/src/components/LogViewer.jsx +5 -3
  93. package/src/components/MessageFeedback.jsx +2 -2
  94. package/src/components/PromptField.jsx +4 -4
  95. package/src/components/StatsBar.jsx +11 -7
  96. package/src/components/SuggestionPanel.jsx +53 -0
  97. package/src/components/UsageMenu.jsx +18 -13
  98. package/src/helpers/appInfo.js +7 -7
  99. package/src/helpers/constants.js +41 -0
  100. package/src/helpers/containerOptionsBuilder.js +16 -5
  101. package/src/helpers/dockerService/dockerService.js +1 -1
  102. package/src/helpers/dockerService/serviceComponents/containerActions.js +116 -43
  103. package/src/helpers/dockerService/serviceComponents/containerList.js +31 -25
  104. package/src/helpers/dockerService/serviceComponents/containerLogs.js +41 -26
  105. package/src/helpers/dockerService/serviceComponents/containerStats.js +13 -11
  106. package/src/helpers/dockerService/serviceComponents/imageUtils.js +5 -4
  107. package/src/helpers/exitWithMessage.js +13 -7
  108. package/src/helpers/imageNameUtils.js +32 -0
  109. package/src/helpers/logger.js +15 -15
  110. package/src/helpers/safeCall.js +2 -2
  111. package/src/helpers/validationHelpers.js +62 -21
  112. package/src/hooks/creation/useContainerActions.js +19 -12
  113. package/src/hooks/creation/useContainerCreation.js +161 -37
  114. package/src/hooks/creation/useLogsViewer.js +2 -2
  115. package/src/hooks/debug/useDebugLogs.js +15 -10
  116. package/src/hooks/navigation/useContainerSelection.js +17 -14
  117. package/src/hooks/useContainerCommandRouter.js +71 -58
  118. package/src/hooks/useContainers.js +4 -4
  119. package/src/hooks/useControls.js +120 -68
  120. package/src/hooks/useEraseConfirmation.js +3 -3
  121. package/src/hooks/useExitHandler.js +29 -26
  122. package/src/index.js +4 -4
  123. package/test/ContainerCreationPrompt.dom.test.js +68 -0
  124. package/test/SuggestionPanel.dom.test.js +53 -0
  125. package/test/constants.test.js +34 -0
  126. package/test/containerActions.test.js +104 -13
  127. package/test/containerOptionsBuilder.test.js +36 -0
  128. package/test/useContainerCreation.dom.test.js +166 -2
  129. package/test/useControls.dom.test.js +313 -0
  130. package/test/validationHelpers.test.js +51 -0
@@ -1,16 +1,17 @@
1
- import React from "react";
2
- import { useInput } from "ink";
3
- import { useContainerActions } from "./creation/useContainerActions";
4
- import { useContainerCreation } from "./creation/useContainerCreation";
5
- import { useLogsViewer } from "./creation/useLogsViewer";
6
- import { useContainerSelection } from "./navigation/useContainerSelection";
7
- import { useDebugLogs } from "./debug/useDebugLogs";
8
- import { useEraseConfirmation } from "./useEraseConfirmation";
9
- import { useExitHandler } from "./useExitHandler";
10
- import { useContainerCommandRouter } from "./useContainerCommandRouter";
11
- import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
12
- import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
13
- import { buildContainerOptions } from "../helpers/containerOptionsBuilder.js";
1
+ import React from 'react';
2
+ import { useInput } from 'ink';
3
+ import { useContainerActions } from './creation/useContainerActions';
4
+ import { useContainerCreation } from './creation/useContainerCreation';
5
+ import { useLogsViewer } from './creation/useLogsViewer';
6
+ import { useContainerSelection } from './navigation/useContainerSelection';
7
+ import { useDebugLogs } from './debug/useDebugLogs';
8
+ import { useEraseConfirmation } from './useEraseConfirmation';
9
+ import { useExitHandler } from './useExitHandler';
10
+ import { useContainerCommandRouter } from './useContainerCommandRouter';
11
+ import { getLogsStream } from '../helpers/dockerService/serviceComponents/containerLogs.js';
12
+ import { createContainer as svcCreateContainer } from '../helpers/dockerService/serviceComponents/containerActions.js';
13
+ import { buildContainerOptions } from '../helpers/containerOptionsBuilder.js';
14
+ import { DB_IMAGES } from '../helpers/constants.js';
14
15
 
15
16
  // Principal hook to manage user inputs and control the app state
16
17
  /**
@@ -28,22 +29,35 @@ export function useControls(containers = []) {
28
29
 
29
30
  const creation = useContainerCreation({
30
31
  onCreate: async ({ imageName, containerName, portInput, envInput }) => {
31
- const options = buildContainerOptions({ imageName, containerName, portInput, envInput });
32
+ const options = buildContainerOptions({
33
+ imageName,
34
+ containerName,
35
+ portInput,
36
+ envInput,
37
+ });
32
38
  actions.setMessage(`Creating container ${imageName}...`);
33
- actions.setMessageColor("yellow");
39
+ actions.setMessageColor('yellow');
34
40
  try {
35
- const id = await svcCreateContainer(imageName, options);
36
- actions.setMessage(`Created container ${id}`);
37
- actions.setMessageColor("green");
41
+ const { id, ports } = await svcCreateContainer(imageName, options);
42
+ let portMsg = '';
43
+ if (ports && ports.length) {
44
+ portMsg =
45
+ ' | Ports: ' +
46
+ ports
47
+ .map((p) => `${p.hostPort}→${p.containerPort}/${p.protocol}`)
48
+ .join(', ');
49
+ }
50
+ actions.setMessage(`Created container ${id}${portMsg}`);
51
+ actions.setMessageColor('green');
38
52
  } catch (err) {
39
53
  actions.setMessage(`Error creating container: ${err.message}`);
40
- actions.setMessageColor("red");
54
+ actions.setMessageColor('red');
41
55
  } finally {
42
56
  setCreatingContainer(false);
43
57
  }
44
58
  },
45
59
  onCancel: () => setCreatingContainer(false),
46
- dbImages: ["mysql", "mariadb", "postgres", "mongo", "mssql", "redis"],
60
+ dbImages: DB_IMAGES,
47
61
  });
48
62
 
49
63
  const logsViewer = useLogsViewer();
@@ -54,14 +68,14 @@ export function useControls(containers = []) {
54
68
  onConfirm: () => {
55
69
  actions.handleAction({
56
70
  actionFn: async (id) => await actions.removeContainer(id),
57
- actionLabel: "Erasing",
71
+ actionLabel: 'Erasing',
58
72
  selected: selection.selected,
59
73
  });
60
- actions.setMessageColor("yellow");
74
+ actions.setMessageColor('yellow');
61
75
  },
62
76
  onCancel: () => {
63
- actions.setMessage("");
64
- actions.setMessageColor("");
77
+ actions.setMessage('');
78
+ actions.setMessageColor('');
65
79
  },
66
80
  });
67
81
 
@@ -69,17 +83,22 @@ export function useControls(containers = []) {
69
83
  * Pipe container logs into the viewer while applying a hard limit.
70
84
  * @param {string} containerId - Container identifier used by Docker.
71
85
  */
72
- const startLogsStream = React.useCallback((containerId) => {
73
- getLogsStream(
74
- containerId,
75
- (data) => logsViewer.setLogs((prev) => {
76
- const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
77
- return newLogs.slice(-1000);
78
- }),
79
- () => {},
80
- (err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
81
- );
82
- }, [logsViewer]);
86
+ const startLogsStream = React.useCallback(
87
+ (containerId) => {
88
+ getLogsStream(
89
+ containerId,
90
+ (data) =>
91
+ logsViewer.setLogs((prev) => {
92
+ const newLogs = [...prev, ...data.split('\n').filter(Boolean)];
93
+ return newLogs.slice(-1000);
94
+ }),
95
+ () => {},
96
+ (err) =>
97
+ logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
98
+ );
99
+ },
100
+ [logsViewer]
101
+ );
83
102
 
84
103
  const commandRouter = useContainerCommandRouter({
85
104
  actions,
@@ -90,8 +109,10 @@ export function useControls(containers = []) {
90
109
  startLogsStream,
91
110
  onStartErase: () => {
92
111
  eraseConfirmation.startErase();
93
- actions.setMessage("Are you sure you want to erase this container? (y/n)");
94
- actions.setMessageColor("yellow");
112
+ actions.setMessage(
113
+ 'Are you sure you want to erase this container? (y/n)'
114
+ );
115
+ actions.setMessageColor('yellow');
95
116
  },
96
117
  onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
97
118
  onStartCreate: () => setCreatingContainer(true),
@@ -99,8 +120,8 @@ export function useControls(containers = []) {
99
120
 
100
121
  const exitHandler = useExitHandler({
101
122
  onBeforeExit: () => {
102
- actions.setMessage("Exiting...");
103
- actions.setMessageColor("yellow");
123
+ actions.setMessage('Exiting...');
124
+ actions.setMessageColor('yellow');
104
125
  logsViewer.closeLogs();
105
126
  debugLogs.setShowDebugLogs(false);
106
127
  },
@@ -109,38 +130,67 @@ export function useControls(containers = []) {
109
130
  /**
110
131
  * Route keystrokes to the container creation wizard.
111
132
  */
112
- const processCreationInput = React.useCallback((input, key) => {
113
- const step = creation.step;
133
+ const processCreationInput = React.useCallback(
134
+ (input, key) => {
135
+ const step = creation.step;
114
136
 
115
- const removeLastChar = (setter) => setter((value) => (value || "").slice(0, -1));
116
- const appendChar = (setter, current, ch) => setter((current || "") + ch);
137
+ const removeLastChar = (setter) =>
138
+ setter((value) => (value || '').slice(0, -1));
139
+ const appendChar = (setter, current, ch) => setter((current || '') + ch);
117
140
 
118
- if (key.escape) {
119
- creation.cancelCreation();
120
- setCreatingContainer(false);
121
- return;
122
- }
141
+ if (key.escape) {
142
+ creation.cancelCreation();
143
+ setCreatingContainer(false);
144
+ return;
145
+ }
123
146
 
124
- if (input === "\r" || input === "\n") {
125
- creation.nextStep();
126
- return;
127
- }
147
+ if (input === '\r' || input === '\n') {
148
+ // If on step 0 with a focused suggestion, apply it instead of advancing
149
+ if (step === 0 && creation.selectedSuggestionIndex >= 0) {
150
+ creation.applyFocusedSuggestion();
151
+ return;
152
+ }
153
+ creation.nextStep();
154
+ return;
155
+ }
128
156
 
129
- if (key.backspace || key.delete) {
130
- if (step === 0) removeLastChar(creation.setImageName);
131
- if (step === 1) removeLastChar(creation.setContainerName);
132
- if (step === 2) removeLastChar(creation.setPortInput);
133
- if (step === 3) removeLastChar(creation.setEnvInput);
134
- return;
135
- }
157
+ // Arrow keys on step 0: navigate suggestion list
158
+ if (step === 0 && creation.suggestions.length > 0) {
159
+ if (key.upArrow) {
160
+ creation.moveSuggestionSelection(-1);
161
+ return;
162
+ }
163
+ if (key.downArrow) {
164
+ creation.moveSuggestionSelection(1);
165
+ return;
166
+ }
167
+ }
136
168
 
137
- if (input && input.length === 1 && !key.ctrl && !key.meta) {
138
- if (step === 0) appendChar(creation.setImageName, creation.imageName, input);
139
- if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
140
- if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
141
- if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
142
- }
143
- }, [creation, setCreatingContainer]);
169
+ if (key.backspace || key.delete) {
170
+ if (step === 0) {
171
+ const newValue = (creation.imageName || '').slice(0, -1);
172
+ creation.updateImageInput(newValue);
173
+ }
174
+ if (step === 1) removeLastChar(creation.setContainerName);
175
+ if (step === 2) removeLastChar(creation.setPortInput);
176
+ if (step === 3) removeLastChar(creation.setEnvInput);
177
+ return;
178
+ }
179
+
180
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
181
+ if (step === 0) {
182
+ creation.updateImageInput((creation.imageName || '') + input);
183
+ }
184
+ if (step === 1)
185
+ appendChar(creation.setContainerName, creation.containerName, input);
186
+ if (step === 2)
187
+ appendChar(creation.setPortInput, creation.portInput, input);
188
+ if (step === 3)
189
+ appendChar(creation.setEnvInput, creation.envInput, input);
190
+ }
191
+ },
192
+ [creation, setCreatingContainer]
193
+ );
144
194
 
145
195
  // Single keyboard entry point
146
196
  useInput((input, key) => {
@@ -150,7 +200,7 @@ export function useControls(containers = []) {
150
200
  }
151
201
 
152
202
  if (logsViewer.showLogs) {
153
- if (input === "q" || key.escape) {
203
+ if (input === 'q' || key.escape) {
154
204
  logsViewer.closeLogs();
155
205
  }
156
206
  return;
@@ -175,7 +225,9 @@ export function useControls(containers = []) {
175
225
  selected: selection.selected,
176
226
  setSelected: selection.setSelected,
177
227
  message: creatingContainer ? creation.message : actions.message,
178
- messageColor: creatingContainer ? creation.messageColor : actions.messageColor,
228
+ messageColor: creatingContainer
229
+ ? creation.messageColor
230
+ : actions.messageColor,
179
231
  showLogs: logsViewer.showLogs,
180
232
  logs: logsViewer.logs,
181
233
  exitLogs: logsViewer.closeLogs,
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from "react";
1
+ import { useState, useCallback } from 'react';
2
2
 
3
3
  /**
4
4
  * Hook to manage the erase confirmation dialog flow.
@@ -20,13 +20,13 @@ export function useEraseConfirmation({ onConfirm, onCancel }) {
20
20
 
21
21
  const processEraseConfirmation = useCallback(
22
22
  (input, key) => {
23
- if (input === "y" || input === "Y") {
23
+ if (input === 'y' || input === 'Y') {
24
24
  onConfirm();
25
25
  setConfirmErase(false);
26
26
  return;
27
27
  }
28
28
 
29
- if (input === "n" || input === "N" || key.escape) {
29
+ if (input === 'n' || input === 'N' || key.escape) {
30
30
  setConfirmErase(false);
31
31
  onCancel();
32
32
  return;
@@ -30,37 +30,40 @@ export function useExitHandler({ onBeforeExit } = {}) {
30
30
  * @param {string} input - Raw character input from Ink's useInput
31
31
  * @returns {boolean} True when the input was consumed, false otherwise
32
32
  */
33
- const handleExitCommand = useCallback((input) => {
34
- if (input !== 'q') {
35
- return false;
36
- }
33
+ const handleExitCommand = useCallback(
34
+ (input) => {
35
+ if (input !== 'q') {
36
+ return false;
37
+ }
37
38
 
38
- if (typeof onBeforeExit === 'function') {
39
- onBeforeExit();
40
- }
39
+ if (typeof onBeforeExit === 'function') {
40
+ onBeforeExit();
41
+ }
41
42
 
42
- const clearTerminal = () => {
43
- try {
44
- if (process.stdout && process.stdout.isTTY) {
45
- process.stdout.write('\u001Bc');
46
- } else {
47
- console.clear();
43
+ const clearTerminal = () => {
44
+ try {
45
+ if (process.stdout && process.stdout.isTTY) {
46
+ process.stdout.write('\u001Bc');
47
+ } else {
48
+ console.clear();
49
+ }
50
+ } catch (err) {
51
+ // Ignore clear errors to avoid blocking exit.
48
52
  }
49
- } catch (err) {
50
- // Ignore clear errors to avoid blocking exit.
51
- }
52
- };
53
+ };
53
54
 
54
- setTimeout(() => {
55
- clearTerminal();
56
- exit();
57
- if (process.env.NODE_ENV !== 'test') {
58
- setTimeout(() => process.exit(0), 50);
59
- }
60
- }, EXIT_DELAY);
55
+ setTimeout(() => {
56
+ clearTerminal();
57
+ exit();
58
+ if (process.env.NODE_ENV !== 'test') {
59
+ setTimeout(() => process.exit(0), 50);
60
+ }
61
+ }, EXIT_DELAY);
61
62
 
62
- return true;
63
- }, [exit, onBeforeExit]);
63
+ return true;
64
+ },
65
+ [exit, onBeforeExit]
66
+ );
64
67
 
65
68
  return { handleExitCommand };
66
69
  }
package/src/index.js CHANGED
@@ -8,9 +8,9 @@
8
8
  * node index.js
9
9
  */
10
10
 
11
- import React from "react";
12
- import { render } from "ink";
13
- import App from './App';
11
+ import React from 'react';
12
+ import { render } from 'ink';
13
+ import App from './App.jsx';
14
14
 
15
15
  console.clear();
16
- render(<App />);
16
+ render(<App />);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { render } from '@testing-library/react';
6
+ import { ContainerCreationPrompt } from '../src/components/ContainerCreationPrompt.jsx';
7
+
8
+ describe('ContainerCreationPrompt — SuggestionPanel wiring', () => {
9
+ test('does NOT render SuggestionPanel when step > 0', () => {
10
+ const { queryByText } = render(
11
+ <ContainerCreationPrompt
12
+ step={1}
13
+ imageName="nginx"
14
+ containerName=""
15
+ portInput=""
16
+ envInput=""
17
+ message=""
18
+ messageColor="yellow"
19
+ suggestions={['nginx', 'node']}
20
+ selectedSuggestionIndex={-1}
21
+ visibleOffset={0}
22
+ />
23
+ );
24
+ // On step 1, the imageName field is not active — no suggestion panel
25
+ expect(queryByText(/^nginx$/)).toBeNull();
26
+ });
27
+
28
+ test('does NOT render SuggestionPanel on step 0 when suggestions is empty', () => {
29
+ const { queryByTestId } = render(
30
+ <ContainerCreationPrompt
31
+ step={0}
32
+ imageName="ng"
33
+ containerName=""
34
+ portInput=""
35
+ envInput=""
36
+ message=""
37
+ messageColor="yellow"
38
+ suggestions={[]}
39
+ selectedSuggestionIndex={-1}
40
+ visibleOffset={0}
41
+ />
42
+ );
43
+ // No suggestion panel should render when suggestions array is empty
44
+ expect(queryByTestId('suggestion-panel')).toBeNull();
45
+ });
46
+
47
+ test('renders SuggestionPanel items on step 0 when suggestions exist', () => {
48
+ const { getAllByText, getByText } = render(
49
+ <ContainerCreationPrompt
50
+ step={0}
51
+ imageName="ng"
52
+ containerName=""
53
+ portInput=""
54
+ envInput=""
55
+ message=""
56
+ messageColor="yellow"
57
+ suggestions={['nginx', 'golang']}
58
+ selectedSuggestionIndex={-1}
59
+ visibleOffset={0}
60
+ />
61
+ );
62
+ // golang is not in any label — appears only in suggestion panel
63
+ expect(getByText(/golang/)).toBeTruthy();
64
+ // nginx appears in suggestion panel (may also appear in label text, but it does appear)
65
+ const nginxEls = getAllByText(/nginx/);
66
+ expect(nginxEls.length).toBeGreaterThan(0);
67
+ });
68
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { render } from '@testing-library/react';
6
+ import { SuggestionPanel } from '../src/components/SuggestionPanel.jsx';
7
+
8
+ describe('SuggestionPanel', () => {
9
+ const items = ['caddy', 'elasticsearch', 'golang', 'httpd', 'kafka', 'mariadb', 'memcached', 'minio'];
10
+
11
+ test('renders nothing when items is empty', () => {
12
+ const { container } = render(
13
+ <SuggestionPanel items={[]} selectedIndex={-1} visibleOffset={0} maxVisible={5} />
14
+ );
15
+ expect(container.firstChild).toBeNull();
16
+ });
17
+
18
+ test('renders only maxVisible items from visibleOffset', () => {
19
+ const { getByText, queryByText } = render(
20
+ <SuggestionPanel items={items} selectedIndex={-1} visibleOffset={0} maxVisible={5} />
21
+ );
22
+ expect(getByText(/caddy/)).toBeTruthy();
23
+ expect(getByText(/elasticsearch/)).toBeTruthy();
24
+ expect(getByText(/kafka/)).toBeTruthy();
25
+ expect(queryByText(/mariadb/)).toBeNull();
26
+ });
27
+
28
+ test('respects visibleOffset by skipping early items', () => {
29
+ const { queryByText, getByText } = render(
30
+ <SuggestionPanel items={items} selectedIndex={-1} visibleOffset={2} maxVisible={3} />
31
+ );
32
+ expect(getByText(/golang/)).toBeTruthy();
33
+ expect(getByText(/httpd/)).toBeTruthy();
34
+ expect(getByText(/kafka/)).toBeTruthy();
35
+ expect(queryByText(/caddy/)).toBeNull();
36
+ });
37
+
38
+ test('focused row has › prefix marker', () => {
39
+ const { getByText } = render(
40
+ <SuggestionPanel items={items} selectedIndex={1} visibleOffset={0} maxVisible={5} />
41
+ );
42
+ const el = getByText(/› elasticsearch/);
43
+ expect(el).toBeTruthy();
44
+ });
45
+
46
+ test('non-focused rows do NOT have › prefix', () => {
47
+ const { getByText } = render(
48
+ <SuggestionPanel items={items} selectedIndex={1} visibleOffset={0} maxVisible={5} />
49
+ );
50
+ const el = getByText(/caddy/);
51
+ expect(el.textContent).not.toMatch(/›/);
52
+ });
53
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+ import { IMAGE_PROFILES } from '../src/helpers/constants.js';
5
+
6
+ describe('IMAGE_PROFILES', () => {
7
+ test('has ~20 entries', () => {
8
+ const count = Object.keys(IMAGE_PROFILES).length;
9
+ expect(count).toBeGreaterThanOrEqual(20);
10
+ });
11
+
12
+ test('every entry has requiredEnv (array), defaultPort (string), and suggestedEnv (array)', () => {
13
+ for (const [key, profile] of Object.entries(IMAGE_PROFILES)) {
14
+ expect(Array.isArray(profile.requiredEnv)).toBe(true);
15
+ expect(typeof profile.defaultPort).toBe('string');
16
+ expect(Array.isArray(profile.suggestedEnv)).toBe(true);
17
+ }
18
+ });
19
+
20
+ test('postgres has POSTGRES_PASSWORD in requiredEnv and suggestedEnv hints', () => {
21
+ expect(IMAGE_PROFILES.postgres.requiredEnv).toContain('POSTGRES_PASSWORD');
22
+ expect(IMAGE_PROFILES.postgres.suggestedEnv.length).toBeGreaterThan(0);
23
+ });
24
+
25
+ test('nginx has empty requiredEnv, defaultPort 80, and empty suggestedEnv', () => {
26
+ expect(IMAGE_PROFILES.nginx.requiredEnv).toEqual([]);
27
+ expect(IMAGE_PROFILES.nginx.defaultPort).toBe('80');
28
+ expect(IMAGE_PROFILES.nginx.suggestedEnv).toEqual([]);
29
+ });
30
+
31
+ test('node has suggestedEnv with NODE_ENV hint', () => {
32
+ expect(IMAGE_PROFILES.node.suggestedEnv).toContain('NODE_ENV=production');
33
+ });
34
+ });