fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
241
admin-v2/e2e/fixtures/sdk-fixtures.ts
Normal file
241
admin-v2/e2e/fixtures/sdk-fixtures.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Playwright Test Fixtures for AI Compliance SDK
|
||||
*
|
||||
* These fixtures provide reusable setup for SDK E2E tests.
|
||||
* Demo data is seeded via the API (same storage path as real data).
|
||||
*/
|
||||
|
||||
import { test as base, expect, Page } from '@playwright/test'
|
||||
|
||||
// Selectors for SDK components
|
||||
export const selectors = {
|
||||
// Dashboard
|
||||
dashboard: '[data-testid="sdk-dashboard"]',
|
||||
loadDemoDataButton: '[data-testid="load-demo-data"]',
|
||||
progressBar: '[data-testid="progress-bar"]',
|
||||
|
||||
// Sidebar
|
||||
sidebar: '[data-testid="sdk-sidebar"]',
|
||||
sidebarPhase1: '[data-testid="sidebar-phase-1"]',
|
||||
sidebarPhase2: '[data-testid="sidebar-phase-2"]',
|
||||
sidebarStep: (stepId: string) => `[data-testid="sidebar-step-${stepId}"]`,
|
||||
|
||||
// Command Bar
|
||||
commandBar: '[data-testid="command-bar"]',
|
||||
commandInput: '[data-testid="command-input"]',
|
||||
commandSuggestion: (index: number) => `[data-testid="command-suggestion-${index}"]`,
|
||||
|
||||
// Navigation
|
||||
prevButton: '[data-testid="prev-step-button"]',
|
||||
nextButton: '[data-testid="next-step-button"]',
|
||||
stepHeader: '[data-testid="step-header"]',
|
||||
|
||||
// Use Case Workshop
|
||||
useCaseCard: (index: number) => `[data-testid="use-case-card-${index}"]`,
|
||||
addUseCaseButton: '[data-testid="add-use-case-button"]',
|
||||
useCaseForm: '[data-testid="use-case-form"]',
|
||||
|
||||
// Risk Matrix
|
||||
riskCard: (riskId: string) => `[data-testid="risk-card-${riskId}"]`,
|
||||
riskMatrix: '[data-testid="risk-matrix"]',
|
||||
addRiskButton: '[data-testid="add-risk-button"]',
|
||||
|
||||
// Controls
|
||||
controlCard: (controlId: string) => `[data-testid="control-card-${controlId}"]`,
|
||||
controlsGrid: '[data-testid="controls-grid"]',
|
||||
|
||||
// Export
|
||||
exportButton: '[data-testid="export-button"]',
|
||||
exportPdfButton: '[data-testid="export-pdf-button"]',
|
||||
exportZipButton: '[data-testid="export-zip-button"]',
|
||||
}
|
||||
|
||||
// Type for SDK test fixtures
|
||||
type SDKFixtures = {
|
||||
sdkPage: Page
|
||||
withDemoData: Page
|
||||
commandBar: Page
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended test with SDK-specific fixtures
|
||||
*/
|
||||
export const test = base.extend<SDKFixtures>({
|
||||
// Basic SDK page (navigates to dashboard)
|
||||
sdkPage: async ({ page }, use) => {
|
||||
await page.goto('/sdk')
|
||||
// Wait for page to be ready
|
||||
await page.waitForLoadState('networkidle')
|
||||
await use(page)
|
||||
},
|
||||
|
||||
// SDK page with demo data loaded
|
||||
withDemoData: async ({ page }, use) => {
|
||||
// Navigate to SDK
|
||||
await page.goto('/sdk')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Seed demo data via API (same storage path as real data)
|
||||
await page.evaluate(async () => {
|
||||
const response = await fetch('/api/sdk/v1/demo/seed', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenantId: 'e2e-test-tenant' }),
|
||||
})
|
||||
if (!response.ok) {
|
||||
// If endpoint doesn't exist, try seeding via localStorage
|
||||
// This is a fallback for development
|
||||
console.warn('Demo seed endpoint not available, using localStorage fallback')
|
||||
}
|
||||
})
|
||||
|
||||
// Reload page to pick up demo data
|
||||
await page.reload()
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
await use(page)
|
||||
|
||||
// Cleanup: Clear demo data after test
|
||||
await page.evaluate(async () => {
|
||||
try {
|
||||
await fetch('/api/sdk/v1/demo/clear', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenantId: 'e2e-test-tenant' }),
|
||||
})
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// SDK page with command bar opened
|
||||
commandBar: async ({ page }, use) => {
|
||||
await page.goto('/sdk')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Open command bar with keyboard shortcut
|
||||
await page.keyboard.press('Meta+k')
|
||||
|
||||
// Wait for command bar to be visible
|
||||
await page.waitForSelector(selectors.commandBar, { state: 'visible' })
|
||||
|
||||
await use(page)
|
||||
},
|
||||
})
|
||||
|
||||
// Re-export expect for convenience
|
||||
export { expect }
|
||||
|
||||
/**
|
||||
* Helper: Wait for SDK to initialize
|
||||
*/
|
||||
export async function waitForSDKInit(page: Page): Promise<void> {
|
||||
await page.waitForLoadState('networkidle')
|
||||
// Wait for React hydration
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('[data-testid="sdk-dashboard"]') !== null ||
|
||||
document.querySelector('[data-testid="step-header"]') !== null
|
||||
}, { timeout: 10000 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Navigate to a specific SDK step
|
||||
*/
|
||||
export async function navigateToStep(page: Page, stepId: string): Promise<void> {
|
||||
const stepUrl = getStepUrl(stepId)
|
||||
await page.goto(stepUrl)
|
||||
await waitForSDKInit(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get URL for a step
|
||||
*/
|
||||
export function getStepUrl(stepId: string): string {
|
||||
const stepUrls: Record<string, string> = {
|
||||
'use-case-workshop': '/sdk/advisory-board',
|
||||
'screening': '/sdk/screening',
|
||||
'modules': '/sdk/modules',
|
||||
'requirements': '/sdk/requirements',
|
||||
'controls': '/sdk/controls',
|
||||
'evidence': '/sdk/evidence',
|
||||
'audit-checklist': '/sdk/audit-checklist',
|
||||
'risks': '/sdk/risks',
|
||||
'ai-act': '/sdk/ai-act',
|
||||
'obligations': '/sdk/obligations',
|
||||
'dsfa': '/sdk/dsfa',
|
||||
'tom': '/sdk/tom',
|
||||
'loeschfristen': '/sdk/loeschfristen',
|
||||
'vvt': '/sdk/vvt',
|
||||
'consent': '/sdk/consent',
|
||||
'cookie-banner': '/sdk/cookie-banner',
|
||||
'einwilligungen': '/sdk/einwilligungen',
|
||||
'dsr': '/sdk/dsr',
|
||||
'escalations': '/sdk/escalations',
|
||||
}
|
||||
return stepUrls[stepId] || '/sdk'
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Use command bar to navigate
|
||||
*/
|
||||
export async function useCommandBarToNavigate(page: Page, searchTerm: string): Promise<void> {
|
||||
// Open command bar
|
||||
await page.keyboard.press('Meta+k')
|
||||
await page.waitForSelector(selectors.commandBar, { state: 'visible' })
|
||||
|
||||
// Type search term
|
||||
await page.fill(selectors.commandInput, searchTerm)
|
||||
|
||||
// Wait for suggestions
|
||||
await page.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Click first suggestion
|
||||
await page.click(selectors.commandSuggestion(0))
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Seed demo data programmatically
|
||||
*/
|
||||
export async function seedDemoData(page: Page, tenantId: string = 'e2e-test'): Promise<boolean> {
|
||||
const result = await page.evaluate(async (tid) => {
|
||||
try {
|
||||
const response = await fetch('/api/sdk/v1/demo/seed', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenantId: tid }),
|
||||
})
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, tenantId)
|
||||
|
||||
if (result) {
|
||||
await page.reload()
|
||||
await waitForSDKInit(page)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Clear demo data
|
||||
*/
|
||||
export async function clearDemoData(page: Page, tenantId: string = 'e2e-test'): Promise<boolean> {
|
||||
return await page.evaluate(async (tid) => {
|
||||
try {
|
||||
const response = await fetch('/api/sdk/v1/demo/clear', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenantId: tid }),
|
||||
})
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, tenantId)
|
||||
}
|
||||
141
admin-v2/e2e/specs/command-bar.spec.ts
Normal file
141
admin-v2/e2e/specs/command-bar.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* E2E Tests: Command Bar
|
||||
*
|
||||
* Tests for the command bar functionality including
|
||||
* keyboard shortcuts, search, navigation, and actions.
|
||||
*/
|
||||
|
||||
import { test, expect, selectors, useCommandBarToNavigate } from '../fixtures/sdk-fixtures'
|
||||
|
||||
test.describe('Command Bar', () => {
|
||||
test('Cmd+K opens command bar', async ({ sdkPage }) => {
|
||||
// Open command bar with keyboard shortcut
|
||||
await sdkPage.keyboard.press('Meta+k')
|
||||
|
||||
// Command bar should be visible
|
||||
const commandBar = sdkPage.locator(selectors.commandBar)
|
||||
await expect(commandBar).toBeVisible()
|
||||
})
|
||||
|
||||
test('Ctrl+K opens command bar (Windows/Linux)', async ({ sdkPage }) => {
|
||||
await sdkPage.keyboard.press('Control+k')
|
||||
|
||||
const commandBar = sdkPage.locator(selectors.commandBar)
|
||||
await expect(commandBar).toBeVisible()
|
||||
})
|
||||
|
||||
test('Escape closes command bar', async ({ commandBar }) => {
|
||||
// Command bar should already be open from fixture
|
||||
await expect(commandBar.locator(selectors.commandBar)).toBeVisible()
|
||||
|
||||
// Press Escape
|
||||
await commandBar.keyboard.press('Escape')
|
||||
|
||||
// Command bar should be closed
|
||||
await expect(commandBar.locator(selectors.commandBar)).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('shows navigation suggestions', async ({ commandBar }) => {
|
||||
// Type a navigation term
|
||||
await commandBar.fill(selectors.commandInput, 'DSFA')
|
||||
|
||||
// Should show relevant suggestions
|
||||
const suggestions = commandBar.locator('[data-testid^="command-suggestion"]')
|
||||
await expect(suggestions.first()).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// First suggestion should be related to DSFA
|
||||
const firstSuggestion = suggestions.first()
|
||||
await expect(firstSuggestion).toContainText(/DSFA|Datenschutz/i)
|
||||
})
|
||||
|
||||
test('navigates to step from command bar', async ({ commandBar }) => {
|
||||
// Type search term
|
||||
await commandBar.fill(selectors.commandInput, 'Risk')
|
||||
|
||||
// Wait for suggestions
|
||||
await commandBar.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Click suggestion
|
||||
await commandBar.click(selectors.commandSuggestion(0))
|
||||
|
||||
// Should navigate to risks page
|
||||
await expect(commandBar).toHaveURL(/risk/i)
|
||||
})
|
||||
|
||||
test('shows action suggestions', async ({ commandBar }) => {
|
||||
// Type an action keyword
|
||||
await commandBar.fill(selectors.commandInput, 'Export')
|
||||
|
||||
// Should show export-related actions
|
||||
const suggestions = commandBar.locator('[data-testid^="command-suggestion"]')
|
||||
await expect(suggestions.first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('keyboard navigation through suggestions', async ({ commandBar }) => {
|
||||
// Type search term
|
||||
await commandBar.fill(selectors.commandInput, 'a')
|
||||
|
||||
// Wait for suggestions
|
||||
await commandBar.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Press down arrow to navigate
|
||||
await commandBar.keyboard.press('ArrowDown')
|
||||
|
||||
// Second suggestion should be focused/highlighted
|
||||
const secondSuggestion = commandBar.locator(selectors.commandSuggestion(1))
|
||||
await expect(secondSuggestion).toHaveAttribute('data-highlighted', 'true')
|
||||
})
|
||||
|
||||
test('Enter selects highlighted suggestion', async ({ commandBar }) => {
|
||||
// Type search term
|
||||
await commandBar.fill(selectors.commandInput, 'TOMs')
|
||||
|
||||
// Wait for suggestions
|
||||
await commandBar.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Press Enter to select first suggestion
|
||||
await commandBar.keyboard.press('Enter')
|
||||
|
||||
// Command bar should close and navigation should happen
|
||||
await expect(commandBar.locator(selectors.commandBar)).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Command Bar with Demo Data', () => {
|
||||
test('shows recent searches', async ({ withDemoData }) => {
|
||||
// Demo data includes recent searches
|
||||
await withDemoData.keyboard.press('Meta+k')
|
||||
|
||||
const commandBar = withDemoData.locator(selectors.commandBar)
|
||||
await expect(commandBar).toBeVisible()
|
||||
|
||||
// Should show recent searches section
|
||||
const recentSection = withDemoData.locator('text=Zuletzt gesucht')
|
||||
// This may or may not be visible depending on implementation
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Command Bar RAG Search', () => {
|
||||
test('searches for DSGVO content', async ({ commandBar }) => {
|
||||
// Type a legal query
|
||||
await commandBar.fill(selectors.commandInput, 'DSGVO Art. 5')
|
||||
|
||||
// Wait for search results
|
||||
await commandBar.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Should show search type suggestions
|
||||
const suggestions = commandBar.locator('[data-testid^="command-suggestion"]')
|
||||
const count = await suggestions.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('searches for AI Act content', async ({ commandBar }) => {
|
||||
await commandBar.fill(selectors.commandInput, 'AI Act Hochrisiko')
|
||||
|
||||
await commandBar.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
const suggestions = commandBar.locator('[data-testid^="command-suggestion"]')
|
||||
const count = await suggestions.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
373
admin-v2/e2e/specs/dsr.spec.ts
Normal file
373
admin-v2/e2e/specs/dsr.spec.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { navigateToSDK, waitForPageLoad, waitForElement, countElements } from '../utils/test-helpers'
|
||||
|
||||
/**
|
||||
* DSR (Data Subject Request) Module E2E Tests
|
||||
* Tests for GDPR Art. 15-21 functionality
|
||||
*/
|
||||
|
||||
test.describe('DSR Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr')
|
||||
})
|
||||
|
||||
test('should load DSR dashboard', async ({ page }) => {
|
||||
// Check page title/header exists (use heading role for specificity)
|
||||
await expect(page.getByRole('heading', { name: 'DSR Portal' })).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('should display tab navigation', async ({ page }) => {
|
||||
// Check all tabs are present - use exact text matching
|
||||
await expect(page.getByRole('button', { name: /Uebersicht/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Eingang \d+/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /In Bearbeitung \d+/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Abgeschlossen \d+/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Einstellungen' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display statistics cards', async ({ page }) => {
|
||||
// Check statistics cards exist - use exact text matching
|
||||
await expect(page.getByText('Gesamt', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText('Neue Anfragen')).toBeVisible()
|
||||
await expect(page.getByText('Ueberfaellig', { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have "Anfrage erfassen" button', async ({ page }) => {
|
||||
const button = page.getByRole('link', { name: 'Anfrage erfassen' })
|
||||
await expect(button).toBeVisible()
|
||||
await expect(button).toHaveAttribute('href', '/sdk/dsr/new')
|
||||
})
|
||||
|
||||
test('should display mock DSR requests', async ({ page }) => {
|
||||
// Wait for requests to load
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check that request cards are displayed (using Link elements)
|
||||
const requestCards = page.locator('a[href^="/sdk/dsr/dsr-"]')
|
||||
const count = await requestCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('should filter by tab - Eingang', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Eingang \d+/ }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Check that the tab is active (has border-purple-600)
|
||||
await expect(page.getByRole('button', { name: /Eingang \d+/ })).toHaveClass(/border-purple-600/)
|
||||
})
|
||||
|
||||
test('should filter by tab - In Bearbeitung', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /In Bearbeitung \d+/ }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByRole('button', { name: /In Bearbeitung \d+/ })).toHaveClass(/border-purple-600/)
|
||||
})
|
||||
|
||||
test('should filter by tab - Abgeschlossen', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Abgeschlossen \d+/ }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByRole('button', { name: /Abgeschlossen \d+/ })).toHaveClass(/border-purple-600/)
|
||||
})
|
||||
|
||||
test('should have filter dropdowns', async ({ page }) => {
|
||||
// Check filter controls exist using select elements
|
||||
await expect(page.locator('select').first()).toBeVisible()
|
||||
// Verify we have filter dropdowns
|
||||
const selects = page.locator('select')
|
||||
const count = await selects.count()
|
||||
expect(count).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
test('should filter by type', async ({ page }) => {
|
||||
// Select "Auskunft" type from the first dropdown
|
||||
await page.locator('select').first().selectOption('access')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Check filter is applied
|
||||
const typeDropdown = page.locator('select').first()
|
||||
await expect(typeDropdown).toHaveValue('access')
|
||||
})
|
||||
|
||||
test('should show info box about deadlines', async ({ page }) => {
|
||||
await expect(page.getByText('Fristen beachten')).toBeVisible()
|
||||
await expect(page.getByText('Art. 12 DSGVO')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate to request details on click', async ({ page }) => {
|
||||
// Click on first request card
|
||||
const firstCard = page.locator('a[href^="/sdk/dsr/dsr-"]').first()
|
||||
await firstCard.click()
|
||||
await waitForPageLoad(page)
|
||||
|
||||
// Check URL changed to detail page
|
||||
await expect(page).toHaveURL(/\/sdk\/dsr\/dsr-/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSR New Request Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr/new')
|
||||
})
|
||||
|
||||
test('should load new request page', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Neue Anfrage erfassen' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display all DSR types (Art. 15-21)', async ({ page }) => {
|
||||
// Check for Art. numbers in buttons
|
||||
await expect(page.getByRole('button', { name: /15.*Auskunft/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /16.*Berichtigung/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /17.*Loeschung/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /18.*Einschraenkung/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /20.*Uebertragung/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /21.*Widerspruch/ })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have requester input fields', async ({ page }) => {
|
||||
// Check for name and email inputs
|
||||
await expect(page.locator('input').first()).toBeVisible()
|
||||
await expect(page.locator('input[type="email"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have source selection buttons', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'Webformular' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'E-Mail' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Brief' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Telefon' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have priority buttons', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'Niedrig' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Normal' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Hoch' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Kritisch' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should select DSR type', async ({ page }) => {
|
||||
// Click on Art. 15 Auskunft
|
||||
await page.getByRole('button', { name: /Auskunft.*15/ }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Check type is selected (should show description)
|
||||
await expect(page.getByText('Auskunftsrecht')).toBeVisible()
|
||||
await expect(page.getByText('30 Tage')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should validate required fields', async ({ page }) => {
|
||||
// Try to submit without filling required fields
|
||||
await page.getByRole('button', { name: 'Anfrage erfassen' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Should show validation errors (at least one)
|
||||
await expect(page.getByText(/Bitte waehlen Sie/).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('should fill form and show no errors', async ({ page }) => {
|
||||
// Select type
|
||||
await page.getByRole('button', { name: /Auskunft.*15/ }).click()
|
||||
|
||||
// Fill requester info
|
||||
await page.locator('input').first().fill('Test Person')
|
||||
await page.locator('input[type="email"]').fill('test@example.com')
|
||||
|
||||
// Select source
|
||||
await page.getByRole('button', { name: 'E-Mail' }).click()
|
||||
|
||||
// Form should be valid now - check that error messages are gone
|
||||
await page.getByRole('button', { name: 'Anfrage erfassen' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Should not show type error anymore
|
||||
const typeError = page.getByText('Bitte waehlen Sie den Anfragetyp')
|
||||
await expect(typeError).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate back to dashboard', async ({ page }) => {
|
||||
// Click back/cancel link (labeled "Abbrechen" with href to /sdk/dsr)
|
||||
await page.getByRole('link', { name: 'Abbrechen' }).click()
|
||||
await waitForPageLoad(page)
|
||||
|
||||
await expect(page).toHaveURL(/\/sdk\/dsr$/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSR Detail Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to first mock request
|
||||
await navigateToSDK(page, '/dsr/dsr-001')
|
||||
})
|
||||
|
||||
test('should load detail page', async ({ page }) => {
|
||||
// Check reference number is displayed
|
||||
await expect(page.getByText('DSR-2025-000001')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display workflow stepper', async ({ page }) => {
|
||||
// Check workflow steps exist - use first() for duplicates
|
||||
await expect(page.getByText('Eingang').first()).toBeVisible()
|
||||
await expect(page.getByText('ID-Pruefung').first()).toBeVisible()
|
||||
await expect(page.getByText('Bearbeitung').first()).toBeVisible()
|
||||
await expect(page.getByText('Abschluss').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display requester information', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Max Mustermann' })).toBeVisible()
|
||||
await expect(page.getByText('max.mustermann@example.de')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have content tabs', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'Details' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Kommunikation' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display identity verification status', async ({ page }) => {
|
||||
// For dsr-001, identity is not verified
|
||||
await expect(page.getByText('Identitaetspruefung').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have sidebar with status info', async ({ page }) => {
|
||||
await expect(page.getByText('Status').first()).toBeVisible()
|
||||
await expect(page.getByText('Prioritaet').first()).toBeVisible()
|
||||
await expect(page.getByText('Zugewiesen an')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have action buttons', async ({ page }) => {
|
||||
await expect(page.getByText('Aktionen').first()).toBeVisible()
|
||||
// For unverified request, should have verify identity button
|
||||
await expect(page.getByRole('button', { name: 'Identitaet verifizieren' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display audit log', async ({ page }) => {
|
||||
await expect(page.getByText('Aktivitaeten')).toBeVisible()
|
||||
await expect(page.getByText('Erstellt').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('should switch to communication tab', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Kommunikation' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Kommunikation' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should have back navigation', async ({ page }) => {
|
||||
// Click back link (icon link in header area, first link to /sdk/dsr in main content)
|
||||
await page.locator('main a[href="/sdk/dsr"]').first().click()
|
||||
await waitForPageLoad(page)
|
||||
|
||||
await expect(page).toHaveURL(/\/sdk\/dsr$/)
|
||||
})
|
||||
|
||||
test('should open identity verification modal', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Identitaet verifizieren' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Modal should open - use level 2 heading to be specific
|
||||
await expect(page.locator('h2:has-text("Identitaet verifizieren")')).toBeVisible()
|
||||
await expect(page.getByText('Verifizierungsmethode', { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSR Identity Modal', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr/dsr-001')
|
||||
await page.getByRole('button', { name: 'Identitaet verifizieren' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
test('should display verification methods', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: /Ausweisdokument/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /E-Mail-Bestaetigung/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Bestehendes Konto/ })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Telefonische Bestaetigung/ })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should select verification method', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Ausweisdokument/ }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Check selection is visible
|
||||
const selectedButton = page.getByRole('button', { name: /Ausweisdokument/ })
|
||||
await expect(selectedButton).toHaveClass(/border-purple-500/)
|
||||
})
|
||||
|
||||
test('should close modal on cancel', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Abbrechen' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Modal should be closed
|
||||
await expect(page.getByText('Verifizierungsmethode')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should close modal on cancel button', async ({ page }) => {
|
||||
// Click on cancel button to close modal
|
||||
await page.getByRole('button', { name: 'Abbrechen' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await expect(page.getByText('Verifizierungsmethode')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should verify identity and update UI', async ({ page }) => {
|
||||
// Select method
|
||||
await page.getByRole('button', { name: /Ausweisdokument/ }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Click verify button
|
||||
await page.getByRole('button', { name: 'Identitaet bestaetigen' }).click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Modal should close and status should update
|
||||
await expect(page.getByText('Verifizierungsmethode')).not.toBeVisible()
|
||||
await expect(page.getByText('Identitaet verifiziert').first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSR Type-Specific Features', () => {
|
||||
test('should show erasure checklist for Art. 17 request', async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr/dsr-002') // Erasure request
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click on type-specific tab
|
||||
const typeTab = page.getByRole('button', { name: 'Loeschung' })
|
||||
if (await typeTab.isVisible()) {
|
||||
await typeTab.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Art\. 17\(3\)/ })).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('should show data export for Art. 15 request', async ({ page }) => {
|
||||
await navigateToSDK(page, '/dsr/dsr-001') // Access request
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click on type-specific tab
|
||||
const typeTab = page.getByRole('button', { name: 'Auskunft' })
|
||||
if (await typeTab.isVisible()) {
|
||||
await typeTab.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Should show export options
|
||||
await expect(page.getByText('Export-Format')).toBeVisible()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSR Responsive Design', () => {
|
||||
test('should be responsive on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 })
|
||||
await navigateToSDK(page, '/dsr')
|
||||
|
||||
// Dashboard should still be usable
|
||||
await expect(page.getByRole('heading', { name: 'DSR Portal' })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: 'Anfrage erfassen' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('should be responsive on tablet', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 })
|
||||
await navigateToSDK(page, '/dsr')
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'DSR Portal' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Uebersicht/ })).toBeVisible()
|
||||
})
|
||||
})
|
||||
153
admin-v2/e2e/specs/export.spec.ts
Normal file
153
admin-v2/e2e/specs/export.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* E2E Tests: Export Functionality
|
||||
*
|
||||
* Tests for PDF and ZIP export features.
|
||||
*/
|
||||
|
||||
import { test, expect, selectors, navigateToStep } from '../fixtures/sdk-fixtures'
|
||||
|
||||
test.describe('Export Functionality', () => {
|
||||
test('export button is visible', async ({ withDemoData }) => {
|
||||
// Navigate to dashboard or any step
|
||||
await navigateToStep(withDemoData, 'use-case-workshop')
|
||||
|
||||
// Look for export button in header or command bar
|
||||
const exportButton = withDemoData.locator(selectors.exportButton)
|
||||
if (await exportButton.isVisible()) {
|
||||
await expect(exportButton).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('can trigger PDF export from command bar', async ({ withDemoData }) => {
|
||||
// Open command bar
|
||||
await withDemoData.keyboard.press('Meta+k')
|
||||
|
||||
// Type export command
|
||||
await withDemoData.fill(selectors.commandInput, 'PDF Export')
|
||||
|
||||
// Wait for export suggestion
|
||||
await withDemoData.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Set up download listener
|
||||
const downloadPromise = withDemoData.waitForEvent('download', { timeout: 30000 })
|
||||
|
||||
// Click export suggestion
|
||||
await withDemoData.click(selectors.commandSuggestion(0))
|
||||
|
||||
// Verify download started
|
||||
try {
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toMatch(/\.pdf$/i)
|
||||
} catch {
|
||||
// If download doesn't happen, that's acceptable for this test
|
||||
// (export might open in new tab or have different behavior)
|
||||
}
|
||||
})
|
||||
|
||||
test('can trigger ZIP export from command bar', async ({ withDemoData }) => {
|
||||
await withDemoData.keyboard.press('Meta+k')
|
||||
|
||||
await withDemoData.fill(selectors.commandInput, 'ZIP Export')
|
||||
|
||||
await withDemoData.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
const downloadPromise = withDemoData.waitForEvent('download', { timeout: 30000 })
|
||||
|
||||
await withDemoData.click(selectors.commandSuggestion(0))
|
||||
|
||||
try {
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toMatch(/\.zip$/i)
|
||||
} catch {
|
||||
// Acceptable if download behavior differs
|
||||
}
|
||||
})
|
||||
|
||||
test('can export JSON data', async ({ withDemoData }) => {
|
||||
await withDemoData.keyboard.press('Meta+k')
|
||||
|
||||
await withDemoData.fill(selectors.commandInput, 'JSON Export')
|
||||
|
||||
await withDemoData.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
const downloadPromise = withDemoData.waitForEvent('download', { timeout: 30000 })
|
||||
|
||||
await withDemoData.click(selectors.commandSuggestion(0))
|
||||
|
||||
try {
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toMatch(/\.json$/i)
|
||||
} catch {
|
||||
// Acceptable
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Export Content Verification', () => {
|
||||
test('PDF export contains expected content', async ({ withDemoData }) => {
|
||||
// This test verifies the export process completes without errors
|
||||
// Actual PDF content verification would require additional tools
|
||||
|
||||
await withDemoData.keyboard.press('Meta+k')
|
||||
await withDemoData.fill(selectors.commandInput, 'PDF')
|
||||
await withDemoData.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
|
||||
// Just verify the action is available
|
||||
const suggestion = withDemoData.locator(selectors.commandSuggestion(0))
|
||||
await expect(suggestion).toContainText(/PDF|Export/i)
|
||||
})
|
||||
|
||||
test('export with no data shows appropriate message', async ({ sdkPage }) => {
|
||||
// Without demo data, export might show warning or be disabled
|
||||
await navigateToStep(sdkPage, 'use-case-workshop')
|
||||
|
||||
// Open command bar
|
||||
await sdkPage.keyboard.press('Meta+k')
|
||||
await sdkPage.fill(selectors.commandInput, 'Export')
|
||||
|
||||
// Export should still be available, but might show "no data" message
|
||||
await sdkPage.waitForSelector(selectors.commandSuggestion(0), { timeout: 5000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Export Button Direct Access', () => {
|
||||
test('clicking export button opens export options', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'vvt')
|
||||
|
||||
const exportButton = withDemoData.locator(selectors.exportButton)
|
||||
if (await exportButton.isVisible()) {
|
||||
await exportButton.click()
|
||||
|
||||
// Should show export options dropdown or modal
|
||||
const pdfOption = withDemoData.locator(selectors.exportPdfButton)
|
||||
const zipOption = withDemoData.locator(selectors.exportZipButton)
|
||||
|
||||
const pdfVisible = await pdfOption.isVisible({ timeout: 2000 })
|
||||
const zipVisible = await zipOption.isVisible({ timeout: 2000 })
|
||||
|
||||
expect(pdfVisible || zipVisible).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Export API Integration', () => {
|
||||
test('API export endpoint is accessible', async ({ withDemoData }) => {
|
||||
// Test direct API call
|
||||
const response = await withDemoData.evaluate(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/export?format=json', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
return { status: res.status, ok: res.ok }
|
||||
} catch (error) {
|
||||
return { status: 0, ok: false, error: String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
// API should respond (status might vary based on implementation)
|
||||
expect(response.status).toBeGreaterThanOrEqual(200)
|
||||
})
|
||||
})
|
||||
132
admin-v2/e2e/specs/sdk-navigation.spec.ts
Normal file
132
admin-v2/e2e/specs/sdk-navigation.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* E2E Tests: SDK Navigation
|
||||
*
|
||||
* Tests for navigating through the SDK workflow,
|
||||
* sidebar functionality, and step transitions.
|
||||
*/
|
||||
|
||||
import { test, expect, selectors, navigateToStep, getStepUrl } from '../fixtures/sdk-fixtures'
|
||||
|
||||
test.describe('SDK Navigation', () => {
|
||||
test('should display SDK dashboard', async ({ sdkPage }) => {
|
||||
await expect(sdkPage).toHaveURL(/\/sdk/)
|
||||
})
|
||||
|
||||
test('should navigate to Use Case Workshop', async ({ sdkPage }) => {
|
||||
await sdkPage.click('text=Use Case Workshop')
|
||||
await expect(sdkPage).toHaveURL('/sdk/advisory-board')
|
||||
})
|
||||
|
||||
test('should navigate through all Phase 1 steps', async ({ sdkPage }) => {
|
||||
const phase1Steps = [
|
||||
{ id: 'use-case-workshop', url: '/sdk/advisory-board', name: 'Use Case Workshop' },
|
||||
{ id: 'screening', url: '/sdk/screening', name: 'System Screening' },
|
||||
{ id: 'modules', url: '/sdk/modules', name: 'Compliance Modules' },
|
||||
{ id: 'requirements', url: '/sdk/requirements', name: 'Requirements' },
|
||||
{ id: 'controls', url: '/sdk/controls', name: 'Controls' },
|
||||
{ id: 'evidence', url: '/sdk/evidence', name: 'Evidence' },
|
||||
{ id: 'audit-checklist', url: '/sdk/audit-checklist', name: 'Audit Checklist' },
|
||||
{ id: 'risks', url: '/sdk/risks', name: 'Risk Matrix' },
|
||||
]
|
||||
|
||||
for (const step of phase1Steps) {
|
||||
await navigateToStep(sdkPage, step.id)
|
||||
await expect(sdkPage).toHaveURL(step.url)
|
||||
// Verify page loaded (no error state)
|
||||
await expect(sdkPage.locator('body')).not.toContainText('Error')
|
||||
}
|
||||
})
|
||||
|
||||
test('should navigate through all Phase 2 steps', async ({ sdkPage }) => {
|
||||
const phase2Steps = [
|
||||
{ id: 'ai-act', url: '/sdk/ai-act', name: 'AI Act Klassifizierung' },
|
||||
{ id: 'obligations', url: '/sdk/obligations', name: 'Pflichtenübersicht' },
|
||||
{ id: 'dsfa', url: '/sdk/dsfa', name: 'DSFA' },
|
||||
{ id: 'tom', url: '/sdk/tom', name: 'TOMs' },
|
||||
{ id: 'loeschfristen', url: '/sdk/loeschfristen', name: 'Löschfristen' },
|
||||
{ id: 'vvt', url: '/sdk/vvt', name: 'Verarbeitungsverzeichnis' },
|
||||
{ id: 'consent', url: '/sdk/consent', name: 'Rechtliche Vorlagen' },
|
||||
{ id: 'cookie-banner', url: '/sdk/cookie-banner', name: 'Cookie Banner' },
|
||||
{ id: 'einwilligungen', url: '/sdk/einwilligungen', name: 'Einwilligungen' },
|
||||
{ id: 'dsr', url: '/sdk/dsr', name: 'DSR Portal' },
|
||||
{ id: 'escalations', url: '/sdk/escalations', name: 'Escalations' },
|
||||
]
|
||||
|
||||
for (const step of phase2Steps) {
|
||||
await navigateToStep(sdkPage, step.id)
|
||||
await expect(sdkPage).toHaveURL(step.url)
|
||||
await expect(sdkPage.locator('body')).not.toContainText('Error')
|
||||
}
|
||||
})
|
||||
|
||||
test('should use next/previous buttons for navigation', async ({ sdkPage }) => {
|
||||
// Start at Use Case Workshop
|
||||
await navigateToStep(sdkPage, 'use-case-workshop')
|
||||
|
||||
// Check that prev button is disabled on first step
|
||||
const prevButton = sdkPage.locator(selectors.prevButton)
|
||||
if (await prevButton.isVisible()) {
|
||||
await expect(prevButton).toBeDisabled()
|
||||
}
|
||||
|
||||
// Click next button
|
||||
const nextButton = sdkPage.locator(selectors.nextButton)
|
||||
if (await nextButton.isVisible()) {
|
||||
await nextButton.click()
|
||||
await expect(sdkPage).toHaveURL('/sdk/screening')
|
||||
}
|
||||
})
|
||||
|
||||
test('sidebar should highlight current step', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'requirements')
|
||||
|
||||
// The current step in sidebar should have active styling
|
||||
const sidebarStep = sdkPage.locator(selectors.sidebarStep('requirements'))
|
||||
if (await sidebarStep.isVisible()) {
|
||||
await expect(sidebarStep).toHaveAttribute('aria-current', 'page')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('SDK Navigation with Demo Data', () => {
|
||||
test('should display progress bar with demo data', async ({ withDemoData }) => {
|
||||
const progressBar = withDemoData.locator(selectors.progressBar)
|
||||
if (await progressBar.isVisible()) {
|
||||
// Demo data has completed steps, so progress should be > 0
|
||||
const progressValue = await progressBar.getAttribute('aria-valuenow')
|
||||
expect(parseInt(progressValue || '0')).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
test('should show completed steps in sidebar', async ({ withDemoData }) => {
|
||||
// With demo data, some steps should be marked as completed
|
||||
const sidebar = withDemoData.locator(selectors.sidebar)
|
||||
if (await sidebar.isVisible()) {
|
||||
// Look for completed step indicators
|
||||
const completedSteps = sidebar.locator('[data-completed="true"]')
|
||||
const count = await completedSteps.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Deep Linking', () => {
|
||||
test('should load correct step from direct URL', async ({ page }) => {
|
||||
// Navigate directly to a specific step
|
||||
await page.goto('/sdk/risks')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Page should display Risk Matrix content
|
||||
await expect(page).toHaveURL('/sdk/risks')
|
||||
})
|
||||
|
||||
test('should handle invalid step URL gracefully', async ({ page }) => {
|
||||
// Navigate to non-existent step
|
||||
await page.goto('/sdk/non-existent-step')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Should redirect to dashboard or show 404
|
||||
// Verify no crash
|
||||
await expect(page.locator('body')).not.toContainText('Unhandled')
|
||||
})
|
||||
})
|
||||
209
admin-v2/e2e/specs/sdk-workflow.spec.ts
Normal file
209
admin-v2/e2e/specs/sdk-workflow.spec.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* E2E Tests: SDK Workflow
|
||||
*
|
||||
* Tests for completing the full compliance workflow,
|
||||
* including checkpoints and state persistence.
|
||||
*/
|
||||
|
||||
import { test, expect, selectors, navigateToStep, waitForSDKInit } from '../fixtures/sdk-fixtures'
|
||||
|
||||
test.describe('Use Case Workshop', () => {
|
||||
test('can create a new use case', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'use-case-workshop')
|
||||
|
||||
// Click add use case button
|
||||
const addButton = sdkPage.locator(selectors.addUseCaseButton)
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click()
|
||||
|
||||
// Fill in use case form
|
||||
const form = sdkPage.locator(selectors.useCaseForm)
|
||||
if (await form.isVisible()) {
|
||||
await sdkPage.fill('[name="name"]', 'E2E Test Use Case')
|
||||
await sdkPage.fill('[name="description"]', 'This is a test use case created by E2E tests')
|
||||
|
||||
// Submit form
|
||||
await sdkPage.click('button[type="submit"]')
|
||||
|
||||
// Verify use case was created
|
||||
await expect(sdkPage.locator('text=E2E Test Use Case')).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('displays use cases with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'use-case-workshop')
|
||||
|
||||
// With demo data, should show existing use cases
|
||||
const useCaseCards = withDemoData.locator('[data-testid^="use-case-card"]')
|
||||
const count = await useCaseCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Risk Matrix', () => {
|
||||
test('displays risk matrix', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'risks')
|
||||
|
||||
// Page should load without errors
|
||||
await expect(sdkPage).toHaveURL('/sdk/risks')
|
||||
})
|
||||
|
||||
test('shows risks with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'risks')
|
||||
|
||||
// With demo data, should show risk cards
|
||||
const riskCards = withDemoData.locator('[data-testid^="risk-card"]')
|
||||
const count = await riskCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('can filter risks by severity', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'risks')
|
||||
|
||||
// Look for severity filter
|
||||
const filterSelect = withDemoData.locator('[data-testid="severity-filter"]')
|
||||
if (await filterSelect.isVisible()) {
|
||||
await filterSelect.selectOption('CRITICAL')
|
||||
|
||||
// Verify filtered results
|
||||
const riskCards = withDemoData.locator('[data-testid^="risk-card"]')
|
||||
const visibleCards = riskCards.filter({ hasText: 'CRITICAL' })
|
||||
const count = await visibleCards.count()
|
||||
expect(count).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Controls', () => {
|
||||
test('displays controls grid', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'controls')
|
||||
|
||||
await expect(sdkPage).toHaveURL('/sdk/controls')
|
||||
})
|
||||
|
||||
test('shows controls with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'controls')
|
||||
|
||||
const controlCards = withDemoData.locator('[data-testid^="control-card"]')
|
||||
const count = await controlCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('can view control details', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'controls')
|
||||
|
||||
// Click on first control card
|
||||
const firstControl = withDemoData.locator('[data-testid^="control-card"]').first()
|
||||
if (await firstControl.isVisible()) {
|
||||
await firstControl.click()
|
||||
|
||||
// Should show control details (modal or expanded view)
|
||||
const detailView = withDemoData.locator('[data-testid="control-details"]')
|
||||
if (await detailView.isVisible({ timeout: 2000 })) {
|
||||
await expect(detailView).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DSFA', () => {
|
||||
test('displays DSFA page', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'dsfa')
|
||||
|
||||
await expect(sdkPage).toHaveURL('/sdk/dsfa')
|
||||
})
|
||||
|
||||
test('shows DSFA content with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'dsfa')
|
||||
|
||||
// Demo data includes a DSFA with sections
|
||||
const dsfaSections = withDemoData.locator('[data-testid^="dsfa-section"]')
|
||||
const count = await dsfaSections.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('TOMs', () => {
|
||||
test('displays TOMs page', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'tom')
|
||||
|
||||
await expect(sdkPage).toHaveURL('/sdk/tom')
|
||||
})
|
||||
|
||||
test('shows TOMs with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'tom')
|
||||
|
||||
const tomCards = withDemoData.locator('[data-testid^="tom-card"]')
|
||||
const count = await tomCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('VVT (Verarbeitungsverzeichnis)', () => {
|
||||
test('displays VVT page', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'vvt')
|
||||
|
||||
await expect(sdkPage).toHaveURL('/sdk/vvt')
|
||||
})
|
||||
|
||||
test('shows processing activities with demo data', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'vvt')
|
||||
|
||||
const activityCards = withDemoData.locator('[data-testid^="processing-activity"]')
|
||||
const count = await activityCards.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Checkpoint Validation', () => {
|
||||
test('validates use case checkpoint', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'use-case-workshop')
|
||||
|
||||
// Look for checkpoint validation button
|
||||
const validateButton = withDemoData.locator('[data-testid="validate-checkpoint"]')
|
||||
if (await validateButton.isVisible()) {
|
||||
await validateButton.click()
|
||||
|
||||
// With demo data, checkpoint should pass
|
||||
const successIndicator = withDemoData.locator('[data-testid="checkpoint-passed"]')
|
||||
await expect(successIndicator).toBeVisible({ timeout: 5000 })
|
||||
}
|
||||
})
|
||||
|
||||
test('shows checkpoint errors when requirements not met', async ({ sdkPage }) => {
|
||||
await navigateToStep(sdkPage, 'use-case-workshop')
|
||||
|
||||
// Without demo data, checkpoint should fail
|
||||
const validateButton = sdkPage.locator('[data-testid="validate-checkpoint"]')
|
||||
if (await validateButton.isVisible()) {
|
||||
await validateButton.click()
|
||||
|
||||
// Should show error
|
||||
const errorIndicator = sdkPage.locator('[data-testid="checkpoint-failed"]')
|
||||
if (await errorIndicator.isVisible({ timeout: 5000 })) {
|
||||
await expect(errorIndicator).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('State Persistence', () => {
|
||||
test('persists state after page reload', async ({ withDemoData }) => {
|
||||
await navigateToStep(withDemoData, 'use-case-workshop')
|
||||
|
||||
// Get current use case count
|
||||
const useCaseCards = withDemoData.locator('[data-testid^="use-case-card"]')
|
||||
const initialCount = await useCaseCards.count()
|
||||
|
||||
// Reload page
|
||||
await withDemoData.reload()
|
||||
await waitForSDKInit(withDemoData)
|
||||
|
||||
// Count should be same after reload
|
||||
const afterReloadCards = withDemoData.locator('[data-testid^="use-case-card"]')
|
||||
const afterReloadCount = await afterReloadCards.count()
|
||||
|
||||
expect(afterReloadCount).toBe(initialCount)
|
||||
})
|
||||
})
|
||||
160
admin-v2/e2e/utils/test-helpers.ts
Normal file
160
admin-v2/e2e/utils/test-helpers.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* E2E Test Helpers for SDK Testing
|
||||
*/
|
||||
|
||||
// Wait for page to be fully loaded
|
||||
export async function waitForPageLoad(page: Page) {
|
||||
await page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
// Handle role selection screen if present
|
||||
async function handleRoleSelection(page: Page) {
|
||||
// Check if role selection screen is visible
|
||||
const roleScreen = page.locator('text=Willkommen im Admin Center')
|
||||
if (await roleScreen.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
// Click on DSB (Datenschutzbeauftragter) role for DSR testing
|
||||
const dsbButton = page.locator('button:has-text("DSB")')
|
||||
if (await dsbButton.isVisible()) {
|
||||
await dsbButton.click()
|
||||
await page.waitForTimeout(500)
|
||||
await waitForPageLoad(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to SDK page and wait for load
|
||||
export async function navigateToSDK(page: Page, path: string) {
|
||||
const targetUrl = `/sdk${path}`
|
||||
|
||||
// First navigate to the page
|
||||
await page.goto(targetUrl)
|
||||
await waitForPageLoad(page)
|
||||
|
||||
// Handle role selection if it appears
|
||||
await handleRoleSelection(page)
|
||||
|
||||
// Always navigate again after role selection to ensure we reach the target
|
||||
const currentUrl = page.url()
|
||||
if (!currentUrl.includes('/sdk/dsr')) {
|
||||
await page.goto(targetUrl)
|
||||
await waitForPageLoad(page)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if element is visible
|
||||
export async function isVisible(page: Page, selector: string): Promise<boolean> {
|
||||
const element = page.locator(selector)
|
||||
return await element.isVisible()
|
||||
}
|
||||
|
||||
// Click and wait for navigation
|
||||
export async function clickAndWait(page: Page, selector: string) {
|
||||
await page.click(selector)
|
||||
await waitForPageLoad(page)
|
||||
}
|
||||
|
||||
// Fill form field
|
||||
export async function fillField(page: Page, selector: string, value: string) {
|
||||
await page.fill(selector, value)
|
||||
}
|
||||
|
||||
// Select dropdown option
|
||||
export async function selectOption(page: Page, selector: string, value: string) {
|
||||
await page.selectOption(selector, value)
|
||||
}
|
||||
|
||||
// Get text content of element
|
||||
export async function getText(page: Page, selector: string): Promise<string> {
|
||||
const element = page.locator(selector)
|
||||
return await element.textContent() || ''
|
||||
}
|
||||
|
||||
// Count elements
|
||||
export async function countElements(page: Page, selector: string): Promise<number> {
|
||||
const elements = page.locator(selector)
|
||||
return await elements.count()
|
||||
}
|
||||
|
||||
// Wait for element to appear
|
||||
export async function waitForElement(page: Page, selector: string, timeout = 10000) {
|
||||
await page.waitForSelector(selector, { timeout })
|
||||
}
|
||||
|
||||
// Check page title contains text
|
||||
export async function checkTitleContains(page: Page, text: string) {
|
||||
await expect(page).toHaveTitle(new RegExp(text, 'i'))
|
||||
}
|
||||
|
||||
// Check URL contains path
|
||||
export async function checkUrlContains(page: Page, path: string) {
|
||||
await expect(page).toHaveURL(new RegExp(path))
|
||||
}
|
||||
|
||||
// Take screenshot with timestamp
|
||||
export async function takeScreenshot(page: Page, name: string) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
await page.screenshot({ path: `e2e/screenshots/${name}-${timestamp}.png` })
|
||||
}
|
||||
|
||||
// Check for console errors
|
||||
export function setupConsoleErrorListener(page: Page): string[] {
|
||||
const errors: string[] = []
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text())
|
||||
}
|
||||
})
|
||||
return errors
|
||||
}
|
||||
|
||||
// Wait for API response
|
||||
export async function waitForAPI(page: Page, urlPattern: string | RegExp) {
|
||||
await page.waitForResponse(response =>
|
||||
response.url().match(urlPattern) !== null && response.status() === 200
|
||||
)
|
||||
}
|
||||
|
||||
// SDK specific helpers
|
||||
export const SDK_PAGES = {
|
||||
// Phase 1
|
||||
useCaseWorkshop: '/advisory-board',
|
||||
screening: '/screening',
|
||||
modules: '/modules',
|
||||
requirements: '/requirements',
|
||||
controls: '/controls',
|
||||
evidence: '/evidence',
|
||||
auditChecklist: '/audit-checklist',
|
||||
risks: '/risks',
|
||||
// Phase 2
|
||||
aiAct: '/ai-act',
|
||||
obligations: '/obligations',
|
||||
dsfa: '/dsfa',
|
||||
tom: '/tom',
|
||||
einwilligungen: '/einwilligungen',
|
||||
loeschfristen: '/loeschfristen',
|
||||
vvt: '/vvt',
|
||||
consent: '/consent',
|
||||
cookieBanner: '/cookie-banner',
|
||||
dsr: '/dsr',
|
||||
escalations: '/escalations',
|
||||
}
|
||||
|
||||
// DSR specific helpers
|
||||
export const DSR_TYPES = {
|
||||
access: 'Art. 15',
|
||||
rectification: 'Art. 16',
|
||||
erasure: 'Art. 17',
|
||||
restriction: 'Art. 18',
|
||||
portability: 'Art. 20',
|
||||
objection: 'Art. 21',
|
||||
}
|
||||
|
||||
export const DSR_STATUSES = {
|
||||
intake: 'Eingang',
|
||||
identity_verification: 'ID-Pruefung',
|
||||
processing: 'In Bearbeitung',
|
||||
completed: 'Abgeschlossen',
|
||||
rejected: 'Abgelehnt',
|
||||
}
|
||||
Reference in New Issue
Block a user