import { test, expect } from "./e2e-fixtures"; import Database from "better-sqlite3"; import { createHash } from "node:crypto"; test("managed relay UI, authenticated script, deployment state and subscription membership", async ({ page, request, }) => { 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.getByRole("link", { name: "节点管理", exact: true }).click(); const node = page .locator("details") .filter({ has: page.getByRole("heading", { name: "新增 HY2 节点", exact: true }), }); await node.locator("summary").click(); await node.getByLabel("Slug", { exact: true }).fill("jp2"); await node.getByLabel("显示名", { exact: true }).fill("JP2 direct"); await node .getByLabel("客户端地址", { exact: true }) .fill("jp2.p.xn--876a.net"); await node.getByRole("button", { name: "创建节点" }).click(); await expect( page.getByText("JP2 direct", { exact: true }).first(), ).toBeVisible(); await page.getByRole("link", { name: "中转线路", exact: true }).click(); const host = page .locator("details") .filter({ has: page.getByRole("heading", { name: "新增中转主机" }) }); await host.getByLabel("主机名称").fill("HK2"); await host.getByLabel("公网域名 / IP").fill("hk2.p.xn--876a.net"); await host.getByRole("button", { name: "创建主机" }).click(); await expect(page.getByText("等待安装", { exact: true })).toBeVisible(); const row = page .locator("details") .filter({ has: page.locator("summary strong", { hasText: /^HK2$/ }) }); await row.locator("summary").click(); const href = await row .getByRole("link", { name: "下载安装 SH" }) .getAttribute("href"); expect((await request.get(href!, { maxRedirects: 0 })).status()).toBe(307); const script = await page.request.get(href!); expect(script.status()).toBe(200); expect(await script.text()).toContain("realm"); const create = page .locator("details") .filter({ has: page.getByRole("heading", { name: "新增中转线路" }) }); await create.getByLabel("线路名称").fill("JP2 via HK2"); await create .getByLabel("中转主机", { exact: true }) .selectOption({ label: "HK2" }); await create .getByLabel("目标节点") .selectOption({ label: "JP2 direct · UDP" }); await create.getByLabel("公网端口").fill("51044"); await create.getByLabel("本机监听端口").fill("51044"); await create.getByRole("button", { name: "创建线路" }).click(); await expect( page.locator("summary strong", { hasText: "JP2 via HK2" }), ).toBeVisible(); const db = new Database(process.env.HY2_E2E_DATABASE!); const token = "a".repeat(64); const id = (db.prepare("SELECT id FROM relay_hosts").get() as { id: string }) .id; db.prepare("UPDATE relay_hosts SET token_hash=? WHERE id=?").run( createHash("sha256").update(token).digest("hex"), id, ); db.close(); expect( ( await request.post(`/api/relay/${id}/sync`, { data: { appliedHash: "", active: false, error: "" }, }) ).status(), ).toBe(403); expect( ( await request.post(`/api/relay/${id}/sync`, { data: "x".repeat(17000) }) ).status(), ).toBe(413); const sync = await request.post(`/api/relay/${id}/sync`, { headers: { Authorization: `Bearer ${token}` }, data: { appliedHash: "", active: false, error: "" }, }); expect(sync.status()).toBe(200); const desired = await sync.json(); expect(desired.config.endpoints[0].network.use_udp).toBe(true); await request.post(`/api/relay/${id}/sync`, { headers: { Authorization: `Bearer ${token}` }, data: { appliedHash: desired.hash, active: true, error: "" }, }); await page.reload(); await expect( page.getByText("配置已应用", { exact: true }).first(), ).toBeVisible(); await page.screenshot({ path: "test-results/relays-desktop.png", fullPage: true, }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: "test-results/relays-mobile.png", fullPage: true, }); expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth + 1, ), ).toBe(true); await page.getByRole("link", { name: "订阅配置", exact: true }).click(); await expect( page.getByRole("button", { name: "拖拽排序 JP2 via HK2" }), ).toBeVisible(); });