20260617 增加RPKI Explorer前端

This commit is contained in:
2026-06-17 17:29:20 +08:00
parent 6ef2c98890
commit 56f0d10dc6
28 changed files with 7362 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
test("renders live object detail and lazy tab requests", async ({ page }) => {
const apiRequests: string[] = [];
const consoleErrors: string[] = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.startsWith("/api/v1")) {
apiRequests.push(`${url.pathname}${url.search}`);
}
});
page.on("console", (message) => {
if (message.type() === "error") {
consoleErrors.push(message.text());
}
});
await page.goto("/");
await page.getByRole("button", { name: /Objects/i }).click();
await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 });
await expect(page.getByText("5A179648B3EF2369DCE7BDB58140FF7DC7060ABF.mft").first()).toBeVisible();
await expect(page.getByText("Final status: valid")).toBeVisible();
const objectDetail = page.getByRole("complementary", { name: "Live object detail" });
await expect(objectDetail.getByText("Authoritative", { exact: true })).toBeVisible();
expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/parsed"))).toBe(false);
await page.getByRole("tab", { name: "Parsed" }).click();
await expect(page.getByText("Projection unavailable")).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/parsed"))).toBe(true);
await page.getByRole("tab", { name: "Chain" }).click();
await expect(page.getByText("No chain edges recorded for this object.")).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/chain"))).toBe(true);
await page.getByRole("tab", { name: "Validation" }).click();
await page.getByRole("button", { name: "Explain validation" }).click();
await expect(page.getByText("audit_projection")).toBeVisible({ timeout: 30_000 });
await expect(objectDetail.getByText("false").last()).toBeVisible();
expect(apiRequests.some((request) => request.includes("/validation/explain"))).toBe(true);
await page.screenshot({
path: "../../../../specs/develop/20260617/m5_playwright/rpki-explorer-object-detail-live.png",
fullPage: true
});
expect(consoleErrors).toEqual([]);
});

View File

@@ -0,0 +1,39 @@
import { expect, test } from "@playwright/test";
test("renders live overview data from query service without global object list", async ({ page }) => {
const apiRequests: string[] = [];
const consoleErrors: string[] = [];
page.on("request", (request) => {
const path = new URL(request.url()).pathname;
if (path.startsWith("/api/v1")) {
apiRequests.push(path);
}
});
page.on("console", (message) => {
if (message.type() === "error") {
consoleErrors.push(message.text());
}
});
await page.goto("/");
await expect(page.getByText("run 7144")).toBeVisible();
await expect(page.getByText("963,779")).toBeVisible();
await expect(page.getByText("534,760")).toBeVisible();
await expect(page.getByText("Top repositories by workload")).toBeVisible();
await expect(page.locator(".issue-card").first().getByText("Reject")).toBeVisible();
expect(apiRequests).toContain("/api/v1/latest_run");
expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/object-types");
expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/validation");
expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/reasons");
expect(apiRequests).toContain("/api/v1/runs/run_7144/repos");
expect(apiRequests).not.toContain("/api/v1/runs/run_7144/objects");
expect(consoleErrors).toEqual([]);
await page.screenshot({
path: "../../../../specs/develop/20260617/m3_playwright/rpki-explorer-overview-live.png",
fullPage: true
});
});

View File

@@ -0,0 +1,51 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
test("loads repository, publication point, and object data on demand", async ({ page }) => {
const apiRequests: string[] = [];
const consoleErrors: string[] = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.startsWith("/api/v1")) {
apiRequests.push(`${url.pathname}${url.search}`);
}
});
page.on("console", (message) => {
if (message.type() === "error") {
consoleErrors.push(message.text());
}
});
await page.goto("/");
await page.getByRole("button", { name: /Repositories/i }).click();
await expect(page.getByRole("heading", { name: "Repository / publication point / object browser" })).toBeVisible();
await expect(page.getByText("Select a repository to load its publication points.")).toBeVisible();
expect(apiRequests.some((request) => request.includes("/publication-points"))).toBe(false);
expect(apiRequests.some((request) => request.includes("/objects"))).toBe(false);
await page.getByRole("button", { name: /sakuya\.nat\.moe/i }).click();
await expect(page.getByRole("button", { name: /5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/i })).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("/repos/0490c1fe6e4d4ae5cc354948/publication-points"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/objects"))).toBe(false);
await page.getByRole("button", { name: /5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/i }).click();
await expect.poll(
() => apiRequests.some((request) => request.includes("/publication-points/node_10342/objects")),
{ timeout: 70_000 }
).toBe(true);
const objectsPanel = page.getByRole("region", { name: "Objects for publication point" });
await expect(
objectsPanel.getByText(/rsync:\/\/sakuya\.nat\.moe\/repo\/NATOCA\/1\/5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/)
).toBeVisible({ timeout: 70_000 });
await expect(objectsPanel.getByText("manifest").first()).toBeVisible();
await expect(page.getByText("limit=50")).toBeVisible();
expect(consoleErrors).toEqual([]);
await page.screenshot({
path: "../../../../specs/develop/20260617/m4_playwright/rpki-explorer-repository-browser.png",
fullPage: true
});
});

View File

@@ -0,0 +1,56 @@
import { expect, test } from "@playwright/test";
test("opens the RPKI Explorer overview prototype", async ({ page }) => {
const consoleErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") {
const text = message.text();
if (!text.includes("Failed to load resource")) {
consoleErrors.push(text);
}
}
});
await page.goto("/");
await expect(page.getByRole("heading", { name: "RPKI Explorer" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Global RPKI validation health" })).toBeVisible();
await expect(page.getByRole("button", { name: /Overview/i })).toBeVisible();
await expect(page.getByText("Top repositories by workload")).toBeVisible();
await expect(page.getByText("Recent validation issues")).toBeVisible();
await expect(page.getByRole("cell", { name: /RRDP|RSYNC/i }).first()).toBeVisible();
await page.screenshot({
path: "../../../../specs/develop/20260617/m2_playwright/rpki-explorer-overview.png",
fullPage: true
});
expect(consoleErrors).toEqual([]);
});
test("opens the RPKI Explorer object detail prototype", async ({ page }) => {
test.setTimeout(90_000);
const consoleErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") {
const text = message.text();
if (!text.includes("Failed to load resource")) {
consoleErrors.push(text);
}
}
});
await page.goto("/");
await page.getByRole("button", { name: /Objects/i }).click();
await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 });
await expect(page.getByText("Object detail · live query service")).toBeVisible();
await expect(page.getByText("File and chain checks")).toBeVisible();
await expect(page.getByRole("tab", { name: "Validation" })).toBeVisible();
await page.getByRole("tab", { name: "Validation" }).click();
await expect(page.getByText("Final status: valid")).toBeVisible();
await page.screenshot({
path: "../../../../specs/develop/20260617/m2_playwright/rpki-explorer-object-detail.png",
fullPage: true
});
expect(consoleErrors).toEqual([]);
});

View File

@@ -0,0 +1,40 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
test("supports URI search and reports raw/export workflow status", async ({ page }) => {
const apiRequests: string[] = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.startsWith("/api/v1")) {
apiRequests.push(`${request.method()} ${url.pathname}${url.search}`);
}
});
await page.goto("/");
await page.getByRole("button", { name: /Objects/i }).click();
await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 });
await page.getByRole("button", { name: "Use selected URI" }).click();
await page.getByPlaceholder("Exact URI lookup...").press("Enter");
await expect.poll(
() => apiRequests.some((request) => request.includes("/objects/by-uri")),
{ timeout: 30_000 }
).toBe(true);
await page.getByRole("button", { name: "Download raw" }).click();
await expect(page.getByText("Raw download failed: repo-bytes db is not configured for raw download")).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/raw"))).toBe(true);
await page.getByRole("button", { name: "Export object" }).click();
await expect(page.getByText("Object export failed: repo-bytes db is required for export jobs")).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("POST /api/v1/runs/run_7144/exports"))).toBe(true);
await page.getByRole("button", { name: "Export selected PP" }).click();
await expect(page.getByText("PP export failed: repo-bytes db is required for export jobs")).toBeVisible({ timeout: 30_000 });
await page.screenshot({
path: "../../../../specs/develop/20260617/m6_playwright/rpki-explorer-workflows.png",
fullPage: true
});
});