Plan step 19, containment.md C-27 and C-28. `.inner-body` was 24px, a value not on M3's 0/4/8/12/16/20/28/32/48 scale; it is 28px, the extra-large corner of the dialog and bottom-sheet surface it stands in for. `.header a` is title-large at weight 400, which its own section comment and the `h2` under it already said. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
258 lines
9.2 KiB
PHP
258 lines
9.2 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')->toContain('border-radius: 28px')
|
|
->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('keeps every corner and weight on M3\'s scales', function () {
|
|
$css = view('livewire-material::mail.theme')->render();
|
|
|
|
preg_match_all('/border-radius: (\d+)px/', $css, $corners);
|
|
|
|
// M3's shape scale, plus the `full` corner a button draws as a large pixel value.
|
|
expect(array_unique($corners[1]))->each->toBeIn(['0', '4', '8', '12', '16', '20', '28', '32', '48', '9999'])
|
|
->and($css)->toContain(<<<'CSS'
|
|
.header a {
|
|
CSS)
|
|
->toMatch('/\.header a \{[^}]*font-weight: 400/');
|
|
});
|
|
|
|
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.');
|
|
});
|