Add browser and unit test suite with Pest + Playwright

Add comprehensive test coverage for core site features (content,
navigation, language, FAQ, news, petition, newsletter) using Pest
browser tests and unit tests for custom plugins. Includes test
infrastructure (Containerfile.test, compose.test.yaml), test
documentation, and test files covering petition form logic, CSV
handling, translation, date formatting, rate limiting, and map data
building.
This commit is contained in:
Ruben 2026-03-17 16:43:39 +01:00
parent 0b61643ec5
commit b8e6d2537d
20 changed files with 1331 additions and 33 deletions

View file

@ -0,0 +1,162 @@
<?php
// PetitionFormTest.php (which loads first alphabetically) already required
// petition-form.php and defined Context / Hook / Hooks stubs.
// This file only adds tests — no re-requires or re-declarations needed.
/**
* Build a minimal Context mock that supports get('translations') and get('currentLang').
*/
function mockContext(array $translations = [], string $lang = 'no'): object
{
return new class($translations, $lang) extends Context {
public function __construct(
private array $translations,
private string $lang
) {}
public function get(string $key, mixed $default = null): mixed
{
return match ($key) {
'translations' => $this->translations,
'currentLang' => $this->lang,
default => $default,
};
}
};
}
// --- petitionT ---
it('returns the key when no translation is found', function () {
$ctx = mockContext([]);
expect(petitionT($ctx, 'petition', 'missing_key'))->toBe('missing_key');
});
it('returns the translated string when found', function () {
$ctx = mockContext(['petition.greeting' => 'Hei!']);
expect(petitionT($ctx, 'petition', 'greeting'))->toBe('Hei!');
});
it('applies placeholder replacements', function () {
$ctx = mockContext(['petition.email_greeting' => 'Hei {name}!']);
expect(petitionT($ctx, 'petition', 'email_greeting', ['name' => 'Kari']))->toBe('Hei Kari!');
});
it('applies multiple replacements in a single string', function () {
$ctx = mockContext(['petition.thanks' => 'Takk {name} for {title}']);
$result = petitionT($ctx, 'petition', 'thanks', ['name' => 'Ola', 'title' => 'kampanjen']);
expect($result)->toBe('Takk Ola for kampanjen');
});
it('uses the section as part of the translation key', function () {
$ctx = mockContext(['regions.oslo' => 'Oslo', 'petition.oslo' => 'Wrong']);
expect(petitionT($ctx, 'regions', 'oslo'))->toBe('Oslo');
});
// --- petitionGetIdFromPath ---
it('returns the basename of a path as the petition ID', function () {
expect(petitionGetIdFromPath('/content/underskriftskampanje/my-petition'))->toBe('my-petition');
});
it('strips a leading numeric prefix from the folder name', function () {
expect(petitionGetIdFromPath('/content/01-my-petition'))->toBe('my-petition');
expect(petitionGetIdFromPath('/content/001-my-petition'))->toBe('my-petition');
});
it('leaves non-prefixed names intact', function () {
expect(petitionGetIdFromPath('/content/medisinsk-cannabis'))->toBe('medisinsk-cannabis');
});
it('returns null when the basename is empty', function () {
expect(petitionGetIdFromPath('/'))->toBeNull();
});
// --- petitionFormatDate ---
it('formats a date in Norwegian by default', function () {
date_default_timezone_set('UTC');
$ctx = mockContext([], 'no');
// 2024-03-15 10:05:00 UTC
$ts = mktime(10, 5, 0, 3, 15, 2024);
expect(petitionFormatDate($ts, $ctx))->toBe('15. mars 2024, 10:05');
});
it('formats a date in English when lang is en', function () {
date_default_timezone_set('UTC');
$ctx = mockContext([], 'en');
$ts = mktime(10, 5, 0, 3, 15, 2024);
expect(petitionFormatDate($ts, $ctx))->toBe('March 15, 2024, 10:05');
});
it('uses Norwegian month names for an unknown language', function () {
date_default_timezone_set('UTC');
$ctx = mockContext([], 'fr');
$ts = mktime(0, 0, 0, 1, 1, 2024);
$result = petitionFormatDate($ts, $ctx);
expect($result)->toContain('januar');
});
it('uses the correct Norwegian month names', function () {
date_default_timezone_set('UTC');
$ctx = mockContext([], 'no');
$months = ['januar','februar','mars','april','mai','juni',
'juli','august','september','oktober','november','desember'];
foreach ($months as $i => $name) {
$ts = mktime(0, 0, 0, $i + 1, 1, 2024);
expect(petitionFormatDate($ts, $ctx))->toContain($name);
}
});
// --- petitionFormatSignature ---
it('shows only first name and region for semi display', function () {
$ctx = mockContext([
'petition.from_region' => 'fra',
'regions.oslo' => 'Oslo',
]);
$sig = ['firstname' => 'Kari', 'surname' => 'Hansen', 'region' => 'oslo', 'display' => 'semi'];
$result = petitionFormatSignature($sig, $ctx);
expect($result)->toBe('Kari fra Oslo');
expect($result)->not->toContain('Hansen');
});
it('shows full name and region for full display', function () {
$ctx = mockContext([
'petition.from_region' => 'fra',
'regions.oslo' => 'Oslo',
]);
$sig = ['firstname' => 'Kari', 'surname' => 'Hansen', 'region' => 'oslo', 'display' => 'full'];
expect(petitionFormatSignature($sig, $ctx))->toBe('Kari Hansen fra Oslo');
});
it('shows anonymous label for anonymous display', function () {
$ctx = mockContext([
'petition.from_region' => 'fra',
'petition.anonymous_name' => 'Anonym',
'regions.oslo' => 'Oslo',
]);
$sig = ['firstname' => 'Kari', 'surname' => 'Hansen', 'region' => 'oslo', 'display' => 'anonymous'];
$result = petitionFormatSignature($sig, $ctx);
expect($result)->toContain('Anonym');
expect($result)->not->toContain('Kari');
expect($result)->not->toContain('Hansen');
});
it('HTML-escapes names in full display mode', function () {
$ctx = mockContext([
'petition.from_region' => 'fra',
'regions.oslo' => 'Oslo',
]);
$sig = ['firstname' => '<b>Kari</b>', 'surname' => '<script>', 'region' => 'oslo', 'display' => 'full'];
$result = petitionFormatSignature($sig, $ctx);
expect($result)->toContain('&lt;b&gt;');
expect($result)->not->toContain('<b>');
});
it('falls back to the region key when no translation exists', function () {
$ctx = mockContext(['petition.from_region' => 'fra']);
$sig = ['firstname' => 'Ola', 'surname' => 'Nilsen', 'region' => 'trondelag', 'display' => 'semi'];
expect(petitionFormatSignature($sig, $ctx))->toContain('trondelag');
});