- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
355 lines
11 KiB
PHP
355 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Email\CssInliner;
|
|
use App\Framework\Email\EmailContext;
|
|
use App\Framework\Email\EmailTemplateRenderer;
|
|
use App\Framework\Email\ValueObjects\EmailContent;
|
|
use App\Framework\Email\ValueObjects\EmailSubject;
|
|
use App\Framework\Template\Parser\DomTemplateParser;
|
|
|
|
beforeEach(function () {
|
|
$this->parser = new DomTemplateParser();
|
|
$this->cssInliner = new CssInliner($this->parser);
|
|
$this->renderer = new EmailTemplateRenderer($this->parser, $this->cssInliner);
|
|
});
|
|
|
|
describe('CssInliner', function () {
|
|
|
|
it('inlines CSS from style tags', function () {
|
|
$html = '
|
|
<html>
|
|
<head>
|
|
<style>
|
|
.button { background-color: red; padding: 10px; }
|
|
p { color: blue; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<p>Hello</p>
|
|
<a class="button">Click me</a>
|
|
</body>
|
|
</html>
|
|
';
|
|
|
|
$result = $this->cssInliner->inline($html);
|
|
|
|
expect($result)->toContain('style="color: blue"');
|
|
expect($result)->toContain('style="background-color: red; padding: 10px"');
|
|
});
|
|
|
|
it('handles multiple selectors', function () {
|
|
$html = '
|
|
<html>
|
|
<head>
|
|
<style>
|
|
h1, h2, h3 { color: green; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Title 1</h1>
|
|
<h2>Title 2</h2>
|
|
<h3>Title 3</h3>
|
|
</body>
|
|
</html>
|
|
';
|
|
|
|
$result = $this->cssInliner->inline($html);
|
|
|
|
expect($result)->toContain('<h1 style="color: green">');
|
|
expect($result)->toContain('<h2 style="color: green">');
|
|
expect($result)->toContain('<h3 style="color: green">');
|
|
});
|
|
|
|
it('preserves existing inline styles with higher priority', function () {
|
|
$html = '
|
|
<html>
|
|
<head>
|
|
<style>
|
|
p { color: blue; font-size: 14px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<p style="color: red;">Text</p>
|
|
</body>
|
|
</html>
|
|
';
|
|
|
|
$result = $this->cssInliner->inline($html);
|
|
|
|
// Existing inline style should win
|
|
expect($result)->toContain('color: red');
|
|
// But new property should be added
|
|
expect($result)->toContain('font-size: 14px');
|
|
});
|
|
|
|
it('removes !important from inline styles', function () {
|
|
$html = '
|
|
<html>
|
|
<head>
|
|
<style>
|
|
.urgent { color: red !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<p class="urgent">Alert</p>
|
|
</body>
|
|
</html>
|
|
';
|
|
|
|
$result = $this->cssInliner->inline($html);
|
|
|
|
expect($result)->toContain('style="color: red"');
|
|
expect($result)->not->toContain('!important');
|
|
});
|
|
});
|
|
|
|
describe('EmailContext', function () {
|
|
|
|
it('implements TemplateContext interface', function () {
|
|
$context = new EmailContext(
|
|
data: ['name' => 'John', 'email' => 'john@example.com']
|
|
);
|
|
|
|
expect($context->getData())->toBe(['name' => 'John', 'email' => 'john@example.com']);
|
|
expect($context->get('name'))->toBe('John');
|
|
expect($context->get('missing', 'default'))->toBe('default');
|
|
expect($context->has('email'))->toBeTrue();
|
|
expect($context->has('missing'))->toBeFalse();
|
|
});
|
|
|
|
it('creates context with tracking parameters', function () {
|
|
$context = EmailContext::withTracking(
|
|
data: ['name' => 'John'],
|
|
source: 'newsletter',
|
|
campaign: 'welcome'
|
|
);
|
|
|
|
expect($context->utmParams)->toHaveKey('utm_source', 'newsletter');
|
|
expect($context->utmParams)->toHaveKey('utm_medium', 'email');
|
|
expect($context->utmParams)->toHaveKey('utm_campaign', 'welcome');
|
|
expect($context->trackingId)->toStartWith('email_');
|
|
});
|
|
});
|
|
|
|
describe('EmailContent', function () {
|
|
|
|
it('calculates size using Byte value object', function () {
|
|
$content = new EmailContent(
|
|
html: '<p>Hello World</p>', // 18 bytes
|
|
text: 'Hello World' // 11 bytes
|
|
);
|
|
|
|
$size = $content->getSize();
|
|
|
|
expect($size)->toBeInstanceOf(Byte::class);
|
|
expect($size->toBytes())->toBe(29);
|
|
});
|
|
|
|
it('detects multipart content', function () {
|
|
$withBoth = new EmailContent(
|
|
html: '<p>Hello</p>',
|
|
text: 'Hello'
|
|
);
|
|
|
|
$htmlOnly = new EmailContent(
|
|
html: '<p>Hello</p>',
|
|
text: ''
|
|
);
|
|
|
|
expect($withBoth->hasMultipart())->toBeTrue();
|
|
expect($htmlOnly->hasMultipart())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('EmailSubject', function () {
|
|
|
|
it('validates subject length', function () {
|
|
$subject = new EmailSubject('Valid subject');
|
|
expect($subject->value)->toBe('Valid subject');
|
|
expect($subject->isWithinRecommendedLength())->toBeTrue();
|
|
});
|
|
|
|
it('removes line breaks from subject', function () {
|
|
$subject = new EmailSubject("Subject with\nnewline");
|
|
expect($subject->value)->toBe('Subject with newline');
|
|
});
|
|
|
|
it('throws exception for empty subject', function () {
|
|
expect(fn () => new EmailSubject(' '))->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('creates preview with ellipsis', function () {
|
|
$longSubject = new EmailSubject('This is a very long subject that needs to be truncated');
|
|
expect($longSubject->getPreview(20))->toBe('This is a very lo...');
|
|
});
|
|
});
|
|
|
|
describe('EmailTemplateRenderer', function () {
|
|
|
|
it('renders template with variable replacement', function () {
|
|
// Create a test template
|
|
$templatePath = __DIR__ . '/test-email.html';
|
|
file_put_contents($templatePath, '
|
|
<html>
|
|
<head>
|
|
<title>Test Email</title>
|
|
<meta name="email-subject" content="Hello {{name}}!">
|
|
</head>
|
|
<body>
|
|
<p>Dear {{name}},</p>
|
|
<p>Your email is {{email}}</p>
|
|
</body>
|
|
</html>
|
|
');
|
|
|
|
$context = new EmailContext(
|
|
data: [
|
|
'name' => 'John Doe',
|
|
'email' => 'john@example.com',
|
|
]
|
|
);
|
|
|
|
$content = $this->renderer->render($templatePath, $context);
|
|
|
|
expect($content->html)->toContain('Dear John Doe,');
|
|
expect($content->html)->toContain('Your email is john@example.com');
|
|
expect($content->subject)->toBeInstanceOf(EmailSubject::class);
|
|
expect($content->subject->value)->toBe('Hello John Doe!');
|
|
expect($content->text)->toContain('Dear John Doe,');
|
|
|
|
// Clean up
|
|
unlink($templatePath);
|
|
});
|
|
|
|
it('adds UTM tracking parameters to links', function () {
|
|
$templatePath = __DIR__ . '/test-tracking.html';
|
|
file_put_contents($templatePath, '
|
|
<html>
|
|
<body>
|
|
<a href="https://example.com">Link 1</a>
|
|
<a href="https://example.com?foo=bar">Link 2</a>
|
|
<a href="#anchor">Anchor</a>
|
|
<a href="mailto:test@example.com">Email</a>
|
|
</body>
|
|
</html>
|
|
');
|
|
|
|
$context = EmailContext::withTracking(
|
|
data: [],
|
|
source: 'email',
|
|
campaign: 'test'
|
|
);
|
|
|
|
$content = $this->renderer->render($templatePath, $context);
|
|
|
|
expect($content->html)->toContain('https://example.com?utm_source=email');
|
|
expect($content->html)->toContain('https://example.com?foo=bar&utm_source=email');
|
|
expect($content->html)->toContain('href="#anchor"'); // Unchanged
|
|
expect($content->html)->toContain('href="mailto:test@example.com"'); // Unchanged
|
|
|
|
unlink($templatePath);
|
|
});
|
|
|
|
it('generates plain text from HTML', function () {
|
|
$templatePath = __DIR__ . '/test-plaintext.html';
|
|
file_put_contents($templatePath, '
|
|
<html>
|
|
<head>
|
|
<style>p { color: blue; }</style>
|
|
</head>
|
|
<body>
|
|
<h1>Title</h1>
|
|
<p>Paragraph 1</p>
|
|
<p>Paragraph 2</p>
|
|
<ul>
|
|
<li>Item 1</li>
|
|
<li>Item 2</li>
|
|
</ul>
|
|
<a href="https://example.com">Click here</a>
|
|
</body>
|
|
</html>
|
|
');
|
|
|
|
$context = new EmailContext();
|
|
$content = $this->renderer->render($templatePath, $context);
|
|
|
|
expect($content->text)->not->toContain('<');
|
|
expect($content->text)->not->toContain('>');
|
|
expect($content->text)->toContain("Title\n\n");
|
|
expect($content->text)->toContain("Paragraph 1\n\n");
|
|
expect($content->text)->toContain('• Item 1');
|
|
expect($content->text)->toContain('• Item 2');
|
|
expect($content->text)->toContain('Click here (https://example.com)');
|
|
|
|
unlink($templatePath);
|
|
});
|
|
|
|
it('adds preheader text', function () {
|
|
$templatePath = __DIR__ . '/test-preheader.html';
|
|
file_put_contents($templatePath, '
|
|
<html>
|
|
<body>
|
|
<p>Content</p>
|
|
</body>
|
|
</html>
|
|
');
|
|
|
|
$context = new EmailContext(
|
|
data: [],
|
|
preheader: 'This is a preview text'
|
|
);
|
|
|
|
$content = $this->renderer->render($templatePath, $context);
|
|
|
|
expect($content->html)->toContain('<body>');
|
|
expect($content->html)->toContain('display:none');
|
|
expect($content->html)->toContain('This is a preview text');
|
|
|
|
unlink($templatePath);
|
|
});
|
|
});
|
|
|
|
describe('Integration: Welcome Email Template', function () {
|
|
|
|
it('renders the welcome email template correctly', function () {
|
|
$context = new EmailContext(
|
|
data: [
|
|
'name' => 'Max Mustermann',
|
|
'email' => 'max@example.com',
|
|
'company_name' => 'Test Company',
|
|
'login_url' => 'https://app.example.com/login',
|
|
'current_year' => '2024',
|
|
'facebook_url' => 'https://facebook.com/testcompany',
|
|
'twitter_url' => 'https://twitter.com/testcompany',
|
|
'linkedin_url' => 'https://linkedin.com/company/testcompany',
|
|
'unsubscribe_url' => 'https://app.example.com/unsubscribe',
|
|
'webview_url' => 'https://app.example.com/email/view/123',
|
|
],
|
|
preheader: 'Willkommen bei Test Company!'
|
|
);
|
|
|
|
$content = $this->renderer->render('welcome', $context);
|
|
|
|
// Check HTML content
|
|
expect($content->html)->toContain('Hallo Max Mustermann,');
|
|
expect($content->html)->toContain('max@example.com');
|
|
expect($content->html)->toContain('Test Company');
|
|
expect($content->html)->toContain('https://app.example.com/login');
|
|
|
|
// Check subject extraction
|
|
expect($content->subject)->toBeInstanceOf(EmailSubject::class);
|
|
expect($content->subject->value)->toBe('Willkommen bei Test Company!');
|
|
|
|
// Check plain text generation
|
|
expect($content->text)->toContain('Hallo Max Mustermann,');
|
|
expect($content->text)->not->toContain('<div>');
|
|
expect($content->text)->not->toContain('<style>');
|
|
|
|
// Check CSS was inlined
|
|
expect($content->html)->toContain('style=');
|
|
});
|
|
});
|