The About page addresses the reader formally and speaks as "we"; the
landing pages used informal second-person singular ("تعریف میکنی") and
the home hero was a slogan. Both now match the About tone.
Landing copy: all informal verb forms and possessives converted to the
formal plural. Home: hero headline and lead rewritten as a plain
statement of what we built, plus small wording passes on the devices,
specialties and stats blocks.
The home-page smoke assertion follows the new headline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
6.4 KiB
PHP
161 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Shared;
|
|
|
|
use App\Shared\Landing\LandingRegistry;
|
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
|
|
/**
|
|
* صفحات فرود واقعاً رندر میشوند و متادیتای هرکدام مالِ خودش است.
|
|
*
|
|
* روت `/{slug}` هر مسیر تکبخشی را میگیرد، پس اینجا هم بررسی میشود که صفحهٔ اصلی و
|
|
* مسیرهای موجود را نخورده باشد.
|
|
*/
|
|
class LandingPageTest extends WebTestCase
|
|
{
|
|
private KernelBrowser $client;
|
|
private LandingRegistry $registry;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->client = static::createClient();
|
|
$this->registry = new LandingRegistry();
|
|
}
|
|
|
|
private function html(string $path): string
|
|
{
|
|
$this->client->request('GET', $path);
|
|
|
|
return $this->client->getResponse()->getContent();
|
|
}
|
|
|
|
public function testEveryLandingRenders(): void
|
|
{
|
|
foreach ($this->registry->all() as $slug => $page) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode(), "صفحهٔ «{$slug}» باز نشد");
|
|
self::assertStringContainsString('<h1 class="reveal">' . $page->h1 . '</h1>', $html);
|
|
self::assertStringContainsString('<title>' . $page->metaTitle . '</title>', $html);
|
|
self::assertStringContainsString('content="' . $page->metaDescription . '"', $html);
|
|
}
|
|
}
|
|
|
|
public function testCanonicalPointsAtTheSamePage(): void
|
|
{
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
self::assertMatchesRegularExpression(
|
|
'~<link rel="canonical" href="[^"]+/' . preg_quote($slug, '~') . '"/>~u',
|
|
$html,
|
|
"canonical صفحهٔ «{$slug}» به خودش اشاره نمیکند",
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testUnknownSlugIsNotFound(): void
|
|
{
|
|
$this->client->request('GET', '/یک-اسلاگ-که-وجود-ندارد');
|
|
|
|
self::assertSame(404, $this->client->getResponse()->getStatusCode());
|
|
}
|
|
|
|
/** روت catch-all نباید صفحهٔ اصلی را بخورد. */
|
|
public function testHomePageStillWorks(): void
|
|
{
|
|
$html = $this->html('/');
|
|
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
|
self::assertStringContainsString('کارِ مطب،', $html);
|
|
}
|
|
|
|
public function testExistingRoutesAreNotShadowed(): void
|
|
{
|
|
foreach (['/sitemap.xml', '/health'] as $path) {
|
|
$this->client->request('GET', $path);
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode(), "{$path} گرفته شد");
|
|
}
|
|
}
|
|
|
|
public function testSitemapListsHomeAndEveryLanding(): void
|
|
{
|
|
$xml = $this->html('/sitemap.xml');
|
|
$doc = new \SimpleXMLElement($xml);
|
|
|
|
// خانه + دربارهٔ ما + هر صفحهٔ فرود. عدد از خودِ رجیستری میآید تا افزودن خوشهٔ
|
|
// کلیدواژهٔ تازه این تست را نشکند، ولی جاافتادن یک صفحه از sitemap هنوز قرمز شود.
|
|
self::assertCount(2 + count($this->registry->all()), $doc->url);
|
|
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
self::assertStringContainsString(rawurlencode($slug), $xml, "«{$slug}» در sitemap نیست");
|
|
}
|
|
}
|
|
|
|
/** بدون لینک داخلی، صفحات فقط از sitemap دیده میشوند. */
|
|
public function testFooterLinksToTheFeaturedLandingsOnEveryPage(): void
|
|
{
|
|
foreach (['/', '/' . LandingRegistry::CRM] as $path) {
|
|
$html = $this->html($path);
|
|
|
|
foreach ($this->registry->featured() as $page) {
|
|
self::assertStringContainsString('href="/' . $page->slug . '"', $html, "لینک «{$page->slug}» در {$path} نیست");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* فوتر دیگر همهٔ صفحهها را فهرست نمیکند، پس باید ثابت بماند که از همان چند
|
|
* لینکِ فوتر، با دنبالکردن «راهکارهای دیگر»، به هر صفحه میرسیم. صفحهای که فقط
|
|
* در sitemap باشد عملاً بدون لینک داخلی است و رتبه نمیگیرد.
|
|
*/
|
|
public function testEveryLandingIsReachableFromTheFooterLinks(): void
|
|
{
|
|
$pages = $this->registry->all();
|
|
$queue = array_map(static fn ($page): string => $page->slug, $this->registry->featured());
|
|
$reached = array_flip($queue);
|
|
|
|
while ($queue !== []) {
|
|
$slug = array_shift($queue);
|
|
foreach ($pages[$slug]->related as $next) {
|
|
if (!isset($reached[$next])) {
|
|
$reached[$next] = true;
|
|
$queue[] = $next;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($pages) as $slug) {
|
|
self::assertArrayHasKey($slug, $reached, "«{$slug}» از هیچ صفحهای لینک نمیگیرد");
|
|
}
|
|
}
|
|
|
|
public function testStructuredDataIsValidJson(): void
|
|
{
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
preg_match('~<script type="application/ld\+json">(.*?)</script>~s', $html, $m);
|
|
self::assertNotEmpty($m, "صفحهٔ «{$slug}» بلاک JSON-LD ندارد");
|
|
|
|
$data = json_decode(html_entity_decode($m[1]), true, flags: JSON_THROW_ON_ERROR);
|
|
$types = array_column($data['@graph'], '@type');
|
|
|
|
self::assertContains('SoftwareApplication', $types);
|
|
self::assertContains('BreadcrumbList', $types);
|
|
self::assertContains('FAQPage', $types);
|
|
}
|
|
}
|
|
|
|
/** تم مشترک است، پس همان CSS صفحهٔ اصلی باید بارگذاری شود. */
|
|
public function testLandingUsesTheHomepageTheme(): void
|
|
{
|
|
preg_match_all('~href="(/build/[^"]+\.css)"~', $this->html('/'), $home);
|
|
preg_match_all('~href="(/build/[^"]+\.css)"~', $this->html('/' . LandingRegistry::BEAUTY), $landing);
|
|
|
|
self::assertNotEmpty($home[1]);
|
|
self::assertSame($home[1], $landing[1], 'لندینگ باید همان CSS صفحهٔ اصلی را بارگذاری کند');
|
|
}
|
|
}
|