import { test, expect } from "@playwright/test"; test("compact management flows, drag persistence, filters and mobile layout", async ({ page, context, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/login"); await page.getByLabel("用户名", { exact: true }).fill("ui-review"); await page.getByLabel("密码", { exact: true }).fill("ui-review-local-2026"); await page.getByRole("button", { name: "登录", exact: true }).click(); await expect(page).toHaveURL(/dashboard/); await page.screenshot({ path: "test-results/dashboard-desktop.png", fullPage: true, }); await page.getByRole("link", { name: "节点管理" }).click(); for (const [slug, name] of [ ["tokyo", "Tokyo"], ["singapore", "Singapore"], ["frankfurt", "Frankfurt"], ]) { const create = page.locator(".compact-create-panel details"); if ((await create.getAttribute("open")) === null) await create.locator("summary").click(); await create.getByLabel("Slug", { exact: true }).fill(slug); await create.getByLabel("显示名", { exact: true }).fill(name); await create .getByLabel("客户端地址", { exact: true }) .fill(`${slug}.example.com`); await create.getByRole("button", { name: "创建节点" }).click(); await expect( page.locator(".entity-main-cell strong", { hasText: name }), ).toBeVisible(); } await page.getByRole("searchbox").fill("Tokyo"); await expect(page.locator(".entity-table__item:visible")).toHaveCount(1); await page.getByRole("button", { name: "清除筛选" }).click(); await expect(page.locator(".entity-table__item:visible")).toHaveCount(3); await page.screenshot({ path: "test-results/nodes-desktop.png", fullPage: true, }); await page.getByRole("link", { name: "订阅配置" }).click(); const main = page.locator(".proxy-group-card").first(); const labels = () => main.locator(".proxy-sort-item strong").allTextContents(); await expect(main.locator(".proxy-sort-item")).toHaveCount(3); const initial = await labels(); const handles = main.getByRole("button", { name: /拖拽排序/ }); await handles.first().focus(); await page.keyboard.press("Space"); await expect(handles.first()).toHaveAttribute("aria-pressed", "true"); // dnd-kit measures droppable rows and attaches its keyboard listener after activation. await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())), ), ); await page.keyboard.press("ArrowDown"); await expect(page.locator("[id^=DndLiveRegion]")).toContainText("目标位置 2"); await page.keyboard.press("Space"); await expect.poll(labels).toEqual([initial[1], initial[0], initial[2]]); await main.getByRole("button", { name: "撤销修改" }).click(); await expect.poll(labels).toEqual(initial); const from = await handles.first().boundingBox(); const to = await handles.nth(2).boundingBox(); await page.mouse.move(from!.x + 15, from!.y + 15); await page.mouse.down(); await page.mouse.move(to!.x + 15, to!.y + 15, { steps: 12 }); await page.mouse.up(); await expect.poll(labels).toEqual([initial[1], initial[2], initial[0]]); await main.getByRole("button", { name: "保存顺序", exact: true }).click(); await expect(main.locator(".action-row [role=status]")).toHaveText("已保存"); await page.reload(); await expect.poll(labels).toEqual([initial[1], initial[2], initial[0]]); const newGroup = page .locator("details") .filter({ has: page.locator("summary", { hasText: "新建分组" }) }); await newGroup.locator("summary").click(); await newGroup.getByLabel("新建分组名").fill("Work"); await newGroup.getByRole("button", { name: "全部添加" }).click(); await newGroup.getByRole("button", { name: "新建分组", exact: true }).click(); const group = page .locator("details") .filter({ has: page.locator("summary strong", { hasText: /^Work$/ }) }); await expect(group).toBeVisible(); await group.locator("summary").click(); await group.getByRole("button", { name: "删除分组" }).click(); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "取消", exact: true }).click(); await expect(group).toBeVisible(); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await page.getByRole("button", { name: "复制", exact: true }).click(); await expect(page.getByRole("button", { name: "已复制" })).toBeVisible(); await expect .poll(() => page.evaluate(() => navigator.clipboard.readText())) .toContain("/api/subscription/"); await page.screenshot({ path: "test-results/subscriptions-desktop.png", fullPage: true, }); await group.getByRole("button", { name: "删除分组" }).click(); await page.getByRole("button", { name: "确认删除分组" }).click(); await expect(group).toHaveCount(0); await page.getByRole("link", { name: "用户管理" }).click(); await page.locator(".compact-create-summary").click(); const createUser = page.locator(".compact-create-panel"); await createUser.getByLabel("用户名", { exact: true }).fill("test-member"); await createUser.getByLabel("密码", { exact: true }).fill("test-member-pass"); await createUser.getByRole("button", { name: "创建用户" }).click(); const user = page .locator(".entity-table__item") .filter({ hasText: "test-member" }); await expect(user).toBeVisible(); await user.locator("summary").click(); await user .getByRole("textbox", { name: "备注", exact: true }) .fill("updated via UI"); await user.getByRole("button", { name: "保存修改" }).click(); await expect(user.locator("p[role=status]")).toHaveText("已保存"); await page.reload(); await user.locator("summary").click(); await expect( user.getByRole("textbox", { name: "备注", exact: true }), ).toHaveValue("updated via UI"); await page.screenshot({ path: "test-results/users-desktop.png", fullPage: true, }); await user .getByRole("textbox", { name: "备注", exact: true }) .fill("keep on failure"); await page.route("**/admin/users", (route) => route.request().headers()["next-action"] ? route.abort() : route.continue(), ); await user.getByRole("button", { name: "保存修改" }).click(); await expect(user.getByRole("alert")).toContainText("操作失败"); await expect( user.getByRole("textbox", { name: "备注", exact: true }), ).toHaveValue("keep on failure"); await page.unroute("**/admin/users"); await page.setViewportSize({ width: 390, height: 844 }); for (const route of [ "users", "nodes", "subscriptions", "audits", "dashboard", ]) { await page.goto(`/admin/${route}`); await expect .poll(() => page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ) .toBe(true); await page.screenshot({ path: `test-results/${route}-mobile.png`, fullPage: true, }); } // Exercise native touch input against the same handles on a narrow viewport. await page.goto("/admin/subscriptions"); await expect(main.locator(".proxy-sort-item")).toHaveCount(3); const beforeTouch = await labels(); const touchFrom = await handles.first().boundingBox(); const touchTo = await handles.nth(1).boundingBox(); const cdp = await context.newCDPSession(page); await cdp.send("Emulation.setTouchEmulationEnabled", { enabled: true }); await cdp.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x: touchFrom!.x + 15, y: touchFrom!.y + 15 }], }); for (let i = 1; i <= 10; i++) { await cdp.send("Input.dispatchTouchEvent", { type: "touchMove", touchPoints: [ { x: touchFrom!.x + 15, y: touchFrom!.y + 15 + ((touchTo!.y - touchFrom!.y) * i) / 10, }, ], }); } await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [], }); await expect .poll(labels) .toEqual([beforeTouch[1], beforeTouch[0], beforeTouch[2]]); await main.getByRole("button", { name: "撤销修改" }).click(); await cdp.detach(); await page.getByRole("button", { name: "退出登录" }).click(); await page.getByLabel("用户名", { exact: true }).fill("test-member"); await page.getByLabel("密码", { exact: true }).fill("test-member-pass"); await page.getByRole("button", { name: "登录", exact: true }).click(); await expect(page).toHaveURL(/\/me$/); await expect .poll(() => page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ) .toBe(true); await page.screenshot({ path: "test-results/account-mobile.png", fullPage: true, }); expect(errors).toEqual([]); });