24-8: articles and legal pages — implemented, QA Review
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: 24-8
|
id: 24-8
|
||||||
title: 24-8 Articles and Legal Pages
|
title: 24-8 Articles and Legal Pages
|
||||||
status: Ready for Dev
|
status: QA Review
|
||||||
date: 2026-10-31
|
date: 2026-10-31
|
||||||
parent: 24
|
parent: 24
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Manual tests — 24-8 Articles and Legal Pages
|
||||||
|
|
||||||
|
Automated (passed 2026-10-31, `tests/Feature/Design/ArticlesLegalRedesignTest.php`, 9 tests; Design dir 106 incl. old `PostsTest`): `/posts` section head,
|
||||||
|
one h1, debounced search input, text-only post cards; search filters, shows the count line and clear action; empty-state text;
|
||||||
|
pagination wrapper present with 12 posts; article page meta/h1/prose/back link, unpublished post is 404; `/page/terms` shows title,
|
||||||
|
sanitised content (script stripped) and last-updated line; terms/privacy views one h1; `/admin/posts` loads no public assets.
|
||||||
|
UI loop (`/posts`, `/page/terms`): no horizontal scroll at 390/768/1440.
|
||||||
|
|
||||||
|
Ruling logged: `x-site.section-head` gained an optional `tag` prop (default `h2`); `/posts` and `/booking` pass `tag="h1"` so each page has exactly one h1 (the spec's own acceptance criterion).
|
||||||
|
|
||||||
|
- [ ] `/posts`: type in the search box; after a short pause the list filters and "N results" appears; Clear resets.
|
||||||
|
- [ ] With more than 10 published posts: pagination pills at the bottom, current page forest green.
|
||||||
|
- [ ] Open an article: date, large display heading, readable body, Back button.
|
||||||
|
- [ ] `/page/terms` and `/page/privacy`: white card with the CMS text and "last updated" line; check Arabic and English.
|
||||||
|
- [ ] 390px on each page: no sideways scroll.
|
||||||
@@ -144,6 +144,6 @@
|
|||||||
| 24-5 | 24-5 Home Booking Card (Compact Booking and Day Strip) | QA Review |
|
| 24-5 | 24-5 Home Booking Card (Compact Booking and Day Strip) | QA Review |
|
||||||
| 24-6 | 24-6 Booking Page and Booking Success | QA Review |
|
| 24-6 | 24-6 Booking Page and Booking Success | QA Review |
|
||||||
| 24-7 | 24-7 About Page | QA Review |
|
| 24-7 | 24-7 About Page | QA Review |
|
||||||
| 24-8 | 24-8 Articles and Legal Pages | Ready for Dev |
|
| 24-8 | 24-8 Articles and Legal Pages | QA Review |
|
||||||
| 24-9 | 24-9 Auth Pages Type and Controls | Ready for Dev |
|
| 24-9 | 24-9 Auth Pages Type and Controls | Ready for Dev |
|
||||||
| 24-10 | 24-10 Redesign Integration Verification | Ready for Dev |
|
| 24-10 | 24-10 Redesign Integration Verification | Ready for Dev |
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ html[dir=ltr] body.site .site-progress{transform-origin:left}
|
|||||||
body.site section{position:relative}
|
body.site section{position:relative}
|
||||||
body.site .sec{padding:120px 0}
|
body.site .sec{padding:120px 0}
|
||||||
body.site .sec-head{display:flex;align-items:end;justify-content:space-between;gap:32px;margin-bottom:54px;flex-wrap:wrap}
|
body.site .sec-head{display:flex;align-items:end;justify-content:space-between;gap:32px;margin-bottom:54px;flex-wrap:wrap}
|
||||||
body.site .sec-head h2{font-size:clamp(2rem,3.4vw,3rem);margin-top:12px}
|
body.site .sec-head h2,body.site .sec-head h1{font-size:clamp(2rem,3.4vw,3rem);margin-top:12px}
|
||||||
body.site .sec-head p{max-width:30em;color:var(--muted);margin:0}
|
body.site .sec-head p{max-width:30em;color:var(--muted);margin:0}
|
||||||
|
|
||||||
|
|
||||||
@@ -338,6 +338,26 @@ body.site .row-static:hover{background:none;color:inherit;transform:none;box-sha
|
|||||||
body.site .row-static::before{display:none}
|
body.site .row-static::before{display:none}
|
||||||
@media (max-width:760px){body.site .row-static{grid-template-columns:52px 1fr}}
|
@media (max-width:760px){body.site .row-static{grid-template-columns:52px 1fr}}
|
||||||
|
|
||||||
|
/* ---------- Articles and legal pages (24-8) ---------- */
|
||||||
|
body.site .search{min-height:48px;border-radius:999px;padding:12px 20px;border:1.5px solid rgba(45,54,36,.18);background:#fff;width:min(360px,100%);font:400 1rem var(--body);color:var(--ink);transition:border-color .2s,box-shadow .2s}
|
||||||
|
body.site .search:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 4px rgba(166,137,102,.2)}
|
||||||
|
body.site .search-info{color:var(--muted);margin:0 0 24px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
|
||||||
|
body.site .search-info .btn{min-height:44px;padding:8px 18px}
|
||||||
|
body.site .posts-index .post:last-child{grid-column:auto}
|
||||||
|
body.site .pagination-wrap{margin-top:40px}
|
||||||
|
body.site .pagination-wrap nav>div:first-child{display:none}
|
||||||
|
body.site .pagination-wrap nav a,body.site .pagination-wrap nav span[aria-disabled] span,body.site .pagination-wrap nav span[aria-current] span{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0 14px;border-radius:999px;border:1.5px solid var(--line);background:#fff;color:var(--ink);text-decoration:none;font-weight:500}
|
||||||
|
body.site .pagination-wrap nav a:hover{border-color:var(--gold)}
|
||||||
|
body.site .pagination-wrap nav span[aria-current] span{background:var(--forest);border-color:var(--forest);color:var(--cream)}
|
||||||
|
body.site .pagination-wrap nav span[aria-disabled] span{opacity:.5}
|
||||||
|
body.site .site-prose :is(h2,h3,h4){font-family:var(--display)}
|
||||||
|
body.site .site-prose a{color:var(--olive)}
|
||||||
|
body.site .site-prose{font-size:1.06rem;line-height:1.85}
|
||||||
|
body.site .post-meta,body.site article>.meta{display:block;font-size:.85rem;font-weight:600;color:var(--gold-ink)}
|
||||||
|
body.site .legal-card{background:#fff;border-radius:16px;box-shadow:var(--shadow-1);padding:40px}
|
||||||
|
body.site .legal-updated{font-size:.85rem;color:var(--muted);margin:16px 0 0}
|
||||||
|
@media (max-width:760px){body.site .legal-card{padding:24px}}
|
||||||
|
|
||||||
/* ---------- Reveal (hidden only when JavaScript runs) ---------- */
|
/* ---------- Reveal (hidden only when JavaScript runs) ---------- */
|
||||||
body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)}
|
body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)}
|
||||||
body.site.js [data-reveal].in{opacity:1;transform:none}
|
body.site.js [data-reveal].in{opacity:1;transform:none}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
@props(['label', 'title', 'lede' => null])
|
@props(['label', 'title', 'lede' => null, 'tag' => 'h2'])
|
||||||
|
|
||||||
<div class="sec-head">
|
<div class="sec-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="art-no" data-reveal>{{ $label }}</span>
|
<span class="art-no" data-reveal>{{ $label }}</span>
|
||||||
<h2 data-reveal data-d="1">{{ $title }}</h2>
|
<{{ $tag }} data-reveal data-d="1">{{ $title }}</{{ $tag }}>
|
||||||
</div>
|
</div>
|
||||||
@if ($lede)
|
@if ($lede)
|
||||||
<p data-reveal data-d="2">{{ $lede }}</p>
|
<p data-reveal data-d="2">{{ $lede }}</p>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ new #[Layout('components.layouts.public')] class extends Component
|
|||||||
@else
|
@else
|
||||||
<section class="sec" style="padding-top:56px">
|
<section class="sec" style="padding-top:56px">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<x-site.section-head :label="__('site.art_booking')" :title="__('booking.request_consultation')" :lede="__('site.booking_lede')" />
|
<x-site.section-head tag="h1" :label="__('site.art_booking')" :title="__('booking.request_consultation')" :lede="__('site.booking_lede')" />
|
||||||
|
|
||||||
<div class="book">
|
<div class="book">
|
||||||
<div class="book-l on-dark">
|
<div class="book-l on-dark">
|
||||||
|
|||||||
@@ -55,91 +55,44 @@ new #[Layout('components.layouts.public')] class extends Component
|
|||||||
}
|
}
|
||||||
}; ?>
|
}; ?>
|
||||||
|
|
||||||
<div class="wrap py-8">
|
<section class="sec" style="padding-top:56px">
|
||||||
<div class="max-w-4xl mx-auto px-0">
|
<div class="wrap">
|
||||||
<flux:heading size="xl" class="text-body text-xl sm:text-2xl lg:text-3xl">{{ __('posts.posts') }}</flux:heading>
|
<x-site.section-head tag="h1" :label="__('site.hero_eyebrow')" :title="__('site.nav_articles')">
|
||||||
|
<div style="position:relative">
|
||||||
<!-- Search Bar -->
|
<label class="sr-only" for="posts-search">{{ __('posts.search_placeholder') }}</label>
|
||||||
<div class="mt-4 sm:mt-6 relative">
|
<input
|
||||||
<flux:input
|
id="posts-search"
|
||||||
|
type="search"
|
||||||
|
class="search"
|
||||||
wire:model.live.debounce.300ms="search"
|
wire:model.live.debounce.300ms="search"
|
||||||
placeholder="{{ __('posts.search_placeholder') }}"
|
placeholder="{{ __('posts.search_placeholder') }}"
|
||||||
class="w-full min-h-[44px]"
|
|
||||||
>
|
>
|
||||||
<x-slot:iconLeading>
|
|
||||||
<flux:icon name="magnifying-glass" class="w-5 h-5 text-body/50" />
|
|
||||||
</x-slot:iconLeading>
|
|
||||||
</flux:input>
|
|
||||||
|
|
||||||
@if($search)
|
|
||||||
<button
|
|
||||||
wire:click="clearSearch"
|
|
||||||
class="absolute end-3 top-1/2 -translate-y-1/2 text-body/50 hover:text-body min-h-[44px] min-w-[44px] flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<flux:icon name="x-mark" class="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
</x-site.section-head>
|
||||||
|
|
||||||
<!-- Search Results Info -->
|
|
||||||
@if($search)
|
@if($search)
|
||||||
<p class="mt-4 text-sm text-body/70">
|
<p class="search-info">
|
||||||
@if($posts->total() > 0)
|
@if($posts->total() > 0)
|
||||||
{{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }}
|
{{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }}
|
||||||
@else
|
@else
|
||||||
{{ __('posts.no_results', ['query' => $search]) }}
|
{{ __('posts.no_results', ['query' => $search]) }}
|
||||||
@endif
|
@endif
|
||||||
|
<button type="button" class="btn btn-ghost" wire:click="clearSearch">{{ __('posts.clear_search') }}</button>
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Posts List -->
|
@if($posts->count() > 0)
|
||||||
<div class="mt-6 sm:mt-8 space-y-4 sm:space-y-6">
|
<div class="posts posts-index">
|
||||||
@forelse($posts as $post)
|
@foreach($posts as $post)
|
||||||
<article wire:key="post-{{ $post->id }}" class="bg-card p-4 sm:p-6 rounded-lg shadow-card hover:shadow-card-hover transition-shadow">
|
<x-site.post-card :post="$post" wire:key="post-{{ $post->id }}" />
|
||||||
<h2 class="text-lg sm:text-xl font-semibold text-text">
|
@endforeach
|
||||||
<a href="{{ route('posts.show', $post) }}" class="hover:text-cta" wire:navigate>
|
|
||||||
@if($search)
|
|
||||||
{!! $this->highlightSearch($post->getTitle(), $search) !!}
|
|
||||||
@else
|
|
||||||
{{ $post->getTitle() }}
|
|
||||||
@endif
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<time class="text-xs sm:text-sm text-body/70 mt-2 block">
|
|
||||||
{{ $post->published_at?->translatedFormat('d F Y') ?? $post->created_at->translatedFormat('d F Y') }}
|
|
||||||
</time>
|
|
||||||
|
|
||||||
<p class="mt-2 sm:mt-3 text-body text-sm sm:text-base">
|
|
||||||
@if($search)
|
|
||||||
{!! $this->highlightSearch($post->getExcerpt(), $search) !!}
|
|
||||||
@else
|
|
||||||
{{ $post->getExcerpt() }}
|
|
||||||
@endif
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a href="{{ route('posts.show', $post) }}" class="text-cta hover:text-cta-hover hover:underline mt-3 sm:mt-4 inline-flex items-center min-h-[44px]" wire:navigate>
|
|
||||||
{{ __('posts.read_more') }} →
|
|
||||||
</a>
|
|
||||||
</article>
|
|
||||||
@empty
|
|
||||||
<div class="empty-state bg-card rounded-lg">
|
|
||||||
@if($search)
|
|
||||||
<flux:icon name="magnifying-glass" class="empty-state-icon text-body/30" />
|
|
||||||
<p class="text-body/70">{{ __('posts.no_results', ['query' => $search]) }}</p>
|
|
||||||
<flux:button wire:click="clearSearch" class="mt-4">
|
|
||||||
{{ __('posts.clear_search') }}
|
|
||||||
</flux:button>
|
|
||||||
@else
|
|
||||||
<flux:icon name="document-text" class="empty-state-icon text-body/40" />
|
|
||||||
<p class="text-body/70">{{ __('posts.no_posts') }}</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
</div>
|
||||||
|
@elseif(! $search)
|
||||||
|
<p class="text-center" style="color:var(--muted)">{{ __('posts.no_posts') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="pagination-wrap">
|
||||||
{{ $posts->links() }}
|
{{ $posts->links() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|||||||
@@ -18,24 +18,17 @@ new #[Layout('components.layouts.public')] class extends Component
|
|||||||
}
|
}
|
||||||
}; ?>
|
}; ?>
|
||||||
|
|
||||||
<div class="wrap py-8">
|
<section class="sec" style="padding-top:56px">
|
||||||
<article class="max-w-3xl mx-auto">
|
<article class="wrap" style="max-width:768px">
|
||||||
<header class="mb-8">
|
<span class="meta">{{ $post->published_at?->translatedFormat('l, d F Y') ?? $post->created_at->translatedFormat('l, d F Y') }}</span>
|
||||||
<flux:heading size="xl" class="text-body">{{ $post->getTitle() }}</flux:heading>
|
<h1 style="font-size:clamp(2rem,3.4vw,3rem);margin:10px 0 28px">{{ $post->getTitle() }}</h1>
|
||||||
|
|
||||||
<time class="text-body/70 mt-2 block">
|
<div class="prose prose-lg prose-brand max-w-none site-prose">
|
||||||
{{ $post->published_at?->translatedFormat('l, d F Y') ?? $post->created_at->translatedFormat('l, d F Y') }}
|
|
||||||
</time>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="prose prose-lg prose-brand max-w-none">
|
|
||||||
{!! $post->getBody() !!}
|
{!! $post->getBody() !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-12 pt-6 border-t border-cta/20">
|
<footer style="border-top:1px solid var(--line);margin-top:48px;padding-top:24px">
|
||||||
<a href="{{ route('posts.index') }}" class="text-cta hover:text-cta-hover hover:underline" wire:navigate>
|
<a class="btn btn-ghost" href="{{ route('posts.index') }}" wire:navigate>{{ __('posts.back_to_posts') }}</a>
|
||||||
← {{ __('posts.back_to_posts') }}
|
|
||||||
</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</section>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<x-layouts.public>
|
<x-layouts.public>
|
||||||
<div class="wrap py-8">
|
<section class="sec" style="padding-top:56px">
|
||||||
<h1 class="mb-6 text-3xl font-bold text-text">{{ $page->getTitle() }}</h1>
|
<div class="wrap" style="max-width:768px">
|
||||||
<div class="prose prose-brand max-w-none rounded-lg bg-card p-8 shadow-card" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
|
<h1 style="font-size:clamp(2rem,3.4vw,3rem);margin:0 0 28px">{{ $page->getTitle() }}</h1>
|
||||||
|
<div class="prose prose-brand max-w-none site-prose legal-card" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
|
||||||
@if ($page->getContent())
|
@if ($page->getContent())
|
||||||
{!! clean($page->getContent()) !!}
|
{!! clean($page->getContent()) !!}
|
||||||
@else
|
@else
|
||||||
<p class="text-body">{{ __('pages.content_coming_soon') }}</p>
|
<p>{{ __('pages.content_coming_soon') }}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-body/70">
|
<p class="legal-updated">
|
||||||
{{ __('pages.last_updated') }}: {{ $page->updated_at->format('M d, Y') }}
|
{{ __('pages.last_updated') }}: {{ $page->updated_at->format('M d, Y') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</x-layouts.public>
|
</x-layouts.public>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<x-layouts.public>
|
<x-layouts.public>
|
||||||
<div class="wrap py-8">
|
<section class="sec" style="padding-top:56px">
|
||||||
<h1 class="text-3xl font-bold text-text mb-6">{{ __('footer.privacy') }}</h1>
|
<div class="wrap" style="max-width:768px">
|
||||||
<div class="bg-card p-8 rounded-lg shadow-card prose prose-brand max-w-none">
|
<h1 style="font-size:clamp(2rem,3.4vw,3rem);margin:0 0 28px">{{ __('footer.privacy') }}</h1>
|
||||||
<p class="text-body">{{ __('Privacy policy content will be added here.') }}</p>
|
<div class="prose prose-brand max-w-none site-prose legal-card">
|
||||||
|
<p>{{ __('Privacy policy content will be added here.') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</x-layouts.public>
|
</x-layouts.public>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<x-layouts.public>
|
<x-layouts.public>
|
||||||
<div class="wrap py-8">
|
<section class="sec" style="padding-top:56px">
|
||||||
<h1 class="text-3xl font-bold text-text mb-6">{{ __('footer.terms') }}</h1>
|
<div class="wrap" style="max-width:768px">
|
||||||
<div class="bg-card p-8 rounded-lg shadow-card prose prose-brand max-w-none">
|
<h1 style="font-size:clamp(2rem,3.4vw,3rem);margin:0 0 28px">{{ __('footer.terms') }}</h1>
|
||||||
<p class="text-body">{{ __('Terms of service content will be added here.') }}</p>
|
<div class="prose prose-brand max-w-none site-prose legal-card">
|
||||||
|
<p>{{ __('Terms of service content will be added here.') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</x-layouts.public>
|
</x-layouts.public>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Page;
|
||||||
|
use App\Models\Post;
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
test('posts index shows the section head, search and text-only cards', function () {
|
||||||
|
Post::factory()->published()->count(3)->create();
|
||||||
|
|
||||||
|
$html = $this->withSession(['locale' => 'en'])->get('/posts')->assertOk()->getContent();
|
||||||
|
|
||||||
|
expect(substr_count($html, '<h1'))->toBe(1);
|
||||||
|
expect($html)
|
||||||
|
->toContain('class="sec-head"')
|
||||||
|
->toContain('wire:model.live.debounce.300ms="search"')
|
||||||
|
->toContain('class="search"')
|
||||||
|
->toContain('posts-index');
|
||||||
|
expect(substr_count($html, 'class="post"'))->toBe(3);
|
||||||
|
|
||||||
|
preg_match('~<div class="posts posts-index">(.*?)</section>~s', $html, $block);
|
||||||
|
expect($block[1])->not->toContain('<img');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search filters and shows the count line, with a clear action', function () {
|
||||||
|
Post::factory()->published()->create(['title' => ['en' => 'Alimony guide', 'ar' => 'دليل النفقة']]);
|
||||||
|
Post::factory()->published()->create(['title' => ['en' => 'Contracts', 'ar' => 'العقود']]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
Volt::test('pages.posts.index')->set('search', 'Alimony')
|
||||||
|
->assertSee('Alimony guide')->assertDontSee('Contracts')
|
||||||
|
->assertSee(__('posts.search_results', ['count' => 1, 'query' => 'Alimony']))
|
||||||
|
->assertSee(__('posts.clear_search'))
|
||||||
|
->set('search', 'zzzz')->assertSee(__('posts.no_results', ['query' => 'zzzz']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty index shows the no posts message', function () {
|
||||||
|
Volt::test('pages.posts.index')->assertSee(__('posts.no_posts'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pagination is wrapped and styled when there are more than ten posts', function () {
|
||||||
|
Post::factory()->published()->count(12)->create();
|
||||||
|
|
||||||
|
expect($this->get('/posts')->assertOk()->getContent())->toContain('pagination-wrap');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a published post shows meta, h1, body and a back link; unpublished is 404', function () {
|
||||||
|
$post = Post::factory()->published()->create();
|
||||||
|
$html = $this->get(route('posts.show', $post))->assertOk()->getContent();
|
||||||
|
|
||||||
|
expect($html)->toContain('class="meta"')->toContain('site-prose')->toContain(route('posts.index'))->toContain(__('posts.back_to_posts'));
|
||||||
|
expect(substr_count($html, '<h1'))->toBe(1);
|
||||||
|
|
||||||
|
$draft = Post::factory()->draft()->create();
|
||||||
|
$this->get(route('posts.show', $draft))->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legal pages show title, sanitised content and the last updated line', function () {
|
||||||
|
Page::factory()->terms()->create(['content_en' => '<p>Hello</p><script>alert(1)</script>', 'content_ar' => '<p>مرحبا</p><script>alert(1)</script>']);
|
||||||
|
|
||||||
|
$html = $this->withSession(['locale' => 'en'])->get('/page/terms')->assertOk()->getContent();
|
||||||
|
|
||||||
|
expect($html)->toContain('Terms of Service')->toContain('Hello')->not->toContain('<script>alert(1)')->toContain('site-prose')
|
||||||
|
->toContain(__('pages.last_updated'));
|
||||||
|
expect(substr_count($html, '<h1'))->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('terms and privacy views render with one h1', function () {
|
||||||
|
foreach (['terms', 'privacy'] as $v) {
|
||||||
|
$html = view("pages.$v")->render();
|
||||||
|
expect(substr_count($html, '<h1'))->toBe(1);
|
||||||
|
expect($html)->toContain('class="wrap"')->not->toContain('class="wrap py-8"');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin posts do not load the public foundation', function () {
|
||||||
|
$html = $this->actingAs(User::factory()->admin()->create())->get('/admin/posts')->assertOk()->getContent();
|
||||||
|
expect($html)->not->toContain('public.css')->not->toContain('class="site');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('articles css rules exist', function () {
|
||||||
|
expect(file_get_contents(resource_path('css/public.css')))
|
||||||
|
->toContain('body.site .posts-index .post:last-child{grid-column:auto}')
|
||||||
|
->toContain('body.site .pagination-wrap')
|
||||||
|
->toContain('body.site .site-prose a{color:var(--olive)}');
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ beforeEach(function () {
|
|||||||
test('booking page shows the section head, the forest card and the month calendar', function () {
|
test('booking page shows the section head, the forest card and the month calendar', function () {
|
||||||
$html = $this->get('/booking')->assertOk()->getContent();
|
$html = $this->get('/booking')->assertOk()->getContent();
|
||||||
|
|
||||||
|
expect(substr_count($html, '<h1'))->toBe(1);
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('class="sec-head"')
|
->toContain('class="sec-head"')
|
||||||
->toContain('class="book"')
|
->toContain('class="book"')
|
||||||
|
|||||||
Reference in New Issue
Block a user