Add skeleton, updates to client email formatting

This commit is contained in:
2026-05-04 16:30:05 +12:00
parent d1dd103a6e
commit bf9331bb5b
10 changed files with 875 additions and 20 deletions
+3 -5
View File
@@ -392,7 +392,7 @@ def client_email(data: BookingSubmission) -> str:
</h1>
<p style="margin:0 0 32px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:16px;color:#555;line-height:1.65;">
We&rsquo;ve received your enquiry and Aless will be in touch shortly to arrange
We&rsquo;ve received your enquiry and we will be in touch shortly to arrange
a <strong style="color:#213021;">Meet &amp; Greet</strong> with you and
{data.petName}.
</p>
@@ -431,7 +431,7 @@ def client_email(data: BookingSubmission) -> str:
</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#666;line-height:1.6;">
We will review your details and reach out within 1&ndash;2 business days
We will review your details and reach out within 1 business days
to schedule a free Meet &amp; Greet. No commitment required &mdash; just a
chance for {data.petName} to make a new best friend.
</div>
@@ -441,9 +441,7 @@ def client_email(data: BookingSubmission) -> str:
<p style="margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#888;line-height:1.6;">
Questions? Just reply to this email or reach us at
<a href="mailto:{REPLY_TO}" style="color:#213021;font-weight:600;
text-decoration:none;">{REPLY_TO}</a>.
Questions? Just reply to this email or reach us at 022 642 1011.
</p>
</td>
+38 -9
View File
@@ -40,11 +40,11 @@
}
.instagram-panel {
padding: 34px 360px 44px 44px;
padding: 42px 44px 112px;
border-radius: 32px;
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.52), transparent 42%),
linear-gradient(135deg, rgba(255, 250, 236, 0.96), rgba(255, 240, 188, 0.94));
linear-gradient(135deg, rgba(255, 252, 242, 0.98), rgba(255, 243, 198, 0.96));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 26px 50px rgba(106, 80, 16, 0.14);
@@ -53,7 +53,9 @@
.instagram-copy {
padding-top: 10px;
text-align: left;
max-width: 620px;
margin: 0 auto;
text-align: center;
}
.instagram-kicker {
@@ -70,12 +72,16 @@
}
.instagram-copy :global(h2) {
max-width: 11ch;
max-width: 12ch;
margin-left: auto;
margin-right: auto;
}
.instagram-blurb {
max-width: 420px;
max-width: 520px;
margin-bottom: 0;
margin-left: auto;
margin-right: auto;
}
.instagram-button {
@@ -84,13 +90,30 @@
.instagram-dog-wrap {
position: absolute;
right: 24px;
bottom: -12px;
left: 50%;
bottom: -74px;
display: flex;
align-items: flex-end;
justify-content: center;
width: clamp(240px, 30vw, 360px);
width: clamp(300px, 36vw, 430px);
pointer-events: none;
transform: translateX(-50%);
z-index: 1;
}
.instagram-dog-wrap::before {
content: '';
position: absolute;
left: 50%;
bottom: 54px;
width: 92%;
height: 56%;
border-radius: 999px 999px 40px 40px;
background:
radial-gradient(circle at 50% 35%, rgba(255, 244, 194, 0.95), rgba(255, 224, 122, 0.88));
transform: translateX(-50%);
filter: blur(2px);
z-index: -1;
}
.instagram-dog {
@@ -117,7 +140,7 @@
}
.instagram-copy {
text-align: center;
max-width: none;
}
.instagram-copy :global(h2) {
@@ -142,6 +165,12 @@
transform: translateX(-50%);
}
.instagram-dog-wrap::before {
width: 86%;
height: 52%;
bottom: 8px;
}
.instagram-dog {
width: 100%;
}
+730
View File
@@ -0,0 +1,730 @@
<script lang="ts">
export let pathname = '/';
const serviceRoutes = new Set(['/pack-walks', '/dog-walking', '/puppy-visits']);
const legalRoutes = new Set(['/terms-and-conditions', '/privacy-policy']);
const heroLines = [0, 1, 2];
const shortLines = [0, 1];
const cardTriplet = [0, 1, 2];
const cardQuartet = [0, 1, 2, 3];
const doubleStack = [0, 1];
function getVariant(path: string) {
if (path === '/') return 'home';
if (serviceRoutes.has(path)) return 'service';
if (path === '/about' || path === '/about-us') return 'about';
if (path === '/our-pricing') return 'pricing';
if (path === '/contact-us') return 'contact';
if (legalRoutes.has(path)) return 'legal';
return 'generic';
}
$: variant = getVariant(pathname);
</script>
<div class="route-skeleton" data-skeleton-variant={variant}>
<div class="skeleton-shell">
<header class="skeleton-header">
<div class="skeleton-inner skeleton-nav">
<div class="skeleton-row skeleton-nav-links" aria-hidden="true">
{#each cardQuartet as _}
<span class="skeleton-pill skeleton-shimmer"></span>
{/each}
</div>
<span class="skeleton-logo skeleton-shimmer" aria-hidden="true"></span>
<div class="skeleton-row skeleton-nav-actions" aria-hidden="true">
<span class="skeleton-circle skeleton-shimmer"></span>
<span class="skeleton-button skeleton-shimmer"></span>
</div>
</div>
</header>
{#if variant === 'home'}
<main class="skeleton-main">
<section class="skeleton-section skeleton-home-hero">
<div class="skeleton-inner skeleton-home-hero-grid">
<div class="skeleton-copy">
<span class="skeleton-kicker skeleton-shimmer"></span>
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
<span class="skeleton-title skeleton-title-md skeleton-shimmer"></span>
{#each heroLines as _}
<span class="skeleton-line skeleton-shimmer"></span>
{/each}
<div class="skeleton-row skeleton-hero-actions">
<span class="skeleton-button skeleton-button-wide skeleton-shimmer"></span>
<span class="skeleton-button skeleton-button-outline skeleton-shimmer"></span>
</div>
</div>
<div class="skeleton-media skeleton-media-hero skeleton-shimmer"></div>
</div>
</section>
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-strip skeleton-shimmer"></div>
</div>
</section>
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-section-heading skeleton-section-heading-left">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
{#each shortLines as _}
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
{/each}
</div>
<div class="skeleton-grid skeleton-grid-3">
{#each cardTriplet as _}
<article class="skeleton-card skeleton-shimmer"></article>
{/each}
</div>
</div>
</section>
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-section-heading">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
<div class="skeleton-grid skeleton-grid-2">
{#each doubleStack as _}
<article class="skeleton-quote-card skeleton-shimmer">
<span class="skeleton-line skeleton-line-medium"></span>
<span class="skeleton-line"></span>
<span class="skeleton-line skeleton-line-medium"></span>
</article>
{/each}
</div>
</div>
</section>
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-booking">
<div class="skeleton-booking-header">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
<div class="skeleton-row skeleton-stepper">
<span class="skeleton-chip skeleton-shimmer"></span>
<span class="skeleton-chip skeleton-shimmer"></span>
</div>
</div>
<div class="skeleton-grid skeleton-grid-2">
<article class="skeleton-form-card skeleton-shimmer"></article>
<article class="skeleton-form-card skeleton-shimmer"></article>
</div>
</div>
</div>
</section>
</main>
{:else if variant === 'service'}
<main class="skeleton-main">
<section class="skeleton-section skeleton-service-hero">
<div class="skeleton-inner skeleton-home-hero-grid">
<div class="skeleton-copy">
<span class="skeleton-kicker skeleton-shimmer"></span>
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
{#each heroLines as _}
<span class="skeleton-line skeleton-shimmer"></span>
{/each}
</div>
<div class="skeleton-media skeleton-media-hero skeleton-shimmer"></div>
</div>
</section>
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-section-heading">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
<div class="skeleton-grid skeleton-grid-3">
{#each cardTriplet as _}
<article class="skeleton-plan-card skeleton-shimmer"></article>
{/each}
</div>
</div>
</section>
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-grid skeleton-grid-3">
{#each cardTriplet as _}
<article class="skeleton-card skeleton-card-short skeleton-shimmer"></article>
{/each}
</div>
</div>
</section>
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-booking skeleton-shimmer"></div>
</div>
</section>
</main>
{:else if variant === 'about'}
<main class="skeleton-main">
<section class="skeleton-section skeleton-about-hero">
<div class="skeleton-inner skeleton-centered-heading">
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
</div>
</section>
{#each doubleStack as index}
<section class="skeleton-section">
<div class:skeleton-about-grid-reverse={index % 2 === 1} class="skeleton-inner skeleton-about-grid">
<div class="skeleton-copy">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
{#each heroLines as _}
<span class="skeleton-line skeleton-shimmer"></span>
{/each}
</div>
<div class="skeleton-media skeleton-media-tall skeleton-shimmer"></div>
</div>
</section>
{/each}
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-section-heading">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
</div>
<div class="skeleton-grid skeleton-grid-3">
{#each cardTriplet as _}
<article class="skeleton-card skeleton-shimmer"></article>
{/each}
</div>
</div>
</section>
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-contact-card skeleton-shimmer"></div>
</div>
</section>
</main>
{:else if variant === 'pricing'}
<main class="skeleton-main">
<section class="skeleton-section skeleton-about-hero">
<div class="skeleton-inner skeleton-centered-heading">
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
</section>
{#each doubleStack as _}
<section class="skeleton-section">
<div class="skeleton-inner">
<div class="skeleton-section-heading skeleton-section-heading-left">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
<div class="skeleton-grid skeleton-grid-3">
{#each cardTriplet as _}
<article class="skeleton-plan-card skeleton-shimmer"></article>
{/each}
</div>
</div>
</section>
{/each}
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-booking skeleton-shimmer"></div>
</div>
</section>
</main>
{:else if variant === 'contact'}
<main class="skeleton-main">
<section class="skeleton-section skeleton-about-hero">
<div class="skeleton-inner skeleton-centered-heading">
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
</section>
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-booking">
<div class="skeleton-booking-header">
<span class="skeleton-title skeleton-title-sm skeleton-shimmer"></span>
<div class="skeleton-row skeleton-stepper">
<span class="skeleton-chip skeleton-shimmer"></span>
<span class="skeleton-chip skeleton-shimmer"></span>
</div>
</div>
<div class="skeleton-grid skeleton-grid-2">
<article class="skeleton-form-card skeleton-shimmer"></article>
<article class="skeleton-form-card skeleton-shimmer"></article>
</div>
</div>
</div>
</section>
</main>
{:else}
<main class="skeleton-main">
<section class="skeleton-section skeleton-about-hero">
<div class="skeleton-inner skeleton-centered-heading">
<span class="skeleton-title skeleton-title-lg skeleton-shimmer"></span>
<span class="skeleton-line skeleton-line-short skeleton-shimmer"></span>
</div>
</section>
<section class="skeleton-section skeleton-section-last">
<div class="skeleton-inner">
<div class="skeleton-legal-card skeleton-shimmer">
{#each cardQuartet as _}
<span class="skeleton-line skeleton-line-long"></span>
{/each}
{#each heroLines as _}
<span class="skeleton-line"></span>
{/each}
</div>
</div>
</section>
</main>
{/if}
<footer class="skeleton-footer">
<div class="skeleton-inner skeleton-footer-grid">
<article class="skeleton-footer-card skeleton-shimmer"></article>
<article class="skeleton-footer-card skeleton-shimmer"></article>
<article class="skeleton-footer-card skeleton-shimmer"></article>
</div>
</footer>
</div>
</div>
<style>
.route-skeleton {
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(229, 214, 194, 0.45), transparent 34%),
linear-gradient(180deg, #fcfaf6 0%, #f7f3eb 100%);
color: transparent;
}
.skeleton-shell {
min-height: 100vh;
}
.skeleton-header {
position: sticky;
top: 0;
z-index: 1;
backdrop-filter: blur(14px);
background: rgba(252, 250, 246, 0.84);
border-bottom: 1px solid rgba(33, 48, 33, 0.08);
}
.skeleton-main,
.skeleton-footer {
padding: 0 0 40px;
}
.skeleton-section {
padding: 32px 0 0;
}
.skeleton-section-last {
padding-bottom: 20px;
}
.skeleton-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
}
.skeleton-nav,
.skeleton-row,
.skeleton-home-hero-grid,
.skeleton-about-grid,
.skeleton-footer-grid,
.skeleton-booking-header,
.skeleton-grid {
display: grid;
gap: 22px;
}
.skeleton-nav {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
min-height: 92px;
}
.skeleton-row {
display: flex;
align-items: center;
}
.skeleton-nav-links {
gap: 14px;
}
.skeleton-nav-actions {
gap: 14px;
justify-content: end;
}
.skeleton-logo {
display: block;
width: 188px;
height: 46px;
border-radius: 18px;
}
.skeleton-pill,
.skeleton-circle,
.skeleton-button,
.skeleton-logo,
.skeleton-kicker,
.skeleton-title,
.skeleton-line,
.skeleton-media,
.skeleton-strip,
.skeleton-card,
.skeleton-plan-card,
.skeleton-quote-card,
.skeleton-chip,
.skeleton-form-card,
.skeleton-booking,
.skeleton-contact-card,
.skeleton-legal-card,
.skeleton-footer-card {
display: block;
position: relative;
overflow: hidden;
background: rgba(33, 48, 33, 0.08);
}
.skeleton-shimmer::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.58) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: skeleton-shimmer 1.25s ease-in-out infinite;
}
.skeleton-pill {
width: 88px;
height: 14px;
border-radius: 999px;
}
.skeleton-circle {
width: 40px;
height: 40px;
border-radius: 999px;
}
.skeleton-button {
width: 148px;
height: 48px;
border-radius: 999px;
}
.skeleton-button-wide {
width: 182px;
}
.skeleton-button-outline {
width: 154px;
}
.skeleton-home-hero,
.skeleton-service-hero,
.skeleton-about-hero {
padding-top: 52px;
}
.skeleton-home-hero-grid,
.skeleton-about-grid {
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
align-items: center;
}
.skeleton-about-grid-reverse {
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
}
.skeleton-copy {
display: grid;
gap: 16px;
align-content: start;
}
.skeleton-kicker {
width: 96px;
height: 14px;
border-radius: 999px;
}
.skeleton-title {
border-radius: 18px;
}
.skeleton-title-lg {
width: min(540px, 100%);
height: 64px;
}
.skeleton-title-md {
width: min(460px, 88%);
height: 54px;
}
.skeleton-title-sm {
width: min(320px, 70%);
height: 44px;
}
.skeleton-line {
width: min(620px, 100%);
height: 16px;
border-radius: 999px;
}
.skeleton-line-short {
width: min(420px, 72%);
}
.skeleton-line-medium {
width: 86%;
}
.skeleton-line-long {
width: 100%;
}
.skeleton-hero-actions {
gap: 16px;
margin-top: 10px;
}
.skeleton-media {
border-radius: 30px;
}
.skeleton-media-hero {
min-height: 500px;
}
.skeleton-media-tall {
min-height: 360px;
}
.skeleton-strip {
height: 112px;
border-radius: 30px;
}
.skeleton-section-heading {
display: grid;
gap: 16px;
justify-items: center;
margin-bottom: 30px;
}
.skeleton-section-heading-left {
justify-items: start;
}
.skeleton-centered-heading {
display: grid;
gap: 16px;
justify-items: center;
}
.skeleton-grid-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.skeleton-grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.skeleton-card,
.skeleton-plan-card,
.skeleton-quote-card,
.skeleton-form-card {
border-radius: 30px;
min-height: 220px;
}
.skeleton-card-short {
min-height: 180px;
}
.skeleton-plan-card {
min-height: 280px;
}
.skeleton-quote-card {
min-height: 190px;
padding: 28px;
display: grid;
align-content: start;
gap: 16px;
}
.skeleton-booking {
padding: 30px;
border-radius: 34px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(33, 48, 33, 0.08);
box-shadow: 0 16px 38px rgba(33, 48, 33, 0.06);
}
.skeleton-booking-header {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
margin-bottom: 26px;
}
.skeleton-stepper {
gap: 12px;
}
.skeleton-chip {
width: 108px;
height: 38px;
border-radius: 999px;
}
.skeleton-form-card {
min-height: 260px;
}
.skeleton-contact-card {
min-height: 220px;
border-radius: 36px;
}
.skeleton-legal-card {
min-height: 420px;
border-radius: 36px;
padding: 36px;
display: grid;
align-content: start;
gap: 18px;
}
.skeleton-footer {
padding-top: 12px;
}
.skeleton-footer-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.skeleton-footer-card {
min-height: 210px;
border-radius: 30px;
}
@keyframes skeleton-shimmer {
100% {
transform: translateX(100%);
}
}
@media (prefers-reduced-motion: reduce) {
.skeleton-shimmer::after {
animation: none;
}
}
@media (max-width: 1024px) {
.skeleton-home-hero-grid,
.skeleton-about-grid,
.skeleton-about-grid-reverse,
.skeleton-grid-3,
.skeleton-footer-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.skeleton-home-hero-grid,
.skeleton-about-grid,
.skeleton-about-grid-reverse {
align-items: start;
}
}
@media (max-width: 768px) {
.skeleton-inner {
padding: 0 24px;
}
.skeleton-nav {
grid-template-columns: 1fr auto;
gap: 16px;
min-height: 78px;
}
.skeleton-nav-links {
display: none;
}
.skeleton-nav-actions .skeleton-circle {
display: none;
}
.skeleton-home-hero,
.skeleton-service-hero,
.skeleton-about-hero {
padding-top: 28px;
}
.skeleton-home-hero-grid,
.skeleton-about-grid,
.skeleton-about-grid-reverse,
.skeleton-grid-2,
.skeleton-grid-3,
.skeleton-footer-grid,
.skeleton-booking-header {
grid-template-columns: 1fr;
}
.skeleton-title-lg {
height: 46px;
}
.skeleton-title-md,
.skeleton-title-sm {
height: 34px;
}
.skeleton-media-hero {
min-height: 320px;
}
.skeleton-media-tall,
.skeleton-card,
.skeleton-quote-card,
.skeleton-plan-card,
.skeleton-form-card,
.skeleton-footer-card {
min-height: 180px;
}
.skeleton-strip {
height: 80px;
}
.skeleton-booking,
.skeleton-legal-card {
padding: 24px;
}
}
</style>
+50
View File
@@ -1,7 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { navigating, page } from '$app/stores';
import { afterNavigate, disableScrollHandling } from '$app/navigation';
import { initClickTracking, trackPageView } from '$lib/analytics';
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte';
import '$lib/styles/variables.css';
import '$lib/styles/base.css';
import '$lib/styles/layout.css';
@@ -13,6 +15,26 @@
onMount(() => initClickTracking());
function shouldShowSkeleton() {
const navigation = $navigating;
if (!navigation?.to?.url) {
return false;
}
const fromPath = navigation.from?.url.pathname ?? $page.url.pathname;
const toPath = navigation.to.url.pathname;
if (navigation.to.url.hash && toPath === fromPath) {
return false;
}
return toPath !== fromPath;
}
$: loadingPath = $navigating?.to?.url.pathname ?? $page.url.pathname;
$: showRouteSkeleton = shouldShowSkeleton();
afterNavigate(({ from, to }) => {
if (!from || !to || to.url.hash) {
return;
@@ -38,4 +60,32 @@
});
</script>
<div class="layout-shell">
<div class:layout-content-loading={showRouteSkeleton} class="layout-content" aria-hidden={showRouteSkeleton}>
<slot />
</div>
{#if showRouteSkeleton}
<div class="layout-skeleton-layer" role="status" aria-label="Loading page" aria-live="polite">
<RouteSkeleton pathname={loadingPath} />
</div>
{/if}
</div>
<style>
.layout-shell {
position: relative;
}
.layout-content-loading {
visibility: hidden;
}
.layout-skeleton-layer {
position: fixed;
inset: 0;
z-index: 30;
overflow-y: auto;
background: rgba(251, 251, 251, 0.98);
}
</style>
+22
View File
@@ -1,5 +1,6 @@
import { render } from '@testing-library/svelte';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { setMockNavigating, setMockPage } from '../test/mocks/app-stores';
const afterNavigate = vi.fn();
const disableScrollHandling = vi.fn();
@@ -49,4 +50,25 @@ describe('root layout navigation behavior', () => {
expect(disableScrollHandling).not.toHaveBeenCalled();
});
it('shows a route skeleton while navigating to a new page', async () => {
setMockPage('https://www.goodwalk.co.nz/about');
setMockNavigating('https://www.goodwalk.co.nz/contact-us', 'https://www.goodwalk.co.nz/about');
const { default: Layout } = await import('./+layout.svelte');
const { getByLabelText, container } = render(Layout);
expect(getByLabelText('Loading page')).toBeInTheDocument();
expect(container.querySelector('[data-skeleton-variant="contact"]')).toBeInTheDocument();
});
it('does not show the skeleton for hash-only navigation', async () => {
setMockPage('https://www.goodwalk.co.nz/about');
setMockNavigating('https://www.goodwalk.co.nz/about#team', 'https://www.goodwalk.co.nz/about');
const { default: Layout } = await import('./+layout.svelte');
const { queryByLabelText } = render(Layout);
expect(queryByLabelText('Loading page')).not.toBeInTheDocument();
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ import { GET } from './+server';
describe('robots endpoint', () => {
it('returns the crawl policy and sitemap location', async () => {
const response = GET();
const response = await GET({} as never);
const body = await response.text();
expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
+1 -1
View File
@@ -10,7 +10,7 @@ describe('sitemap endpoint', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-01T09:15:00Z'));
const response = GET();
const response = await GET({} as never);
const body = await response.text();
expect(response.headers.get('content-type')).toBe('application/xml; charset=utf-8');
+20 -1
View File
@@ -31,7 +31,12 @@ export function createMockPage(
}
export const pageStore = writable<MockPageStoreValue>(createMockPage());
export const navigatingStore = writable<null>(null);
export type MockNavigatingStoreValue = {
from: { url: URL } | null;
to: { url: URL } | null;
} | null;
export const navigatingStore = writable<MockNavigatingStoreValue>(null);
const updatedWritable = writable(false);
export const updatedStore = {
@@ -46,3 +51,17 @@ export function setMockPage(url: string, overrides: Partial<MockPageStoreValue>
export function resetMockPage() {
pageStore.set(createMockPage());
}
export function setMockNavigating(
to: string,
from = 'https://www.goodwalk.co.nz/'
) {
navigatingStore.set({
from: { url: new URL(from) },
to: { url: new URL(to) }
});
}
export function resetMockNavigating() {
navigatingStore.set(null);
}
+1 -1
View File
@@ -9,6 +9,6 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"types": ["node"]
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
}
}
+8 -1
View File
@@ -1,7 +1,13 @@
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/svelte';
import { afterEach, beforeAll, vi } from 'vitest';
import { navigatingStore, pageStore, resetMockPage, updatedStore } from './src/test/mocks/app-stores';
import {
navigatingStore,
pageStore,
resetMockNavigating,
resetMockPage,
updatedStore
} from './src/test/mocks/app-stores';
vi.mock('$app/stores', () => ({
page: { subscribe: pageStore.subscribe },
@@ -81,6 +87,7 @@ beforeAll(() => {
afterEach(() => {
cleanup();
resetMockPage();
resetMockNavigating();
vi.clearAllMocks();
document.head.innerHTML = '';
});