/** * US-17 — UI polish: dedup buttons + alignment + icons. * Covers AC-17.1 (single New button on EngagementsListPage) * and AC-17.3 (UsersAdminPage Create form alignment). */ import { test, expect } from '@playwright/test'; import { adminToken, createEngagement, deleteEngagement, deleteUserByUsername, ensureUser, login, } from '../fixtures/api'; import { seedTokenInStorage } from '../fixtures/auth'; const REDTEAM_USER = 'us17-redteam'; const PASS = 'us17-pass-strong'; test.describe('US-17 — UI polish', () => { let redteamToken: string; let adminTok: string; let engagementId: number; test.beforeAll(async () => { adminTok = await adminToken(); await ensureUser(REDTEAM_USER, PASS, 'redteam'); redteamToken = (await login(REDTEAM_USER, PASS)).token; // Seed one engagement so the list is non-empty (EmptyState won't show extra "New" link) const eng = await createEngagement(redteamToken, { name: 'US-17 engagement', start_date: '2026-01-01', }); engagementId = eng.id; }); test.afterAll(async () => { try { const tok = await adminToken(); await deleteEngagement(tok, engagementId); await deleteUserByUsername(tok, REDTEAM_USER); } catch { /* noop */ } }); test('AC-17.1 — EngagementsListPage has exactly one "New" CTA button', async ({ page, context, }) => { await seedTokenInStorage(context, redteamToken); await page.goto('/engagements'); // Wait for list to load so EmptyState doesn't briefly appear await page.waitForLoadState('networkidle'); // Sprint 4: single "New" button (+ icon). Old "Create engagement" duplicate removed. const newButtons = page.getByRole('link', { name: /new/i }); // Should have at least one await expect(newButtons.first()).toBeVisible(); // Count all buttons/links that say "new engagement" or "create engagement" const newEngagementLinks = await page.getByRole('link', { name: /new/i }).count(); const createEngagementLinks = await page.getByRole('link', { name: /create engagement/i }).count(); const createButtons = await page.getByRole('button', { name: /create engagement/i }).count(); // Exactly one "New" CTA — zero "Create engagement" duplicates expect(newEngagementLinks).toBe(1); expect(createEngagementLinks).toBe(0); expect(createButtons).toBe(0); }); test('AC-17.1 — "New" button navigates to engagement creation form', async ({ page, context, }) => { await seedTokenInStorage(context, redteamToken); await page.goto('/engagements'); await page.getByRole('link', { name: /new/i }).first().click(); await expect(page).toHaveURL(/\/engagements\/new/); }); test('AC-17.3 — UsersAdminPage Create account form: inputs and button aligned', async ({ page, context, }) => { // UsersAdminPage is admin-only await seedTokenInStorage(context, adminTok); await page.goto('/admin/users'); // The form should be visible. Sprint 4: inputs use id="new-username" / id="new-password" const usernameInput = page.locator('#new-username').first(); const passwordInput = page.locator('#new-password').first(); const createBtn = page.getByRole('button', { name: /^create$/i }).first(); await expect(usernameInput).toBeVisible(); await expect(passwordInput).toBeVisible(); await expect(createBtn).toBeVisible(); // Alignment check via boundingBox: the 4-column grid layout puts username, password, // role, and button in the same row. All elements must be on the same vertical plane // (same y/height) and within the viewport. const usernameBox = await usernameInput.boundingBox(); const passwordBox = await passwordInput.boundingBox(); const btnBox = await createBtn.boundingBox(); expect(usernameBox).toBeTruthy(); expect(passwordBox).toBeTruthy(); expect(btnBox).toBeTruthy(); // All inputs are in separate columns — their y-positions (vertical alignment) should // be within one element-height of each other (same grid row). const yDiff = Math.abs(usernameBox!.y - passwordBox!.y); expect(yDiff).toBeLessThanOrEqual(usernameBox!.height + 4); // All elements should be visible within the viewport (not overflowing off-screen) const viewportWidth = page.viewportSize()!.width; expect(usernameBox!.x + usernameBox!.width).toBeLessThanOrEqual(viewportWidth + 4); expect(passwordBox!.x + passwordBox!.width).toBeLessThanOrEqual(viewportWidth + 4); expect(btnBox!.x + btnBox!.width).toBeLessThanOrEqual(viewportWidth + 4); // Username comes before password horizontally (left-to-right grid order) expect(usernameBox!.x).toBeLessThan(passwordBox!.x); // Button is positioned after the inputs (rightmost in the grid) expect(btnBox!.x).toBeGreaterThan(passwordBox!.x); }); });