From bbcef8c300797fe45cc64c042d1a1dee9bb1e1d9 Mon Sep 17 00:00:00 2001 From: Ananovo Date: Sat, 1 Aug 2026 17:10:51 +0800 Subject: [PATCH] fix(web): preserve share export layout fidelity (#1277) * fix(web): preserve share export layout fidelity * fix(web): keep mobile and multi-image exports responsive * fix(web): distinguish coarse pointers from touch desktops --- e2e/share-turn.spec.ts | 82 +++++++++++- web/e2e-fixtures/share-turn-fixture.tsx | 17 ++- .../components/AssistantChat/HappyThread.tsx | 7 + .../AssistantChat/ShareTurnDialog.tsx | 121 ++++++++++++------ 4 files changed, 181 insertions(+), 46 deletions(-) diff --git a/e2e/share-turn.spec.ts b/e2e/share-turn.spec.ts index ceffe51b..abe4acf5 100644 --- a/e2e/share-turn.spec.ts +++ b/e2e/share-turn.spec.ts @@ -60,7 +60,7 @@ for (const viewport of [ } } const mediaGrid = page.getByRole('dialog').locator('.hapi-share-media-grid') - await expect(mediaGrid).toHaveCSS('display', 'grid') + await expect(mediaGrid).toHaveCSS('display', 'flex') 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) @@ -192,6 +192,86 @@ test('keeps code and image controls interactive in preview', async ({ page }, te await expect(dialog).toBeVisible() }) +test('preserves the desktop source width but keeps mobile export width stable', async ({ page }, testInfo) => { + await page.addInitScript(() => { + Object.defineProperty(navigator, 'maxTouchPoints', { configurable: true, value: 5 }) + }) + await page.setViewportSize({ width: 1440, height: 1600 }) + await page.goto('/e2e-fixtures/share-turn-fixture.html?wide=1') + const sourceWidth = await page.getByTestId('source-turn').evaluate((element) => element.getBoundingClientRect().width) + expect(sourceWidth).toBe(1080) + await page.getByRole('button', { name: 'Open share preview' }).click() + + const preview = page.getByRole('dialog').locator('.hapi-share-preview-root') + const previewWidth = await preview.evaluate((element) => element.getBoundingClientRect().width) + expect(previewWidth).toBeGreaterThan(650) + expect(previewWidth).toBeLessThanOrEqual(720) + const inlineCode = preview.locator('.aui-md-code').last() + await expect(inlineCode).toHaveCSS('display', 'inline') + + const desktopDownloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download' }).click() + const desktopDownload = await desktopDownloadPromise + const desktopPath = testInfo.outputPath('source-width-desktop.png') + await desktopDownload.saveAs(desktopPath) + expect(pngSize(await readFile(desktopPath)).width).toBe(2240) + + await page.setViewportSize({ width: 390, height: 844 }) + await page.reload() + await page.getByRole('button', { name: 'Open share preview' }).click() + const mobileDownloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download' }).click() + const mobileDownload = await mobileDownloadPromise + const mobilePath = testInfo.outputPath('source-width-mobile.png') + await mobileDownload.saveAs(mobilePath) + expect(pngSize(await readFile(mobilePath)).width).toBe(1920) +}) + +test('keeps a landscape touch device on the fixed mobile export path', async ({ page }, testInfo) => { + await page.addInitScript(() => { + const nativeMatchMedia = window.matchMedia.bind(window) + window.matchMedia = (query: string) => { + if (query !== '(pointer: coarse)') return nativeMatchMedia(query) + return { + matches: true, + media: query, + onchange: null, + addListener: () => undefined, + removeListener: () => undefined, + addEventListener: () => undefined, + removeEventListener: () => undefined, + dispatchEvent: () => false, + } + } + }) + await page.setViewportSize({ width: 844, height: 390 }) + await page.goto('/e2e-fixtures/share-turn-fixture.html?wide=1') + await page.getByRole('button', { name: 'Open share preview' }).click() + + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download' }).click() + const download = await downloadPromise + const path = testInfo.outputPath('landscape-touch-mobile.png') + await download.saveAs(path) + expect(pngSize(await readFile(path)).width).toBe(1920) +}) + +test('allows three or more attachments to wrap instead of shrinking into one row', async ({ page }) => { + await page.goto('/e2e-fixtures/share-turn-fixture.html') + await page.getByTestId('source-turn').evaluate((source) => { + const grid = source.querySelector('.hapi-share-media-grid') + const firstAttachment = grid?.querySelector('button') + if (!grid || !firstAttachment) throw new Error('Missing attachment fixture') + grid.appendChild(firstAttachment.cloneNode(true)) + grid.dataset.hapiImageCount = '3' + }) + await page.getByRole('button', { name: 'Open share preview' }).click() + + const mediaGrid = page.getByRole('dialog').locator('.hapi-share-media-grid') + await expect(mediaGrid).toHaveCSS('flex-wrap', 'wrap') + await expect(mediaGrid.locator(':scope > button')).toHaveCount(3) +}) + 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: '' } diff --git a/web/e2e-fixtures/share-turn-fixture.tsx b/web/e2e-fixtures/share-turn-fixture.tsx index 4369666f..b58197da 100644 --- a/web/e2e-fixtures/share-turn-fixture.tsx +++ b/web/e2e-fixtures/share-turn-fixture.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from 'react' import ReactDOM from 'react-dom/client' import '../src/index.css' import { ShareTurnDialog } from '../src/components/AssistantChat/ShareTurnDialog' -import { getUserBubbleClassName } from '../src/components/AssistantChat/messages/user-bubble' +import { getUserBubbleClassName, UserBubbleContent } from '../src/components/AssistantChat/messages/user-bubble' import { MarkdownRenderer } from '../src/components/MarkdownRenderer' import { I18nProvider } from '../src/lib/i18n-context' @@ -26,6 +26,8 @@ const portraitFixtureImage = 'data:image/svg+xml;charset=utf-8,' + encodeURIComp const markdown = `## Complex response fixture +## ✅ AList 的 frp 和 Caddy 配置已彻底移除 + This paragraph contains **bold text**, *emphasis*, ~~strikethrough~~, inline \`code\`, a [safe link](https://example.com), 中文内容,以及一段足够长的文字,用于验证换行、行高和宽屏导出效果是否与原始 HAPI 页面保持一致。 > A multi-line blockquote used to verify borders, indentation, colors, and wrapping. \n> 第二行引用包含中文。 @@ -54,6 +56,10 @@ console.log(result) --- Final paragraph after the divider. + +\`alist.techotaku39.top\` 已失效,5244 端口也不再监听。 + +请将 \`machine-status-widget/hapi-machine-status.user.js\` 全文覆盖到油猴脚本中,然后强制刷新页面。 ` if (new URLSearchParams(window.location.search).get('theme') === 'dark') { @@ -66,6 +72,7 @@ function App() { const sourceRef = useRef(null) const [snapshots, setSnapshots] = useState([]) const [open, setOpen] = useState(false) + const wideSource = new URLSearchParams(window.location.search).get('wide') === '1' const openShare = () => { const searchParams = new URLSearchParams(window.location.search) @@ -91,14 +98,11 @@ function App() { return ( -
+
-
- plan - {'请导出这一轮复杂对话,并确保代码、表格、图片附件和长文本的样式全部保留。\n第二行用于验证换行。'} -
+