Files
imagefind/frontend/tests/fnos.spec.ts
T

121 lines
7.8 KiB
TypeScript

import { expect, Page, test } from "@playwright/test";
const sourceName=process.env.IMAGEFIND_E2E_SOURCE_NAME;
async function appApi<T>(page:Page,path:string,options:RequestInit={}):Promise<T>{
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<T>;
}
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<HTMLElement>(".content")!;const shell=document.querySelector<HTMLElement>(".app-shell")!;return {access:document.documentElement.dataset.imagefindAccess,topbarTop:Math.round(document.querySelector<HTMLElement>(".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<HTMLElement>(".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<Array<{id:string;filename:string}>>(page,"/uploads?limit=100");
expect(uploads.some(item=>item.filename===filename)).toBe(true);
}finally{
if(sourceId){
const videos=await appApi<Array<{source_id:string;source_key:string;display_name:string}>>(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<Array<{id:string;display_name:string}>>(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);
}
}
});