import { expect, Page, test } from "@playwright/test"; const sourceName=process.env.IMAGEFIND_E2E_SOURCE_NAME; async function appApi(page:Page,path:string,options:RequestInit={}):Promise{ return page.evaluate(async({path,options})=>{ const base=(window.__IMAGEFIND_BASE__||"/").replace(/\/$/,""); const headers=new Headers(options.headers);const csrf=sessionStorage.getItem("imagefind:csrf"); if(csrf)headers.set("X-CSRF-Token",csrf); if(typeof options.body==="string")headers.set("Content-Type","application/json"); const response=await fetch(`${base}/api/v1${path}`,{...options,headers,credentials:"same-origin"}); if(!response.ok)throw new Error(`${response.status} ${await response.text()}`); return response.status===204?null:response.json(); },{path,options}) as Promise; } test("fnOS deployment renders the app shell and system-history navigation",async({page})=>{ await page.setViewportSize({width:390,height:844}); await page.goto(""); await expect(page.getByRole("heading",{name:"最近新增"})).toBeVisible(); const homeLayout=await page.evaluate(()=>{const content=document.querySelector(".content")!;const shell=document.querySelector(".app-shell")!;return {access:document.documentElement.dataset.imagefindAccess,topbarTop:Math.round(document.querySelector(".topbar")!.getBoundingClientRect().top),contentOverflow:getComputedStyle(content).overflowY,shellOverflow:getComputedStyle(shell).overflow,documentHeight:document.documentElement.scrollHeight,viewportHeight:innerHeight}}); expect(["direct","gateway"]).toContain(homeLayout.access); expect(homeLayout).toMatchObject({topbarTop:0,contentOverflow:"auto",shellOverflow:"hidden"}); expect(homeLayout.documentHeight).toBeLessThanOrEqual(homeLayout.viewportHeight+1); await expect(page.locator(".sidebar nav > button:visible")).toHaveCount(5); await page.getByRole("button",{name:"更多"}).click(); await expect(page.locator(".mobile-more-menu").getByText("退出登录",{exact:true})).toHaveCount(homeLayout.access==="gateway"?0:1); await expect(page.getByText("返回飞牛桌面",{exact:true})).toHaveCount(0); await page.locator(".mobile-more-grid>.mobile-more-item").filter({hasText:"设置"}).click(); await expect(page.getByRole("heading",{name:"设置"})).toBeVisible(); await expect(page.locator(".topbar")).toBeHidden(); await expect(page.locator(".sidebar")).toBeHidden(); await expect(page.locator(".content")).not.toHaveClass(/sheet-locked/); await expect.poll(()=>page.locator(".content").evaluate(element=>element.scrollHeight-element.clientHeight)).toBeGreaterThan(320); await page.locator(".content").evaluate(element=>element.scrollTo({top:320,behavior:"instant"})); await expect.poll(()=>page.locator(".content").evaluate(element=>element.scrollTop)).toBeGreaterThan(100); await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0); await page.goBack(); await expect(page.getByRole("heading",{name:"最近新增"})).toBeVisible(); }); test("live transfer, profile, resources and media surfaces remain usable",async({page})=>{ await page.setViewportSize({width:390,height:844}); await page.goto(""); const status=await appApi<{configured:boolean;version:string;access_mode:string}>(page,"/status"); expect(status).toMatchObject({configured:true,version:"0.5.45"}); await page.getByRole("button",{name:"更多"}).click(); await page.locator(".mobile-more-grid>.mobile-more-item").filter({hasText:"上传中心"}).click(); await expect(page.getByRole("heading",{name:"传输中心"})).toBeVisible(); await expect(page.locator(".transfer-tabs>button")).toHaveCount(2); await page.locator(".transfer-tabs").getByRole("button",{name:/后台下载/}).click(); const aria2=await appApi<{available:boolean;running:boolean}>(page,"/downloads/runtime"); expect(typeof aria2.available).toBe("boolean"); await expect(page.locator(".download-list")).toBeVisible(); await page.goBack(); await page.locator(".mobile-profile-button").click(); await expect(page.getByRole("button",{name:"个人资料"})).toBeVisible(); await expect(page.locator(".profile-stats article")).toHaveCount(4); await page.getByRole("button",{name:"更多"}).click(); await page.locator(".mobile-more-grid>.mobile-more-item").filter({hasText:"设置"}).click(); await page.getByRole("button",{name:"任务与偏好"}).click(); await expect(page.locator(".resource-panel")).toContainText("前台访问优先"); await expect(page.locator(".resource-lanes article")).toHaveCount(4); const resources=await appApi<{profile:string;policy:{cpu_pause_percent:number;cpu_resume_percent:number}}>(page,"/system/resources"); expect(["conservative","balanced","turbo"]).toContain(resources.profile); expect(resources.policy.cpu_resume_percent).toBeLessThan(resources.policy.cpu_pause_percent); await page.goBack(); await page.locator(".nav-item-1").click(); const cards=page.locator(".video-card"); if(await cards.count()){ await cards.first().click(); await expect(page.locator(".player-stage video")).toBeVisible(); await expect(page.getByRole("button",{name:"删除视频"})).toBeVisible(); const source=await page.locator(".player-stage video").getAttribute("src"); expect(source).toContain("/api/v1/videos/"); } }); for(const width of [320,390])test(`live mobile layout has no horizontal overflow at ${width}px`,async({page})=>{ await page.setViewportSize({width,height:780}); await page.goto(""); const geometry=await page.evaluate(()=>({viewport:innerWidth,document:document.documentElement.scrollWidth,content:document.querySelector(".content")!.scrollWidth})); expect(geometry.document).toBeLessThanOrEqual(geometry.viewport+1); expect(geometry.content).toBeLessThanOrEqual(geometry.viewport+1); await expect(page.locator(".sidebar nav > button:visible")).toHaveCount(5); }); test("fnOS isolated source supports upload and test-owned cleanup",async({page})=>{ test.skip(!sourceName,"设置 IMAGEFIND_E2E_SOURCE_NAME 后才运行写入测试"); await page.setViewportSize({width:390,height:844}); await page.goto(""); const filename=`E2E-${Date.now()}.mp4`; let sourceId=""; try{ await page.locator(".sidebar nav > .upload-status").click(); const sourceSelect=page.getByLabel("目标媒体库"); const option=sourceSelect.locator("option").filter({hasText:sourceName!}); await expect(option).toHaveCount(1); sourceId=await option.getAttribute("value")||""; await sourceSelect.selectOption(sourceId); await page.locator('.drop-zone input[type="file"]').setInputFiles({name:filename,mimeType:"video/mp4",buffer:Buffer.from("imagefind isolated e2e fixture")}); await page.getByRole("button",{name:"开始上传"}).click(); await expect(page.getByText(/已进入后台传输队列|1 个文件已排队/)).toBeVisible(); await page.waitForTimeout(1500); const uploads=await appApi>(page,"/uploads?limit=100"); expect(uploads.some(item=>item.filename===filename)).toBe(true); }finally{ if(sourceId){ const videos=await appApi>(page,"/videos?limit=500").catch(()=>[]); const owned=videos.filter(item=>item.source_id===sourceId&&(item.display_name===filename||item.source_key.endsWith(`/${filename}`)||item.source_key===filename)); if(owned.length)await appApi(page,"/files/trash",{method:"POST",body:JSON.stringify({source_id:sourceId,keys:owned.map(item=>item.source_key)})}).catch(()=>null); const trash=await appApi>(page,`/trash?source_id=${encodeURIComponent(sourceId)}`).catch(()=>[]); for(const item of trash.filter(value=>value.display_name===filename))await appApi(page,`/trash/${item.id}`,{method:"DELETE"}).catch(()=>null); } } });