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
+
+
+
+
📥
+
+
+
+
+
+
+
+
+
+
+```
+
+### 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"}}
+
+{{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