test+design: add E2E drag-drop tests and design wireframes

E2E tests cover:
- Drag & drop image into editor inserts image block with data URL
- Drop zone overlay appears during image drag
- Non-image file drops are correctly ignored

Design file shows three states: idle, drag-over (dashed border +
overlay label), and after-drop (image block inserted inline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-23 20:31:24 +01:00
parent 45e6c33de5
commit 1bfeb78868
2 changed files with 407 additions and 0 deletions

222
design/drag-drop-images.pen Normal file
View File

@@ -0,0 +1,222 @@
{
"version": "2.8",
"children": [
{
"type": "frame",
"id": "ddi01",
"name": "Drag & Drop — Idle State",
"x": 0,
"y": 0,
"width": 760,
"height": 500,
"fill": "$--background",
"layout": "vertical",
"children": [
{
"type": "text",
"id": "ddi02",
"content": "Editor — Normal State",
"fontSize": 13,
"fontWeight": 600,
"fill": "$--muted-foreground",
"padding": 16
},
{
"type": "frame",
"id": "ddi03",
"name": "Editor Content Area",
"layout": "vertical",
"fill": "$--background",
"width": "fill_container",
"height": "fill_container",
"padding": 40,
"gap": 8,
"children": [
{
"type": "text",
"id": "ddi04",
"content": "# My Note Title",
"fontSize": 24,
"fontWeight": 700,
"fill": "$--foreground"
},
{
"type": "text",
"id": "ddi05",
"content": "Some paragraph text in the editor. The user can drag images from Finder directly into this area.",
"fontSize": 15,
"fill": "$--foreground"
},
{
"type": "text",
"id": "ddi06",
"content": "Another paragraph of content below.",
"fontSize": 15,
"fill": "$--foreground"
}
]
}
]
},
{
"type": "frame",
"id": "ddi10",
"name": "Drag & Drop — Drag Over State",
"x": 860,
"y": 0,
"width": 760,
"height": 500,
"fill": "$--background",
"layout": "vertical",
"children": [
{
"type": "text",
"id": "ddi11",
"content": "Editor — Image Dragged Over",
"fontSize": 13,
"fontWeight": 600,
"fill": "$--muted-foreground",
"padding": 16
},
{
"type": "frame",
"id": "ddi12",
"name": "Editor Content Area (drag-over)",
"layout": "vertical",
"width": "fill_container",
"height": "fill_container",
"padding": 40,
"gap": 8,
"stroke": "$--primary",
"strokeWidth": 2,
"strokeStyle": "dashed",
"fill": "rgba(21, 93, 255, 0.06)",
"cornerRadius": 0,
"children": [
{
"type": "text",
"id": "ddi13",
"content": "# My Note Title",
"fontSize": 24,
"fontWeight": 700,
"fill": "$--foreground"
},
{
"type": "text",
"id": "ddi14",
"content": "Some paragraph text in the editor.",
"fontSize": 15,
"fill": "$--foreground"
},
{
"type": "frame",
"id": "ddi15",
"name": "Drop Overlay Label",
"layout": "horizontal",
"padding": 12,
"cornerRadius": 8,
"fill": "$--background",
"alignSelf": "center",
"children": [
{
"type": "text",
"id": "ddi16",
"content": "Drop image here",
"fontSize": 14,
"fontWeight": 500,
"fill": "$--primary"
}
]
},
{
"type": "text",
"id": "ddi17",
"content": "Another paragraph of content below.",
"fontSize": 15,
"fill": "$--foreground"
}
]
}
]
},
{
"type": "frame",
"id": "ddi20",
"name": "Drag & Drop — Image Inserted",
"x": 0,
"y": 600,
"width": 760,
"height": 560,
"fill": "$--background",
"layout": "vertical",
"children": [
{
"type": "text",
"id": "ddi21",
"content": "Editor — After Image Drop",
"fontSize": 13,
"fontWeight": 600,
"fill": "$--muted-foreground",
"padding": 16
},
{
"type": "frame",
"id": "ddi22",
"name": "Editor Content Area (with image)",
"layout": "vertical",
"fill": "$--background",
"width": "fill_container",
"height": "fill_container",
"padding": 40,
"gap": 8,
"children": [
{
"type": "text",
"id": "ddi23",
"content": "# My Note Title",
"fontSize": 24,
"fontWeight": 700,
"fill": "$--foreground"
},
{
"type": "text",
"id": "ddi24",
"content": "Some paragraph text in the editor.",
"fontSize": 15,
"fill": "$--foreground"
},
{
"type": "frame",
"id": "ddi25",
"name": "Inserted Image Block",
"width": "fill_container",
"height": 200,
"fill": "#E8E8E8",
"cornerRadius": 4,
"layout": "vertical",
"children": [
{
"type": "text",
"id": "ddi26",
"content": "[Dropped image preview — photo.jpg]",
"fontSize": 13,
"fill": "$--muted-foreground",
"textAlign": "center"
}
]
},
{
"type": "text",
"id": "ddi27",
"content": "Another paragraph of content below the inserted image.",
"fontSize": 15,
"fill": "$--foreground"
}
]
}
]
}
],
"variables": {},
"themes": {}
}

185
e2e/image-drag-drop.spec.ts Normal file
View File

@@ -0,0 +1,185 @@
import { test, expect } from '@playwright/test'
import * as path from 'path'
import * as fs from 'fs'
// Minimal valid PNG: 1x1 red pixel
const TEST_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
function createTestImage(filepath: string) {
fs.mkdirSync(path.dirname(filepath), { recursive: true })
fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64'))
}
test('drag & drop image into editor inserts image block', async ({ page }) => {
await page.goto('/')
await page.waitForTimeout(1000)
// Open a note
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
await page.waitForTimeout(500)
const editor = page.locator('.bn-editor')
await expect(editor).toBeVisible({ timeout: 10000 })
await editor.click()
await page.waitForTimeout(200)
// Create a test image file
const testImagePath = path.join(process.cwd(), 'test-results', 'drag-test-image.png')
createTestImage(testImagePath)
// Screenshot before
await page.screenshot({ path: 'test-results/drag-drop-before.png', fullPage: true })
// Simulate drag-and-drop of a file into the editor
// Playwright supports dispatching drag events with DataTransfer
const editorContainer = page.locator('.editor__blocknote-container')
const box = await editorContainer.boundingBox()
expect(box).toBeTruthy()
// Read the file as a buffer for the DataTransfer
const fileBuffer = fs.readFileSync(testImagePath)
// Use Playwright's page.dispatchEvent with a custom script to simulate file drop
await page.evaluate(async ({ base64, x, y }) => {
const container = document.querySelector('.editor__blocknote-container')
if (!container) throw new Error('Editor container not found')
// Convert base64 to Uint8Array
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
// Create a File object
const file = new File([bytes], 'drag-test-image.png', { type: 'image/png' })
// Create DataTransfer with the file
const dt = new DataTransfer()
dt.items.add(file)
// Dispatch dragover first (to set the drop effect)
const dragOverEvent = new DragEvent('dragover', {
dataTransfer: dt,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
})
container.dispatchEvent(dragOverEvent)
// Then dispatch drop
const dropEvent = new DragEvent('drop', {
dataTransfer: dt,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
})
container.dispatchEvent(dropEvent)
}, {
base64: TEST_PNG_BASE64,
x: box!.x + box!.width / 2,
y: box!.y + box!.height / 2,
})
// Wait for the image to be uploaded and inserted
await page.waitForTimeout(2000)
// Verify: image element exists in the editor
const images = page.locator('.bn-editor img')
await expect(images.first()).toBeVisible({ timeout: 5000 })
// Verify: image uses data URL (browser mode)
const src = await images.first().getAttribute('src')
expect(src).toMatch(/^data:/)
await page.screenshot({ path: 'test-results/drag-drop-after.png', fullPage: true })
// Clean up
if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath)
})
test('drop zone overlay appears during image drag', async ({ page }) => {
await page.goto('/')
await page.waitForTimeout(1000)
// Open a note
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
await page.waitForTimeout(500)
const editor = page.locator('.bn-editor')
await expect(editor).toBeVisible({ timeout: 10000 })
const editorContainer = page.locator('.editor__blocknote-container')
const box = await editorContainer.boundingBox()
expect(box).toBeTruthy()
// Simulate dragover with an image file to trigger overlay
await page.evaluate(({ x, y }) => {
const container = document.querySelector('.editor__blocknote-container')
if (!container) throw new Error('Editor container not found')
const file = new File([new Uint8Array(1)], 'test.png', { type: 'image/png' })
const dt = new DataTransfer()
dt.items.add(file)
const event = new DragEvent('dragover', {
dataTransfer: dt,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
})
container.dispatchEvent(event)
}, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 })
// The drop overlay should be visible
const overlay = page.locator('.editor__drop-overlay')
await expect(overlay).toBeVisible({ timeout: 2000 })
await expect(overlay).toContainText('Drop image here')
await page.screenshot({ path: 'test-results/drag-drop-overlay.png', fullPage: true })
})
test('non-image file drop is ignored', async ({ page }) => {
await page.goto('/')
await page.waitForTimeout(1000)
// Open a note
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
await page.waitForTimeout(500)
const editor = page.locator('.bn-editor')
await expect(editor).toBeVisible({ timeout: 10000 })
const editorContainer = page.locator('.editor__blocknote-container')
const box = await editorContainer.boundingBox()
expect(box).toBeTruthy()
// Count images before
const imagesBefore = await page.locator('.bn-editor img').count()
// Simulate dropping a text file
await page.evaluate(({ x, y }) => {
const container = document.querySelector('.editor__blocknote-container')
if (!container) throw new Error('Editor container not found')
const file = new File(['not an image'], 'readme.txt', { type: 'text/plain' })
const dt = new DataTransfer()
dt.items.add(file)
container.dispatchEvent(new DragEvent('drop', {
dataTransfer: dt,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
}))
}, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 })
await page.waitForTimeout(500)
// No new images should have been added
const imagesAfter = await page.locator('.bn-editor img').count()
expect(imagesAfter).toBe(imagesBefore)
})