···11+import { test, expect } from '@playwright/test';
22+import { createNewDiagram } from './helpers';
33+44+test.describe('Diagrams - Canvas and Toolbar', () => {
55+ test.beforeEach(async ({ page }) => {
66+ await createNewDiagram(page);
77+ });
88+99+ test('page loads with SVG canvas visible', async ({ page }) => {
1010+ const canvas = page.locator('#diagram-canvas');
1111+ await expect(canvas).toBeVisible();
1212+ // Canvas should have the grid pattern background
1313+ await expect(page.locator('.diagrams-grid')).toBeVisible();
1414+ // The shape layer should exist (empty initially)
1515+ await expect(page.locator('#diagram-layer')).toBeAttached();
1616+ });
1717+1818+ test('URL matches /diagrams/{id}#{key} pattern', async ({ page }) => {
1919+ const url = page.url();
2020+ expect(url).toMatch(/\/diagrams\/[^/]+#.+/);
2121+ });
2222+2323+ test('title input shows default name', async ({ page }) => {
2424+ const title = page.locator('#diagram-title');
2525+ await expect(title).toBeVisible();
2626+ await expect(title).toHaveValue('Untitled Diagram');
2727+ });
2828+2929+ test('all tool buttons exist and are visible', async ({ page }) => {
3030+ const tools = ['select', 'rectangle', 'ellipse', 'diamond', 'text', 'freehand', 'arrow'];
3131+ for (const tool of tools) {
3232+ await expect(page.locator(`#tool-${tool}`)).toBeVisible();
3333+ }
3434+ });
3535+3636+ test('select tool is active by default', async ({ page }) => {
3737+ await expect(page.locator('#tool-select')).toHaveClass(/active/);
3838+ // Other tools should not be active
3939+ await expect(page.locator('#tool-rectangle')).not.toHaveClass(/active/);
4040+ await expect(page.locator('#tool-ellipse')).not.toHaveClass(/active/);
4141+ });
4242+4343+ test('clicking a tool button activates it', async ({ page }) => {
4444+ await page.click('#tool-rectangle');
4545+ await expect(page.locator('#tool-rectangle')).toHaveClass(/active/);
4646+ await expect(page.locator('#tool-select')).not.toHaveClass(/active/);
4747+4848+ await page.click('#tool-ellipse');
4949+ await expect(page.locator('#tool-ellipse')).toHaveClass(/active/);
5050+ await expect(page.locator('#tool-rectangle')).not.toHaveClass(/active/);
5151+ });
5252+});
5353+5454+test.describe('Diagrams - Shape Creation', () => {
5555+ test.beforeEach(async ({ page }) => {
5656+ await createNewDiagram(page);
5757+ });
5858+5959+ test('add rectangle shape by selecting tool and clicking canvas', async ({ page }) => {
6060+ // Select rectangle tool
6161+ await page.click('#tool-rectangle');
6262+ await expect(page.locator('#tool-rectangle')).toHaveClass(/active/);
6363+6464+ // Click on the canvas to place a shape
6565+ const canvas = page.locator('#diagram-canvas');
6666+ await canvas.click({ position: { x: 300, y: 300 } });
6767+6868+ // A shape should appear in the diagram layer
6969+ const shape = page.locator('.diagram-shape');
7070+ await expect(shape).toHaveCount(1);
7171+ await expect(shape).toBeVisible();
7272+7373+ // Shape should contain a rect element (rectangle kind)
7474+ await expect(shape.locator('rect')).toBeAttached();
7575+7676+ // Tool should revert to select after placing a shape
7777+ await expect(page.locator('#tool-select')).toHaveClass(/active/);
7878+ });
7979+8080+ test('add ellipse shape', async ({ page }) => {
8181+ await page.click('#tool-ellipse');
8282+ const canvas = page.locator('#diagram-canvas');
8383+ await canvas.click({ position: { x: 300, y: 300 } });
8484+8585+ const shape = page.locator('.diagram-shape');
8686+ await expect(shape).toHaveCount(1);
8787+ // Ellipse shape should contain an ellipse SVG element
8888+ await expect(shape.locator('ellipse')).toBeAttached();
8989+ });
9090+9191+ test('add diamond shape', async ({ page }) => {
9292+ await page.click('#tool-diamond');
9393+ const canvas = page.locator('#diagram-canvas');
9494+ await canvas.click({ position: { x: 300, y: 300 } });
9595+9696+ const shape = page.locator('.diagram-shape');
9797+ await expect(shape).toHaveCount(1);
9898+ // Diamond uses a polygon element
9999+ await expect(shape.locator('polygon')).toBeAttached();
100100+ });
101101+102102+ test('add text shape', async ({ page }) => {
103103+ await page.click('#tool-text');
104104+ const canvas = page.locator('#diagram-canvas');
105105+ await canvas.click({ position: { x: 300, y: 300 } });
106106+107107+ const shape = page.locator('.diagram-shape');
108108+ await expect(shape).toHaveCount(1);
109109+ // Text shape should have a label "Text"
110110+ await expect(shape.locator('text')).toHaveText('Text');
111111+ });
112112+113113+ test('add multiple shapes to the canvas', async ({ page }) => {
114114+ const canvas = page.locator('#diagram-canvas');
115115+116116+ // Add a rectangle
117117+ await page.click('#tool-rectangle');
118118+ await canvas.click({ position: { x: 200, y: 200 } });
119119+120120+ // Add an ellipse
121121+ await page.click('#tool-ellipse');
122122+ await canvas.click({ position: { x: 400, y: 200 } });
123123+124124+ // Add a diamond
125125+ await page.click('#tool-diamond');
126126+ await canvas.click({ position: { x: 300, y: 400 } });
127127+128128+ // Three shapes should exist
129129+ await expect(page.locator('.diagram-shape')).toHaveCount(3);
130130+ });
131131+});
132132+133133+test.describe('Diagrams - Zoom Controls', () => {
134134+ test.beforeEach(async ({ page }) => {
135135+ await createNewDiagram(page);
136136+ });
137137+138138+ test('zoom label shows 100% by default', async ({ page }) => {
139139+ await expect(page.locator('#zoom-label')).toHaveText('100%');
140140+ });
141141+142142+ test('zoom in button increases zoom level', async ({ page }) => {
143143+ await page.click('#btn-zoom-in');
144144+ await expect(page.locator('#zoom-label')).toHaveText('125%');
145145+ });
146146+147147+ test('zoom out button decreases zoom level', async ({ page }) => {
148148+ await page.click('#btn-zoom-out');
149149+ await expect(page.locator('#zoom-label')).toHaveText('75%');
150150+ });
151151+152152+ test('multiple zoom in clicks accumulate', async ({ page }) => {
153153+ await page.click('#btn-zoom-in');
154154+ await page.click('#btn-zoom-in');
155155+ await expect(page.locator('#zoom-label')).toHaveText('150%');
156156+ });
157157+158158+ test('zoom has a lower bound', async ({ page }) => {
159159+ // Click zoom out many times to hit the floor
160160+ for (let i = 0; i < 20; i++) {
161161+ await page.click('#btn-zoom-out');
162162+ }
163163+ const text = await page.locator('#zoom-label').textContent();
164164+ const percent = parseInt(text || '0');
165165+ // Minimum zoom is 0.1 = 10%
166166+ expect(percent).toBeGreaterThanOrEqual(10);
167167+ });
168168+169169+ test('zoom has an upper bound', async ({ page }) => {
170170+ // Click zoom in many times to hit the ceiling
171171+ for (let i = 0; i < 40; i++) {
172172+ await page.click('#btn-zoom-in');
173173+ }
174174+ const text = await page.locator('#zoom-label').textContent();
175175+ const percent = parseInt(text || '0');
176176+ // Maximum zoom is 5 = 500%
177177+ expect(percent).toBeLessThanOrEqual(500);
178178+ });
179179+});
180180+181181+test.describe('Diagrams - Snap to Grid', () => {
182182+ test.beforeEach(async ({ page }) => {
183183+ await createNewDiagram(page);
184184+ });
185185+186186+ test('snap-to-grid button exists', async ({ page }) => {
187187+ await expect(page.locator('#btn-snap-grid')).toBeVisible();
188188+ });
189189+190190+ test('snap-to-grid is active by default', async ({ page }) => {
191191+ // WhiteboardState defaults snapToGrid=true; the button should have active class
192192+ await expect(page.locator('#btn-snap-grid')).toHaveClass(/active/);
193193+ });
194194+195195+ test('clicking snap-to-grid toggles it off and on', async ({ page }) => {
196196+ const btn = page.locator('#btn-snap-grid');
197197+198198+ // Initially active
199199+ await expect(btn).toHaveClass(/active/);
200200+201201+ // Click to toggle off
202202+ await btn.click();
203203+ await expect(btn).not.toHaveClass(/active/);
204204+205205+ // Click to toggle back on
206206+ await btn.click();
207207+ await expect(btn).toHaveClass(/active/);
208208+ });
209209+});
210210+211211+test.describe('Diagrams - Shape Selection and Properties Panel', () => {
212212+ test.beforeEach(async ({ page }) => {
213213+ await createNewDiagram(page);
214214+ });
215215+216216+ test('properties panel is hidden when no shape is selected', async ({ page }) => {
217217+ await expect(page.locator('#props-panel')).not.toBeVisible();
218218+ });
219219+220220+ test('selecting a shape shows the properties panel', async ({ page }) => {
221221+ const canvas = page.locator('#diagram-canvas');
222222+223223+ // Add a rectangle
224224+ await page.click('#tool-rectangle');
225225+ await canvas.click({ position: { x: 300, y: 300 } });
226226+227227+ // Now we are back in select mode; click on the shape to select it
228228+ // The shape was placed around (300,300) in screen coords; click same spot
229229+ await canvas.click({ position: { x: 300, y: 300 } });
230230+231231+ // Properties panel should be visible
232232+ await expect(page.locator('#props-panel')).toBeVisible();
233233+ // The selected shape should have the .selected class
234234+ await expect(page.locator('.diagram-shape.selected')).toHaveCount(1);
235235+ });
236236+237237+ test('properties panel shows width and height inputs', async ({ page }) => {
238238+ const canvas = page.locator('#diagram-canvas');
239239+240240+ // Add and select a rectangle
241241+ await page.click('#tool-rectangle');
242242+ await canvas.click({ position: { x: 300, y: 300 } });
243243+ await canvas.click({ position: { x: 300, y: 300 } });
244244+245245+ await expect(page.locator('#props-panel')).toBeVisible();
246246+ await expect(page.locator('#prop-width')).toBeVisible();
247247+ await expect(page.locator('#prop-height')).toBeVisible();
248248+ await expect(page.locator('#prop-label')).toBeVisible();
249249+ });
250250+251251+ test('properties panel shows correct default dimensions', async ({ page }) => {
252252+ const canvas = page.locator('#diagram-canvas');
253253+254254+ // Add and select a rectangle (default size is 120x80)
255255+ await page.click('#tool-rectangle');
256256+ await canvas.click({ position: { x: 300, y: 300 } });
257257+ await canvas.click({ position: { x: 300, y: 300 } });
258258+259259+ await expect(page.locator('#props-panel')).toBeVisible();
260260+ await expect(page.locator('#prop-width')).toHaveValue('120');
261261+ await expect(page.locator('#prop-height')).toHaveValue('80');
262262+ });
263263+264264+ test('clicking empty canvas deselects shape and hides properties panel', async ({ page }) => {
265265+ const canvas = page.locator('#diagram-canvas');
266266+267267+ // Add a rectangle and select it
268268+ await page.click('#tool-rectangle');
269269+ await canvas.click({ position: { x: 300, y: 300 } });
270270+ await canvas.click({ position: { x: 300, y: 300 } });
271271+ await expect(page.locator('#props-panel')).toBeVisible();
272272+273273+ // Click on an empty area of the canvas
274274+ await canvas.click({ position: { x: 50, y: 50 } });
275275+ await expect(page.locator('#props-panel')).not.toBeVisible();
276276+ await expect(page.locator('.diagram-shape.selected')).toHaveCount(0);
277277+ });
278278+});
279279+280280+test.describe('Diagrams - Delete Shape', () => {
281281+ test.beforeEach(async ({ page }) => {
282282+ await createNewDiagram(page);
283283+ });
284284+285285+ test('delete button exists', async ({ page }) => {
286286+ await expect(page.locator('#btn-delete')).toBeVisible();
287287+ });
288288+289289+ test('delete button removes a selected shape', async ({ page }) => {
290290+ const canvas = page.locator('#diagram-canvas');
291291+292292+ // Add a rectangle
293293+ await page.click('#tool-rectangle');
294294+ await canvas.click({ position: { x: 300, y: 300 } });
295295+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
296296+297297+ // Select the shape
298298+ await canvas.click({ position: { x: 300, y: 300 } });
299299+ await expect(page.locator('.diagram-shape.selected')).toHaveCount(1);
300300+301301+ // Delete it
302302+ await page.click('#btn-delete');
303303+ await expect(page.locator('.diagram-shape')).toHaveCount(0);
304304+305305+ // Properties panel should be hidden
306306+ await expect(page.locator('#props-panel')).not.toBeVisible();
307307+ });
308308+309309+ test('delete with keyboard (Delete key) removes selected shape', async ({ page }) => {
310310+ const canvas = page.locator('#diagram-canvas');
311311+312312+ // Add and select a rectangle
313313+ await page.click('#tool-rectangle');
314314+ await canvas.click({ position: { x: 300, y: 300 } });
315315+ await canvas.click({ position: { x: 300, y: 300 } });
316316+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
317317+318318+ // Press Delete key
319319+ await page.keyboard.press('Delete');
320320+ await expect(page.locator('.diagram-shape')).toHaveCount(0);
321321+ });
322322+323323+ test('delete with Backspace key removes selected shape', async ({ page }) => {
324324+ const canvas = page.locator('#diagram-canvas');
325325+326326+ // Add and select a rectangle
327327+ await page.click('#tool-rectangle');
328328+ await canvas.click({ position: { x: 300, y: 300 } });
329329+ await canvas.click({ position: { x: 300, y: 300 } });
330330+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
331331+332332+ // Press Backspace key
333333+ await page.keyboard.press('Backspace');
334334+ await expect(page.locator('.diagram-shape')).toHaveCount(0);
335335+ });
336336+337337+ test('delete does nothing when no shape is selected', async ({ page }) => {
338338+ const canvas = page.locator('#diagram-canvas');
339339+340340+ // Add a rectangle but do not select it (click empty space after)
341341+ await page.click('#tool-rectangle');
342342+ await canvas.click({ position: { x: 300, y: 300 } });
343343+ await canvas.click({ position: { x: 50, y: 50 } });
344344+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
345345+ await expect(page.locator('.diagram-shape.selected')).toHaveCount(0);
346346+347347+ // Click delete -- nothing should happen
348348+ await page.click('#btn-delete');
349349+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
350350+ });
351351+});
352352+353353+test.describe('Diagrams - Keyboard Shortcuts', () => {
354354+ test.beforeEach(async ({ page }) => {
355355+ await createNewDiagram(page);
356356+ });
357357+358358+ test('pressing V activates select tool', async ({ page }) => {
359359+ // First switch away from select
360360+ await page.click('#tool-rectangle');
361361+ await expect(page.locator('#tool-rectangle')).toHaveClass(/active/);
362362+363363+ // Press V to go back to select
364364+ await page.keyboard.press('v');
365365+ await expect(page.locator('#tool-select')).toHaveClass(/active/);
366366+ await expect(page.locator('#tool-rectangle')).not.toHaveClass(/active/);
367367+ });
368368+369369+ test('pressing R activates rectangle tool', async ({ page }) => {
370370+ await page.keyboard.press('r');
371371+ await expect(page.locator('#tool-rectangle')).toHaveClass(/active/);
372372+ });
373373+374374+ test('pressing E activates ellipse tool', async ({ page }) => {
375375+ await page.keyboard.press('e');
376376+ await expect(page.locator('#tool-ellipse')).toHaveClass(/active/);
377377+ });
378378+379379+ test('pressing D activates diamond tool', async ({ page }) => {
380380+ await page.keyboard.press('d');
381381+ await expect(page.locator('#tool-diamond')).toHaveClass(/active/);
382382+ });
383383+384384+ test('pressing T activates text tool', async ({ page }) => {
385385+ await page.keyboard.press('t');
386386+ await expect(page.locator('#tool-text')).toHaveClass(/active/);
387387+ });
388388+389389+ test('pressing P activates freehand tool', async ({ page }) => {
390390+ await page.keyboard.press('p');
391391+ await expect(page.locator('#tool-freehand')).toHaveClass(/active/);
392392+ });
393393+394394+ test('pressing A activates arrow tool', async ({ page }) => {
395395+ await page.keyboard.press('a');
396396+ await expect(page.locator('#tool-arrow')).toHaveClass(/active/);
397397+ });
398398+399399+ test('shortcuts are ignored when typing in an input field', async ({ page }) => {
400400+ // Focus the title input
401401+ const titleInput = page.locator('#diagram-title');
402402+ await titleInput.click();
403403+ await titleInput.fill('');
404404+405405+ // Type R -- should go into the input, not activate rectangle tool
406406+ await page.keyboard.type('r');
407407+ await expect(page.locator('#tool-select')).toHaveClass(/active/);
408408+ await expect(page.locator('#tool-rectangle')).not.toHaveClass(/active/);
409409+ });
410410+411411+ test('keyboard shortcut to create shape then place it on canvas', async ({ page }) => {
412412+ const canvas = page.locator('#diagram-canvas');
413413+414414+ // Press R then click canvas
415415+ await page.keyboard.press('r');
416416+ await canvas.click({ position: { x: 300, y: 300 } });
417417+418418+ await expect(page.locator('.diagram-shape')).toHaveCount(1);
419419+ await expect(page.locator('.diagram-shape rect')).toBeAttached();
420420+ });
421421+});
+383
e2e/forms.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { createNewForm } from './helpers';
33+44+test.describe('Forms - Builder', () => {
55+ test.beforeEach(async ({ page }) => {
66+ await createNewForm(page);
77+ });
88+99+ test('form builder loads with toolbar and empty state', async ({ page }) => {
1010+ // Toolbar buttons should be visible
1111+ await expect(page.locator('#btn-add-question')).toBeVisible();
1212+ await expect(page.locator('#btn-preview')).toBeVisible();
1313+ await expect(page.locator('#btn-responses')).toBeVisible();
1414+ await expect(page.locator('#btn-settings')).toBeVisible();
1515+1616+ // Title input should show default
1717+ await expect(page.locator('#form-title')).toHaveValue('Untitled Form');
1818+1919+ // Description textarea should be empty
2020+ await expect(page.locator('#form-description')).toHaveValue('');
2121+2222+ // Empty state message should be visible
2323+ await expect(page.locator('#form-questions')).toContainText('No questions yet');
2424+ });
2525+2626+ test('add question dialog opens and shows all question types', async ({ page }) => {
2727+ await page.click('#btn-add-question');
2828+2929+ // Dialog overlay should appear
3030+ await expect(page.locator('.add-question-overlay')).toBeVisible();
3131+3232+ // All 10 question type buttons should be present
3333+ const typeButtons = page.locator('.form-type-btn');
3434+ await expect(typeButtons).toHaveCount(10);
3535+3636+ // Verify specific types are listed
3737+ await expect(page.locator('.form-type-btn[data-type="short_text"]')).toBeVisible();
3838+ await expect(page.locator('.form-type-btn[data-type="long_text"]')).toBeVisible();
3939+ await expect(page.locator('.form-type-btn[data-type="number"]')).toBeVisible();
4040+ await expect(page.locator('.form-type-btn[data-type="email"]')).toBeVisible();
4141+ await expect(page.locator('.form-type-btn[data-type="single_choice"]')).toBeVisible();
4242+ await expect(page.locator('.form-type-btn[data-type="multiple_choice"]')).toBeVisible();
4343+ await expect(page.locator('.form-type-btn[data-type="dropdown"]')).toBeVisible();
4444+ await expect(page.locator('.form-type-btn[data-type="date"]')).toBeVisible();
4545+ await expect(page.locator('.form-type-btn[data-type="rating"]')).toBeVisible();
4646+ await expect(page.locator('.form-type-btn[data-type="scale"]')).toBeVisible();
4747+4848+ // Cancel button should close the dialog
4949+ await page.click('#add-q-cancel');
5050+ await expect(page.locator('.add-question-overlay')).not.toBeVisible();
5151+ });
5252+5353+ test('add a short text question', async ({ page }) => {
5454+ await page.click('#btn-add-question');
5555+ await page.click('.form-type-btn[data-type="short_text"]');
5656+5757+ // Dialog should close
5858+ await expect(page.locator('.add-question-overlay')).not.toBeVisible();
5959+6060+ // A question card should appear
6161+ const card = page.locator('.form-question-card');
6262+ await expect(card).toHaveCount(1);
6363+6464+ // Card should show question number and type badge
6565+ await expect(card.locator('.form-question-number')).toHaveText('1');
6666+ await expect(card.locator('.form-question-type-badge')).toHaveText('Short Text');
6767+6868+ // Card should have a label input, description input, and controls
6969+ await expect(card.locator('.form-question-label')).toBeVisible();
7070+ await expect(card.locator('.form-question-desc')).toBeVisible();
7171+ await expect(card.locator('.form-question-required')).toBeVisible();
7272+ await expect(card.locator('.form-question-delete')).toBeVisible();
7373+ });
7474+7575+ test('add a single choice question with default options', async ({ page }) => {
7676+ await page.click('#btn-add-question');
7777+ await page.click('.form-type-btn[data-type="single_choice"]');
7878+7979+ const card = page.locator('.form-question-card');
8080+ await expect(card).toHaveCount(1);
8181+ await expect(card.locator('.form-question-type-badge')).toHaveText('Single Choice');
8282+8383+ // Should have 2 default options
8484+ const optionInputs = card.locator('.form-option-input');
8585+ await expect(optionInputs).toHaveCount(2);
8686+ await expect(optionInputs.first()).toHaveValue('Option 1');
8787+ await expect(optionInputs.last()).toHaveValue('Option 2');
8888+8989+ // Should have an "Add option" button
9090+ await expect(card.locator('.form-add-option')).toBeVisible();
9191+9292+ // Add a third option
9393+ await card.locator('.form-add-option').click();
9494+ await expect(card.locator('.form-option-input')).toHaveCount(3);
9595+ await expect(card.locator('.form-option-input').last()).toHaveValue('Option 3');
9696+ });
9797+9898+ test('edit question label', async ({ page }) => {
9999+ // Add a short text question
100100+ await page.click('#btn-add-question');
101101+ await page.click('.form-type-btn[data-type="short_text"]');
102102+103103+ const labelInput = page.locator('.form-question-label');
104104+ await labelInput.fill('What is your name?');
105105+ // Trigger change event (change fires on blur/commit, not on input)
106106+ await labelInput.dispatchEvent('change');
107107+108108+ // Verify the label persisted by checking the input value
109109+ await expect(labelInput).toHaveValue('What is your name?');
110110+ });
111111+112112+ test('toggle required checkbox', async ({ page }) => {
113113+ await page.click('#btn-add-question');
114114+ await page.click('.form-type-btn[data-type="short_text"]');
115115+116116+ const checkbox = page.locator('.form-question-required');
117117+118118+ // Should not be checked by default
119119+ await expect(checkbox).not.toBeChecked();
120120+121121+ // Check it
122122+ await checkbox.check();
123123+ await expect(checkbox).toBeChecked();
124124+125125+ // Uncheck it
126126+ await checkbox.uncheck();
127127+ await expect(checkbox).not.toBeChecked();
128128+ });
129129+130130+ test('reorder questions with move up and move down', async ({ page }) => {
131131+ // Add two questions
132132+ await page.click('#btn-add-question');
133133+ await page.click('.form-type-btn[data-type="short_text"]');
134134+135135+ await page.click('#btn-add-question');
136136+ await page.click('.form-type-btn[data-type="number"]');
137137+138138+ // Verify initial order
139139+ const cards = page.locator('.form-question-card');
140140+ await expect(cards).toHaveCount(2);
141141+ await expect(cards.first().locator('.form-question-type-badge')).toHaveText('Short Text');
142142+ await expect(cards.last().locator('.form-question-type-badge')).toHaveText('Number');
143143+144144+ // First question's move-up should be disabled (already at top)
145145+ await expect(cards.first().locator('.form-question-move-up')).toBeDisabled();
146146+ // Last question's move-down should be disabled (already at bottom)
147147+ await expect(cards.last().locator('.form-question-move-down')).toBeDisabled();
148148+149149+ // Move the first question down
150150+ await cards.first().locator('.form-question-move-down').click();
151151+152152+ // Order should be reversed now
153153+ const reorderedCards = page.locator('.form-question-card');
154154+ await expect(reorderedCards.first().locator('.form-question-type-badge')).toHaveText('Number');
155155+ await expect(reorderedCards.last().locator('.form-question-type-badge')).toHaveText('Short Text');
156156+157157+ // Move the last question up to restore original order
158158+ await reorderedCards.last().locator('.form-question-move-up').click();
159159+ const restoredCards = page.locator('.form-question-card');
160160+ await expect(restoredCards.first().locator('.form-question-type-badge')).toHaveText('Short Text');
161161+ await expect(restoredCards.last().locator('.form-question-type-badge')).toHaveText('Number');
162162+ });
163163+164164+ test('delete a question', async ({ page }) => {
165165+ // Add two questions
166166+ await page.click('#btn-add-question');
167167+ await page.click('.form-type-btn[data-type="short_text"]');
168168+ await page.click('#btn-add-question');
169169+ await page.click('.form-type-btn[data-type="email"]');
170170+171171+ await expect(page.locator('.form-question-card')).toHaveCount(2);
172172+173173+ // Delete the first question
174174+ await page.locator('.form-question-card').first().locator('.form-question-delete').click();
175175+176176+ // Only one question should remain
177177+ await expect(page.locator('.form-question-card')).toHaveCount(1);
178178+ await expect(page.locator('.form-question-type-badge')).toHaveText('Email');
179179+ });
180180+181181+ test('delete last question shows empty state', async ({ page }) => {
182182+ await page.click('#btn-add-question');
183183+ await page.click('.form-type-btn[data-type="short_text"]');
184184+ await expect(page.locator('.form-question-card')).toHaveCount(1);
185185+186186+ // Delete the only question
187187+ await page.locator('.form-question-delete').click();
188188+189189+ await expect(page.locator('.form-question-card')).toHaveCount(0);
190190+ await expect(page.locator('#form-questions')).toContainText('No questions yet');
191191+ });
192192+193193+ test('form title editing', async ({ page }) => {
194194+ const titleInput = page.locator('#form-title');
195195+ await expect(titleInput).toHaveValue('Untitled Form');
196196+197197+ await titleInput.fill('Employee Feedback Survey');
198198+ await titleInput.dispatchEvent('change');
199199+200200+ await expect(titleInput).toHaveValue('Employee Feedback Survey');
201201+ });
202202+203203+ test('form description editing', async ({ page }) => {
204204+ const descInput = page.locator('#form-description');
205205+ await descInput.fill('Please fill out this quarterly survey.');
206206+ await descInput.dispatchEvent('change');
207207+208208+ await expect(descInput).toHaveValue('Please fill out this quarterly survey.');
209209+ });
210210+});
211211+212212+test.describe('Forms - Preview Mode', () => {
213213+ test.beforeEach(async ({ page }) => {
214214+ await createNewForm(page);
215215+ });
216216+217217+ test('switch to preview mode shows preview pane', async ({ page }) => {
218218+ // Add a question so preview has content
219219+ await page.click('#btn-add-question');
220220+ await page.click('.form-type-btn[data-type="short_text"]');
221221+222222+ // Set a label so we can verify it appears in preview
223223+ const labelInput = page.locator('.form-question-label');
224224+ await labelInput.fill('Your name');
225225+ await labelInput.dispatchEvent('change');
226226+227227+ // Switch to preview
228228+ await page.click('#btn-preview');
229229+230230+ // Preview pane should be visible, questions container should be hidden
231231+ await expect(page.locator('#form-preview')).toBeVisible();
232232+ await expect(page.locator('#form-questions')).not.toBeVisible();
233233+234234+ // Preview should show the form title
235235+ await expect(page.locator('.form-preview-container h2')).toHaveText('Untitled Form');
236236+237237+ // Preview should show the question
238238+ await expect(page.locator('.form-preview-question')).toHaveCount(1);
239239+ await expect(page.locator('.form-preview-label')).toContainText('Your name');
240240+241241+ // Preview should have a submit button
242242+ await expect(page.locator('#preview-submit')).toBeVisible();
243243+ });
244244+245245+ test('preview shows form description when set', async ({ page }) => {
246246+ const descInput = page.locator('#form-description');
247247+ await descInput.fill('A test description');
248248+ await descInput.dispatchEvent('change');
249249+250250+ await page.click('#btn-preview');
251251+ await expect(page.locator('.form-preview-desc')).toHaveText('A test description');
252252+ });
253253+254254+ test('preview renders input types correctly', async ({ page }) => {
255255+ // Add short_text
256256+ await page.click('#btn-add-question');
257257+ await page.click('.form-type-btn[data-type="short_text"]');
258258+259259+ // Add single_choice
260260+ await page.click('#btn-add-question');
261261+ await page.click('.form-type-btn[data-type="single_choice"]');
262262+263263+ // Add date
264264+ await page.click('#btn-add-question');
265265+ await page.click('.form-type-btn[data-type="date"]');
266266+267267+ // Switch to preview
268268+ await page.click('#btn-preview');
269269+270270+ const questions = page.locator('.form-preview-question');
271271+ await expect(questions).toHaveCount(3);
272272+273273+ // Short text should render a text input
274274+ await expect(questions.nth(0).locator('input[type="text"]')).toBeVisible();
275275+276276+ // Single choice should render radio buttons
277277+ await expect(questions.nth(1).locator('input[type="radio"]')).toHaveCount(2);
278278+279279+ // Date should render a date input
280280+ await expect(questions.nth(2).locator('input[type="date"]')).toBeVisible();
281281+ });
282282+283283+ test('preview shows required marker for required questions', async ({ page }) => {
284284+ await page.click('#btn-add-question');
285285+ await page.click('.form-type-btn[data-type="short_text"]');
286286+287287+ // Mark as required
288288+ await page.locator('.form-question-required').check();
289289+290290+ await page.click('#btn-preview');
291291+292292+ // Required mark should appear
293293+ await expect(page.locator('.form-required-mark')).toBeVisible();
294294+ await expect(page.locator('.form-required-mark')).toHaveText('*');
295295+ });
296296+297297+ test('switch back to builder from preview', async ({ page }) => {
298298+ await page.click('#btn-preview');
299299+300300+ // Should be in preview mode
301301+ await expect(page.locator('#form-preview')).toBeVisible();
302302+303303+ // Click preview again to toggle back
304304+ await page.click('#btn-preview');
305305+306306+ // Should be back in builder mode
307307+ await expect(page.locator('#form-questions')).toBeVisible();
308308+ await expect(page.locator('#form-preview')).not.toBeVisible();
309309+ });
310310+});
311311+312312+test.describe('Forms - Responses View', () => {
313313+ test.beforeEach(async ({ page }) => {
314314+ await createNewForm(page);
315315+ });
316316+317317+ test('switch to responses view shows empty state', async ({ page }) => {
318318+ await page.click('#btn-responses');
319319+320320+ // Responses pane should be visible
321321+ await expect(page.locator('#form-responses-view')).toBeVisible();
322322+ await expect(page.locator('#form-questions')).not.toBeVisible();
323323+ await expect(page.locator('#form-preview')).not.toBeVisible();
324324+325325+ // Should show "No responses yet" message
326326+ await expect(page.locator('#form-responses-view')).toContainText('No responses yet');
327327+ });
328328+329329+ test('switch back to builder from responses view', async ({ page }) => {
330330+ await page.click('#btn-responses');
331331+ await expect(page.locator('#form-responses-view')).toBeVisible();
332332+333333+ // Click responses again to toggle back
334334+ await page.click('#btn-responses');
335335+ await expect(page.locator('#form-questions')).toBeVisible();
336336+ await expect(page.locator('#form-responses-view')).not.toBeVisible();
337337+ });
338338+});
339339+340340+test.describe('Forms - Add Question Dialog', () => {
341341+ test.beforeEach(async ({ page }) => {
342342+ await createNewForm(page);
343343+ });
344344+345345+ test('clicking overlay background closes dialog', async ({ page }) => {
346346+ await page.click('#btn-add-question');
347347+ await expect(page.locator('.add-question-overlay')).toBeVisible();
348348+349349+ // Click the overlay background (not the dialog content)
350350+ await page.locator('.add-question-overlay').click({ position: { x: 5, y: 5 } });
351351+ await expect(page.locator('.add-question-overlay')).not.toBeVisible();
352352+ });
353353+354354+ test('add multiple questions of different types', async ({ page }) => {
355355+ // Add short text
356356+ await page.click('#btn-add-question');
357357+ await page.click('.form-type-btn[data-type="short_text"]');
358358+359359+ // Add rating
360360+ await page.click('#btn-add-question');
361361+ await page.click('.form-type-btn[data-type="rating"]');
362362+363363+ // Add dropdown
364364+ await page.click('#btn-add-question');
365365+ await page.click('.form-type-btn[data-type="dropdown"]');
366366+367367+ const cards = page.locator('.form-question-card');
368368+ await expect(cards).toHaveCount(3);
369369+370370+ // Verify numbering
371371+ await expect(cards.nth(0).locator('.form-question-number')).toHaveText('1');
372372+ await expect(cards.nth(1).locator('.form-question-number')).toHaveText('2');
373373+ await expect(cards.nth(2).locator('.form-question-number')).toHaveText('3');
374374+375375+ // Verify types
376376+ await expect(cards.nth(0).locator('.form-question-type-badge')).toHaveText('Short Text');
377377+ await expect(cards.nth(1).locator('.form-question-type-badge')).toHaveText('Rating');
378378+ await expect(cards.nth(2).locator('.form-question-type-badge')).toHaveText('Dropdown');
379379+380380+ // Dropdown should have options section
381381+ await expect(cards.nth(2).locator('.form-option-input')).toHaveCount(2);
382382+ });
383383+});
+42
e2e/helpers.ts
···5555}
56565757/**
5858+ * Create a new presentation via the landing page and wait for the canvas to load.
5959+ * Returns the full URL of the new presentation.
6060+ */
6161+export async function createNewSlides(page: Page): Promise<string> {
6262+ await goToLanding(page);
6363+ // Click triggers async createDocument (fetch + window.location.href), so wait for navigation
6464+ await Promise.all([
6565+ page.waitForURL(/\/slides\//, { timeout: 15000 }),
6666+ page.click('#new-slide'),
6767+ ]);
6868+ // Wait for the slide canvas to render
6969+ await page.waitForSelector('#slide-canvas', { timeout: 15000 });
7070+ // Wait for thumbnails to populate (at least one slide)
7171+ await page.waitForSelector('.slides-thumbnail', { timeout: 15000 });
7272+ return page.url();
7373+}
7474+7575+/**
7676+ * Create a new form via the landing page and wait for the builder to load.
7777+ * Returns the full URL of the new form.
7878+ */
7979+export async function createNewForm(page: Page): Promise<string> {
8080+ await goToLanding(page);
8181+ await page.click('#new-form');
8282+ // Wait for the form builder toolbar to render
8383+ await page.waitForSelector('#form-toolbar', { timeout: 15000 });
8484+ return page.url();
8585+}
8686+8787+/**
8888+ * Create a new diagram via the landing page and wait for the SVG canvas to load.
8989+ * Returns the full URL of the new diagram.
9090+ */
9191+export async function createNewDiagram(page: Page): Promise<string> {
9292+ await goToLanding(page);
9393+ await page.click('#new-diagram');
9494+ // Wait for the SVG canvas to render
9595+ await page.waitForSelector('#diagram-canvas', { timeout: 15000 });
9696+ return page.url();
9797+}
9898+9999+/**
58100 * Click a cell in the spreadsheet by its cell ID (e.g. "A1", "B3").
59101 */
60102export async function clickCell(page: Page, cellId: string): Promise<void> {
+218
e2e/slides.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { createNewSlides } from './helpers';
33+44+test.describe('Slides - Basic', () => {
55+ test.beforeEach(async ({ page }) => {
66+ await createNewSlides(page);
77+ });
88+99+ test('page loads with slide canvas visible', async ({ page }) => {
1010+ const canvas = page.locator('#slide-canvas');
1111+ await expect(canvas).toBeVisible();
1212+1313+ // Canvas should have fixed dimensions (960x540)
1414+ const style = await canvas.getAttribute('style');
1515+ expect(style).toContain('960px');
1616+ expect(style).toContain('540px');
1717+1818+ // Toolbar should be visible
1919+ await expect(page.locator('#slides-toolbar')).toBeVisible();
2020+2121+ // At least one thumbnail should exist (the initial slide)
2222+ const thumbs = page.locator('.slides-thumbnail');
2323+ await expect(thumbs).toHaveCount(1);
2424+2525+ // Title input should show default title
2626+ await expect(page.locator('#deck-title')).toHaveValue('Untitled Presentation');
2727+ });
2828+2929+ test('add slide button creates a new slide', async ({ page }) => {
3030+ // Start with one thumbnail
3131+ await expect(page.locator('.slides-thumbnail')).toHaveCount(1);
3232+3333+ // Click the add slide button
3434+ await page.click('#btn-add-slide');
3535+3636+ // Should now have two thumbnails
3737+ await expect(page.locator('.slides-thumbnail')).toHaveCount(2);
3838+3939+ // Second thumbnail should be active (navigated to new slide)
4040+ const secondThumb = page.locator('.slides-thumbnail').nth(1);
4141+ await expect(secondThumb).toHaveClass(/active/);
4242+ });
4343+4444+ test('add text element places text on the canvas', async ({ page }) => {
4545+ // Canvas should start empty
4646+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(0);
4747+4848+ // Click the add text button
4949+ await page.click('#btn-add-text');
5050+5151+ // A text element should appear on the canvas
5252+ const element = page.locator('#slide-canvas .slide-element');
5353+ await expect(element).toHaveCount(1);
5454+5555+ // It should contain an editable text region
5656+ const textContent = element.locator('[contenteditable="true"]');
5757+ await expect(textContent).toBeVisible();
5858+ await expect(textContent).toContainText('Click to edit');
5959+ });
6060+6161+ test('add shape element places shape on the canvas', async ({ page }) => {
6262+ // Canvas should start empty
6363+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(0);
6464+6565+ // Click the add shape button
6666+ await page.click('#btn-add-shape');
6767+6868+ // A shape element should appear on the canvas
6969+ const element = page.locator('#slide-canvas .slide-element');
7070+ await expect(element).toHaveCount(1);
7171+7272+ // Shape should render as SVG
7373+ const svg = element.locator('svg');
7474+ await expect(svg).toBeVisible();
7575+ });
7676+7777+ test('thumbnail navigation switches to clicked slide', async ({ page }) => {
7878+ // Add a second slide
7979+ await page.click('#btn-add-slide');
8080+ await expect(page.locator('.slides-thumbnail')).toHaveCount(2);
8181+8282+ // Add a text element to the second slide so it is distinguishable
8383+ await page.click('#btn-add-text');
8484+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(1);
8585+8686+ // Click the first thumbnail to navigate back
8787+ await page.locator('.slides-thumbnail').first().click();
8888+8989+ // First thumbnail should now be active
9090+ await expect(page.locator('.slides-thumbnail').first()).toHaveClass(/active/);
9191+9292+ // Canvas should be empty (first slide has no elements)
9393+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(0);
9494+9595+ // Click the second thumbnail again
9696+ await page.locator('.slides-thumbnail').nth(1).click();
9797+9898+ // Second thumbnail should be active, and the text element should be back
9999+ await expect(page.locator('.slides-thumbnail').nth(1)).toHaveClass(/active/);
100100+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(1);
101101+ });
102102+103103+ test('theme selector changes theme', async ({ page }) => {
104104+ const themeSelect = page.locator('#theme-select');
105105+ await expect(themeSelect).toBeVisible();
106106+107107+ // Get the initial theme value
108108+ const initialTheme = await themeSelect.inputValue();
109109+110110+ // Get all available theme options
111111+ const options = await themeSelect.locator('option').all();
112112+ expect(options.length).toBeGreaterThan(1);
113113+114114+ // Select a different theme
115115+ const secondOption = await options[1].getAttribute('value');
116116+ expect(secondOption).toBeTruthy();
117117+ await themeSelect.selectOption(secondOption!);
118118+119119+ // Theme should have changed
120120+ const newTheme = await themeSelect.inputValue();
121121+ expect(newTheme).toBe(secondOption);
122122+ expect(newTheme).not.toBe(initialTheme);
123123+ });
124124+125125+ test('layout selector works', async ({ page }) => {
126126+ const layoutSelect = page.locator('#layout-select');
127127+ await expect(layoutSelect).toBeVisible();
128128+129129+ // Get all available layout options
130130+ const options = await layoutSelect.locator('option').all();
131131+ expect(options.length).toBeGreaterThan(1);
132132+133133+ // Select a different layout
134134+ const secondOption = await options[1].getAttribute('value');
135135+ expect(secondOption).toBeTruthy();
136136+ await layoutSelect.selectOption(secondOption!);
137137+138138+ // Layout should have changed
139139+ const newLayout = await layoutSelect.inputValue();
140140+ expect(newLayout).toBe(secondOption);
141141+ });
142142+143143+ test('presenter mode opens and can be exited', async ({ page }) => {
144144+ const overlay = page.locator('#presenter-overlay');
145145+146146+ // Overlay should be hidden initially
147147+ await expect(overlay).toBeHidden();
148148+149149+ // Click the present button
150150+ await page.click('#btn-present');
151151+152152+ // Overlay should now be visible
153153+ await expect(overlay).toBeVisible({ timeout: 5000 });
154154+155155+ // Body should have presenting class
156156+ await expect(page.locator('body')).toHaveClass(/presenting/);
157157+158158+ // Timer and progress should be visible
159159+ await expect(page.locator('#presenter-timer')).toBeVisible();
160160+ await expect(page.locator('#presenter-progress')).toContainText('1 / 1');
161161+162162+ // Click exit button
163163+ await page.click('#btn-presenter-exit');
164164+165165+ // Overlay should be hidden again
166166+ await expect(overlay).toBeHidden();
167167+ await expect(page.locator('body')).not.toHaveClass(/presenting/);
168168+ });
169169+170170+ test('notes input accepts and displays speaker notes', async ({ page }) => {
171171+ const notesInput = page.locator('#notes-input');
172172+ await expect(notesInput).toBeVisible();
173173+174174+ // Type speaker notes
175175+ await notesInput.click();
176176+ await page.keyboard.type('Remember to pause here for questions');
177177+178178+ // Verify notes are in the input
179179+ await expect(notesInput).toHaveValue('Remember to pause here for questions');
180180+181181+ // Navigate away and back to verify notes persist per slide
182182+ await page.click('#btn-add-slide');
183183+ await expect(page.locator('.slides-thumbnail')).toHaveCount(2);
184184+185185+ // New slide should have empty notes
186186+ await expect(notesInput).toHaveValue('');
187187+188188+ // Navigate back to first slide
189189+ await page.locator('.slides-thumbnail').first().click();
190190+191191+ // Notes should still be there
192192+ await expect(notesInput).toHaveValue('Remember to pause here for questions');
193193+ });
194194+195195+ test('Delete key removes selected element', async ({ page }) => {
196196+ // Add a text element
197197+ await page.click('#btn-add-text');
198198+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(1);
199199+200200+ // Click the element to select it (mousedown on the element)
201201+ await page.locator('#slide-canvas .slide-element').click();
202202+203203+ // The element should get the selected class
204204+ await expect(page.locator('#slide-canvas .slide-element.selected')).toHaveCount(1);
205205+206206+ // Click on canvas background first to ensure focus is not on a text input
207207+ // then re-select the element
208208+ await page.locator('#slide-canvas').click({ position: { x: 5, y: 5 } });
209209+ await page.locator('#slide-canvas .slide-element').click();
210210+ await expect(page.locator('#slide-canvas .slide-element.selected')).toHaveCount(1);
211211+212212+ // Press Delete to remove the element
213213+ await page.keyboard.press('Delete');
214214+215215+ // Element should be gone
216216+ await expect(page.locator('#slide-canvas .slide-element')).toHaveCount(0);
217217+ });
218218+});