test(e2e): sprint 5 acceptance tests — US-26 → US-28 + dropdown adaptations
Add three new spec files: - us26-templates-crud: API CRUD (AC-26.3–26.7) + UI list/form/delete/redirect (AC-26.8) - us27-instantiate-from-template: template_id copy + name override + 404 + decoupling (AC-27.1–27.3) + no auto-transition/engagement-activate (AC-27.4–27.5) + dropdown UI + picker modal + empty state + SOC gate (AC-27.6–27.7) - us28-templates-nav: Templates link admin+redteam only, SOC redirect, form editable (AC-28.1–28.3) Adapt sprint 2/3 e2e for sprint 5 dropdown: - us4-engagements: getByRole link "New simulation" → getByTestId "new-simulation-btn" - us7-simulation-create: same — split-button dropdown replaced the link Suite: 201 passed (1 pre-existing flaky in us3 re DB state, unrelated to sprint 5). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
136
e2e/tests/us28-templates-nav.spec.ts
Normal file
136
e2e/tests/us28-templates-nav.spec.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* US-28 — Admin/redteam access templates from the nav.
|
||||
* Covers AC-28.1 (Templates link in topbar), AC-28.2 (ProtectedRoute SOC redirect),
|
||||
* AC-28.3 (page is always edit-capable, no read-only mode).
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
adminToken,
|
||||
deleteUserByUsername,
|
||||
ensureUser,
|
||||
login,
|
||||
} from '../fixtures/api';
|
||||
import { seedTokenInStorage } from '../fixtures/auth';
|
||||
|
||||
const REDTEAM_USER = 'us28-redteam';
|
||||
const SOC_USER = 'us28-soc';
|
||||
const PASS = 'us28-pass-strong';
|
||||
|
||||
test.describe('US-28 — templates nav', () => {
|
||||
let redteamToken: string;
|
||||
let socToken: string;
|
||||
let adminTok: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
adminTok = await adminToken();
|
||||
await ensureUser(REDTEAM_USER, PASS, 'redteam');
|
||||
await ensureUser(SOC_USER, PASS, 'soc');
|
||||
redteamToken = (await login(REDTEAM_USER, PASS)).token;
|
||||
socToken = (await login(SOC_USER, PASS)).token;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
try {
|
||||
const tok = await adminToken();
|
||||
for (const u of [REDTEAM_USER, SOC_USER]) await deleteUserByUsername(tok, u);
|
||||
} catch { /* noop */ }
|
||||
});
|
||||
|
||||
// AC-28.1 — Templates nav link visible to admin + redteam
|
||||
test('AC-28.1 — redteam sees "Templates" link in topbar nav', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, redteamToken);
|
||||
await page.goto('/engagements');
|
||||
|
||||
const link = page.getByRole('link', { name: /^templates$/i });
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute('href', '/admin/templates');
|
||||
});
|
||||
|
||||
test('AC-28.1 — admin sees "Templates" link in topbar nav', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, adminTok);
|
||||
await page.goto('/engagements');
|
||||
|
||||
await expect(page.getByRole('link', { name: /^templates$/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('AC-28.1 — SOC does NOT see "Templates" link in topbar nav', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, socToken);
|
||||
await page.goto('/engagements');
|
||||
|
||||
// Wait for page to fully load before asserting absence
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('link', { name: /^templates$/i })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('AC-28.1 — clicking Templates link navigates to /admin/templates', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, redteamToken);
|
||||
await page.goto('/engagements');
|
||||
|
||||
await page.getByRole('link', { name: /^templates$/i }).click();
|
||||
await expect(page).toHaveURL(/\/admin\/templates/);
|
||||
});
|
||||
|
||||
// AC-28.2 — SOC typing /admin/templates URL directly → redirected
|
||||
test('AC-28.2 — SOC direct URL /admin/templates → redirected to /engagements', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, socToken);
|
||||
await page.goto('/admin/templates');
|
||||
|
||||
await expect(page).toHaveURL(/\/engagements/, { timeout: 5_000 });
|
||||
});
|
||||
|
||||
test('AC-28.2 — SOC direct URL /admin/templates/new → redirected to /engagements', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, socToken);
|
||||
await page.goto('/admin/templates/new');
|
||||
|
||||
await expect(page).toHaveURL(/\/engagements/, { timeout: 5_000 });
|
||||
});
|
||||
|
||||
// AC-28.3 — Page assumes canEdit=true (form inputs are never disabled for admin/redteam)
|
||||
test('AC-28.3 — TemplateFormPage for redteam: name input is editable (no read-only mode)', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, redteamToken);
|
||||
await page.goto('/admin/templates/new');
|
||||
|
||||
const nameInput = page.getByLabel(/name/i).first();
|
||||
await expect(nameInput).toBeVisible();
|
||||
await expect(nameInput).toBeEnabled();
|
||||
|
||||
// Should be able to type
|
||||
await nameInput.fill('AC-28.3 editability test');
|
||||
await expect(nameInput).toHaveValue('AC-28.3 editability test');
|
||||
});
|
||||
|
||||
test('AC-28.3 — admin has same edit access on /admin/templates', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await seedTokenInStorage(context, adminTok);
|
||||
await page.goto('/admin/templates');
|
||||
|
||||
// Page loads (not redirected)
|
||||
await expect(page).toHaveURL(/\/admin\/templates/);
|
||||
await expect(page.getByRole('heading', { name: 'Templates', exact: true })).toBeVisible({ timeout: 5_000 });
|
||||
// "New" link in header when templates exist, "New template" in empty state — either is fine
|
||||
await expect(page.getByRole('link', { name: /new( template)?/i }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user