fix(website): restore contracts and focus indicators
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"build": "vite build",
|
||||
"audit": "python3 ../scripts/check_bun_audit.py --project . --baseline bun_audit_baseline.json",
|
||||
"preview": "vite preview",
|
||||
"test": "bun test",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
|
||||
@@ -163,7 +163,6 @@
|
||||
.store-button:focus-visible {
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.store-button :global(svg) {
|
||||
@@ -181,7 +180,6 @@
|
||||
.linux-button.active {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.desktop-button:hover,
|
||||
@@ -271,7 +269,6 @@
|
||||
.linux-menu-item:focus-visible {
|
||||
color: var(--color-text);
|
||||
background: rgb(237 237 237 / 0.12);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
.faq-toggle:focus-visible {
|
||||
background: rgb(237 237 237 / 0.14);
|
||||
outline: none;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
@@ -220,6 +220,5 @@
|
||||
.faq-answer-content :global(a:hover),
|
||||
.faq-answer-content :global(a:focus-visible) {
|
||||
text-decoration-color: var(--color-text);
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
.footer-nav a:focus-visible {
|
||||
color: var(--color-text);
|
||||
background: rgb(237 237 237 / 0.12);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<div class="noise-overlay" aria-hidden="true"></div>
|
||||
@@ -406,7 +406,6 @@
|
||||
.carousel-dot:focus-visible {
|
||||
color: var(--color-text);
|
||||
background: rgb(237 237 237 / 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.carousel-dot:focus-visible {
|
||||
|
||||
@@ -346,7 +346,6 @@
|
||||
.device-button:not(.active):focus-visible {
|
||||
color: var(--color-text);
|
||||
background: rgb(237 237 237 / 0.12);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.device-button.active {
|
||||
@@ -358,7 +357,6 @@
|
||||
.device-button.active:focus-visible {
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.device-button :global(svg),
|
||||
@@ -401,7 +399,6 @@
|
||||
.scroll-arrow.enabled:focus-visible {
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-highest);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.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
|
||||
};
|
||||
}
|
||||
@@ -93,6 +93,5 @@
|
||||
.home-link:focus-visible {
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,62 +1,17 @@
|
||||
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 }) => {
|
||||
let appStoreRating: { score: number; count: number } | null = null;
|
||||
let playStoreRating: { score: number; count: number } | null = null;
|
||||
let appStorePrice: string | null = null;
|
||||
let playStorePrice: string | null = null;
|
||||
|
||||
try {
|
||||
const res = await fetch('https://itunes.apple.com/lookup?id=6754315964');
|
||||
if (!res.ok) throw new Error(`App Store lookup failed: HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
const app = data.results?.[0];
|
||||
if (app?.averageUserRating && app?.userRatingCount) {
|
||||
appStoreRating = {
|
||||
score: app.averageUserRating,
|
||||
count: app.userRatingCount
|
||||
};
|
||||
export const load: PageServerLoad = async ({ fetch }) =>
|
||||
loadHomepageStoreMetadata({
|
||||
fetch,
|
||||
loadPlayStoreListing: async () => {
|
||||
// Module initialization is optional external data and stays inside the
|
||||
// helper's Play Store failure boundary.
|
||||
const { default: gplay } = await import('google-play-scraper');
|
||||
return gplay.app({
|
||||
appId: 'com.edde746.plezy',
|
||||
country: 'us',
|
||||
lang: 'en'
|
||||
});
|
||||
}
|
||||
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 };
|
||||
};
|
||||
});
|
||||
|
||||
@@ -90,6 +90,17 @@ button {
|
||||
-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,
|
||||
input,
|
||||
textarea,
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
.back-link:focus-visible {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-highest);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.back-logo :global(img) {
|
||||
@@ -222,6 +221,5 @@
|
||||
.prose a:hover,
|
||||
.prose a:focus-visible {
|
||||
text-decoration-color: var(--color-text);
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
.store-button:focus-visible {
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.store-button :global(svg) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user