1
2
3(function (global) {
4 'use strict';
5
6 const isNode = typeof document === 'undefined';
7 const core = isNode ? require('./editor.js') : global.PixelEditorCore;
8 const {
9 GRID_SIZE, createGrid, cloneGrid, gridsEqual,
10 floodFill, UndoStack, serialize, deserialize,
11 } = core;
12
13 const results = [];
14
15 function test(name, fn) {
16 try {
17 fn();
18 results.push({ name, ok: true });
19 } catch (err) {
20 results.push({ name, ok: false, message: err && err.message ? err.message : String(err) });
21 }
22 }
23
24 function assert(cond, message) {
25 if (!cond) throw new Error(message || 'expected condition to hold');
26 }
27
28 function assertEqual(actual, expected, message) {
29 if (actual !== expected) {
30 throw new Error(`${message || 'values differ'} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
31 }
32 }
33
34
35 function gridFrom(rows, colors) {
36 return rows.map((row) => row.split('').map((ch) => (ch === '.' ? null : colors[ch])));
37 }
38
39 const R = '#ff0000';
40 const G = '#00ff00';
41 const B = '#0000ff';
42
43
44
45 test('floodFill fills a connected region and returns the changed count', () => {
46 const g = gridFrom(['RR.', 'R..', '...'], { R });
47 const changed = floodFill(g, 0, 0, B);
48 assertEqual(changed, 3);
49 assertEqual(g[0][0], B);
50 assertEqual(g[0][1], B);
51 assertEqual(g[1][0], B);
52 assertEqual(g[0][2], null, 'cells outside the region stay untouched');
53 });
54
55 test('floodFill does not cross a boundary of another color', () => {
56 const g = gridFrom(['..G..', '..G..', '..G..'], { G });
57 floodFill(g, 0, 0, R);
58 assertEqual(g[0][0], R);
59 assertEqual(g[0][4], null, 'right side of the wall must stay empty');
60 assertEqual(g[1][2], G, 'the wall must keep its color');
61 });
62
63 test('floodFill with the same color is a no-op', () => {
64 const g = gridFrom(['RR', 'RR'], { R });
65 const before = cloneGrid(g);
66 assertEqual(floodFill(g, 0, 0, R), 0);
67 assert(gridsEqual(g, before), 'grid must be unchanged');
68 });
69
70 test('floodFill outside the grid changes nothing', () => {
71 const g = createGrid(4);
72 assertEqual(floodFill(g, -1, 0, R), 0);
73 assertEqual(floodFill(g, 0, 99, R), 0);
74 assert(gridsEqual(g, createGrid(4)), 'grid must be unchanged');
75 });
76
77 test('floodFill uses 4-connectivity (no diagonal leaks)', () => {
78 const g = gridFrom(['R.', '.R'], { R });
79 floodFill(g, 0, 0, B);
80 assertEqual(g[0][0], B);
81 assertEqual(g[1][1], R, 'the diagonal cell must not be filled');
82 });
83
84 test('floodFill fills an entire empty 32x32 grid', () => {
85 const g = createGrid();
86 assertEqual(floodFill(g, 16, 16, R), GRID_SIZE * GRID_SIZE);
87 assert(g.every((row) => row.every((v) => v === R)), 'every cell must be filled');
88 });
89
90 test('floodFill fills a ring-enclosed pocket without escaping', () => {
91 const g = gridFrom([
92 'GGGGG',
93 'G...G',
94 'G.G.G',
95 'G...G',
96 'GGGGG',
97 ], { G });
98 const changed = floodFill(g, 1, 1, R);
99 assertEqual(changed, 8, 'only the 8 interior cells around the center');
100 assertEqual(g[2][2], G, 'the center pixel keeps its color');
101 assertEqual(g[0][0], G, 'the border keeps its color');
102 });
103
104 test('floodFill can fill transparent with a color and erase back to transparent', () => {
105 const g = createGrid(3);
106 assertEqual(floodFill(g, 0, 0, R), 9);
107 assertEqual(floodFill(g, 2, 2, null), 9);
108 assert(gridsEqual(g, createGrid(3)), 'grid must be fully transparent again');
109 });
110
111
112
113 test('undo returns the previous snapshot', () => {
114 const h = new UndoStack();
115 h.push('v1');
116 assertEqual(h.undo('v2'), 'v1');
117 });
118
119 test('redo restores the undone state', () => {
120 const h = new UndoStack();
121 h.push('v1');
122 const prev = h.undo('v2');
123 assertEqual(prev, 'v1');
124 assertEqual(h.redo(prev), 'v2');
125 });
126
127 test('undo on an empty stack returns null', () => {
128 const h = new UndoStack();
129 assertEqual(h.undo('current'), null);
130 assertEqual(h.canUndo(), false);
131 });
132
133 test('redo on an empty stack returns null', () => {
134 const h = new UndoStack();
135 h.push('v1');
136 assertEqual(h.redo('current'), null);
137 assertEqual(h.canRedo(), false);
138 });
139
140 test('a new edit clears the redo stack', () => {
141 const h = new UndoStack();
142 h.push('v1');
143 h.undo('v2');
144 assertEqual(h.canRedo(), true);
145 h.push('v1-diverged');
146 assertEqual(h.canRedo(), false);
147 assertEqual(h.redo('x'), null);
148 });
149
150 test('undo/redo walk a multi-step history in order', () => {
151 const h = new UndoStack();
152 h.push('v1');
153 h.push('v2');
154 h.push('v3');
155 let cur = 'v4';
156 cur = h.undo(cur); assertEqual(cur, 'v3');
157 cur = h.undo(cur); assertEqual(cur, 'v2');
158 cur = h.redo(cur); assertEqual(cur, 'v3');
159 cur = h.redo(cur); assertEqual(cur, 'v4');
160 assertEqual(h.canRedo(), false);
161 cur = h.undo(cur); assertEqual(cur, 'v3');
162 });
163
164 test('the stack drops its oldest entries beyond the limit', () => {
165 const h = new UndoStack(3);
166 ['v1', 'v2', 'v3', 'v4'].forEach((v) => h.push(v));
167 assertEqual(h.undo('v5'), 'v4');
168 assertEqual(h.undo('v4'), 'v3');
169 assertEqual(h.undo('v3'), 'v2');
170 assertEqual(h.undo('v2'), null, 'v1 was trimmed by the limit');
171 });
172
173 test('undo snapshots hold grid states, not live references', () => {
174 const h = new UndoStack();
175 const g = createGrid(2);
176 h.push(cloneGrid(g));
177 g[0][0] = R;
178 const prev = h.undo(cloneGrid(g));
179 assertEqual(prev[0][0], null, 'snapshot must be unaffected by later edits');
180 });
181
182
183
184 test('serialize/deserialize round-trips a grid', () => {
185 const g = createGrid();
186 g[0][0] = R;
187 g[31][31] = '#ABCDEF';
188 const out = deserialize(serialize(g));
189 assertEqual(out[0][0], R);
190 assertEqual(out[31][31], '#abcdef', 'colors are normalized to lowercase');
191 assertEqual(out[5][5], null);
192 });
193
194 test('deserialize rejects malformed documents', () => {
195 let threw = false;
196 try {
197 deserialize('{"pixels": [[1, 2], [3]]}');
198 } catch (err) {
199 threw = true;
200 }
201 assert(threw, 'expected an error for a bad payload');
202 });
203
204
205
206 const failures = results.filter((r) => !r.ok);
207
208 if (isNode) {
209 for (const r of results) {
210 console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.name}${r.ok ? '' : `\n ${r.message}`}`);
211 }
212 console.log(`\n${results.length - failures.length}/${results.length} passed`);
213 process.exitCode = failures.length ? 1 : 0;
214 } else {
215 const list = document.getElementById('results');
216 for (const r of results) {
217 const li = document.createElement('li');
218 li.className = r.ok ? 'pass' : 'fail';
219 li.textContent = `${r.ok ? '✓' : '✗'} ${r.name}${r.ok ? '' : ` — ${r.message}`}`;
220 list.appendChild(li);
221 }
222 const summary = document.getElementById('summary');
223 summary.textContent = `${results.length - failures.length}/${results.length} passed`;
224 summary.className = failures.length ? 'fail' : 'pass';
225 }
226})(typeof globalThis !== 'undefined' ? globalThis : this);
227
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.