test(e2e): cover Internal Viewer nav, key, and team-info gating (#29075)
* test(e2e): cover Internal Viewer nav, key, and team-info gating Three previously-uncovered manual-QA paths for the Internal Viewer role: - Nav only renders the read-only sections; admin-only items (Internal Users, Organizations, Models + Endpoints) stay hidden. - Virtual Keys page hides Create New Key, and the key detail view hides Regenerate / Reset Spend / Delete actions. - Team info page hides Members and Settings tabs for the viewer. * test(e2e): scope viewer nav to sidebar, strengthen tab assertions Address review feedback on the Internal Viewer e2e spec: - Scope the nav test to the sidebar complementary landmark and match items by link role + accessible name. The prior CSS nav, aside selector grabbed the top bar (the sidebar is a complementary landmark, not a <nav> tag), so the assertions never hit the real nav links. - Land via navigateToPage so the networkidle wait settles the role-gated nav before asserting. - Assert the Virtual Keys tab is visible (was only commented). - Use toHaveCount(0) for hidden team tabs to match the nav block; tabs are conditionally rendered, not CSS-hidden. - Drop redundant dismissFeedbackPopup calls (navigateToPage already dismisses internally).
This commit is contained in:
parent
a55817cbc6
commit
10bda4456a
@ -0,0 +1,90 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
E2E_TEAM_CRUD_ID,
|
||||
E2E_VIEWER_KEY_ALIAS,
|
||||
INTERNAL_VIEWER_STORAGE_PATH,
|
||||
} from "../../constants";
|
||||
import { Page } from "../../fixtures/pages";
|
||||
import { navigateToPage } from "../../helpers/navigation";
|
||||
|
||||
async function clickTeamId(page: import("@playwright/test").Page, teamId: string) {
|
||||
const cell = page.locator("td").filter({ hasText: teamId }).first();
|
||||
await expect(cell).toBeVisible({ timeout: 10_000 });
|
||||
await cell.click();
|
||||
await expect(page.getByText("Back to Teams")).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
test.describe("Internal Viewer", () => {
|
||||
test.use({ storageState: INTERNAL_VIEWER_STORAGE_PATH });
|
||||
|
||||
test("Nav shows only the allowed options for the Internal Viewer role", async ({ page }) => {
|
||||
// Use navigateToPage so the networkidle wait lets the async role-gated nav
|
||||
// settle before we assert — a bare page.goto races the permission fetch.
|
||||
await navigateToPage(page, Page.ApiKeys);
|
||||
|
||||
// Scope to the sidebar and match items by their link role + accessible
|
||||
// name. The sidebar is a `complementary` landmark (the `navigation` role
|
||||
// is the top bar), and each item renders as a link inside it — far tighter
|
||||
// than a CSS `nav, aside` selector or a getByText on stray text nodes.
|
||||
const nav = page.getByRole("complementary");
|
||||
|
||||
// Items that must be visible per the manual-QA checklist
|
||||
const expectedVisible = [
|
||||
"Virtual Keys",
|
||||
"MCP Servers",
|
||||
"Guardrails",
|
||||
"Usage",
|
||||
"Logs",
|
||||
"Teams",
|
||||
"API Reference",
|
||||
"AI Hub",
|
||||
];
|
||||
for (const label of expectedVisible) {
|
||||
await expect(
|
||||
nav.getByRole("link", { name: label, exact: true }).first(),
|
||||
`expected nav item "${label}" to render for Internal Viewer`,
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
}
|
||||
|
||||
// Items that must NOT be visible (admin-only surface)
|
||||
const expectedHidden = ["Internal Users", "Organizations", "Models + Endpoints"];
|
||||
for (const label of expectedHidden) {
|
||||
await expect(
|
||||
nav.getByRole("link", { name: label, exact: true }),
|
||||
`nav item "${label}" must not render for Internal Viewer`,
|
||||
).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("Virtual Keys page hides Create / Regenerate / Reset / Delete controls", async ({ page }) => {
|
||||
await navigateToPage(page, Page.ApiKeys);
|
||||
|
||||
// Create button is gated on rolesWithWriteAccess (Internal Viewer is not in it)
|
||||
await expect(page.getByRole("button", { name: /Create New Key/i })).toHaveCount(0);
|
||||
|
||||
// Open the viewer's own key info page
|
||||
const keyRow = page.locator("tr", { hasText: E2E_VIEWER_KEY_ALIAS });
|
||||
await expect(keyRow).toBeVisible({ timeout: 10_000 });
|
||||
await keyRow.locator("button").first().click();
|
||||
await expect(page.getByText("Back to Keys")).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// None of the destructive / mutating actions should render
|
||||
await expect(page.getByRole("button", { name: "Regenerate Key" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: /Reset Spend/i })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Delete Key" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Team info page omits Members and Settings tabs for an Internal Viewer", async ({ page }) => {
|
||||
await navigateToPage(page, Page.Teams);
|
||||
|
||||
await clickTeamId(page, E2E_TEAM_CRUD_ID);
|
||||
|
||||
// Overview / Virtual Keys are always visible; Settings + Members are not.
|
||||
// Tabs are conditionally rendered (getTeamInfoVisibleTabs filters the list),
|
||||
// so assert absence from the DOM with toHaveCount(0) to match the nav block.
|
||||
await expect(page.getByRole("tab", { name: "Overview" })).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.getByRole("tab", { name: "Virtual Keys" })).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.getByRole("tab", { name: "Settings" })).toHaveCount(0);
|
||||
await expect(page.getByRole("tab", { name: "Members" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user