Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions e2e/project-admin/project-admin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
// spec: .specs/FR-2209-project-admin-management/spec.md
// Stories 1-4 (admin scope + members page) plus ProjectSelect badge.
// Full validation requires sibling stack PRs (#6652 serving, #6654 vfolder,
// #6655 sessions, and #6656 header switch confirm) to be merged — tests
// that exercise features exclusively on those branches are marked with
// `test.fixme` with an explanatory comment.
Comment on lines +1 to +6
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new E2E spec file is added under e2e/project-admin/, but the repo’s e2e/E2E_COVERAGE_REPORT.md does not appear to include the new Project Admin routes/tests. Please update the coverage report (summary row(s), detailed feature tables, and the "Last Updated" date) to reflect the added coverage and any intentionally skipped (test.fixme) scenarios.

Copilot uses AI. Check for mistakes.
import {
loginAsCreatedAccount,
navigateTo,
webuiEndpoint,
} from '../utils/test-util';
import { test, expect, Page } from '@playwright/test';

/**
* Credentials for the dedicated project-admin test account.
*
* Resolved exclusively from env vars (`E2E_PROJECT_ADMIN_EMAIL` and
* `E2E_PROJECT_ADMIN_PASSWORD`). No hardcoded fallbacks — real test-server
* credentials must not be committed. When either var is missing, every test
* in this file is skipped via `test.skip` with an explanatory message.
*
* The referenced account must be a project admin of the project named
* `E2E_PROJECT_ADMIN_PROJECT` (defaults to `default`) on the test cluster.
*/
const projectAdminEmail = process.env.E2E_PROJECT_ADMIN_EMAIL;
const projectAdminPassword = process.env.E2E_PROJECT_ADMIN_PASSWORD;
const hasProjectAdminCredentials = Boolean(
projectAdminEmail && projectAdminPassword,
);
const SKIP_REASON =
'requires E2E_PROJECT_ADMIN_EMAIL and E2E_PROJECT_ADMIN_PASSWORD env vars';

const DEFAULT_PROJECT_NAME = process.env.E2E_PROJECT_ADMIN_PROJECT || 'default';

async function loginAsProjectAdmin(
page: Page,
request: Parameters<typeof loginAsCreatedAccount>[1],
): Promise<void> {
// `test.skip` at the describe-level guards entry, so by the time this
// runs both values are defined. The non-null assertions keep the type
// narrow without re-introducing any fallback literal.
await loginAsCreatedAccount(
page,
request,
projectAdminEmail!,
projectAdminPassword!,
);
}
Comment on lines +14 to +48
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new project-admin-specific credentials/env vars (E2E_PROJECT_ADMIN_EMAIL/E2E_PROJECT_ADMIN_PASSWORD) but they aren’t centralized alongside the other E2E accounts in e2e/utils/test-util.ts (userInfo) or documented in the E2E setup docs. Consider adding a userInfo.projectAdmin entry and documenting the required env vars so contributors/CI can run these tests consistently.

Copilot uses AI. Check for mistakes.

test.beforeEach(() => {
// Skip all tests in this file if the project-admin credentials are not
// supplied via env vars. We intentionally avoid hardcoded fallbacks so
// real cluster credentials cannot be committed to source control.
test.skip(!hasProjectAdminCredentials, SKIP_REASON);
});

test.describe(
'FR-2209 Project Admin - menu visibility',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('project admin can see the Admin category in the sider', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);

// The project-admin tier surfaces a subset of admin pages
// (Sessions, Serving, Data, Members). Presence of any one of them in
// the sider is sufficient to confirm the admin category is mounted.
await expect(
page.getByRole('menuitem', { name: 'Sessions' }).first(),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Project Members' }),
).toBeVisible();
});
},
);

test.describe(
'FR-2209 Story 1 - admin sessions scope',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('project admin can navigate to Admin Sessions page', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);
await navigateTo(page, '/admin-session');

// Assert the page rendered (breadcrumb or heading). The exact
// per-project filter is enforced on the backend side by PR #6655.
await expect(page).toHaveURL(/\/admin-session/);
await expect(
page.getByTestId('webui-breadcrumb').getByText(/Sessions/i),
).toBeVisible();
});

// Requires sibling PR #6655 (FR-2554): backend + UI scope sessions list
// to the project-admin's project only. Until #6655 merges, the list is
// unscoped and a content-based assertion would be meaningless.
test.fixme('admin sessions list only contains the project-admin project (requires #6655)', async () => {
// Pending: assert every row's project column equals `default`.
});
},
);

test.describe(
'FR-2209 Story 2 - admin serving scope',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('project admin can navigate to Admin Serving page', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);
await navigateTo(page, '/admin-serving');

await expect(page).toHaveURL(/\/admin-serving/);
});

// The "Start Service" action must be hidden for project-admins in
// admin-serving. The button is conditionally rendered by PR #6652.
test.fixme('"Start Service" button is hidden on Admin Serving for project admins (requires #6652)', async () => {
// Pending: assert getByRole('button', { name: 'Start Service' })
// is hidden on /admin-serving for a project-admin session.
});
},
);

test.describe(
'FR-2209 Story 3 - admin vfolder scope',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('project admin can navigate to Admin Data page', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);
await navigateTo(page, '/admin-data');

await expect(page).toHaveURL(/\/admin-data/);
});

// Create Folder modal should lock the type and project fields for
// project admins, and only project-scoped folders should appear in
// the list. That gating lives on PR #6654.
test.fixme('Create Folder modal locks type and project for project admins (requires #6654)', async () => {
// Pending: open Create Folder modal and assert type + project
// fields are disabled and prefilled with the current project.
});
},
);

test.describe(
'FR-2209 Story 4 - project members page',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('project admin sees read-only members table with Name/Email/Role columns', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);
await navigateTo(page, '/admin-members');

await expect(page).toHaveURL(/\/admin-members/);

// Card title / page heading
await expect(
page.getByText('Project Members', { exact: true }).first(),
).toBeVisible();

// Table column headers — BAITable surfaces them as
// `role="columnheader"` on the underlying <th>.
await expect(
page.getByRole('columnheader', { name: 'Name', exact: true }),
).toBeVisible();
await expect(
page.getByRole('columnheader', { name: 'Email', exact: true }),
).toBeVisible();
await expect(
page.getByRole('columnheader', { name: 'Role', exact: true }),
).toBeVisible();

// Read-only view: there should be no Add Member / Remove Member
// controls on the page. Project-admins manage members exclusively
// via the RBAC Management page per the FR-2209 spec.
await expect(
page.getByRole('button', { name: /Add Member/i }),
).toHaveCount(0);
await expect(
page.getByRole('button', { name: /Remove Member/i }),
).toHaveCount(0);
});
},
);

test.describe(
'FR-2209 ProjectSelect - Project Admin badge',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
test('"Project Admin" badge appears next to the project in the ProjectSelect dropdown', async ({
page,
request,
}) => {
await loginAsProjectAdmin(page, request);
await page.goto(webuiEndpoint);

// Open the project selector in the header (rendered as a
// Select-like button with the current project name).
const selectorTrigger = page
.getByRole('combobox')
.filter({ hasText: DEFAULT_PROJECT_NAME })
.first();
await expect(selectorTrigger).toBeVisible();
await selectorTrigger.click();

// Badge is rendered as a Tag inside the option label.
await expect(
page.getByText('Project Admin', { exact: true }).first(),
).toBeVisible();
});
},
);

test.describe(
'FR-2209 PR-1b - admin-mode switch confirm',
{ tag: ['@project-admin', '@rbac', '@functional'] },
() => {
// Header-level switch-out-of-admin confirm dialog is introduced in
// PR #6656 (FR-2553) which is a sibling of this PR's base chain.
// The i18n keys and logic are not present here yet.
test.fixme('selecting a non-admin project in admin mode prompts a confirm modal (requires #6656)', async () => {
// Pending: open header project selector, pick a project where
// the user is NOT admin, assert confirm modal appears with the
// SwitchOutOfAdminConfirmTitle/Content copy. Click Cancel and
// assert the selector restores the previous project.
});
},
);
12 changes: 12 additions & 0 deletions resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,17 @@
"ProjectIDFilterRuleMessage": "Ungültige UUID.",
"ResourcePolicy": "Ressourcenrichtlinie"
},
"projectMembers": {
"Email": "E-Mail",
"Name": "Name",
"PageTitle": "Projektmitglieder",
"Role": "Rolle",
"RoleAdmin": "Administrator",
"RoleMember": "Mitglied"
},
"projectSelect": {
"ProjectAdminBadge": "Projektadministrator"
},
"rbac": {
"Active": "Aktiv",
"Assign": "Zuweisen",
Expand Down Expand Up @@ -2947,6 +2958,7 @@
"PrivacyPolicy": "Datenschutz-Bestimmungen",
"ProfileUpdated": "Das Profil wurde erfolgreich aktualisiert.",
"Project": "Projekt",
"ProjectMembers": "Projektmitglieder",
"Projects": "Projekte",
"RBACManagement": "RBAC-Verwaltung",
"Remaining": "Übrig",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,17 @@
"ProjectIDFilterRuleMessage": "Μη έγκυρο UUID.",
"ResourcePolicy": "Πολιτική πόρων"
},
"projectMembers": {
"Email": "Email",
"Name": "Όνομα",
"PageTitle": "Μέλη Έργου",
"Role": "Ρόλος",
"RoleAdmin": "Διαχειριστής",
"RoleMember": "Μέλος"
},
"projectSelect": {
"ProjectAdminBadge": "Διαχειριστής Έργου"
},
"rbac": {
"Active": "Ενεργός",
"Assign": "Ανάθεση",
Expand Down Expand Up @@ -2945,6 +2956,7 @@
"PrivacyPolicy": "Πολιτική απορρήτου",
"ProfileUpdated": "Το προφίλ ενημερώθηκε με επιτυχία.",
"Project": "Έργο",
"ProjectMembers": "Μέλη Έργου",
"Projects": "Έργα",
"RBACManagement": "Διαχείριση RBAC",
"Remaining": "Παραμένων",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,17 @@
"ProjectIDFilterRuleMessage": "UUID no válido.",
"ResourcePolicy": "Política de recursos"
},
"projectMembers": {
"Email": "Correo electrónico",
"Name": "Nombre",
"PageTitle": "Miembros del proyecto",
"Role": "Rol",
"RoleAdmin": "Administrador",
"RoleMember": "Miembro"
},
"projectSelect": {
"ProjectAdminBadge": "Administrador del proyecto"
},
"rbac": {
"Active": "Activo",
"Assign": "Asignar",
Expand Down Expand Up @@ -2945,6 +2956,7 @@
"PrivacyPolicy": "Política de privacidad",
"ProfileUpdated": "El perfil se ha actualizado correctamente.",
"Project": "Proyecto",
"ProjectMembers": "Miembros del proyecto",
"Projects": "Proyectos",
"RBACManagement": "Gestión RBAC",
"Remaining": "Restante",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,17 @@
"ProjectIDFilterRuleMessage": "Virheellinen UUID.",
"ResourcePolicy": "Resurssipolitiikka"
},
"projectMembers": {
"Email": "Sähköposti",
"Name": "Nimi",
"PageTitle": "Projektin jäsenet",
"Role": "Rooli",
"RoleAdmin": "Ylläpitäjä",
"RoleMember": "Jäsen"
},
"projectSelect": {
"ProjectAdminBadge": "Projektin ylläpitäjä"
},
"rbac": {
"Active": "Aktiivinen",
"Assign": "Määritä",
Expand Down Expand Up @@ -2946,6 +2957,7 @@
"PrivacyPolicy": "Tietosuojakäytäntö",
"ProfileUpdated": "Profiili on päivitetty onnistuneesti.",
"Project": "Hanke",
"ProjectMembers": "Projektin jäsenet",
"Projects": "Hankkeet",
"RBACManagement": "RBAC-hallinta",
"Remaining": "Jäljellä oleva",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,17 @@
"ProjectIDFilterRuleMessage": "UUID invalide.",
"ResourcePolicy": "Politique de ressources"
},
"projectMembers": {
"Email": "E-mail",
"Name": "Nom",
"PageTitle": "Membres du projet",
"Role": "Rôle",
"RoleAdmin": "Administrateur",
"RoleMember": "Membre"
},
"projectSelect": {
"ProjectAdminBadge": "Administrateur de projet"
},
"rbac": {
"Active": "Actif",
"Assign": "Attribuer",
Expand Down Expand Up @@ -2949,6 +2960,7 @@
"PrivacyPolicy": "Politique de confidentialité",
"ProfileUpdated": "Le profil a été mis à jour avec succès.",
"Project": "Projet",
"ProjectMembers": "Membres du projet",
"Projects": "Projets",
"RBACManagement": "Gestion RBAC",
"Remaining": "Restant",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,17 @@
"ProjectIDFilterRuleMessage": "UUID tidak valid.",
"ResourcePolicy": "Kebijakan Sumber Daya"
},
"projectMembers": {
"Email": "Email",
"Name": "Nama",
"PageTitle": "Anggota Proyek",
"Role": "Peran",
"RoleAdmin": "Admin",
"RoleMember": "Anggota"
},
"projectSelect": {
"ProjectAdminBadge": "Admin Proyek"
},
"rbac": {
"Active": "Aktif",
"Assign": "Tetapkan",
Expand Down Expand Up @@ -2949,6 +2960,7 @@
"PrivacyPolicy": "Kebijakan Privasi",
"ProfileUpdated": "Profil berhasil diperbarui.",
"Project": "Proyek",
"ProjectMembers": "Anggota Proyek",
"Projects": "Proyek",
"RBACManagement": "Manajemen RBAC",
"Remaining": "Tersisa",
Expand Down
Loading
Loading