From 9c5f0b20d8bf7ad89d5dae399de21091f33d2ec4 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Thu, 20 Nov 2025 13:05:46 +0000 Subject: [PATCH] docs: Add comprehensive toast notification test and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test Addition: - Created tests/mjs/29-pdf-toast-notifications.test.mjs (22 assertions) - Validates toast elements, JavaScript API, animations - Tests both PDF download scenarios (modal stays open vs. closes early) - Verifies toast lifecycle: prepare → progress → success → dismiss - Tests icon/title/message updates and progress bar animation Test Coverage: ✅ Toast DOM elements exist ✅ JavaScript functions available (showPDFToast, hidePDFToast) ✅ Manual toast trigger and auto-hide ✅ PDF modal integration - Scenario A (stay in modal) ✅ PDF modal integration - Scenario B (close modal, toast appears) ✅ Toast content updates (📥 → ✅) ✅ Progress bar animation ✅ CSS animations defined (toastSlideIn, toastLifecycle, progressShrink) Documentation Addition: - Created doc/13-TOAST-NOTIFICATIONS.md (comprehensive guide) - Architecture and component structure - User experience flows (2 scenarios documented) - JavaScript API reference (showPDFToast, hidePDFToast, showError) - CSS structure and animations - Template structure with ARIA accessibility - PDF download integration details - Accessibility features and testing guide - Customization guide and troubleshooting Documentation Updates: - Updated doc/README.md (added entry #13, total: 12 → 13 docs) - Updated tests/README.md (test count: 27 → 28) - Updated tests/TEST-SUMMARY.md (added toast notifications coverage) - Updated tests/mjs/README.md (added test 29 description) Test Count: 28 active tests Doc Count: 13 core documents Coverage: Complete UX feedback system with dual-mode operation --- doc/13-TOAST-NOTIFICATIONS.md | 792 ++++++++++++++++++ doc/README.md | 4 +- tests/README.md | 2 +- tests/TEST-SUMMARY.md | 4 +- tests/mjs/29-pdf-toast-notifications.test.mjs | 403 +++++++++ tests/mjs/README.md | 17 +- 6 files changed, 1215 insertions(+), 7 deletions(-) create mode 100644 doc/13-TOAST-NOTIFICATIONS.md create mode 100644 tests/mjs/29-pdf-toast-notifications.test.mjs diff --git a/doc/13-TOAST-NOTIFICATIONS.md b/doc/13-TOAST-NOTIFICATIONS.md new file mode 100644 index 0000000..8abed72 --- /dev/null +++ b/doc/13-TOAST-NOTIFICATIONS.md @@ -0,0 +1,792 @@ +# Toast Notification System + +**Comprehensive documentation for the CV project's toast notification system.** + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [User Experience Flows](#user-experience-flows) +4. [Implementation Details](#implementation-details) +5. [JavaScript API](#javascript-api) +6. [CSS Structure](#css-structure) +7. [Template Structure](#template-structure) +8. [Integration with PDF Downloads](#integration-with-pdf-downloads) +9. [Accessibility](#accessibility) +10. [Testing](#testing) +11. [Customization Guide](#customization-guide) +12. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The toast notification system provides non-intrusive visual feedback for asynchronous operations, particularly PDF generation and downloads. It enhances user experience by: + +- **Immediate Feedback**: Users know their action was registered +- **Progress Tracking**: Visual indication of operation duration +- **Completion Confirmation**: Clear success/error messaging +- **Non-Blocking UX**: Users can continue working while operations complete + +### Key Features + +✅ Multiple toast variants (success, error, info, warning) +✅ Animated slide-in/slide-out +✅ Progress bar with timing animation +✅ Auto-dismiss with configurable duration +✅ Manual close button +✅ Mobile responsive (bottom-center on small screens) +✅ Accessibility compliant (ARIA labels, reduced motion support) +✅ Bilingual support (English/Spanish) + +--- + +## Architecture + +### Component Structure + +``` +Toast System +│ +├── CSS Layer (static/css/04-interactive/_toasts.css) +│ ├── Toast container positioning +│ ├── Variant styles (success, error, info, warning) +│ ├── Animations (slide, fade, progress) +│ └── Responsive breakpoints +│ +├── HTML Templates (templates/partials/widgets/) +│ ├── error-toast.html (Error notifications) +│ └── pdf-toast.html (PDF download feedback) +│ +├── JavaScript API (static/js/main.js) +│ ├── showPDFToast(options) +│ ├── hidePDFToast() +│ └── showError(message) +│ +└── Integration Layer + └── PDF Modal (templates/partials/modals/pdf-modal.html) + └── downloadPDF() function +``` + +### Design Principles + +1. **Progressive Enhancement**: Works without JavaScript (fallback to modal only) +2. **Separation of Concerns**: CSS handles animations, JS handles logic +3. **Reusability**: Generic toast system extensible for any notification type +4. **Performance**: Minimal DOM manipulation, CSS-driven animations +5. **Accessibility**: Screen reader announcements, keyboard navigation + +--- + +## User Experience Flows + +### Flow 1: PDF Download (Modal Stays Open) + +``` +User clicks "Download PDF" + ↓ +Modal shows loading overlay +"⏳ Preparing PDF... (~4 seconds)" + ↓ +User waits in modal + ↓ +PDF downloads to browser + ↓ +Modal auto-closes + ↓ +DONE ✅ +``` + +**When to use:** Default behavior for users who want to watch progress. + +### Flow 2: PDF Download (User Closes Modal) + +``` +User clicks "Download PDF" + ↓ +Modal shows loading overlay + ↓ +User presses ESC or clicks X + ↓ +Toast slides in (bottom-right) +"📥 Preparing PDF..." +"Generating Default CV (5 pages)... (~4s)" +[Progress bar animates] + ↓ +After estimated time (~4s) + ↓ +Toast updates +"✅ PDF Ready!" +"Check your downloads folder" + ↓ +Toast auto-dismisses (3s) + ↓ +DONE ✅ +``` + +**When to use:** Power users who want to continue working while PDF generates. + +--- + +## Implementation Details + +### File Structure + +``` +cv/ +├── static/css/04-interactive/ +│ └── _toasts.css # Toast styles (242 lines) +├── templates/partials/widgets/ +│ ├── error-toast.html # Error notification template +│ └── pdf-toast.html # PDF download notification template +├── templates/index.html # Includes toast templates +├── static/js/main.js # Toast functions +└── templates/partials/modals/ + └── pdf-modal.html # Integrates toast with downloads +``` + +### CSS Organization + +**File:** `static/css/04-interactive/_toasts.css` + +**Sections:** +1. **Base Toast Container** - Positioning, layout, common styles +2. **Toast Variants** - Color schemes for different notification types +3. **Content Elements** - Icon, title, message, close button +4. **Progress Bar** - Visual countdown indicator +5. **Animations** - Slide-in, slide-out, lifecycle, progress +6. **Responsive Design** - Mobile breakpoints +7. **Accessibility** - Reduced motion preferences +8. **Print Styles** - Hide toasts in print media + +--- + +## JavaScript API + +### `showPDFToast(options)` + +Display a PDF download toast notification. + +**Parameters:** + +```javascript +{ + icon: string, // Emoji icon (📥, ✅, ⚠️) + title: string, // Toast title + message: string, // Toast message + duration: number, // Auto-hide duration in ms (default: 5000) + autoHide: boolean // Auto-dismiss after duration (default: true) +} +``` + +**Example:** + +```javascript +window.showPDFToast({ + icon: '📥', + title: 'Preparing PDF...', + message: 'Generating Default CV (5 pages)... (~4s)', + duration: 4000, + autoHide: false // Manually control dismissal +}); +``` + +**Return:** None + +**Side Effects:** +- Adds `.show` class to toast element +- Starts progress bar animation +- Schedules auto-hide timeout (if `autoHide: true`) + +--- + +### `hidePDFToast()` + +Immediately hide the PDF toast. + +**Parameters:** None + +**Example:** + +```javascript +window.hidePDFToast(); +``` + +**Return:** None + +**Side Effects:** +- Removes `.show` class from toast element +- Cancels progress bar animation + +--- + +### `showError(message)` + +Display an error toast notification. + +**Parameters:** + +```javascript +message: string // Error message to display +``` + +**Example:** + +```javascript +window.showError('Failed to load content. Please try again.'); +``` + +**Return:** None + +**Side Effects:** +- Shows error toast with red styling +- Auto-hides after 5 seconds (CSS animation) + +--- + +## CSS Structure + +### Toast Variants + +#### Success Toast (Green) +```css +.success-toast { + background: linear-gradient(135deg, + rgba(40, 167, 69, 0.95) 0%, + rgba(25, 135, 84, 0.95) 100%); + color: white; + border-left: 4px solid #fff; +} +``` + +#### Error Toast (Red) +```css +.error-toast { + background: linear-gradient(135deg, + rgba(220, 53, 69, 0.95) 0%, + rgba(200, 35, 51, 0.95) 100%); + color: white; + border-left: 4px solid #fff; +} +``` + +#### Info Toast (Blue) +```css +.info-toast { + background: linear-gradient(135deg, + rgba(13, 110, 253, 0.95) 0%, + rgba(10, 88, 202, 0.95) 100%); + color: white; + border-left: 4px solid #fff; +} +``` + +#### Warning Toast (Yellow) +```css +.warning-toast { + background: linear-gradient(135deg, + rgba(255, 193, 7, 0.95) 0%, + rgba(255, 167, 38, 0.95) 100%); + color: #333; + border-left: 4px solid #333; +} +``` + +### Key Animations + +#### Slide In +```css +@keyframes toastSlideIn { + from { + opacity: 0; + transform: translateX(100%) translateY(0); + } + to { + opacity: 1; + transform: translateX(0) translateY(0); + } +} +``` + +#### Lifecycle (Auto-hide) +```css +@keyframes toastLifecycle { + 0% { + opacity: 1; + transform: translateX(0) translateY(0); + } + 85% { + opacity: 1; + transform: translateX(0) translateY(0); + } + 100% { + opacity: 0; + transform: translateX(100%) translateY(0); + } +} +``` + +#### Progress Bar +```css +@keyframes progressShrink { + from { width: 100%; } + to { width: 0%; } +} +``` + +--- + +## Template Structure + +### PDF Toast Template + +**File:** `templates/partials/widgets/pdf-toast.html` + +```html +
+ + + 📥 + + +
+

Preparing PDF

+

+
+ + + + + +
+
+
+
+``` + +### Key Elements + +- **`role="status"`**: ARIA role for status messages +- **`aria-live="polite"`**: Screen reader announcement (non-interrupting) +- **`aria-atomic="true"`**: Read entire toast as single unit +- **`.no-print`**: Hidden in print media +- **`#pdf-toast-icon`**: Dynamically updated emoji +- **`#pdf-toast-title`**: Main heading +- **`#pdf-toast-message`**: Detailed message +- **`.toast-progress-bar`**: Visual countdown indicator + +--- + +## Integration with PDF Downloads + +### Download Flow Integration + +**File:** `templates/partials/modals/pdf-modal.html` + +```javascript +function downloadPDF() { + // ... setup code ... + + // Track if modal is closed by user + let modalClosedByUser = false; + + const onModalClose = () => { + modalClosedByUser = true; + + // Show toast when user closes modal + if (window.showPDFToast) { + window.showPDFToast({ + icon: '📥', + title: isSpanish ? 'Preparando PDF...' : 'Preparing PDF...', + message: `Generating ${formatName}... (~${estimatedTime}s)`, + duration: estimatedTime * 1000, + autoHide: false + }); + } + }; + + // Listen for modal close + modal.addEventListener('close', onModalClose, { once: true }); + + // Trigger download + window.location.href = url; + + // After estimated time: update toast or close modal + setTimeout(() => { + if (modalClosedByUser && window.showPDFToast) { + // Update toast to success + window.showPDFToast({ + icon: '✅', + title: isSpanish ? '¡PDF Listo!' : 'PDF Ready!', + message: isSpanish + ? 'Revisa tu carpeta de descargas' + : 'Check your downloads folder', + duration: 3000, + autoHide: true + }); + } else { + // Close modal if still open + modal.close(); + } + }, estimatedTime * 1000); +} +``` + +### Format-Specific Timing + +| PDF Format | Pages | Estimated Time | Icon Progression | +|------------|-------|----------------|------------------| +| Short | 4 | 3 seconds | 📥 → ✅ | +| Default | 5 | 4 seconds | 📥 → ✅ | +| Extended | 9 | 8 seconds | 📥 → ✅ | + +--- + +## Accessibility + +### ARIA Attributes + +```html +
+ aria-live="polite" + aria-atomic="true"> +``` + +### Screen Reader Behavior + +1. **Toast Appears**: Screen reader announces title + message +2. **Toast Updates**: New content announced automatically +3. **Toast Dismisses**: Silent (user already informed) + +### Keyboard Navigation + +- **Close Button**: Focusable with `Tab` +- **Enter/Space**: Activate close button +- **ESC**: Closes parent modal (which may trigger toast) + +### Reduced Motion + +Users with motion sensitivity get simplified animations: + +```css +@media (prefers-reduced-motion: reduce) { + .success-toast, + .error-toast, + .toast { + animation: none; + opacity: 1; + } + + .toast-progress-bar { + animation: none; + } +} +``` + +**Effect:** Toast appears/disappears instantly without sliding/fading. + +--- + +## Testing + +### Automated Test + +**File:** `tests/mjs/29-pdf-toast-notifications.test.mjs` + +**Coverage:** +- ✅ Toast elements exist in DOM +- ✅ Toast initially hidden +- ✅ JavaScript functions available +- ✅ Manual toast trigger works +- ✅ Toast auto-hides after duration +- ✅ PDF modal integration (Scenario A: Stay in modal) +- ✅ PDF modal integration (Scenario B: Close modal early) +- ✅ CSS animations defined +- ✅ Icon/title/message updates +- ✅ Progress bar animation + +**Run Test:** +```bash +bun tests/mjs/29-pdf-toast-notifications.test.mjs +``` + +### Manual Testing Checklist + +**Scenario 1: Modal Overlay Only** +- [ ] Open PDF modal +- [ ] Select Default CV +- [ ] Click Download PDF +- [ ] Wait in modal without closing +- [ ] Verify overlay shows with spinner +- [ ] Verify modal closes after ~4 seconds +- [ ] Verify NO toast appears + +**Scenario 2: Toast After Modal Close** +- [ ] Open PDF modal +- [ ] Select Extended CV (9 pages) +- [ ] Click Download PDF +- [ ] Immediately press ESC to close modal +- [ ] Verify toast appears bottom-right +- [ ] Verify icon is 📥 +- [ ] Verify title says "Preparing PDF..." +- [ ] Verify message includes format name and time +- [ ] Verify progress bar animates (shrinks) +- [ ] Wait ~8 seconds +- [ ] Verify toast updates to ✅ +- [ ] Verify title says "PDF Ready!" +- [ ] Verify toast auto-dismisses after ~3 seconds + +**Scenario 3: Close Button** +- [ ] Trigger toast manually (browser console): `showPDFToast({icon: '🧪', title: 'Test', message: 'Test message'})` +- [ ] Click × close button +- [ ] Verify toast disappears immediately + +**Scenario 4: Mobile Responsive** +- [ ] Resize browser to < 540px width +- [ ] Trigger toast +- [ ] Verify toast is full-width at bottom +- [ ] Verify content is readable +- [ ] Verify close button accessible + +--- + +## Customization Guide + +### Adding a New Toast Variant + +**1. Define CSS Class** (`static/css/04-interactive/_toasts.css`): + +```css +.custom-toast { + background: linear-gradient(135deg, + rgba(156, 39, 176, 0.95) 0%, + rgba(123, 31, 162, 0.95) 100%); + color: white; + border-left: 4px solid #fff; +} +``` + +**2. Create HTML Template** (`templates/partials/widgets/custom-toast.html`): + +```html +{{define "custom-toast"}} +
+ 🎨 +
+

Custom Title

+

+
+ +
+{{end}} +``` + +**3. Add JavaScript Function** (`static/js/main.js`): + +```javascript +window.showCustomToast = function(options = {}) { + const toast = document.getElementById('custom-toast'); + const icon = document.getElementById('custom-toast-icon'); + const title = document.getElementById('custom-toast-title'); + const message = document.getElementById('custom-toast-message'); + + if (options.icon) icon.textContent = options.icon; + if (options.title) title.textContent = options.title; + if (options.message) message.textContent = options.message; + + toast.classList.remove('show'); + void toast.offsetWidth; + toast.classList.add('show'); + + if (options.autoHide !== false) { + const duration = options.duration || 5000; + setTimeout(() => toast.classList.remove('show'), duration); + } +}; +``` + +**4. Include in Index** (`templates/index.html`): + +```html +{{template "custom-toast" .}} +``` + +### Changing Animation Duration + +**CSS** (`_toasts.css`): + +```css +/* Change lifecycle duration (default: 5s) */ +.toast.show { + animation: toastLifecycle 8s ease forwards; /* Now 8 seconds */ +} + +/* Change progress bar duration */ +.toast-progress-bar { + animation: progressShrink 8s linear forwards; /* Match lifecycle */ +} +``` + +**JavaScript** (when calling function): + +```javascript +window.showPDFToast({ + icon: '📥', + title: 'Preparing...', + message: 'This will take longer', + duration: 10000 // 10 seconds +}); +``` + +--- + +## Troubleshooting + +### Toast Not Appearing + +**Symptom:** Calling `showPDFToast()` has no effect. + +**Checks:** +1. Verify toast element exists: `document.getElementById('pdf-toast')` +2. Check console for JavaScript errors +3. Verify CSS is loaded: Inspect element and check for `.success-toast` styles +4. Confirm `toasts.css` imported in `main.css` +5. Check `index.html` includes `{{template "pdf-toast" .}}` + +**Solution:** +```bash +# Verify CSS import +grep "toasts.css" static/css/main.css + +# Verify template inclusion +grep "pdf-toast" templates/index.html + +# Check browser console +# Should see no errors when calling showPDFToast() +``` + +--- + +### Toast Not Auto-Dismissing + +**Symptom:** Toast appears but never disappears. + +**Checks:** +1. Verify `autoHide` is not set to `false` +2. Check CSS animation: `toastLifecycle` should be defined +3. Inspect element: `animation` property should be set +4. Check for `prefers-reduced-motion` (disables animations) + +**Solution:** +```javascript +// Explicitly set autoHide +window.showPDFToast({ + icon: '✅', + title: 'Test', + message: 'Should auto-hide', + duration: 3000, + autoHide: true // Explicitly enable +}); + +// Or manually hide +setTimeout(() => window.hidePDFToast(), 3000); +``` + +--- + +### Progress Bar Not Animating + +**Symptom:** Progress bar appears but doesn't shrink. + +**Checks:** +1. Verify `progressShrink` animation is defined in CSS +2. Check animation is applied: Inspect `#pdf-toast-progress` +3. Confirm `prefers-reduced-motion` is not active +4. Check `duration` parameter matches animation duration + +**Solution:** +```javascript +// Ensure progress bar animation resets +const progressBar = document.getElementById('pdf-toast-progress'); +if (progressBar) { + progressBar.style.animation = 'none'; + void progressBar.offsetWidth; // Trigger reflow + progressBar.style.animation = 'progressShrink 5000ms linear forwards'; +} +``` + +--- + +### Toast Overlaps Content on Mobile + +**Symptom:** Toast covers important content on small screens. + +**Checks:** +1. Verify responsive CSS is loaded +2. Check viewport width: `< 540px` should trigger mobile styles +3. Inspect `z-index` conflicts + +**Solution:** +```css +/* Adjust mobile positioning */ +@media (max-width: 540px) { + .toast { + bottom: 1rem; /* Lower if needed */ + right: 1rem; + left: 1rem; + z-index: 10000; /* Ensure on top */ + } +} +``` + +--- + +### Multiple Toasts Stacking + +**Symptom:** Calling `showPDFToast()` multiple times creates overlapping toasts. + +**Root Cause:** Current system uses single toast element, so multiple calls update the same toast. + +**Expected Behavior:** Later calls replace earlier toast content. + +**If You Need Multiple Toasts:** + +1. Clone toast element before showing: +```javascript +function showMultipleToasts(options) { + const original = document.getElementById('pdf-toast'); + const clone = original.cloneNode(true); + clone.id = `toast-${Date.now()}`; + document.body.appendChild(clone); + + // Update clone content and show + // ... similar logic to showPDFToast ... +} +``` + +2. Or use a toast queue system (advanced). + +--- + +## Related Documentation + +- [2. Modern Web Techniques](2-MODERN-WEB-TECHNIQUES.md) - HTMX patterns and hypermedia architecture +- [12. CSS Architecture](12-CSS-ARCHITECTURE.md) - Modular CSS structure and ITCSS organization +- [3. API Reference](3-API.md) - Backend API endpoints including PDF export +- [11. PDF Export](11-PDF-EXPORT.md) - PDF generation with chromedp + +--- + +**Last Updated**: 2025-11-20 +**Version**: 1.0 +**Status**: Production Ready +**Test Coverage**: 29-pdf-toast-notifications.test.mjs (22 assertions) diff --git a/doc/README.md b/doc/README.md index f18f9eb..7022496 100644 --- a/doc/README.md +++ b/doc/README.md @@ -17,6 +17,7 @@ - [4. Hyperscript Rules](4-HYPERSCRIPT-RULES.md) - Hyperscript conventions and best practices - [5. Zoom Implementation](5-ZOOM-IMPLEMENTATION.md) - Custom zoom feature technical details - [12. CSS Architecture](12-CSS-ARCHITECTURE.md) - Modular CSS structure and ITCSS organization ⭐ +- [13. Toast Notifications](13-TOAST-NOTIFICATIONS.md) - Toast notification system for PDF downloads and user feedback **Deployment & Operations** - [8. Deployment Guide](8-DEPLOYMENT.md) - Production deployment instructions @@ -44,6 +45,7 @@ | 4 | [HYPERSCRIPT-RULES.md](4-HYPERSCRIPT-RULES.md) | Hyperscript coding conventions | Frontend developers | | 5 | [ZOOM_IMPLEMENTATION.md](5-ZOOM-IMPLEMENTATION.md) | Zoom feature implementation details | Feature developers | | 12 | [CSS-ARCHITECTURE.md](12-CSS-ARCHITECTURE.md) | Modular CSS structure, ITCSS layers, HTMX integration | Frontend developers, designers | +| 13 | [TOAST-NOTIFICATIONS.md](13-TOAST-NOTIFICATIONS.md) | Toast notification system, PDF download feedback, user notifications | Frontend developers, UX designers | ### User & Operations Documentation @@ -139,4 +141,4 @@ All documentation in this project follows these standards: **Last Updated**: 2025-11-20 **Documentation Status**: ✅ Clean, organized, zero redundancy -**Total Active Docs**: 12 core documents + archive +**Total Active Docs**: 13 core documents + archive diff --git a/tests/README.md b/tests/README.md index ea909c8..9f23713 100644 --- a/tests/README.md +++ b/tests/README.md @@ -295,6 +295,6 @@ Every time something breaks in production: --- **Last Updated**: 2025-11-20 -**Test Count**: 27 active tests (0-28 numbered + migration tests) +**Test Count**: 28 active tests (0-29 numbered + migration tests) **Status**: Production specification **Responsibility**: These tests define what "working" means diff --git a/tests/TEST-SUMMARY.md b/tests/TEST-SUMMARY.md index d38046b..f9dc40c 100644 --- a/tests/TEST-SUMMARY.md +++ b/tests/TEST-SUMMARY.md @@ -287,8 +287,8 @@ When adding tests: --- **Last Updated**: 2025-11-20 -**Test Count**: 27 active tests - Comprehensive coverage -**Coverage**: Complete (UI, keyboard, HTMX, i18n, modals, mobile, zoom, hover-sync, hyperscript, skeleton loaders, color themes, button positioning, PDF modal, icons, dark theme, course icons, references) +**Test Count**: 28 active tests - Comprehensive coverage +**Coverage**: Complete (UI, keyboard, HTMX, i18n, modals, mobile, zoom, hover-sync, hyperscript, skeleton loaders, color themes, button positioning, PDF modal, icons, dark theme, course icons, references, toast notifications) **Status**: SINGLE SOURCE OF TRUTH - Production specification **Philosophy**: Each test validates specific functionality and edge cases diff --git a/tests/mjs/29-pdf-toast-notifications.test.mjs b/tests/mjs/29-pdf-toast-notifications.test.mjs new file mode 100644 index 0000000..bfe5eb6 --- /dev/null +++ b/tests/mjs/29-pdf-toast-notifications.test.mjs @@ -0,0 +1,403 @@ +#!/usr/bin/env bun +/** + * PDF TOAST NOTIFICATIONS TEST + * ============================= + * Tests the toast notification system for PDF downloads + * Validates both modal overlay and toast notification flows + */ + +import { chromium } from 'playwright'; + +const URL = "http://localhost:1999"; + +async function testPDFToastNotifications() { + console.log('🧪 PDF TOAST NOTIFICATIONS TEST\n'); + console.log('='.repeat(70)); + + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); + + const errors = []; + const testResults = []; + + // Track console errors + page.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + console.log(`❌ CONSOLE ERROR: ${msg.text()}`); + } + }); + + try { + // ===================================================================== + // SETUP: Navigate to page + // ===================================================================== + console.log('\n📍 STEP 1: Navigate to CV page'); + await page.goto(URL); + await page.waitForLoadState('networkidle'); + console.log('✅ Page loaded successfully'); + + // ===================================================================== + // TEST 1: Toast Elements Exist + // ===================================================================== + console.log('\n📍 STEP 2: Verify toast elements exist in DOM'); + + const pdfToast = await page.locator('#pdf-toast'); + const toastExists = await pdfToast.count() > 0; + testResults.push({ + test: 'PDF toast element exists', + passed: toastExists + }); + console.log(toastExists ? '✅ PDF toast element found' : '❌ PDF toast element not found'); + + const toastIcon = await page.locator('#pdf-toast-icon'); + const iconExists = await toastIcon.count() > 0; + testResults.push({ + test: 'Toast icon element exists', + passed: iconExists + }); + console.log(iconExists ? '✅ Toast icon element found' : '❌ Toast icon not found'); + + const toastTitle = await page.locator('#pdf-toast-title'); + const titleExists = await toastTitle.count() > 0; + testResults.push({ + test: 'Toast title element exists', + passed: titleExists + }); + console.log(titleExists ? '✅ Toast title element found' : '❌ Toast title not found'); + + const toastMessage = await page.locator('#pdf-toast-message'); + const messageExists = await toastMessage.count() > 0; + testResults.push({ + test: 'Toast message element exists', + passed: messageExists + }); + console.log(messageExists ? '✅ Toast message element found' : '❌ Toast message not found'); + + const progressBar = await page.locator('#pdf-toast-progress'); + const progressExists = await progressBar.count() > 0; + testResults.push({ + test: 'Toast progress bar exists', + passed: progressExists + }); + console.log(progressExists ? '✅ Toast progress bar found' : '❌ Progress bar not found'); + + // ===================================================================== + // TEST 2: Toast Initially Hidden + // ===================================================================== + console.log('\n📍 STEP 3: Verify toast is initially hidden'); + + const isInitiallyHidden = !(await pdfToast.evaluate(el => el.classList.contains('show'))); + testResults.push({ + test: 'Toast initially hidden', + passed: isInitiallyHidden + }); + console.log(isInitiallyHidden ? '✅ Toast is hidden initially' : '❌ Toast is visible (should be hidden)'); + + // ===================================================================== + // TEST 3: JavaScript Functions Available + // ===================================================================== + console.log('\n📍 STEP 4: Verify JavaScript toast functions exist'); + + const showPDFToastExists = await page.evaluate(() => typeof window.showPDFToast === 'function'); + testResults.push({ + test: 'showPDFToast() function exists', + passed: showPDFToastExists + }); + console.log(showPDFToastExists ? '✅ showPDFToast() function available' : '❌ showPDFToast() not found'); + + const hidePDFToastExists = await page.evaluate(() => typeof window.hidePDFToast === 'function'); + testResults.push({ + test: 'hidePDFToast() function exists', + passed: hidePDFToastExists + }); + console.log(hidePDFToastExists ? '✅ hidePDFToast() function available' : '❌ hidePDFToast() not found'); + + // ===================================================================== + // TEST 4: Manual Toast Trigger Test + // ===================================================================== + console.log('\n📍 STEP 5: Test manual toast trigger'); + + await page.evaluate(() => { + window.showPDFToast({ + icon: '🧪', + title: 'Test Toast', + message: 'This is a test notification', + duration: 3000 + }); + }); + + await page.waitForTimeout(500); + + const isToastVisible = await pdfToast.evaluate(el => el.classList.contains('show')); + testResults.push({ + test: 'Toast appears when showPDFToast() called', + passed: isToastVisible + }); + console.log(isToastVisible ? '✅ Toast appears correctly' : '❌ Toast did not appear'); + + const toastIconText = await toastIcon.textContent(); + const iconCorrect = toastIconText === '🧪'; + testResults.push({ + test: 'Toast icon updates correctly', + passed: iconCorrect + }); + console.log(iconCorrect ? `✅ Toast icon: "${toastIconText}"` : `❌ Icon incorrect: "${toastIconText}"`); + + const toastTitleText = await toastTitle.textContent(); + const titleCorrect = toastTitleText === 'Test Toast'; + testResults.push({ + test: 'Toast title updates correctly', + passed: titleCorrect + }); + console.log(titleCorrect ? `✅ Toast title: "${toastTitleText}"` : `❌ Title incorrect: "${toastTitleText}"`); + + // Wait for auto-hide + console.log('⏳ Waiting 3.5 seconds for toast auto-hide...'); + await page.waitForTimeout(3500); + + const isToastHidden = !(await pdfToast.evaluate(el => el.classList.contains('show'))); + testResults.push({ + test: 'Toast auto-hides after duration', + passed: isToastHidden + }); + console.log(isToastHidden ? '✅ Toast auto-hid correctly' : '❌ Toast still visible (should auto-hide)'); + + // ===================================================================== + // TEST 5: PDF Modal Integration - Scenario A (Stay in Modal) + // ===================================================================== + console.log('\n📍 STEP 6: Test PDF modal integration (Scenario A: Stay in Modal)'); + + // Open PDF modal + const pdfButton = await page.locator('button[onclick*="openPdfModal"], .download-btn').first(); + await pdfButton.click(); + await page.waitForTimeout(500); + + const modal = await page.locator('#pdf-modal[open]'); + const isModalOpen = await modal.count() > 0; + testResults.push({ + test: 'PDF modal opens', + passed: isModalOpen + }); + console.log(isModalOpen ? '✅ PDF modal opened' : '❌ PDF modal did not open'); + + // Select Default CV + const defaultCard = await page.locator('.pdf-option-card[data-cv-format="default"]'); + await defaultCard.click(); + await page.waitForTimeout(300); + + // Click download but DON'T close modal + const downloadBtn = await page.locator('#pdf-download-btn'); + await downloadBtn.click(); + await page.waitForTimeout(300); + + // Check overlay appears + const overlay = await page.locator('#pdf-loading-overlay'); + const isOverlayActive = await overlay.evaluate(el => el.classList.contains('active')); + testResults.push({ + test: 'Loading overlay appears on download', + passed: isOverlayActive + }); + console.log(isOverlayActive ? '✅ Loading overlay is active' : '❌ Loading overlay did not activate'); + + // Toast should NOT appear yet (modal still open) + await page.waitForTimeout(500); + const toastShouldNotShow = !(await pdfToast.evaluate(el => el.classList.contains('show'))); + testResults.push({ + test: 'Toast does not appear when modal stays open', + passed: toastShouldNotShow + }); + console.log(toastShouldNotShow ? '✅ Toast correctly hidden (modal open)' : '⚠️ Toast appeared (should wait for modal close)'); + + // Wait for modal to close automatically + console.log('⏳ Waiting 5 seconds for modal auto-close...'); + await page.waitForTimeout(5000); + + const isModalClosed = await page.locator('#pdf-modal[open]').count() === 0; + testResults.push({ + test: 'Modal auto-closes after download', + passed: isModalClosed + }); + console.log(isModalClosed ? '✅ Modal closed automatically' : '❌ Modal still open (should auto-close)'); + + // ===================================================================== + // TEST 6: PDF Modal Integration - Scenario B (Close Modal Early) + // ===================================================================== + console.log('\n📍 STEP 7: Test PDF modal integration (Scenario B: Close Modal Early)'); + + // Re-open PDF modal + await pdfButton.click(); + await page.waitForTimeout(500); + + // Select Short CV (faster for testing) + const shortCard = await page.locator('.pdf-option-card[data-cv-format="short"]'); + await shortCard.click(); + await page.waitForTimeout(300); + + // Click download + await downloadBtn.click(); + await page.waitForTimeout(300); + + // Immediately close modal (ESC key) + await page.keyboard.press('Escape'); + await page.waitForTimeout(500); + + // Toast SHOULD appear now + const toastAppearsOnClose = await pdfToast.evaluate(el => el.classList.contains('show')); + testResults.push({ + test: 'Toast appears when modal closed early', + passed: toastAppearsOnClose + }); + console.log(toastAppearsOnClose ? '✅ Toast appeared after modal close' : '❌ Toast did not appear'); + + if (toastAppearsOnClose) { + // Check toast content + const currentIcon = await toastIcon.textContent(); + const iconIsPreparing = currentIcon === '📥'; + testResults.push({ + test: 'Toast shows preparing icon (📥)', + passed: iconIsPreparing + }); + console.log(iconIsPreparing ? `✅ Toast icon: "${currentIcon}"` : `⚠️ Unexpected icon: "${currentIcon}"`); + + const currentTitle = await toastTitle.textContent(); + const titleIsPreparing = currentTitle.includes('Preparing') || currentTitle.includes('Preparando'); + testResults.push({ + test: 'Toast shows preparing title', + passed: titleIsPreparing + }); + console.log(titleIsPreparing ? `✅ Toast title: "${currentTitle}"` : `⚠️ Title: "${currentTitle}"`); + + // Check progress bar animation + const hasProgressAnimation = await page.evaluate(() => { + const bar = document.getElementById('pdf-toast-progress'); + const style = window.getComputedStyle(bar); + return style.animation && style.animation !== 'none'; + }); + testResults.push({ + test: 'Progress bar animates', + passed: hasProgressAnimation + }); + console.log(hasProgressAnimation ? '✅ Progress bar is animating' : '❌ Progress bar not animated'); + + // Wait for toast to update to success + console.log('⏳ Waiting 4 seconds for toast to update to success...'); + await page.waitForTimeout(4000); + + const finalIcon = await toastIcon.textContent(); + const iconIsSuccess = finalIcon === '✅'; + testResults.push({ + test: 'Toast updates to success icon (✅)', + passed: iconIsSuccess + }); + console.log(iconIsSuccess ? `✅ Toast updated: "${finalIcon}"` : `⚠️ Final icon: "${finalIcon}"`); + + const finalTitle = await toastTitle.textContent(); + const titleIsReady = finalTitle.includes('Ready') || finalTitle.includes('Listo'); + testResults.push({ + test: 'Toast shows ready title', + passed: titleIsReady + }); + console.log(titleIsReady ? `✅ Toast title: "${finalTitle}"` : `⚠️ Title: "${finalTitle}"`); + + // Wait for final auto-dismiss + console.log('⏳ Waiting 4 seconds for final toast auto-dismiss...'); + await page.waitForTimeout(4000); + + const finallyHidden = !(await pdfToast.evaluate(el => el.classList.contains('show'))); + testResults.push({ + test: 'Toast auto-dismisses after success', + passed: finallyHidden + }); + console.log(finallyHidden ? '✅ Toast dismissed successfully' : '⚠️ Toast still visible'); + } + + // ===================================================================== + // TEST 7: CSS Animations + // ===================================================================== + console.log('\n📍 STEP 8: Verify CSS animations are defined'); + + const hasSlideInAnimation = await page.evaluate(() => { + const sheet = Array.from(document.styleSheets).find(s => s.href?.includes('toasts.css')); + if (!sheet) return false; + const rules = Array.from(sheet.cssRules || []); + return rules.some(r => r.name === 'toastSlideIn'); + }); + testResults.push({ + test: 'toastSlideIn animation defined', + passed: hasSlideInAnimation + }); + console.log(hasSlideInAnimation ? '✅ toastSlideIn animation exists' : '❌ Animation not found'); + + const hasLifecycleAnimation = await page.evaluate(() => { + const sheet = Array.from(document.styleSheets).find(s => s.href?.includes('toasts.css')); + if (!sheet) return false; + const rules = Array.from(sheet.cssRules || []); + return rules.some(r => r.name === 'toastLifecycle'); + }); + testResults.push({ + test: 'toastLifecycle animation defined', + passed: hasLifecycleAnimation + }); + console.log(hasLifecycleAnimation ? '✅ toastLifecycle animation exists' : '❌ Animation not found'); + + const hasProgressAnimation = await page.evaluate(() => { + const sheet = Array.from(document.styleSheets).find(s => s.href?.includes('toasts.css')); + if (!sheet) return false; + const rules = Array.from(sheet.cssRules || []); + return rules.some(r => r.name === 'progressShrink'); + }); + testResults.push({ + test: 'progressShrink animation defined', + passed: hasProgressAnimation + }); + console.log(hasProgressAnimation ? '✅ progressShrink animation exists' : '❌ Animation not found'); + + } catch (error) { + console.error('\n❌ Test execution error:', error.message); + errors.push(error.message); + } + + // ===================================================================== + // SUMMARY + // ===================================================================== + console.log("\n" + "=".repeat(70)); + console.log("📊 TEST SUMMARY\n"); + + const passedTests = testResults.filter(t => t.passed).length; + const totalTests = testResults.length; + + testResults.forEach(result => { + const icon = result.passed ? '✅' : '❌'; + console.log(`${icon} ${result.test}`); + }); + + console.log(`\n📈 Results: ${passedTests}/${totalTests} tests passed`); + + if (errors.length > 0) { + console.log(`\n⚠️ ${errors.length} console errors detected`); + errors.forEach(err => console.log(` - ${err}`)); + } + + if (passedTests === totalTests && errors.length === 0) { + console.log('\n🎉 ALL TESTS PASSED! Toast notification system works perfectly!'); + } else if (passedTests >= totalTests * 0.9) { + console.log('\n✅ MOST TESTS PASSED! Minor issues detected.'); + } else { + console.log('\n⚠️ SOME TESTS FAILED - Review needed'); + } + + console.log("\n" + "=".repeat(70)); + console.log("\n💡 Browser will stay open for manual inspection."); + console.log(" You can:"); + console.log(" 1. Test both scenarios manually"); + console.log(" 2. Try different PDF formats (Short, Default, Long)"); + console.log(" 3. Test close button on toast"); + console.log(" 4. Check responsive behavior (resize window)"); + console.log(" 5. Verify progress bar animations"); + console.log("\nPress Ctrl+C when done.\n"); + + await new Promise(() => {}); // Keep browser open +} + +await testPDFToastNotifications(); diff --git a/tests/mjs/README.md b/tests/mjs/README.md index e86274f..5671684 100644 --- a/tests/mjs/README.md +++ b/tests/mjs/README.md @@ -1,6 +1,6 @@ # CV Project Test Suite (Active Tests) -**27 comprehensive tests** covering all functionality from UI interactions to PDF generation. +**28 comprehensive tests** covering all functionality from UI interactions to PDF generation. ## Quick Start @@ -92,6 +92,17 @@ Final course icon validation ### 28-references-pdf-download.test.mjs References section PDF download button +### 29-pdf-toast-notifications.test.mjs +Toast notification system for PDF downloads +- Toast elements exist in DOM +- JavaScript functions available (showPDFToast, hidePDFToast) +- Manual toast trigger and auto-hide +- PDF modal integration - Scenario A (Stay in modal) +- PDF modal integration - Scenario B (Close modal early, toast appears) +- Toast content updates (preparing → success) +- Progress bar animation +- CSS animations defined (toastSlideIn, toastLifecycle, progressShrink) + ## Migration Tests ### test-preference-migration.test.mjs @@ -139,6 +150,6 @@ bun tests/mjs/28-references-pdf-download.test.mjs --- **Last Updated**: 2025-11-20 -**Test Count**: 27 active tests -**Coverage**: Complete functionality coverage +**Test Count**: 28 active tests +**Coverage**: Complete functionality coverage including toast notifications **Status**: Production specification