- 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>
396 lines
No EOL
17 KiB
JavaScript
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>');
|
|
});
|
|
});
|
|
}); |