feat(download): enhance download page with checksum integration and platform-specific releases

- Added a new `release-data.ts` file to manage release information with dynamic checksums.
- Introduced `CHECKSUMS` constant for easy access to file checksums.
- Updated `PlatformDownload.astro` to support additional release types and improved type safety.
- Enhanced the download page to correctly display platform-specific download links and checksums.
- Refactored tests to validate the new download functionality and checksum integration.
This commit is contained in:
taroj1205 2025-05-16 12:28:14 +12:00
parent 466c829a8a
commit 6a7bd311c0
No known key found for this signature in database
GPG key ID: 0FCB6CFFE0981AB7
9 changed files with 231 additions and 103 deletions

View file

@ -13,7 +13,8 @@
"format": "biome format ./src", "format": "biome format ./src",
"prepare": "husky", "prepare": "husky",
"test": "npx vitest run", "test": "npx vitest run",
"test:coverage": "npx vitest --coverage" "test:coverage": "npx vitest --coverage",
"test:playwright": "npx playwright test"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",

View file

@ -9,7 +9,16 @@ interface PlatformReleases {
universal?: ReleaseInfo universal?: ReleaseInfo
all?: ReleaseInfo all?: ReleaseInfo
tarball?: ReleaseInfo tarball?: ReleaseInfo
x86_64?: { tarball: ReleaseInfo } | ReleaseInfo x86_64?:
| {
tarball?: ReleaseInfo
appImage?: ReleaseInfo
}
| ReleaseInfo
aarch64?: {
tarball?: ReleaseInfo
appImage?: ReleaseInfo
}
arm64?: ReleaseInfo arm64?: ReleaseInfo
flathub?: { all: ReleaseInfo } flathub?: { all: ReleaseInfo }
} }
@ -27,11 +36,15 @@ import { Image } from 'astro:assets'
import AppIconDark from '../../assets/app-icon-dark.png' import AppIconDark from '../../assets/app-icon-dark.png'
import AppIconLight from '../../assets/app-icon-light.png' import AppIconLight from '../../assets/app-icon-light.png'
import DownloadCard from './ButtonCard.astro' import DownloadCard from './ButtonCard.astro'
function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
return !!obj && typeof obj === 'object' && 'link' in obj
}
--- ---
<div <div
id={`${platform}-downloads`} id={`${platform}-downloads`}
data-active={platform === 'mac'} data-active={platform === "mac"}
class="platform-section data-[active='false']:hidden" class="platform-section data-[active='false']:hidden"
> >
<div class="items-center gap-8 md:flex"> <div class="items-center gap-8 md:flex">
@ -45,39 +58,108 @@ import DownloadCard from './ButtonCard.astro'
<p class="text-muted-foreground mb-6" set:html={description} /> <p class="text-muted-foreground mb-6" set:html={description} />
<div class="space-y-6"> <div class="space-y-6">
{ {
platform === 'linux' ? ( platform === "linux" ? (
<> <>
{releases.flathub && releases.flathub.all.label && <div> {releases.flathub && releases.flathub.all.label && (
<h4 class="mb-3 text-lg font-medium">Package Managers</h4> <div>
<div class="space-y-3"> <h4 class="mb-3 text-lg font-medium">Package Managers</h4>
<DownloadCard <div class="space-y-3">
label={releases.flathub.all.label} <DownloadCard
href={releases.flathub.all.link} label={releases.flathub.all.label}
variant="flathub" href={releases.flathub.all.link}
/> variant="flathub"
/>
</div>
</div> </div>
</div>} )}
{releases.x86_64 && 'tarball' in releases.x86_64 && <div> {releases.x86_64 &&
<h4 class="mb-3 text-lg font-medium">Tarball</h4> typeof releases.x86_64 === "object" &&
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> "tarball" in releases.x86_64 &&
<DownloadCard (releases.x86_64.tarball || releases.x86_64.appImage) && (
label="x86_64" <div>
href={releases.x86_64.tarball.link} <h4 class="mb-3 text-lg font-medium">x86_64</h4>
variant="x86_64" <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
checksum={releases.x86_64.tarball.checksum} {releases.x86_64.tarball && (
/> <DownloadCard
<DownloadCard label={
label="ARM64" releases.x86_64.tarball.label
href={releases.x86_64.tarball.link} ? releases.x86_64.tarball.label
variant="aarch64" : ""
checksum={releases.x86_64.tarball.checksum} }
/> href={
</div> releases.x86_64.tarball.link
</div>} ? releases.x86_64.tarball.link
: ""
}
variant="x86_64"
checksum={releases.x86_64.tarball.checksum}
/>
)}
{releases.x86_64.appImage && (
<DownloadCard
label={
releases.x86_64.appImage.label
? releases.x86_64.appImage.label
: ""
}
href={
releases.x86_64.appImage.link
? releases.x86_64.appImage.link
: ""
}
variant="x86_64"
checksum={releases.x86_64.appImage.checksum}
/>
)}
</div>
</div>
)}
{releases.aarch64 &&
typeof releases.aarch64 === "object" &&
"tarball" in releases.aarch64 &&
(releases.aarch64.tarball || releases.aarch64.appImage) && (
<div>
<h4 class="mb-3 text-lg font-medium">ARM64</h4>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
{releases.aarch64.tarball && (
<DownloadCard
label={
releases.aarch64.tarball.label
? releases.aarch64.tarball.label
: ""
}
href={
releases.aarch64.tarball.link
? releases.aarch64.tarball.link
: ""
}
variant="aarch64"
checksum={releases.aarch64.tarball.checksum}
/>
)}
{releases.aarch64.appImage && (
<DownloadCard
label={
releases.aarch64.appImage.label
? releases.aarch64.appImage.label
: ""
}
href={
releases.aarch64.appImage.link
? releases.aarch64.appImage.link
: ""
}
variant="aarch64"
checksum={releases.aarch64.appImage.checksum}
/>
)}
</div>
</div>
)}
</> </>
) : ( ) : (
<div class="space-y-4"> <div class="flex flex-col gap-4">
<div class="space-y-3"> <div class="flex flex-col gap-3">
{releases.universal && releases.universal.label && ( {releases.universal && releases.universal.label && (
<DownloadCard <DownloadCard
label={releases.universal.label} label={releases.universal.label}
@ -85,23 +167,15 @@ import DownloadCard from './ButtonCard.astro'
checksum={releases.universal.checksum} checksum={releases.universal.checksum}
/> />
)} )}
{releases.x86_64 && ( {releases.x86_64 &&
'tarball' in releases.x86_64 isFlatReleaseInfo(releases.x86_64) &&
? releases.x86_64.tarball.label && ( releases.x86_64.label && (
<DownloadCard <DownloadCard
label={releases.x86_64.tarball.label} label={releases.x86_64.label}
href={releases.x86_64.tarball.link} href={releases.x86_64.link}
checksum={releases.x86_64.tarball.checksum} checksum={releases.x86_64.checksum}
/> />
) )}
: releases.x86_64.label && (
<DownloadCard
label={releases.x86_64.label}
href={releases.x86_64.link}
checksum={releases.x86_64.checksum}
/>
)
)}
{releases.arm64 && releases.arm64.label && ( {releases.arm64 && releases.arm64.label && (
<DownloadCard <DownloadCard
label={releases.arm64.label} label={releases.arm64.label}

View file

@ -1,4 +1,3 @@
---
/** /**
* Returns the releases object, injecting checksums dynamically. * Returns the releases object, injecting checksums dynamically.
* @param checksums Record<string, string> mapping filenames to SHA-256 hashes * @param checksums Record<string, string> mapping filenames to SHA-256 hashes
@ -28,24 +27,24 @@ export function getReleasesWithChecksums(checksums: Record<string, string>) {
x86_64: { x86_64: {
tarball: { tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz', link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: 'Tarball x86_64', label: 'Tarball',
checksum: checksums['zen.linux-x86_64.tar.xz'], checksum: checksums['zen.linux-x86_64.tar.xz'],
}, },
appImage: { appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage', link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage',
label: 'AppImage x86_64', label: 'AppImage',
checksum: checksums['zen-x86_64.AppImage'], checksum: checksums['zen-x86_64.AppImage'],
}, },
}, },
aarch64: { aarch64: {
tarball: { tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz', link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: 'Tarball aarch64', label: 'Tarball',
checksum: checksums['zen.linux-aarch64.tar.xz'], checksum: checksums['zen.linux-aarch64.tar.xz'],
}, },
appImage: { appImage: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage', link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage',
label: 'AppImage aarch64', label: 'AppImage',
checksum: checksums['zen-aarch64.AppImage'], checksum: checksums['zen-aarch64.AppImage'],
}, },
}, },
@ -58,4 +57,3 @@ export function getReleasesWithChecksums(checksums: Record<string, string>) {
}, },
} }
} }
---

View file

@ -0,0 +1,9 @@
export const CHECKSUMS = {
'zen.macos-universal.dmg': 'macsum',
'zen.installer.exe': 'winsum',
'zen.installer-arm64.exe': 'winarmsum',
'zen.linux-x86_64.tar.xz': 'linuxsum',
'zen-x86_64.AppImage': 'linuxappsum',
'zen.linux-aarch64.tar.xz': 'linuxarmsum',
'zen-aarch64.AppImage': 'linuxarmappsum',
}

View file

@ -1,5 +1,7 @@
import { CHECKSUMS } from './checksum'
import { I18N } from './i18n' import { I18N } from './i18n'
export const CONSTANT = { export const CONSTANT = {
I18N, I18N,
CHECKSUMS,
} }

View file

@ -1,36 +1,41 @@
--- ---
import Description from '~/components/Description.astro' import Description from "~/components/Description.astro";
import DownloadScript from '~/components/download/DownloadScript.astro' import DownloadScript from "~/components/download/DownloadScript.astro";
import PlatformDownload from '~/components/download/PlatformDownload.astro' import PlatformDownload from "~/components/download/PlatformDownload.astro";
import { getReleasesWithChecksums } from '~/components/download/release-data.astro' import { getReleasesWithChecksums } from "~/components/download/release-data";
import Layout from '~/layouts/Layout.astro' import Layout from "~/layouts/Layout.astro";
import { getChecksums } from '~/utils/githubChecksums' import { getChecksums } from "~/utils/githubChecksums";
import { getLocale, getUI } from '~/utils/i18n' import { getLocale, getUI } from "~/utils/i18n";
import { icon, library } from '@fortawesome/fontawesome-svg-core' import { icon, library } from "@fortawesome/fontawesome-svg-core";
import { faApple, faGithub, faLinux, faWindows } from '@fortawesome/free-brands-svg-icons' import {
import ExternalLinkIcon from '~/icons/ExternalLink.astro' faApple,
import LockIcon from '~/icons/LockIcon.astro' faGithub,
faLinux,
faWindows,
} from "@fortawesome/free-brands-svg-icons";
import ExternalLinkIcon from "~/icons/ExternalLink.astro";
import LockIcon from "~/icons/LockIcon.astro";
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from "~/utils/i18n";
const locale = getLocale(Astro) const locale = getLocale(Astro);
const { const {
routes: { download }, routes: { download },
layout, layout,
} = getUI(locale) } = getUI(locale);
library.add(faWindows, faLinux, faApple, faGithub) library.add(faWindows, faLinux, faApple, faGithub);
const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' }) const windowsIcon = icon({ prefix: "fab", iconName: "windows" });
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' }) const linuxIcon = icon({ prefix: "fab", iconName: "linux" });
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' }) const appleIcon = icon({ prefix: "fab", iconName: "apple" });
const githubIcon = icon({ prefix: 'fab', iconName: 'github' }) const githubIcon = icon({ prefix: "fab", iconName: "github" });
const checksums = await getChecksums() const checksums = await getChecksums();
const releases = getReleasesWithChecksums(checksums) const releases = getReleasesWithChecksums(checksums);
const platformNames = download.platformNames const platformNames = download.platformNames;
const platformDescriptions = download.platformDescriptions const platformDescriptions = download.platformDescriptions;
--- ---
<DownloadScript /> <DownloadScript />

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { getReleasesWithChecksums } from '~/components/download/release-data.astro' import { getReleasesWithChecksums } from '~/components/download/release-data'
describe('getReleasesWithChecksums', () => { describe('getReleasesWithChecksums', () => {
it('returns correct structure with checksums', () => { it('returns correct structure with checksums', () => {

View file

@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import type { BrowserContextOptions, Page } from '@playwright/test' import type { BrowserContextOptions, Page } from '@playwright/test'
import { getReleasesWithChecksums } from '~/components/download/release-data'
import { CONSTANT } from '~/constants'
// Helper to get the platform section by id // Helper to get the platform section by id
const getPlatformSection = (page: Page, platform: string) => const getPlatformSection = (page: Page, platform: string) =>
@ -9,6 +11,10 @@ const getPlatformSection = (page: Page, platform: string) =>
const getPlatformButton = (page: Page, platform: string) => const getPlatformButton = (page: Page, platform: string) =>
page.locator(`button.platform-selector[data-platform='${platform}']`) page.locator(`button.platform-selector[data-platform='${platform}']`)
// Helper to get the platform download link
const getPlatformDownloadLink = (page: Page, platform: string, label: string) =>
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [ const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
{ {
name: 'windows', name: 'windows',
@ -29,25 +35,27 @@ const platformConfigs: { name: string; userAgent: string; platform: string }[] =
}, },
] ]
for (const { name, userAgent, platform } of platformConfigs) { test.describe('Download page default tab per platform', () => {
test(`shows correct default tab for ${name} platform`, async ({ browser }) => { for (const { name, userAgent, platform } of platformConfigs) {
const context = await browser.newContext({ test(`shows correct default tab for ${name} platform`, async ({ browser }) => {
userAgent, const context = await browser.newContext({
locale: 'en-US', userAgent,
platform, locale: 'en-US',
} as BrowserContextOptions) platform,
const page = await context.newPage() } as BrowserContextOptions)
await page.goto('/download') const page = await context.newPage()
await expect(getPlatformSection(page, name)).toBeVisible() await page.goto('/download')
await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true') await expect(getPlatformSection(page, name)).toBeVisible()
// Other platforms should not be active await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true')
for (const other of platformConfigs.filter((p) => p.name !== name)) { // Other platforms should not be active
await expect(getPlatformSection(page, other.name)).toBeHidden() for (const other of platformConfigs.filter((p) => p.name !== name)) {
await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true') await expect(getPlatformSection(page, other.name)).toBeHidden()
} await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true')
await context.close() }
}) await context.close()
} })
}
})
test.describe('Download page platform detection and tab switching', () => { test.describe('Download page platform detection and tab switching', () => {
test('shows correct platform section and tab when switching platforms', async ({ page }) => { test('shows correct platform section and tab when switching platforms', async ({ page }) => {
@ -65,3 +73,36 @@ test.describe('Download page platform detection and tab switching', () => {
} }
}) })
}) })
test.describe('Download page download links', () => {
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
return {
mac: [releases.macos.universal],
windows: [releases.windows.x86_64, releases.windows.arm64],
linux: [
releases.linux.x86_64.tarball,
releases.linux.x86_64.appImage,
releases.linux.aarch64.tarball,
releases.linux.aarch64.appImage,
releases.linux.flathub.all,
],
}
}
test('all platform download links are correct', async ({ page }) => {
const platforms = ['windows', 'mac', 'linux']
const platformLinkSelectors = getPlatformLinks(releases)
await page.goto('/download')
await page.waitForLoadState('domcontentloaded')
for (const platform of platforms) {
await getPlatformButton(page, platform).click()
for (const { label, link } of platformLinkSelectors[platform as keyof typeof platformLinkSelectors]) {
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
await expect(downloadLink).toContainText(label)
await expect(downloadLink).toHaveAttribute('href', link)
}
}
})
})

View file

@ -1,14 +1,12 @@
import { CONSTANT } from '~/constants'
/** /**
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums. * Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
* Returns a mapping from filename to checksum. * Returns a mapping from filename to checksum.
*/ */
export async function getChecksums() { export async function getChecksums() {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
return { return CONSTANT.CHECKSUMS
'zen.macos-universal.dmg': 'macsum',
'zen.installer.exe': 'winsum',
'zen.installer-arm64.exe': 'winarmsum',
}
} }
const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', { const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', {
headers: { headers: {