2026-07-01 07:40:29 +00:00
|
|
|
import type { Page } from '@playwright/test'
|
|
|
|
|
|
|
|
|
|
import { expect, test } from '#tests/e2e/fixtures'
|
|
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
2026-08-15 11:17:05 +00:00
|
|
|
import { installTauriClipboardMock } from '#tests/helpers/tauri/clipboard-mock'
|
2026-07-01 07:40:29 +00:00
|
|
|
|
|
|
|
|
type PageChildSummary = {
|
|
|
|
|
name: string
|
|
|
|
|
type: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTauriEditorPage(page: Page) {
|
2026-08-15 11:17:05 +00:00
|
|
|
const clipboard = await installTauriClipboardMock(page)
|
2026-07-01 07:40:29 +00:00
|
|
|
await page.goto('/')
|
|
|
|
|
const canvas = new CanvasHelper(page)
|
|
|
|
|
await canvas.waitForInit()
|
2026-08-15 11:17:05 +00:00
|
|
|
return { canvas, clipboard }
|
2026-07-01 07:40:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pageChildren(page: Page): Promise<PageChildSummary[]> {
|
|
|
|
|
return page.evaluate(() => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
|
|
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
|
|
|
return store.graph.getChildren(store.state.currentPageId).map((node) => ({
|
|
|
|
|
name: node.name,
|
|
|
|
|
type: node.type
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedCount(page: Page): Promise<number> {
|
|
|
|
|
return page.evaluate(() => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
|
|
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
|
|
|
return store.state.selectedIds.size
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 23:13:30 +00:00
|
|
|
async function dispatchClipboardEvent(
|
|
|
|
|
page: Page,
|
|
|
|
|
type: 'copy' | 'cut' | 'paste',
|
|
|
|
|
target = 'window'
|
|
|
|
|
) {
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
({ eventType, selector }) => {
|
|
|
|
|
const eventTarget = selector === 'window' ? window : document.querySelector(selector)
|
|
|
|
|
if (!eventTarget) throw new Error(`Clipboard event target not found: ${selector}`)
|
|
|
|
|
eventTarget.dispatchEvent(new ClipboardEvent(eventType, { bubbles: true, cancelable: true }))
|
|
|
|
|
},
|
|
|
|
|
{ eventType: type, selector: target }
|
|
|
|
|
)
|
2026-07-01 07:40:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('Tauri copy writes selected design HTML without requiring ClipboardEvent.clipboardData', async ({
|
|
|
|
|
page
|
|
|
|
|
}) => {
|
2026-08-15 11:17:05 +00:00
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
2026-07-01 07:40:29 +00:00
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
|
|
|
|
|
|
|
|
|
await dispatchClipboardEvent(page, 'copy')
|
|
|
|
|
|
2026-08-15 11:17:05 +00:00
|
|
|
await expect.poll(() => clipboard.snapshot()).toMatchObject({ text: 'Rectangle' })
|
|
|
|
|
const snapshot = clipboard.snapshot()
|
|
|
|
|
expect(snapshot.html).toContain('(figma)')
|
|
|
|
|
expect(snapshot.writes.some((entry) => entry.cmd === 'plugin:clipboard-manager|write_html')).toBe(
|
|
|
|
|
true
|
|
|
|
|
)
|
2026-07-01 07:40:29 +00:00
|
|
|
await expect(page.getByText('Clipboard access is blocked in this browser context')).toHaveCount(0)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('Tauri paste restores copied design data from plugin clipboard text', async ({ page }) => {
|
2026-08-15 11:17:05 +00:00
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
2026-07-01 07:40:29 +00:00
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
|
|
|
|
await dispatchClipboardEvent(page, 'copy')
|
2026-08-15 11:17:05 +00:00
|
|
|
await expect.poll(() => clipboard.snapshot()).toMatchObject({ text: 'Rectangle' })
|
2026-07-01 07:40:29 +00:00
|
|
|
|
|
|
|
|
expect(await pageChildren(page)).toHaveLength(1)
|
|
|
|
|
await canvas.deleteSelection()
|
|
|
|
|
expect(await pageChildren(page)).toHaveLength(0)
|
|
|
|
|
|
|
|
|
|
await dispatchClipboardEvent(page, 'paste')
|
|
|
|
|
await canvas.waitForRender()
|
|
|
|
|
|
|
|
|
|
const children = await pageChildren(page)
|
|
|
|
|
expect(children).toEqual([{ name: 'Rectangle', type: 'RECTANGLE' }])
|
|
|
|
|
expect(await selectedCount(page)).toBe(1)
|
|
|
|
|
await expect(page.getByText('Clipboard access is blocked in this browser context')).toHaveCount(0)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-15 11:17:05 +00:00
|
|
|
test('Tauri cut writes design data and deletes the selection', async ({ page }) => {
|
|
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
2026-07-01 07:40:29 +00:00
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
|
|
|
|
|
|
|
|
|
await dispatchClipboardEvent(page, 'cut')
|
|
|
|
|
await canvas.waitForRender()
|
|
|
|
|
|
|
|
|
|
await expect.poll(() => pageChildren(page)).toEqual([])
|
2026-08-15 11:17:05 +00:00
|
|
|
const snapshot = clipboard.snapshot()
|
|
|
|
|
expect(snapshot.html).toContain('(figma)')
|
|
|
|
|
expect(snapshot.text).toBe('Rectangle')
|
2026-07-01 07:40:29 +00:00
|
|
|
await expect(page.getByText('Clipboard access is blocked in this browser context')).toHaveCount(0)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-15 11:17:05 +00:00
|
|
|
test('Tauri cut keeps the selection when the clipboard write fails', async ({ page }) => {
|
|
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
|
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
|
|
|
|
clipboard.rejectHTMLWrites()
|
|
|
|
|
|
|
|
|
|
await dispatchClipboardEvent(page, 'cut')
|
|
|
|
|
await canvas.waitForRender()
|
|
|
|
|
|
|
|
|
|
await expect.poll(() => pageChildren(page)).toEqual([{ name: 'Rectangle', type: 'RECTANGLE' }])
|
|
|
|
|
expect(await selectedCount(page)).toBe(1)
|
|
|
|
|
const snapshot = clipboard.snapshot()
|
|
|
|
|
expect(snapshot.html).toBe('')
|
|
|
|
|
expect(snapshot.text).toBe('')
|
|
|
|
|
expect(snapshot.writes.some((entry) => entry.cmd === 'plugin:clipboard-manager|write_html')).toBe(
|
|
|
|
|
true
|
|
|
|
|
)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 23:13:30 +00:00
|
|
|
test('Tauri clipboard events from editable fields keep native text behavior', async ({ page }) => {
|
2026-08-15 11:17:05 +00:00
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
2026-08-11 23:13:30 +00:00
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
2026-08-15 11:17:05 +00:00
|
|
|
const before = clipboard.snapshot()
|
2026-08-11 23:13:30 +00:00
|
|
|
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
const host = document.createElement('div')
|
|
|
|
|
host.innerHTML =
|
2026-08-12 10:12:51 +00:00
|
|
|
'<input data-clipboard-probe="input"><textarea data-clipboard-probe="textarea"></textarea><div contenteditable="true"><span>Text</span></div>'
|
2026-08-11 23:13:30 +00:00
|
|
|
document.body.append(host)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-12 10:12:51 +00:00
|
|
|
await dispatchClipboardEvent(page, 'copy', '[data-clipboard-probe="input"]')
|
2026-08-15 11:17:05 +00:00
|
|
|
expect(clipboard.snapshot()).toEqual(before)
|
2026-08-12 10:12:51 +00:00
|
|
|
await dispatchClipboardEvent(page, 'copy', '[data-clipboard-probe="textarea"]')
|
2026-08-15 11:17:05 +00:00
|
|
|
expect(clipboard.snapshot()).toEqual(before)
|
2026-08-11 23:13:30 +00:00
|
|
|
await dispatchClipboardEvent(page, 'cut', '[contenteditable] span')
|
2026-08-15 11:17:05 +00:00
|
|
|
expect(clipboard.snapshot()).toEqual(before)
|
2026-08-11 23:13:30 +00:00
|
|
|
await dispatchClipboardEvent(page, 'paste', '[contenteditable] span')
|
|
|
|
|
|
2026-08-15 11:17:05 +00:00
|
|
|
expect(clipboard.snapshot()).toEqual(before)
|
2026-08-11 23:13:30 +00:00
|
|
|
expect(await pageChildren(page)).toHaveLength(1)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-01 07:40:29 +00:00
|
|
|
test('Tauri context-menu copy uses plugin clipboard fallback instead of blocked browser command', async ({
|
|
|
|
|
page
|
|
|
|
|
}) => {
|
2026-08-15 11:17:05 +00:00
|
|
|
const { canvas, clipboard } = await createTauriEditorPage(page)
|
2026-07-01 07:40:29 +00:00
|
|
|
await canvas.drawRect(160, 160, 96, 72)
|
|
|
|
|
|
|
|
|
|
const box = await canvas.canvas.boundingBox()
|
|
|
|
|
if (!box) throw new Error('Canvas has no bounding box')
|
|
|
|
|
await page.mouse.click(box.x + 190, box.y + 190, { button: 'right' })
|
|
|
|
|
await page.getByTestId('context-copy').click()
|
|
|
|
|
|
2026-08-15 11:17:05 +00:00
|
|
|
await expect.poll(() => clipboard.snapshot()).toMatchObject({ text: 'Rectangle' })
|
2026-07-01 07:40:29 +00:00
|
|
|
await expect(page.getByText('Clipboard access is blocked in this browser context')).toHaveCount(0)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|