/** * Secret Detection UI E2E Tests. * Sprint: SPRINT_20260104_008_FE (Secret Detection UI) * Task: SDU-012 - E2E tests * * Tests the complete secret detection configuration and viewing flow: * 1. Settings page navigation and toggles * 2. Revelation policy selection * 3. Rule category configuration * 4. Exception management * 5. Alert destination configuration * 6. Findings list and masked value display */ import { test, expect, Page } from '@playwright/test'; test.describe('Secret Detection UI', () => { const settingsUrl = '/tenants/test-tenant/secrets/settings'; const findingsUrl = '/tenants/test-tenant/secrets/findings'; test.describe('Settings Page', () => { test('displays settings page with header and tabs', async ({ page }) => { await page.goto(settingsUrl); // Verify header is visible const header = page.locator('.settings-header'); await expect(header).toBeVisible(); await expect(header).toContainText('Secret Detection'); // Verify enable/disable toggle const toggle = page.locator('mat-slide-toggle'); await expect(toggle).toBeVisible(); // Verify tabs are present const tabs = page.locator('mat-tab-group'); await expect(tabs).toBeVisible(); await expect(page.locator('role=tab')).toHaveCount(3); }); test('toggles secret detection on and off', async ({ page }) => { await page.goto(settingsUrl); const toggle = page.locator('mat-slide-toggle'); const initialState = await toggle.getAttribute('class'); // Toggle the switch await toggle.click(); // Wait for API call and state change await page.waitForResponse(resp => resp.url().includes('/secrets/config/settings') && resp.status() === 200 ); // Verify snackbar confirmation const snackbar = page.locator('mat-snack-bar-container'); await expect(snackbar).toBeVisible(); }); test('navigates between tabs', async ({ page }) => { await page.goto(settingsUrl); // Click Exceptions tab await page.locator('role=tab', { hasText: 'Exceptions' }).click(); await expect(page.locator('app-exception-manager')).toBeVisible(); // Click Alerts tab await page.locator('role=tab', { hasText: 'Alerts' }).click(); await expect(page.locator('app-alert-destination-config')).toBeVisible(); // Back to General tab await page.locator('role=tab', { hasText: 'General' }).click(); await expect(page.locator('app-revelation-policy-selector')).toBeVisible(); }); }); test.describe('Revelation Policy Selector', () => { test('displays all policy options', async ({ page }) => { await page.goto(settingsUrl); // Wait for component to load const selector = page.locator('app-revelation-policy-selector'); await expect(selector).toBeVisible(); // Verify all four options are visible await expect(page.locator('.policy-option')).toHaveCount(4); await expect(page.getByText('Fully Masked')).toBeVisible(); await expect(page.getByText('Partially Revealed')).toBeVisible(); await expect(page.getByText('Full Revelation')).toBeVisible(); await expect(page.getByText('Redacted')).toBeVisible(); }); test('shows preview for selected policy', async ({ page }) => { await page.goto(settingsUrl); // Select partial reveal const partialOption = page.locator('.policy-option', { hasText: 'Partially Revealed' }); await partialOption.click(); // Verify preview is shown const preview = partialOption.locator('.preview code'); await expect(preview).toBeVisible(); await expect(preview).toContainText('****'); }); test('shows additional controls when masked policy selected', async ({ page }) => { await page.goto(settingsUrl); // Select masked policy const maskedOption = page.locator('.policy-option', { hasText: 'Fully Masked' }); await maskedOption.click(); // Verify additional controls appear await expect(maskedOption.locator('.option-controls')).toBeVisible(); await expect(maskedOption.locator('mat-form-field', { hasText: 'Mask Character' })).toBeVisible(); }); test('updates preview when mask length changes', async ({ page }) => { await page.goto(settingsUrl); // Select masked policy const maskedOption = page.locator('.policy-option', { hasText: 'Fully Masked' }); await maskedOption.click(); // Change mask length const lengthSelect = maskedOption.locator('mat-select', { hasText: /characters/ }); await lengthSelect.click(); await page.locator('mat-option', { hasText: '16 characters' }).click(); // Verify preview updated const preview = maskedOption.locator('.preview code'); const text = await preview.textContent(); expect(text?.length).toBe(16); }); }); test.describe('Rule Category Toggles', () => { test('displays rule categories grouped by type', async ({ page }) => { await page.goto(settingsUrl); const toggles = page.locator('app-rule-category-toggles'); await expect(toggles).toBeVisible(); // Verify accordion panels exist const panels = page.locator('mat-expansion-panel'); await expect(panels.first()).toBeVisible(); }); test('shows selection count in toolbar', async ({ page }) => { await page.goto(settingsUrl); const selectionInfo = page.locator('.selection-info'); await expect(selectionInfo).toBeVisible(); await expect(selectionInfo).toContainText(/\d+ of \d+ categories selected/); }); test('toggles entire group when group checkbox clicked', async ({ page }) => { await page.goto(settingsUrl); // Expand first group const panel = page.locator('mat-expansion-panel').first(); await panel.click(); // Get the group checkbox const groupCheckbox = panel.locator('mat-expansion-panel-header mat-checkbox'); // Toggle group await groupCheckbox.click(); // Wait for API call await page.waitForResponse(resp => resp.url().includes('/secrets/config/settings') && resp.status() === 200 ); }); test('select all and clear all buttons work', async ({ page }) => { await page.goto(settingsUrl); // Click select all await page.locator('button', { hasText: 'Select All' }).click(); // Wait for API response await page.waitForResponse(resp => resp.url().includes('/secrets/config/settings') && resp.status() === 200 ); // Click clear all await page.locator('button', { hasText: 'Clear All' }).click(); await page.waitForResponse(resp => resp.url().includes('/secrets/config/settings') && resp.status() === 200 ); }); }); test.describe('Exception Manager', () => { test('displays exception list on Exceptions tab', async ({ page }) => { await page.goto(settingsUrl); // Navigate to exceptions tab await page.locator('role=tab', { hasText: 'Exceptions' }).click(); const manager = page.locator('app-exception-manager'); await expect(manager).toBeVisible(); }); test('opens create dialog when Add Exception clicked', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Exceptions' }).click(); // Click add button await page.locator('button', { hasText: 'Add Exception' }).click(); // Verify dialog opens const dialog = page.locator('.dialog-overlay'); await expect(dialog).toBeVisible(); await expect(page.locator('.dialog-card')).toContainText('Create Exception'); }); test('validates required fields in exception form', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Exceptions' }).click(); await page.locator('button', { hasText: 'Add Exception' }).click(); // Try to submit without required fields const submitButton = page.locator('button[type="submit"]'); await expect(submitButton).toBeDisabled(); // Fill required fields await page.locator('input[formcontrolname="name"]').fill('Test Exception'); await page.locator('input[formcontrolname="pattern"]').fill('test-pattern'); // Button should now be enabled await expect(submitButton).toBeEnabled(); }); test('closes dialog when Cancel clicked', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Exceptions' }).click(); await page.locator('button', { hasText: 'Add Exception' }).click(); // Click cancel await page.locator('button', { hasText: 'Cancel' }).click(); // Dialog should close const dialog = page.locator('.dialog-overlay'); await expect(dialog).not.toBeVisible(); }); }); test.describe('Alert Destination Configuration', () => { test('displays alert configuration on Alerts tab', async ({ page }) => { await page.goto(settingsUrl); // Navigate to alerts tab await page.locator('role=tab', { hasText: 'Alerts' }).click(); const config = page.locator('app-alert-destination-config'); await expect(config).toBeVisible(); await expect(page.getByText('Alert Destinations')).toBeVisible(); }); test('shows global settings when alerts enabled', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Alerts' }).click(); // Enable alerts if not already const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle'); const isChecked = await enableToggle.getAttribute('class'); if (!isChecked?.includes('checked')) { await enableToggle.click(); } // Verify global settings visible await expect(page.locator('.global-settings')).toBeVisible(); await expect(page.getByText('Minimum Severity')).toBeVisible(); await expect(page.getByText('Rate Limit')).toBeVisible(); }); test('adds new destination when Add Destination clicked', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Alerts' }).click(); // Enable alerts const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle'); const isChecked = await enableToggle.getAttribute('class'); if (!isChecked?.includes('checked')) { await enableToggle.click(); } // Count initial destinations const initialCount = await page.locator('mat-expansion-panel').count(); // Add destination await page.locator('button', { hasText: 'Add Destination' }).click(); // Verify new panel added const newCount = await page.locator('mat-expansion-panel').count(); expect(newCount).toBe(initialCount + 1); }); test('shows different config fields based on destination type', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Alerts' }).click(); // Enable alerts and add destination const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle'); const isChecked = await enableToggle.getAttribute('class'); if (!isChecked?.includes('checked')) { await enableToggle.click(); } await page.locator('button', { hasText: 'Add Destination' }).click(); // Expand the new destination const panel = page.locator('mat-expansion-panel').last(); await panel.click(); // Select Slack type await panel.locator('mat-select', { hasText: 'Type' }).click(); await page.locator('mat-option', { hasText: 'Slack' }).click(); // Verify Slack-specific fields await expect(panel.locator('mat-form-field', { hasText: 'Webhook URL' })).toBeVisible(); await expect(panel.locator('mat-form-field', { hasText: 'Channel' })).toBeVisible(); }); test('test button sends test alert', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Alerts' }).click(); // Enable alerts const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle'); const isChecked = await enableToggle.getAttribute('class'); if (!isChecked?.includes('checked')) { await enableToggle.click(); } // Expand first destination (if exists) const panel = page.locator('mat-expansion-panel').first(); if (await panel.isVisible()) { await panel.click(); // Click test button const testButton = panel.locator('button', { hasText: 'Test' }); await testButton.click(); // Wait for API response await page.waitForResponse(resp => resp.url().includes('/alerts/test') && resp.status() === 200 ); // Verify snackbar shows result const snackbar = page.locator('mat-snack-bar-container'); await expect(snackbar).toBeVisible(); } }); }); test.describe('Findings List', () => { test('displays findings list with filters', async ({ page }) => { await page.goto(findingsUrl); // Verify header await expect(page.getByText('Secret Findings')).toBeVisible(); // Verify filters await expect(page.locator('.filters')).toBeVisible(); await expect(page.locator('mat-form-field', { hasText: 'Severity' })).toBeVisible(); await expect(page.locator('mat-form-field', { hasText: 'Status' })).toBeVisible(); }); test('displays table with correct columns', async ({ page }) => { await page.goto(findingsUrl); const table = page.locator('table[mat-table]'); await expect(table).toBeVisible(); // Verify column headers await expect(page.locator('th', { hasText: 'Severity' })).toBeVisible(); await expect(page.locator('th', { hasText: 'Type' })).toBeVisible(); await expect(page.locator('th', { hasText: 'Location' })).toBeVisible(); await expect(page.locator('th', { hasText: 'Value' })).toBeVisible(); await expect(page.locator('th', { hasText: 'Status' })).toBeVisible(); }); test('filters by severity', async ({ page }) => { await page.goto(findingsUrl); // Open severity filter await page.locator('mat-form-field', { hasText: 'Severity' }).click(); // Select Critical await page.locator('mat-option', { hasText: 'Critical' }).click(); // Close dropdown await page.keyboard.press('Escape'); // Wait for filtered results await page.waitForResponse(resp => resp.url().includes('/findings') && resp.url().includes('severity') ); }); test('pagination works correctly', async ({ page }) => { await page.goto(findingsUrl); const paginator = page.locator('mat-paginator'); await expect(paginator).toBeVisible(); // Click next page if available const nextButton = paginator.locator('button[aria-label="Next page"]'); if (await nextButton.isEnabled()) { await nextButton.click(); // Verify page changed await page.waitForResponse(resp => resp.url().includes('/findings') && resp.url().includes('page=2') ); } }); test('clicking row navigates to details', async ({ page }) => { await page.goto(findingsUrl); // Click first row const firstRow = page.locator('tr[mat-row]').first(); if (await firstRow.isVisible()) { await firstRow.click(); // Verify navigation await expect(page).toHaveURL(/\/findings\/.+/); } }); }); test.describe('Masked Value Display', () => { test('displays masked value by default', async ({ page }) => { await page.goto(findingsUrl); const maskedDisplay = page.locator('app-masked-value-display').first(); if (await maskedDisplay.isVisible()) { const code = maskedDisplay.locator('.value-text'); await expect(code).toBeVisible(); await expect(code).toContainText(/\*+/); } }); test('reveal button shows for authorized users', async ({ page }) => { await page.goto(findingsUrl); const maskedDisplay = page.locator('app-masked-value-display').first(); if (await maskedDisplay.isVisible()) { const revealButton = maskedDisplay.locator('button[mattooltip="Reveal secret value"]'); // Button visibility depends on user permissions // Just verify the component structure await expect(maskedDisplay.locator('.value-actions')).toBeVisible(); } }); test('copy button is disabled when value is masked', async ({ page }) => { await page.goto(findingsUrl); const maskedDisplay = page.locator('app-masked-value-display').first(); if (await maskedDisplay.isVisible()) { const copyButton = maskedDisplay.locator('button[mattooltip*="Copy"]'); if (await copyButton.isVisible()) { // Copy should be disabled when not revealed await expect(copyButton).toBeDisabled(); } } }); }); test.describe('Accessibility', () => { test('settings page is keyboard navigable', async ({ page }) => { await page.goto(settingsUrl); // Tab through elements await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); // Verify focus is visible const focusedElement = await page.locator(':focus'); await expect(focusedElement).toBeVisible(); }); test('tabs can be selected with keyboard', async ({ page }) => { await page.goto(settingsUrl); // Focus tab list const tabList = page.locator('mat-tab-group'); await tabList.focus(); // Navigate tabs with arrow keys await page.keyboard.press('ArrowRight'); // Verify tab changed const activeTab = page.locator('[role="tab"][aria-selected="true"]'); await expect(activeTab).toContainText('Exceptions'); }); test('dialogs trap focus', async ({ page }) => { await page.goto(settingsUrl); await page.locator('role=tab', { hasText: 'Exceptions' }).click(); await page.locator('button', { hasText: 'Add Exception' }).click(); // Tab through dialog elements await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); // Focus should stay within dialog const focusedElement = await page.locator(':focus'); const dialogCard = page.locator('.dialog-card'); await expect(dialogCard.locator(':focus')).toBeVisible(); }); }); });