feat(gaze): add Gaze subdomain landing page with direct download

Move the Gaze marketing home from the legacy /marketing/gaze route to
a dedicated gaze subdomain landing page (src/routes/gaze). The new page
adds a richer hero, feature highlights, and a direct .dmg download flow
that resolves a signed S3 URL via the tRPC downloads.getDownloadUrl
endpoint.

Extract the download-resolution logic into a shared, testable
downloadAsset helper (src/lib/download-asset) so current and future
subdomain landing pages (InputHalo, Lineage, …) share one code path.
The helper is pure over an injected DownloadApi + redirect sink, keeping
it unit-testable without importing solid-js / CSRF cookie code.

Replace the old marketing/gaze route with a permanent 308 redirect to
https://gaze.freno.me so existing inbound links keep resolving to the
canonical home.

Add unit tests covering the tRPC call shape, redirect sink, and error
handling.
This commit is contained in:
2026-07-23 12:15:04 -04:00
parent 6fdbbbbe2b
commit f2ff75e6df
4 changed files with 359 additions and 56 deletions

View File

@@ -0,0 +1,88 @@
/**
* Unit tests for the shared `downloadAsset` helper (task 05).
*
* The helper is a pure function over an injected `DownloadApi` + redirect sink,
* so these tests verify the tRPC call shape, redirect, and error handling
* without importing `~/lib/api` (which pulls in solid-js / CSRF cookie code).
*/
import { describe, it, expect, mock } from "bun:test";
import { downloadAsset, type DownloadApi } from "~/lib/download-asset";
function makeFakeApi(
url: string,
seen: { input: { asset_name: string } }[] = []
): DownloadApi {
return {
downloads: {
getDownloadUrl: {
query: async (input: { asset_name: string }) => {
seen.push({ input });
return { downloadURL: url };
}
}
}
};
}
describe("downloadAsset", () => {
it("queries getDownloadUrl with the provided asset_name", async () => {
const seen: { input: { asset_name: string } }[] = [];
const api = makeFakeApi("https://s3/gaze.dmg", seen);
await downloadAsset({ api, assetName: "gaze", redirect: () => {} });
expect(seen).toHaveLength(1);
expect(seen[0]!.input.asset_name).toBe("gaze");
});
it("redirects to the returned signed URL", async () => {
const api = makeFakeApi("https://s3/gaze.dmg");
const sink = mock((u: string) => {});
await downloadAsset({ api, assetName: "gaze", redirect: sink });
expect(sink).toHaveBeenCalledTimes(1);
expect(sink).toHaveBeenCalledWith("https://s3/gaze.dmg");
});
it("routes failures to onError without throwing", async () => {
const api: DownloadApi = {
downloads: {
getDownloadUrl: {
query: async () => {
throw new Error("boom");
}
}
}
};
const sink = mock((u: string) => {});
const errSink = mock((e: unknown) => {});
await expect(
downloadAsset({ api, assetName: "gaze", redirect: sink, onError: errSink })
).resolves.toBeUndefined();
expect(sink).not.toHaveBeenCalled();
expect(errSink).toHaveBeenCalledTimes(1);
});
it("swallows errors silently when no onError is provided", async () => {
const api: DownloadApi = {
downloads: {
getDownloadUrl: {
query: async () => {
throw new Error("boom");
}
}
}
};
const sink = mock((u: string) => {});
await expect(
downloadAsset({ api, assetName: "gaze", redirect: sink })
).resolves.toBeUndefined();
expect(sink).not.toHaveBeenCalled();
});
it("works for any asset name (not hard-coded to gaze)", async () => {
const seen: { input: { asset_name: string } }[] = [];
const api = makeFakeApi("https://s3/inputhalo.dmg", seen);
const sink = mock((u: string) => {});
await downloadAsset({ api, assetName: "inputhalo", redirect: sink });
expect(seen[0]!.input.asset_name).toBe("inputhalo");
expect(sink).toHaveBeenCalledWith("https://s3/inputhalo.dmg");
});
});

76
src/lib/download-asset.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* Pure, testable helper for triggering a signed-S3 download via the tRPC
* `downloads.getDownloadUrl` endpoint.
*
* Extracted (task 05) so the Gaze landing page's download button — and any
* other subdomain landing page that needs the same flow (InputHalo, Lineage,
* …) — can share a single code path AND be unit-tested without importing
* `~/lib/api` (which transitively imports solid-js / CSRF cookie access).
*
* The component layer supplies the concrete `api` (dynamic-imported at click
* time) and the `redirect` sink (defaults to `window.location.href = url`).
* Tests inject a fake `api` and a capture sink.
*/
/** Structural shape of the tRPC downloads proxy this helper depends on. */
export interface DownloadApi {
downloads: {
getDownloadUrl: {
query: (input: { asset_name: string }) => Promise<{
downloadURL: string;
}>;
};
};
}
/** A `(url) => void` sink the helper calls with the signed S3 URL. */
export type DownloadRedirect = (url: string) => void;
export interface DownloadAssetOptions {
/** tRPC proxy (or fake) exposing `downloads.getDownloadUrl.query`. */
api: DownloadApi;
/** Asset key known to the downloads router (e.g. `"gaze"`). */
assetName: string;
/** Called with the signed URL. Defaults to `window.location.href = url`. */
redirect?: DownloadRedirect;
/** Invoked on failure; defaults to a no-op (component shows its own UI). */
onError?: (error: unknown) => void;
}
/** Default redirect sink — browser navigation to the signed S3 URL. */
const defaultRedirect: DownloadRedirect = (url) => {
if (typeof window !== "undefined") {
window.location.href = url;
}
};
/**
* Resolve the latest signed download URL for `assetName` and redirect the
* browser to it. Never throws — failures are routed to `onError`.
*
* @example
* ```ts
* import("~/lib/api").then(({ api }) => {
* downloadAsset({ api, assetName: "gaze" });
* });
* ```
*/
export async function downloadAsset(
options: DownloadAssetOptions
): Promise<void> {
const {
api,
assetName,
redirect = defaultRedirect,
onError
} = options;
try {
const data = await api.downloads.getDownloadUrl.query({
asset_name: assetName
});
redirect(data.downloadURL);
} catch (error) {
if (onError) onError(error);
}
}