forked from Mike/freno-dev
feat: add InputHalo landing page with DMG download flow
Add a marketing landing page for inputhalo.freno.me with a hero section (theme-aware app icon, glitch title, tagline), feature highlights, and a download section offering a macOS DMG via the tRPC signed-S3 URL endpoint plus an App Store link. Extract the download orchestration into a pure, side-effect-free helper (query/performInputHaloDownload) so the asset-name and redirect contract is unit-tested in isolation, mirroring the existing site-aware page-head / nav-config split. Include dark and default app-icon exports in the public folder.
This commit is contained in:
145
src/routes/inputhalo/download.test.ts
Normal file
145
src/routes/inputhalo/download.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Unit tests for the InputHalo landing-page download flow (task 06).
|
||||
*
|
||||
* The helper in `./download.ts` is pure (no solid-js / router / meta imports),
|
||||
* so we exercise the acceptance criterion directly — "the download button
|
||||
* calls `api.downloads.getDownloadUrl` with `'inputhalo'` and redirects to the
|
||||
* signed S3 URL" — without a DOM.
|
||||
*
|
||||
* Integration / visual checks (the rendered landing page, the tRPC client) are
|
||||
* covered by the build gate (`bun run build`) and the manual validation steps
|
||||
* in the task spec; the component is a thin wrapper over this helper.
|
||||
*/
|
||||
import { describe, it, expect, mock } from "bun:test";
|
||||
import {
|
||||
INPUTHALO_APP_STORE_URL,
|
||||
INPUTHALO_ASSET_NAME,
|
||||
INPUTHALO_ICON_DARK,
|
||||
INPUTHALO_ICON_DEFAULT,
|
||||
INPUTHALO_MIN_SYSTEM_VERSION,
|
||||
performInputHaloDownload,
|
||||
queryInputHaloDownload,
|
||||
type DownloadQueryApi
|
||||
} from "./download";
|
||||
|
||||
describe("InputHalo download constants", () => {
|
||||
it("asset name is 'inputhalo' (matches downloads.tsx)", () => {
|
||||
expect(INPUTHALO_ASSET_NAME).toBe("inputhalo");
|
||||
});
|
||||
|
||||
it("app store URL points at the InputHalo listing", () => {
|
||||
expect(INPUTHALO_APP_STORE_URL).toBe(
|
||||
"https://apps.apple.com/us/app/inputhalo/"
|
||||
);
|
||||
});
|
||||
|
||||
it("minimum system version is 14.6 (per Info.plist)", () => {
|
||||
expect(INPUTHALO_MIN_SYSTEM_VERSION).toBe("14.6");
|
||||
});
|
||||
|
||||
it("icons resolve from the 'InputHalo Exports' subfolder (Gaze pattern)", () => {
|
||||
expect(INPUTHALO_ICON_DARK).toBe(
|
||||
"/InputHalo Exports/InputHalo-iOS-Dark-1024x1024@1x.png"
|
||||
);
|
||||
expect(INPUTHALO_ICON_DEFAULT).toBe(
|
||||
"/InputHalo Exports/InputHalo-iOS-Default-1024x1024@1x.png"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryInputHaloDownload", () => {
|
||||
it("calls the query with asset_name 'inputhalo' and returns the signed URL", async () => {
|
||||
const query = mock(
|
||||
(async (input: { asset_name: string }) => {
|
||||
expect(input.asset_name).toBe("inputhalo");
|
||||
return { downloadURL: "https://s3.example.com/InputHalo.dmg?signed=1" };
|
||||
}) as DownloadQueryApi
|
||||
);
|
||||
|
||||
const url = await queryInputHaloDownload(query);
|
||||
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
expect(url).toBe("https://s3.example.com/InputHalo.dmg?signed=1");
|
||||
});
|
||||
|
||||
it("propagates the exact asset name 'inputhalo' (spelled correctly)", async () => {
|
||||
let captured: string | undefined;
|
||||
const query: DownloadQueryApi = async (input) => {
|
||||
captured = input.asset_name;
|
||||
return { downloadURL: "https://example.com/x.dmg" };
|
||||
};
|
||||
await queryInputHaloDownload(query);
|
||||
expect(captured).toBe("inputhalo");
|
||||
});
|
||||
|
||||
it("propagates query rejection", async () => {
|
||||
const boom = new Error("S3 unreachable");
|
||||
const query: DownloadQueryApi = async () => {
|
||||
throw boom;
|
||||
};
|
||||
await expect(queryInputHaloDownload(query)).rejects.toThrow(boom);
|
||||
});
|
||||
});
|
||||
|
||||
describe("performInputHaloDownload", () => {
|
||||
const SIGNED_URL = "https://s3.example.com/InputHalo-0.1.0.dmg?sig=abc";
|
||||
|
||||
it("redirects to the signed S3 URL returned by the query", async () => {
|
||||
const query = mock(
|
||||
(async () => ({ downloadURL: SIGNED_URL })) as DownloadQueryApi
|
||||
);
|
||||
const redirect = mock((url: string) => url);
|
||||
|
||||
const ok = await performInputHaloDownload(query, redirect);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
expect(redirect).toHaveBeenCalledTimes(1);
|
||||
expect(redirect).toHaveBeenCalledWith(SIGNED_URL);
|
||||
});
|
||||
|
||||
it("queries the tRPC endpoint with asset_name 'inputhalo'", async () => {
|
||||
const seen: { asset_name: string }[] = [];
|
||||
const query: DownloadQueryApi = async (input) => {
|
||||
seen.push(input);
|
||||
return { downloadURL: SIGNED_URL };
|
||||
};
|
||||
const redirect = mock((_url: string) => {});
|
||||
|
||||
await performInputHaloDownload(query, redirect);
|
||||
|
||||
expect(seen).toEqual([{ asset_name: "inputhalo" }]);
|
||||
});
|
||||
|
||||
it("does not redirect when the query rejects", async () => {
|
||||
const query: DownloadQueryApi = async () => {
|
||||
throw new Error("network down");
|
||||
};
|
||||
const redirect = mock((_url: string) => {});
|
||||
const onError = mock((_: unknown) => {});
|
||||
|
||||
const ok = await performInputHaloDownload(query, redirect, onError);
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(redirect).not.toHaveBeenCalled();
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("defaults the error sink to console.error", async () => {
|
||||
const original = console.error;
|
||||
const seen: unknown[] = [];
|
||||
console.error = (...args: unknown[]) => seen.push(args);
|
||||
|
||||
const query: DownloadQueryApi = async () => {
|
||||
throw new Error("boom");
|
||||
};
|
||||
|
||||
try {
|
||||
const ok = await performInputHaloDownload(query, () => {});
|
||||
expect(ok).toBe(false);
|
||||
expect(seen.length).toBeGreaterThanOrEqual(1);
|
||||
} finally {
|
||||
console.error = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user