mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(web): include session title in share image filename * fix(web): polish shared turn image UX
180 lines
9.1 KiB
TypeScript
180 lines
9.1 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
function pngSize(bytes: Buffer): { width: number; height: number } {
|
|
expect(bytes.subarray(1, 4).toString()).toBe('PNG')
|
|
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) }
|
|
}
|
|
|
|
for (const viewport of [
|
|
{ name: 'desktop', width: 1280, height: 2200, theme: 'light' },
|
|
{ name: 'mobile', width: 390, height: 844, theme: 'light' },
|
|
{ name: 'mobile-dark', width: 390, height: 844, theme: 'dark' }
|
|
]) {
|
|
test(`exports a populated wide PNG on ${viewport.name}`, async ({ page }, testInfo) => {
|
|
let stylesheetRequests = 0
|
|
await page.route('**/share-turn-extra.css', async (route) => {
|
|
stylesheetRequests += 1
|
|
if (stylesheetRequests === 1) {
|
|
await route.fulfill({
|
|
contentType: 'text/css',
|
|
headers: { 'cache-control': 'no-store' },
|
|
body: '.share-turn-network-style{border-left:5px solid rgb(124 58 237);border-radius:14px;background:rgb(124 58 237 / 10%);padding:12px 16px}'
|
|
})
|
|
return
|
|
}
|
|
await route.abort()
|
|
})
|
|
await page.setViewportSize(viewport)
|
|
await page.goto(`/e2e-fixtures/share-turn-fixture.html?theme=${viewport.theme}`)
|
|
|
|
await expect(page.getByText('Complex response fixture')).toBeVisible()
|
|
await expect(page.getByText('Excluded tool output')).toBeVisible()
|
|
await expect(page.getByText(/type ExportResult/)).toBeVisible()
|
|
await page.getByTestId('source-turn').screenshot({ path: testInfo.outputPath(`source-${viewport.name}.png`) })
|
|
await page.getByRole('button', { name: 'Open share preview' }).click()
|
|
await expect(page.getByRole('dialog')).toBeVisible()
|
|
await expect(page.getByRole('dialog').getByText('Excluded tool output')).toHaveCount(0)
|
|
await expect(page.getByRole('dialog').locator('.happy-message-actions')).toHaveCount(0)
|
|
await expect(page.getByRole('dialog').locator('.hapi-share-hidden-content-spacer')).toHaveCount(1)
|
|
await expect(page.getByRole('dialog').locator('[title="Click to zoom"]')).toHaveCount(0)
|
|
if (viewport.name === 'desktop') {
|
|
const styles = await page.evaluate(() => {
|
|
const source = document.querySelector<HTMLElement>('[data-testid="source-turn"]')
|
|
const preview = document.querySelector<HTMLElement>('[data-hapi-share-body="true"]')
|
|
if (!source || !preview) throw new Error('Missing comparison roots')
|
|
const selectors = ['.happy-user-bubble', 'h2', 'blockquote', 'table', 'pre']
|
|
const properties = ['fontFamily', 'fontSize', 'fontWeight', 'color', 'backgroundColor', 'borderRadius'] as const
|
|
return selectors.map((selector) => {
|
|
const sourceElement = source.querySelector<HTMLElement>(selector)
|
|
const previewElement = preview.querySelector<HTMLElement>(selector)
|
|
if (!sourceElement || !previewElement) throw new Error(`Missing ${selector}`)
|
|
const sourceStyle = getComputedStyle(sourceElement)
|
|
const previewStyle = getComputedStyle(previewElement)
|
|
return properties.map((property) => [sourceStyle[property], previewStyle[property]])
|
|
})
|
|
})
|
|
for (const pairs of styles) {
|
|
for (const [sourceValue, previewValue] of pairs) {
|
|
expect(previewValue).toBe(sourceValue)
|
|
}
|
|
}
|
|
const mediaGrid = page.getByRole('dialog').locator('.hapi-share-media-grid')
|
|
await expect(mediaGrid).toHaveCSS('display', 'grid')
|
|
const imageTops = await mediaGrid.locator('img').evaluateAll((images) => images.map((image) => image.getBoundingClientRect().top))
|
|
expect(imageTops).toHaveLength(2)
|
|
expect(Math.abs(imageTops[0] - imageTops[1])).toBeLessThan(1)
|
|
}
|
|
const downloadPromise = page.waitForEvent('download')
|
|
await page.getByRole('button', { name: /^(Download|下载)$/ }).click()
|
|
const download = await downloadPromise
|
|
const path = testInfo.outputPath(`share-turn-${viewport.name}.png`)
|
|
await download.saveAs(path)
|
|
|
|
const bytes = await readFile(path)
|
|
const size = pngSize(bytes)
|
|
expect(download.suggestedFilename()).toMatch(/^HAPI-Complex HAPI turn-\d{14}\.png$/)
|
|
expect(bytes.byteLength).toBeGreaterThan(80_000)
|
|
expect(size.width).toBe(1920)
|
|
expect(size.height).toBeGreaterThan(1_000)
|
|
expect(stylesheetRequests).toBe(1)
|
|
})
|
|
}
|
|
|
|
test('exports a text-only user fallback alongside assistant DOM', async ({ page }, testInfo) => {
|
|
await page.goto('/e2e-fixtures/share-turn-fixture.html?fallback=user')
|
|
await page.getByRole('button', { name: 'Open share preview' }).click()
|
|
|
|
const dialog = page.getByRole('dialog')
|
|
await expect(dialog.getByText(/请导出这一轮复杂对话/)).toBeVisible()
|
|
await expect(dialog.getByText('Complex response fixture')).toBeVisible()
|
|
await dialog.screenshot({ path: testInfo.outputPath('fallback-preview.png') })
|
|
|
|
const downloadPromise = page.waitForEvent('download')
|
|
await dialog.getByRole('button', { name: /^(Download|下载)$/ }).click()
|
|
const download = await downloadPromise
|
|
const path = testInfo.outputPath('fallback-export.png')
|
|
await download.saveAs(path)
|
|
|
|
const bytes = await readFile(path)
|
|
const size = pngSize(bytes)
|
|
expect(bytes.byteLength).toBeGreaterThan(80_000)
|
|
expect(size.width).toBe(1920)
|
|
expect(size.height).toBeGreaterThan(1_000)
|
|
})
|
|
|
|
test('keeps a stripped tool-only assistant snapshot out of the export', async ({ page }, testInfo) => {
|
|
await page.goto('/e2e-fixtures/share-turn-fixture.html?toolOnly=assistant')
|
|
await page.getByRole('button', { name: 'Open share preview' }).click()
|
|
|
|
const dialog = page.getByRole('dialog')
|
|
await expect(dialog.getByText('Complex response fixture')).toBeVisible()
|
|
await expect(dialog.getByText('TOOL_ONLY_SECRET_SHOULD_NOT_EXPORT')).toHaveCount(0)
|
|
|
|
const downloadPromise = page.waitForEvent('download')
|
|
await dialog.getByRole('button', { name: /^(Download|下载)$/ }).click()
|
|
const download = await downloadPromise
|
|
const path = testInfo.outputPath('tool-only-excluded-export.png')
|
|
await download.saveAs(path)
|
|
|
|
const bytes = await readFile(path)
|
|
const size = pngSize(bytes)
|
|
expect(bytes.byteLength).toBeGreaterThan(80_000)
|
|
expect(size.width).toBe(1920)
|
|
expect(size.height).toBeGreaterThan(1_000)
|
|
})
|
|
|
|
test('localizes the share dialog actions in Chinese', async ({ page }) => {
|
|
await page.addInitScript(() => localStorage.setItem('hapi-lang', 'zh-CN'))
|
|
await page.goto('/e2e-fixtures/share-turn-fixture.html')
|
|
await page.getByRole('button', { name: 'Open share preview' }).click()
|
|
|
|
const dialog = page.getByRole('dialog')
|
|
await expect(dialog.getByRole('heading', { name: '将本轮对话分享为图片' })).toBeVisible()
|
|
await expect(dialog.getByText('分享会话', { exact: true })).toHaveCount(0)
|
|
await expect(dialog.getByText('Generated by HAPI', { exact: true })).toBeVisible()
|
|
await expect(dialog.getByRole('button', { name: '取消' })).toHaveCount(0)
|
|
await expect(dialog.getByRole('button', { name: '复制' })).toBeVisible()
|
|
await expect(dialog.getByRole('button', { name: '分享' })).toBeVisible()
|
|
await expect(dialog.getByRole('button', { name: '下载' })).toBeVisible()
|
|
})
|
|
|
|
test('uses a prepared PNG while native share still has click activation', async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
const state = { calls: 0, active: false, fileType: '', fileName: '' }
|
|
Object.defineProperty(window, '__hapiShareTest', { value: state, configurable: true })
|
|
Object.defineProperty(navigator, 'canShare', {
|
|
configurable: true,
|
|
value: () => true
|
|
})
|
|
Object.defineProperty(navigator, 'share', {
|
|
configurable: true,
|
|
value: (data: ShareData) => {
|
|
state.calls += 1
|
|
state.active = navigator.userActivation?.isActive ?? false
|
|
state.fileType = data.files?.[0]?.type ?? ''
|
|
state.fileName = data.files?.[0]?.name ?? ''
|
|
return Promise.resolve()
|
|
}
|
|
})
|
|
})
|
|
await page.goto('/e2e-fixtures/share-turn-fixture.html')
|
|
await page.getByRole('button', { name: 'Open share preview' }).click()
|
|
|
|
const shareButton = page.getByRole('dialog').getByRole('button', { name: 'Share' })
|
|
await expect(shareButton).toBeEnabled()
|
|
await shareButton.click()
|
|
|
|
await expect.poll(() => page.evaluate(() => {
|
|
return (window as typeof window & { __hapiShareTest: { calls: number } }).__hapiShareTest.calls
|
|
})).toBe(1)
|
|
const result = await page.evaluate(() => {
|
|
return (window as typeof window & {
|
|
__hapiShareTest: { active: boolean; fileType: string; fileName: string }
|
|
}).__hapiShareTest
|
|
})
|
|
expect(result.active).toBe(true)
|
|
expect(result.fileType).toBe('image/png')
|
|
expect(result.fileName).toMatch(/^HAPI-Complex HAPI turn-\d{14}\.png$/)
|
|
})
|