A mail client resolves no attribute, so the JSON's top level — the standard roles — is what it wears, whatever levels the file carries beside them. Plan: docs/plans/material-3-alignment.md, step 7 (core C2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
245 lines
8.6 KiB
PHP
245 lines
8.6 KiB
PHP
<?php
|
|
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Markdown;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\View;
|
|
use Illuminate\Support\Str;
|
|
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
|
|
use NoNameWeb\LivewireMaterial\Support\Scheme;
|
|
|
|
class ThemedProbeMail extends Mailable
|
|
{
|
|
public function content(): Content
|
|
{
|
|
return new Content(markdown: 'mail-theme-probe');
|
|
}
|
|
}
|
|
|
|
beforeEach(function () {
|
|
$this->temporary = sys_get_temp_dir().'/livewire-material-mail-'.Str::random(8);
|
|
File::ensureDirectoryExists($this->temporary);
|
|
|
|
File::put($this->temporary.'/mail-theme-probe.blade.php', <<<'BLADE'
|
|
<x-mail::message>
|
|
# Your export is ready
|
|
|
|
The report has **finished**.
|
|
|
|
<x-mail::button :url="'https://example.com/export'">
|
|
Download
|
|
</x-mail::button>
|
|
|
|
<x-mail::panel>
|
|
Files expire after seven days.
|
|
</x-mail::panel>
|
|
|
|
<x-mail::table>
|
|
| File | Size |
|
|
|:-----|-----:|
|
|
| orders.csv | 1.2 MB |
|
|
</x-mail::table>
|
|
</x-mail::message>
|
|
BLADE);
|
|
|
|
File::put($this->temporary.'/mail-footer-probe.blade.php', <<<'BLADE'
|
|
<x-mail::message>
|
|
Hello.
|
|
|
|
<x-slot:footer>
|
|
Sent by the probe · [Unsubscribe](https://example.com/unsubscribe)
|
|
</x-slot:footer>
|
|
</x-mail::message>
|
|
BLADE);
|
|
|
|
View::addLocation($this->temporary);
|
|
|
|
config(['mail.markdown.theme' => 'livewire-material::mail.theme']);
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::deleteDirectory($this->temporary);
|
|
});
|
|
|
|
/**
|
|
* The inline style of the first element carrying the class.
|
|
*/
|
|
function inlineStyle(string $html, string $class): string
|
|
{
|
|
preg_match('/<[^>]+class="[^"]*\b'.preg_quote($class, '/').'\b[^"]*"[^>]*style="([^"]*)"/', $html, $match);
|
|
|
|
return $match[1] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Put the package's mail components on `mail.markdown.paths`, as the provider does when
|
|
* `livewire-material.mail.components` is on at boot, and make the renderer again.
|
|
*/
|
|
function withPackageMailComponents(): void
|
|
{
|
|
config(['livewire-material.mail.components' => true]);
|
|
|
|
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
|
|
(fn () => $this->registerMailComponents())->call($provider);
|
|
|
|
app()->forgetInstance(Markdown::class);
|
|
}
|
|
|
|
it('stays on the standard contrast level, which is the one a mail client can show', function () {
|
|
File::put($this->temporary.'/material-scheme.json', json_encode([
|
|
'light' => ['primary' => '#123456', 'on-primary' => '#fefefe', 'surface-container' => '#eeeeee', 'surface-container-lowest' => '#fdfdfd'],
|
|
'dark' => ['primary' => '#abcdef'],
|
|
'contrast' => [
|
|
'standard' => 0,
|
|
'high' => ['light' => ['primary' => '#000000', 'on-primary' => '#ffffff'], 'dark' => ['primary' => '#ffffff']],
|
|
],
|
|
]));
|
|
|
|
config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']);
|
|
|
|
expect(inlineStyle((new ThemedProbeMail)->render(), 'button-primary'))
|
|
->toContain('background-color: #123456')
|
|
->not->toContain('background-color: #000000');
|
|
});
|
|
|
|
it('inlines the application\'s scheme onto the mail', function () {
|
|
File::put($this->temporary.'/material-scheme.json', json_encode([
|
|
'light' => ['primary' => '#123456', 'on-primary' => '#fefefe', 'surface-container' => '#eeeeee', 'surface-container-lowest' => '#fdfdfd'],
|
|
'dark' => ['primary' => '#abcdef'],
|
|
]));
|
|
|
|
config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']);
|
|
|
|
$html = (new ThemedProbeMail)->render();
|
|
|
|
expect(inlineStyle($html, 'button-primary'))
|
|
->toContain('background-color: #123456')
|
|
->toContain('border-top: 16px solid #123456')
|
|
->toContain('color: #fefefe')
|
|
->toContain('border-radius: 9999px')
|
|
->and(inlineStyle($html, 'inner-body'))->toContain('background-color: #fdfdfd')
|
|
->and(inlineStyle($html, 'wrapper'))->toContain('background-color: #eeeeee')
|
|
->and(inlineStyle($html, 'panel-content'))->toContain('background-color: #eeeeee')
|
|
->and($html)
|
|
->toMatch('/<h1 style="[^"]*font-size: 24px[^"]*line-height: 32px/')
|
|
->toMatch('/<p style="[^"]*font-size: 16px[^"]*letter-spacing: 0.5px/')
|
|
->toMatch('/<th [^>]*style="[^"]*font-weight: 500/')
|
|
->toMatch('/<th align="right" style="[^"]*text-align: right/')
|
|
->not->toContain('#abcdef')
|
|
->not->toContain('var(--')
|
|
->not->toContain('color-mix');
|
|
});
|
|
|
|
it('inlines the active colour profile onto the mail', function () {
|
|
File::put($this->temporary.'/material-scheme.json', json_encode([
|
|
'light' => ['primary' => '#4f46e5'],
|
|
'dark' => ['primary' => '#aaaaff'],
|
|
'default' => 'indigo',
|
|
'profiles' => [
|
|
'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5'], 'dark' => ['primary' => '#aaaaff']],
|
|
'rose' => ['label' => 'Rose', 'light' => ['primary' => '#c2185b'], 'dark' => ['primary' => '#ffb0c8']],
|
|
],
|
|
]));
|
|
|
|
config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']);
|
|
Scheme::resolveProfileUsing(fn (): string => 'rose');
|
|
|
|
try {
|
|
expect(inlineStyle((new ThemedProbeMail)->render(), 'button-primary'))->toContain('background-color: #c2185b');
|
|
} finally {
|
|
Scheme::resolveProfileUsing(null);
|
|
}
|
|
});
|
|
|
|
it('takes the theme from a mailable too', function () {
|
|
config(['mail.markdown.theme' => 'default']);
|
|
|
|
$mail = new ThemedProbeMail;
|
|
$mail->theme = 'livewire-material::mail.theme';
|
|
|
|
expect(inlineStyle($mail->render(), 'button-primary'))->toContain('border-radius: 9999px');
|
|
});
|
|
|
|
it('falls back to the package\'s default scheme', function (?string $contents) {
|
|
$path = $this->temporary.'/material-scheme.json';
|
|
|
|
if ($contents !== null) {
|
|
File::put($path, $contents);
|
|
}
|
|
|
|
config(['livewire-material.scheme' => $path]);
|
|
|
|
$default = json_decode(File::get(__DIR__.'/../../resources/css/tokens/scheme.json'), true);
|
|
|
|
expect(inlineStyle((new ThemedProbeMail)->render(), 'button-primary'))
|
|
->toContain("background-color: {$default['light']['primary']}");
|
|
})->with([
|
|
'no scheme file' => [null],
|
|
'not JSON' => ['{ nope'],
|
|
'not a colour' => [json_encode(['light' => ['primary' => 'red; background: url(x)']])],
|
|
]);
|
|
|
|
it('styles every button colour Laravel and M3 name', function () {
|
|
$css = view('livewire-material::mail.theme')->render();
|
|
|
|
foreach (['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info', 'blue', 'green', 'red'] as $color) {
|
|
expect($css)->toContain(".button-{$color} {");
|
|
}
|
|
|
|
expect($css)->not->toContain('@media');
|
|
});
|
|
|
|
it('leaves the application\'s mail components alone unless asked', function () {
|
|
expect(config('mail.markdown.paths'))->not->toContain(LivewireMaterialServiceProvider::mailComponentPath());
|
|
|
|
withPackageMailComponents();
|
|
withPackageMailComponents();
|
|
|
|
expect(config('mail.markdown.paths'))->toBe([
|
|
resource_path('views/vendor/mail'),
|
|
LivewireMaterialServiceProvider::mailComponentPath(),
|
|
]);
|
|
});
|
|
|
|
it('renders the package header: the app name, never Laravel\'s logo', function () {
|
|
config(['app.name' => 'Laravel']);
|
|
|
|
$framework = (new ThemedProbeMail)->render();
|
|
|
|
withPackageMailComponents();
|
|
|
|
$html = (new ThemedProbeMail)->render();
|
|
|
|
expect($framework)->toContain('notification-logo')
|
|
->and($html)->not->toContain('notification-logo')
|
|
->toMatch('/<td class="header"[^>]*>\s*<a [^>]*>\s*Laravel\s*<\/a>/');
|
|
});
|
|
|
|
it('renders a configured logo in the header, sized by attributes', function () {
|
|
config(['app.name' => 'Probe', 'livewire-material.mail.logo' => ['src' => 'https://example.com/logo.png', 'width' => 160, 'height' => 40]]);
|
|
|
|
withPackageMailComponents();
|
|
|
|
expect((new ThemedProbeMail)->render())
|
|
->toMatch('/<img src="https:\/\/example.com\/logo.png" class="logo" alt="Probe" width="160" height="40"/');
|
|
});
|
|
|
|
it('lets a mail replace the footer, in HTML and in text', function () {
|
|
config(['app.name' => 'Probe']);
|
|
|
|
withPackageMailComponents();
|
|
|
|
$markdown = app(Markdown::class);
|
|
|
|
expect((string) $markdown->render('mail-footer-probe'))
|
|
->toContain('Sent by the probe')
|
|
->toContain('href="https://example.com/unsubscribe"')
|
|
->not->toContain('All rights reserved.')
|
|
->and((string) $markdown->renderText('mail-footer-probe'))
|
|
->toContain('Sent by the probe')
|
|
->not->toContain('All rights reserved.')
|
|
->and((string) $markdown->render('mail-theme-probe'))
|
|
->toContain('© '.date('Y').' Probe. All rights reserved.');
|
|
});
|