|
1 | 1 | import { test, expect } from '@playwright/test'; |
2 | | -import { waitForApp } from './helpers'; |
| 2 | +import { importProject, waitForApp } from './helpers'; |
| 3 | + |
| 4 | +/** Minimal grid so Design / Layout mode has bound fields on the canvas. */ |
| 5 | +const LAYOUT_GRID_SEED = { |
| 6 | + definition: { |
| 7 | + $formspec: '1.0', |
| 8 | + url: 'urn:design-mode-e2e', |
| 9 | + version: '1.0.0', |
| 10 | + formPresentation: { pageMode: 'single' }, |
| 11 | + items: [ |
| 12 | + { key: 'firstName', type: 'field', dataType: 'string', label: 'First Name' }, |
| 13 | + { key: 'lastName', type: 'field', dataType: 'string', label: 'Last Name' }, |
| 14 | + ], |
| 15 | + }, |
| 16 | + component: { |
| 17 | + $formspecComponent: '0.1', |
| 18 | + version: '0.1.0', |
| 19 | + targetDefinition: { url: 'urn:design-mode-e2e' }, |
| 20 | + tree: { |
| 21 | + component: 'Form', |
| 22 | + children: [ |
| 23 | + { |
| 24 | + component: 'Page', |
| 25 | + nodeId: 'page-main', |
| 26 | + title: 'Main', |
| 27 | + _layout: true, |
| 28 | + children: [ |
| 29 | + { |
| 30 | + component: 'Grid', |
| 31 | + nodeId: 'grid-main', |
| 32 | + _layout: true, |
| 33 | + columns: 2, |
| 34 | + children: [ |
| 35 | + { component: 'TextInput', bind: 'firstName' }, |
| 36 | + { component: 'TextInput', bind: 'lastName' }, |
| 37 | + ], |
| 38 | + }, |
| 39 | + ], |
| 40 | + }, |
| 41 | + ], |
| 42 | + }, |
| 43 | + }, |
| 44 | +}; |
3 | 45 |
|
4 | 46 | test.describe('Studio Design Mode', () => { |
5 | 47 | test.beforeEach(async ({ page }) => { |
6 | 48 | await waitForApp(page); |
7 | | - // Switch to design mode |
| 49 | + await importProject(page, LAYOUT_GRID_SEED); |
8 | 50 | await page.click('[data-testid="mode-toggle-design"]'); |
9 | | - await expect(page.locator('[data-testid="authoring-overlay"]')).toBeVisible(); |
| 51 | + await expect(page.locator('[data-testid="design-canvas-shell"]')).toBeVisible(); |
| 52 | + await expect(page.locator('[data-testid="workspace-Layout"]')).toBeVisible(); |
10 | 53 | }); |
11 | 54 |
|
12 | | - test('selection ring shows design handle and resize handles', async ({ page }) => { |
13 | | - const field = page.locator('[data-name="firstName"]'); |
14 | | - await field.click(); |
15 | | - |
16 | | - const ring = page.locator('[data-testid="selection-ring"]'); |
17 | | - await expect(ring).toBeVisible(); |
18 | | - await expect(ring).toContainText('Design'); |
19 | | - |
20 | | - // Resize handles should be visible in design mode |
21 | | - const resizeHandle = page.locator('.cursor-ew-resize'); |
22 | | - await expect(resizeHandle).toBeVisible(); |
23 | | - }); |
24 | | - |
25 | | - test('toggles component variants via selection ring', async ({ page }) => { |
26 | | - const field = page.locator('[data-name="firstName"]'); |
27 | | - await field.click(); |
28 | | - |
29 | | - const outlineBtn = page.locator('button:has-text("Outline")'); |
30 | | - await expect(outlineBtn).toBeVisible(); |
31 | | - await outlineBtn.click(); |
32 | | - |
33 | | - // Verify it stays active |
34 | | - await expect(outlineBtn).toHaveClass(/bg-accent\/10/); |
35 | | - }); |
36 | | - |
37 | | - test('detects region drop targets for header/footer', async ({ page }) => { |
38 | | - // First, ensure regions are active in the theme |
39 | | - // We might need to toggle them via the Design sidebar if the fixture doesn't have them |
40 | | - await page.click('button:has-text("Structure & Regions")'); |
41 | | - const headerToggle = page.locator('div:has-text("Header") button'); |
42 | | - await headerToggle.click(); |
43 | | - |
44 | | - const field = page.locator('[data-name="firstName"]'); |
45 | | - const handle = page.locator('[title="Drag to reorder"]'); |
46 | | - |
47 | | - await field.hover(); |
48 | | - await field.click(); |
49 | | - |
50 | | - await handle.hover(); |
51 | | - await page.mouse.down(); |
52 | | - |
53 | | - // Drag to the header area |
54 | | - const headerRegion = page.locator('[data-region-id="header"]'); |
55 | | - await headerRegion.hover(); |
56 | | - |
57 | | - // Drop |
58 | | - await page.mouse.up(); |
59 | | - |
60 | | - // Verify the field is now in the header (via DOM structure check) |
61 | | - await expect(headerRegion.locator('[data-name="firstName"]')).toBeVisible(); |
| 55 | + test('layout field selects and shows column resize affordance in grid', async ({ page }) => { |
| 56 | + const row = page.locator('[data-testid="layout-field-firstName"]'); |
| 57 | + await row.click(); |
| 58 | + await expect(row).toHaveAttribute('aria-pressed', 'true'); |
| 59 | + await expect(row.locator('[data-testid="resize-handle-col"]')).toBeVisible(); |
62 | 60 | }); |
63 | 61 |
|
64 | | - test('resizes field width (span) via drag handles', async ({ page }) => { |
65 | | - const field = page.locator('[data-name="firstName"]'); |
66 | | - await field.click(); |
67 | | - |
68 | | - const ring = page.locator('[data-testid="selection-ring"]'); |
69 | | - const handle = ring.locator('.cursor-ew-resize').first(); // Right handle |
70 | | - |
71 | | - const initialBox = await field.boundingBox(); |
72 | | - if (!initialBox) throw new Error('Could not get initial bounding box'); |
73 | | - |
74 | | - await handle.hover(); |
75 | | - await page.mouse.down(); |
76 | | - |
77 | | - // Drag left to decrease span (-50px is a safer relative drag than -200px to avoid viewport edge cases) |
78 | | - await page.mouse.move(initialBox.x + initialBox.width - 50, initialBox.y + initialBox.height / 2, { steps: 5 }); |
79 | | - await page.mouse.up(); |
80 | | - |
81 | | - // The field should be smaller now |
82 | | - const newBox = await field.boundingBox(); |
83 | | - if (!newBox) throw new Error('Could not get new bounding box'); |
84 | | - |
85 | | - expect(newBox.width).toBeLessThan(initialBox.width); |
86 | | - }); |
87 | 62 | }); |
0 commit comments