Compare commits
5 Commits
b54908541f
...
_base-main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a12b7d82c | |||
| af8484c74e | |||
| 35bc5a59d9 | |||
| 27711ad0c2 | |||
| af31738021 |
3
.husky/pre-commit
Executable file
3
.husky/pre-commit
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
composer pint:check && composer phpstan
|
||||
33
.husky/pre-push
Executable file
33
.husky/pre-push
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Abort push when the test database is unreachable, so broken code never
|
||||
# reaches CI. Test DB settings come from phpunit.xml.
|
||||
php -r '
|
||||
$xml = @simplexml_load_file("phpunit.xml");
|
||||
if ($xml === false) {
|
||||
fwrite(STDERR, "phpunit.xml not found — aborting pre-push.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$defaults = ["DB_HOST" => "127.0.0.1", "DB_PORT" => "5432", "DB_DATABASE" => "amare_test", "DB_USERNAME" => "amare", "DB_PASSWORD" => "secret"];
|
||||
$env = [];
|
||||
foreach ($xml->php->env as $node) {
|
||||
$name = (string) $node["name"];
|
||||
if (isset($defaults[$name])) {
|
||||
$env[$name] = (string) $node["value"];
|
||||
}
|
||||
}
|
||||
$config = array_merge($defaults, $env);
|
||||
|
||||
$dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s", $config["DB_HOST"], $config["DB_PORT"], $config["DB_DATABASE"]);
|
||||
try {
|
||||
new PDO($dsn, $config["DB_USERNAME"], $config["DB_PASSWORD"], [PDO::ATTR_TIMEOUT => 3]);
|
||||
} catch (PDOException $e) {
|
||||
fwrite(STDERR, "\033[31mPostgreSQL is unreachable on {$config["DB_HOST"]}:{$config["DB_PORT"]} (db: {$config["DB_DATABASE"]}).\033[0m\n");
|
||||
fwrite(STDERR, "Start it with: docker compose up -d\n");
|
||||
fwrite(STDERR, "Then retry the push.\n");
|
||||
exit(1);
|
||||
}
|
||||
' || exit 1
|
||||
|
||||
composer test:unit && composer test:feature
|
||||
@@ -15,6 +15,13 @@ This is a Laravel 13 application for an event-planning consultancy. Application
|
||||
|
||||
Feature and browser tests require the `amare_test` PostgreSQL database configured in `phpunit.xml`.
|
||||
|
||||
## Git Hooks (husky)
|
||||
|
||||
Hooks live in `.husky/` and auto-install on any plain `npm install` via the `prepare` script. Note `composer setup` runs `npm install --ignore-scripts`, which skips hook installation — after setup, run `npm install` once (or `npx husky`) to activate hooks.
|
||||
|
||||
- `pre-commit`: runs `composer pint:check` and `composer phpstan`.
|
||||
- `pre-push`: gates on the `amare_test` database (settings parsed from `phpunit.xml`), blocks the push with a `docker compose up -d` hint when Postgres is unreachable, then runs `composer test:unit` and `composer test:feature`. Browser tests are CI-only (FrankenPHP container).
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Follow PSR-4 and Laravel conventions: PascalCase classes, camelCase methods, and snake_case database columns. Use four spaces (two in YAML, except four in Compose files), LF endings, and UTF-8 as defined by `.editorconfig`. Every project-owned PHP file must place `declare(strict_types=1);` immediately after `<?php`. Keep domain code independent of Filament and Livewire. Run `composer pint` to format and `composer phpstan` before review.
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\PortfolioCase;
|
||||
use App\Models\PortfolioImage;
|
||||
use App\Models\Service;
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -23,7 +22,7 @@ class ContentSeeder extends Seeder
|
||||
$this->seedSiteSettings();
|
||||
$this->seedServices();
|
||||
$this->seedPortfolioCases();
|
||||
$this->seedTestimonials();
|
||||
$this->call(TestimonialsSeeder::class);
|
||||
}
|
||||
|
||||
private function seedSiteSettings(): void
|
||||
@@ -170,67 +169,6 @@ class ContentSeeder extends Seeder
|
||||
}
|
||||
}
|
||||
|
||||
private function seedTestimonials(): void
|
||||
{
|
||||
// Real couples from depoimentos.md. Production publication still requires
|
||||
// explicit couple authorization before setting published_at outside local/demo seeds.
|
||||
$testimonials = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
],
|
||||
];
|
||||
|
||||
$keepAuthors = array_column($testimonials, 'author_name');
|
||||
|
||||
Testimonial::query()
|
||||
->whereNotIn('author_name', $keepAuthors)
|
||||
->delete();
|
||||
|
||||
foreach ($testimonials as $testimonial) {
|
||||
Testimonial::query()->updateOrCreate(
|
||||
['author_name' => $testimonial['author_name']],
|
||||
[
|
||||
...$testimonial,
|
||||
'photo_path' => null,
|
||||
'photo_alt' => null,
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFixture(string $fixtureName, string $destination): string
|
||||
{
|
||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||
|
||||
70
database/seeders/TestimonialsSeeder.php
Normal file
70
database/seeders/TestimonialsSeeder.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TestimonialsSeeder extends Seeder
|
||||
{
|
||||
private const PUBLISHED_AT = '2026-08-05 00:00:00';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$testimonials = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
],
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($testimonials): void {
|
||||
foreach ($testimonials as $testimonial) {
|
||||
Testimonial::query()->updateOrCreate(
|
||||
['author_name' => $testimonial['author_name']],
|
||||
[
|
||||
'quote' => $testimonial['quote'],
|
||||
'context' => $testimonial['context'],
|
||||
'sort_order' => $testimonial['sort_order'],
|
||||
'is_featured' => $testimonial['is_featured'],
|
||||
'published_at' => self::PUBLISHED_AT,
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ HTTP 404 from `compose.deploy` usually means the compose id is wrong (Applicatio
|
||||
|
||||
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||
|
||||
## First admin (no `db:seed` in production)
|
||||
## First admin and authorized production seeding
|
||||
|
||||
Seed credentials are local-only. For staging/production:
|
||||
|
||||
@@ -192,6 +192,35 @@ XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::que
|
||||
|
||||
Never reuse `admin@amare.local` / `password`.
|
||||
|
||||
### Load authorized testimonials after migrations
|
||||
|
||||
After migrations, manually load the five authorized testimonials in **staging**, then repeat in **production**. From Dokploy, open a terminal on the environment's `web` service (or run an equivalent one-off process):
|
||||
|
||||
```bash
|
||||
php artisan db:seed --class='Database\Seeders\TestimonialsSeeder' --force --no-interaction
|
||||
```
|
||||
|
||||
This seeder is safe to rerun: it overwrites canonical source-owned fields, preserves curated photo fields, and leaves unrelated testimonials unchanged. The five records are published with the approved deterministic timestamp. Upsert keys on `author_name` (no unique DB constraint); keep one row per couple before/after running.
|
||||
|
||||
Optional verification:
|
||||
|
||||
```bash
|
||||
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||
\$expected = collect(['Jeniffer e Maick', 'Quesia e Jhonata', 'Milena e Weslley', 'Raquel e Pedro', 'Victoria e Pedro']);
|
||||
\$rows = \\App\\Models\\Testimonial::query()->whereIn('author_name', \$expected)->get(['author_name', 'published_at'])->groupBy('author_name');
|
||||
\$valid = \$expected->every(function (string \$author) use (\$rows): bool {
|
||||
\$matches = \$rows->get(\$author, collect());
|
||||
return \$matches->count() === 1
|
||||
&& \$matches->first()->published_at?->format('Y-m-d H:i:s') === '2026-08-05 00:00:00';
|
||||
});
|
||||
echo (\$valid ? 'ok' : 'invalid').PHP_EOL;
|
||||
"
|
||||
```
|
||||
|
||||
Expected output: `ok`.
|
||||
|
||||
Do not run `DatabaseSeeder` or `ContentSeeder` in staging or production: they include local credentials and/or broad demo-content effects. Deployment workflows intentionally remain migrate-only; loading these testimonials is a deliberate manual operation in each environment.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"laravel-vite-plugin": "^3.1",
|
||||
"playwright": "^1.62.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
@@ -890,6 +891,22 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
"dev": "vite",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"laravel-vite-plugin": "^3.1",
|
||||
"playwright": "^1.62.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
|
||||
@@ -179,9 +179,9 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
/* Transform/clip only — never fade text opacity (axe + WCAG mid-transition). */
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="seal"],
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="cta"] {
|
||||
opacity: 0;
|
||||
transform: translateY(0.5rem);
|
||||
}
|
||||
|
||||
@@ -194,11 +194,9 @@
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
clip-path: inset(0 0 0 0);
|
||||
transition:
|
||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
clip-path var(--amare-duration-focal) var(--amare-ease-arrival);
|
||||
}
|
||||
@@ -220,29 +218,21 @@
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-reveal]:not(.is-revealed) {
|
||||
opacity: 0;
|
||||
transform: translateY(0.5rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-reveal].is-revealed {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition:
|
||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-slow) var(--amare-ease-arrival);
|
||||
transition: transform var(--amare-duration-slow) var(--amare-ease-arrival);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) {
|
||||
opacity: 0;
|
||||
transform: translateY(0.4rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="page-open"].is-active {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition:
|
||||
opacity var(--amare-duration-normal) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-normal) var(--amare-ease-arrival);
|
||||
transition: transform var(--amare-duration-normal) var(--amare-ease-arrival);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
tasks.md
Normal file
8
tasks.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Tasks: Git hooks (pre-commit + pre-push)
|
||||
|
||||
- [x] Install husky + add prepare script
|
||||
- [x] Create .husky/pre-commit (pint + phpstan)
|
||||
- [x] Create .husky/pre-push (DB gate + tests)
|
||||
- [x] Install hooks into repo, chmod +x
|
||||
- [x] Verify hooks (DB up/down, pre-commit)
|
||||
- [x] Document hooks in AGENTS.md
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,8 @@ use Livewire\Features\SupportFileUploads\FileUploadConfiguration;
|
||||
it('keeps livewire temporary uploads on the local disk when the default filesystem is r2', function (): void {
|
||||
config(['filesystems.default' => 'r2']);
|
||||
|
||||
// Livewire swaps FileUploadConfiguration::disk() to tmp-for-tests under PHPUnit;
|
||||
// isUsingS3() still reads the configured temp disk and drives signed R2 uploads.
|
||||
expect(config('livewire.temporary_file_upload.disk'))->toBe('local')
|
||||
->and(FileUploadConfiguration::disk())->toBe('local')
|
||||
->and(FileUploadConfiguration::isUsingS3())->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\ContentSeeder;
|
||||
use Database\Seeders\TestimonialsSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
@@ -18,6 +19,59 @@ class TestimonialsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @var list<array{
|
||||
* quote: string,
|
||||
* author_name: string,
|
||||
* context: string,
|
||||
* sort_order: int,
|
||||
* is_featured: bool,
|
||||
* published_at: string
|
||||
* }>
|
||||
*/
|
||||
private const EXPECTED_TESTIMONIALS = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
];
|
||||
|
||||
public function test_published_scope_excludes_unpublished_testimonials(): void
|
||||
{
|
||||
Testimonial::factory()->create(['published_at' => null]);
|
||||
@@ -48,11 +102,27 @@ class TestimonialsTest extends TestCase
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_maps_all_five_real_couples_from_depoimentos(): void
|
||||
{
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$testimonials = Testimonial::query()
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $this->sourceOwnedState($testimonial))
|
||||
->all();
|
||||
|
||||
$this->assertSame(self::EXPECTED_TESTIMONIALS, $testimonials);
|
||||
}
|
||||
|
||||
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$this->seed(ContentSeeder::class);
|
||||
$this->artisan('db:seed', [
|
||||
'--class' => ContentSeeder::class,
|
||||
'--no-interaction' => true,
|
||||
])->assertExitCode(0);
|
||||
|
||||
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
||||
|
||||
@@ -64,11 +134,6 @@ class TestimonialsTest extends TestCase
|
||||
'Victoria e Pedro',
|
||||
], $authors);
|
||||
|
||||
$this->assertNull(
|
||||
Testimonial::query()->where('author_name', 'Ana Souza')->first(),
|
||||
'Fictional demo authors must not remain after seeding real testimonials.',
|
||||
);
|
||||
|
||||
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
||||
|
||||
$this->assertNotNull($jeniffer);
|
||||
@@ -77,6 +142,87 @@ class TestimonialsTest extends TestCase
|
||||
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_authoritatively_overwrites_source_owned_fields(): void
|
||||
{
|
||||
$testimonial = Testimonial::factory()->create([
|
||||
'quote' => 'Depoimento desatualizado.',
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Contexto desatualizado',
|
||||
'sort_order' => 99,
|
||||
'is_featured' => false,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$this->assertSame(
|
||||
self::EXPECTED_TESTIMONIALS[0],
|
||||
$this->sourceOwnedState($testimonial->refresh()),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_preserves_curated_photo_fields_for_known_author(): void
|
||||
{
|
||||
$testimonial = Testimonial::factory()->create([
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'photo_path' => 'testimonials/jeniffer-e-maick.jpg',
|
||||
'photo_alt' => 'Jeniffer e Maick durante o casamento',
|
||||
]);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$testimonial->refresh();
|
||||
|
||||
$this->assertSame('testimonials/jeniffer-e-maick.jpg', $testimonial->photo_path);
|
||||
$this->assertSame('Jeniffer e Maick durante o casamento', $testimonial->photo_alt);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_leaves_unrelated_testimonials_unchanged(): void
|
||||
{
|
||||
$unrelated = Testimonial::factory()->published()->featured()->create([
|
||||
'quote' => 'Depoimento cadastrado pela equipe.',
|
||||
'author_name' => 'Casal não presente na fonte',
|
||||
'context' => 'Bodas · 02/08/2026',
|
||||
'photo_path' => 'testimonials/casal-equipe.jpg',
|
||||
'photo_alt' => 'Casal cadastrado pela equipe',
|
||||
'sort_order' => 42,
|
||||
]);
|
||||
$originalState = $unrelated->getAttributes();
|
||||
ksort($originalState);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$persisted = $unrelated->fresh();
|
||||
|
||||
$this->assertNotNull($persisted);
|
||||
$persistedState = $persisted->getAttributes();
|
||||
ksort($persistedState);
|
||||
|
||||
$this->assertSame($originalState, $persistedState);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_is_idempotent_without_duplicate_authors(): void
|
||||
{
|
||||
$this->runTestimonialsSeeder();
|
||||
$firstRunState = Testimonial::query()
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||
->all();
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$secondRunState = Testimonial::query()
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||
->all();
|
||||
|
||||
$this->assertCount(5, $secondRunState);
|
||||
$this->assertSame($firstRunState, $secondRunState);
|
||||
$this->assertSame(5, Testimonial::query()->distinct()->count('author_name'));
|
||||
}
|
||||
|
||||
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
@@ -97,4 +243,34 @@ class TestimonialsTest extends TestCase
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private function runTestimonialsSeeder(): void
|
||||
{
|
||||
$this->artisan('db:seed', [
|
||||
'--class' => TestimonialsSeeder::class,
|
||||
'--no-interaction' => true,
|
||||
])->assertExitCode(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* quote: string,
|
||||
* author_name: string,
|
||||
* context: string,
|
||||
* sort_order: int,
|
||||
* is_featured: bool,
|
||||
* published_at: string
|
||||
* }
|
||||
*/
|
||||
private function sourceOwnedState(Testimonial $testimonial): array
|
||||
{
|
||||
return [
|
||||
'quote' => (string) $testimonial->quote,
|
||||
'author_name' => (string) $testimonial->author_name,
|
||||
'context' => (string) $testimonial->context,
|
||||
'sort_order' => (int) $testimonial->sort_order,
|
||||
'is_featured' => (bool) $testimonial->is_featured,
|
||||
'published_at' => $testimonial->published_at?->format('Y-m-d H:i:s') ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,20 @@ class MotionMarkupTest extends TestCase
|
||||
$this->assertStringContainsString('[data-chapter-index]', $appCss);
|
||||
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
|
||||
|
||||
// Entrance motion must not fade text opacity — mid-fade fails WCAG contrast (axe).
|
||||
$this->assertDoesNotMatchRegularExpression(
|
||||
'/\[data-motion-beat="seal"\][^{]*\{[^}]*opacity:\s*0/s',
|
||||
$appCss,
|
||||
);
|
||||
$this->assertDoesNotMatchRegularExpression(
|
||||
'/\[data-motion="page-open"\]:not\(\.is-active\)[^{]*\{[^}]*opacity:\s*0/s',
|
||||
$appCss,
|
||||
);
|
||||
$this->assertDoesNotMatchRegularExpression(
|
||||
'/\[data-reveal\]:not\(\.is-revealed\)[^{]*\{[^}]*opacity:\s*0/s',
|
||||
$appCss,
|
||||
);
|
||||
|
||||
$this->assertFileExists(resource_path('js/motion.js'));
|
||||
$this->assertStringContainsString("import './motion.js'", $appJs);
|
||||
$this->assertStringContainsString('prefers-reduced-motion', (string) file_get_contents(resource_path('js/motion.js')));
|
||||
|
||||
Reference in New Issue
Block a user