Files
Charon/tests/modal-dropdown-triage.spec.ts
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

398 lines
14 KiB
TypeScript
Executable File

import { test, expect } from '@playwright/test'
/**
* Modal Dropdown Z-Index Triage Tests
* Verifies that all 7 critical modal components properly expose their dropdowns
* and interact correctly with the 3-layer modal architecture.
*
* Test Reference: /docs/issues/created/20260204-modal_dropdown_handoff_contract.md
*/
test.describe('Modal Dropdown Z-Index Triage', () => {
// Common setup
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:8080'
// Helper to check if a dropdown can be opened
async function testDropdownInteraction(page: any, labelText: RegExp, stepName: string) {
const select = page.getByLabel(labelText).first()
const selectVisible = await select.isVisible({ timeout: 3000 }).catch(() => false)
if (!selectVisible) {
console.log(`${stepName}: Select element not visible for label`)
return { opened: false, selectedValue: null }
}
try {
await expect(select).toBeEnabled()
await select.click()
const firstOption = select.locator('option').first()
const optionVisible = await firstOption.isVisible({ timeout: 2000 }).catch(() => false)
if (optionVisible) {
const selectedValue = await select.locator('option[selected]').first().textContent().catch(() => 'unknown')
console.log(`${stepName}: Dropdown OPENED and has options. Selected: "${selectedValue}"`)
return { opened: true, selectedValue }
} else {
console.log(`⚠️ ${stepName}: Click registered but dropdown may not have opened`)
return { opened: false, selectedValue: null }
}
} catch (e) {
console.log(`${stepName}: Click failed - ${(e as Error).message}`)
return { opened: false, selectedValue: null }
}
}
test('A. ProxyHostForm - ACL Dropdown', async ({ page }) => {
let proxyHostModalVisible = false
await test.step('Navigate to Proxy Hosts page', async () => {
await page.goto(`${baseURL}/proxy-hosts`)
await page.waitForLoadState('networkidle')
})
await test.step('Click "Add Proxy Host" button', async () => {
const addButton = page
.getByRole('button', { name: /^add proxy host$/i })
.or(page.getByRole('button', { name: /add.*proxy.*host|create.*proxy.*host|new.*proxy.*host/i }))
.first()
if (!await addButton.isVisible().catch(() => false)) {
console.log('⚠️ ProxyHostForm: Add Proxy Host button not found in this environment')
return
}
await addButton.click()
proxyHostModalVisible = await page.getByRole('dialog').first().isVisible({ timeout: 3000 }).catch(() => false)
})
await test.step('Verify 3-layer modal structure', async () => {
if (!proxyHostModalVisible) {
console.log('⚠️ ProxyHostForm: Add Proxy Host did not open a dialog in this environment')
return
}
const modalContent = page.getByRole('dialog').first()
await expect(modalContent).toBeVisible()
console.log('✅ Modal dialog detected for ProxyHostForm')
})
await test.step('Test ACL dropdown', async () => {
if (!proxyHostModalVisible) {
return
}
const result = await testDropdownInteraction(page, /access list|acl|access control/i, 'ACL Dropdown')
if (!result.opened) {
console.log('⚠️ ProxyHostForm: ACL dropdown may have z-index issue')
}
})
await test.step('Test Security Headers dropdown', async () => {
if (!proxyHostModalVisible) {
return
}
const result = await testDropdownInteraction(page, /security headers/i, 'Security Headers Dropdown')
if (!result.opened) {
console.log('⚠️ ProxyHostForm: Security Headers dropdown may have z-index issue')
}
})
await test.step('Close modal', async () => {
if (!proxyHostModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('B. UsersPage - InviteUserModal Role Dropdown', async ({ page }) => {
let inviteModalVisible = false
await test.step('Navigate to Users page', async () => {
await page.goto(`${baseURL}/users`)
await page.waitForLoadState('domcontentloaded')
await page.waitForURL(/users|login/i)
})
await test.step('Click "Invite User" button', async () => {
const inviteButton = page
.getByRole('button', { name: /invite user|send invite|invite|add user|create user/i })
.first()
if (!await inviteButton.isVisible().catch(() => false)) {
console.log('⚠️ UsersPage: Invite User button not found in this environment')
return
}
await inviteButton.click()
inviteModalVisible = await page.getByRole('dialog').first().isVisible({ timeout: 3000 }).catch(() => false)
if (!inviteModalVisible) {
console.log('⚠️ UsersPage: Invite User button did not open a dialog in this environment')
}
})
await test.step('Verify modal is displayed', async () => {
if (!inviteModalVisible) {
return
}
const modal = page.locator('[role="dialog"]')
await expect(modal).toBeVisible()
})
await test.step('Test Role dropdown', async () => {
if (!inviteModalVisible) {
return
}
const result = await testDropdownInteraction(page, /role/i, 'Role Dropdown')
if (!result.opened) {
console.log('⚠️ UsersPage: Role dropdown may have z-index issue')
}
})
await test.step('Test Permission Mode dropdown', async () => {
if (!inviteModalVisible) {
return
}
// There should be a second select for permissions
const allSelects = page.locator('select')
const selectCount = await allSelects.count()
if (selectCount > 1) {
const result = await testDropdownInteraction(page, /permission|mode/i, 'Permission Dropdown')
if (!result.opened) {
console.log('⚠️ UsersPage: Permission dropdown may have z-index issue')
}
}
})
await test.step('Close modal', async () => {
if (!inviteModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('C. UsersPage - EditPermissionsModal Dropdowns', async ({ page }) => {
let editPermissionsModalVisible = false
await test.step('Navigate to Users page', async () => {
await page.goto(`${baseURL}/users`)
await page.waitForLoadState('domcontentloaded')
await page.waitForURL(/users|login/i)
})
await test.step('Find and click Edit Permissions for first user', async () => {
const editButtons = page.getByRole('button', { name: /edit|permissions|manage/i })
if (await editButtons.first().isVisible()) {
await editButtons.first().click()
await expect(page.getByRole('dialog').first()).toBeVisible({ timeout: 3000 })
editPermissionsModalVisible = true
} else {
console.log('⚠️ No users found or edit button not visible')
return
}
})
await test.step('Test permission dropdowns', async () => {
if (!editPermissionsModalVisible) {
return
}
const allSelects = page.locator('select')
const selectCount = await allSelects.count()
if (selectCount === 0) {
console.log('⚠️ No dropdowns found in EditPermissionsModal')
return
}
console.log(`Found ${selectCount} select elements in EditPermissionsModal`)
for (let i = 0; i < selectCount && i < 3; i++) {
const result = await testDropdownInteraction(page, /role|permission|access/i, `EditPermissions Dropdown ${i + 1}`)
}
})
await test.step('Close modal', async () => {
if (!editPermissionsModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('D. Uptime - CreateMonitorModal Type Dropdown', async ({ page }) => {
let createMonitorModalVisible = false
await test.step('Navigate to Uptime page', async () => {
await page.goto(`${baseURL}/uptime`)
await page.waitForLoadState('networkidle')
})
await test.step('Click "Create Monitor" button', async () => {
const createButton = page.getByRole('button', { name: /create monitor|add monitor|new monitor/i }).first()
if (await createButton.isVisible()) {
await createButton.click()
} else {
const plusButton = page.locator('button').filter({ hasText: '+' })
if (await plusButton.isVisible()) {
await plusButton.click()
} else {
console.log('⚠️ Create Monitor button not found')
return
}
}
createMonitorModalVisible = await page.getByRole('dialog').first().isVisible({ timeout: 3000 }).catch(() => false)
if (!createMonitorModalVisible) {
console.log('⚠️ Uptime: Create Monitor button did not open a dialog in this environment')
}
})
await test.step('Test Monitor Type dropdown', async () => {
if (!createMonitorModalVisible) {
return
}
const result = await testDropdownInteraction(page, /monitor type|type|protocol/i, 'Monitor Type Dropdown')
if (!result.opened) {
console.log('⚠️ Uptime: Monitor Type dropdown may have z-index issue')
}
})
await test.step('Close modal', async () => {
if (!createMonitorModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('D. RemoteServerForm - Provider Dropdown', async ({ page }) => {
let addServerModalVisible = false
await test.step('Navigate to Remote Servers page', async () => {
await page.goto(`${baseURL}/remote-servers`)
await page.waitForLoadState('domcontentloaded')
await page.waitForURL(/remote-servers|login/i)
})
await test.step('Click "Add Server" button', async () => {
const addButton = page.getByRole('button', { name: /^add server$/i }).first()
if (await addButton.isVisible()) {
await addButton.click()
addServerModalVisible = await page.getByRole('dialog').first().isVisible({ timeout: 3000 }).catch(() => false)
if (!addServerModalVisible) {
console.log('⚠️ RemoteServerForm: Add Server did not open a dialog in this environment')
}
} else {
console.log('⚠️ Add Server button not found')
return
}
})
await test.step('Test Provider dropdown', async () => {
if (!addServerModalVisible) {
return
}
const result = await testDropdownInteraction(page, /provider|type|docker|generic/i, 'Provider Dropdown')
if (!result.opened) {
console.log('⚠️ RemoteServerForm: Provider dropdown may have z-index issue')
}
})
await test.step('Close modal', async () => {
if (!addServerModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('E. CrowdSecConfig - BanIPModal Duration Dropdown', async ({ page }) => {
let banIpModalVisible = false
await test.step('Navigate to CrowdSec page', async () => {
await page.goto(`${baseURL}/security/crowdsec`)
await page.waitForLoadState('networkidle')
})
await test.step('Find and click Ban IP button', async () => {
const banButton = page.getByTestId('ban-ip-trigger').first()
if (await banButton.isVisible()) {
const enabled = await banButton.isEnabled().catch(() => false)
if (!enabled) {
console.log('⚠️ CrowdSecConfig: Ban IP trigger is disabled in this environment')
return
}
await banButton.click()
banIpModalVisible = await page.getByRole('dialog').first().isVisible({ timeout: 3000 }).catch(() => false)
if (!banIpModalVisible) {
console.log('⚠️ CrowdSecConfig: Ban IP trigger did not open a dialog in this environment')
}
} else {
console.log('⚠️ Ban IP button not found on CrowdSec page')
return
}
})
await test.step('Test Duration dropdown', async () => {
if (!banIpModalVisible) {
return
}
const result = await testDropdownInteraction(page, /duration|time|ban.*duration/i, 'Duration Dropdown')
if (!result.opened) {
console.log('⚠️ CrowdSecConfig: Duration dropdown may have z-index issue')
}
})
await test.step('Close modal', async () => {
if (!banIpModalVisible) {
return
}
await page.keyboard.press('Escape')
await expect(page.getByRole('dialog').first()).toBeHidden({ timeout: 3000 })
})
})
test('Accessibility Verification - Modal 3-Layer Architecture', async ({ page }) => {
await test.step('Verify 3-layer modal structure in ProxyHostForm', async () => {
await page.goto(`${baseURL}/proxy-hosts`)
await page.waitForLoadState('networkidle')
const addButton = page.getByRole('button', { name: /^add proxy host$/i }).first()
if (await addButton.isVisible()) {
await addButton.click()
await expect(page.getByRole('dialog').first()).toBeVisible({ timeout: 3000 })
}
// Check for 3-layer structure in DOM
const layer1 = page.locator('.fixed.inset-0.bg-black').first()
const layer2 = page.locator('.fixed.inset-0.pointer-events-none').first()
const layer3 = page.locator('[role="dialog"]').first()
const hasLayer1 = await layer1.isVisible({ timeout: 2000 }).catch(() => false)
const hasLayer2 = await layer2.isVisible({ timeout: 2000 }).catch(() => false)
const hasLayer3 = await layer3.isVisible({ timeout: 2000 }).catch(() => false)
console.log(`
3-Layer Modal Structure Check:
- Layer 1 (Overlay z-40): ${hasLayer1 ? '✅' : '❌'}
- Layer 2 (Container z-50): ${hasLayer2 ? '✅' : '❌'}
- Layer 3 (Content Dialog): ${hasLayer3 ? '✅' : '❌'}
`)
if (hasLayer1 && hasLayer2 && hasLayer3) {
console.log('✅ 3-Layer modal architecture VERIFIED in ProxyHostForm')
} else {
console.log('❌ 3-Layer modal architecture INCOMPLETE in ProxyHostForm')
}
await page.keyboard.press('Escape')
})
})
})