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,105 @@
<?php
// newsletter-signup.php calls session_start() at load time — pre-initialise
// $_SESSION so the guard fires correctly without emitting headers.
$_SESSION = [];
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
$_SERVER['HTTP_HOST'] ??= 'localhost';
require_once CUSTOM_DIR . '/plugins/page/newsletter-signup.php';
// --- newsletterT ---
it('returns the fallback value when no global context is present', function () {
unset($GLOBALS['ctx']);
expect(newsletterT('name_label'))->toBe('Navn');
expect(newsletterT('submit_button'))->toBe('Meld deg på');
expect(newsletterT('success_message'))->toBe('Sjekk innboksen din!');
});
it('returns the key itself for an unknown key when no context is present', function () {
unset($GLOBALS['ctx']);
expect(newsletterT('nonexistent_key'))->toBe('nonexistent_key');
});
it('returns a translation from context when available', function () {
$GLOBALS['ctx'] = new class {
public function get(string $key, mixed $default = null): mixed
{
return $key === 'translations'
? ['newsletter.name_label' => 'Name']
: $default;
}
};
expect(newsletterT('name_label'))->toBe('Name');
unset($GLOBALS['ctx']);
});
it('falls back to the key when context has no matching translation', function () {
$GLOBALS['ctx'] = new class {
public function get(string $key, mixed $default = null): mixed
{
return $key === 'translations' ? [] : $default;
}
};
expect(newsletterT('name_label'))->toBe('name_label');
unset($GLOBALS['ctx']);
});
// --- newsletterCheckRateLimit ---
it('returns true when no previous submission is recorded in the session', function () {
unset($_SESSION['newsletter_last_submit']);
expect(newsletterCheckRateLimit())->toBeTrue();
});
it('returns false immediately after a submission', function () {
$_SESSION['newsletter_last_submit'] = time();
expect(newsletterCheckRateLimit())->toBeFalse();
});
it('returns false when 29 seconds have elapsed', function () {
$_SESSION['newsletter_last_submit'] = time() - 29;
expect(newsletterCheckRateLimit())->toBeFalse();
});
it('returns true when exactly 30 seconds have elapsed', function () {
$_SESSION['newsletter_last_submit'] = time() - 30;
expect(newsletterCheckRateLimit())->toBeTrue();
});
it('returns true when more than 30 seconds have elapsed', function () {
$_SESSION['newsletter_last_submit'] = time() - 60;
expect(newsletterCheckRateLimit())->toBeTrue();
});
// --- newsletterGetTranslations ---
it('returns all expected translation keys', function () {
unset($GLOBALS['ctx']);
expect(newsletterGetTranslations())->toHaveKeys([
'nameLabel', 'namePlaceholder', 'emailLabel', 'emailPlaceholder',
'notice', 'submitButton', 'successMessage', 'successConfirm', 'errorMessage',
]);
});
it('HTML-escapes special characters in translation values', function () {
$GLOBALS['ctx'] = new class {
public function get(string $key, mixed $default = null): mixed
{
return $key === 'translations'
? ['newsletter.name_label' => '<b>Name & "Label"</b>']
: $default;
}
};
$translations = newsletterGetTranslations();
expect($translations['nameLabel'])->toBe('&lt;b&gt;Name &amp; &quot;Label&quot;&lt;/b&gt;');
unset($GLOBALS['ctx']);
});
it('returns string values for all keys', function () {
unset($GLOBALS['ctx']);
foreach (newsletterGetTranslations() as $key => $value) {
expect($value)->toBeString("Key '{$key}' should be a string");
}
});