feat: Add Email Attendees functionality (Phase 2)

Implements the Email Attendees feature which allows trainers to:
- Email event attendees directly from the Event Summary page
- Filter attendees by ticket type
- Use a rich text editor to compose messages
- Include CC recipients
- Send personalized emails to selected attendees

Includes unit tests, integration tests, and E2E tests to verify functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-05-20 10:33:03 -03:00
parent 11bad93a65
commit e6bdce4301
14 changed files with 1871 additions and 6 deletions

View file

@ -0,0 +1,125 @@
#!/bin/bash
# Email Attendees Feature Tests Runner
# This script:
# 1. Transfers test files to the staging server
# 2. Runs the tests on the staging server
# 3. Returns the results
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
STAGING_HOST="146.190.76.204"
STAGING_USER="roodev"
REMOTE_PLUGIN_PATH="/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events"
LOCAL_PATH="$(pwd)"
# Print header
echo -e "${GREEN}=== Running Email Attendees Tests on Staging ===${NC}"
echo -e "Remote host: ${STAGING_HOST}"
echo -e "Remote user: ${STAGING_USER}"
echo -e "Plugin path: ${REMOTE_PLUGIN_PATH}"
echo -e "======================================="
# Check if test directory exists on staging
ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests; fi"
ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests/unit ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests/unit; fi"
ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests/integration ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests/integration; fi"
# Transfer test files
echo -e "Transferring test files to staging server..."
scp "${LOCAL_PATH}/wordpress/wp-content/plugins/hvac-community-events/tests/unit/test-email-attendees-data.php" ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/unit/
scp "${LOCAL_PATH}/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-email-attendees-integration.php" ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/integration/
# Create bootstrap file if it doesn't exist
cat > /tmp/bootstrap-staging.php << 'EOF'
<?php
/**
* PHPUnit bootstrap file for staging tests
*/
// Load WordPress test environment
$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = '/tmp/wordpress-tests-lib';
}
// Give access to tests_add_filter() function
require_once $_tests_dir . '/includes/functions.php';
/**
* Manually load the plugin being tested
*/
function _manually_load_plugin() {
// First load The Events Calendar and other dependencies
require '/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/the-events-calendar/the-events-calendar.php';
require '/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/event-tickets/event-tickets.php';
// Then load our plugin
require '/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/hvac-community-events.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment
require $_tests_dir . '/includes/bootstrap.php';
EOF
# Transfer bootstrap file
scp /tmp/bootstrap-staging.php ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/
# Create test config file
cat > /tmp/wp-tests-config-staging.php << 'EOF'
<?php
/**
* WordPress Test Suite configuration for staging.
*/
// Test with WordPress debug mode on
define( 'WP_DEBUG', true );
// Path to WordPress core
define( 'ABSPATH', '/home/974670.cloudwaysapps.com/uberrxmprk/public_html/' );
// Test database config
define( 'DB_NAME', 'uberrxmprk' );
define( 'DB_USER', 'uberrxmprk' );
define( 'DB_PASSWORD', '89FqzFg9vG' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );
// WordPress DB table prefix
$table_prefix = 'wp_';
// Test with multisite disabled
define( 'WP_TESTS_MULTISITE', false );
// Disable automatic updates
define( 'AUTOMATIC_UPDATER_DISABLED', true );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', false );
define( 'SAVEQUERIES', false );
EOF
# Transfer config file
scp /tmp/wp-tests-config-staging.php ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/
# Run the tests on staging
echo -e "Running tests on staging server..."
ssh ${STAGING_USER}@${STAGING_HOST} "cd ${REMOTE_PLUGIN_PATH} && php vendor/bin/phpunit --bootstrap tests/bootstrap-staging.php tests/unit/test-email-attendees-data.php"
echo -e "---------------------------------------"
ssh ${STAGING_USER}@${STAGING_HOST} "cd ${REMOTE_PLUGIN_PATH} && php vendor/bin/phpunit --bootstrap tests/bootstrap-staging.php tests/integration/test-email-attendees-integration.php"
# Run E2E tests if available
echo -e "---------------------------------------"
echo -e "${YELLOW}To run E2E tests:${NC}"
echo -e "cd ${LOCAL_PATH} && npx playwright test email-attendees.test.ts --config=playwright.config.ts --reporter=list"
echo -e "${GREEN}===================================================${NC}"
echo -e "${GREEN}Email Attendees Test Suite Completed${NC}"

View file

@ -0,0 +1,159 @@
import { test, expect } from '@playwright/test';
import { loginAsTrainer } from './utils/login-helpers';
import { createTestEvent } from './utils/event-helpers';
/**
* Email Attendees feature tests
* @group @email-attendees
*/
test.describe('Email Attendees Functionality', () => {
let eventId: string;
let eventTitle: string = 'Test Event for Email Attendees';
test.beforeAll(async ({ browser }) => {
// Create a test event with attendees
const context = await browser.newContext();
const page = await context.newPage();
await loginAsTrainer(page);
// Create a test event
eventId = await createTestEvent(page, {
title: eventTitle,
description: 'This is a test event for the email attendees functionality',
ticketType: 'General Admission',
price: '50.00'
});
// Add test attendees - In a real environment, you'd use the Tribe Tickets API
// For testing, we'd either mock this or use a separate helper to purchase tickets
await context.close();
});
test('can access Email Attendees page from Event Summary', async ({ page }) => {
// Login as trainer
await loginAsTrainer(page);
// Navigate to event summary
await page.goto(`/event-summary/?event_id=${eventId}`);
await expect(page).toHaveTitle(new RegExp(eventTitle));
// Check that the Email Attendees button exists
const emailAttendeesButton = page.locator('a:text("Email Attendees")');
await expect(emailAttendeesButton).toBeVisible();
// Click Email Attendees button
await emailAttendeesButton.click();
// Verify we're on the Email Attendees page
await expect(page).toHaveURL(new RegExp(`/email-attendees/\\?event_id=${eventId}`));
await expect(page.locator('h1:text("Email Attendees")')).toBeVisible();
await expect(page.locator(`h2:text("${eventTitle}")`)).toBeVisible();
});
test('email form has all required elements', async ({ page }) => {
// Login and go to Email Attendees page
await loginAsTrainer(page);
await page.goto(`/email-attendees/?event_id=${eventId}`);
// Check for required form elements
await expect(page.locator('#email_subject')).toBeVisible();
await expect(page.locator('#email_cc')).toBeVisible();
// The rich text editor might be in an iframe, so check for either
const hasEditor = await page.locator('.wp-editor-area, #email_message').count() > 0;
expect(hasEditor).toBeTruthy();
// Check for recipients section
await expect(page.locator('h3:text("Recipients")')).toBeVisible();
// Check for Send Email button
await expect(page.locator('button[name="hvac_send_email"]')).toBeVisible();
});
test('can filter attendees by ticket type', async ({ page }) => {
// Login and go to Email Attendees page
await loginAsTrainer(page);
await page.goto(`/email-attendees/?event_id=${eventId}`);
// Check if there's a ticket type filter (may not be visible if only one ticket type)
const hasTicketTypeFilter = await page.locator('#ticket_type_filter').count() > 0;
if (hasTicketTypeFilter) {
// Select a ticket type
await page.selectOption('#ticket_type_filter', 'General Admission');
// Wait for page to refresh
await page.waitForLoadState('networkidle');
// Verify URL contains the ticket type parameter
await expect(page).toHaveURL(new RegExp('ticket_type=General\\+Admission'));
// Verify attendees are filtered
const attendeeItems = page.locator('.hvac-attendee-item');
await expect(attendeeItems).toContainText(['General Admission']);
}
});
test('can select all attendees', async ({ page }) => {
// Login and go to Email Attendees page
await loginAsTrainer(page);
await page.goto(`/email-attendees/?event_id=${eventId}`);
// Get initial count of checked boxes
const initialCheckedCount = await page.locator('.hvac-attendee-checkbox:checked').count();
expect(initialCheckedCount).toBe(0);
// Click "Select All" checkbox
await page.locator('#select_all_attendees').click();
// Verify all checkboxes are now checked
const attendeeCheckboxes = page.locator('.hvac-attendee-checkbox');
const attendeeCount = await attendeeCheckboxes.count();
const checkedCount = await page.locator('.hvac-attendee-checkbox:checked').count();
expect(checkedCount).toBe(attendeeCount);
});
test('shows validation error when form is incomplete', async ({ page }) => {
// Login and go to Email Attendees page
await loginAsTrainer(page);
await page.goto(`/email-attendees/?event_id=${eventId}`);
// Submit form without filling required fields
await page.locator('button[name="hvac_send_email"]').click();
// Verify error message is shown
await expect(page.locator('.hvac-email-error')).toBeVisible();
await expect(page.locator('.hvac-email-error')).toContainText('fill in all required fields');
});
test('can send email to attendees', async ({ page }) => {
// Login and go to Email Attendees page
await loginAsTrainer(page);
await page.goto(`/email-attendees/?event_id=${eventId}`);
// Fill out the form
await page.fill('#email_subject', 'Test Email Subject');
// Fill the message (handling both regular textarea and TinyMCE)
if (await page.locator('#email_message').count() > 0) {
await page.fill('#email_message', 'This is a test email message.');
} else {
// For TinyMCE, we need to use the iframe
const frame = page.frameLocator('.wp-editor-container iframe');
await frame.locator('body').fill('This is a test email message.');
}
// Select recipients (first attendee)
await page.locator('.hvac-attendee-checkbox').first().check();
// Submit form
await page.locator('button[name="hvac_send_email"]').click();
// Verify success message
await expect(page.locator('.hvac-email-sent')).toBeVisible();
await expect(page.locator('.hvac-email-sent')).toContainText('Email successfully sent');
});
});

View file

@ -0,0 +1,89 @@
import { Page } from '@playwright/test';
import { DashboardPage } from '../pages/DashboardPage';
import { CreateEventPage } from '../pages/CreateEventPage';
interface EventData {
title: string;
description?: string;
startDate?: string;
endDate?: string;
startTime?: string;
endTime?: string;
ticketType?: string;
price?: string;
venue?: string;
organizer?: string;
}
/**
* Helper function to create a test event
* @param page Playwright Page object
* @param eventData Event data to create
* @returns Event ID of the created event
*/
export async function createTestEvent(page: Page, eventData: EventData): Promise<string> {
// Navigate to dashboard
const dashboardPage = new DashboardPage(page);
await dashboardPage.navigate();
// Click create event button
await dashboardPage.clickCreateEvent();
// Fill event form
const createEventPage = new CreateEventPage(page);
await createEventPage.fillEventTitle(eventData.title);
if (eventData.description) {
await createEventPage.fillEventDescription(eventData.description);
}
// Set dates and times if provided
if (eventData.startDate) {
await createEventPage.setStartDate(eventData.startDate);
}
if (eventData.endDate) {
await createEventPage.setEndDate(eventData.endDate);
}
if (eventData.startTime) {
await createEventPage.setStartTime(eventData.startTime);
}
if (eventData.endTime) {
await createEventPage.setEndTime(eventData.endTime);
}
// Add ticket if price is provided
if (eventData.ticketType && eventData.price) {
await createEventPage.addTicket(eventData.ticketType, eventData.price);
}
// Set venue if provided
if (eventData.venue) {
await createEventPage.setVenue(eventData.venue);
}
// Set organizer if provided
if (eventData.organizer) {
await createEventPage.setOrganizer(eventData.organizer);
}
// Submit the form
const eventId = await createEventPage.submitForm();
return eventId;
}
/**
* Helper function to get event ID from URL
* @param url Event URL
* @returns Event ID extracted from URL
*/
export function extractEventId(url: string): string {
const match = url.match(/event_id=(\d+)/);
if (match && match[1]) {
return match[1];
}
return '';
}

View file

@ -0,0 +1,34 @@
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Helper function to login as a trainer
* @param page Playwright Page object
*/
export async function loginAsTrainer(page: Page): Promise<void> {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('trainer');
}
/**
* Helper function to login as an admin trainer
* @param page Playwright Page object
*/
export async function loginAsAdminTrainer(page: Page): Promise<void> {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('adminTrainer');
}
/**
* Helper function to login with custom credentials
* @param page Playwright Page object
* @param username Username to login with
* @param password Password to login with
*/
export async function loginWithCredentials(page: Page, username: string, password: string): Promise<void> {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.loginWithCredentials(username, password);
}

View file

@ -0,0 +1,88 @@
# Email Attendees Feature - Manual Test Plan
This document outlines a manual testing plan for the Email Attendees functionality in the HVAC Community Events plugin.
## Prerequisites
- A WordPress site with The Events Calendar and Event Tickets plugins installed and activated
- The HVAC Community Events plugin installed and activated
- A user with the `hvac_trainer` role
- At least one event created by the trainer
- At least one attendee registered for the event
## Test Scenarios
### 1. Navigation and Access Control
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 1.1 | As a logged-in trainer, navigate to Event Summary page of your event | Event Summary page loads with event details and Email Attendees button is visible | |
| 1.2 | Click on Email Attendees button | Redirects to Email Attendees page with the same event ID | |
| 1.3 | Try to access Email Attendees page directly when logged out | Redirects to login page | |
| 1.4 | Try to access Email Attendees page for an event you don't own | Access denied message appears | |
### 2. UI Elements
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 2.1 | Load Email Attendees page for an event with attendees | Page shows event title, navigation buttons, email form, and list of attendees | |
| 2.2 | Verify form elements | Subject field, CC field, rich text editor, and recipient list are present | |
| 2.3 | Check navigation buttons | View Event Summary and Return to Dashboard buttons are functional | |
| 2.4 | Verify attendee list | List shows attendee names, emails, and ticket types | |
| 2.5 | Check for Select All functionality | Select All checkbox selects/deselects all attendees | |
### 3. Filtering
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 3.1 | With multiple ticket types, check if filter exists | Ticket type filter dropdown is visible with all ticket types | |
| 3.2 | Select a specific ticket type | Page reloads with only attendees of that ticket type | |
| 3.3 | Select "All Tickets" option | Page shows all attendees regardless of ticket type | |
### 4. Form Validation
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 4.1 | Submit form without subject | Error message indicates subject is required | |
| 4.2 | Submit form without message | Error message indicates message is required | |
| 4.3 | Submit form without selecting recipients | Error message indicates at least one recipient is required | |
| 4.4 | Enter invalid email in CC field | Error message indicates invalid email format | |
### 5. Email Sending
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 5.1 | Fill all required fields and submit | Success message indicates email was sent | |
| 5.2 | Check recipient receives email | Email is received with correct subject and content | |
| 5.3 | Send email with CC | CC recipient also receives the email | |
| 5.4 | Verify personalization | Email begins with attendee's name if available | |
| 5.5 | Check email subject | Subject contains event title and custom subject text | |
### 6. Edge Cases
| # | Test Case | Expected Result | Pass/Fail |
|---|-----------|-----------------|-----------|
| 6.1 | Access page for an event with no attendees | Message indicates no attendees available | |
| 6.2 | Try to email non-existent event | Redirects to dashboard with error message | |
| 6.3 | Test with a large number of attendees | List correctly paginates or scrolls, Select All works correctly | |
| 6.4 | Test with special characters in email fields | Special characters are properly escaped | |
## Test Results
**Tester:** ______________________
**Date:** ________________________
**Environment:** _________________
**Overall Result:** _______________
## Notes
- Record any bugs, issues, or unexpected behavior in this section
- Include browser and device information for any UI or display issues
- Document any performance concerns or usability issues
## Recommendations
- Add any recommendations for future improvements or feature enhancements here

View file

@ -0,0 +1,136 @@
/**
* Styles for the Email Attendees page
*/
.hvac-email-attendees-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.hvac-email-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.hvac-email-title h1 {
margin: 0 0 10px 0;
}
.hvac-email-navigation {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.hvac-email-form {
margin-top: 20px;
}
.hvac-email-info {
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-email-form-row {
margin-bottom: 15px;
}
.hvac-email-form-row label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.hvac-email-form-row input[type="text"],
.hvac-email-form-row textarea {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
.hvac-email-recipients {
margin-top: 20px;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
}
.hvac-email-filter {
margin-bottom: 15px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.hvac-attendee-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
}
.hvac-attendee-item {
margin-bottom: 10px;
padding: 5px;
background-color: #f9f9f9;
border-radius: 3px;
}
.hvac-attendee-checkbox {
margin-right: 10px;
}
.hvac-email-sent {
background-color: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-email-error {
background-color: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-count-badge {
background-color: #007cba;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8em;
margin-left: 5px;
}
.hvac-select-all-container {
margin-bottom: 10px;
}
/* Responsive styles */
@media (max-width: 768px) {
.hvac-email-header {
flex-direction: column;
align-items: flex-start;
}
.hvac-email-navigation {
margin-top: 15px;
margin-bottom: 15px;
}
.hvac-email-filter {
flex-direction: column;
align-items: flex-start;
}
}

View file

@ -63,6 +63,10 @@ function hvac_ce_create_required_pages() {
'title' => 'Event Summary',
'content' => '<!-- wp:shortcode -->[hvac_event_summary]<!-- /wp:shortcode -->',
],
'email-attendees' => [ // Add email attendees page
'title' => 'Email Attendees',
'content' => '<!-- wp:shortcode -->[hvac_email_attendees]<!-- /wp:shortcode -->',
],
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
// 'submit-event' => [
// 'title' => 'Submit Event',
@ -231,6 +235,22 @@ function hvac_ce_enqueue_event_summary_styles() {
}
add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_event_summary_styles' );
/**
* Enqueue styles specifically for the HVAC Email Attendees page.
*/
function hvac_ce_enqueue_email_attendees_styles() {
// Check if we are on the email attendees page
if ( is_page( 'email-attendees' ) ) {
wp_enqueue_style(
'hvac-email-attendees-style',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-email-attendees.css',
[], // No dependencies for now
HVAC_CE_VERSION
);
}
}
add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_email_attendees_styles' );
// Include the main plugin class

View file

@ -110,6 +110,9 @@ class HVAC_Community_Events {
// Add authentication check for event summary page
add_action('template_redirect', array($this, 'check_event_summary_auth'));
// Add authentication check for email attendees page
add_action('template_redirect', array($this, 'check_email_attendees_auth'));
} // End init_hooks
/**
@ -124,6 +127,18 @@ class HVAC_Community_Events {
}
}
/**
* Check authentication for email attendees page
*/
public function check_email_attendees_auth() {
// Check if we're on the email-attendees page
if (is_page('email-attendees') && !is_user_logged_in()) {
// Redirect to login page
wp_redirect(home_url('/community-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI'])));
exit;
}
}
/**
* Plugin activation (Should be called statically or from the main plugin file context)
*/
@ -235,6 +250,9 @@ class HVAC_Community_Events {
// Add edit profile shortcode
add_shortcode('hvac_edit_profile', array('HVAC_Registration', 'render_edit_profile_form'));
// Add email attendees shortcode
add_shortcode('hvac_email_attendees', array($this, 'render_email_attendees'));
// Remove the event form shortcode as we're using TEC's shortcode instead
// add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form'));
@ -303,6 +321,40 @@ class HVAC_Community_Events {
return ob_get_clean();
}
/**
* Render email attendees content
*/
public function render_email_attendees() {
// Check if user is logged in
if (!is_user_logged_in()) {
return '<p>Please log in to email event attendees.</p>';
}
// Get event ID from URL parameter
$event_id = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0;
if ($event_id <= 0) {
return '<div class="hvac-error">No event ID provided. Please access this page from your dashboard or event summary page.</div>';
}
// Check if the event exists and user has permission to view it
$event = get_post($event_id);
if (!$event || get_post_type($event) !== Tribe__Events__Main::POSTTYPE) {
return '<div class="hvac-error">Event not found or invalid.</div>';
}
// Check if the current user has permission to view this event
// For now, we'll check if they're the post author or have edit_posts capability
if ($event->post_author != get_current_user_id() && !current_user_can('edit_posts')) {
return '<div class="hvac-error">You do not have permission to email attendees for this event.</div>';
}
// Include the email attendees template
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php';
return ob_get_clean();
}
/**
* Include custom templates for plugin pages
*/
@ -347,6 +399,14 @@ class HVAC_Community_Events {
}
}
// Check for email-attendees page
if (is_page('email-attendees')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php';
if (file_exists($custom_template)) {
return $custom_template;
}
}
// Check for edit-profile page
if (is_page('edit-profile')) {
$custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-edit-profile.php';

View file

@ -0,0 +1,277 @@
<?php
/**
* HVAC Community Events - Email Attendees Data Class
*
* Handles retrieving attendee data and sending emails for the Email Attendees functionality.
*
* @package HVAC_Community_Events
* @subpackage Community
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Email_Attendees_Data
*
* Handles data operations for the Email Attendees functionality.
*/
class HVAC_Email_Attendees_Data {
/**
* The event ID.
*
* @var int
*/
private $event_id;
/**
* Constructor.
*
* @param int $event_id The event ID.
*/
public function __construct( $event_id = 0 ) {
$this->event_id = intval( $event_id );
}
/**
* Check if the event is valid.
*
* @return bool Whether the event exists and is valid.
*/
public function is_valid_event() {
if ( empty( $this->event_id ) ) {
return false;
}
$event = get_post( $this->event_id );
return ( $event && $event->post_type === 'tribe_events' );
}
/**
* Check if the current user can view and email attendees for this event.
*
* @return bool Whether the user can view and email attendees.
*/
public function user_can_email_attendees() {
if ( ! is_user_logged_in() ) {
return false;
}
$event = get_post( $this->event_id );
if ( ! $event ) {
return false;
}
// Allow event author or admins with edit_posts capability
return ( get_current_user_id() === (int) $event->post_author || current_user_can( 'edit_posts' ) );
}
/**
* Get all attendees for the event.
*
* @return array Array of attendee data.
*/
public function get_attendees() {
if ( ! $this->is_valid_event() ) {
return array();
}
// Use The Events Calendar's function to get attendees
$attendees = tribe_tickets_get_attendees( $this->event_id );
$processed_attendees = array();
if ( ! empty( $attendees ) ) {
foreach ( $attendees as $attendee ) {
$email = isset( $attendee['holder_email'] ) ? $attendee['holder_email'] : '';
$name = isset( $attendee['holder_name'] ) ? $attendee['holder_name'] : '';
$ticket_name = isset( $attendee['ticket_name'] ) ? $attendee['ticket_name'] : '';
// Only include attendees with valid emails
if ( ! empty( $email ) && is_email( $email ) ) {
$processed_attendees[] = array(
'name' => $name,
'email' => $email,
'ticket_name' => $ticket_name,
'attendee_id' => isset( $attendee['attendee_id'] ) ? $attendee['attendee_id'] : 0,
'order_id' => isset( $attendee['order_id'] ) ? $attendee['order_id'] : 0,
);
}
}
}
return $processed_attendees;
}
/**
* Get attendees filtered by ticket type.
*
* @param string $ticket_type The ticket type to filter by.
* @return array Filtered attendees.
*/
public function get_attendees_by_ticket_type( $ticket_type ) {
$attendees = $this->get_attendees();
if ( empty( $ticket_type ) ) {
return $attendees;
}
return array_filter( $attendees, function( $attendee ) use ( $ticket_type ) {
return $attendee['ticket_name'] === $ticket_type;
});
}
/**
* Get all ticket types for the event.
*
* @return array Array of ticket types.
*/
public function get_ticket_types() {
$attendees = $this->get_attendees();
$ticket_types = array();
foreach ( $attendees as $attendee ) {
if ( ! empty( $attendee['ticket_name'] ) && ! in_array( $attendee['ticket_name'], $ticket_types ) ) {
$ticket_types[] = $attendee['ticket_name'];
}
}
return $ticket_types;
}
/**
* Get the event details.
*
* @return array Event details.
*/
public function get_event_details() {
if ( ! $this->is_valid_event() ) {
return array();
}
$event = get_post( $this->event_id );
return array(
'id' => $this->event_id,
'title' => get_the_title( $event ),
'start_date' => tribe_get_start_date( $event, false, 'F j, Y' ),
'start_time' => tribe_get_start_date( $event, false, 'g:i a' ),
'end_date' => tribe_get_end_date( $event, false, 'F j, Y' ),
'end_time' => tribe_get_end_date( $event, false, 'g:i a' ),
);
}
/**
* Send email to attendees.
*
* @param array $recipients Array of recipient emails or attendee IDs.
* @param string $subject The email subject.
* @param string $message The email message.
* @param string $cc Optional CC email addresses.
* @return array Result with status and message.
*/
public function send_email( $recipients, $subject, $message, $cc = '' ) {
if ( empty( $recipients ) || empty( $subject ) || empty( $message ) ) {
return array(
'success' => false,
'message' => 'Missing required fields (recipients, subject, or message).',
);
}
if ( ! $this->is_valid_event() || ! $this->user_can_email_attendees() ) {
return array(
'success' => false,
'message' => 'You do not have permission to email attendees for this event.',
);
}
$headers = array('Content-Type: text/html; charset=UTF-8');
$event_details = $this->get_event_details();
$event_title = $event_details['title'];
// Add CC if provided
if ( ! empty( $cc ) ) {
$cc_emails = explode( ',', $cc );
foreach ( $cc_emails as $cc_email ) {
$cc_email = trim( $cc_email );
if ( is_email( $cc_email ) ) {
$headers[] = 'Cc: ' . $cc_email;
}
}
}
// Add sender information
$current_user = wp_get_current_user();
$from_name = $current_user->display_name;
$from_email = $current_user->user_email;
$headers[] = 'From: ' . $from_name . ' <' . $from_email . '>';
// Process recipients
$all_attendees = $this->get_attendees();
$attendee_emails = array();
$sent_count = 0;
$error_count = 0;
// Handle numeric IDs or email addresses
foreach ( $recipients as $recipient ) {
if ( is_numeric( $recipient ) ) {
// Find attendee by ID
foreach ( $all_attendees as $attendee ) {
if ( $attendee['attendee_id'] == $recipient ) {
$attendee_emails[$attendee['email']] = $attendee['name'];
break;
}
}
} elseif ( is_email( $recipient ) ) {
// Add directly if it's an email
$attendee_name = '';
foreach ( $all_attendees as $attendee ) {
if ( $attendee['email'] === $recipient ) {
$attendee_name = $attendee['name'];
break;
}
}
$attendee_emails[$recipient] = $attendee_name;
}
}
// Subject with event title
$email_subject = sprintf( '[%s] %s', $event_title, $subject );
// Send to each recipient individually for personalization
foreach ( $attendee_emails as $email => $name ) {
// Personalize message with attendee name if available
$personalized_message = $message;
if ( ! empty( $name ) ) {
$personalized_message = "Hello " . $name . ",\n\n" . $message;
}
$mail_sent = wp_mail( $email, $email_subject, wpautop( $personalized_message ), $headers );
if ( $mail_sent ) {
$sent_count++;
} else {
$error_count++;
}
}
// Return results
if ( $error_count > 0 ) {
return array(
'success' => $sent_count > 0,
'message' => sprintf(
'Email sent to %d recipients. Failed to send to %d recipients.',
$sent_count,
$error_count
),
);
}
return array(
'success' => true,
'message' => sprintf( 'Email successfully sent to %d recipients.', $sent_count ),
);
}
}

View file

@ -0,0 +1,336 @@
<?php
/**
* HVAC Community Events - Email Attendees Template
*
* Template for the Email Attendees page.
*
* @package HVAC_Community_Events
* @subpackage Templates
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Check if user is logged in
if ( ! is_user_logged_in() ) {
wp_redirect( site_url( '/community-login/' ) );
exit;
}
// Get the event ID from the URL
$event_id = isset( $_GET['event_id'] ) ? intval( $_GET['event_id'] ) : 0;
// Load the email attendees data class
require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php';
$email_data = new HVAC_Email_Attendees_Data( $event_id );
// Check if event is valid and user has permission
if ( ! $email_data->is_valid_event() ) {
wp_redirect( site_url( '/hvac-dashboard/' ) );
exit;
}
if ( ! $email_data->user_can_email_attendees() ) {
wp_die( __( 'You do not have permission to email attendees for this event.', 'hvac-community-events' ) );
}
// Get event details and attendees
$event_details = $email_data->get_event_details();
$attendees = $email_data->get_attendees();
$ticket_types = $email_data->get_ticket_types();
// Handle form submission
$email_sent = false;
$email_error = '';
$email_success = '';
if ( isset( $_POST['hvac_send_email'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'hvac_email_attendees_' . $event_id ) ) {
$subject = isset( $_POST['email_subject'] ) ? sanitize_text_field( $_POST['email_subject'] ) : '';
$message = isset( $_POST['email_message'] ) ? wp_kses_post( $_POST['email_message'] ) : '';
$cc = isset( $_POST['email_cc'] ) ? sanitize_text_field( $_POST['email_cc'] ) : '';
// Get selected recipients
$recipients = array();
if ( isset( $_POST['email_attendees'] ) && is_array( $_POST['email_attendees'] ) ) {
$recipients = array_map( 'sanitize_text_field', $_POST['email_attendees'] );
}
// Validate and send email
if ( empty( $subject ) || empty( $message ) || empty( $recipients ) ) {
$email_error = __( 'Please fill in all required fields (subject, message, and select at least one recipient).', 'hvac-community-events' );
} else {
$result = $email_data->send_email( $recipients, $subject, $message, $cc );
if ( $result['success'] ) {
$email_sent = true;
$email_success = $result['message'];
} else {
$email_error = $result['message'];
}
}
}
// Get filtered attendees if a ticket type is selected
$selected_ticket_type = isset( $_GET['ticket_type'] ) ? sanitize_text_field( $_GET['ticket_type'] ) : '';
if ( ! empty( $selected_ticket_type ) ) {
$attendees = $email_data->get_attendees_by_ticket_type( $selected_ticket_type );
}
// Get the site title for the page title
$site_title = get_bloginfo( 'name' );
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo esc_html( $site_title ); ?> - <?php _e( 'Email Attendees', 'hvac-community-events' ); ?></title>
<?php wp_head(); ?>
<style>
.hvac-email-attendees-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.hvac-email-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.hvac-email-title h1 {
margin: 0 0 10px 0;
}
.hvac-email-navigation {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.hvac-email-form {
margin-top: 20px;
}
.hvac-email-info {
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-email-form-row {
margin-bottom: 15px;
}
.hvac-email-form-row label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.hvac-email-form-row input[type="text"],
.hvac-email-form-row textarea {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
.hvac-email-recipients {
margin-top: 20px;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
}
.hvac-email-filter {
margin-bottom: 15px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.hvac-attendee-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
}
.hvac-attendee-item {
margin-bottom: 10px;
padding: 5px;
background-color: #f9f9f9;
border-radius: 3px;
}
.hvac-attendee-checkbox {
margin-right: 10px;
}
.hvac-email-sent {
background-color: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-email-error {
background-color: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.hvac-count-badge {
background-color: #007cba;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8em;
margin-left: 5px;
}
.hvac-select-all-container {
margin-bottom: 10px;
}
</style>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<div class="hvac-email-attendees-wrapper">
<div class="hvac-email-header">
<div class="hvac-email-title">
<h1><?php _e( 'Email Attendees', 'hvac-community-events' ); ?></h1>
<h2><?php echo esc_html( $event_details['title'] ); ?></h2>
<p>
<?php echo esc_html( $event_details['start_date'] ); ?>
<?php echo esc_html( $event_details['start_time'] ); ?> -
<?php
if ( $event_details['start_date'] !== $event_details['end_date'] ) {
echo esc_html( $event_details['end_date'] ) . ' ';
}
echo esc_html( $event_details['end_time'] );
?>
</p>
</div>
<div class="hvac-email-navigation">
<a href="<?php echo esc_url( site_url( '/event-summary/?event_id=' . $event_id ) ); ?>" class="ast-button ast-button-secondary">
<?php _e( 'View Event Summary', 'hvac-community-events' ); ?>
</a>
<a href="<?php echo esc_url( site_url( '/hvac-dashboard/' ) ); ?>" class="ast-button ast-button-secondary">
<?php _e( 'Return to Dashboard', 'hvac-community-events' ); ?>
</a>
</div>
</div>
<?php if ( $email_sent ) : ?>
<div class="hvac-email-sent">
<?php echo esc_html( $email_success ); ?>
</div>
<?php endif; ?>
<?php if ( $email_error ) : ?>
<div class="hvac-email-error">
<?php echo esc_html( $email_error ); ?>
</div>
<?php endif; ?>
<?php if ( empty( $attendees ) ) : ?>
<div class="hvac-email-info">
<?php _e( 'This event has no attendees registered yet.', 'hvac-community-events' ); ?>
</div>
<?php else : ?>
<form method="post" class="hvac-email-form">
<?php wp_nonce_field( 'hvac_email_attendees_' . $event_id ); ?>
<div class="hvac-email-form-row">
<label for="email_subject"><?php _e( 'Subject:', 'hvac-community-events' ); ?> <span class="required">*</span></label>
<input type="text" name="email_subject" id="email_subject" required value="<?php echo isset( $_POST['email_subject'] ) ? esc_attr( $_POST['email_subject'] ) : ''; ?>">
</div>
<div class="hvac-email-form-row">
<label for="email_cc"><?php _e( 'CC:', 'hvac-community-events' ); ?></label>
<input type="text" name="email_cc" id="email_cc" value="<?php echo isset( $_POST['email_cc'] ) ? esc_attr( $_POST['email_cc'] ) : ''; ?>" placeholder="<?php _e( 'Separate multiple emails with commas', 'hvac-community-events' ); ?>">
</div>
<div class="hvac-email-form-row">
<label for="email_message"><?php _e( 'Message:', 'hvac-community-events' ); ?> <span class="required">*</span></label>
<?php
// Use WordPress editor if available
if ( function_exists( 'wp_editor' ) ) {
$content = isset( $_POST['email_message'] ) ? wp_kses_post( $_POST['email_message'] ) : '';
$editor_settings = array(
'textarea_name' => 'email_message',
'textarea_rows' => 10,
'media_buttons' => false,
'teeny' => true,
);
wp_editor( $content, 'email_message', $editor_settings );
} else {
// Fallback to textarea
echo '<textarea name="email_message" id="email_message" rows="10" required>' .
( isset( $_POST['email_message'] ) ? esc_textarea( $_POST['email_message'] ) : '' ) .
'</textarea>';
}
?>
</div>
<div class="hvac-email-recipients">
<h3><?php _e( 'Recipients', 'hvac-community-events' ); ?> <span class="hvac-count-badge"><?php echo count( $attendees ); ?></span></h3>
<?php if ( ! empty( $ticket_types ) && count( $ticket_types ) > 1 ) : ?>
<div class="hvac-email-filter">
<label for="ticket_type_filter"><?php _e( 'Filter by ticket type:', 'hvac-community-events' ); ?></label>
<select id="ticket_type_filter" onchange="window.location.href='<?php echo esc_url( add_query_arg( array( 'event_id' => $event_id ), site_url( '/email-attendees/' ) ) ); ?>&ticket_type=' + this.value">
<option value=""><?php _e( 'All Tickets', 'hvac-community-events' ); ?></option>
<?php foreach ( $ticket_types as $ticket_type ) : ?>
<option value="<?php echo esc_attr( $ticket_type ); ?>" <?php selected( $selected_ticket_type, $ticket_type ); ?>>
<?php echo esc_html( $ticket_type ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="hvac-select-all-container">
<label>
<input type="checkbox" id="select_all_attendees" onclick="toggleAllAttendees(this)">
<?php _e( 'Select All', 'hvac-community-events' ); ?>
</label>
</div>
<div class="hvac-attendee-list">
<?php foreach ( $attendees as $attendee ) : ?>
<div class="hvac-attendee-item">
<label>
<input type="checkbox" class="hvac-attendee-checkbox" name="email_attendees[]" value="<?php echo esc_attr( $attendee['email'] ); ?>">
<strong><?php echo esc_html( $attendee['name'] ); ?></strong>
(<?php echo esc_html( $attendee['email'] ); ?>)
<?php if ( ! empty( $attendee['ticket_name'] ) ) : ?>
- <?php echo esc_html( $attendee['ticket_name'] ); ?>
<?php endif; ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="hvac-email-form-row" style="margin-top: 20px;">
<button type="submit" name="hvac_send_email" class="ast-button ast-button-primary">
<?php _e( 'Send Email', 'hvac-community-events' ); ?>
</button>
</div>
</form>
<script>
function toggleAllAttendees(checkbox) {
var attendeeCheckboxes = document.querySelectorAll('.hvac-attendee-checkbox');
for (var i = 0; i < attendeeCheckboxes.length; i++) {
attendeeCheckboxes[i].checked = checkbox.checked;
}
}
</script>
<?php endif; ?>
</div>
<?php wp_footer(); ?>
</body>
</html>

View file

@ -131,10 +131,9 @@ get_header();
// View public event page
echo '<a href="' . esc_url( $event_details['permalink'] ) . '" class="ast-button ast-button-secondary" target="_blank">View Public Page</a>';
// Email attendees link (future feature)
// Email attendees link
if ( current_user_can( 'edit_post', $event_id ) ) {
// TODO: Link to actual Email Attendees page when implemented (Phase 2)
$email_url = '#'; // Placeholder for now
$email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) );
echo '<a href="' . esc_url( $email_url ) . '" class="ast-button ast-button-secondary">Email Attendees</a>';
}
?>

View file

@ -131,10 +131,9 @@ get_header();
// View public event page
echo '<a href="' . esc_url( $event_details['permalink'] ) . '" class="ast-button ast-button-secondary" target="_blank">View Public Page</a>';
// Email attendees link (future feature)
// Email attendees link
if ( current_user_can( 'edit_post', $event_id ) ) {
// TODO: Link to actual Email Attendees page when implemented (Phase 2)
$email_url = '#'; // Placeholder for now
$email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) );
echo '<a href="' . esc_url( $email_url ) . '" class="ast-button ast-button-secondary">Email Attendees</a>';
}
?>

View file

@ -0,0 +1,198 @@
<?php
/**
* Integration tests for the Email Attendees functionality
*/
class Test_Email_Attendees_Integration extends WP_UnitTestCase {
/**
* @var int Test event ID
*/
private $event_id;
/**
* @var int Test user ID
*/
private $user_id;
/**
* @var WP_REST_Server
*/
private $server;
/**
* Set up test data before each test
*/
public function setUp(): void {
parent::setUp();
// Initialize REST API server
global $wp_rest_server;
$this->server = $wp_rest_server = new WP_REST_Server;
do_action('rest_api_init');
// Create a test user with trainer role
$this->user_id = $this->factory->user->create(array(
'role' => 'hvac_trainer',
'user_login' => 'test_trainer',
'user_pass' => 'password',
'user_email' => 'trainer@example.com',
'display_name' => 'Test Trainer',
));
// Set current user for permission testing
wp_set_current_user($this->user_id);
// Make sure the classes exist
require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php';
require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-event-summary-data.php';
// Create a test event
$event_data = array(
'post_title' => 'Test Event for Email Attendees Integration',
'post_content' => 'Test event integration test content',
'post_status' => 'publish',
'post_type' => Tribe__Events__Main::POSTTYPE,
'post_author' => $this->user_id,
);
$this->event_id = wp_insert_post($event_data);
// Set up mock attendees if needed using Event Tickets functions
// This is more complex and would depend on the actual implementation
}
/**
* Clean up test data after each test
*/
public function tearDown(): void {
// Delete test event
wp_delete_post($this->event_id, true);
// Delete test user
wp_delete_user($this->user_id);
parent::tearDown();
}
/**
* Test that the email attendees shortcode exists and renders
*/
public function test_email_attendees_shortcode_exists() {
// Verify the shortcode is registered
global $shortcode_tags;
$this->assertArrayHasKey('hvac_email_attendees', $shortcode_tags);
// Test shortcode output with no parameters
$output = do_shortcode('[hvac_email_attendees]');
$this->assertStringContainsString('Please log in to email event attendees', $output);
}
/**
* Test the integration between Event Summary and Email Attendees
*/
public function test_event_summary_links_to_email_attendees() {
// Get event summary output
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/event-summary/template-event-summary.php';
$output = ob_get_clean();
// Check that the email attendees link is included
$expected_url = add_query_arg('event_id', $this->event_id, home_url('/email-attendees/'));
$this->assertStringContainsString($expected_url, $output);
$this->assertStringContainsString('Email Attendees', $output);
}
/**
* Test that the email attendees template loads correctly
*/
public function test_email_attendees_template_loads() {
// Set up $_GET parameter
$_GET['event_id'] = $this->event_id;
// Capture template output
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php';
$output = ob_get_clean();
// Verify template elements
$this->assertStringContainsString('Email Attendees', $output);
$this->assertStringContainsString('Subject:', $output);
$this->assertStringContainsString('CC:', $output);
$this->assertStringContainsString('Message:', $output);
$this->assertStringContainsString('Recipients', $output);
$this->assertStringContainsString('Send Email', $output);
// Unset $_GET parameter
unset($_GET['event_id']);
}
/**
* Test the authentication check for email attendees page
*/
public function test_email_attendees_auth_check() {
// Create an instance of the main plugin class
$plugin = new HVAC_Community_Events();
// Log out to test the redirect
wp_set_current_user(0);
// Mock WordPress is_page function
global $wp_query;
$wp_query->is_page = true;
$wp_query->queried_object = (object) array('post_name' => 'email-attendees');
// Capture redirect
ob_start();
$plugin->check_email_attendees_auth();
$output = ob_get_clean();
// Since we're not in a real page request, the function will try to redirect
// but won't exit, so we don't expect any output
$this->assertEmpty($output);
// Reset current user
wp_set_current_user($this->user_id);
}
/**
* Test form submission handling
*/
public function test_form_submission_handling() {
// This test is more complex as it involves form submission
// We would need to mock the $_POST data and possibly override wp_mail
// Set up $_GET and $_POST data
$_GET['event_id'] = $this->event_id;
$_POST['hvac_send_email'] = 1;
$_POST['email_subject'] = 'Integration Test Subject';
$_POST['email_message'] = 'Integration test message content';
$_POST['email_attendees'] = array('test@example.com');
// Create nonce
$_POST['_wpnonce'] = wp_create_nonce('hvac_email_attendees_' . $this->event_id);
// Override wp_mail for testing
add_filter('wp_mail', function($args) {
// Capture email arguments for validation
$this->assertEquals('Integration Test Subject', $args['subject']);
$this->assertEquals('test@example.com', $args['to']);
return false; // Prevent actual email sending
});
// Capture template output
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php';
$output = ob_get_clean();
// Verify success message appears in output
$this->assertStringContainsString('Email successfully sent', $output);
// Clean up
unset($_GET['event_id']);
unset($_POST['hvac_send_email']);
unset($_POST['email_subject']);
unset($_POST['email_message']);
unset($_POST['email_attendees']);
unset($_POST['_wpnonce']);
remove_all_filters('wp_mail');
}
}

View file

@ -0,0 +1,345 @@
<?php
/**
* Unit tests for the Email Attendees Data class
*/
class Test_HVAC_Email_Attendees_Data extends WP_UnitTestCase {
/**
* @var int Test event ID
*/
private $event_id;
/**
* @var int Test user ID
*/
private $user_id;
/**
* Set up test data before each test
*/
public function setUp(): void {
parent::setUp();
// Create a test user
$this->user_id = $this->factory->user->create(array(
'role' => 'hvac_trainer',
));
// Set current user for permission testing
wp_set_current_user($this->user_id);
// Make sure the class exists
require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php';
// Create a test event
$event_data = array(
'post_title' => 'Test Event for Email Attendees',
'post_content' => 'Test event content',
'post_status' => 'publish',
'post_type' => Tribe__Events__Main::POSTTYPE,
'post_author' => $this->user_id,
);
$this->event_id = wp_insert_post($event_data);
// Mock tribe_tickets_get_attendees functionality if needed
// This would require more setup depending on how deeply you want to test
}
/**
* Clean up test data after each test
*/
public function tearDown(): void {
// Delete test event
wp_delete_post($this->event_id, true);
// Delete test user
wp_delete_user($this->user_id);
parent::tearDown();
}
/**
* Test constructor
*/
public function test_constructor() {
$email_data = new HVAC_Email_Attendees_Data($this->event_id);
$this->assertInstanceOf('HVAC_Email_Attendees_Data', $email_data);
}
/**
* Test is_valid_event method
*/
public function test_is_valid_event() {
$email_data = new HVAC_Email_Attendees_Data($this->event_id);
$this->assertTrue($email_data->is_valid_event());
// Test with invalid event ID
$email_data_invalid = new HVAC_Email_Attendees_Data(999999);
$this->assertFalse($email_data_invalid->is_valid_event());
// Test with empty event ID
$email_data_empty = new HVAC_Email_Attendees_Data();
$this->assertFalse($email_data_empty->is_valid_event());
}
/**
* Test user_can_email_attendees method
*/
public function test_user_can_email_attendees() {
$email_data = new HVAC_Email_Attendees_Data($this->event_id);
// Current user is event author, should return true
$this->assertTrue($email_data->user_can_email_attendees());
// Create another user who is not the event author
$another_user_id = $this->factory->user->create(array(
'role' => 'hvac_trainer',
));
wp_set_current_user($another_user_id);
// Another user without edit_posts capability should return false
$this->assertFalse($email_data->user_can_email_attendees());
// Create admin user
$admin_user_id = $this->factory->user->create(array(
'role' => 'administrator',
));
wp_set_current_user($admin_user_id);
// Admin with edit_posts capability should return true
$this->assertTrue($email_data->user_can_email_attendees());
// Reset to original user
wp_set_current_user($this->user_id);
}
/**
* Test get_event_details method
*/
public function test_get_event_details() {
$email_data = new HVAC_Email_Attendees_Data($this->event_id);
$event_details = $email_data->get_event_details();
// Verify event details contain expected data
$this->assertIsArray($event_details);
$this->assertEquals($this->event_id, $event_details['id']);
$this->assertEquals('Test Event for Email Attendees', $event_details['title']);
}
/**
* Test get_attendees and get_attendees_by_ticket_type methods with mocked data
*/
public function test_get_attendees_with_mock_data() {
// Create a test instance
$email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data')
->setConstructorArgs(array($this->event_id))
->setMethods(array('get_attendees'))
->getMock();
// Mock attendee data
$mock_attendees = array(
array(
'name' => 'Test Attendee 1',
'email' => 'attendee1@example.com',
'ticket_name' => 'General Admission',
'attendee_id' => 101,
'order_id' => 1001,
),
array(
'name' => 'Test Attendee 2',
'email' => 'attendee2@example.com',
'ticket_name' => 'VIP Pass',
'attendee_id' => 102,
'order_id' => 1002,
),
array(
'name' => 'Test Attendee 3',
'email' => 'attendee3@example.com',
'ticket_name' => 'General Admission',
'attendee_id' => 103,
'order_id' => 1003,
),
);
// Configure mock to return test data
$email_data->expects($this->any())
->method('get_attendees')
->will($this->returnValue($mock_attendees));
// Use reflection to access non-public methods for testing
$reflection = new ReflectionClass('HVAC_Email_Attendees_Data');
$method = $reflection->getMethod('get_attendees_by_ticket_type');
$method->setAccessible(true);
// Test filtering by ticket type
$general_admission = $method->invokeArgs($email_data, array('General Admission'));
$this->assertCount(2, $general_admission);
$this->assertEquals('Test Attendee 1', $general_admission[0]['name']);
$this->assertEquals('Test Attendee 3', $general_admission[2]['name']);
// Test filtering by another ticket type
$vip_pass = $method->invokeArgs($email_data, array('VIP Pass'));
$this->assertCount(1, $vip_pass);
$this->assertEquals('Test Attendee 2', $vip_pass[0]['name']);
// Test getting all attendees (no filter)
$all_attendees = $method->invokeArgs($email_data, array(''));
$this->assertCount(3, $all_attendees);
}
/**
* Test get_ticket_types method with mocked data
*/
public function test_get_ticket_types_with_mock_data() {
// Create a test instance
$email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data')
->setConstructorArgs(array($this->event_id))
->setMethods(array('get_attendees'))
->getMock();
// Mock attendee data
$mock_attendees = array(
array(
'name' => 'Test Attendee 1',
'email' => 'attendee1@example.com',
'ticket_name' => 'General Admission',
'attendee_id' => 101,
'order_id' => 1001,
),
array(
'name' => 'Test Attendee 2',
'email' => 'attendee2@example.com',
'ticket_name' => 'VIP Pass',
'attendee_id' => 102,
'order_id' => 1002,
),
array(
'name' => 'Test Attendee 3',
'email' => 'attendee3@example.com',
'ticket_name' => 'General Admission',
'attendee_id' => 103,
'order_id' => 1003,
),
);
// Configure mock to return test data
$email_data->expects($this->any())
->method('get_attendees')
->will($this->returnValue($mock_attendees));
// Use reflection to access the get_ticket_types method
$reflection = new ReflectionClass('HVAC_Email_Attendees_Data');
$method = $reflection->getMethod('get_ticket_types');
$method->setAccessible(true);
// Test getting ticket types
$ticket_types = $method->invoke($email_data);
$this->assertIsArray($ticket_types);
$this->assertCount(2, $ticket_types);
$this->assertContains('General Admission', $ticket_types);
$this->assertContains('VIP Pass', $ticket_types);
}
/**
* Test send_email method with mocked wp_mail
*/
public function test_send_email() {
global $wp_mail_called, $wp_mail_args;
// Override wp_mail for testing
$wp_mail_called = 0;
$wp_mail_args = array();
function test_wp_mail($to, $subject, $message, $headers = '', $attachments = array()) {
global $wp_mail_called, $wp_mail_args;
$wp_mail_called++;
$wp_mail_args[] = array(
'to' => $to,
'subject' => $subject,
'message' => $message,
'headers' => $headers,
'attachments' => $attachments,
);
return true;
}
// Replace WordPress's wp_mail with our test version
add_filter('wp_mail', 'test_wp_mail', 10, 5);
// Create a test instance
$email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data')
->setConstructorArgs(array($this->event_id))
->setMethods(array('is_valid_event', 'user_can_email_attendees', 'get_attendees', 'get_event_details'))
->getMock();
// Mock necessary methods
$email_data->expects($this->any())
->method('is_valid_event')
->will($this->returnValue(true));
$email_data->expects($this->any())
->method('user_can_email_attendees')
->will($this->returnValue(true));
$email_data->expects($this->any())
->method('get_event_details')
->will($this->returnValue(array(
'id' => $this->event_id,
'title' => 'Test Event for Email Attendees',
'start_date' => date('F j, Y'),
'start_time' => '10:00 am',
'end_date' => date('F j, Y'),
'end_time' => '4:00 pm',
)));
$mock_attendees = array(
array(
'name' => 'Test Attendee 1',
'email' => 'attendee1@example.com',
'ticket_name' => 'General Admission',
'attendee_id' => 101,
'order_id' => 1001,
),
array(
'name' => 'Test Attendee 2',
'email' => 'attendee2@example.com',
'ticket_name' => 'VIP Pass',
'attendee_id' => 102,
'order_id' => 1002,
),
);
$email_data->expects($this->any())
->method('get_attendees')
->will($this->returnValue($mock_attendees));
// Test sending email
$recipients = array('attendee1@example.com', 'attendee2@example.com');
$subject = 'Test Email Subject';
$message = 'Test email message body';
$cc = 'cc@example.com';
$result = $email_data->send_email($recipients, $subject, $message, $cc);
// Verify email was sent successfully
$this->assertTrue($result['success']);
$this->assertEquals(2, $wp_mail_called); // One email per recipient
// Verify email content
$this->assertEquals('attendee1@example.com', $wp_mail_args[0]['to']);
$this->assertStringContains('[Test Event for Email Attendees]', $wp_mail_args[0]['subject']);
$this->assertStringContains('Test email message body', $wp_mail_args[0]['message']);
// Verify CC header
foreach ($wp_mail_args as $args) {
$this->assertStringContains('Cc: cc@example.com', implode("\n", $args['headers']));
}
// Test with invalid parameters
$result = $email_data->send_email(array(), $subject, $message);
$this->assertFalse($result['success']);
// Clean up
remove_filter('wp_mail', 'test_wp_mail');
}
}