1
2
3
4
5
6
7
8let floodFillFn, UndoManagerClass, exportToJSONFn, importFromJSONFn;
9
10if (typeof require !== 'undefined') {
11 const editor = require('./editor.js');
12 floodFillFn = editor.floodFill;
13 UndoManagerClass = editor.UndoManager;
14 exportToJSONFn = editor.exportToJSON;
15 importFromJSONFn = editor.importFromJSON;
16} else if (typeof window !== 'undefined') {
17 floodFillFn = window.floodFill;
18 UndoManagerClass = window.UndoManager;
19 exportToJSONFn = window.exportToJSON;
20 importFromJSONFn = window.importFromJSON;
21}
22
23const testResults = [];
24
25function runTest(suite, name, fn) {
26 const startTime = Date.now();
27 let passed = false;
28 let errorMsg = null;
29
30 try {
31 fn();
32 passed = true;
33 } catch (err) {
34 passed = false;
35 errorMsg = err.message || String(err);
36 }
37
38 const duration = Date.now() - startTime;
39 const result = { suite, name, passed, errorMsg, duration };
40 testResults.push(result);
41 return result;
42}
43
44function assert(condition, message) {
45 if (!condition) {
46 throw new Error(message || 'Assertion failed');
47 }
48}
49
50function assertEqual(actual, expected, message) {
51 if (actual !== expected) {
52 throw new Error(`${message || 'Assertion failed'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
53 }
54}
55
56function assertDeepEqual(actual, expected, message) {
57 if (JSON.stringify(actual) !== JSON.stringify(expected)) {
58 throw new Error(`${message || 'Deep equality failed'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
59 }
60}
61
62
63function createBlankGrid(w = 32, h = 32) {
64 return new Array(w * h).fill(null);
65}
66
67
68function executeAllTests() {
69 testResults.length = 0;
70
71
72
73
74
75 runTest('Flood Fill', 'Should fill entire empty 32x32 grid', () => {
76 const grid = createBlankGrid(32, 32);
77 const result = floodFillFn(grid, 32, 32, 0, 0, '#FF0000');
78 assertEqual(result.length, 1024, 'Grid size should remain 1024');
79 const filledCount = result.filter(c => c === '#FF0000').length;
80 assertEqual(filledCount, 1024, 'All pixels in empty grid should be filled');
81 });
82
83 runTest('Flood Fill', 'Should fill only enclosed box interior', () => {
84 const grid = createBlankGrid(32, 32);
85
86 for (let x = 10; x <= 14; x++) {
87 grid[10 * 32 + x] = '#000000';
88 grid[14 * 32 + x] = '#000000';
89 }
90 for (let y = 10; y <= 14; y++) {
91 grid[y * 32 + 10] = '#000000';
92 grid[y * 32 + 14] = '#000000';
93 }
94
95
96 const result = floodFillFn(grid, 32, 32, 12, 12, '#00FF00');
97
98
99 for (let y = 11; y <= 13; y++) {
100 for (let x = 11; x <= 13; x++) {
101 assertEqual(result[y * 32 + x], '#00FF00', `Pixel at (${x},${y}) should be filled`);
102 }
103 }
104
105
106 assertEqual(result[10 * 32 + 10], '#000000', 'Top-left corner of box should remain black');
107
108
109 assertEqual(result[0], null, 'Outside of box should remain null');
110 });
111
112 runTest('Flood Fill', 'Should handle target color equal to fill color gracefully', () => {
113 const grid = createBlankGrid(32, 32);
114 grid[0] = '#FF0000';
115 const result = floodFillFn(grid, 32, 32, 0, 0, '#FF0000');
116 assertEqual(result[0], '#FF0000', 'Pixel color should remain unchanged');
117 });
118
119 runTest('Flood Fill', 'Should fill corner and edge pixels without boundary overflow', () => {
120 const grid = createBlankGrid(32, 32);
121
122 const result = floodFillFn(grid, 32, 32, 31, 31, '#0000FF');
123 assertEqual(result[31 * 32 + 31], '#0000FF', 'Bottom-right corner should be filled');
124 });
125
126 runTest('Flood Fill', 'Should support flood filling with null (erasing connected color)', () => {
127 const grid = createBlankGrid(32, 32);
128
129 for (let i = 0; i < grid.length; i++) grid[i] = '#AAAAAA';
130
131
132 const result = floodFillFn(grid, 32, 32, 15, 15, null);
133 const nullCount = result.filter(c => c === null).length;
134 assertEqual(nullCount, 1024, 'All pixels should be erased to null');
135 });
136
137 runTest('Flood Fill', 'Should be case-insensitive for hex color strings', () => {
138 const grid = createBlankGrid(32, 32);
139 grid[0] = '#ff0000';
140 const result = floodFillFn(grid, 32, 32, 0, 0, '#FF0000');
141 assertEqual(result[0], '#FF0000', 'Hex string should normalize to uppercase');
142 });
143
144
145
146
147
148 runTest('Undo Stack', 'Initial state should have canUndo=false and canRedo=false', () => {
149 const undoMgr = new UndoManagerClass(50);
150 assertEqual(undoMgr.canUndo(), false, 'canUndo should initially be false');
151 assertEqual(undoMgr.canRedo(), false, 'canRedo should initially be false');
152 });
153
154 runTest('Undo Stack', 'Push states and perform undo/redo', () => {
155 const undoMgr = new UndoManagerClass(50);
156 const state0 = createBlankGrid(32, 32);
157 const state1 = [...state0];
158 state1[0] = '#FF0000';
159 const state2 = [...state1];
160 state2[1] = '#00FF00';
161
162 undoMgr.push(state0);
163 undoMgr.push(state1);
164 undoMgr.push(state2);
165
166 assertEqual(undoMgr.canUndo(), true, 'canUndo should be true after pushes');
167 assertEqual(undoMgr.canRedo(), false, 'canRedo should be false at latest state');
168
169
170 const popped1 = undoMgr.undo();
171 assertEqual(popped1[0], '#FF0000', 'State 1 index 0 should be #FF0000');
172 assertEqual(popped1[1], null, 'State 1 index 1 should be null');
173 assertEqual(undoMgr.canRedo(), true, 'canRedo should be true after undo');
174
175
176 const popped0 = undoMgr.undo();
177 assertEqual(popped0[0], null, 'State 0 index 0 should be null');
178
179
180 const redone1 = undoMgr.redo();
181 assertEqual(redone1[0], '#FF0000', 'Redone State 1 index 0 should be #FF0000');
182
183
184 const redone2 = undoMgr.redo();
185 assertEqual(redone2[1], '#00FF00', 'Redone State 2 index 1 should be #00FF00');
186 });
187
188 runTest('Undo Stack', 'New edit after undo should truncate redo stack', () => {
189 const undoMgr = new UndoManagerClass(50);
190 const state0 = createBlankGrid(32, 32);
191 const state1 = [...state0];
192 state1[0] = '#FF0000';
193 const state2 = [...state1];
194 state2[1] = '#00FF00';
195
196 undoMgr.push(state0);
197 undoMgr.push(state1);
198 undoMgr.push(state2);
199
200
201 undoMgr.undo();
202 assertEqual(undoMgr.canRedo(), true, 'canRedo should be true');
203
204
205 const state3 = [...state1];
206 state3[2] = '#0000FF';
207 undoMgr.push(state3);
208
209 assertEqual(undoMgr.canRedo(), false, 'canRedo should be cleared after new push');
210
211
212 const current = undoMgr.undo();
213 assertEqual(current[0], '#FF0000', 'Index 0 should be #FF0000');
214 assertEqual(current[2], null, 'Index 2 should be null');
215 });
216
217 runTest('Undo Stack', 'Should respect maximum history size limit', () => {
218 const maxSize = 3;
219 const undoMgr = new UndoManagerClass(maxSize);
220
221 undoMgr.push([1]);
222 undoMgr.push([2]);
223 undoMgr.push([3]);
224 undoMgr.push([4]);
225
226 assertEqual(undoMgr.stack.length, 3, 'Stack length should be capped at maxSize');
227 assertEqual(undoMgr.stack[0][0], 2, 'Oldest state should now be [2]');
228 });
229
230 runTest('Undo Stack', 'Clear should reset undo manager', () => {
231 const undoMgr = new UndoManagerClass(50);
232 undoMgr.push([1]);
233 undoMgr.push([2]);
234 undoMgr.clear();
235 assertEqual(undoMgr.canUndo(), false, 'canUndo should be false after clear');
236 assertEqual(undoMgr.canRedo(), false, 'canRedo should be false after clear');
237 });
238
239
240
241
242
243 runTest('JSON Export/Import', 'Should serialize grid to JSON and parse back correctly', () => {
244 const grid = createBlankGrid(32, 32);
245 grid[0] = '#FF0000';
246 grid[1023] = '#00FF00';
247
248 const jsonStr = exportToJSONFn(grid, 32, 32);
249 assert(typeof jsonStr === 'string', 'exportToJSON should return string');
250
251 const imported = importFromJSONFn(jsonStr, 32, 32);
252 assertEqual(imported.width, 32, 'Imported width should be 32');
253 assertEqual(imported.height, 32, 'Imported height should be 32');
254 assertEqual(imported.pixels[0], '#FF0000', 'Pixel 0 should be restored');
255 assertEqual(imported.pixels[1023], '#00FF00', 'Pixel 1023 should be restored');
256 });
257
258 runTest('JSON Export/Import', 'Should throw descriptive error for invalid input', () => {
259 let errorCaught = false;
260 try {
261 importFromJSONFn('{"invalid": true}', 32, 32);
262 } catch (e) {
263 errorCaught = true;
264 }
265 assert(errorCaught, 'Should throw error for missing pixels array');
266 });
267
268 return testResults;
269}
270
271
272if (typeof process !== 'undefined' && process.env) {
273 console.log('\n========================================');
274 console.log(' RUNNING HEADLESS TESTS (Node.js)');
275 console.log('========================================\n');
276
277 const results = executeAllTests();
278 let passedCount = 0;
279 let failedCount = 0;
280
281 results.forEach(r => {
282 if (r.passed) {
283 passedCount++;
284 console.log(` \x1b[32m✓ PASS\x1b[0m [${r.suite}] ${r.name} (${r.duration}ms)`);
285 } else {
286 failedCount++;
287 console.log(` \x1b[31m✗ FAIL\x1b[0m [${r.suite}] ${r.name}`);
288 console.log(` Error: ${r.errorMsg}`);
289 }
290 });
291
292 console.log('\n----------------------------------------');
293 console.log(` Summary: ${passedCount} passed, ${failedCount} failed (${results.length} total)`);
294 console.log('----------------------------------------\n');
295
296 if (failedCount > 0) {
297 process.exit(1);
298 } else {
299 process.exit(0);
300 }
301}
302
303
304if (typeof window !== 'undefined') {
305 window.executeAllTests = executeAllTests;
306}
307
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.