20260623 optimize rpki explorer ui interactions

This commit is contained in:
2026-06-23 15:47:50 +08:00
parent 4f41fbe04e
commit 574e40a4d4
16 changed files with 1483 additions and 297 deletions

View File

@@ -0,0 +1,62 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
const screenshotRoot = "../../../../specs/develop/20260623_2/m7_copyable_values_pagination_playwright";
async function openRepositoryBranch(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByRole("button", { name: /Repositories/i }).click();
const reposSection = page.locator('section[aria-label="Repositories"]');
await expect(reposSection.locator(".stack-row").first()).toBeVisible({ timeout: 30_000 });
await reposSection.getByLabel("Filter repositories on current page").fill("sakuya");
await reposSection.locator(".stack-row-select").first().click();
const ppSection = page.locator('section[aria-label="Publication points"]');
await expect(ppSection.locator(".stack-row").first()).toBeVisible({ timeout: 30_000 });
await ppSection.locator(".stack-row-select").first().click();
const objectsPanel = page.getByRole("region", { name: "Objects for publication point" });
await expect(objectsPanel.locator("tbody tr").first()).toBeVisible({ timeout: 70_000 });
return objectsPanel;
}
test("long repository, publication point, URI, and hash values expose copy and full hover text", async ({ context, page }) => {
await context.grantPermissions(["clipboard-write"], { origin: "http://127.0.0.1:5173" });
const objectsPanel = await openRepositoryBranch(page);
await expect(page.getByRole("button", { name: "Copy repository URI" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Copy publication point URI" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Copy manifest URI" }).first()).toBeVisible();
await expect(objectsPanel.getByRole("button", { name: "Copy object URI" }).first()).toBeVisible();
await expect(objectsPanel.getByRole("button", { name: "Copy object SHA256" }).first()).toBeVisible();
const objectUri = objectsPanel.locator(".uri-cell .copyable-value-text").first();
const objectUriText = await objectUri.innerText();
await objectUri.hover();
await expect(page.locator(".copyable-tooltip")).toContainText(objectUriText);
await objectsPanel.getByRole("button", { name: "Copy object URI" }).first().click();
await expect(objectsPanel.locator(".copy-state-text").first()).toHaveText("Copied");
await objectUri.hover();
await page.screenshot({ path: `${screenshotRoot}/repository-copyable-tooltip.png`, fullPage: true });
});
test("object detail keeps copy controls without extra object list panels", async ({ page }) => {
const objectsPanel = await openRepositoryBranch(page);
await objectsPanel.getByRole("button", { name: "Open" }).first().click();
await expect(page.getByText("Object detail · live query service")).toBeVisible({ timeout: 70_000 });
await expect.poll(async () => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect(page.getByRole("complementary", { name: "Publication point object list" })).toHaveCount(0);
await expect(page.getByRole("region", { name: "Publication point object table" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Copy selected object URI" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy SHA256" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy Repository" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy Publication Point" })).toBeVisible();
const objectHash = page.locator(".object-meta-grid").getByText(/^[a-f0-9]{64}$/).first();
const objectHashText = await objectHash.innerText();
await objectHash.hover();
await expect(page.locator(".copyable-tooltip")).toContainText(objectHashText.toLowerCase());
await page.screenshot({ path: `${screenshotRoot}/object-detail-copyable-only.png`, fullPage: true });
});

View File

@@ -1,6 +1,22 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
const screenshotRoot = "../../../../specs/develop/20260623_2/m6_full_e2e";
async function openFirstRepositoryObject(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByRole("button", { name: /Repositories/i }).click();
const reposSection = page.locator('section[aria-label="Repositories"]');
await reposSection.getByLabel("Filter repositories on current page").fill("sakuya");
await reposSection.locator(".stack-row-select").first().click();
await page.locator('section[aria-label="Publication points"] .stack-row-select').first().click();
const row = page.locator('section[aria-label="Objects for publication point"] tbody tr').first();
await expect(row).toBeVisible({ timeout: 70_000 });
const expectedUri = (await row.locator(".uri-cell .copyable-value-text").innerText()).trim();
await row.getByRole("button", { name: "Open" }).click();
await expect(page.locator(".object-detail-card")).toContainText(expectedUri, { timeout: 70_000 });
return expectedUri;
}
test("renders live object detail and lazy tab requests", async ({ page }) => {
const apiRequests: string[] = [];
@@ -18,33 +34,32 @@ test("renders live object detail and lazy tab requests", async ({ page }) => {
}
});
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();
await openFirstRepositoryObject(page);
await expect(page.getByText("Object detail · live query service")).toBeVisible();
await expect(page.getByText("File and chain checks")).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 expect(page.getByRole("complementary", { name: "Publication point object list" })).toHaveCount(0);
await expect(page.getByRole("region", { name: "Publication point object table" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Copy selected object URI" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy SHA256" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy Repository" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy Publication Point" })).toBeVisible();
expect(apiRequests.some((request) => request.includes("/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 expect(page.getByText(/Projection JSON|Projection unavailable/)).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("/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 expect(page.getByRole("tabpanel")).toHaveAttribute("aria-labelledby", "object-tab-chain");
expect(apiRequests.some((request) => request.includes("/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();
await expect(page.getByText(/audit_projection|cached audit projection|Explain failed/)).toBeVisible({ timeout: 30_000 });
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
});
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-object-detail-live.png`, fullPage: true });
expect(consoleErrors).toEqual([]);
});

View File

@@ -1,13 +1,15 @@
import { expect, test } from "@playwright/test";
const screenshotRoot = "../../../../specs/develop/20260623_2/m6_full_e2e";
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);
const url = new URL(request.url());
if (url.pathname.startsWith("/api/v1")) {
apiRequests.push(`${url.pathname}${url.search}`);
}
});
page.on("console", (message) => {
@@ -18,22 +20,18 @@ test("renders live overview data from query service without global object list",
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.getByRole("heading", { name: "Global RPKI validation health" })).toBeVisible();
await expect(page.getByText(/run_\d+/).first()).toBeVisible();
await expect(page.getByText("Top repositories by workload")).toBeVisible();
await expect(page.locator(".issue-card").first().getByText("Reject")).toBeVisible();
await expect(page.locator(".issue-card").first()).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(apiRequests.some((request) => request.includes("/stats/object-types"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/stats/validation"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/stats/reasons"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/repos?limit=8"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/objects?"))).toBe(false);
expect(consoleErrors).toEqual([]);
await page.screenshot({
path: "../../../../specs/develop/20260617/m3_playwright/rpki-explorer-overview-live.png",
fullPage: true
});
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-overview-live.png`, fullPage: true });
});

View File

@@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
const screenshotRoot = "../../../../specs/develop/20260623_2/m6_full_e2e";
test("loads repository, publication point, and object data on demand", async ({ page }) => {
const apiRequests: string[] = [];
@@ -24,28 +25,71 @@ test("loads repository, publication point, and object data on demand", async ({
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);
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);
const reposSection = page.locator('section[aria-label="Repositories"]');
await expect(reposSection.locator(".stack-row").first()).toBeVisible({ timeout: 30_000 });
await expect(reposSection.getByLabel("Cursor pagination")).toContainText("Page 1");
await expect(reposSection.getByLabel("Cursor pagination")).toContainText(/1-\d+\/.+ shown/);
await reposSection.getByRole("button", { name: "Collapse repositories column" }).click();
await expect(reposSection.locator(".collapsed-panel-summary")).toContainText("Repositories");
await expect(reposSection.getByRole("button", { name: "Expand repositories column" })).toBeVisible();
await reposSection.getByRole("button", { name: "Expand repositories column" }).click();
await reposSection.getByLabel("Filter repositories on current page").fill("sakuya");
await expect(reposSection.locator(".stack-row")).toHaveCount(1);
await reposSection.locator(".stack-row-select").first().click();
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 ppSection = page.locator('section[aria-label="Publication points"]');
await expect(ppSection.locator(".stack-row").first()).toBeVisible({ timeout: 30_000 });
await expect(ppSection.getByLabel("Cursor pagination")).toContainText("Page 1");
await expect(ppSection.getByLabel("Cursor pagination")).toContainText(/1-\d+\/\d+ shown/);
await ppSection.getByRole("button", { name: "Collapse publication points column" }).click();
await expect(ppSection.getByRole("button", { name: "Expand publication points column" })).toBeVisible();
await ppSection.getByRole("button", { name: "Expand publication points column" }).click();
expect(apiRequests.some((request) => request.includes("/publication-points"))).toBe(true);
expect(apiRequests.some((request) => request.includes("/objects?"))).toBe(false);
await ppSection.locator(".stack-row-select").first().click();
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();
await expect(objectsPanel.locator("tbody tr").first()).toBeVisible({ timeout: 70_000 });
expect(apiRequests.some((request) => request.includes("/publication-points/") && request.includes("/objects?limit=50"))).toBe(true);
await expect(objectsPanel.getByRole("button", { name: "Open" }).first()).toBeVisible();
await expect(objectsPanel.getByRole("button", { name: "Copy object URI" }).first()).toBeVisible();
await expect(objectsPanel.getByRole("button", { name: "Copy object SHA256" }).first()).toBeVisible();
await expect(objectsPanel.getByLabel("Filter objects on current page")).toBeVisible();
await expect(objectsPanel.getByLabel("Cursor pagination")).toBeVisible();
await expect(objectsPanel.getByLabel("Cursor pagination")).toContainText("Page 1");
await expect(objectsPanel.getByLabel("Cursor pagination")).toContainText(/1-\d+\/\d+ shown/);
expect(consoleErrors).toEqual([]);
await page.screenshot({
path: "../../../../specs/develop/20260617/m4_playwright/rpki-explorer-repository-browser.png",
fullPage: true
});
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-repository-browser.png`, fullPage: true });
});
test("keeps selected repository and publication point after opening object detail", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: /Repositories/i }).click();
const reposSection = page.locator('section[aria-label="Repositories"]');
await reposSection.getByLabel("Filter repositories on current page").fill("sakuya");
await expect(reposSection.locator(".stack-row")).toHaveCount(1, { timeout: 30_000 });
const selectedRepoUri = (await reposSection.locator(".stack-row-copy .copyable-value-text").first().innerText()).trim();
await reposSection.locator(".stack-row-select").first().click();
const ppSection = page.locator('section[aria-label="Publication points"]');
await expect(ppSection.locator(".stack-row").first()).toBeVisible({ timeout: 30_000 });
const selectedPpUri = (await ppSection.locator(".stack-row-copy .copyable-value-text").first().innerText()).trim();
await ppSection.locator(".stack-row-select").first().click();
const objectsPanel = page.getByRole("region", { name: "Objects for publication point" });
await expect(objectsPanel.locator("tbody tr").first()).toBeVisible({ timeout: 70_000 });
const selectedObjectUri = (await objectsPanel.locator(".uri-cell .copyable-value-text").first().innerText()).trim();
await objectsPanel.getByRole("button", { name: "Open" }).first().click();
await expect(page.locator(".object-detail-card")).toContainText(selectedObjectUri, { timeout: 70_000 });
await page.getByRole("button", { name: /Repositories/i }).click();
await expect(page.getByRole("heading", { name: "Repository / publication point / object browser" })).toBeVisible();
await expect(reposSection.getByLabel("Filter repositories on current page")).toHaveValue("sakuya");
await expect(reposSection.locator(".stack-row.active .stack-row-copy").first()).toContainText(selectedRepoUri);
await expect(ppSection.locator(".stack-row.active .stack-row-copy").first()).toContainText(selectedPpUri);
await expect(objectsPanel.locator("tbody tr").first()).toContainText(selectedObjectUri);
});

View File

@@ -1,6 +1,8 @@
import { expect, test } from "@playwright/test";
test("opens the RPKI Explorer overview prototype", async ({ page }) => {
const screenshotRoot = "../../../../specs/develop/20260623_2/m6_full_e2e";
function collectConsoleErrors(page: import("@playwright/test").Page) {
const consoleErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") {
@@ -10,47 +12,72 @@ test("opens the RPKI Explorer overview prototype", async ({ page }) => {
}
}
});
return consoleErrors;
}
test("opens the RPKI Explorer overview", async ({ page }) => {
const consoleErrors = collectConsoleErrors(page);
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.getByLabel("Current run")).toContainText("latest indexed run");
await expect(page.getByPlaceholder("Exact URI lookup planned in M2")).toBeDisabled();
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
});
await page.screenshot({ path: `${screenshotRoot}/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);
}
}
});
test("collapses sidebar to icon-only navigation on desktop", async ({ page }) => {
const consoleErrors = collectConsoleErrors(page);
await page.goto("/");
await expect(page.getByRole("heading", { name: "RPKI Explorer" })).toBeVisible();
await page.getByRole("button", { name: "Collapse navigation" }).click();
await expect(page.locator(".app-shell")).toHaveClass(/sidebar-collapsed/);
await expect(page.getByRole("button", { name: "Expand navigation" })).toBeVisible();
await expect(page.locator(".brand-copy")).toHaveCSS("position", "absolute");
await expect(page.locator(".nav-item").first().locator("span")).toHaveCSS("position", "absolute");
await expect(page.getByRole("button", { name: "Repositories" })).toBeVisible();
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-sidebar-collapsed.png`, fullPage: true });
await page.getByRole("button", { name: "Repositories" }).click();
await expect(page.getByRole("heading", { name: "Repository / publication point / object browser" })).toBeVisible();
await page.getByRole("button", { name: "Expand navigation" }).click();
await expect(page.locator(".app-shell")).not.toHaveClass(/sidebar-collapsed/);
await expect(page.getByRole("heading", { name: "RPKI Explorer" })).toBeVisible();
expect(consoleErrors).toEqual([]);
});
test("opens truthful placeholders for unfinished navigation", async ({ page }) => {
const consoleErrors = collectConsoleErrors(page);
await page.goto("/");
for (const name of ["Publication Points", "Validation", "Exports", "Runs", "API"]) {
await page.getByRole("button", { name }).click();
await expect(page.locator("#coming-soon-heading")).toHaveText(name);
await expect(page.getByText("Coming soon", { exact: true })).toBeVisible();
await expect(page.getByRole("heading", { name: "Global RPKI validation health" })).toHaveCount(0);
}
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-coming-soon.png`, fullPage: true });
expect(consoleErrors).toEqual([]);
});
test("objects page starts from explicit empty state", async ({ page }) => {
const consoleErrors = collectConsoleErrors(page);
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
});
await expect(page.getByRole("heading", { name: "Object object" })).toBeVisible();
await expect(page.getByText("Search an exact URI or open an object from Repository Browser.")).toBeVisible();
await expect(page.locator("body")).not.toContainText("sample PP");
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-objects-empty.png`, fullPage: true });
expect(consoleErrors).toEqual([]);
});

View File

@@ -1,6 +1,21 @@
import { expect, test } from "@playwright/test";
test.setTimeout(120_000);
const screenshotRoot = "../../../../specs/develop/20260623_2/m6_full_e2e";
async function openFirstRepositoryObject(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByRole("button", { name: /Repositories/i }).click();
const reposSection = page.locator('section[aria-label="Repositories"]');
await reposSection.getByLabel("Filter repositories on current page").fill("sakuya");
await reposSection.locator(".stack-row-select").first().click();
await page.locator('section[aria-label="Publication points"] .stack-row-select').first().click();
const row = page.locator('section[aria-label="Objects for publication point"] tbody tr').first();
await expect(row).toBeVisible({ timeout: 70_000 });
const expectedUri = (await row.locator(".uri-cell .copyable-value-text").innerText()).trim();
await row.getByRole("button", { name: "Open" }).click();
await expect(page.locator(".object-detail-card")).toContainText(expectedUri, { timeout: 70_000 });
}
test("supports URI search and reports raw/export workflow status", async ({ page }) => {
const apiRequests: string[] = [];
@@ -11,9 +26,7 @@ test("supports URI search and reports raw/export workflow status", async ({ page
}
});
await page.goto("/");
await page.getByRole("button", { name: /Objects/i }).click();
await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 });
await openFirstRepositoryObject(page);
await page.getByRole("button", { name: "Use selected URI" }).click();
await page.getByPlaceholder("Exact URI lookup...").press("Enter");
@@ -23,18 +36,17 @@ test("supports URI search and reports raw/export workflow status", async ({ page
).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 expect.poll(
() => apiRequests.some((request) => request.includes("/raw")),
{ timeout: 30_000 }
).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 expect(page.getByText(/Object export job|Object export failed/)).toBeVisible({ timeout: 30_000 });
expect(apiRequests.some((request) => request.includes("POST /api/v1/runs/") && request.includes("/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 expect(page.getByText(/PP export job|PP export failed/)).toBeVisible({ timeout: 30_000 });
await page.screenshot({
path: "../../../../specs/develop/20260617/m6_playwright/rpki-explorer-workflows.png",
fullPage: true
});
await page.screenshot({ path: `${screenshotRoot}/rpki-explorer-workflows.png`, fullPage: true });
});