feat: sync interactive image previews
Engine CI / test (push) Waiting to run

This commit is contained in:
2026-08-30 22:20:30 +08:00
parent e3b0b2d576
commit e23fa413be
11 changed files with 1296 additions and 117 deletions
+126 -5
View File
@@ -5,7 +5,8 @@ test('landing page links to a rendered deck', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Engineering Slides', exact: true })).toBeVisible()
await page.getByRole('link', { name: 'easy-slides 快速开始' }).click()
await expect(page.getByRole('heading', { name: 'easy-slides', exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '演示者' })).toBeVisible()
await expect(page.getByRole('button', { name: '演示者' })).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Presenter Mode' })).toHaveCount(0)
})
test('presenter token unlocks, survives refresh, and can be locked', async ({ page }) => {
@@ -122,15 +123,63 @@ test('presenter sends image previews to every audience window', async ({ context
await expect(secondAudienceDialog).toBeVisible()
await expect(presenterDialog).toBeHidden()
await expect(audienceDialog.locator('img')).toHaveAttribute('src', /cover\.svg/)
const audiencePreview = audienceDialog.locator('img')
const secondAudiencePreview = secondAudienceDialog.locator('img')
const sharedPreviewSource = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200"><rect width="100%" height="100%" fill="#2563eb"/></svg>')}`
await Promise.all([
audiencePreview.evaluate((element, source) => {
;(element as HTMLImageElement).src = source
}, sharedPreviewSource),
secondAudiencePreview.evaluate((element, source) => {
;(element as HTMLImageElement).src = source
}, sharedPreviewSource),
])
await expect.poll(() => audiencePreview.evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(1600)
await expect.poll(() => secondAudiencePreview.evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(1600)
const initialScale = await audiencePreview.evaluate(element => Number(element.getAttribute('data-easy-image-scale')))
expect(initialScale).toBeGreaterThanOrEqual(0.1)
expect(initialScale).toBeLessThanOrEqual(1)
await audienceDialog.getByRole('button', { name: '放大图片' }).click()
await expect.poll(() => secondAudiencePreview.evaluate(element => Number(element.getAttribute('data-easy-image-scale'))))
.toBeCloseTo(initialScale + 0.1, 3)
const viewport = audienceDialog.locator('.easy-image-lightbox-viewport')
const viewportBox = await viewport.boundingBox()
expect(viewportBox).not.toBeNull()
await page.mouse.move(viewportBox!.x + viewportBox!.width / 2, viewportBox!.y + viewportBox!.height / 2)
await page.mouse.down()
await page.mouse.move(viewportBox!.x + viewportBox!.width / 2, viewportBox!.y + viewportBox!.height / 2 - 90, { steps: 5 })
await page.mouse.up()
const draggedCenterY = await audiencePreview.evaluate(element => Number(element.getAttribute('data-easy-image-center-y')))
expect(draggedCenterY).toBeGreaterThan(0.5)
await expect.poll(() => secondAudiencePreview.evaluate(element => Number(element.getAttribute('data-easy-image-center-y'))))
.toBeCloseTo(draggedCenterY, 3)
await page.getByRole('button', { name: '关闭图片全屏预览' }).click()
await secondAudience.getByRole('button', { name: '关闭图片全屏预览' }).click()
await expect(audienceDialog).toBeHidden()
await expect(secondAudienceDialog).toBeHidden()
await nextImage.press('Enter')
await expect(audienceDialog).toBeVisible()
await expect(secondAudienceDialog).toBeVisible()
await expect(presenterDialog).toBeHidden()
await page.keyboard.press('Escape')
await secondAudience.keyboard.press('Escape')
await expect(audienceDialog).toBeHidden()
await expect(secondAudienceDialog).toBeHidden()
await currentImage.click()
await expect(audienceDialog).toBeVisible()
await expect(secondAudienceDialog).toBeVisible()
await secondAudienceDialog.locator('.easy-image-lightbox-viewport').click({ position: { x: 4, y: 4 } })
await expect(audienceDialog).toBeHidden()
await expect(secondAudienceDialog).toBeHidden()
const localImage = page.locator('.slidev-layout img.easy-image-preview-trigger').first()
await localImage.click()
await expect(audienceDialog).toBeVisible()
await expect(secondAudienceDialog).toBeHidden()
await page.keyboard.press('Escape')
})
test('presenter opens an image locally when no audience acknowledges it', async ({ context, page }) => {
@@ -155,13 +204,16 @@ test('presenter opens an image locally when no audience acknowledges it', async
await auditPage.close()
})
test('present mode sends image previews across isolated browser contexts', async ({ browser }) => {
test('present mode syncs image preview views across isolated browser contexts', async ({ browser }) => {
const audienceContext = await browser.newContext()
const secondAudienceContext = await browser.newContext()
const presenterContext = await browser.newContext()
const audience = await audienceContext.newPage()
const secondAudience = await secondAudienceContext.newPage()
const presenter = await presenterContext.newPage()
try {
await audience.goto('http://127.0.0.1:4174/4')
await secondAudience.goto('http://127.0.0.1:4174/4')
await presenter.goto('http://127.0.0.1:4174/presenter/?password=easy-slides-local')
await presenter.getByLabel('演示者 token').fill('test-token')
await presenter.getByRole('button', { name: '解锁' }).click()
@@ -173,11 +225,24 @@ test('present mode sends image previews across isolated browser contexts', async
await expect(audience.getByRole('heading', { name: '智能图片' })).toBeVisible()
await current.locator('img.easy-image-preview-trigger').first().click()
await expect(audience.getByRole('dialog', { name: '图片全屏预览' })).toBeVisible()
const audienceDialog = audience.getByRole('dialog', { name: '图片全屏预览' })
const secondAudienceDialog = secondAudience.getByRole('dialog', { name: '图片全屏预览' })
await expect(audienceDialog).toBeVisible()
await expect(secondAudienceDialog).toBeVisible()
await expect(presenter.getByRole('dialog', { name: '图片全屏预览' })).toBeHidden()
await audienceDialog.getByRole('button', { name: '放大图片' }).click()
const firstScale = await audienceDialog.locator('img').evaluate(element => element.getAttribute('data-easy-image-scale'))
await expect.poll(() => secondAudienceDialog.locator('img').evaluate(element => element.getAttribute('data-easy-image-scale')))
.toBe(firstScale)
await secondAudienceDialog.getByRole('button', { name: '关闭图片全屏预览' }).click()
await expect(audienceDialog).toBeHidden()
await expect(secondAudienceDialog).toBeHidden()
}
finally {
await audienceContext.close()
await secondAudienceContext.close()
await presenterContext.close()
}
})
@@ -330,6 +395,62 @@ test('image fullscreen preserves layout and table scaling stays out of non-inter
expect(overlay).not.toBeNull()
expect(overlay!.width).toBe(viewport!.width)
expect(overlay!.height).toBe(viewport!.height)
const previewViewport = dialog.locator('.easy-image-lightbox-viewport')
const previewImage = dialog.locator('img')
const scaleValue = dialog.locator('.easy-image-scale-value')
const interactiveSource = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200"><rect width="100%" height="100%" fill="#2563eb"/></svg>')}`
await previewImage.evaluate((element, source) => {
;(element as HTMLImageElement).src = source
}, interactiveSource)
await expect.poll(() => previewImage.evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(1600)
const initialScale = await previewImage.evaluate(element => Number(element.getAttribute('data-easy-image-scale')))
expect(initialScale).toBeGreaterThanOrEqual(0.1)
expect(initialScale).toBeLessThanOrEqual(1)
await expect(scaleValue).toHaveText(`${Math.round(initialScale * 100)}%`)
const previewViewportBox = await previewViewport.boundingBox()
expect(previewViewportBox).not.toBeNull()
await page.mouse.move(
previewViewportBox!.x + previewViewportBox!.width * 0.25,
previewViewportBox!.y + previewViewportBox!.height * 0.25,
)
await page.mouse.wheel(0, -180)
await expect.poll(() => previewImage.evaluate(element => Number(element.getAttribute('data-easy-image-scale'))))
.toBeGreaterThan(initialScale)
await expect.poll(() => previewImage.evaluate(element => Number(element.getAttribute('data-easy-image-center-y'))))
.not.toBe(0.5)
const increase = dialog.getByRole('button', { name: '放大图片' })
await increase.evaluate((element) => {
for (let index = 0; index < 50; index += 1) {
const button = element as HTMLButtonElement
button.click()
}
})
await expect(scaleValue).toHaveText('400%')
await expect(increase).toBeDisabled()
const decrease = dialog.getByRole('button', { name: '缩小图片' })
await decrease.evaluate((element) => {
for (let index = 0; index < 50; index += 1) {
const button = element as HTMLButtonElement
button.click()
}
})
await expect(scaleValue).toHaveText('10%')
await expect(decrease).toBeDisabled()
const pinchCenterX = previewViewportBox!.x + previewViewportBox!.width / 2
const pinchCenterY = previewViewportBox!.y + previewViewportBox!.height / 2
await previewViewport.dispatchEvent('pointerdown', { buttons: 1, clientX: pinchCenterX - 40, clientY: pinchCenterY, pointerId: 1, pointerType: 'touch' })
await previewViewport.dispatchEvent('pointerdown', { buttons: 1, clientX: pinchCenterX + 40, clientY: pinchCenterY, pointerId: 2, pointerType: 'touch' })
await previewViewport.dispatchEvent('pointermove', { buttons: 1, clientX: pinchCenterX + 80, clientY: pinchCenterY, pointerId: 2, pointerType: 'touch' })
await previewViewport.dispatchEvent('pointerup', { clientX: pinchCenterX - 40, clientY: pinchCenterY, pointerId: 1, pointerType: 'touch' })
await previewViewport.dispatchEvent('pointerup', { clientX: pinchCenterX + 80, clientY: pinchCenterY, pointerId: 2, pointerType: 'touch' })
await expect.poll(() => previewImage.evaluate(element => Number(element.getAttribute('data-easy-image-scale'))))
.toBeGreaterThan(0.1)
await page.keyboard.press('Escape')
await expect(dialog).toBeHidden()
const after = await image.evaluate(element => ({