import { test as base, expect, type Page, type Cookie } from '@playwright/test' // Reuse one administrator login for UI fixtures, preserving the real login // rate limiter and leaving its remaining burst for the full transfer scenario. const test=base.extend<{}, {adminCookies:Cookie[]}>({ adminCookies:[async({browser},use)=>{ const context=await browser.newContext() try{ const response=await context.request.post('http://127.0.0.1:18981/api/v1/admin/login',{headers:{Origin:'http://127.0.0.1:18981'},data:{username:'admin',password:'e2e-secret-password'}}) expect(response.status()).toBe(200) await use(await context.cookies()) }finally{await context.close()} },{scope:'worker'}] }) async function adminFixture(page: Page) { const parcelQueries: URLSearchParams[] = [], auditQueries: URLSearchParams[] = [] await page.route('**/api/v1/admin/parcels?**', async route => { const q = new URL(route.request().url()).searchParams; parcelQueries.push(q) const size = Number(q.get('page_size') ?? 10), total = q.get('q') ? 1 : 23, pages = Math.max(1, Math.ceil(total / size)), current = Math.min(Number(q.get('page') ?? 1), pages) const items = Array.from({length: Math.min(size, total-(current-1)*size)}, (_,i) => { const n = (current-1)*size+i+1 return { id: n.toString(16).padStart(32,'0'), state:'ready', ip:'203.0.113.1', created:1791187200, expires:1791792000, count:1, size:12, files:[{id:'file-'+n,name:'样例文件-'+n+'.txt',size:12,status:'ready',remaining:5,expires:1791792000}] } }) await route.fulfill({json:{items,total,page:current,page_size:size,pages}}) }) await page.route('**/api/v1/admin/audit?**', async route => { const q = new URL(route.request().url()).searchParams; auditQueries.push(q) const size = Number(q.get('page_size') ?? 20), total = q.get('source') ? (q.get('source')==='no-match' ? 0 : 2) : 100000, pages = Math.max(1,Math.ceil(total/size)), current = Math.min(Number(q.get('page') ?? 1),pages) const items = Array.from({length:Math.min(size,Math.max(0,total-(current-1)*size))}, (_,i) => ({id:total-(current-1)*size-i,created:1791187200,action:q.get('action')||'admin.login',target:q.get('target')||'audit-'+(total-(current-1)*size-i),source:q.get('source')||'203.0.113.1'})) await route.fulfill({json:{items,total,page:current,page_size:size,pages}}) }) await page.goto('/#/admin') await expect(page.getByRole('tab',{name:'文件管理'})).toBeVisible() await expect(page.getByRole('navigation',{name:'文件管理分页'})).toContainText('共 23 个包裹') return {parcelQueries,auditQueries} } for(const mobile of [false,true]) { test(`admin tabs expand naturally and lists paginate/filter (${mobile?'mobile':'desktop'})`,async({page,adminCookies})=>{ await page.context().addCookies(adminCookies) if(mobile)await page.setViewportSize({width:390,height:844}) const errors:string[]=[];page.on('pageerror',e=>errors.push(e.message)) const {parcelQueries,auditQueries}=await adminFixture(page) const pager=page.getByRole('navigation',{name:'文件管理分页'}) await expect(page.locator('.admin-parcel:visible')).toHaveCount(10) await pager.getByRole('button',{name:'上一页',exact:true}).isDisabled().then(v=>expect(v).toBeTruthy()) await pager.getByRole('button',{name:'下一页',exact:true}).click() await expect(pager).toContainText('第 2 / 3 页');await expect(page.getByText('样例文件-11.txt',{exact:true})).toBeVisible() expect(parcelQueries.at(-1)?.get('page')).toBe('2') await pager.getByRole('button',{name:'下一页',exact:true}).click() await expect(page.locator('.admin-parcel:visible')).toHaveCount(3) await expect(pager.getByRole('button',{name:'下一页',exact:true})).toBeDisabled() await page.getByLabel('文件名或包裹 ID',{exact:true}).fill('样例文件-2') await page.locator('.parcel-filters').getByRole('button',{name:'搜索',exact:true}).click() await expect(pager).toContainText('共 1 个包裹');await expect(pager).toContainText('第 1 / 1 页') expect(parcelQueries.at(-1)?.get('q')).toBe('样例文件-2') await expect(pager.getByRole('button',{name:'下一页',exact:true})).toBeDisabled() await page.locator('.parcel-filters').getByRole('button',{name:'重置',exact:true}).click() await expect(pager).toContainText('共 23 个包裹') for(const name of ['文件管理','服务策略','容量与保留','操作审计']){ await page.getByRole('tab',{name,exact:true}).click() expect(await page.locator('.admin-panel:visible .admin-scroll').evaluate(node=>node.scrollHeight<=node.clientHeight+1)).toBeTruthy() expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBeTruthy() } const auditPager=page.getByRole('navigation',{name:'操作审计分页'}) await expect(page.locator('.audit-table tbody tr')).toHaveCount(20) await expect(auditPager).toContainText('共 100000 条') await expect(auditPager).toContainText('第 1 / 5000 页') await auditPager.getByRole('button',{name:'下一页',exact:true}).click() await expect(auditPager).toContainText('第 2 / 5000 页');expect(auditQueries.at(-1)?.get('page')).toBe('2') await page.getByLabel('操作审计分页跳转页码',{exact:true}).fill('5000') await auditPager.getByRole('button',{name:'跳转',exact:true}).click() await expect(auditPager).toContainText('第 5000 / 5000 页');await expect(auditPager.getByRole('button',{name:'下一页',exact:true})).toBeDisabled() await page.getByLabel('开始时间',{exact:true}).fill('2026-10-05T00:00');await page.getByLabel('结束时间',{exact:true}).fill('2026-10-06T00:00') await page.getByLabel('来源 IP',{exact:true}).fill('203.0.113') await page.getByLabel('目标标识',{exact:true}).fill('target-fragment') await page.locator('.audit-filters .n-base-selection').click() await page.getByText('删除包裹 · admin.delete',{exact:true}).click() await page.locator('.audit-filters').getByRole('button',{name:'搜索',exact:true}).click() await expect(auditPager).toContainText('共 2 条');await expect(auditPager).toContainText('第 1 / 1 页') const query=auditQueries.at(-1)!;expect(query.get('action')).toBe('admin.delete');expect(query.get('source')).toBe('203.0.113');expect(query.get('target')).toBe('target-fragment');expect(query.get('from')).toBe(await page.evaluate(()=>String(new Date('2026-10-05T00:00').getTime()/1000)));expect(query.get('to')).toBe(await page.evaluate(()=>String(new Date('2026-10-06T00:00').getTime()/1000))) await expect(page.locator('.audit-table tbody tr')).toHaveCount(2) await page.getByLabel('来源 IP',{exact:true}).fill('no-match');await page.locator('.audit-filters').getByRole('button',{name:'搜索',exact:true}).click() await expect(page.getByText('没有符合条件的审计记录。',{exact:true})).toBeVisible();await expect(auditPager).toContainText('共 0 条');await expect(auditPager.getByRole('button',{name:'下一页',exact:true})).toBeDisabled() await page.locator('.audit-filters').getByRole('button',{name:'重置',exact:true}).click() await expect(auditPager).toContainText('共 100000 条');expect(auditQueries.at(-1)?.has('source')).toBeFalsy() await page.getByLabel('操作审计分页每页条数',{exact:true}).selectOption('50');await expect(page.locator('.audit-table tbody tr')).toHaveCount(50);await expect(auditPager).toContainText('第 1 / 2000 页') expect(errors).toEqual([]) }) }