Spaces:
Sleeping
Sleeping
| /** | |
| * Property-Based Tests for ui-feedback.js | |
| * | |
| * Feature: frontend-cleanup, Property 4: Error toast display | |
| * Validates: Requirements 3.4, 4.4, 7.3 | |
| * | |
| * Property 4: Error toast display | |
| * For any failed API call, the UIFeedback.fetchJSON function should display | |
| * an error toast with the error message | |
| */ | |
| import fc from 'fast-check'; | |
| import { JSDOM } from 'jsdom'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| // Load ui-feedback.js content | |
| const uiFeedbackPath = path.join(__dirname, '..', 'static', 'js', 'ui-feedback.js'); | |
| const uiFeedbackCode = fs.readFileSync(uiFeedbackPath, 'utf-8'); | |
| // Helper to create a fresh DOM environment for each test | |
| async function createTestEnvironment() { | |
| const html = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head></head> | |
| <body> | |
| <script>${uiFeedbackCode}</script> | |
| </body> | |
| </html> | |
| `; | |
| const dom = new JSDOM(html, { | |
| url: 'http://localhost', | |
| runScripts: 'dangerously', | |
| resources: 'usable' | |
| }); | |
| const { window } = dom; | |
| const { document } = window; | |
| // Wait for scripts to execute and DOMContentLoaded to fire | |
| await new Promise(resolve => { | |
| if (document.readyState === 'complete') { | |
| resolve(); | |
| } else { | |
| window.addEventListener('load', resolve); | |
| } | |
| }); | |
| // Give a bit more time for the toast stack to be appended | |
| await new Promise(resolve => setTimeout(resolve, 50)); | |
| return { window, document }; | |
| } | |
| // Mock fetch to simulate API failures | |
| function createMockFetch(shouldFail, statusCode, errorMessage) { | |
| return async (url, options) => { | |
| if (shouldFail) { | |
| if (statusCode) { | |
| // HTTP error response | |
| return { | |
| ok: false, | |
| status: statusCode, | |
| statusText: errorMessage || 'Error', | |
| text: async () => errorMessage || 'Request failed', | |
| json: async () => { throw new Error('Invalid JSON'); } | |
| }; | |
| } else { | |
| // Network error | |
| throw new Error(errorMessage || 'Network error'); | |
| } | |
| } | |
| // Success case | |
| return { | |
| ok: true, | |
| status: 200, | |
| json: async () => ({ data: 'success' }) | |
| }; | |
| }; | |
| } | |
| console.log('Running Property-Based Tests for ui-feedback.js...\n'); | |
| async function runTests() { | |
| console.log('Property 4.1: fetchJSON should display error toast on HTTP errors'); | |
| // Test that HTTP errors (4xx, 5xx) trigger error toasts | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.integer({ min: 400, max: 599 }), // HTTP error status codes | |
| fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Non-empty error message | |
| async (statusCode, errorMessage) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Mock fetch to return HTTP error | |
| window.fetch = createMockFetch(true, statusCode, errorMessage); | |
| // Track toast creation | |
| let toastCreated = false; | |
| let toastType = null; | |
| let toastContent = null; | |
| // Check if UIFeedback is defined | |
| if (!window.UIFeedback) { | |
| throw new Error('UIFeedback not defined on window'); | |
| } | |
| // Override toast creation to capture calls | |
| const originalToast = window.UIFeedback.toast; | |
| window.UIFeedback.toast = (type, title, message) => { | |
| toastCreated = true; | |
| toastType = type; | |
| toastContent = { title, message }; | |
| // Still create the actual toast | |
| originalToast(type, title, message); | |
| }; | |
| // Call fetchJSON and expect it to throw | |
| let errorThrown = false; | |
| try { | |
| await window.UIFeedback.fetchJSON('/api/test', {}, 'Test Context'); | |
| } catch (err) { | |
| errorThrown = true; | |
| } | |
| // Verify error toast was created | |
| if (!toastCreated) { | |
| throw new Error(`No toast created for HTTP ${statusCode} error`); | |
| } | |
| if (toastType !== 'error') { | |
| throw new Error(`Expected error toast, got ${toastType}`); | |
| } | |
| if (!errorThrown) { | |
| throw new Error('fetchJSON should throw error on HTTP failure'); | |
| } | |
| // Verify toast is in the DOM | |
| const toastStack = document.querySelector('.toast-stack'); | |
| if (!toastStack) { | |
| throw new Error('Toast stack not found in DOM'); | |
| } | |
| const errorToasts = toastStack.querySelectorAll('.toast.error'); | |
| if (errorToasts.length === 0) { | |
| throw new Error('No error toast found in toast stack'); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.1 passed: HTTP errors trigger error toasts\n'); | |
| console.log('Property 4.2: fetchJSON should display error toast on network errors'); | |
| // Test that network errors trigger error toasts | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Non-empty error message | |
| async (errorMessage) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Mock fetch to throw network error | |
| window.fetch = createMockFetch(true, null, errorMessage); | |
| // Track toast creation | |
| let toastCreated = false; | |
| let toastType = null; | |
| // Override toast creation to capture calls | |
| const originalToast = window.UIFeedback.toast; | |
| window.UIFeedback.toast = (type, title, message) => { | |
| toastCreated = true; | |
| toastType = type; | |
| originalToast(type, title, message); | |
| }; | |
| // Call fetchJSON and expect it to throw | |
| let errorThrown = false; | |
| try { | |
| await window.UIFeedback.fetchJSON('/api/test', {}, 'Test Context'); | |
| } catch (err) { | |
| errorThrown = true; | |
| } | |
| // Verify error toast was created | |
| if (!toastCreated) { | |
| throw new Error('No toast created for network error'); | |
| } | |
| if (toastType !== 'error') { | |
| throw new Error(`Expected error toast, got ${toastType}`); | |
| } | |
| if (!errorThrown) { | |
| throw new Error('fetchJSON should throw error on network failure'); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.2 passed: Network errors trigger error toasts\n'); | |
| console.log('Property 4.3: fetchJSON should return data on success'); | |
| // Test that successful requests don't create error toasts | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // URL path | |
| async (urlPath) => { | |
| const { window } = await createTestEnvironment(); | |
| // Mock fetch to return success | |
| const mockData = { result: 'success', path: urlPath }; | |
| window.fetch = async () => ({ | |
| ok: true, | |
| status: 200, | |
| json: async () => mockData | |
| }); | |
| // Track toast creation | |
| let errorToastCreated = false; | |
| // Override toast creation to capture calls | |
| const originalToast = window.UIFeedback.toast; | |
| window.UIFeedback.toast = (type, title, message) => { | |
| if (type === 'error') { | |
| errorToastCreated = true; | |
| } | |
| originalToast(type, title, message); | |
| }; | |
| // Call fetchJSON | |
| const result = await window.UIFeedback.fetchJSON(`/api/${urlPath}`, {}, 'Test'); | |
| // Verify no error toast was created | |
| if (errorToastCreated) { | |
| throw new Error('Error toast created for successful request'); | |
| } | |
| // Verify data was returned | |
| if (JSON.stringify(result) !== JSON.stringify(mockData)) { | |
| throw new Error('fetchJSON did not return correct data'); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.3 passed: Successful requests return data without error toasts\n'); | |
| console.log('Property 4.4: toast function should create visible toast elements'); | |
| // Test that toast function creates DOM elements | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.constantFrom('success', 'error', 'warning', 'info'), // Toast types | |
| fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // Title | |
| fc.option(fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), { nil: null }), // Optional message | |
| async (type, title, message) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Create toast | |
| window.UIFeedback.toast(type, title, message); | |
| // Verify toast was added to DOM | |
| const toastStack = document.querySelector('.toast-stack'); | |
| if (!toastStack) { | |
| throw new Error('Toast stack not found'); | |
| } | |
| const toasts = toastStack.querySelectorAll(`.toast.${type}`); | |
| if (toasts.length === 0) { | |
| throw new Error(`No ${type} toast found in stack`); | |
| } | |
| const lastToast = toasts[toasts.length - 1]; | |
| const toastHTML = lastToast.innerHTML; | |
| // Verify title is in toast | |
| if (!toastHTML.includes(title)) { | |
| throw new Error(`Toast does not contain title: ${title}`); | |
| } | |
| // Verify message is in toast if provided | |
| if (message && !toastHTML.includes(message)) { | |
| throw new Error(`Toast does not contain message: ${message}`); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.4 passed: Toast function creates visible elements\n'); | |
| console.log('Property 4.5: setBadge should update element class and text'); | |
| // Test that setBadge updates badge elements correctly | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0), // Badge text | |
| fc.constantFrom('info', 'success', 'warning', 'danger'), // Badge tone | |
| async (text, tone) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Create a badge element | |
| const badge = document.createElement('span'); | |
| badge.className = 'badge'; | |
| document.body.appendChild(badge); | |
| // Update badge | |
| window.UIFeedback.setBadge(badge, text, tone); | |
| // Verify text was set | |
| if (badge.textContent !== text) { | |
| throw new Error(`Badge text not set correctly. Expected: ${text}, Got: ${badge.textContent}`); | |
| } | |
| // Verify class was set | |
| if (!badge.classList.contains('badge')) { | |
| throw new Error('Badge should have "badge" class'); | |
| } | |
| if (!badge.classList.contains(tone)) { | |
| throw new Error(`Badge should have "${tone}" class`); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.5 passed: setBadge updates element correctly\n'); | |
| console.log('Property 4.6: showLoading should display loading indicator'); | |
| // Test that showLoading creates loading indicators | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.option(fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), { nil: undefined }), // Optional message | |
| async (message) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Create a container | |
| const container = document.createElement('div'); | |
| container.id = 'test-container'; | |
| document.body.appendChild(container); | |
| // Show loading | |
| window.UIFeedback.showLoading(container, message); | |
| // Verify loading indicator was added | |
| const loadingIndicator = container.querySelector('.loading-indicator'); | |
| if (!loadingIndicator) { | |
| throw new Error('Loading indicator not found'); | |
| } | |
| // Verify message is displayed | |
| const expectedMessage = message || 'Loading data...'; | |
| if (!loadingIndicator.textContent.includes(expectedMessage)) { | |
| throw new Error(`Loading indicator does not contain expected message: ${expectedMessage}`); | |
| } | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.6 passed: showLoading displays loading indicator\n'); | |
| console.log('Property 4.7: fadeReplace should update container content'); | |
| // Test that fadeReplace updates content | |
| await fc.assert( | |
| fc.asyncProperty( | |
| fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0), // HTML content | |
| async (html) => { | |
| const { window, document } = await createTestEnvironment(); | |
| // Create a container | |
| const container = document.createElement('div'); | |
| container.id = 'test-container'; | |
| container.innerHTML = '<p>Old content</p>'; | |
| document.body.appendChild(container); | |
| // Replace content | |
| window.UIFeedback.fadeReplace(container, html); | |
| // Verify content was replaced | |
| if (container.innerHTML !== html) { | |
| throw new Error('Container content not replaced'); | |
| } | |
| // Verify fade-in class was added (may be removed by timeout) | |
| // We just check that the content was updated | |
| return true; | |
| } | |
| ), | |
| { numRuns: 50, verbose: true } | |
| ); | |
| console.log('✓ Property 4.7 passed: fadeReplace updates container content\n'); | |
| console.log('\n✓ All property-based tests for ui-feedback.js passed!'); | |
| console.log('✓ Property 4: Error toast display validated successfully'); | |
| } | |
| runTests().catch(err => { | |
| console.error('Test failed:', err); | |
| process.exit(1); | |
| }); | |