chore(website): remove tests and update SvelteKit

This commit is contained in:
edde746
2026-07-24 20:30:45 +02:00
parent d1cec00b51
commit db7bc17c49
7 changed files with 15 additions and 383 deletions
-1
View File
@@ -5,7 +5,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR/website"
bun install --frozen-lockfile
bun test
bun run check
bun run build
python3 "$ROOT_DIR/scripts/test_check_bun_audit.py"
+13 -7
View File
@@ -11,7 +11,7 @@
"@iconify/json": "^2.2.474",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/enhanced-img": "^0.10.4",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"google-play-scraper": "^10.1.2",
"svelte": "^5.48.2",
@@ -211,7 +211,7 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.11", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw=="],
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
@@ -219,7 +219,7 @@
"@sveltejs/enhanced-img": ["@sveltejs/enhanced-img@0.10.4", "", { "dependencies": { "magic-string": "^0.30.5", "sharp": "^0.34.1", "svelte-parse-markup": "^0.1.5", "vite-imagetools": "^9.0.3", "zimmerframe": "^1.1.2" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0 || ^7.0.0", "svelte": "^5.0.0", "vite": "^6.3.0 || >=7.0.0" } }, "sha512-Am5nmAKUo7Nboqq7Dhtfn7dcXA087d7gIz6Vecn1opB41aJ680+0q9U9KvEcMgduOyeiwckTIOQOx4Mmq9GcvA=="],
"@sveltejs/kit": ["@sveltejs/kit@2.50.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA=="],
"@sveltejs/kit": ["@sveltejs/kit@2.70.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
@@ -241,7 +241,7 @@
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
@@ -283,7 +283,7 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -503,12 +503,18 @@
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"svelte/@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
"svelte/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"svelte/devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
"unplugin/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
}
}
+1 -49
View File
@@ -1,55 +1,7 @@
{
"schemaVersion": 1,
"reviewedOn": "2026-07-21",
"reviewedOn": "2026-07-24",
"accepted": [
{
"id": 1113317,
"package": "@sveltejs/kit",
"severity": "moderate",
"vulnerableRange": ">=2.49.0 <=2.52.1",
"expiresOn": "2026-10-19",
"rationale": "GHSA-88qp-p4qg-rqm6: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1113318,
"package": "@sveltejs/kit",
"severity": "moderate",
"vulnerableRange": ">=2.49.0 <=2.52.1",
"expiresOn": "2026-10-19",
"rationale": "GHSA-vrhm-gvg7-fpcf: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1113631,
"package": "@sveltejs/kit",
"severity": "low",
"vulnerableRange": ">=2.49.0 <=2.53.2",
"expiresOn": "2026-10-19",
"rationale": "GHSA-fpg4-jhqr-589c: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1116432,
"package": "@sveltejs/kit",
"severity": "moderate",
"vulnerableRange": "<=2.57.0",
"expiresOn": "2026-10-19",
"rationale": "GHSA-3f6h-2hrp-w5wx: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1116433,
"package": "@sveltejs/kit",
"severity": "high",
"vulnerableRange": "<=2.57.0",
"expiresOn": "2026-10-19",
"rationale": "GHSA-2crg-3p73-43xp: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1122155,
"package": "@sveltejs/kit",
"severity": "moderate",
"vulnerableRange": ">=2.38.0 <=2.60.0",
"expiresOn": "2026-10-19",
"rationale": "GHSA-hgv7-v322-mmgr: Static adapter/prerender site has no remote functions or forms, query.batch, hooks/redirects, or adapter-node body handling."
},
{
"id": 1103907,
"package": "cookie",
+1 -2
View File
@@ -7,7 +7,6 @@
"dev": "vite dev",
"build": "vite build",
"audit": "python3 ../scripts/check_bun_audit.py --project . --baseline bun_audit_baseline.json",
"test": "bun test",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -17,7 +16,7 @@
"@iconify/json": "^2.2.474",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/enhanced-img": "^0.10.4",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"google-play-scraper": "^10.1.2",
"svelte": "^5.48.2",
-61
View File
@@ -1,61 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { createServer, type ViteDevServer } from 'vite';
let vite: ViteDevServer;
beforeAll(async () => {
vite = await createServer({
root: process.cwd(),
appType: 'custom',
server: { middlewareMode: true },
});
}, 30_000);
afterAll(async () => {
await vite?.close();
}, 30_000);
async function renderComponent(path: string): Promise<string> {
const [{ default: component }, { render: renderOnViteGraph }] = await Promise.all([
vite.ssrLoadModule(path),
vite.ssrLoadModule('svelte/server'),
]);
return renderOnViteGraph(component).body;
}
describe('SSR component contracts', () => {
test('Logo renders the one managed asset as a decorative non-draggable image', async () => {
const html = await renderComponent('/src/lib/components/Logo.svelte');
expect(html).toContain('<img');
expect(html).toContain('alt=""');
expect(html).toContain('aria-hidden="true"');
expect(html).toContain('draggable="false"');
expect(html).not.toContain('<path');
}, 30_000);
test('Linux downloads prerender as an ordinary inert disclosure', async () => {
const html = await renderComponent('/src/lib/components/DownloadButtons.svelte');
expect(html).toContain('aria-expanded="false"');
expect(html).toContain('aria-controls="');
expect(html).toContain('aria-hidden="true"');
expect(html).toContain('inert');
expect(html).not.toContain('aria-haspopup');
expect(html).not.toContain('role="menu"');
expect(html).not.toContain('role="menuitem"');
expect(html.match(/plezy-linux-(?:x64|arm64)\.(?:deb|rpm|pkg\.tar\.zst|tar\.gz)/g)).toHaveLength(8);
}, 30_000);
test('Screenshots prerender stable regions but only the initial lazy phone images', async () => {
const html = await renderComponent('/src/lib/components/Screenshots.svelte');
for (const device of ['phone', 'tablet', 'desktop', 'tv']) {
expect(html).toContain(`id="screenshots-${device}-panel"`);
}
expect(html.match(/<picture\b/g)).toHaveLength(4);
expect(html.match(/loading="lazy"/g)).toHaveLength(4);
expect(html.match(/width="\d+" height="\d+"/g)).toHaveLength(4);
expect(html).toContain('alt="Plezy home screen"');
expect(html).toContain('style="opacity: 1; transform: translateY(0px);');
expect(html).not.toContain('alt="Plezy on tablet - home"');
expect(html).not.toContain('alt="Plezy on desktop - home"');
expect(html).not.toContain('alt="Plezy on TV - home"');
}, 120_000);
});
-95
View File
@@ -1,95 +0,0 @@
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { createServer, type ViteDevServer } from 'vite';
import { faqs, faqSchemaMainEntity, watchTogetherFaqAnswer } from '../src/lib/content/faqs';
import { csr, prerender } from '../src/routes/privacy/+page';
let vite: ViteDevServer;
let renderedPrivacyHead: string;
let renderedPrivacyHtml: string;
let privacySource: string;
beforeAll(async () => {
vite = await createServer({
root: process.cwd(),
appType: 'custom',
server: { middlewareMode: true },
});
const [{ default: privacyPage }, { render }] = await Promise.all([
vite.ssrLoadModule('/src/routes/privacy/+page.svelte'),
vite.ssrLoadModule('svelte/server'),
]);
const rendered = render(privacyPage);
renderedPrivacyHead = rendered.head;
renderedPrivacyHtml = rendered.body;
privacySource = await Bun.file('src/routes/privacy/+page.svelte').text();
}, 90_000);
afterAll(async () => {
await vite?.close();
}, 30_000);
function visibleText(html: string): string {
return html
.replace(/<[^>]+>/g, ' ')
.replaceAll('&amp;', '&')
.replaceAll('&#39;', "'")
.replace(/\s+/g, ' ')
.trim();
}
describe('privacy page', () => {
test('remains a prerendered, server-rendered route', () => {
expect(prerender).toBe(true);
expect(csr).toBe(false);
expect(renderedPrivacyHead).toContain('<title>Privacy Policy - Plezy</title>');
expect(renderedPrivacyHead).toContain('<link rel="canonical" href="https://plezy.app/privacy"');
});
test('renders a concise semantic policy without inventory cards or grids', () => {
for (const heading of [
'Overview',
'Data on your device',
'Connections and third parties',
'Optional Plezy services',
'Your choices',
'Retention and security',
'Contact and changes',
]) {
expect(renderedPrivacyHtml).toContain(`>${heading}</h2>`);
}
expect(renderedPrivacyHtml.match(/<section/g)).toHaveLength(7);
expect(renderedPrivacyHtml).not.toContain('flow-card');
expect(renderedPrivacyHtml).not.toContain('<dl');
expect(renderedPrivacyHtml).not.toContain('<ul');
expect(privacySource).not.toContain('display: grid');
expect(visibleText(renderedPrivacyHtml).split(' ').length).toBeLessThan(900);
});
test('uses first-party language and preserves the material privacy boundaries', () => {
const text = visibleText(renderedPrivacyHtml);
expect(text).not.toMatch(/\brepositor(?:y|ies)\b/i);
expect(text).not.toMatch(/repository-verified/i);
expect(text).toContain('We do not sell personal data or use it for advertising');
expect(text).toContain('access tokens and other sign-in data');
expect(text).toContain('may contain a server access credential');
expect(text).toContain('does not carry the media stream or your media-server credentials');
expect(text).toContain('Support-log uploads are always explicit');
expect(text).toContain('up to 90 days');
expect(text).toContain('retrieved for three days');
expect(text).toContain('no more than three hours');
expect(renderedPrivacyHtml).toContain('rel="noopener noreferrer"');
});
test('keeps the Watch Together disclosure aligned with visible FAQ content', () => {
const faqIndex = faqs.findIndex((faq) => faq.id === 'watch-together');
expect(faqIndex).toBeGreaterThanOrEqual(0);
expect(faqs[faqIndex]!.answer).toBe(watchTogetherFaqAnswer);
expect(faqSchemaMainEntity[faqIndex]!.acceptedAnswer.text).toBe(watchTogetherFaqAnswer);
expect(watchTogetherFaqAnswer).toContain('server and media identifiers');
expect(watchTogetherFaqAnswer).toContain('does not relay the media stream');
expect(watchTogetherFaqAnswer).toContain('custom relay');
});
});
-168
View File
@@ -1,168 +0,0 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
import {
detectMobileStorePlatform,
linuxArchitectures,
storeOptionsForPlatform,
} from '../src/lib/content/downloads';
import {
buildSoftwareApplicationOffers,
normalizeUsdStorePrice,
} from '../src/lib/content/software_app_offers';
type PlayStoreFixture = {
available?: boolean;
price?: unknown;
currency?: unknown;
score?: number;
ratings?: number;
};
let playStoreFixture: PlayStoreFixture | Error;
mock.module('google-play-scraper', () => ({
default: {
app: async () => {
if (playStoreFixture instanceof Error) throw playStoreFixture;
return playStoreFixture;
},
},
}));
// Dynamic loading is intentional here: Bun must install the scraper mock before
// the route first loads its optional external dependency.
const { load } = await import('../src/routes/+page.server');
function appleFetch(body: unknown, status = 200): typeof fetch {
return (async () => new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
})) as typeof fetch;
}
async function loadHomepage(fetcher: typeof fetch) {
return await (load as (event: { fetch: typeof fetch }) => Promise<{
appStorePrice: string | null;
playStorePrice: string | null;
aggregateRating: { ratingValue: string; ratingCount: number } | null;
}>)({ fetch: fetcher });
}
beforeEach(() => {
playStoreFixture = {
available: true,
price: 4.99,
currency: 'USD',
score: 4.5,
ratings: 10,
};
});
afterEach(() => {
mock.restore();
});
describe('download component contracts', () => {
test('detects mobile stores without browser globals', () => {
expect(detectMobileStorePlatform()).toBe('unknown');
expect(detectMobileStorePlatform({ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0)' })).toBe('ios');
expect(detectMobileStorePlatform({ userAgent: 'Mozilla/5.0 (Linux; Android 15)' })).toBe('android');
expect(detectMobileStorePlatform({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
platform: 'MacIntel',
maxTouchPoints: 5,
})).toBe('ios');
expect(detectMobileStorePlatform({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
platform: 'MacIntel',
maxTouchPoints: 0,
})).toBe('unknown');
});
test('unknown platforms retain both truthful store choices', () => {
expect(storeOptionsForPlatform('unknown').map((option) => option.label)).toEqual(['App Store', 'Google Play']);
expect(storeOptionsForPlatform('ios').map((option) => option.label)).toEqual(['App Store']);
expect(storeOptionsForPlatform('android').map((option) => option.label)).toEqual(['Google Play']);
});
test('Linux disclosure data contains both architectures and eight unique native links', () => {
expect(linuxArchitectures.map((architecture) => architecture.label)).toEqual(['x64 (Intel/AMD)', 'ARM64']);
const links = linuxArchitectures.flatMap((architecture) => architecture.formats.map((format) => format.url));
expect(links).toHaveLength(8);
expect(new Set(links).size).toBe(8);
expect(links.every((url) => url.startsWith('https://github.com/edde746/plezy/releases/latest/download/'))).toBe(true);
});
});
describe('homepage store metadata', () => {
test('normalizes only finite nonnegative numeric USD prices', () => {
expect(normalizeUsdStorePrice(4.99, 'USD')).toBe('4.99');
expect(normalizeUsdStorePrice(0, 'USD')).toBe('0');
for (const value of [null, undefined, '4.99', Number.NaN, Number.POSITIVE_INFINITY, -1]) {
expect(normalizeUsdStorePrice(value, 'USD')).toBeNull();
}
expect(normalizeUsdStorePrice(4.99, undefined)).toBeNull();
expect(normalizeUsdStorePrice(4.99, 'EUR')).toBeNull();
});
test('keeps Google Play metadata when the App Store request fails', async () => {
const data = await loadHomepage((async () => {
throw new Error('offline');
}) as typeof fetch);
expect(data.appStorePrice).toBeNull();
expect(data.playStorePrice).toBe('4.99');
expect(data.aggregateRating).toEqual({ ratingValue: '4.5', ratingCount: 10 });
});
test('keeps App Store metadata when Google Play is unavailable', async () => {
playStoreFixture = { available: false, price: 0, currency: 'USD' };
const data = await loadHomepage(appleFetch({
results: [{ price: 5.99, currency: 'USD', averageUserRating: 4, userRatingCount: 20 }],
}));
expect(data.appStorePrice).toBe('5.99');
expect(data.playStorePrice).toBeNull();
expect(data.aggregateRating).toEqual({ ratingValue: '4.0', ratingCount: 20 });
});
test('keeps App Store metadata when Google Play throws', async () => {
playStoreFixture = new Error('scraper failed');
const data = await loadHomepage(appleFetch({
results: [{ price: 5.99, currency: 'USD' }],
}));
expect(data.appStorePrice).toBe('5.99');
expect(data.playStorePrice).toBeNull();
});
test('rejects non-OK and malformed store responses independently', async () => {
playStoreFixture = { price: 'free', currency: 'USD' };
const data = await loadHomepage(appleFetch({ results: [{ price: 4.99, currency: 'USD' }] }, 503));
expect(data.appStorePrice).toBeNull();
expect(data.playStorePrice).toBeNull();
});
test('retains paid store URLs when live prices are unavailable', () => {
const unavailable = buildSoftwareApplicationOffers({ appStorePrice: null, playStorePrice: null });
expect(unavailable.map((offer) => offer.category)).toEqual([
'App Store',
'Google Play',
'Amazon Appstore',
'GitHub',
]);
expect(unavailable.find((offer) => offer.category === 'App Store')).toMatchObject({
url: 'https://apps.apple.com/us/app/id6754315964',
});
expect(unavailable.find((offer) => offer.category === 'Google Play')).toMatchObject({
url: 'https://play.google.com/store/apps/details?id=com.edde746.plezy',
});
expect(unavailable.filter((offer) => offer.price === '0').map((offer) => offer.category)).toEqual(['GitHub']);
expect(
unavailable
.filter((offer) => ['App Store', 'Google Play'].includes(offer.category))
.every((offer) => !('price' in offer)),
).toBe(true);
const available = buildSoftwareApplicationOffers({ appStorePrice: '5.99', playStorePrice: '4.99' });
expect(available.find((offer) => offer.category === 'App Store')).toMatchObject({ price: '5.99', priceCurrency: 'USD' });
expect(available.find((offer) => offer.category === 'Google Play')).toMatchObject({ price: '4.99', priceCurrency: 'USD' });
});
});