openpencil/tests/e2e/components/assets-panel.spec.ts
Danila Poyarkov 3ea63ad09f fix: refresh Undo and Redo command availability
Publish committed history changes independently of scene mutations so menus observe history recorded after the final draw. Align the assets regression with the documented top-left default.
2026-09-14 00:25:36 +03:00

336 lines
12 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { expectDefined } from '#tests/helpers/assert'
import { CanvasHelper } from '#tests/helpers/canvas'
async function selectedNodeSnapshot(page: Page) {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const selectedId = [...store.state.selectedIds][0]
const selected = selectedId ? store.graph.getNode(selectedId) : null
return selected
? {
id: selected.id,
type: selected.type,
parentId: selected.parentId,
componentId: selected.componentId,
pageId: store.state.currentPageId,
width: selected.width,
childTexts: store.graph
.getChildren(selected.id)
.filter((child) => child.type === 'TEXT')
.map((child) => child.text)
}
: null
})
}
test('assets panel groups component sets and inserts the default variant', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test')
await canvas.waitForInit()
const ids = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageNode = store.graph.getNode(store.state.currentPageId)
if (!pageNode) throw new Error('Current page not found')
const set = store.graph.createNode('COMPONENT_SET', pageNode.id, {
name: 'Button',
x: 80,
y: 80,
width: 260,
height: 100,
sourceLibraryKey: 'lk-test-library',
symbolDescription: 'Reusable button component',
symbolLinks: [{ uri: 'https://example.com/button', displayName: 'Button docs' }],
componentPropertyDefinitions: [
{
id: 'prop:type',
name: 'Type',
type: 'VARIANT',
defaultValue: 'Secondary',
variantOptions: ['Primary', 'Secondary']
}
]
})
const primary = store.graph.createNode('COMPONENT', set.id, {
name: 'Type=Primary',
x: 0,
y: 0,
width: 96,
height: 40,
componentPropertyValues: { Type: 'Primary' }
})
store.graph.createNode('TEXT', primary.id, {
name: 'Label',
text: 'Primary',
width: 72,
height: 20
})
const secondary = store.graph.createNode('COMPONENT', set.id, {
name: 'Type=Secondary',
x: 120,
y: 0,
width: 132,
height: 40,
componentPropertyValues: { Type: 'Secondary' }
})
store.graph.createNode('TEXT', secondary.id, {
name: 'Label',
text: 'Secondary',
width: 96,
height: 20
})
const duplicateSecondary = store.graph.createNode('COMPONENT', set.id, {
name: 'Type=Secondary duplicate',
x: 280,
y: 0,
width: 132,
height: 40,
componentPropertyValues: { Type: 'Secondary' }
})
const card = store.graph.createNode('COMPONENT', pageNode.id, {
name: 'Card',
x: 80,
y: 240,
width: 160,
height: 100
})
store.requestRender()
return {
setId: set.id,
primaryId: primary.id,
secondaryId: secondary.id,
duplicateSecondaryId: duplicateSecondary.id,
cardId: card.id
}
})
await canvas.waitForRender()
await page.getByTestId('left-panel-assets-tab').click()
await page.getByRole('button', { name: 'List view' }).click()
const assetsPanel = page.getByTestId('assets-panel')
const assetItems = page.getByTestId('asset-item')
await expect(assetItems).toHaveCount(2)
await expect(assetsPanel).toContainText('Button')
await expect(assetsPanel).toContainText('Card')
await expect(assetsPanel).not.toContainText('Type=Primary')
await expect(assetsPanel).not.toContainText('Type=Secondary')
const buttonAsset = page.locator(`[data-asset-id="${ids.setId}"]`)
await expect(buttonAsset.getByTestId('asset-variant-summary')).toContainText('Type')
await expect(buttonAsset.getByTestId('asset-library-badge')).toContainText('Library')
await expect(buttonAsset.getByTestId('asset-description')).toContainText(
'Reusable button component'
)
await expect(buttonAsset.getByTestId('asset-docs')).toBeVisible()
await expect(buttonAsset.getByTestId('asset-variant-conflict')).toContainText(
'Duplicate variant values'
)
await buttonAsset.click()
const details = page.getByTestId('asset-details-dialog')
await expect(details).toBeVisible()
await expect(details).toContainText('Button')
await expect(details.getByTestId('asset-details-preview')).toBeVisible()
await expect(details.getByTestId('asset-details-preview-image')).toBeVisible()
await expect(details.getByTestId('asset-details-description')).toContainText(
'Reusable button component'
)
await expect(details.getByTestId('asset-details-library')).toContainText('lk-test-library')
await expect(details.getByTestId('asset-details-docs')).toBeVisible()
await expect(details.getByTestId('asset-details-property')).toContainText('Type')
await page.getByTestId('asset-details-close').click()
await expect(details).toBeHidden()
await page.getByTestId('assets-search').fill('card')
await expect(assetItems).toHaveCount(1)
await expect(assetsPanel).toContainText('Card')
await page.locator(`[data-asset-id="${ids.cardId}"]`).getByTestId('asset-insert').click()
await canvas.waitForRender()
const cardInstance = await selectedNodeSnapshot(page)
expect(cardInstance?.type).toBe('INSTANCE')
expect(cardInstance?.componentId).toBe(ids.cardId)
await page.getByTestId('assets-search').fill('missing asset')
await expect(page.getByTestId('assets-empty')).toBeVisible()
await expect(assetItems).toHaveCount(0)
await page.getByTestId('assets-search').fill('button')
await expect(assetItems).toHaveCount(1)
await page.locator(`[data-asset-id="${ids.setId}"]`).getByTestId('asset-insert').click()
await canvas.waitForRender()
const inserted = await selectedNodeSnapshot(page)
expect(inserted?.type).toBe('INSTANCE')
// The spatially first variant is the documented default, regardless of property defaults.
expect(inserted?.componentId).toBe(ids.primaryId)
expect(inserted?.parentId).toBe(inserted?.pageId)
expect(inserted?.width).toBe(96)
expect(inserted?.childTexts).toEqual(['Primary'])
const variantSection = page.getByRole('region', { name: 'Variants' })
await expect(variantSection).toBeVisible()
await variantSection.getByRole('combobox', { name: 'Type' }).click()
await page.getByRole('option', { name: 'Secondary' }).click()
expectDefined(inserted?.id, 'inserted instance id')
const switched = await selectedNodeSnapshot(page)
expect(switched?.componentId).toBe(ids.secondaryId)
expect(switched?.width).toBe(132)
expect(switched?.childTexts).toEqual(['Secondary'])
canvas.assertNoErrors()
})
test('assets panel supports Figma-style views, component actions, and canvas drag', async ({
page
}) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test')
await canvas.waitForInit()
const setup = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.state.panX = 0
store.state.panY = 0
store.state.zoom = 1
const currentPage = store.graph.getNode(store.state.currentPageId)
if (!currentPage) throw new Error('Current page not found')
const secondPage = store.graph.addPage('Components')
const component = store.graph.createNode('COMPONENT', secondPage.id, {
name: 'Toolbar button',
x: 100,
y: 120,
width: 96,
height: 40
})
store.requestRender()
return { currentPageId: currentPage.id, secondPageId: secondPage.id, componentId: component.id }
})
await canvas.waitForRender()
await page.getByTestId('left-panel-assets-tab').click()
const asset = page.locator(`[data-asset-id="${setup.componentId}"]`)
await expect(page.getByRole('heading', { name: 'Components' })).toBeVisible()
await expect(asset.locator('[data-slot="asset-thumbnail"]')).toHaveClass(/size-24/)
await page.getByRole('button', { name: 'List view' }).click()
await expect(asset.locator('[data-slot="asset-thumbnail"]')).toHaveClass(/size-10/)
await asset.click({ button: 'right' })
await page.getByTestId('asset-context-go-to-main').click()
await expect
.poll(() =>
page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return {
pageId: store.state.currentPageId,
selectedId: [...store.state.selectedIds][0]
}
})
)
.toEqual({ pageId: setup.secondPageId, selectedId: setup.componentId })
await page.getByTestId('left-panel-assets-tab').click()
const canvasBox = expectDefined(await canvas.canvas.boundingBox(), 'canvas bounds')
await page
.locator(`[data-asset-id="${setup.componentId}"]`)
.dragTo(canvas.canvas, { targetPosition: { x: canvasBox.width / 2, y: canvasBox.height / 2 } })
await canvas.waitForRender()
const inserted = await selectedNodeSnapshot(page)
expect(inserted?.type).toBe('INSTANCE')
expect(inserted?.componentId).toBe(setup.componentId)
canvas.assertNoErrors()
})
test('assets insertion accounts for entered container coordinates', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test')
await canvas.waitForInit()
const setup = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.state.panX = 0
store.state.panY = 0
store.state.zoom = 1
const pageNode = store.graph.getNode(store.state.currentPageId)
if (!pageNode) throw new Error('Current page not found')
const frame = store.graph.createNode('FRAME', pageNode.id, {
name: 'Target Frame',
x: 200,
y: 150,
width: 300,
height: 240
})
const component = store.graph.createNode('COMPONENT', pageNode.id, {
name: 'Panel Card',
x: 40,
y: 40,
width: 120,
height: 60
})
store.state.enteredContainerId = frame.id
store.requestRender()
return { frameId: frame.id, componentId: component.id }
})
await canvas.waitForRender()
await page.getByTestId('left-panel-assets-tab').click()
await page.getByRole('button', { name: 'List view' }).click()
await page.locator(`[data-asset-id="${setup.componentId}"]`).getByTestId('asset-insert').click()
await canvas.waitForRender()
const inserted = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const selectedId = [...store.state.selectedIds][0]
const selected = selectedId ? store.graph.getNode(selectedId) : null
if (!selected) return null
const abs = store.graph.getAbsolutePosition(selected.id)
const center = store.screenToCanvas(
...(Object.values(store.viewportCanvasCenter()) as [number, number])
)
return {
parentId: selected.parentId,
centerX: abs.x + selected.width / 2,
centerY: abs.y + selected.height / 2,
expectedCenterX: center.x,
expectedCenterY: center.y
}
})
expect(inserted?.parentId).toBe(setup.frameId)
expect(inserted?.centerX).toBeCloseTo(inserted?.expectedCenterX ?? 0, 1)
expect(inserted?.centerY).toBeCloseTo(inserted?.expectedCenterY ?? 0, 1)
canvas.assertNoErrors()
})
test('demo exposes component set assets', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/demo')
await canvas.waitForInit()
await page.getByTestId('left-panel-assets-tab').click()
await page.getByRole('button', { name: 'List view' }).click()
const assetsPanel = page.getByTestId('assets-panel')
await expect(assetsPanel).toContainText('Button')
await expect(assetsPanel).toContainText('2 variants · Variant')
await expect(assetsPanel).toContainText('Avatar')
await expect(assetsPanel).not.toContainText('Button/Primary')
canvas.assertNoErrors()
})