226 lines
9.1 KiB
TypeScript
226 lines
9.1 KiB
TypeScript
import { test, expect } from "./e2e-fixtures";
|
|
|
|
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")
|
|
.filter({
|
|
has: page.getByRole("heading", { name: "新增 HY2 节点", exact: true }),
|
|
});
|
|
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(() => ({
|
|
width: document.documentElement.scrollWidth,
|
|
viewport: innerWidth,
|
|
overflowing: [...document.querySelectorAll("body *")].filter(el => el.getBoundingClientRect().right > innerWidth && getComputedStyle(el).position !== "absolute").slice(0, 6).map(el => ({ tag: el.tagName, class: el.className, right: el.getBoundingClientRect().right })),
|
|
}))).toMatchObject({ width: 390, viewport: 390 });
|
|
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([]);
|
|
});
|