upskill-event-manager/tests/test-rich-text-editor.js
ben 90193ea18c security: implement Phase 1 critical vulnerability fixes
- Add XSS protection with DOMPurify sanitization in rich text editor
- Implement comprehensive file upload security validation
- Enhance server-side content sanitization with wp_kses
- Add comprehensive security test suite with 194+ test cases
- Create security remediation plan documentation

Security fixes address:
- CRITICAL: XSS vulnerability in event description editor
- HIGH: File upload security bypass for malicious files
- HIGH: Enhanced CSRF protection verification
- MEDIUM: Input validation and error handling improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 18:53:23 -03:00

396 lines
No EOL
17 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { HVACTestBase } = require('./page-objects/HVACTestBase');
/**
* Rich Text Editor Comprehensive Test Suite
*
* Tests the contentEditable-based rich text editor including:
* - Toolbar functionality and commands
* - Content synchronization between editor and textarea
* - XSS prevention and content sanitization
* - Keyboard shortcuts and accessibility
* - Character limits and validation
* - Deprecated API usage handling
*/
test.describe('Rich Text Editor Functionality', () => {
let hvacTest;
test.beforeEach(async ({ page }) => {
hvacTest = new HVACTestBase(page);
await hvacTest.loginAsTrainer();
await hvacTest.navigateToCreateEvent();
// Wait for rich text editor to be ready
await expect(page.locator('#event-description-editor')).toBeVisible();
await expect(page.locator('#event-description-toolbar')).toBeVisible();
});
test.describe('Basic Editor Functionality', () => {
test('should initialize rich text editor correctly', async ({ page }) => {
// Verify editor elements are present
await expect(page.locator('#event-description-editor')).toBeVisible();
await expect(page.locator('#event-description-toolbar')).toBeVisible();
await expect(page.locator('#event_description')).toBeVisible();
// Verify editor is contentEditable
const isEditable = await page.locator('#event-description-editor').getAttribute('contenteditable');
expect(isEditable).toBe('true');
// Verify initial state
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent.trim()).toBe('');
});
test('should allow text input and maintain focus', async ({ page }) => {
const testText = 'This is a test of the rich text editor.';
await page.click('#event-description-editor');
await page.keyboard.type(testText);
const editorContent = await page.locator('#event-description-editor').textContent();
expect(editorContent).toBe(testText);
// Verify focus is maintained
const focused = await page.evaluate(() =>
document.activeElement.id === 'event-description-editor'
);
expect(focused).toBe(true);
});
test('should synchronize content between editor and hidden textarea', async ({ page }) => {
const testContent = '<p>Test paragraph with <strong>bold text</strong>.</p>';
// Set content in editor
await page.click('#event-description-editor');
await page.locator('#event-description-editor').fill(testContent);
// Trigger content sync (blur event)
await page.click('body');
// Verify content is synchronized to hidden textarea
const textareaValue = await page.locator('#event_description').inputValue();
expect(textareaValue).toContain('Test paragraph');
expect(textareaValue).toContain('<strong>bold text</strong>');
});
test('should restore content from hidden textarea on page reload', async ({ page }) => {
const testContent = '<p>Persistent content test</p>';
// Set initial content
await page.locator('#event_description').fill(testContent);
// Reload page to test content restoration
await page.reload();
await hvacTest.navigateToCreateEvent();
// Verify content is restored in editor
const restoredContent = await page.locator('#event-description-editor').innerHTML();
expect(restoredContent).toContain('Persistent content test');
});
});
test.describe('Toolbar Commands', () => {
test('should execute bold command correctly', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Bold test');
// Select the text
await page.keyboard.press('Control+a');
// Click bold button
await page.click('[data-command="bold"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<b>Bold test</b>');
});
test('should execute italic command correctly', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Italic test');
await page.keyboard.press('Control+a');
await page.click('[data-command="italic"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<i>Italic test</i>');
});
test('should execute underline command correctly', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Underline test');
await page.keyboard.press('Control+a');
await page.click('[data-command="underline"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<u>Underline test</u>');
});
test('should create ordered lists', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('List item 1');
await page.click('[data-command="insertOrderedList"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<ol>');
expect(editorContent).toContain('<li>List item 1</li>');
});
test('should create unordered lists', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Bullet item 1');
await page.click('[data-command="insertUnorderedList"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<ul>');
expect(editorContent).toContain('<li>Bullet item 1</li>');
});
test('should create links with prompt', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Link text');
await page.keyboard.press('Control+a');
// Mock the prompt for link URL
await page.evaluate(() => {
window.prompt = () => 'https://example.com';
});
await page.click('[data-command="createLink"]');
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('<a href="https://example.com">Link text</a>');
});
test('should toggle commands on and off', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Toggle test');
await page.keyboard.press('Control+a');
// Apply bold
await page.click('[data-command="bold"]');
let content = await page.locator('#event-description-editor').innerHTML();
expect(content).toContain('<b>Toggle test</b>');
// Remove bold
await page.click('[data-command="bold"]');
content = await page.locator('#event-description-editor').innerHTML();
expect(content).not.toContain('<b>Toggle test</b>');
});
});
test.describe('Content Validation and Limits', () => {
test('should enforce character limits if configured', async ({ page }) => {
// Check if character limit is set
const hasCharLimit = await page.locator('.char-counter').isVisible().catch(() => false);
if (hasCharLimit) {
const longText = 'A'.repeat(5000); // Very long text
await page.click('#event-description-editor');
await page.keyboard.type(longText);
// Should show character limit warning
await expect(page.locator('.char-limit-warning')).toBeVisible();
// Should prevent further input
const finalContent = await page.locator('#event-description-editor').textContent();
expect(finalContent.length).toBeLessThan(longText.length);
}
});
test('should sanitize pasted content', async ({ page }) => {
const maliciousPasteContent = `
<div>Normal text</div>
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<iframe src="javascript:alert('XSS')"></iframe>
`;
await page.click('#event-description-editor');
// Simulate paste event
await page.evaluate((content) => {
const editor = document.getElementById('event-description-editor');
const event = new ClipboardEvent('paste', {
clipboardData: new DataTransfer()
});
event.clipboardData.setData('text/html', content);
editor.dispatchEvent(event);
}, maliciousPasteContent);
const editorContent = await page.locator('#event-description-editor').innerHTML();
// Should contain safe content
expect(editorContent).toContain('Normal text');
// Should not contain dangerous elements
expect(editorContent).not.toContain('<script>');
expect(editorContent).not.toContain('onerror=');
expect(editorContent).not.toContain('<iframe');
});
test('should handle empty content gracefully', async ({ page }) => {
// Clear any existing content
await page.click('#event-description-editor');
await page.keyboard.press('Control+a');
await page.keyboard.press('Delete');
// Try to submit with empty content
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent.trim()).toBe('');
// Should handle empty state without errors
await page.click('body');
const textareaValue = await page.locator('#event_description').inputValue();
expect(textareaValue).toBe('');
});
});
test.describe('Accessibility Features', () => {
test('should support keyboard shortcuts', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Keyboard shortcut test');
await page.keyboard.press('Control+a');
// Test Ctrl+B for bold
await page.keyboard.press('Control+b');
const boldContent = await page.locator('#event-description-editor').innerHTML();
expect(boldContent).toContain('<b>');
// Test Ctrl+I for italic
await page.keyboard.press('Control+i');
const italicContent = await page.locator('#event-description-editor').innerHTML();
expect(italicContent).toContain('<i>');
});
test('should have proper ARIA labels on toolbar buttons', async ({ page }) => {
const toolbarButtons = await page.locator('#event-description-toolbar button').all();
for (const button of toolbarButtons) {
const ariaLabel = await button.getAttribute('aria-label');
const title = await button.getAttribute('title');
// Should have either aria-label or title for accessibility
expect(ariaLabel || title).toBeTruthy();
}
});
test('should maintain focus management', async ({ page }) => {
// Focus should move to editor when toolbar button is clicked
await page.click('[data-command="bold"]');
const focused = await page.evaluate(() =>
document.activeElement.id === 'event-description-editor'
);
expect(focused).toBe(true);
});
test('should support screen reader navigation', async ({ page }) => {
// Verify editor has proper role and labels
const editorRole = await page.locator('#event-description-editor').getAttribute('role');
const editorAriaLabel = await page.locator('#event-description-editor').getAttribute('aria-label');
expect(editorRole).toBe('textbox');
expect(editorAriaLabel).toBeTruthy();
});
});
test.describe('Error Handling', () => {
test('should handle execCommand failures gracefully', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Test content');
// Disable execCommand to simulate browser incompatibility
await page.evaluate(() => {
document.execCommand = () => false;
});
// Commands should not throw errors even if execCommand fails
await page.click('[data-command="bold"]');
await page.click('[data-command="italic"]');
// Editor should remain functional
const editorContent = await page.locator('#event-description-editor').textContent();
expect(editorContent).toBe('Test content');
});
test('should recover from DOM manipulation errors', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Recovery test');
// Corrupt editor DOM structure
await page.evaluate(() => {
const editor = document.getElementById('event-description-editor');
editor.innerHTML = '<div><span>Broken <b>structure</div>';
});
// Toolbar commands should still work
await page.click('[data-command="bold"]');
// Content should be preserved
const finalContent = await page.locator('#event-description-editor').textContent();
expect(finalContent).toContain('Broken');
});
test('should handle rapid command execution', async ({ page }) => {
await page.click('#event-description-editor');
await page.keyboard.type('Rapid test');
await page.keyboard.press('Control+a');
// Execute multiple commands rapidly
await Promise.all([
page.click('[data-command="bold"]'),
page.click('[data-command="italic"]'),
page.click('[data-command="underline"]')
]);
// Should handle concurrent operations without errors
const editorContent = await page.locator('#event-description-editor').innerHTML();
expect(editorContent).toContain('Rapid test');
});
});
test.describe('Browser Compatibility', () => {
test('should work with different content models', async ({ page }) => {
// Test different ways content might be structured
const contentVariations = [
'<p>Paragraph content</p>',
'<div>Div content</div>',
'Plain text content',
'<span>Span content</span>'
];
for (const content of contentVariations) {
await page.locator('#event-description-editor').fill('');
await page.locator('#event-description-editor').fill(content);
// Apply formatting
await page.keyboard.press('Control+a');
await page.click('[data-command="bold"]');
// Should maintain content integrity
const result = await page.locator('#event-description-editor').innerHTML();
expect(result).toContain('content');
}
});
test('should handle deprecated execCommand warnings', async ({ page }) => {
const consoleWarnings = [];
page.on('console', msg => {
if (msg.type() === 'warning' && msg.text().includes('execCommand')) {
consoleWarnings.push(msg.text());
}
});
await page.click('#event-description-editor');
await page.keyboard.type('Deprecation test');
await page.keyboard.press('Control+a');
await page.click('[data-command="bold"]');
// Should function despite deprecation warnings
const content = await page.locator('#event-description-editor').innerHTML();
expect(content).toContain('<b>Deprecation test</b>');
});
});
});