diff --git a/apps/viewer/e2e/embed-v2.spec.ts b/apps/viewer/e2e/embed-v2.spec.ts index 23aeb81..c584176 100644 --- a/apps/viewer/e2e/embed-v2.spec.ts +++ b/apps/viewer/e2e/embed-v2.spec.ts @@ -309,44 +309,24 @@ test.describe.serial('E04 interactions: pause, click lock, keyboard, destroy', ( expect(after).toBeLessThan(resumedAt); // 恢复滚动(translateY 更负) }); - test('photo click opens viewer deep link for the clicked photo (§4.1)', async ({ page }) => { + test('photo click opens album page in a new tab (user decision 2026-09-24)', async ({ page }) => { await openHost(page, { width: 1440, height: 900 }); const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]'); await frame.waitFor(); - - // 悬停冻结 + 坐标直击当前命中的照片(.first() 可能已滚出视口——flaky 源)。 + // 悬停冻结(点击防错图语义下稳定命中)。 const box = await frame.boundingBox(); - const cx = (box!.x ?? 0) + 90; - const cy = (box!.y ?? 0) + 140; - await page.mouse.move(cx, cy); + await page.mouse.move((box?.x ?? 0) + 90, (box?.y ?? 0) + 140); await page.waitForTimeout(400); - const embedFrame0 = page.frames().find((f) => f.url().includes('v=2')); - expect(embedFrame0).toBeDefined(); - const photoId = await embedFrame0!.evaluate(([_x, _y]) => { - const el = document.elementFromPoint(_x, _y)?.closest('a.stream-photo'); - return el?.getAttribute('data-photo-id') ?? ''; - }, [90, 140] as const); - expect(photoId).not.toBe(''); - // 宿主内弹框(用户决策 2026-09-23):不新开标签,宿主页覆盖层 + 详情 iframe。 - let popupOpened = false; - page.once('popup', () => { - popupOpened = true; - }); - await page.mouse.click(cx, cy); - const modalFrame = page.locator('iframe[title="PhotoLive photo detail"]'); - await modalFrame.waitFor({ timeout: 5000 }); - expect(popupOpened).toBe(false); - await expect - .poll(async () => { - const f = page.frames().find((fr) => (fr.url().includes('/p/') ?? false)); - return f?.url() ?? ''; - }) - .toContain(`/p/${photoId}`); - await page.screenshot({ path: `${OUT_ROOT}/actual/server/photo-modal.png` }); - await page - .locator('button[aria-label*="Close photo detail"], button[aria-label*="关闭照片详情"]') - .click(); - await expect(modalFrame).toHaveCount(0); + const popupPromise = page.waitForEvent('popup', { timeout: 6000 }); + await page.mouse.click((box?.x ?? 0) + 90, (box?.y ?? 0) + 140); + const popup = await popupPromise; + // 新标签打开的是相册页(/a/:token),不含照片深链 /p/。 + expect(popup.url()).toMatch(/\/a\/[^/]+\?lang=/); + expect(popup.url()).not.toContain('/p/'); + await popup.close(); + // 宿主内不出现弹框。 + await page.waitForTimeout(600); + expect(await page.locator('iframe[title="PhotoLive photo detail"]').count()).toBe(0); }); test('drag beyond 8px cancels click (§4.1)', async ({ page }) => { diff --git a/apps/viewer/src/pages/EmbedPage.vue b/apps/viewer/src/pages/EmbedPage.vue index 8414096..2a472cf 100644 --- a/apps/viewer/src/pages/EmbedPage.vue +++ b/apps/viewer/src/pages/EmbedPage.vue @@ -18,7 +18,6 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useEmbedFeed } from '../embed/useEmbedFeed'; import { clampEmbedIntervalMs, EMBED_CAROUSEL_DEFAULT_MS } from '../embed/feed'; -import { postEmbedMessage } from '../embed/postMessage'; import EmbedCarousel from './embed/EmbedCarousel.vue'; import EmbedStream from './embed/EmbedStream.vue'; @@ -118,28 +117,13 @@ const albumUrl = computed(() => { return url.toString(); }); -/** v2 照片深链构造器(真实 href;受控打开同 openPhoto)。 */ -function streamPhotoUrl(photoId: string): string { - const albumToken = feed.album?.publicToken; - const base = albumToken ?? ''; - const url = new URL(`/a/${encodeURIComponent(base)}/p/${encodeURIComponent(photoId)}`, window.location.origin); - url.searchParams.set('lang', feed.resolvedLocale ?? 'zh-CN'); - return url.toString(); -} - -function openPhoto(photoId: string): void { - const albumToken = feed.album?.publicToken; - if (albumToken === undefined) return; - const url = new URL(`/a/${encodeURIComponent(albumToken)}/p/${encodeURIComponent(photoId)}`, window.location.origin); - url.searchParams.set('lang', feed.resolvedLocale ?? 'zh-CN'); - // 宿主内弹框(用户决策 2026-09-23):有父窗(loader 嵌入)→ postMessage - // 由宿主弹框内嵌详情页,不跳出当前网站;无父窗(直接打开/预览)→ 新标签 - // noopener(sandbox allow-popups-to-escape-sandbox 配合)。 - if (window.parent !== window) { - postEmbedMessage({ photoliveEmbed: true, type: 'open-photo', url: url.toString() }); - return; - } - window.open(url, '_blank', 'noopener,noreferrer'); +// 用户决策 2026-09-24:点击照片 → 新标签页打开观众页(相册页),不定位到 +// 具体照片、不在当前页弹框——v1 与 v2 统一该语义(此前的宿主内弹框与照片 +// 深链均退役;loader v2 的 open-photo 弹框逻辑保留但不再被触发)。 +function openPhoto(): void { + if (albumUrl.value === null) return; + // noopener:新标签页不持有 opener(sandbox allow-popups-to-escape-sandbox 配合)。 + window.open(albumUrl.value, '_blank', 'noopener,noreferrer'); } @@ -151,7 +135,7 @@ function openPhoto(photoId: string): void { :photos="feed.status === 'ready' ? feed.photos : []" :album-token="feed.album?.publicToken ?? null" :album-title="feed.album?.title ?? ''" - :photo-url="streamPhotoUrl" + :photo-url="() => albumUrl ?? '#'" :speed="streamSpeed" :reactions="streamReactions" :active="feed.active"