push-guardian 1.0.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/.dockerignore +15 -0
- package/.pushguardian-plugins.json +10 -0
- package/Dockerfile +41 -0
- package/Dockerfile.dev +20 -0
- package/README.md +386 -0
- package/TECHNO.md +139 -0
- package/babel.config.js +1 -0
- package/developper_utils.md +119 -0
- package/docker-compose.yml +41 -0
- package/docs/PLUGINS.md +223 -0
- package/docs/technical/architecture.md +298 -0
- package/docs/technical/performance-guide.md +390 -0
- package/docs/technical/plugin-persistence.md +169 -0
- package/docs/technical/plugins-guide.md +409 -0
- package/jest.config.js +22 -0
- package/package.json +53 -0
- package/plugins/example-plugin/index.js +55 -0
- package/plugins/example-plugin/plugin.json +8 -0
- package/scripts/coverage-report.js +75 -0
- package/src/cli/command/config.js +33 -0
- package/src/cli/command/install.js +137 -0
- package/src/cli/command/mirror.js +90 -0
- package/src/cli/command/performance.js +160 -0
- package/src/cli/command/plugin.js +171 -0
- package/src/cli/command/security.js +152 -0
- package/src/cli/command/shell.js +238 -0
- package/src/cli/command/validate.js +54 -0
- package/src/cli/index.js +23 -0
- package/src/cli/install/codeQualityTools.js +156 -0
- package/src/cli/install/hooks.js +89 -0
- package/src/cli/install/mirroring.js +299 -0
- package/src/core/codeQualityTools/configAnalyzer.js +216 -0
- package/src/core/codeQualityTools/configGenerator.js +381 -0
- package/src/core/codeQualityTools/configManager.js +65 -0
- package/src/core/codeQualityTools/fileDetector.js +62 -0
- package/src/core/codeQualityTools/languageTools.js +104 -0
- package/src/core/codeQualityTools/toolInstaller.js +53 -0
- package/src/core/configManager.js +43 -0
- package/src/core/errorCMD.js +9 -0
- package/src/core/interactiveMenu/interactiveMenu.js +73 -0
- package/src/core/mirroring/branchSynchronizer.js +59 -0
- package/src/core/mirroring/generate.js +114 -0
- package/src/core/mirroring/repoManager.js +112 -0
- package/src/core/mirroring/syncManager.js +176 -0
- package/src/core/module/env-loader.js +109 -0
- package/src/core/performance/metricsCollector.js +217 -0
- package/src/core/performance/performanceAnalyzer.js +182 -0
- package/src/core/plugins/basePlugin.js +89 -0
- package/src/core/plugins/pluginManager.js +123 -0
- package/src/core/plugins/pluginRegistry.js +215 -0
- package/src/core/validator.js +53 -0
- package/src/hooks/constrains/constrains.js +174 -0
- package/src/hooks/constrains/constraintEngine.js +140 -0
- package/src/utils/chalk-wrapper.js +26 -0
- package/src/utils/exec-wrapper.js +6 -0
- package/tests/fixtures/mock-eslint-config-array.js +8 -0
- package/tests/fixtures/mock-eslint-config-single.js +6 -0
- package/tests/fixtures/mockLoadedPlugin.js +11 -0
- package/tests/setup.js +28 -0
- package/tests/unit/basePlugin.test.js +355 -0
- package/tests/unit/branchSynchronizer.test.js +308 -0
- package/tests/unit/cli-commands.test.js +144 -0
- package/tests/unit/codeQualityConfigManager.test.js +233 -0
- package/tests/unit/codeQualityTools.test.js +36 -0
- package/tests/unit/command-install.test.js +247 -0
- package/tests/unit/command-mirror.test.js +179 -0
- package/tests/unit/command-performance.test.js +169 -0
- package/tests/unit/command-plugin.test.js +288 -0
- package/tests/unit/command-security.test.js +277 -0
- package/tests/unit/command-shell.test.js +325 -0
- package/tests/unit/configAnalyzer.test.js +593 -0
- package/tests/unit/configGenerator.test.js +808 -0
- package/tests/unit/configManager.test.js +195 -0
- package/tests/unit/constrains.test.js +463 -0
- package/tests/unit/constraint.test.js +554 -0
- package/tests/unit/env-loader.test.js +279 -0
- package/tests/unit/fileDetector.test.js +171 -0
- package/tests/unit/install-codeQualityTools.test.js +343 -0
- package/tests/unit/install-hooks.test.js +280 -0
- package/tests/unit/install-mirroring.test.js +731 -0
- package/tests/unit/install-modules.test.js +81 -0
- package/tests/unit/interactiveMenu.test.js +426 -0
- package/tests/unit/languageTools.test.js +244 -0
- package/tests/unit/metricsCollector.test.js +354 -0
- package/tests/unit/mirroring-generate.test.js +96 -0
- package/tests/unit/modules-exist.test.js +96 -0
- package/tests/unit/performanceAnalyzer.test.js +473 -0
- package/tests/unit/pluginManager.test.js +427 -0
- package/tests/unit/pluginRegistry.test.js +592 -0
- package/tests/unit/repoManager.test.js +469 -0
- package/tests/unit/reviewAppManager.test.js +5 -0
- package/tests/unit/security-command.test.js +43 -0
- package/tests/unit/syncManager.test.js +494 -0
- package/tests/unit/toolInstaller.test.js +240 -0
- package/tests/unit/utils.test.js +144 -0
- package/tests/unit/validator.test.js +215 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Mock interactiveMenu au niveau du module pour éviter les problèmes avec stdin
|
|
2
|
+
jest.mock('../../src/core/interactiveMenu/interactiveMenu', () => {
|
|
3
|
+
return jest.fn(() => Promise.resolve(['Hooks Git']));
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const interactiveMenu = require('../../src/core/interactiveMenu/interactiveMenu');
|
|
7
|
+
|
|
8
|
+
describe('Core InteractiveMenu', () => {
|
|
9
|
+
test('le module doit être une fonction', () => {
|
|
10
|
+
expect(typeof interactiveMenu).toBe('function');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('doit retourner une Promise', () => {
|
|
14
|
+
const result = interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
15
|
+
expect(result).toBeInstanceOf(Promise);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('CLI Install - hooks', () => {
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
25
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
console.log.mockRestore();
|
|
30
|
+
console.error.mockRestore();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('le module doit être chargeable', () => {
|
|
34
|
+
expect(() => require('../../src/cli/install/hooks')).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('installHooks doit être une fonction', () => {
|
|
38
|
+
const { installHooks } = require('../../src/cli/install/hooks');
|
|
39
|
+
expect(typeof installHooks).toBe('function');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('CLI Install - codeQualityTools', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
console.log.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('le module doit être chargeable', () => {
|
|
54
|
+
expect(() => require('../../src/cli/install/codeQualityTools')).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('installCodeQualityTools doit être une fonction', () => {
|
|
58
|
+
const { installCodeQualityTools } = require('../../src/cli/install/codeQualityTools');
|
|
59
|
+
expect(typeof installCodeQualityTools).toBe('function');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('CLI Install - mirroring', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
console.log.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('le module doit être chargeable', () => {
|
|
74
|
+
expect(() => require('../../src/cli/install/mirroring')).not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('installMirroringTools doit être une fonction', () => {
|
|
78
|
+
const { installMirroringTools } = require('../../src/cli/install/mirroring');
|
|
79
|
+
expect(typeof installMirroringTools).toBe('function');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// Mock stdin before requiring the module
|
|
2
|
+
const mockStdin = {
|
|
3
|
+
setRawMode: jest.fn(),
|
|
4
|
+
resume: jest.fn(),
|
|
5
|
+
pause: jest.fn(),
|
|
6
|
+
setEncoding: jest.fn(),
|
|
7
|
+
removeListener: jest.fn(),
|
|
8
|
+
on: jest.fn(),
|
|
9
|
+
removeAllListeners: jest.fn(),
|
|
10
|
+
setMaxListeners: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Replace stdin with mock
|
|
14
|
+
const originalStdin = process.stdin;
|
|
15
|
+
Object.defineProperty(process, 'stdin', {
|
|
16
|
+
value: mockStdin,
|
|
17
|
+
writable: true,
|
|
18
|
+
configurable: true
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const interactiveMenu = require('../../src/core/interactiveMenu/interactiveMenu');
|
|
22
|
+
|
|
23
|
+
describe('Core InteractiveMenu - interactiveMenu', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
jest.spyOn(console, 'clear').mockImplementation(() => {});
|
|
27
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
28
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
29
|
+
|
|
30
|
+
// Reset mock implementations
|
|
31
|
+
mockStdin.setRawMode.mockReturnValue(undefined);
|
|
32
|
+
mockStdin.resume.mockReturnValue(undefined);
|
|
33
|
+
mockStdin.pause.mockReturnValue(undefined);
|
|
34
|
+
mockStdin.setEncoding.mockReturnValue(undefined);
|
|
35
|
+
mockStdin.removeListener.mockReturnValue(undefined);
|
|
36
|
+
mockStdin.on.mockReturnValue(undefined);
|
|
37
|
+
mockStdin.removeAllListeners.mockReturnValue(undefined);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
console.clear.mockRestore();
|
|
42
|
+
console.log.mockRestore();
|
|
43
|
+
if (process.exit && process.exit.mockRestore) process.exit.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('module export', () => {
|
|
47
|
+
test('doit être une fonction', () => {
|
|
48
|
+
expect(typeof interactiveMenu).toBe('function');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('doit retourner une Promise', () => {
|
|
52
|
+
const result = interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
53
|
+
expect(result).toBeInstanceOf(Promise);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('affichage', () => {
|
|
58
|
+
test('doit afficher le message', () => {
|
|
59
|
+
interactiveMenu('Test message', ['Option 1']);
|
|
60
|
+
|
|
61
|
+
expect(console.clear).toHaveBeenCalled();
|
|
62
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Test message'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('doit afficher toutes les options', () => {
|
|
66
|
+
const choices = ['Option 1', 'Option 2', 'Option 3'];
|
|
67
|
+
interactiveMenu('Test', choices);
|
|
68
|
+
|
|
69
|
+
choices.forEach(choice => {
|
|
70
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining(choice));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('doit afficher les instructions', () => {
|
|
75
|
+
interactiveMenu('Test', ['Option 1']);
|
|
76
|
+
|
|
77
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Up'));
|
|
78
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Down'));
|
|
79
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Select'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('doit mettre en surbrillance première option', () => {
|
|
83
|
+
interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
84
|
+
|
|
85
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('>'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('doit marquer options présélectionnées', () => {
|
|
89
|
+
interactiveMenu('Test', ['Option 1', 'Option 2'], [1]);
|
|
90
|
+
|
|
91
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('*'));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('comportement', () => {
|
|
96
|
+
test('doit accepter présélections vides', () => {
|
|
97
|
+
expect(() => {
|
|
98
|
+
interactiveMenu('Test', ['Option 1', 'Option 2'], []);
|
|
99
|
+
}).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('doit accepter présélections multiples', () => {
|
|
103
|
+
expect(() => {
|
|
104
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [0, 2]);
|
|
105
|
+
}).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('doit gérer une seule option', () => {
|
|
109
|
+
interactiveMenu('Test', ['Unique option']);
|
|
110
|
+
|
|
111
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Unique option'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('doit gérer liste vide', () => {
|
|
115
|
+
expect(() => {
|
|
116
|
+
interactiveMenu('Test', []);
|
|
117
|
+
}).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('doit créer Promise pour chaque appel', () => {
|
|
121
|
+
const promise1 = interactiveMenu('Test', ['Option 1']);
|
|
122
|
+
const promise2 = interactiveMenu('Test', ['Option 2']);
|
|
123
|
+
|
|
124
|
+
expect(promise1).toBeInstanceOf(Promise);
|
|
125
|
+
expect(promise2).toBeInstanceOf(Promise);
|
|
126
|
+
expect(promise1).not.toBe(promise2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('paramètres', () => {
|
|
131
|
+
test('doit accepter message et choices', () => {
|
|
132
|
+
expect(() => {
|
|
133
|
+
interactiveMenu('Message', ['Choice 1', 'Choice 2']);
|
|
134
|
+
}).not.toThrow();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('doit accepter présélections optionnelles', () => {
|
|
138
|
+
expect(() => {
|
|
139
|
+
interactiveMenu('Test', ['Option']);
|
|
140
|
+
}).not.toThrow();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('doit gérer tableau présélections', () => {
|
|
144
|
+
expect(() => {
|
|
145
|
+
interactiveMenu('Test', ['A', 'B', 'C'], [0, 1]);
|
|
146
|
+
}).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('navigation avec touches', () => {
|
|
151
|
+
let handleInput;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
mockStdin.on.mockImplementation((event, callback) => {
|
|
155
|
+
if (event === 'data') {
|
|
156
|
+
handleInput = callback;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('doit naviguer vers le haut avec flèche haut', () => {
|
|
162
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
163
|
+
|
|
164
|
+
const initialLogCount = console.log.mock.calls.length;
|
|
165
|
+
handleInput('\u001B[A'); // UP Arrow
|
|
166
|
+
|
|
167
|
+
expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
|
|
168
|
+
expect(console.clear).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('doit naviguer vers le bas avec flèche bas', () => {
|
|
172
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
173
|
+
|
|
174
|
+
const initialLogCount = console.log.mock.calls.length;
|
|
175
|
+
handleInput('\u001B[B'); // DOWN Arrow
|
|
176
|
+
|
|
177
|
+
expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
|
|
178
|
+
expect(console.clear).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('doit boucler navigation vers le haut depuis première option', () => {
|
|
182
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
183
|
+
|
|
184
|
+
console.clear.mockClear();
|
|
185
|
+
handleInput('\u001B[A'); // UP Arrow from first item
|
|
186
|
+
|
|
187
|
+
expect(console.clear).toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('doit boucler navigation vers le bas depuis dernière option', () => {
|
|
191
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
192
|
+
|
|
193
|
+
// Navigate to last item
|
|
194
|
+
handleInput('\u001B[B');
|
|
195
|
+
handleInput('\u001B[B');
|
|
196
|
+
console.clear.mockClear();
|
|
197
|
+
|
|
198
|
+
// Try to go down from last item
|
|
199
|
+
handleInput('\u001B[B');
|
|
200
|
+
|
|
201
|
+
expect(console.clear).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('doit sélectionner option avec flèche droite', () => {
|
|
205
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
206
|
+
|
|
207
|
+
const initialLogCount = console.log.mock.calls.length;
|
|
208
|
+
handleInput('\u001B[C'); // Right Arrow
|
|
209
|
+
|
|
210
|
+
expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
|
|
211
|
+
expect(console.clear).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('doit permettre sélection multiple de la même option', () => {
|
|
215
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
216
|
+
|
|
217
|
+
handleInput('\u001B[C'); // SELECT Option 1
|
|
218
|
+
console.clear.mockClear();
|
|
219
|
+
handleInput('\u001B[C'); // SELECT Option 1 again
|
|
220
|
+
|
|
221
|
+
expect(console.clear).toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('doit désélectionner option avec flèche gauche', () => {
|
|
225
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [0]);
|
|
226
|
+
|
|
227
|
+
const initialLogCount = console.log.mock.calls.length;
|
|
228
|
+
handleInput('\u001B[D'); // Left Arrow
|
|
229
|
+
|
|
230
|
+
expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
|
|
231
|
+
expect(console.clear).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('ne doit rien faire si désélection sur option non sélectionnée', () => {
|
|
235
|
+
interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
236
|
+
|
|
237
|
+
const initialLogCount = console.log.mock.calls.length;
|
|
238
|
+
handleInput('\u001B[D'); // Left Arrow on unselected item
|
|
239
|
+
|
|
240
|
+
expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('doit quitter avec Ctrl+C', () => {
|
|
244
|
+
// Mock process.exit to throw to simulate actual exit
|
|
245
|
+
const exitMock = jest.spyOn(process, 'exit').mockImplementation((code) => {
|
|
246
|
+
throw new Error('Process exit called');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
250
|
+
|
|
251
|
+
expect(() => {
|
|
252
|
+
handleInput('\u0003'); // Ctrl+C
|
|
253
|
+
}).toThrow('Process exit called');
|
|
254
|
+
|
|
255
|
+
expect(exitMock).toHaveBeenCalled();
|
|
256
|
+
|
|
257
|
+
// Restore the mock to the default behavior for other tests
|
|
258
|
+
exitMock.mockRestore();
|
|
259
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('doit appeler process.exit avec Ctrl+C sans throw', () => {
|
|
263
|
+
const exitMock = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
264
|
+
|
|
265
|
+
interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
266
|
+
handleInput('\u0003');
|
|
267
|
+
|
|
268
|
+
expect(exitMock).toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('doit ignorer une touche inconnue', () => {
|
|
272
|
+
interactiveMenu('Test', ['Option 1', 'Option 2']);
|
|
273
|
+
|
|
274
|
+
const clearCount = console.clear.mock.calls.length;
|
|
275
|
+
handleInput('x');
|
|
276
|
+
|
|
277
|
+
expect(console.clear.mock.calls.length).toBe(clearCount);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('validation avec Entrée', () => {
|
|
282
|
+
let handleInput;
|
|
283
|
+
|
|
284
|
+
beforeEach(() => {
|
|
285
|
+
mockStdin.on.mockImplementation((event, callback) => {
|
|
286
|
+
if (event === 'data') {
|
|
287
|
+
handleInput = callback;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('doit retourner option courante si aucune sélection', async () => {
|
|
293
|
+
const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
294
|
+
|
|
295
|
+
handleInput('\r'); // ENTER
|
|
296
|
+
|
|
297
|
+
const result = await promise;
|
|
298
|
+
expect(result).toEqual(['Option 1']);
|
|
299
|
+
expect(mockStdin.setRawMode).toHaveBeenCalledWith(false);
|
|
300
|
+
expect(mockStdin.pause).toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('doit retourner options sélectionnées', async () => {
|
|
304
|
+
const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [1, 2]);
|
|
305
|
+
|
|
306
|
+
handleInput('\r'); // ENTER
|
|
307
|
+
|
|
308
|
+
const result = await promise;
|
|
309
|
+
expect(result).toEqual(['Option 2', 'Option 3']);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('doit nettoyer listeners après validation', async () => {
|
|
313
|
+
const promise = interactiveMenu('Test', ['Option 1']);
|
|
314
|
+
|
|
315
|
+
handleInput('\r'); // ENTER
|
|
316
|
+
|
|
317
|
+
await promise;
|
|
318
|
+
expect(mockStdin.removeListener).toHaveBeenCalledWith('data', handleInput);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('doit retourner sélections après navigation et sélection', async () => {
|
|
322
|
+
const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
323
|
+
|
|
324
|
+
handleInput('\u001B[B'); // DOWN to Option 2
|
|
325
|
+
handleInput('\u001B[C'); // SELECT Option 2
|
|
326
|
+
handleInput('\u001B[B'); // DOWN to Option 3
|
|
327
|
+
handleInput('\u001B[C'); // SELECT Option 3
|
|
328
|
+
handleInput('\r'); // ENTER
|
|
329
|
+
|
|
330
|
+
const result = await promise;
|
|
331
|
+
expect(result).toEqual(['Option 2', 'Option 3']);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('doit gérer navigation puis validation sans sélection', async () => {
|
|
335
|
+
const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
|
|
336
|
+
|
|
337
|
+
handleInput('\u001B[B'); // DOWN to Option 2
|
|
338
|
+
handleInput('\u001B[B'); // DOWN to Option 3
|
|
339
|
+
handleInput('\r'); // ENTER without selecting
|
|
340
|
+
|
|
341
|
+
const result = await promise;
|
|
342
|
+
expect(result).toEqual(['Option 3']);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('gestion stdin', () => {
|
|
347
|
+
test('doit configurer stdin en mode raw', () => {
|
|
348
|
+
interactiveMenu('Test', ['Option 1']);
|
|
349
|
+
|
|
350
|
+
expect(mockStdin.setRawMode).toHaveBeenCalledWith(true);
|
|
351
|
+
expect(mockStdin.resume).toHaveBeenCalled();
|
|
352
|
+
expect(mockStdin.setEncoding).toHaveBeenCalledWith('utf8');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('doit enregistrer listener data', () => {
|
|
356
|
+
interactiveMenu('Test', ['Option 1']);
|
|
357
|
+
|
|
358
|
+
expect(mockStdin.on).toHaveBeenCalledWith('data', expect.any(Function));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('doit restaurer stdin après validation', async () => {
|
|
362
|
+
let handleInput;
|
|
363
|
+
mockStdin.on.mockImplementation((event, callback) => {
|
|
364
|
+
if (event === 'data') handleInput = callback;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const promise = interactiveMenu('Test', ['Option 1']);
|
|
368
|
+
handleInput('\r');
|
|
369
|
+
|
|
370
|
+
await promise;
|
|
371
|
+
|
|
372
|
+
expect(mockStdin.setRawMode).toHaveBeenCalledWith(false);
|
|
373
|
+
expect(mockStdin.pause).toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('scénarios complexes', () => {
|
|
378
|
+
let handleInput;
|
|
379
|
+
|
|
380
|
+
beforeEach(() => {
|
|
381
|
+
mockStdin.on.mockImplementation((event, callback) => {
|
|
382
|
+
if (event === 'data') handleInput = callback;
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('doit gérer sélection multiple puis désélection', async () => {
|
|
387
|
+
const promise = interactiveMenu('Test', ['A', 'B', 'C']);
|
|
388
|
+
|
|
389
|
+
handleInput('\u001B[C'); // SELECT A
|
|
390
|
+
handleInput('\u001B[B'); // DOWN to B
|
|
391
|
+
handleInput('\u001B[C'); // SELECT B
|
|
392
|
+
handleInput('\u001B[D'); // DESELECT B
|
|
393
|
+
handleInput('\r'); // ENTER
|
|
394
|
+
|
|
395
|
+
const result = await promise;
|
|
396
|
+
expect(result).toEqual(['A']);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('doit gérer navigation circulaire complète', () => {
|
|
400
|
+
interactiveMenu('Test', ['A', 'B', 'C']);
|
|
401
|
+
|
|
402
|
+
// Navigate through all items and back
|
|
403
|
+
handleInput('\u001B[B'); // A -> B
|
|
404
|
+
handleInput('\u001B[B'); // B -> C
|
|
405
|
+
handleInput('\u001B[B'); // C -> A
|
|
406
|
+
handleInput('\u001B[A'); // A -> C
|
|
407
|
+
handleInput('\u001B[A'); // C -> B
|
|
408
|
+
handleInput('\u001B[A'); // B -> A
|
|
409
|
+
|
|
410
|
+
expect(console.clear).toHaveBeenCalled();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('doit gérer présélections puis modifications', async () => {
|
|
414
|
+
const promise = interactiveMenu('Test', ['A', 'B', 'C'], [0, 1]);
|
|
415
|
+
|
|
416
|
+
handleInput('\u001B[D'); // DESELECT A
|
|
417
|
+
handleInput('\u001B[B'); // DOWN to B
|
|
418
|
+
handleInput('\u001B[B'); // DOWN to C
|
|
419
|
+
handleInput('\u001B[C'); // SELECT C
|
|
420
|
+
handleInput('\r'); // ENTER
|
|
421
|
+
|
|
422
|
+
const result = await promise;
|
|
423
|
+
expect(result).toEqual(['B', 'C']);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|