test: speed browser suite via visit reuse
Collapse route×viewport cartesian products, merge home motion asserts, skip existing VisualContentSeeder JPEGs, ignore Screenshots.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,3 +26,6 @@ Homestead.json
|
|||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Pest Browser visual diffs (local/CI artifacts)
|
||||||
|
/tests/Browser/Screenshots/
|
||||||
|
|||||||
@@ -200,16 +200,22 @@ class VisualContentSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
||||||
{
|
{
|
||||||
|
$disk = Storage::disk('public');
|
||||||
|
|
||||||
|
if ($disk->exists($destination)) {
|
||||||
|
return $destination;
|
||||||
|
}
|
||||||
|
|
||||||
$image = imagecreatetruecolor($width, $height);
|
$image = imagecreatetruecolor($width, $height);
|
||||||
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
||||||
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
imagejpeg($image, null, 90);
|
imagejpeg($image, null, 80);
|
||||||
$binary = (string) ob_get_clean();
|
$binary = (string) ob_get_clean();
|
||||||
imagedestroy($image);
|
imagedestroy($image);
|
||||||
|
|
||||||
Storage::disk('public')->put($destination, $binary);
|
$disk->put($destination, $binary);
|
||||||
|
|
||||||
return $destination;
|
return $destination;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ uses(RefreshDatabase::class);
|
|||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
});
|
|
||||||
|
|
||||||
it('has no critical or serious accessibility issues on covered public routes', function (): void {
|
$this->portfolioCase = PortfolioCase::factory()->published()->create([
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
'slug' => 'casamento-jardim',
|
||||||
'title' => 'Casamento Jardim',
|
'title' => 'Casamento Jardim',
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no critical accessibility or console issues on covered public routes', function (): void {
|
||||||
$routes = [
|
$routes = [
|
||||||
'/',
|
'/',
|
||||||
'/servicos',
|
'/servicos',
|
||||||
'/portfolio',
|
'/portfolio',
|
||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$this->portfolioCase->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
'/briefing',
|
'/briefing',
|
||||||
@@ -30,9 +30,16 @@ it('has no critical or serious accessibility issues on covered public routes', f
|
|||||||
'/__missing-accessibility-page',
|
'/__missing-accessibility-page',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
$page = $this->visit($routes[0]);
|
||||||
$this->visit($route)
|
|
||||||
->assertNoAccessibilityIssues(1);
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
$page
|
||||||
|
->assertNoAccessibilityIssues(1)
|
||||||
|
->assertNoJavaScriptErrors();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,47 +97,26 @@ it('reaches the primary CTA by keyboard and activates it', function (): void {
|
|||||||
expect($hash)->toBe('#sobre');
|
expect($hash)->toBe('#sobre');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads covered public routes without console errors', function (): void {
|
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ([
|
|
||||||
'/',
|
|
||||||
'/servicos',
|
|
||||||
'/portfolio',
|
|
||||||
'/portfolio/'.$case->slug,
|
|
||||||
'/sobre',
|
|
||||||
'/contato',
|
|
||||||
'/briefing',
|
|
||||||
'/privacidade',
|
|
||||||
'/__missing-console-page',
|
|
||||||
] as $route) {
|
|
||||||
$this->visit($route)
|
|
||||||
->assertNoJavaScriptErrors();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$routes = [
|
$routes = [
|
||||||
'/',
|
'/',
|
||||||
'/servicos',
|
'/servicos',
|
||||||
'/portfolio',
|
'/portfolio',
|
||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$this->portfolioCase->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
'/briefing',
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
$page = $this->visit($routes[0], [
|
||||||
$page = $this->visit($route, [
|
'reducedMotion' => 'reduce',
|
||||||
'reducedMotion' => 'reduce',
|
]);
|
||||||
]);
|
|
||||||
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
$duration = $page->script(<<<'JS'
|
$duration = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ beforeEach(function (): void {
|
|||||||
Artisan::call('db:seed', ['--class' => VisualContentSeeder::class, '--force' => true]);
|
Artisan::call('db:seed', ['--class' => VisualContentSeeder::class, '--force' => true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the hero inside the container as an asymmetric spread on desktop', function (): void {
|
it('keeps the hero asymmetric on desktop and stacked through md and mobile', function (): void {
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
])->resize(1440, 1000);
|
]);
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$waitFrames = '() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))';
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
$page->resize(1440, 1000);
|
||||||
|
$page->script($waitFrames);
|
||||||
|
|
||||||
|
$desktop = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
const header = document.querySelector('.site-header');
|
const header = document.querySelector('.site-header');
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
@@ -41,7 +44,7 @@ it('keeps the hero inside the container as an asymmetric spread on desktop', fun
|
|||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($desktop)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'startsBelowHeader' => true,
|
'startsBelowHeader' => true,
|
||||||
'mediaBesideContent' => true,
|
'mediaBesideContent' => true,
|
||||||
@@ -49,16 +52,11 @@ it('keeps the hero inside the container as an asymmetric spread on desktop', fun
|
|||||||
'mediaInsideContainer' => true,
|
'mediaInsideContainer' => true,
|
||||||
'horizontalOverflow' => false,
|
'horizontalOverflow' => false,
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
it('stacks the hero media below the content up to the md breakpoint', function (): void {
|
$page->resize(767, 1000);
|
||||||
$page = $this->visit('/', [
|
$page->script($waitFrames);
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
])->resize(767, 1000);
|
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$md = $page->script(<<<'JS'
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
const content = hero?.querySelector('[data-hero-content]');
|
const content = hero?.querySelector('[data-hero-content]');
|
||||||
@@ -76,23 +74,18 @@ it('stacks the hero media below the content up to the md breakpoint', function (
|
|||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($md)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'mediaFollowsContent' => true,
|
'mediaFollowsContent' => true,
|
||||||
'mediaFillsContainerWidth' => true,
|
'mediaFillsContainerWidth' => true,
|
||||||
'mediaMinHeight' => true,
|
'mediaMinHeight' => true,
|
||||||
'horizontalOverflow' => false,
|
'horizontalOverflow' => false,
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
it('stacks the hero media below the content on mobile', function (): void {
|
$page->resize(390, 844);
|
||||||
$page = $this->visit('/', [
|
$page->script($waitFrames);
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
])->resize(390, 844);
|
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$mobile = $page->script(<<<'JS'
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
const content = hero?.querySelector('[data-hero-content]');
|
const content = hero?.querySelector('[data-hero-content]');
|
||||||
@@ -109,7 +102,7 @@ it('stacks the hero media below the content on mobile', function (): void {
|
|||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($mobile)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'mediaFollowsContent' => true,
|
'mediaFollowsContent' => true,
|
||||||
'mediaFillsContainerWidth' => true,
|
'mediaFillsContainerWidth' => true,
|
||||||
@@ -118,10 +111,12 @@ it('stacks the hero media below the content on mobile', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the full home cadence accessible and contained at both viewports', function (): void {
|
it('keeps the full home cadence accessible and contained at both viewports', function (): void {
|
||||||
|
$page = $this->visit('/', [
|
||||||
|
'reducedMotion' => 'reduce',
|
||||||
|
]);
|
||||||
|
|
||||||
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
||||||
$page = $this->visit('/', [
|
$page->resize($width, $height);
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
])->resize($width, $height);
|
|
||||||
|
|
||||||
$page->assertNoAccessibilityIssues(1);
|
$page->assertNoAccessibilityIssues(1);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ beforeEach(function (): void {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps every home motion target in final visible state when reduced motion is preferred', function (): void {
|
it('keeps reduced-motion home final and free of ornamental chapter counters', function (): void {
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'is_featured' => true,
|
||||||
|
]);
|
||||||
|
Testimonial::factory()->published()->create([
|
||||||
|
'quote' => 'Relato publicado.',
|
||||||
|
]);
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
]);
|
]);
|
||||||
@@ -37,14 +44,20 @@ it('keeps every home motion target in final visible state when reduced motion is
|
|||||||
const titleStyle = title ? getComputedStyle(title) : null;
|
const titleStyle = title ? getComputedStyle(title) : null;
|
||||||
const finalReveals = [...document.querySelectorAll('[data-reveal]')]
|
const finalReveals = [...document.querySelectorAll('[data-reveal]')]
|
||||||
.every((node) => node.classList.contains('is-revealed'));
|
.every((node) => node.classList.contains('is-revealed'));
|
||||||
|
const flow = document.querySelector('.home-chapters');
|
||||||
|
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
|
||||||
|
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
heroActive: hero?.classList.contains('is-active') ?? false,
|
heroActive: hero?.classList.contains('is-active') ?? false,
|
||||||
titleOpacity: titleStyle ? Number.parseFloat(titleStyle.opacity) : 0,
|
titleOpacity: titleStyle ? Number.parseFloat(titleStyle.opacity) : 0,
|
||||||
hasIndex: Boolean(index),
|
hasIndex: Boolean(index),
|
||||||
hasActiveChapter: Boolean(active),
|
hasActiveChapter: Boolean(active),
|
||||||
finalReveals,
|
finalReveals,
|
||||||
enhancementEnabled: document.documentElement.hasAttribute('data-motion'),
|
enhancementEnabled: document.documentElement.hasAttribute('data-motion'),
|
||||||
|
reset: flow ? getComputedStyle(flow).counterReset : '',
|
||||||
|
increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement),
|
||||||
|
folioCount: folios.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
@@ -55,9 +68,12 @@ it('keeps every home motion target in final visible state when reduced motion is
|
|||||||
expect($state['hasActiveChapter'])->toBeFalse();
|
expect($state['hasActiveChapter'])->toBeFalse();
|
||||||
expect($state['finalReveals'])->toBeTrue();
|
expect($state['finalReveals'])->toBeTrue();
|
||||||
expect($state['enhancementEnabled'])->toBeFalse();
|
expect($state['enhancementEnabled'])->toBeFalse();
|
||||||
|
expect($state['reset'])->toBe('none');
|
||||||
|
expect($state['increments'])->each->toBe('none');
|
||||||
|
expect($state['folioCount'])->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('activates the home opening with 500ms beats and capped 90ms stagger', function (): void {
|
it('opens home motion, reveals once, and allows CTA during open', function (): void {
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'no-preference',
|
'reducedMotion' => 'no-preference',
|
||||||
]);
|
]);
|
||||||
@@ -77,45 +93,39 @@ it('activates the home opening with 500ms beats and capped 90ms stagger', functi
|
|||||||
})
|
})
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$state = $page->script(<<<'JS'
|
$opening = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-motion="page-open"]');
|
const hero = document.querySelector('[data-motion="page-open"]');
|
||||||
const beatDurations = [...hero.querySelectorAll('[data-motion-beat]')]
|
const beatDurations = [...hero.querySelectorAll('[data-motion-beat]')]
|
||||||
.map((beat) => Number.parseFloat(getComputedStyle(beat).transitionDuration));
|
.map((beat) => Number.parseFloat(getComputedStyle(beat).transitionDuration));
|
||||||
const methodItems = [...document.querySelectorAll('[data-chapter="method"] [data-reveal]')];
|
const vertentesItems = [...document.querySelectorAll('[data-chapter="vertentes"] [data-reveal]')];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: hero?.classList.contains('is-active') ?? false,
|
active: hero?.classList.contains('is-active') ?? false,
|
||||||
beatDurations,
|
beatDurations,
|
||||||
methodIndexes: methodItems.map((item) => item.style.getPropertyValue('--motion-index')),
|
vertentesIndexes: vertentesItems.map((item) => item.style.getPropertyValue('--motion-index')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($state['active'])->toBeTrue();
|
expect($opening['active'])->toBeTrue();
|
||||||
expect($state['beatDurations'])->each->toBe(0.5);
|
expect($opening['beatDurations'])->each->toBe(0.5);
|
||||||
expect($state['methodIndexes'])->toBe(['0', '1', '2', '3', '3']);
|
expect($opening['vertentesIndexes'])->toBe(['0', '1', '2']);
|
||||||
});
|
|
||||||
|
|
||||||
it('reveals a section once and keeps it final after returning in the scroll', function (): void {
|
$beforeReveal = $page->script(<<<'JS'
|
||||||
$page = $this->visit('/', [
|
() => document.querySelector('#briefing-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$before = $page->script(<<<'JS'
|
|
||||||
() => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
$page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
document.querySelector('#final-cta-heading')?.scrollIntoView({ block: 'center' });
|
document.querySelector('#briefing-heading')?.scrollIntoView({ block: 'center' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
$page->script(<<<'JS'
|
||||||
() => new Promise((resolve) => {
|
() => new Promise((resolve) => {
|
||||||
const target = document.querySelector('#final-cta-heading')?.closest('[data-reveal]');
|
const target = document.querySelector('#briefing-heading')?.closest('[data-reveal]');
|
||||||
const wait = () => {
|
const wait = () => {
|
||||||
if (target?.classList.contains('is-revealed')) {
|
if (target?.classList.contains('is-revealed')) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@@ -124,68 +134,44 @@ it('reveals a section once and keeps it final after returning in the scroll', fu
|
|||||||
requestAnimationFrame(wait);
|
requestAnimationFrame(wait);
|
||||||
};
|
};
|
||||||
wait();
|
wait();
|
||||||
|
setTimeout(() => resolve(false), 2000);
|
||||||
})
|
})
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script('() => window.scrollTo({ top: 0, behavior: "instant" })');
|
$page->script('() => window.scrollTo({ top: 0, behavior: "instant" })');
|
||||||
|
|
||||||
$after = $page->script(<<<'JS'
|
$afterReveal = $page->script(<<<'JS'
|
||||||
() => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
() => document.querySelector('#briefing-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($before)->toBeFalse();
|
expect($beforeReveal)->toBeFalse();
|
||||||
expect($after)->toBeTrue();
|
expect($afterReveal)->toBeTrue();
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves testimonial directions with reduced lateral distance on mobile', function (): void {
|
$page->script(<<<'JS'
|
||||||
Testimonial::factory()->published()->create([
|
() => {
|
||||||
'quote' => 'Primeiro relato.',
|
const opening = document.querySelector('[data-motion="page-open"]');
|
||||||
'sort_order' => 10,
|
opening?.classList.remove('is-active');
|
||||||
]);
|
requestAnimationFrame(() => opening?.classList.add('is-active'));
|
||||||
Testimonial::factory()->published()->create([
|
return true;
|
||||||
'quote' => 'Segundo relato.',
|
}
|
||||||
'sort_order' => 20,
|
JS);
|
||||||
]);
|
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
$page->click('[data-testid="home-primary-cta"]');
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$readDistances = <<<'JS'
|
expect($page->script('() => window.location.hash'))->toBe('#sobre');
|
||||||
() => [...document.querySelectorAll('[data-chapter="testimonials"] blockquote')]
|
|
||||||
.map((item) => ({
|
|
||||||
direction: item.dataset.revealFrom,
|
|
||||||
distance: getComputedStyle(item).getPropertyValue('--motion-translate-x').trim(),
|
|
||||||
}))
|
|
||||||
JS;
|
|
||||||
|
|
||||||
$page->resize(1440, 1000);
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
|
||||||
$desktop = $page->script($readDistances);
|
|
||||||
|
|
||||||
$page->resize(390, 844);
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
|
||||||
$mobile = $page->script($readDistances);
|
|
||||||
|
|
||||||
expect($desktop)->toBe([
|
|
||||||
['direction' => 'left', 'distance' => 'calc(-1 * 24px)'],
|
|
||||||
['direction' => 'right', 'distance' => '24px'],
|
|
||||||
]);
|
|
||||||
expect($mobile)->toBe([
|
|
||||||
['direction' => 'left', 'distance' => 'calc(-1 * 16px)'],
|
|
||||||
['direction' => 'right', 'distance' => '16px'],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps content and navigation usable without javascript', function (): void {
|
it('keeps content and navigation usable without javascript', function (): void {
|
||||||
$this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'javaScriptEnabled' => false,
|
'javaScriptEnabled' => false,
|
||||||
])
|
]);
|
||||||
|
|
||||||
|
$page
|
||||||
->assertVisible('#hero-heading')
|
->assertVisible('#hero-heading')
|
||||||
->assertVisible('[data-testid="home-primary-cta"]')
|
->assertVisible('[data-testid="home-primary-cta"]')
|
||||||
->click('[data-testid="home-primary-cta"]');
|
->click('[data-testid="home-primary-cta"]');
|
||||||
|
|
||||||
expect($this->script('() => window.location.hash'))->toBe('#sobre');
|
expect($page->script('() => window.location.hash'))->toBe('#sobre');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps targets final when intersection observer is unavailable', function (): void {
|
it('keeps targets final when intersection observer is unavailable', function (): void {
|
||||||
@@ -213,25 +199,6 @@ it('keeps targets final when intersection observer is unavailable', function ():
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows cta activation while its opening is in progress', function (): void {
|
|
||||||
$page = $this->visit('/', [
|
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
|
||||||
() => {
|
|
||||||
const opening = document.querySelector('[data-motion="page-open"]');
|
|
||||||
opening?.classList.remove('is-active');
|
|
||||||
requestAnimationFrame(() => opening?.classList.add('is-active'));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
JS);
|
|
||||||
|
|
||||||
$page->click('[data-testid="home-primary-cta"]');
|
|
||||||
|
|
||||||
expect($page->script('() => window.location.hash'))->toBe('#sobre');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
$case = PortfolioCase::factory()->published()->create([
|
||||||
'slug' => 'casamento-jardim',
|
'slug' => 'casamento-jardim',
|
||||||
@@ -249,11 +216,19 @@ it('avoids horizontal overflow on visual public routes at desktop and mobile', f
|
|||||||
'/__missing-public-motion',
|
'/__missing-public-motion',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
$viewports = [[1440, 1000], [390, 844]];
|
||||||
foreach ($routes as $route) {
|
|
||||||
$page = $this->visit($route, [
|
$page = $this->visit($routes[0], [
|
||||||
'reducedMotion' => 'no-preference',
|
'reducedMotion' => 'no-preference',
|
||||||
])->resize($width, $height);
|
]);
|
||||||
|
|
||||||
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($viewports as [$width, $height]) {
|
||||||
|
$page->resize($width, $height);
|
||||||
|
|
||||||
$overflow = $page->script('() => document.documentElement.scrollWidth - document.documentElement.clientWidth');
|
$overflow = $page->script('() => document.documentElement.scrollWidth - document.documentElement.clientWidth');
|
||||||
|
|
||||||
@@ -261,34 +236,3 @@ it('avoids horizontal overflow on visual public routes at desktop and mobile', f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps rendered home chapters free of ornamental folios and counters', function (): void {
|
|
||||||
PortfolioCase::factory()->published()->create([
|
|
||||||
'is_featured' => true,
|
|
||||||
]);
|
|
||||||
Testimonial::factory()->published()->create([
|
|
||||||
'quote' => 'Relato publicado.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$editorial = $page->script(<<<'JS'
|
|
||||||
() => {
|
|
||||||
const flow = document.querySelector('.home-chapters');
|
|
||||||
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
|
|
||||||
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
reset: flow ? getComputedStyle(flow).counterReset : '',
|
|
||||||
increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement),
|
|
||||||
folioCount: folios.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
JS);
|
|
||||||
|
|
||||||
expect($editorial['reset'])->toBe('none');
|
|
||||||
expect($editorial['increments'])->each->toBe('none');
|
|
||||||
expect($editorial['folioCount'])->toBe(0);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ it('renders the public home page', function (): void {
|
|||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
$this->visit('/')
|
$this->visit('/')
|
||||||
->assertSee($settings->hero_title)
|
->assertSee('Eventos com intenção, cuidado e presença.')
|
||||||
->assertSee($settings->brand_name);
|
->assertSee($settings->brand_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user