Full document, spreadsheet, slideshow, and diagram tooling
1import { describe, it, expect } from 'vitest';
2import {
3 createDeck,
4 addSlide,
5 removeSlide,
6 duplicateSlide,
7 goToSlide,
8 moveSlide,
9 addElement,
10 removeElement,
11 currentSlide,
12 slideCount,
13 elementCount,
14} from '../src/slides/canvas-engine.js';
15
16// =====================================================================
17// 1. ADD SLIDE — currentSlide index behavior
18// =====================================================================
19
20describe('Canvas engine integrity — addSlide updates', () => {
21 it('adding a slide does not change currentSlide index', () => {
22 let deck = createDeck();
23 expect(deck.currentSlide).toBe(0);
24
25 deck = addSlide(deck);
26 // currentSlide should still be 0 (pointing to the original first slide)
27 expect(deck.currentSlide).toBe(0);
28 expect(slideCount(deck)).toBe(2);
29 });
30
31 it('adding a slide at index 0 pushes the original slide to index 1', () => {
32 let deck = createDeck();
33 const originalId = deck.slides[0]!.id;
34
35 deck = addSlide(deck, 0);
36 expect(deck.slides[1]!.id).toBe(originalId);
37 expect(slideCount(deck)).toBe(2);
38 });
39
40 it('currentSlide still points to a valid slide after adding at end', () => {
41 let deck = createDeck();
42 deck = goToSlide(deck, 0);
43 deck = addSlide(deck);
44 deck = addSlide(deck);
45
46 expect(deck.currentSlide).toBeLessThan(slideCount(deck));
47 expect(currentSlide(deck)).toBeDefined();
48 expect(currentSlide(deck).id).toBeTruthy();
49 });
50});
51
52// =====================================================================
53// 2. DELETE ONLY SLIDE — cannot delete the last slide
54// =====================================================================
55
56describe('Canvas engine integrity — delete only slide', () => {
57 it('removing the only slide returns unchanged state', () => {
58 const deck = createDeck();
59 expect(slideCount(deck)).toBe(1);
60
61 const updated = removeSlide(deck, 0);
62 expect(slideCount(updated)).toBe(1);
63 // State reference should be the same (no mutation)
64 expect(updated).toBe(deck);
65 });
66
67 it('after removing one of two slides, currentSlide is clamped', () => {
68 let deck = createDeck();
69 deck = addSlide(deck);
70 deck = goToSlide(deck, 1); // on the second slide
71
72 const updated = removeSlide(deck, 1);
73 expect(slideCount(updated)).toBe(1);
74 expect(updated.currentSlide).toBe(0);
75 });
76
77 it('removing the first slide when on first slide clamps to 0', () => {
78 let deck = createDeck();
79 deck = addSlide(deck);
80 deck = goToSlide(deck, 0);
81
82 const updated = removeSlide(deck, 0);
83 expect(updated.currentSlide).toBe(0);
84 expect(slideCount(updated)).toBe(1);
85 });
86});
87
88// =====================================================================
89// 3. ADD ELEMENT TO EMPTY DECK — works on the default first slide
90// =====================================================================
91
92describe('Canvas engine integrity — add element to empty deck', () => {
93 it('addElement works on the default first slide of a new deck', () => {
94 let deck = createDeck();
95 expect(elementCount(deck)).toBe(0);
96
97 deck = addElement(deck, 'text', 10, 20, 200, 100, 'Hello');
98 expect(elementCount(deck)).toBe(1);
99
100 const el = currentSlide(deck).elements[0]!;
101 expect(el.type).toBe('text');
102 expect(el.content).toBe('Hello');
103 });
104
105 it('element is added to the current slide, not all slides', () => {
106 let deck = createDeck();
107 deck = addSlide(deck);
108
109 // Current slide is 0
110 deck = addElement(deck, 'text', 0, 0, 100, 50, 'On slide 0');
111
112 expect(deck.slides[0]!.elements).toHaveLength(1);
113 expect(deck.slides[1]!.elements).toHaveLength(0);
114 });
115
116 it('adding element to second slide only affects that slide', () => {
117 let deck = createDeck();
118 deck = addSlide(deck);
119 deck = goToSlide(deck, 1);
120
121 deck = addElement(deck, 'image', 0, 0, 300, 200, 'image.png');
122
123 expect(deck.slides[0]!.elements).toHaveLength(0);
124 expect(deck.slides[1]!.elements).toHaveLength(1);
125 });
126});
127
128// =====================================================================
129// 4. ELEMENT POSITIONING AFTER SLIDE REORDER
130// =====================================================================
131
132describe('Canvas engine integrity — element positioning after reorder', () => {
133 it('moving a slide preserves its elements', () => {
134 let deck = createDeck();
135 deck = addElement(deck, 'text', 10, 20, 100, 50, 'Slide 0 text');
136 deck = addSlide(deck);
137 deck = goToSlide(deck, 1);
138 deck = addElement(deck, 'image', 30, 40, 200, 150, 'image.png');
139
140 // Slide 0 has text, Slide 1 has image
141 expect(deck.slides[0]!.elements[0]!.content).toBe('Slide 0 text');
142 expect(deck.slides[1]!.elements[0]!.content).toBe('image.png');
143
144 // Move slide 0 to position 1
145 deck = moveSlide(deck, 0, 1);
146
147 // Now slide at index 0 was the old slide 1 (image), and slide at index 1 is old slide 0 (text)
148 expect(deck.slides[0]!.elements[0]!.content).toBe('image.png');
149 expect(deck.slides[1]!.elements[0]!.content).toBe('Slide 0 text');
150 });
151
152 it('reordering slides does not lose any elements', () => {
153 let deck = createDeck();
154 deck = addElement(deck, 'text', 0, 0, 100, 50, 'el1');
155 deck = addSlide(deck);
156 deck = goToSlide(deck, 1);
157 deck = addElement(deck, 'text', 0, 0, 100, 50, 'el2');
158 deck = addSlide(deck);
159 deck = goToSlide(deck, 2);
160 deck = addElement(deck, 'text', 0, 0, 100, 50, 'el3');
161
162 // Move slide 2 to position 0
163 deck = moveSlide(deck, 2, 0);
164
165 // Count total elements across all slides
166 const totalElements = deck.slides.reduce((sum, s) => sum + s.elements.length, 0);
167 expect(totalElements).toBe(3);
168
169 // Verify all content is preserved
170 const contents = deck.slides.flatMap(s => s.elements.map(e => e.content)).sort();
171 expect(contents).toEqual(['el1', 'el2', 'el3']);
172 });
173});
174
175// =====================================================================
176// 5. DUPLICATE SLIDE PRESERVES ALL ELEMENTS
177// =====================================================================
178
179describe('Canvas engine integrity — duplicate slide', () => {
180 it('duplicated slide has same number of elements', () => {
181 let deck = createDeck();
182 deck = addElement(deck, 'text', 10, 20, 100, 50, 'Hello');
183 deck = addElement(deck, 'image', 30, 40, 200, 150, 'photo.jpg');
184 deck = addElement(deck, 'shape', 50, 60, 80, 80, '', { fill: 'red' });
185
186 deck = duplicateSlide(deck, 0);
187 expect(slideCount(deck)).toBe(2);
188 expect(deck.slides[1]!.elements).toHaveLength(3);
189 });
190
191 it('duplicated elements have different IDs from originals', () => {
192 let deck = createDeck();
193 deck = addElement(deck, 'text', 0, 0, 100, 50, 'content');
194 deck = addElement(deck, 'image', 0, 0, 100, 50, 'img');
195
196 deck = duplicateSlide(deck, 0);
197
198 const originalIds = deck.slides[0]!.elements.map(e => e.id);
199 const duplicateIds = deck.slides[1]!.elements.map(e => e.id);
200
201 for (const oid of originalIds) {
202 expect(duplicateIds).not.toContain(oid);
203 }
204 });
205
206 it('duplicated elements preserve content, type, position, and style', () => {
207 let deck = createDeck();
208 deck = addElement(deck, 'text', 10, 20, 200, 100, 'Test Content', { color: 'blue', fontSize: '24px' });
209
210 deck = duplicateSlide(deck, 0);
211
212 const original = deck.slides[0]!.elements[0]!;
213 const copy = deck.slides[1]!.elements[0]!;
214
215 expect(copy.content).toBe(original.content);
216 expect(copy.type).toBe(original.type);
217 expect(copy.x).toBe(original.x);
218 expect(copy.y).toBe(original.y);
219 expect(copy.width).toBe(original.width);
220 expect(copy.height).toBe(original.height);
221 expect(copy.style).toEqual(original.style);
222 });
223
224 it('duplicated slide preserves notes', () => {
225 let deck = createDeck();
226 deck = {
227 ...deck,
228 slides: deck.slides.map(s => ({ ...s, notes: 'Speaker notes here' })),
229 };
230
231 deck = duplicateSlide(deck, 0);
232 expect(deck.slides[1]!.notes).toBe('Speaker notes here');
233 });
234
235 it('duplicated slide preserves background color', () => {
236 let deck = createDeck();
237 deck = addSlide(deck, undefined, '#ff0000');
238
239 deck = duplicateSlide(deck, 1);
240 expect(deck.slides[2]!.background).toBe('#ff0000');
241 });
242});
243
244// =====================================================================
245// 6. LARGE NUMBER OF ELEMENTS PER SLIDE (100+)
246// =====================================================================
247
248describe('Canvas engine integrity — many elements per slide', () => {
249 it('adding 100 elements to a single slide works correctly', () => {
250 let deck = createDeck();
251 for (let i = 0; i < 100; i++) {
252 deck = addElement(deck, 'text', i * 10, i * 5, 50, 30, `Element ${i}`);
253 }
254 expect(elementCount(deck)).toBe(100);
255 });
256
257 it('all 100 elements have unique IDs', () => {
258 let deck = createDeck();
259 for (let i = 0; i < 100; i++) {
260 deck = addElement(deck, 'text', i * 10, i * 5, 50, 30);
261 }
262
263 const ids = currentSlide(deck).elements.map(e => e.id);
264 const uniqueIds = new Set(ids);
265 expect(uniqueIds.size).toBe(100);
266 });
267
268 it('removing one element from 100 leaves 99', () => {
269 let deck = createDeck();
270 for (let i = 0; i < 100; i++) {
271 deck = addElement(deck, 'text', i * 10, i * 5, 50, 30, `Element ${i}`);
272 }
273
274 const firstId = currentSlide(deck).elements[0]!.id;
275 deck = removeElement(deck, firstId);
276 expect(elementCount(deck)).toBe(99);
277
278 // Verify the removed element is really gone
279 const remaining = currentSlide(deck).elements.find(e => e.id === firstId);
280 expect(remaining).toBeUndefined();
281 });
282
283 it('element zIndex increments correctly for 100 elements', () => {
284 let deck = createDeck();
285 for (let i = 0; i < 100; i++) {
286 deck = addElement(deck, 'text', 0, 0, 50, 30);
287 }
288
289 const elements = currentSlide(deck).elements;
290 for (let i = 0; i < elements.length; i++) {
291 expect(elements[i]!.zIndex).toBe(i);
292 }
293 });
294});
295
296// =====================================================================
297// 7. EDGE CASES — goToSlide clamping, removeElement on wrong slide
298// =====================================================================
299
300describe('Canvas engine integrity — edge cases', () => {
301 it('goToSlide with negative index clamps to 0', () => {
302 const deck = createDeck();
303 const updated = goToSlide(deck, -10);
304 expect(updated.currentSlide).toBe(0);
305 });
306
307 it('goToSlide with index beyond last slide clamps to last', () => {
308 let deck = createDeck();
309 deck = addSlide(deck);
310 const updated = goToSlide(deck, 999);
311 expect(updated.currentSlide).toBe(1);
312 });
313
314 it('removeElement with non-existent ID does not change element count', () => {
315 let deck = createDeck();
316 deck = addElement(deck, 'text', 0, 0, 100, 50, 'test');
317 const before = elementCount(deck);
318
319 deck = removeElement(deck, 'non-existent-id');
320 expect(elementCount(deck)).toBe(before);
321 });
322
323 it('duplicateSlide with invalid index returns unchanged state', () => {
324 const deck = createDeck();
325 const updated = duplicateSlide(deck, 99);
326 expect(updated).toBe(deck);
327 });
328
329 it('removeSlide with out-of-range index still works gracefully', () => {
330 let deck = createDeck();
331 deck = addSlide(deck);
332 // Index 5 is out of range for a 2-slide deck
333 const updated = removeSlide(deck, 5);
334 // filter will not find index 5, so all slides remain
335 expect(slideCount(updated)).toBe(2);
336 });
337});