657 lines
34 KiB
TypeScript
657 lines
34 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
|
|
test('landing page links to a rendered deck', async ({ page }) => {
|
|
await page.goto('/')
|
|
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: '演示者' })).toHaveCount(0)
|
|
await expect(page.getByRole('button', { name: 'Presenter Mode' })).toHaveCount(0)
|
|
})
|
|
|
|
test('presenter token unlocks, survives refresh, and can be locked', async ({ page }) => {
|
|
await page.goto('/getting-started/presenter/')
|
|
const gate = page.getByRole('dialog', { name: '演示者模式' })
|
|
const tokenInput = page.getByLabel('演示者 token')
|
|
await expect(gate).toBeVisible()
|
|
await expect(page.locator('body')).toHaveClass(/easy-presenter-locked/)
|
|
expect(await gate.evaluate(element => element.matches(':modal'))).toBe(true)
|
|
await expect(tokenInput).toBeFocused()
|
|
|
|
const lockedUrl = page.url()
|
|
await page.keyboard.press('ArrowRight')
|
|
expect(page.url()).toBe(lockedUrl)
|
|
let backgroundClickBlocked = false
|
|
try {
|
|
await page.getByTitle('Go to next slide').click({ timeout: 500 })
|
|
}
|
|
catch {
|
|
backgroundClickBlocked = true
|
|
}
|
|
expect(backgroundClickBlocked).toBe(true)
|
|
await page.keyboard.press('Escape')
|
|
await expect(gate).toBeVisible()
|
|
await page.keyboard.press('Tab')
|
|
expect(await gate.evaluate(element => element.contains(document.activeElement))).toBe(true)
|
|
|
|
await tokenInput.fill('wrong-token')
|
|
await page.getByRole('button', { name: '解锁' }).click()
|
|
await expect(page.getByRole('alert')).toHaveText('Token 不正确')
|
|
|
|
await tokenInput.fill('test-token')
|
|
await page.getByRole('button', { name: '解锁' }).click()
|
|
await expect(gate).toHaveCount(0)
|
|
await expect(page.locator('body')).not.toHaveClass(/easy-presenter-locked/)
|
|
await expect(page.getByRole('navigation', { name: '提词器设置' })).toBeVisible()
|
|
const notes = page.getByText('欢迎使用 easy-slides')
|
|
await expect(notes).toBeVisible()
|
|
const initialSize = await notes.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
await page.getByRole('button', { name: 'A+' }).click()
|
|
const enlargedSize = await notes.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
expect(enlargedSize).toBeGreaterThan(initialSize)
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
const toolsBox = await page.getByRole('navigation', { name: '提词器设置' }).boundingBox()
|
|
expect(toolsBox).not.toBeNull()
|
|
expect(toolsBox!.x + toolsBox!.width).toBeLessThanOrEqual(390)
|
|
await page.getByRole('button', { name: '◐' }).click()
|
|
await expect(page.locator('body')).toHaveClass(/easy-notes-contrast/)
|
|
|
|
await page.reload()
|
|
await expect(page.getByRole('navigation', { name: '提词器设置' })).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: '锁定', exact: true }).click()
|
|
await expect(gate).toBeVisible()
|
|
expect(await gate.evaluate(element => element.matches(':modal'))).toBe(true)
|
|
await expect(tokenInput).toBeFocused()
|
|
})
|
|
|
|
test('presenter unlock is scoped to one browser tab', async ({ context, page }) => {
|
|
await page.goto('/getting-started/presenter/')
|
|
await page.getByLabel('演示者 token').fill('test-token')
|
|
await page.getByRole('button', { name: '解锁' }).click()
|
|
|
|
const second = await context.newPage()
|
|
await second.goto('/getting-started/presenter/')
|
|
await expect(second.getByLabel('演示者 token')).toBeVisible()
|
|
})
|
|
|
|
test('presenter sends image previews to every audience window', async ({ context, page }) => {
|
|
await page.goto('/getting-started/#/4')
|
|
const secondAudience = await context.newPage()
|
|
await secondAudience.goto('/getting-started/#/4')
|
|
const presenter = await context.newPage()
|
|
await presenter.goto('/getting-started/presenter/')
|
|
await presenter.getByLabel('演示者 token').fill('test-token')
|
|
await presenter.getByRole('button', { name: '解锁' }).click()
|
|
|
|
const images = presenter.locator('.slidev-layout img[src*="cover.svg"]')
|
|
await expect.poll(async () => {
|
|
if (await images.count() === 0)
|
|
return null
|
|
return images.first().evaluate((element) => {
|
|
const imageElement = element as HTMLImageElement
|
|
return {
|
|
loaded: imageElement.complete && imageElement.naturalWidth > 0,
|
|
pathname: new URL(imageElement.currentSrc).pathname,
|
|
}
|
|
})
|
|
}).toEqual({
|
|
loaded: true,
|
|
pathname: '/getting-started/assets/cover.svg',
|
|
})
|
|
|
|
const presenterCurrent = presenter.locator('.grid-section.main')
|
|
const presenterNext = presenter.locator('.grid-section.next')
|
|
const imageSlideHeading = presenterCurrent.getByRole('heading', { name: '智能图片' })
|
|
for (let step = 0; step < 12 && !(await imageSlideHeading.isVisible()); step += 1)
|
|
await presenter.getByTitle('Go to next slide').click()
|
|
await expect(imageSlideHeading).toBeVisible()
|
|
|
|
await expect(presenterCurrent.getByRole('navigation', { name: '图片缩放' })).toHaveCount(0)
|
|
await expect(presenterNext.getByRole('navigation', { name: '图片缩放' })).toHaveCount(0)
|
|
const currentImage = presenterCurrent.locator('img.easy-image-preview-trigger').first()
|
|
const nextImage = presenterNext.locator('img.easy-image-preview-trigger').first()
|
|
await expect(currentImage).toBeVisible()
|
|
await expect(nextImage).toBeVisible()
|
|
await currentImage.click()
|
|
|
|
const presenterDialog = presenter.getByRole('dialog', { name: '图片全屏预览' })
|
|
const audienceDialog = page.getByRole('dialog', { name: '图片全屏预览' })
|
|
const secondAudienceDialog = secondAudience.getByRole('dialog', { name: '图片全屏预览' })
|
|
await expect(audienceDialog).toBeVisible()
|
|
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 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 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 }) => {
|
|
const auditPage = await context.newPage()
|
|
await auditPage.goto('/getting-started/?easy-slides-audit=1#/4')
|
|
await page.goto('/getting-started/presenter/')
|
|
await page.getByLabel('演示者 token').fill('test-token')
|
|
await page.getByRole('button', { name: '解锁' }).click()
|
|
|
|
const current = page.locator('.grid-section.main')
|
|
const imageSlideHeading = current.getByRole('heading', { name: '智能图片' })
|
|
for (let step = 0; step < 12 && !(await imageSlideHeading.isVisible()); step += 1)
|
|
await page.getByTitle('Go to next slide').click()
|
|
const image = current.locator('img.easy-image-preview-trigger').first()
|
|
await image.click()
|
|
|
|
const dialog = page.getByRole('dialog', { name: '图片全屏预览' })
|
|
await expect(dialog).toBeVisible({ timeout: 2_000 })
|
|
await expect(auditPage.getByRole('dialog', { name: '图片全屏预览' })).toBeHidden()
|
|
await page.keyboard.press('Escape')
|
|
await expect(dialog).toBeHidden()
|
|
await auditPage.close()
|
|
})
|
|
|
|
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()
|
|
await presenter.goto('http://127.0.0.1:4174/presenter/4?password=easy-slides-local')
|
|
|
|
const current = presenter.locator('.grid-section.main')
|
|
const imageSlideHeading = current.getByRole('heading', { name: '智能图片' })
|
|
await expect(imageSlideHeading).toBeVisible()
|
|
await expect(audience.getByRole('heading', { name: '智能图片' })).toBeVisible()
|
|
await current.locator('img.easy-image-preview-trigger').first().click()
|
|
|
|
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()
|
|
}
|
|
})
|
|
|
|
test('presenter advances animations, switches notes, shows timing, and syncs the audience window', async ({ context, page }) => {
|
|
await page.goto('/getting-started/#/1')
|
|
const presenter = await context.newPage()
|
|
await presenter.goto('/getting-started/presenter/')
|
|
await presenter.getByLabel('演示者 token').fill('test-token')
|
|
await presenter.getByRole('button', { name: '解锁' }).click()
|
|
|
|
await expect(presenter.getByText('Next', { exact: true })).toBeVisible()
|
|
await expect(presenter.getByText('欢迎使用 easy-slides')).toBeVisible()
|
|
await expect(presenter.locator('body')).toContainText('-:--')
|
|
|
|
await presenter.getByTitle('Go to next slide').click()
|
|
await expect(page.getByRole('heading', { name: '内容优先' })).toBeVisible()
|
|
await expect(presenter.getByText('依次介绍四个核心能力')).toBeVisible()
|
|
await expect(presenter.getByText('2 / 9', { exact: true })).toBeVisible()
|
|
|
|
await presenter.getByTitle('Go to next slide').click()
|
|
await expect(presenter.getByText('1/4', { exact: true })).toBeVisible()
|
|
await expect(page).toHaveURL(/#\/2\?clicks=1$/)
|
|
})
|
|
|
|
test('table and code controls adjust font size while code uses smart height', async ({ page }) => {
|
|
await page.goto('/getting-started/#/7')
|
|
const table = page.locator('.slidev-layout table').first()
|
|
const tablePreset = table.locator('..').locator('..')
|
|
const tableCell = table.locator('th').first()
|
|
|
|
const initialTableSize = await table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
await page.getByRole('button', { name: '放大表格' }).click()
|
|
await expect.poll(() => table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(initialTableSize * 1.1, 4)
|
|
await page.getByRole('button', { name: '重置表格缩放' }).click()
|
|
await expect.poll(() => table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(initialTableSize)
|
|
|
|
for (const preset of [
|
|
{ className: '', fontSize: 30, paddingY: 10, paddingX: 14 },
|
|
{ className: 'easy-table-sm', fontSize: 24, paddingY: 7, paddingX: 10 },
|
|
{ className: 'easy-table-md', fontSize: 30, paddingY: 10, paddingX: 14 },
|
|
{ className: 'easy-table-lg', fontSize: 36, paddingY: 13, paddingX: 18 },
|
|
]) {
|
|
await tablePreset.evaluate((element, className) => {
|
|
element.classList.remove('easy-table-sm', 'easy-table-md', 'easy-table-lg')
|
|
if (className)
|
|
element.classList.add(className)
|
|
}, preset.className)
|
|
await expect.poll(() => table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(preset.fontSize)
|
|
const padding = await tableCell.evaluate(element => ({
|
|
x: Number.parseFloat(getComputedStyle(element).paddingRight),
|
|
y: Number.parseFloat(getComputedStyle(element).paddingTop),
|
|
}))
|
|
expect(padding).toEqual({ x: preset.paddingX, y: preset.paddingY })
|
|
}
|
|
|
|
await page.goto('/getting-started/#/3')
|
|
const layout = page.locator('.slidev-page-3 .slidev-layout')
|
|
const code = layout.locator('pre').first()
|
|
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(34)
|
|
await expect(page.getByRole('navigation', { name: '代码缩放' })).toBeVisible()
|
|
const initialCodeSize = await code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
await page.getByRole('button', { name: '放大代码' }).click()
|
|
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(initialCodeSize * 1.1, 4)
|
|
await page.getByRole('button', { name: '重置代码缩放' }).click()
|
|
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(initialCodeSize)
|
|
|
|
await expect.poll(() => code.evaluate((element) => {
|
|
return element.scrollHeight - element.clientHeight
|
|
})).toBeGreaterThan(0)
|
|
const overflow = await code.evaluate(element => ({
|
|
horizontal: getComputedStyle(element).overflowX,
|
|
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
|
|
scrollable: element.scrollHeight > element.clientHeight,
|
|
vertical: getComputedStyle(element).overflowY,
|
|
whiteSpace: getComputedStyle(element).whiteSpace,
|
|
}))
|
|
expect(overflow).toMatchObject({ horizontal: 'hidden', scrollable: true, vertical: 'auto', whiteSpace: 'pre-wrap' })
|
|
expect(overflow.lineHeight).toBeGreaterThan(34)
|
|
expect(await code.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1)
|
|
expect(await code.evaluate((element) => {
|
|
element.scrollTop = element.scrollHeight
|
|
return element.scrollTop
|
|
})).toBeGreaterThan(0)
|
|
|
|
for (const preset of [
|
|
{ className: '', fontSize: 28 },
|
|
{ className: 'easy-code-sm', fontSize: 22 },
|
|
{ className: 'easy-code-md', fontSize: 28 },
|
|
{ className: 'easy-code-lg', fontSize: 34 },
|
|
]) {
|
|
await layout.evaluate((element, className) => {
|
|
element.classList.remove('easy-code-sm', 'easy-code-md', 'easy-code-lg')
|
|
if (className)
|
|
element.classList.add(className)
|
|
}, preset.className)
|
|
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(preset.fontSize)
|
|
}
|
|
|
|
for (const preset of [
|
|
{ className: 'easy-code-height-sm', height: 180 },
|
|
{ className: 'easy-code-height-md', height: 300 },
|
|
{ className: 'easy-code-height-lg', height: 420 },
|
|
]) {
|
|
await layout.evaluate((element, className) => {
|
|
element.classList.remove('easy-code-height-sm', 'easy-code-height-md', 'easy-code-height-lg')
|
|
if (className)
|
|
element.classList.add(className)
|
|
}, preset.className)
|
|
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).height))).toBe(preset.height)
|
|
}
|
|
|
|
await layout.evaluate((element) => {
|
|
element.classList.remove('easy-code-height-sm', 'easy-code-height-md', 'easy-code-height-lg')
|
|
})
|
|
await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('auto-height code keeps its natural height and reserves room for following content', async ({ page }) => {
|
|
await page.goto('/getting-started/#/8')
|
|
const layout = page.locator('.slidev-page-8 .slidev-layout')
|
|
const code = layout.locator('pre').first()
|
|
const following = layout.getByRole('heading', { name: '代码之后的正文仍保留在内容区内' })
|
|
|
|
await expect(following).toBeVisible()
|
|
await expect.poll(() => code.evaluate((element) => {
|
|
const layoutElement = element.closest('.slidev-layout') as HTMLElement
|
|
const scale = layoutElement.getBoundingClientRect().height / layoutElement.clientHeight
|
|
return Math.abs(element.getBoundingClientRect().height / scale - element.scrollHeight)
|
|
})).toBeLessThanOrEqual(1)
|
|
const bounds = await layout.evaluate((element) => {
|
|
const codeElement = element.querySelector('pre')!
|
|
const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
|
|
return {
|
|
codeBottom: codeElement.getBoundingClientRect().bottom,
|
|
followingBottom: followingElement.getBoundingClientRect().bottom,
|
|
followingTop: followingElement.getBoundingClientRect().top,
|
|
layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
|
|
}
|
|
})
|
|
expect(bounds.codeBottom).toBeLessThan(bounds.followingTop)
|
|
expect(bounds.followingBottom).toBeLessThanOrEqual(bounds.layoutBottom + 1)
|
|
|
|
await code.evaluate((element) => {
|
|
element.textContent = Array.from({ length: 40 }, (_, index) => `line ${index + 1}: a deliberately long code sample`).join('\n')
|
|
})
|
|
await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(100)
|
|
const longContentBounds = await layout.evaluate((element) => {
|
|
const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
|
|
return {
|
|
followingBottom: followingElement.getBoundingClientRect().bottom,
|
|
layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
|
|
}
|
|
})
|
|
expect(longContentBounds.followingBottom).toBeLessThanOrEqual(longContentBounds.layoutBottom + 1)
|
|
})
|
|
|
|
test('image fullscreen preserves layout and table scaling stays out of non-interactive routes', async ({ page }) => {
|
|
await page.goto('/getting-started/#/4')
|
|
const image = page.locator('.slidev-layout img.easy-image-preview-trigger').first()
|
|
const before = await image.evaluate(element => ({
|
|
height: element.getBoundingClientRect().height,
|
|
parentClass: element.parentElement?.className,
|
|
transform: getComputedStyle(element).transform,
|
|
width: element.getBoundingClientRect().width,
|
|
}))
|
|
await expect(page.getByRole('navigation', { name: '图片缩放' })).toHaveCount(0)
|
|
await image.click()
|
|
const dialog = page.getByRole('dialog', { name: '图片全屏预览' })
|
|
await expect(dialog).toBeVisible()
|
|
const viewport = page.viewportSize()
|
|
const overlay = await dialog.boundingBox()
|
|
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 => ({
|
|
height: element.getBoundingClientRect().height,
|
|
parentClass: element.parentElement?.className,
|
|
transform: getComputedStyle(element).transform,
|
|
width: element.getBoundingClientRect().width,
|
|
}))
|
|
expect(after).toEqual(before)
|
|
|
|
await image.click()
|
|
for (const scenario of [
|
|
{ height: 844, imageHeight: 3200, imageWidth: 120, width: 390 },
|
|
{ height: 390, imageHeight: 120, imageWidth: 3200, width: 844 },
|
|
]) {
|
|
await page.setViewportSize({ width: scenario.width, height: scenario.height })
|
|
const source = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${scenario.imageWidth}" height="${scenario.imageHeight}" viewBox="0 0 ${scenario.imageWidth} ${scenario.imageHeight}"><rect width="100%" height="100%" fill="#2563eb"/></svg>`)}`
|
|
const preview = dialog.locator('img')
|
|
await preview.evaluate((element, nextSource) => {
|
|
;(element as HTMLImageElement).src = nextSource
|
|
}, source)
|
|
await expect.poll(() => preview.evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(scenario.imageWidth)
|
|
const bounds = await dialog.evaluate((element) => {
|
|
const overlay = element as HTMLElement
|
|
const preview = overlay.querySelector('img')!
|
|
const overlayRect = overlay.getBoundingClientRect()
|
|
const previewRect = preview.getBoundingClientRect()
|
|
const style = getComputedStyle(overlay)
|
|
return {
|
|
contentBottom: overlayRect.bottom - Number.parseFloat(style.paddingBottom),
|
|
contentLeft: overlayRect.left + Number.parseFloat(style.paddingLeft),
|
|
contentRight: overlayRect.right - Number.parseFloat(style.paddingRight),
|
|
contentTop: overlayRect.top + Number.parseFloat(style.paddingTop),
|
|
overlayBottom: overlayRect.bottom,
|
|
overlayRight: overlayRect.right,
|
|
previewBottom: previewRect.bottom,
|
|
previewLeft: previewRect.left,
|
|
previewRight: previewRect.right,
|
|
previewTop: previewRect.top,
|
|
scrollHeight: overlay.scrollHeight,
|
|
scrollWidth: overlay.scrollWidth,
|
|
}
|
|
})
|
|
expect(bounds.overlayRight).toBe(scenario.width)
|
|
expect(bounds.overlayBottom).toBe(scenario.height)
|
|
expect(bounds.scrollWidth).toBeLessThanOrEqual(scenario.width)
|
|
expect(bounds.scrollHeight).toBeLessThanOrEqual(scenario.height)
|
|
expect(bounds.previewLeft).toBeGreaterThanOrEqual(bounds.contentLeft - 1)
|
|
expect(bounds.previewTop).toBeGreaterThanOrEqual(bounds.contentTop - 1)
|
|
expect(bounds.previewRight).toBeLessThanOrEqual(bounds.contentRight + 1)
|
|
expect(bounds.previewBottom).toBeLessThanOrEqual(bounds.contentBottom + 1)
|
|
}
|
|
await page.keyboard.press('Escape')
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
|
|
await page.goto('/getting-started/#/7')
|
|
const table = page.locator('.slidev-layout table').first()
|
|
const baseFontSize = await table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
await page.getByRole('button', { name: '放大表格' }).click()
|
|
await page.getByRole('button', { name: '放大表格' }).click()
|
|
await expect.poll(() => table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(baseFontSize * 1.2, 4)
|
|
await page.keyboard.press('ArrowDown')
|
|
await page.keyboard.press('ArrowUp')
|
|
await expect.poll(() => table.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(baseFontSize * 1.2, 4)
|
|
|
|
await page.goto('/getting-started/overview/')
|
|
await expect(page.locator('body')).not.toHaveClass(/easy-slide-scroll-fallback/)
|
|
await expect(page.locator('.easy-content-scale-toolbar')).toHaveCount(0)
|
|
await expect(page.locator('.easy-image-lightbox')).toHaveCount(0)
|
|
await page.goto('/getting-started/export/')
|
|
await expect(page.locator('.easy-content-scale-toolbar')).toHaveCount(0)
|
|
await expect(page.locator('.easy-image-lightbox')).toHaveCount(0)
|
|
await page.goto('/getting-started/editor/')
|
|
await expect(page.locator('.easy-content-scale-toolbar')).toHaveCount(0)
|
|
await expect(page.locator('.easy-image-lightbox')).toHaveCount(0)
|
|
})
|
|
|
|
test('overflowing slides scroll as a fallback without rendering a warning UI', async ({ page }) => {
|
|
const overflowWarnings: string[] = []
|
|
page.on('console', (message) => {
|
|
if (message.text().includes('内容纵向溢出'))
|
|
overflowWarnings.push(message.text())
|
|
})
|
|
await page.goto('/getting-started/#/2')
|
|
const layout = page.locator('.slidev-page-2 .slidev-layout').first()
|
|
await expect(page.locator('body')).toHaveClass(/easy-slide-scroll-fallback/)
|
|
await layout.evaluate((element) => {
|
|
const filler = document.createElement('div')
|
|
filler.dataset.testOverflow = 'true'
|
|
filler.style.height = '900px'
|
|
filler.style.width = '1px'
|
|
element.append(filler)
|
|
})
|
|
await expect.poll(() => layout.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(100)
|
|
expect(await layout.evaluate(element => getComputedStyle(element).overflowY)).toBe('auto')
|
|
expect(await layout.evaluate(element => getComputedStyle(element).overflowX)).toBe('hidden')
|
|
expect(await layout.evaluate((element) => {
|
|
element.scrollTop = element.scrollHeight
|
|
return element.scrollTop
|
|
})).toBeGreaterThan(100)
|
|
await expect(page.locator('.easy-slide-overflow-warning')).toHaveCount(0)
|
|
expect(overflowWarnings).toEqual([])
|
|
})
|
|
|
|
test('code scaling synchronizes between audience and presenter tabs', async ({ context, page }) => {
|
|
await page.goto('/getting-started/#/3')
|
|
const presenter = await context.newPage()
|
|
await presenter.goto('/getting-started/presenter/')
|
|
await presenter.getByLabel('演示者 token').fill('test-token')
|
|
await presenter.getByRole('button', { name: '解锁' }).click()
|
|
|
|
const presenterCurrent = presenter.locator('.grid-section.main')
|
|
const heading = presenterCurrent.getByRole('heading', { name: 'Markdown 与代码' })
|
|
for (let step = 0; step < 12 && !(await heading.isVisible()); step += 1)
|
|
await presenter.getByTitle('Go to next slide').click()
|
|
|
|
const audienceCode = page.locator('.slidev-page-3 pre').first()
|
|
const presenterCode = presenterCurrent.locator('pre').first()
|
|
const initial = await audienceCode.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))
|
|
await presenterCurrent.getByRole('button', { name: '放大代码' }).click()
|
|
await expect.poll(() => audienceCode.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(initial * 1.1, 4)
|
|
await expect.poll(() => presenterCode.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeCloseTo(initial * 1.1, 4)
|
|
})
|
|
|
|
test('stacked image layouts constrain wide images by available height', async ({ page }) => {
|
|
await page.setViewportSize({ width: 925, height: 963 })
|
|
await page.goto('/getting-started/#/4')
|
|
const pane = page.locator('.slidev-layout .easy-image-pane').first()
|
|
const image = pane.locator('img').first()
|
|
|
|
await expect(image).toBeVisible()
|
|
const dimensions = await pane.evaluate((element) => {
|
|
const imageElement = element.querySelector('img')!
|
|
return {
|
|
imageHeight: imageElement.offsetHeight,
|
|
imageWidth: imageElement.offsetWidth,
|
|
paneHeight: element.clientHeight,
|
|
paneWidth: element.clientWidth,
|
|
scrollHeight: element.scrollHeight,
|
|
scrollWidth: element.scrollWidth,
|
|
}
|
|
})
|
|
expect(dimensions.imageHeight).toBeLessThanOrEqual(dimensions.paneHeight)
|
|
expect(dimensions.imageWidth).toBeLessThanOrEqual(dimensions.paneWidth)
|
|
expect(dimensions.scrollHeight - dimensions.paneHeight).toBeLessThanOrEqual(1)
|
|
expect(dimensions.scrollWidth - dimensions.paneWidth).toBeLessThanOrEqual(1)
|
|
})
|
|
|
|
test('sample slides stay inside the 4:3 canvas', async ({ page }) => {
|
|
await page.goto('/getting-started/#/1')
|
|
for (let slide = 1; slide <= 9; slide += 1) {
|
|
const layout = page.locator('.slidev-layout').first()
|
|
await expect(layout).toBeVisible()
|
|
const box = await layout.boundingBox()
|
|
expect(box).not.toBeNull()
|
|
expect(box!.width / box!.height, `slide ${slide} aspect ratio`).toBeCloseTo(4 / 3, 2)
|
|
const overflow = await layout.evaluate(element => ({
|
|
horizontal: element.scrollWidth - element.clientWidth,
|
|
vertical: element.scrollHeight - element.clientHeight,
|
|
}))
|
|
expect(overflow.horizontal, `slide ${slide} horizontal overflow`).toBeLessThanOrEqual(1)
|
|
expect(overflow.vertical, `slide ${slide} vertical overflow`).toBeLessThanOrEqual(1)
|
|
for (const image of await layout.locator('img').all()) {
|
|
const state = await image.evaluate((element) => {
|
|
const imageElement = element as HTMLImageElement
|
|
return {
|
|
complete: imageElement.complete,
|
|
naturalWidth: imageElement.naturalWidth,
|
|
width: imageElement.getBoundingClientRect().width,
|
|
height: imageElement.getBoundingClientRect().height,
|
|
}
|
|
})
|
|
expect(state.complete, `slide ${slide} image loaded`).toBe(true)
|
|
expect(state.naturalWidth, `slide ${slide} image has content`).toBeGreaterThan(0)
|
|
expect(state.width, `slide ${slide} image width`).toBeGreaterThan(0)
|
|
expect(state.height, `slide ${slide} image height`).toBeGreaterThan(0)
|
|
}
|
|
await page.keyboard.press('ArrowDown')
|
|
}
|
|
})
|