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';
− const result = floodFillFn(grid, 32, 32, 0, 0, '#FF0000');
− assertEqual(result[0], '#FF0000', 'Hex string should normalize to uppercase');
140 grid[1] = '#FF0000';
141 const result = floodFillFn(grid, 32, 32, 0, 0, '#00FF00');
142 assertEqual(result[0], '#00FF00', 'Pixel 0 should be filled');
143 assertEqual(result[1], '#00FF00', 'Pixel 1 with uppercase hex should also be recognized as same color and filled');
144 });
145
146
147
148
149
150 runTest('Undo Stack', 'Initial state should have canUndo=false and canRedo=false', () => {
151 const undoMgr = new UndoManagerClass(50);
152 assertEqual(undoMgr.canUndo(), false, 'canUndo should initially be false');
153 assertEqual(undoMgr.canRedo(), false, 'canRedo should initially be false');
154 });
155
156 runTest('Undo Stack', 'Push states and perform undo/redo', () => {
157 const undoMgr = new UndoManagerClass(50);
158 const state0 = createBlankGrid(32, 32);
159 const state1 = [...state0];
160 state1[0] = '#FF0000';
161 const state2 = [...state1];
162 state2[1] = '#00FF00';
163
164 undoMgr.push(state0);
165 undoMgr.push(state1);
166 undoMgr.push(state2);
167
168 assertEqual(undoMgr.canUndo(), true, 'canUndo should be true after pushes');
169 assertEqual(undoMgr.canRedo(), false, 'canRedo should be false at latest state');
170
171
172 const popped1 = undoMgr.undo();
173 assertEqual(popped1[0], '#FF0000', 'State 1 index 0 should be #FF0000');
174 assertEqual(popped1[1], null, 'State 1 index 1 should be null');
175 assertEqual(undoMgr.canRedo(), true, 'canRedo should be true after undo');
176
177
178 const popped0 = undoMgr.undo();
179 assertEqual(popped0[0], null, 'State 0 index 0 should be null');
180
181
182 const redone1 = undoMgr.redo();
183 assertEqual(redone1[0], '#FF0000', 'Redone State 1 index 0 should be #FF0000');
184
185
186 const redone2 = undoMgr.redo();
187 assertEqual(redone2[1], '#00FF00', 'Redone State 2 index 1 should be #00FF00');
188 });
189
190 runTest('Undo Stack', 'New edit after undo should truncate redo stack', () => {
191 const undoMgr = new UndoManagerClass(50);
192 const state0 = createBlankGrid(32, 32);
193 const state1 = [...state0];
194 state1[0] = '#FF0000';
195 const state2 = [...state1];
196 state2[1] = '#00FF00';
197
198 undoMgr.push(state0);
199 undoMgr.push(state1);
200 undoMgr.push(state2);
201
202
203 undoMgr.undo();
204 assertEqual(undoMgr.canRedo(), true, 'canRedo should be true');
205
206
207 const state3 = [...state1];
208 state3[2] = '#0000FF';
209 undoMgr.push(state3);
210
211 assertEqual(undoMgr.canRedo(), false, 'canRedo should be cleared after new push');
212
213
214 const current = undoMgr.undo();
215 assertEqual(current[0], '#FF0000', 'Index 0 should be #FF0000');
216 assertEqual(current[2], null, 'Index 2 should be null');
217 });
218
219 runTest('Undo Stack', 'Should respect maximum history size limit', () => {
220 const maxSize = 3;
221 const undoMgr = new UndoManagerClass(maxSize);
222
223 undoMgr.push([1]);
224 undoMgr.push([2]);
225 undoMgr.push([3]);
226 undoMgr.push([4]);
227
228 assertEqual(undoMgr.stack.length, 3, 'Stack length should be capped at maxSize');
229 assertEqual(undoMgr.stack[0][0], 2, 'Oldest state should now be [2]');
230 });
231
232 runTest('Undo Stack', 'Clear should reset undo manager', () => {
233 const undoMgr = new UndoManagerClass(50);
234 undoMgr.push([1]);
235 undoMgr.push([2]);
236 undoMgr.clear();
237 assertEqual(undoMgr.canUndo(), false, 'canUndo should be false after clear');
238 assertEqual(undoMgr.canRedo(), false, 'canRedo should be false after clear');
239 });
240
241
242
243
244
245 runTest('JSON Export/Import', 'Should serialize grid to JSON and parse back correctly', () => {
246 const grid = createBlankGrid(32, 32);
247 grid[0] = '#FF0000';
248 grid[1023] = '#00FF00';
249
250 const jsonStr = exportToJSONFn(grid, 32, 32);
251 assert(typeof jsonStr === 'string', 'exportToJSON should return string');
252
253 const imported = importFromJSONFn(jsonStr, 32, 32);
254 assertEqual(imported.width, 32, 'Imported width should be 32');
255 assertEqual(imported.height, 32, 'Imported height should be 32');
256 assertEqual(imported.pixels[0], '#FF0000', 'Pixel 0 should be restored');
257 assertEqual(imported.pixels[1023], '#00FF00', 'Pixel 1023 should be restored');
258 });
259
260 runTest('JSON Export/Import', 'Should throw descriptive error for invalid input', () => {
261 let errorCaught = false;
262 try {
263 importFromJSONFn('{"invalid": true}', 32, 32);
264 } catch (e) {
265 errorCaught = true;
266 }
267 assert(errorCaught, 'Should throw error for missing pixels array');
268 });
269
270 return testResults;
271}
272
273
274if (typeof process !== 'undefined' && process.env) {
275 console.log('\n========================================');
276 console.log(' RUNNING HEADLESS TESTS (Node.js)');
277 console.log('========================================\n');
278
279 const results = executeAllTests();
280 let passedCount = 0;
281 let failedCount = 0;
282
283 results.forEach(r => {
284 if (r.passed) {
285 passedCount++;
286 console.log(` \x1b[32m✓ PASS\x1b[0m [${r.suite}] ${r.name} (${r.duration}ms)`);
287 } else {
288 failedCount++;
289 console.log(` \x1b[31m✗ FAIL\x1b[0m [${r.suite}] ${r.name}`);
290 console.log(` Error: ${r.errorMsg}`);
291 }
292 });
293
294 console.log('\n----------------------------------------');
295 console.log(` Summary: ${passedCount} passed, ${failedCount} failed (${results.length} total)`);
296 console.log('----------------------------------------\n');
297
298 if (failedCount > 0) {
299 process.exit(1);
300 } else {
301 process.exit(0);
302 }
303}
304
305
306if (typeof window !== 'undefined') {
307 window.executeAllTests = executeAllTests;
308}
309
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.