feat: 支持自定义订阅节点并兼容现有数据库

This commit is contained in:
2026-09-29 11:52:41 +08:00
parent 3faf960f35
commit ab7d1c937a
27 changed files with 2203 additions and 449 deletions
+133
View File
@@ -0,0 +1,133 @@
import { test, expect } from "./e2e-fixtures";
import { parse } from "yaml";
test("custom YAML nodes use compact forms, persist edits and integrate with subscription groups", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.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.getByRole("link", { name: "节点管理" }).click();
const create = page
.locator(".compact-create-panel details")
.filter({ has: page.getByRole("heading", { name: "新增自定义订阅节点" }) });
await create.locator(":scope > summary").click();
await create.getByLabel("Slug", { exact: true }).fill("custom-browser");
await create.getByLabel("显示名", { exact: true }).fill("香港自定义节点");
await create.getByRole("textbox", { name: "节点 YAML", exact: true }).fill("name: [invalid");
await create.getByRole("button", { name: "创建自定义节点" }).click();
await expect(create.getByRole("alert")).toContainText("YAML 格式错误");
await expect(create.getByRole("textbox", { name: "节点 YAML", exact: true })).toHaveValue(
"name: [invalid",
);
const source =
'# keep this comment\nproxies:\n - name: \'HK "AnyTLS": #1\'\n type: anytls\n server: hk.example.com\n port: 443\n password: "test-only:password"\n udp: true\n';
await create.getByRole("textbox", { name: "节点 YAML", exact: true }).fill(source);
await create.getByRole("button", { name: "创建自定义节点" }).click();
const row = page
.locator(".entity-table__item")
.filter({ hasText: "custom-browser" });
await expect(row).toBeVisible();
await expect(row).toContainText("不轮询");
await row.locator(":scope > summary").click();
await expect(row.getByRole("textbox", { name: "节点 YAML", exact: true })).toHaveValue(
source,
);
await expect(row.getByLabel("客户端地址", { exact: true })).toHaveCount(0);
const updated = source.replace('HK "AnyTLS": #1', 'HK "AnyTLS": #2');
await row.getByRole("textbox", { name: "节点 YAML", exact: true }).fill(updated);
await row.getByRole("button", { name: "保存自定义节点" }).click();
await expect(row.locator("p[role=status]")).toHaveText("已保存");
await page.reload();
await row.locator(":scope > summary").click();
await expect(
row.getByRole("textbox", { name: "节点 YAML", exact: true }),
).toHaveValue(updated);
await page.screenshot({
path: "test-results/custom-node-desktop.png",
fullPage: true,
});
const config = await page.request.get(
"/api/admin/nodes/custom-browser/config",
);
expect(config.status()).toBe(409);
const auth = await page.request.post("/api/hy2/nodes/custom-browser/auth", {
data: { auth: "test:test" },
});
expect(await auth.json()).toMatchObject({ ok: false });
await page.getByRole("link", { name: "订阅配置" }).click();
const main = page.locator(".proxy-group-card").first();
await expect(main.locator(".proxy-sort-item")).toContainText(
'HK "AnyTLS": #2',
);
const createGroup = page
.locator("details")
.filter({ has: page.locator("summary", { hasText: "新建分组" }) });
await createGroup.locator(":scope > summary").click();
await createGroup.getByLabel("新建分组名").fill("Custom Work");
await createGroup.getByRole("button", { name: /HK.*添加/ }).click();
await createGroup
.getByRole("button", { name: "新建分组", exact: true })
.click();
await expect(
page.locator("summary strong", { hasText: "Custom Work" }),
).toBeVisible();
const url = await page.locator(".copy-line code").first().textContent();
const response = await page.request.get(url!);
expect(response.status()).toBe(200);
const subscription = parse(await response.text());
const proxy = subscription.proxies.find(
(p: { type: string }) => p.type === "anytls",
);
expect(proxy).toMatchObject({
name: 'HK "AnyTLS": #2',
server: "hk.example.com",
password: "test-only:password",
udp: true,
});
expect(
subscription["proxy-groups"].find(
(g: { name: string }) => g.name === "Custom Work",
).proxies,
).toEqual([proxy.name]);
const group = page
.locator("details")
.filter({
has: page.locator("summary strong", { hasText: "Custom Work" }),
});
await group.locator(":scope > summary").click();
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.getByRole("searchbox").fill("custom");
await expect(page.locator(".entity-table__item:visible")).toHaveCount(1);
await row.locator(":scope > summary").click();
await page.setViewportSize({ width: 390, height: 844 });
await expect
.poll(() =>
page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
)
.toBe(true);
await page.screenshot({
path: "test-results/custom-node-mobile.png",
fullPage: true,
});
await row.getByLabel("启用节点", { exact: true }).uncheck();
await row.getByRole("button", { name: "保存自定义节点" }).click();
await expect(row.locator("p[role=status]")).toHaveText("已保存");
const disabled = parse(await (await page.request.get(url!)).text());
expect(
disabled.proxies.some((p: { type: string }) => p.type === "anytls"),
).toBe(false);
await row.getByRole("button", { name: "删除节点" }).click();
await page.getByRole("button", { name: "确认删除节点" }).click();
await expect(row).toHaveCount(0);
expect(errors).toEqual([]);
});