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:
222
design/drag-drop-images.pen
Normal file
222
design/drag-drop-images.pen
Normal 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
185
e2e/image-drag-drop.spec.ts
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user