fix(website): restore contracts and focus indicators

This commit is contained in:
edde746
2026-07-25 16:17:05 +02:00
parent 0717009ade
commit 39ddfd2bd9
15 changed files with 433 additions and 74 deletions
+1
View File
@@ -8,6 +8,7 @@
"build": "vite build", "build": "vite build",
"audit": "python3 ../scripts/check_bun_audit.py --project . --baseline bun_audit_baseline.json", "audit": "python3 ../scripts/check_bun_audit.py --project . --baseline bun_audit_baseline.json",
"preview": "vite preview", "preview": "vite preview",
"test": "bun test",
"prepare": "svelte-kit sync || echo ''", "prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
@@ -163,7 +163,6 @@
.store-button:focus-visible { .store-button:focus-visible {
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: #fff; background: #fff;
outline: none;
} }
.store-button :global(svg) { .store-button :global(svg) {
@@ -181,7 +180,6 @@
.linux-button.active { .linux-button.active {
color: var(--color-text); color: var(--color-text);
background: var(--color-surface-hover); background: var(--color-surface-hover);
outline: none;
} }
.desktop-button:hover, .desktop-button:hover,
@@ -271,7 +269,6 @@
.linux-menu-item:focus-visible { .linux-menu-item:focus-visible {
color: var(--color-text); color: var(--color-text);
background: rgb(237 237 237 / 0.12); background: rgb(237 237 237 / 0.12);
outline: none;
} }
@media (max-width: 460px) { @media (max-width: 460px) {
+1 -2
View File
@@ -152,7 +152,7 @@
.faq-toggle:focus-visible { .faq-toggle:focus-visible {
background: rgb(237 237 237 / 0.14); background: rgb(237 237 237 / 0.14);
outline: none; outline-offset: -2px;
} }
.faq-question { .faq-question {
@@ -220,6 +220,5 @@
.faq-answer-content :global(a:hover), .faq-answer-content :global(a:hover),
.faq-answer-content :global(a:focus-visible) { .faq-answer-content :global(a:focus-visible) {
text-decoration-color: var(--color-text); text-decoration-color: var(--color-text);
outline: none;
} }
</style> </style>
-1
View File
@@ -67,7 +67,6 @@
.footer-nav a:focus-visible { .footer-nav a:focus-visible {
color: var(--color-text); color: var(--color-text);
background: rgb(237 237 237 / 0.12); background: rgb(237 237 237 / 0.12);
outline: none;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
@@ -1 +0,0 @@
<div class="noise-overlay" aria-hidden="true"></div>
@@ -406,7 +406,6 @@
.carousel-dot:focus-visible { .carousel-dot:focus-visible {
color: var(--color-text); color: var(--color-text);
background: rgb(237 237 237 / 0.18); background: rgb(237 237 237 / 0.18);
outline: none;
} }
.carousel-dot:focus-visible { .carousel-dot:focus-visible {
@@ -346,7 +346,6 @@
.device-button:not(.active):focus-visible { .device-button:not(.active):focus-visible {
color: var(--color-text); color: var(--color-text);
background: rgb(237 237 237 / 0.12); background: rgb(237 237 237 / 0.12);
outline: none;
} }
.device-button.active { .device-button.active {
@@ -358,7 +357,6 @@
.device-button.active:focus-visible { .device-button.active:focus-visible {
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: #fff; background: #fff;
outline: none;
} }
.device-button :global(svg), .device-button :global(svg),
@@ -401,7 +399,6 @@
.scroll-arrow.enabled:focus-visible { .scroll-arrow.enabled:focus-visible {
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: var(--color-surface-highest); background: var(--color-surface-highest);
outline: none;
} }
.screenshot-panels { .screenshot-panels {
@@ -0,0 +1,104 @@
import { normalizeUsdStorePrice } from '../content/software_app_offers';
export type HomepageAggregateRating = {
ratingValue: string;
ratingCount: number;
};
export type HomepageStoreMetadata = {
aggregateRating: HomepageAggregateRating | null;
appStorePrice: string | null;
playStorePrice: string | null;
};
export type HomepageStoreFetch = (url: string) => Promise<Response>;
export type PlayStoreListing = {
available?: boolean;
score?: number;
ratings?: number;
price?: unknown;
currency?: unknown;
};
export type HomepageStoreMetadataDependencies = {
fetch: HomepageStoreFetch;
loadPlayStoreListing: () => Promise<PlayStoreListing>;
};
type StoreRating = {
score: number;
count: number;
};
type AppStoreLookupResult = {
averageUserRating?: number;
userRatingCount?: number;
price?: unknown;
currency?: unknown;
};
type AppStoreLookupResponse = {
results?: AppStoreLookupResult[];
};
const APP_STORE_LOOKUP_URL = 'https://itunes.apple.com/lookup?id=6754315964';
export async function loadHomepageStoreMetadata({
fetch,
loadPlayStoreListing
}: HomepageStoreMetadataDependencies): Promise<HomepageStoreMetadata> {
let appStoreRating: StoreRating | null = null;
let playStoreRating: StoreRating | null = null;
let appStorePrice: string | null = null;
let playStorePrice: string | null = null;
try {
const response = await fetch(APP_STORE_LOOKUP_URL);
if (!response.ok) throw new Error(`App Store lookup failed: HTTP ${response.status}`);
const data = (await response.json()) as AppStoreLookupResponse;
const app = data.results?.[0];
if (app?.averageUserRating && app.userRatingCount) {
appStoreRating = {
score: app.averageUserRating,
count: app.userRatingCount
};
}
appStorePrice = normalizeUsdStorePrice(app?.price, app?.currency);
} catch {
// App Store metadata is optional and must not suppress Play Store metadata.
}
try {
const app = await loadPlayStoreListing();
if (app.available === false) throw new Error('Google Play listing unavailable');
if (app.score && app.ratings) {
playStoreRating = {
score: app.score,
count: app.ratings
};
}
playStorePrice = normalizeUsdStorePrice(app.price, app.currency);
} catch {
// Play Store metadata is optional and must not suppress App Store metadata.
}
const ratings = [appStoreRating, playStoreRating].filter(
(rating): rating is StoreRating => rating !== null
);
const aggregateRating = aggregateStoreRatings(ratings);
return { aggregateRating, appStorePrice, playStorePrice };
}
function aggregateStoreRatings(ratings: readonly StoreRating[]): HomepageAggregateRating | null {
if (ratings.length === 0) return null;
const ratingCount = ratings.reduce((sum, rating) => sum + rating.count, 0);
const weightedSum = ratings.reduce((sum, rating) => sum + rating.score * rating.count, 0);
return {
ratingValue: (weightedSum / ratingCount).toFixed(1),
ratingCount
};
}
-1
View File
@@ -93,6 +93,5 @@
.home-link:focus-visible { .home-link:focus-visible {
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: #fff; background: #fff;
outline: none;
} }
</style> </style>
+14 -59
View File
@@ -1,62 +1,17 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { normalizeUsdStorePrice } from '$lib/content/software_app_offers'; import { loadHomepageStoreMetadata } from '$lib/server/homepage_store_metadata';
export const load: PageServerLoad = async ({ fetch }) => { export const load: PageServerLoad = async ({ fetch }) =>
let appStoreRating: { score: number; count: number } | null = null; loadHomepageStoreMetadata({
let playStoreRating: { score: number; count: number } | null = null; fetch,
let appStorePrice: string | null = null; loadPlayStoreListing: async () => {
let playStorePrice: string | null = null; // Module initialization is optional external data and stays inside the
// helper's Play Store failure boundary.
try { const { default: gplay } = await import('google-play-scraper');
const res = await fetch('https://itunes.apple.com/lookup?id=6754315964'); return gplay.app({
if (!res.ok) throw new Error(`App Store lookup failed: HTTP ${res.status}`); appId: 'com.edde746.plezy',
const data = await res.json(); country: 'us',
const app = data.results?.[0]; lang: 'en'
if (app?.averageUserRating && app?.userRatingCount) { });
appStoreRating = {
score: app.averageUserRating,
count: app.userRatingCount
};
} }
appStorePrice = normalizeUsdStorePrice(app?.price, app?.currency); });
} catch {
// App Store fetch failed, continue without it
}
try {
// Module initialization is optional external data and must stay inside this failure boundary.
const { default: gplay } = await import('google-play-scraper');
const app = await gplay.app({
appId: 'com.edde746.plezy',
country: 'us',
lang: 'en'
});
if (app.available === false) throw new Error('Google Play listing unavailable');
if (app.score && app.ratings) {
playStoreRating = {
score: app.score,
count: app.ratings
};
}
playStorePrice = normalizeUsdStorePrice(app.price, app.currency);
} catch {
// Play Store fetch failed, continue without it
}
// Compute combined weighted average
let aggregateRating: { ratingValue: string; ratingCount: number } | null = null;
const ratings = [appStoreRating, playStoreRating].filter(Boolean) as {
score: number;
count: number;
}[];
if (ratings.length > 0) {
const totalCount = ratings.reduce((sum, r) => sum + r.count, 0);
const weightedSum = ratings.reduce((sum, r) => sum + r.score * r.count, 0);
aggregateRating = {
ratingValue: (weightedSum / totalCount).toFixed(1),
ratingCount: totalCount
};
}
return { aggregateRating, appStorePrice, playStorePrice };
};
+11
View File
@@ -90,6 +90,17 @@ button {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
:focus-visible {
outline: 2px solid var(--color-text);
outline-offset: 2px;
}
@media (forced-colors: active) {
:focus-visible {
outline-color: Highlight;
}
}
button, button,
input, input,
textarea, textarea,
-2
View File
@@ -164,7 +164,6 @@
.back-link:focus-visible { .back-link:focus-visible {
color: var(--color-text); color: var(--color-text);
background: var(--color-surface-highest); background: var(--color-surface-highest);
outline: none;
} }
.back-logo :global(img) { .back-logo :global(img) {
@@ -222,6 +221,5 @@
.prose a:hover, .prose a:hover,
.prose a:focus-visible { .prose a:focus-visible {
text-decoration-color: var(--color-text); text-decoration-color: var(--color-text);
outline: none;
} }
</style> </style>
-1
View File
@@ -146,7 +146,6 @@
.store-button:focus-visible { .store-button:focus-visible {
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: #fff; background: #fff;
outline: none;
} }
.store-button :global(svg) { .store-button :global(svg) {
+172
View File
@@ -0,0 +1,172 @@
import { describe, expect, test } from 'bun:test';
import {
detectMobileStorePlatform,
linuxArchitectures,
storeOptionsForPlatform
} from '../src/lib/content/downloads';
import {
faqSchemaMainEntity,
faqs,
watchTogetherFaqAnswer
} from '../src/lib/content/faqs';
import {
buildSoftwareApplicationOffers,
normalizeUsdStorePrice
} from '../src/lib/content/software_app_offers';
import { csr as privacyCsr } from '../src/routes/privacy/+page';
describe('mobile store selection', () => {
test('treats missing and unrecognized client evidence as unknown', () => {
expect(detectMobileStorePlatform()).toBe('unknown');
expect(
detectMobileStorePlatform({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
platform: 'Win32',
maxTouchPoints: 0
})
).toBe('unknown');
});
test('detects iOS and Android user agents', () => {
expect(
detectMobileStorePlatform({
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)'
})
).toBe('ios');
expect(
detectMobileStorePlatform({
userAgent: 'Mozilla/5.0 (Linux; Android 15; Pixel 9)'
})
).toBe('android');
});
test('distinguishes desktop-mode iPadOS from a non-touch Mac', () => {
const desktopSafari = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)';
expect(
detectMobileStorePlatform({
userAgent: desktopSafari,
platform: 'MacIntel',
maxTouchPoints: 5
})
).toBe('ios');
expect(
detectMobileStorePlatform({
userAgent: desktopSafari,
platform: 'MacIntel',
maxTouchPoints: 0
})
).toBe('unknown');
});
test('shows only the matching store when known and both stores when unknown', () => {
expect(storeOptionsForPlatform('ios').map((option) => option.id)).toEqual(['app-store']);
expect(storeOptionsForPlatform('android').map((option) => option.id)).toEqual([
'play-store'
]);
expect(storeOptionsForPlatform('unknown').map((option) => option.id)).toEqual([
'app-store',
'play-store'
]);
});
});
describe('Linux download inventory', () => {
test('keeps a unique four-format artifact matrix for x64 and ARM64', () => {
const artifactNames = linuxArchitectures.map((architecture) =>
architecture.formats.map(({ url }) => url.slice(url.lastIndexOf('/') + 1))
);
expect(linuxArchitectures.map(({ label }) => label)).toEqual(['x64 (Intel/AMD)', 'ARM64']);
expect(artifactNames).toEqual([
[
'plezy-linux-x64.deb',
'plezy-linux-x64.rpm',
'plezy-linux-x64.pkg.tar.zst',
'plezy-linux-x64.tar.gz'
],
[
'plezy-linux-arm64.deb',
'plezy-linux-arm64.rpm',
'plezy-linux-arm64.pkg.tar.zst',
'plezy-linux-arm64.tar.gz'
]
]);
const urls = linuxArchitectures.flatMap((architecture) =>
architecture.formats.map(({ url }) => url)
);
expect(new Set(urls).size).toBe(8);
expect(
urls.every((url) =>
url.startsWith('https://github.com/edde746/plezy/releases/latest/download/')
)
).toBe(true);
});
});
describe('software application offers', () => {
test('accepts only finite nonnegative numeric USD prices', () => {
expect(normalizeUsdStorePrice(0, 'USD')).toBe('0');
expect(normalizeUsdStorePrice(4.99, 'USD')).toBe('4.99');
for (const value of [-1, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '4.99', null]) {
expect(normalizeUsdStorePrice(value, 'USD')).toBeNull();
}
for (const currency of ['EUR', 'usd', '', null, undefined]) {
expect(normalizeUsdStorePrice(4.99, currency)).toBeNull();
}
});
test('keeps unavailable paid-store links without describing them as free', () => {
const offers = buildSoftwareApplicationOffers({
appStorePrice: null,
playStorePrice: null
});
expect(offers.find(({ category }) => category === 'App Store')).toEqual({
'@type': 'Offer',
url: 'https://apps.apple.com/us/app/id6754315964',
category: 'App Store'
});
expect(offers.find(({ category }) => category === 'Google Play')).toEqual({
'@type': 'Offer',
url: 'https://play.google.com/store/apps/details?id=com.edde746.plezy',
category: 'Google Play'
});
expect(offers.filter(({ price }) => price === '0').map(({ category }) => category)).toEqual([
'GitHub'
]);
});
test('attaches valid USD prices to each paid mobile store', () => {
const offers = buildSoftwareApplicationOffers({
appStorePrice: '4.99',
playStorePrice: '3.99'
});
expect(offers.find(({ category }) => category === 'App Store')).toMatchObject({
price: '4.99',
priceCurrency: 'USD'
});
expect(offers.find(({ category }) => category === 'Google Play')).toMatchObject({
price: '3.99',
priceCurrency: 'USD'
});
});
});
describe('route content contracts', () => {
test('uses the same Watch Together answer in the visible FAQ and FAQ schema', () => {
const visibleFaq = faqs.find(({ id }) => id === 'watch-together');
expect(visibleFaq).toBeDefined();
expect(visibleFaq?.answer).toBe(watchTogetherFaqAnswer);
const schemaFaq = faqSchemaMainEntity.find(({ name }) => name === visibleFaq?.question);
expect(schemaFaq).toBeDefined();
expect(schemaFaq?.acceptedAnswer.text).toBe(watchTogetherFaqAnswer);
});
test('keeps the privacy route server-rendered without client hydration', () => {
expect(privacyCsr).toBe(false);
});
});
@@ -0,0 +1,130 @@
import { describe, expect, test } from 'bun:test';
import {
loadHomepageStoreMetadata,
type HomepageStoreFetch,
type PlayStoreListing
} from '../src/lib/server/homepage_store_metadata';
const APP_STORE_LOOKUP_URL = 'https://itunes.apple.com/lookup?id=6754315964';
function appStoreResponse(overrides: Record<string, unknown> = {}): Response {
return Response.json({
resultCount: 1,
results: [
{
averageUserRating: 4,
userRatingCount: 10,
price: 4.99,
currency: 'USD',
...overrides
}
]
});
}
function playStoreListing(overrides: Partial<PlayStoreListing> = {}): PlayStoreListing {
return {
available: true,
score: 4.5,
ratings: 20,
price: 3.99,
currency: 'USD',
...overrides
};
}
async function expectAppleFailureKeepsGoogleMetadata(fetch: HomepageStoreFetch): Promise<void> {
const metadata = await loadHomepageStoreMetadata({
fetch,
loadPlayStoreListing: async () => playStoreListing()
});
expect(metadata).toEqual({
aggregateRating: { ratingValue: '4.5', ratingCount: 20 },
appStorePrice: null,
playStorePrice: '3.99'
});
}
async function expectGoogleFailureKeepsAppleMetadata(
loadPlayStoreListing: () => Promise<PlayStoreListing>
): Promise<void> {
const metadata = await loadHomepageStoreMetadata({
fetch: async () => appStoreResponse(),
loadPlayStoreListing
});
expect(metadata).toEqual({
aggregateRating: { ratingValue: '4.0', ratingCount: 10 },
appStorePrice: '4.99',
playStorePrice: null
});
}
describe('loadHomepageStoreMetadata failure isolation', () => {
test('an App Store fetch exception does not discard Google Play metadata', async () => {
await expectAppleFailureKeepsGoogleMetadata(async () => {
throw new Error('offline');
});
});
test('a non-OK App Store response does not discard Google Play metadata', async () => {
await expectAppleFailureKeepsGoogleMetadata(
async () => new Response(null, { status: 503 })
);
});
test('malformed App Store JSON does not discard Google Play metadata', async () => {
await expectAppleFailureKeepsGoogleMetadata(
async () => new Response('{', { headers: { 'content-type': 'application/json' } })
);
});
test('a Google Play exception does not discard App Store metadata', async () => {
await expectGoogleFailureKeepsAppleMetadata(async () => {
throw new Error('offline');
});
});
test('an unavailable Google Play listing does not discard App Store metadata', async () => {
await expectGoogleFailureKeepsAppleMetadata(async () =>
playStoreListing({ available: false })
);
});
});
describe('loadHomepageStoreMetadata prices and ratings', () => {
test('normalizes each store price independently', async () => {
const malformedApplePrice = await loadHomepageStoreMetadata({
fetch: async () => appStoreResponse({ price: '4.99' }),
loadPlayStoreListing: async () => playStoreListing()
});
expect(malformedApplePrice.appStorePrice).toBeNull();
expect(malformedApplePrice.playStorePrice).toBe('3.99');
const malformedGooglePrice = await loadHomepageStoreMetadata({
fetch: async () => appStoreResponse(),
loadPlayStoreListing: async () => playStoreListing({ currency: 'EUR' })
});
expect(malformedGooglePrice.appStorePrice).toBe('4.99');
expect(malformedGooglePrice.playStorePrice).toBeNull();
});
test('computes the count-weighted aggregate to one decimal place', async () => {
const requestedUrls: string[] = [];
const metadata = await loadHomepageStoreMetadata({
fetch: async (url) => {
requestedUrls.push(url);
return appStoreResponse({ averageUserRating: 4, userRatingCount: 10 });
},
loadPlayStoreListing: async () =>
playStoreListing({ score: 5, ratings: 30 })
});
expect(requestedUrls).toEqual([APP_STORE_LOOKUP_URL]);
expect(metadata.aggregateRating).toEqual({
ratingValue: '4.8',
ratingCount: 40
});
});
});