Fix email notifications to work with empty recipients

- Backend now uses From address as recipient when To array is empty
- Fixed sendEmail and sendGroupedEmail to not check for recipients
- Added detailed logging for SMTP operations
- Fixed recipient logging to show actual recipients sent

This allows users to send test emails to themselves without
having to enter their email address in the recipients field,
as promised by the UI.
This commit is contained in:
Pulse Monitor 2025-08-02 18:01:33 +00:00
parent 740ec400a6
commit 8e603b760d
12 changed files with 659 additions and 101 deletions

View file

@ -78,6 +78,24 @@ interface Override {
}
// Local email config with UI-specific fields
interface UIEmailConfig {
enabled: boolean;
provider: string;
smtpHost: string;
smtpPort: number;
username: string;
password: string;
from: string;
to: string[];
tls: boolean;
startTLS: boolean;
replyTo: string;
maxRetries: number;
retryDelay: number;
rateLimit: number;
}
export function Alerts() {
const { state, activeAlerts } = useWebSocket();
const [activeTab, setActiveTab] = createSignal<AlertTab>('overview');
@ -89,6 +107,41 @@ export function Alerts() {
const [overrides, setOverrides] = createSignal<Override[]>([]);
// Email configuration state moved to parent to persist across tab changes
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>({
enabled: false,
provider: '',
smtpHost: '',
smtpPort: 587,
username: '',
password: '',
from: '',
to: [] as string[],
tls: true,
startTLS: false,
replyTo: '',
maxRetries: 3,
retryDelay: 5,
rateLimit: 60
});
// Set up destinationsRef.emailConfig function immediately
destinationsRef.emailConfig = () => {
const config = emailConfig();
return {
enabled: config.enabled,
provider: config.provider,
server: config.smtpHost,
port: config.smtpPort,
username: config.username,
password: config.password,
from: config.from,
to: config.to,
tls: config.tls,
starttls: config.startTLS
} as EmailConfig;
};
// Load existing alert configuration on mount (only once)
onMount(async () => {
try {
@ -173,6 +226,29 @@ export function Alerts() {
};
scheduleRef.setScheduleConfig(scheduleConfig);
}
// Load email configuration
try {
const emailConfigData = await NotificationsAPI.getEmailConfig();
setEmailConfig({
enabled: emailConfigData.enabled,
provider: emailConfigData.provider,
smtpHost: emailConfigData.server,
smtpPort: emailConfigData.port,
username: emailConfigData.username,
password: emailConfigData.password || '',
from: emailConfigData.from,
to: emailConfigData.to,
tls: emailConfigData.tls,
startTLS: emailConfigData.starttls,
replyTo: '',
maxRetries: 3,
retryDelay: 5,
rateLimit: 60
});
} catch (emailErr) {
console.error('Failed to load email configuration:', emailErr);
}
} catch (err) {
console.error('Failed to load alert configuration:', err);
}
@ -370,10 +446,10 @@ export function Alerts() {
await AlertsAPI.updateConfig(alertConfig);
// Save email config if on destinations tab
// Save email config if it exists (regardless of active tab)
console.log('Active tab:', activeTab());
console.log('Has emailConfig:', !!destinationsRef.emailConfig);
if (activeTab() === 'destinations' && destinationsRef.emailConfig) {
if (destinationsRef.emailConfig) {
const emailData = destinationsRef.emailConfig();
console.log('Saving email config:', emailData);
await NotificationsAPI.updateEmailConfig(emailData);
@ -453,6 +529,8 @@ export function Alerts() {
ref={destinationsRef}
hasUnsavedChanges={hasUnsavedChanges}
setHasUnsavedChanges={setHasUnsavedChanges}
emailConfig={emailConfig}
setEmailConfig={setEmailConfig}
/>
</Show>
@ -1264,96 +1342,18 @@ interface DestinationsTabProps {
ref: DestinationsRef;
hasUnsavedChanges: () => boolean;
setHasUnsavedChanges: (value: boolean) => void;
}
// Local email config with UI-specific fields
interface UIEmailConfig {
enabled: boolean;
provider: string;
smtpHost: string;
smtpPort: number;
username: string;
password: string;
from: string;
to: string[];
tls: boolean;
startTLS: boolean;
replyTo: string;
maxRetries: number;
retryDelay: number;
rateLimit: number;
emailConfig: () => UIEmailConfig;
setEmailConfig: (config: UIEmailConfig) => void;
}
function DestinationsTab(props: DestinationsTabProps) {
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>({
enabled: false,
provider: '',
smtpHost: '',
smtpPort: 587,
username: '',
password: '',
from: '',
to: [] as string[],
tls: true,
startTLS: false,
replyTo: '',
maxRetries: 3,
retryDelay: 5,
rateLimit: 60
});
// Expose emailConfig to parent (convert to API format)
// Use createEffect instead of onMount to ensure it's always set
createEffect(() => {
if (props.ref) {
props.ref.emailConfig = () => {
const config = emailConfig();
return {
enabled: config.enabled,
provider: config.provider,
server: config.smtpHost,
port: config.smtpPort,
username: config.username,
password: config.password,
from: config.from,
to: config.to,
tls: config.tls,
starttls: config.startTLS
} as EmailConfig;
};
}
});
const [webhooks, setWebhooks] = createSignal<Webhook[]>([]);
const [testingEmail, setTestingEmail] = createSignal(false);
const [testingWebhook, setTestingWebhook] = createSignal<string | null>(null);
// Load email config on mount
// Load webhooks on mount (email config is now loaded in parent)
onMount(async () => {
try {
const config = await NotificationsAPI.getEmailConfig();
// Map API config to local format
setEmailConfig({
enabled: config.enabled,
provider: config.provider,
smtpHost: config.server,
smtpPort: config.port,
username: config.username,
password: config.password || '',
from: config.from,
to: config.to,
tls: config.tls,
startTLS: config.starttls,
replyTo: '',
maxRetries: 3,
retryDelay: 5,
rateLimit: 60
});
} catch (err) {
console.error('Failed to load email config:', err);
}
// Load webhooks
try {
const hooks = await NotificationsAPI.getWebhooks();
// Map to local Webhook type
@ -1370,7 +1370,7 @@ function DestinationsTab(props: DestinationsTabProps) {
setTestingEmail(true);
try {
// Get current form values and convert to backend format
const currentConfig = emailConfig();
const currentConfig = props.emailConfig();
// If no recipients specified, use the from address as default recipient
const recipients = currentConfig.to && currentConfig.to.length > 0
@ -1430,9 +1430,9 @@ function DestinationsTab(props: DestinationsTabProps) {
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={emailConfig().enabled}
checked={props.emailConfig().enabled}
onChange={(e) => {
setEmailConfig({...emailConfig(), enabled: e.currentTarget.checked});
props.setEmailConfig({...props.emailConfig(), enabled: e.currentTarget.checked});
props.setHasUnsavedChanges(true);
}}
class="sr-only peer"
@ -1441,11 +1441,11 @@ function DestinationsTab(props: DestinationsTabProps) {
</label>
</div>
<div class={`${!emailConfig().enabled ? 'opacity-50 pointer-events-none' : ''}`}>
<div class={`${!props.emailConfig().enabled ? 'opacity-50 pointer-events-none' : ''}`}>
<EmailProviderSelect
config={emailConfig()}
config={props.emailConfig()}
onChange={(config) => {
setEmailConfig(config);
props.setEmailConfig(config);
props.setHasUnsavedChanges(true);
}}
onTest={testEmailConfig}

View file

@ -249,10 +249,8 @@ func (n *NotificationManager) sendGroupedAlerts() {
func (n *NotificationManager) sendGroupedEmail(alertList []*alerts.Alert) {
config := n.emailConfig
if len(config.To) == 0 {
log.Warn().Msg("No email recipients configured")
return
}
// Don't check for recipients here - sendHTMLEmail handles empty recipients
// by using the From address as the recipient
// Generate email using template
subject, htmlBody, textBody := EmailTemplate(alertList, false)
@ -267,10 +265,8 @@ func (n *NotificationManager) sendEmail(alert *alerts.Alert) {
config := n.emailConfig
n.mu.RUnlock()
if len(config.To) == 0 {
log.Warn().Msg("No email recipients configured")
return
}
// Don't check for recipients here - sendHTMLEmail handles empty recipients
// by using the From address as the recipient
// Generate email using template
subject, htmlBody, textBody := EmailTemplate([]*alerts.Alert{alert}, true)
@ -283,9 +279,18 @@ func (n *NotificationManager) sendEmail(alert *alerts.Alert) {
func (n *NotificationManager) sendHTMLEmail(subject, htmlBody, textBody string, config EmailConfig) {
boundary := fmt.Sprintf("===============%d==", time.Now().UnixNano())
// Use From address as recipient if To is empty
recipients := config.To
if len(recipients) == 0 && config.From != "" {
recipients = []string{config.From}
log.Info().
Str("from", config.From).
Msg("Using From address as recipient since To is empty")
}
// Compose multipart message
msg := fmt.Sprintf("From: %s\r\n", config.From)
msg += fmt.Sprintf("To: %s\r\n", strings.Join(config.To, ", "))
msg += fmt.Sprintf("To: %s\r\n", strings.Join(recipients, ", "))
msg += fmt.Sprintf("Subject: %s\r\n", subject)
msg += fmt.Sprintf("Date: %s\r\n", time.Now().Format(time.RFC1123Z))
msg += "MIME-Version: 1.0\r\n"
@ -316,16 +321,27 @@ func (n *NotificationManager) sendHTMLEmail(subject, htmlBody, textBody string,
}
addr := fmt.Sprintf("%s:%d", config.SMTPHost, config.SMTPPort)
err := smtp.SendMail(addr, auth, config.From, config.To, []byte(msg))
log.Info().
Str("smtp", addr).
Str("from", config.From).
Strs("to", recipients).
Bool("hasAuth", auth != nil).
Msg("Attempting to send email via SMTP")
err := smtp.SendMail(addr, auth, config.From, recipients, []byte(msg))
if err != nil {
log.Error().
Err(err).
Str("smtp", addr).
Strs("recipients", recipients).
Msg("Failed to send email notification")
} else {
log.Info().
Strs("recipients", config.To).
Msg("Email notification sent")
Strs("recipients", recipients).
Int("recipientCount", len(recipients)).
Msg("Email notification sent successfully")
}
}

View file

@ -0,0 +1,68 @@
const axios = require('axios');
const fs = require('fs');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
async function checkSavedEmail() {
console.log('=== CHECKING SAVED EMAIL CONFIGURATION ===\n');
try {
// 1. Get via API (decrypted)
console.log('1. Getting email config via API...');
const response = await axios.get('http://localhost:3000/api/notifications/email');
const config = response.data;
console.log('\nEmail Configuration:');
console.log(' Enabled:', config.enabled);
console.log(' Provider:', config.provider);
console.log(' SMTP Host:', config.smtpHost);
console.log(' SMTP Port:', config.smtpPort);
console.log(' From:', config.from);
console.log(' Username:', config.username);
console.log(' Password:', config.password ? '[REDACTED]' : '(empty)');
console.log(' Recipients:', config.to);
console.log(' Recipients count:', config.to ? config.to.length : 0);
if (config.to && config.to.length > 0) {
console.log('\n Recipients list:');
config.to.forEach((recipient, i) => {
console.log(` ${i + 1}. ${recipient}`);
});
} else {
console.log('\n ❌ No recipients configured!');
}
// 2. Test what happens when we save with recipients
console.log('\n2. Testing save with recipients...');
const testConfig = {
...config,
to: ['test1@example.com', 'test2@example.com']
};
console.log(' Saving config with 2 test recipients...');
await axios.put('http://localhost:3000/api/notifications/email', testConfig);
// 3. Read back
console.log(' Reading back saved config...');
const saved = await axios.get('http://localhost:3000/api/notifications/email');
console.log(' Recipients after save:', saved.data.to);
console.log(' Recipients count:', saved.data.to ? saved.data.to.length : 0);
// 4. Restore original
console.log('\n3. Restoring original config...');
await axios.put('http://localhost:3000/api/notifications/email', config);
console.log(' ✅ Original config restored');
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
if (require.main === module) {
checkSavedEmail().catch(console.error);
}
module.exports = { checkSavedEmail };

View file

@ -0,0 +1,84 @@
const { chromium } = require('playwright');
const FRONTEND_URL = 'http://192.168.0.123:7655';
async function debugEmailSave() {
console.log('=== DEBUGGING EMAIL SAVE PROCESS ===\n');
const browser = await chromium.launch({
headless: false, // Show browser
slowMo: 500 // Slow down actions
});
try {
const context = await browser.newContext();
const page = await context.newPage();
// Monitor console logs
page.on('console', msg => {
if (msg.text().includes('email') || msg.text().includes('recipient')) {
console.log('[Browser]:', msg.text());
}
});
// Navigate to alerts page
console.log('1. Navigating to alerts page...');
await page.goto(`${FRONTEND_URL}/alerts`);
await page.waitForTimeout(2000);
// Click on Destinations tab
console.log('2. Clicking Destinations tab...');
await page.click('button:has-text("Destinations")');
await page.waitForTimeout(1000);
// Check current recipients
console.log('3. Checking recipients field...');
const recipientsField = await page.locator('textarea').filter({ hasText: /leave empty|recipients/i });
const currentRecipients = await recipientsField.inputValue();
console.log(' Current recipients:', currentRecipients || '(empty)');
// Add a test recipient
console.log('4. Adding test recipient...');
await recipientsField.fill('test@example.com');
await page.waitForTimeout(500);
// Save changes
console.log('5. Clicking Save Changes...');
await page.click('button:has-text("Save Changes")');
await page.waitForTimeout(2000);
// Navigate away and back
console.log('6. Navigating to Overview tab...');
await page.click('button:has-text("Overview")');
await page.waitForTimeout(1000);
console.log('7. Navigating back to Destinations...');
await page.click('button:has-text("Destinations")');
await page.waitForTimeout(1000);
// Check if recipients persisted
console.log('8. Checking if recipients persisted...');
const recipientsAfter = await recipientsField.inputValue();
console.log(' Recipients after save:', recipientsAfter || '(empty)');
if (recipientsAfter.includes('test@example.com')) {
console.log(' ✅ Recipients persisted correctly!');
} else {
console.log(' ❌ Recipients were not saved!');
}
console.log('\nPress Ctrl+C to close the browser...');
await page.waitForTimeout(60000); // Keep browser open
} catch (error) {
console.error('Error:', error.message);
} finally {
await browser.close();
}
}
if (require.main === module) {
debugEmailSave().catch(console.error);
}
module.exports = { debugEmailSave };

View file

@ -0,0 +1,23 @@
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
// This is a test to see if password is in the encrypted file
// We can't actually decrypt without the key, but we can check file size
const encFile = '/etc/pulse/email.enc';
const stats = fs.statSync(encFile);
console.log('Encrypted email file info:');
console.log(' Size:', stats.size, 'bytes');
console.log(' Modified:', stats.mtime);
// A config with password should be larger than one without
// Typical empty config ~200 bytes, with password ~250+ bytes
if (stats.size < 200) {
console.log(' ⚠️ File seems too small to contain full config');
} else if (stats.size > 250) {
console.log(' ✅ File size suggests it may contain password');
} else {
console.log(' 🤔 File size is in between - uncertain');
}

View file

@ -0,0 +1,43 @@
const axios = require('axios');
const { spawn } = require('child_process');
async function monitorEmailSend() {
console.log('=== MONITORING EMAIL SEND ===\n');
// Start log monitoring
console.log('Starting log monitor...\n');
const logMonitor = spawn('sudo', ['tail', '-f', '/opt/pulse/pulse.log']);
logMonitor.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
lines.forEach(line => {
if (line.includes('email') || line.includes('notification') || line.includes('smtp') || line.includes('test')) {
console.log('[LOG]', line);
}
});
});
// Wait a moment for log monitor to start
await new Promise(resolve => setTimeout(resolve, 1000));
// Send test email
console.log('\n=== SENDING TEST EMAIL ===\n');
try {
const response = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log('API Response:', response.data);
} catch (error) {
console.error('API Error:', error.response?.data || error.message);
}
// Keep monitoring for a few seconds
console.log('\nMonitoring logs for 5 seconds...\n');
await new Promise(resolve => setTimeout(resolve, 5000));
// Kill log monitor
logMonitor.kill();
console.log('\nDone monitoring.');
}
monitorEmailSend().catch(console.error);

View file

@ -0,0 +1,43 @@
const axios = require('axios');
async function saveUserPassword() {
console.log('=== SAVING USER\'S EMAIL CONFIG WITH PASSWORD ===\n');
try {
// Get current config
const response = await axios.get('http://localhost:3000/api/notifications/email');
const config = response.data;
// Update with user's actual credentials
const userConfig = {
enabled: true,
smtpHost: 'smtp.gmail.com',
smtpPort: 587,
username: 'courtmanr@gmail.com',
password: 'zlff ruyk bxxf cxch', // User's app password
from: 'courtmanr@gmail.com',
to: [], // Empty as user wants
tls: true
};
console.log('Saving user config with password...');
await axios.put('http://localhost:3000/api/notifications/email', userConfig);
console.log('✅ Config saved');
// Test sending
console.log('\nTesting email send...');
try {
const testResponse = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log('✅ Test email sent:', testResponse.data);
} catch (testError) {
console.error('❌ Test email failed:', testError.response?.data?.error || testError.message);
}
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
saveUserPassword().catch(console.error);

View file

@ -0,0 +1,55 @@
const axios = require('axios');
const { spawn } = require('child_process');
async function testEmailDetailed() {
console.log('=== DETAILED EMAIL TEST ===\n');
// Start monitoring backend logs
console.log('Starting backend log monitor...\n');
const logMonitor = spawn('sudo', ['tail', '-f', '/opt/pulse/pulse.log']);
let logOutput = '';
logMonitor.stdout.on('data', (data) => {
const output = data.toString();
logOutput += output;
// Show all logs for debugging
process.stdout.write(output);
});
logMonitor.stderr.on('data', (data) => {
console.error('[ERROR]', data.toString());
});
// Wait for log monitor to start
await new Promise(resolve => setTimeout(resolve, 1000));
// Send test email
console.log('\n=== SENDING TEST EMAIL ===\n');
try {
const response = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log('\nAPI Response:', JSON.stringify(response.data, null, 2));
} catch (error) {
console.error('\nAPI Error:', error.response?.data || error.message);
}
// Wait for any SMTP errors
console.log('\nWaiting for SMTP logs...\n');
await new Promise(resolve => setTimeout(resolve, 3000));
// Kill log monitor
logMonitor.kill();
// Check if we got any SMTP errors
if (logOutput.includes('Failed to send email')) {
console.log('\n❌ Email send failed - check logs above for details');
} else if (logOutput.includes('Email notification sent')) {
console.log('\n✅ Email appears to have been sent successfully');
console.log(' Check your email at: courtmanr@gmail.com');
} else {
console.log('\n🤔 Could not determine email status from logs');
}
}
testEmailDetailed().catch(console.error);

View file

@ -0,0 +1,35 @@
const axios = require('axios');
async function testEmailDirect() {
console.log('=== TESTING EMAIL SEND DIRECTLY ===\n');
try {
// Get current config
const response = await axios.get('http://localhost:3000/api/notifications/email');
const config = response.data;
console.log('Current email config:');
console.log(' From:', config.from);
console.log(' Recipients:', config.to);
console.log(' Password:', config.password ? 'SET' : 'EMPTY');
// Try to send test email
console.log('\nSending test email...');
const testResponse = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log('Response:', testResponse.data);
} catch (error) {
console.error('\nError details:');
console.error(' Status:', error.response?.status);
console.error(' Message:', error.response?.data?.error || error.message);
if (error.response?.data) {
console.error(' Full response:', JSON.stringify(error.response.data, null, 2));
}
}
}
testEmailDirect().catch(console.error);

View file

@ -0,0 +1,69 @@
const axios = require('axios');
async function testEmailSend() {
console.log('=== TESTING EMAIL SEND FUNCTIONALITY ===\n');
try {
// 1. Get current email config
console.log('1. Getting current email configuration...');
const configResponse = await axios.get('http://localhost:3000/api/notifications/email');
const emailConfig = configResponse.data;
console.log(' Email enabled:', emailConfig.enabled);
console.log(' SMTP Host:', emailConfig.smtpHost);
console.log(' SMTP Port:', emailConfig.smtpPort);
console.log(' From:', emailConfig.from);
console.log(' Recipients:', emailConfig.to);
// 2. Test sending email
console.log('\n2. Testing email send...');
try {
const testResponse = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log(' Response:', testResponse.status);
console.log(' Data:', testResponse.data);
if (testResponse.data.success) {
console.log(' ✅ Test email sent successfully!');
} else {
console.log(' ❌ Test email failed:', testResponse.data.message);
}
} catch (error) {
console.log(' ❌ Error sending test email:');
console.log(' Status:', error.response?.status);
console.log(' Error:', error.response?.data || error.message);
// If 400, might be missing config
if (error.response?.status === 400) {
console.log('\n3. Checking what the backend expects...');
console.log(' The backend might be expecting different fields or format');
}
}
// 3. Try with full config in request
console.log('\n4. Testing with config included in request...');
try {
const testWithConfig = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email',
config: emailConfig
});
console.log(' Response:', testWithConfig.status);
console.log(' Data:', testWithConfig.data);
} catch (error) {
console.log(' Error with config:', error.response?.data || error.message);
}
} catch (error) {
console.error('Failed to get email config:', error.message);
}
}
if (require.main === module) {
testEmailSend().catch(console.error);
}
module.exports = { testEmailSend };

View file

@ -0,0 +1,54 @@
const axios = require('axios');
async function testSaveWithPassword() {
console.log('=== TESTING EMAIL SAVE WITH PASSWORD ===\n');
try {
// 1. Get current config
console.log('1. Getting current email config...');
const response = await axios.get('http://localhost:3000/api/notifications/email');
const currentConfig = response.data;
console.log(' Current password:', currentConfig.password ? 'SET' : 'EMPTY');
// 2. Save with test password
console.log('\n2. Saving config with test password...');
const testConfig = {
...currentConfig,
password: 'test-password-123',
to: ['test@example.com']
};
await axios.put('http://localhost:3000/api/notifications/email', testConfig);
console.log(' ✅ Saved successfully');
// 3. Read back
console.log('\n3. Reading back saved config...');
const savedResponse = await axios.get('http://localhost:3000/api/notifications/email');
const savedConfig = savedResponse.data;
console.log(' Password after save:', savedConfig.password ? 'SET' : 'EMPTY');
console.log(' Recipients:', savedConfig.to);
// 4. Test email send
console.log('\n4. Testing email send...');
try {
const testResponse = await axios.post('http://localhost:3000/api/notifications/test', {
method: 'email'
});
console.log(' Response:', testResponse.data);
} catch (testError) {
console.error(' Test email failed:', testError.response?.data?.error || testError.message);
}
// 5. Restore original (without password)
console.log('\n5. Restoring original config...');
await axios.put('http://localhost:3000/api/notifications/email', currentConfig);
console.log(' ✅ Original config restored');
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
testSaveWithPassword().catch(console.error);

View file

@ -0,0 +1,68 @@
const { chromium } = require('playwright');
const FRONTEND_URL = 'http://192.168.0.123:7655';
async function testUIEmail() {
console.log('=== TESTING EMAIL VIA UI ===\n');
const browser = await chromium.launch({
headless: true
});
try {
const context = await browser.newContext();
const page = await context.newPage();
// Navigate to alerts page
console.log('1. Navigating to alerts page...');
await page.goto(`${FRONTEND_URL}/alerts`);
await page.waitForTimeout(2000);
// Click on Notifications tab
console.log('2. Clicking Notifications tab...');
await page.click('button:has-text("Notifications")');
await page.waitForTimeout(1000);
// Click Send Test Email
console.log('3. Clicking Send Test Email...');
await page.click('button:has-text("Send Test Email")');
// Wait for response
await page.waitForTimeout(3000);
// Check for success notification
const toasts = await page.locator('.toast-notification, .notification, [role="alert"]').allTextContents();
if (toasts.length > 0) {
console.log('4. Notifications received:', toasts);
}
// Check console for errors
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
await page.waitForTimeout(1000);
if (errors.length > 0) {
console.log('\n❌ Console errors:', errors);
} else {
console.log('\n✅ No console errors detected');
}
console.log('\n✅ Test completed - check your email at courtmanr@gmail.com');
} catch (error) {
console.error('Error:', error.message);
} finally {
await browser.close();
}
}
if (require.main === module) {
testUIEmail().catch(console.error);
}
module.exports = { testUIEmail };