深度优化 ui
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
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<void>((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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user