CluPilotCloud/tests/Feature/SiteDesignSystemTest.php

220 lines
9.8 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
/**
* The public website is one of the three surfaces the token file describes.
*
* It was the last one still speaking the old language: a 900-line page with its
* own design system inside a <style> block — warm paper, a serif headline, 3px
* corners, its own scale of greys — written before the tokens existed. A
* visitor who clicked "Anmelden" left one company and arrived at another.
*
* These tests are about drift, not about looks. Nothing stops someone pasting a
* hex value into a marketing page at four in the afternoon; what stops it
* becoming a second design system is noticing on the first one.
*/
/** Public pages that must draw from the shared stylesheet. */
function sitePages(): array
{
return [
'landing.blade.php',
'legal.blade.php',
'status.blade.php',
'components/layouts/site.blade.php',
];
}
it('builds the public site from the shared tokens, not from its own palette', function (string $page) {
$source = File::get(resource_path('views/'.$page));
// Hex literals are the tell. One is a paste; a dozen is a second design
// system growing quietly beside the first.
preg_match_all('/#[0-9a-fA-F]{3,8}\b/', $source, $found);
expect($found[0])->toBeEmpty()
// The old page set its own type stack. The token file owns that now,
// and a serif headline is precisely the impression the redesign
// removed — every page read as a document.
->and($source)->not->toContain('font-family')
->and($source)->not->toContain('Georgia')
->and($source)->not->toContain('font-serif');
})->with(sitePages());
it('serves the website from the same stylesheet as the portal', function () {
// Not "a stylesheet exists" — the SAME one. Two builds would drift the
// moment a token changed in one of them.
$layout = File::get(resource_path('views/components/layouts/site.blade.php'));
expect($layout)->toContain('<x-shell.head');
$head = File::get(resource_path('views/components/shell/head.blade.php'));
expect($head)->toContain("@vite(['resources/css/app.css'");
});
it('keeps the deliberately self-contained page self-contained', function () {
// The exception, named rather than forgotten. coming-soon is shown when the
// application may not be able to serve a built stylesheet at all — mid
// deploy, or freshly installed — and @vite would throw where the whole
// point is to render something reassuring.
//
// The status page used to be on this list on the same theory and does not
// belong: if the stylesheet cannot be served, the application serving the
// status page is already failing, and mid-deployment every route answers
// with the maintenance page. What the exemption actually bought was a third
// design nobody maintained.
$source = File::get(resource_path('views/coming-soon.blade.php'));
// The CALL, not the word: the file explains in a comment why it does not
// use @vite, and a plain substring match reads that explanation as the
// offence it warns about.
expect($source)->not->toMatch('/@vite\s*\(/')
->and($source)->toContain('<style>');
});
it('does not leave the old marketing stylesheet behind anywhere', function () {
// The tell of the previous edition: warm paper and the 3px radius. Left in
// a partial nobody opened, it would come back the next time somebody
// copied a section.
foreach (File::allFiles(resource_path('views')) as $file) {
if ($file->getFilename() === 'coming-soon.blade.php') {
continue;
}
// The mail layout is deliberately inline-styled: an email client has no
// stylesheet of ours to load.
if (str_contains($file->getPathname(), '/mail/') || str_contains($file->getPathname(), '/pdf/')) {
continue;
}
expect(File::get($file->getPathname()))
->not->toContain('--paper')
->not->toContain('Plex Serif');
}
});
it('does not space the wordmark out into separate words', function () {
// It went out reading "Clu Pilot Cloud". `gap` on a flex container applies
// between EVERY child, and a bare text node is a child — so an inline-flex
// row wrapped around "Clu", <i>Pilot</i> and " Cloud" put nine pixels
// between each of them.
//
// The rule that prevents it: the mark and the wordmark are the only two
// children of the flex row, and the wordmark is one element.
$source = File::get(resource_path('views/coming-soon.blade.php'));
expect($source)->toContain('<span class="word">CluPilot')
->and($source)->not->toContain('Clu<i>Pilot</i>');
});
it('draws the actual mark on the placeholder, not an empty tile', function () {
// It was a plain gradient square. A brand tile with nothing in it reads as
// an image that failed to load — on a "we are still building" page that is
// the last impression to leave.
//
// Inlined rather than pulled from x-ui.logo: this page renders when the
// asset build may not exist, and a Blade component is fine but the SVG has
// to be in the file either way.
$source = File::get(resource_path('views/coming-soon.blade.php'));
expect($source)->toContain('viewBox="0 0 64 64"')
// The ascent triangle — the part that makes it a mark rather than a
// rounded rectangle.
->and($source)->toContain('M32 17 44 44l-12-6-12 6z');
});
it('draws the wordmark from one definition, not five', function () {
// It had five: the site header set it in ink, the sidebar split it into
// "Clu" plus an orange "Pilot", the sign-in plate set it white at another
// size, the placeholder inlined its own, and the maintenance screen had no
// wordmark at all. Five surfaces of one product, each with its own idea of
// the brand.
$offenders = [];
foreach (File::allFiles(resource_path('views')) as $file) {
// The two self-contained pages cannot use a Blade component's compiled
// output — they render when the asset build may not exist — and the
// component itself obviously spells the word out.
// The self-contained pages cannot use a Blade component's compiled
// output — they render when the asset build may not exist — and an
// email has no stylesheet of ours at all, so its lockup is inline by
// necessity. They still have to AGREE with the component, which is
// what the next assertion checks.
if (in_array($file->getFilename(), ['coming-soon.blade.php', 'updating-panel.blade.php', 'brand.blade.php', 'layout.blade.php'], true)) {
continue;
}
$source = preg_replace('/\{\{--.*?--\}\}/s', '', File::get($file->getPathname()));
// The wordmark is markup around the name; a plain sentence mentioning
// the company is not. `>Clu` with NO whitespace after the tag catches
// exactly the lockup forms — the name is the whole content of its
// element there. Whitespace used to be allowed, and then the terms page
// arrived with a paragraph whose first word is the company name, which
// is a sentence and not a brand lockup.
if (preg_match('/>Clu(Pilot|<)/', $source)) {
$offenders[] = str_replace(resource_path().'/', '', $file->getPathname());
}
}
expect($offenders)->toBeEmpty();
// The four that must inline it write the name in one piece, in ink, with
// no orange half — the shape the component defines.
foreach ([
'coming-soon.blade.php',
'partials/updating-panel.blade.php',
'errors/layout.blade.php',
'components/mail/layout.blade.php',
] as $page) {
expect(File::get(resource_path('views/'.$page)))
->toContain('CluPilot')
->not->toContain('Clu<i>Pilot</i>')
->not->toContain('>Clu</span>');
}
});
it('gives the maintenance screen the same typeface as everything else', function () {
// The console loads IBM Plex through Vite. The 503 page has no stylesheet
// at all, so the maintenance screen — the one page shown when somebody is
// already worried — was rendering in the system sans.
$panel = File::get(resource_path('views/partials/updating-panel.blade.php'));
expect($panel)->toContain('@font-face')
->and($panel)->toContain('/fonts/ibm-plex-sans-latin-700-normal.woff2');
// And the file it names is actually there. A @font-face pointing at
// nothing fails silently, which is how the bold weight went missing from
// the placeholder without anyone noticing.
foreach (['400', '500', '600', '700'] as $weight) {
expect(public_path("fonts/ibm-plex-sans-latin-{$weight}-normal.woff2"))->toBeFile();
}
});
it('names the address zone the installation is configured for', function () {
// It said "Adresse auf clupilot.cloud" — a domain the company does not own
// — while provisioning issued addresses under whatever CLUPILOT_DNS_ZONE
// said. The same two-sources-of-truth mistake as a hard-coded price.
App\Support\Settings::set('provisioning.dns_zone', 'example.test');
// Comments stripped: the controller explains at length that it used to
// name clupilot.cloud, and a raw scan reads the explanation as the
// offence. Third time this suite has had to learn it.
$source = implode('', array_map(
fn ($token) => is_array($token) && in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) ? '' : (is_array($token) ? $token[1] : $token),
token_get_all(File::get(app_path('Http/Controllers/LandingController.php'))),
));
expect($source)->not->toContain('clupilot.cloud');
$page = $this->get('/');
$page->assertOk();
// Only where the catalogue actually sells the feature; either way the old
// literal must be gone.
$page->assertDontSee('clupilot.cloud');
});