openpencil/tests/e2e/components/library-lifecycle.spec.ts
2026-08-14 19:31:41 +03:00

274 lines
11 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
async function openAssets(page: Page) {
await page.getByTestId('left-panel-assets-tab').click()
}
async function openLibraries(page: Page) {
await page.getByRole('button', { name: 'Manage libraries' }).click()
}
test('preserves source publication identity across FIG save and reopen', async ({ page }) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test')
await canvas.waitForInit()
const componentId = 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 missing')
return store.graph.createNode('COMPONENT', pageNode.id, {
name: 'Source button',
componentKey: 'source-button',
width: 80,
height: 32
}).id
})
await openAssets(page)
await openLibraries(page)
await page.getByRole('button', { name: 'Publish library' }).click()
const publish = page.getByRole('dialog').filter({ hasText: 'Publish library' })
await publish.getByLabel('Library ID').fill('source-lifecycle')
await publish.getByLabel('Library name').fill('Source lifecycle')
await publish.getByRole('button', { name: 'Publish library' }).click()
await expect(publish.getByLabel('Library ID')).toBeHidden()
await page.keyboard.press('Escape')
await expect(page.getByTestId('asset-libraries-dialog')).toBeHidden()
const firstRevision = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
const value = store?.graph
.getNode(store.graph.rootId)
?.pluginData.find((entry) => entry.key === 'sourceLibraryPublication')?.value
return value ? (JSON.parse(value) as { revisionId: string }).revisionId : null
})
expect(firstRevision).toBeTruthy()
const saved = await page.evaluate(async () => {
const writes: Uint8Array[] = []
window.showSaveFilePicker = async () =>
({
name: 'source.fig',
getFile: async () => new File([], 'source.fig'),
createWritable: async () => ({
write: async (data: Uint8Array) => writes.push(data),
close: async () => undefined
})
}) as FileSystemFileHandle
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
await store.saveFigFileAs()
return Array.from(writes[0] ?? [])
})
expect(saved.length).toBeGreaterThan(0)
await page.evaluate((bytes) => {
const file = new File([new Uint8Array(bytes)], 'source.fig')
window.showOpenFilePicker = async () => [{ getFile: async () => file } as FileSystemFileHandle]
}, saved)
await page.keyboard.press('ControlOrMeta+KeyO')
await expect
.poll(() =>
page.evaluate(() => {
const store = window.openPencil?.getStore?.()
const root = store?.graph.getNode(store.graph.rootId)
return root?.pluginData.find((entry) => entry.key === 'sourceLibraryPublication')?.value
})
)
.toContain('source-lifecycle')
await openAssets(page)
await openLibraries(page)
await page.getByRole('button', { name: 'Publish library' }).click()
await expect(publish.getByLabel('Library ID')).toHaveValue('source-lifecycle')
await expect(publish.getByLabel('Library ID')).toBeDisabled()
await expect(publish.getByText('No asset changes')).toBeVisible()
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.evaluate((sourceId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const component = [...store.graph.getAllNodes()].find(
(node) => node.source.id === sourceId || node.componentKey === 'source-button'
)
if (!component) throw new Error('Reopened source component missing')
store.graph.updateNode(component.id, { width: 144, name: 'Source button updated' })
}, componentId)
await openAssets(page)
await openLibraries(page)
await page.getByRole('button', { name: 'Publish library' }).click()
await expect(publish.getByLabel('Source button updated')).toBeChecked()
await expect(publish.getByText('Modified')).toBeVisible()
await publish.getByRole('button', { name: 'Publish library' }).click()
await expect(publish.getByLabel('Library ID')).toBeHidden()
const secondRevision = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
const value = store?.graph
.getNode(store.graph.rootId)
?.pluginData.find((entry) => entry.key === 'sourceLibraryPublication')?.value
return value ? (JSON.parse(value) as { revisionId: string }).revisionId : null
})
expect(secondRevision).toBeTruthy()
expect(secondRevision).not.toBe(firstRevision)
})
test('publishes, consumes, saves, and reopens a multidimensional library instance offline', async ({
page
}) => {
const canvas = new CanvasHelper(page)
await page.goto('/?test')
await canvas.waitForInit()
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',
componentPropertyDefinitions: [
{
id: 'size',
name: 'Size',
type: 'VARIANT',
defaultValue: 'Small',
variantOptions: ['Small', 'Large']
},
{
id: 'tone',
name: 'Tone',
type: 'VARIANT',
defaultValue: 'Neutral',
variantOptions: ['Neutral', 'Brand']
}
]
})
for (const [index, [size, tone]] of [
['Small', 'Neutral'],
['Small', 'Brand'],
['Large', 'Neutral'],
['Large', 'Brand']
].entries()) {
const component = store.graph.createNode('COMPONENT', set.id, {
name: `Size=${size}, Tone=${tone}`,
x: (index % 2) * 160,
y: Math.floor(index / 2) * 80,
width: size === 'Large' ? 144 : 96,
height: size === 'Large' ? 48 : 32,
componentPropertyValues: { Size: size, Tone: tone }
})
store.graph.createNode('TEXT', component.id, {
name: 'Label',
text: `${size} ${tone}`,
width: 90,
height: 20
})
}
store.requestRender()
})
await openAssets(page)
await openLibraries(page)
await page.getByRole('button', { name: 'Publish library' }).click()
const publish = page.getByRole('dialog').filter({ hasText: 'Publish library' })
await publish.getByLabel('Library ID').fill('e2e-design-system')
await publish.getByLabel('Library name').fill('E2E design system')
await publish.getByRole('button', { name: 'Publish library' }).click()
await expect(publish.getByLabel('Library ID')).toBeHidden()
await page.keyboard.press('Escape')
await expect(page.getByTestId('asset-libraries-dialog')).toBeHidden()
await page.keyboard.press('ControlOrMeta+KeyN')
await expect
.poll(() =>
page.evaluate(() => {
const store = window.openPencil?.getStore?.()
return store ? [...store.graph.getAllNodes()].length : 0
})
)
.toBeGreaterThan(0)
await openAssets(page)
await openLibraries(page)
const libraryRow = page.getByText('E2E design system').locator('..').locator('..')
await libraryRow.getByRole('button', { name: 'Enable library' }).click()
await libraryRow.getByRole('button', { name: 'Prefer this library' }).click()
await page.keyboard.press('Escape')
await expect(page.getByTestId('asset-libraries-dialog')).toBeHidden()
await page.getByRole('button', { name: 'List view' }).click()
const asset = page.getByTestId('asset-item').filter({ hasText: 'Button' })
await asset.getByTestId('asset-insert').click()
await canvas.waitForRender()
const variants = page.getByRole('region', { name: 'Variants' })
await variants.getByRole('combobox', { name: 'Size' }).click()
await page.getByRole('option', { name: 'Large' }).click()
await variants.getByRole('combobox', { name: 'Tone' }).click()
await page.getByRole('option', { name: 'Brand' }).click()
const saved = await page.evaluate(async () => {
const writes: Uint8Array[] = []
window.showSaveFilePicker = async () =>
({
name: 'consumer.fig',
getFile: async () => new File([], 'consumer.fig'),
createWritable: async () => ({
write: async (data: Uint8Array) => writes.push(data),
close: async () => undefined
})
}) as FileSystemFileHandle
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
await store.saveFigFileAs()
return Array.from(writes[0] ?? [])
})
expect(saved.length).toBeGreaterThan(0)
await page.evaluate(async (bytes) => {
const file = new File([new Uint8Array(bytes)], 'consumer.fig')
const originalFetch = window.fetch
window.fetch = async (...args) => {
const url = String(args[0])
if (url.includes('library')) throw new Error('offline')
return originalFetch(...args)
}
window.showOpenFilePicker = async () => [{ getFile: async () => file } as FileSystemFileHandle]
}, saved)
await page.keyboard.press('ControlOrMeta+KeyO')
await expect
.poll(() =>
page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) return null
const instance = [...store.graph.getAllNodes()].find((node) => node.type === 'INSTANCE')
const component = instance?.componentId ? store.graph.getNode(instance.componentId) : null
const definitions = [...store.graph.getAllNodes()].filter((node) => node.librarySource)
return instance && component
? {
values: component.componentPropertyValues,
libraryId: component.librarySource?.identity.libraryId,
revisionId: component.librarySource?.identity.revisionId,
readOnly: component.librarySource?.readOnly,
definitions: definitions.map((node) => ({
name: node.name,
libraryId: node.librarySource?.identity.libraryId
})),
enabled: store.graph.enabledLibraries.get('e2e-design-system')?.enabled
}
: null
})
)
.toMatchObject({
values: { Size: 'Large', Tone: 'Brand' },
definitions: expect.arrayContaining([
expect.objectContaining({ libraryId: 'e2e-design-system' })
])
})
await expect(page.getByText(/send was called before connect/i)).toHaveCount(0)
})