2026-09-20 18:32:34 +08:00
import { expect , test , type Page } from '@playwright/test' ;
import {
cleanupSeededAlbums ,
flushSync ,
seedAlbum ,
seedPhoto ,
} from './helpers' ;
// P4a Task 4 下载 Playwright(真实后端 + 真实签名管线):
// · 入口可见性随相册两级开关(off/off 无按钮 / on/off 单按钮 / on/on 分体
// 菜单两选项);
// · 微信内置浏览器 UA mock( addInitScript 覆写 navigator.userAgent)→ 点击
// 下载出引导浮层(不触端点、不 window.open),「知道了」收起;
// · 真实下载点击:download 端点 200 + window.open 收到签名 URL(对象键模式
// 断言;实际文件保存不在浏览器 e2e 域——由 api e2e 字节级覆盖)。
/** 种「下载相册」:1 张已发布照片,开关组合由参数给定。 */
async function seedDownloadAlbum (
alias : string ,
switches : { downloadEnabled? : boolean ; originalDownloadEnabled? : boolean },
) : Promise < string > {
const album = await seedAlbum ({
alias ,
translations : [{ locale : 'zh-CN' , title : `下载测试相册 ${ alias } ` }],
... switches ,
});
await seedPhoto ( album . id , ` ${ alias } -p0` , {
width : 480 ,
height : 300 ,
publishedAt : new Date ( Date . now () - 60 _000 ),
});
await flushSync ( album . id );
return album . publicToken ;
}
async function openLightbox ( page : Page , token : string , init ?: () => void ) : Promise < void > {
await page . addInitScript (() => localStorage . setItem ( 'viewer.locale' , 'zh-CN' ));
if ( init ) await page . addInitScript ( init );
await page . goto ( `/a/ ${ token } ` );
await expect ( page . locator ( '.cell' ). first ()). toBeVisible ();
await page . locator ( '.cell' ). first (). click ();
2026-09-21 23:10:58 +08:00
await expect ( page . locator ( '.photo-viewer' )). toBeVisible ();
2026-09-20 18:32:34 +08:00
}
test . afterAll ( async () => {
await cleanupSeededAlbums ();
});
test ( 'download entry visibility follows album two-tier switches' , async ({ page }) => {
test . setTimeout ( 120 _000 );
// off/off:无下载入口。
const offOff = await seedDownloadAlbum ( 'dl-offoff' , {
downloadEnabled : false ,
originalDownloadEnabled : false ,
});
await openLightbox ( page , offOff );
await expect ( page . locator ( '.download-group' )). toHaveCount ( 0 );
2026-09-21 23:10:58 +08:00
// on/off:单按钮(无 caret/菜单)。UI-02 V05:下载位在底部操作栏(.download-main)。
2026-09-20 18:32:34 +08:00
const onOff = await seedDownloadAlbum ( 'dl-onoff' , {});
await openLightbox ( page , onOff );
2026-09-21 23:10:58 +08:00
await expect ( page . locator ( '.download-main' )). toBeVisible ();
await expect ( page . locator ( '.download-caret' )). toHaveCount ( 0 );
2026-09-20 18:32:34 +08:00
2026-09-21 23:10:58 +08:00
// on/on:分体按钮,菜单两选项(浏览图 / 原图)。
2026-09-20 18:32:34 +08:00
const onOn = await seedDownloadAlbum ( 'dl-onon' , { originalDownloadEnabled : true });
await openLightbox ( page , onOn );
2026-09-21 23:10:58 +08:00
await expect ( page . locator ( '.download-caret' )). toBeVisible ();
await page . locator ( '.download-caret' ). click ();
await expect ( page . locator ( '.download-menu button' )). toHaveText ([ '浏览图' , '原图' ]);
2026-09-20 18:32:34 +08:00
});
test ( 'WeChat webview UA shows guidance overlay instead of downloading' , async ({ page }) => {
test . setTimeout ( 120 _000 );
const token = await seedDownloadAlbum ( 'dl-wechat' , {});
// UA 覆写先于应用代码(addInitScript);MicroMessenger 触发引导浮层面。
await openLightbox ( page , token , () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49(0x18003133) NetType/WIFI Language/zh_CN' ;
Object . defineProperty ( window . navigator , 'userAgent' , { get : () => ua , configurable : true });
});
let endpointHit = false ;
page . on ( 'request' , ( request ) => {
if ( request . url (). includes ( '/download?' )) endpointHit = true ;
});
2026-09-21 23:10:58 +08:00
await page . locator ( '.download-main' ). click ();
2026-09-20 18:32:34 +08:00
await expect ( page . locator ( '.wechat-guide[role="dialog"]' )). toBeVisible ();
await expect ( page . locator ( '.wechat-guide h2' )). toHaveText ( '保存图片指引' );
await expect ( page . locator ( '.guide-copy' )). toBeVisible ();
// 「知道了」收起浮层;期间 download 端点零命中(引导替代直接下载)。
await page . locator ( '.guide-dismiss' ). click ();
await expect ( page . locator ( '.wechat-guide' )). toHaveCount ( 0 );
expect ( endpointHit ). toBe ( false );
});
test ( 'real click downloads browse via signed url from the endpoint' , async ({ page }) => {
test . setTimeout ( 120 _000 );
const token = await seedDownloadAlbum ( 'dl-real' , {});
// 捕获 window.open(避免真实弹出标签页):记录 URL 供对象键断言。
await openLightbox ( page , token , () => {
( window as unknown as { __openedUrls : string [] }). __openedUrls = [];
window . open = (( url : string | URL ) => {
( window as unknown as { __openedUrls : string [] }). __openedUrls . push ( String ( url ));
return {} as Window ;
}) as typeof window . open ;
});
const endpoint = page . waitForResponse (
( response ) =>
response . url (). includes ( '/download?variant=browse' ) && response . status () === 200 ,
);
2026-09-21 23:10:58 +08:00
await page . locator ( '.download-main' ). click ();
2026-09-20 18:32:34 +08:00
const response = await endpoint ;
expect (( await response . json ()). variant ). toBe ( 'browse' );
await expect
. poll ( async () =>
page . evaluate (() => ( window as unknown as { __openedUrls : string [] }). __openedUrls [ 0 ] ?? '' ),
)
. toContain ( '/variants/' );
const opened = await page . evaluate (
() => ( window as unknown as { __openedUrls : string [] }). __openedUrls [ 0 ] ?? '' ,
);
// 短期签名凭证(SigV4 查询参数面)+ browse 对象键(无水印相册)。
expect ( opened ). toContain ( 'X-Amz-Signature' );
expect ( opened ). toContain ( '/browse.jpg' );
});