Files
Jesse 49f6edd7fb
Staging Backend Quality Gate / validate (push) Successful in 13m34s
Staging Backend Quality Gate / publish (push) Has been cancelled
feat(web): add persistent beam avatars
2026-08-07 12:07:57 +08:00

101 lines
5.2 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { UserAvatar } from "../src/components/user-avatar.tsx";
import {
BEAM_AVATAR_RENDERER_VERSION,
beamAvatarPalettes,
beamAvatarPatchSchema,
beamAvatarSchema,
createBeamAvatarProfilePatch,
} from "../src/lib/beam-avatar.ts";
const readProjectFile = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
const accountRoute = readProjectFile("src/app/api/account/route.ts");
const avatarRoute = readProjectFile("src/app/api/account/avatar/route.ts");
const avatarComponent = readProjectFile("src/components/user-avatar.tsx");
const sidebar = readProjectFile("src/components/app-sidebar.tsx");
const page = readProjectFile("src/app/page.tsx");
const migration = readProjectFile("db/migrations/20260807010000_profile_beam_avatars.sql");
const seed = "11111111-1111-4111-8111-111111111111";
test("defines eight reviewed Beam palettes and versioned persisted parameters", () => {
assert.equal(beamAvatarPalettes.length, 8);
assert.equal(new Set(beamAvatarPalettes.map((palette) => palette.name)).size, 8);
assert.equal(beamAvatarPalettes.every((palette) => palette.colors.length === 5), true);
assert.deepEqual(beamAvatarSchema.parse({
seed,
palette: 7,
rendererVersion: BEAM_AVATAR_RENDERER_VERSION,
}), { seed, palette: 7, rendererVersion: 1 });
assert.equal(beamAvatarSchema.safeParse({ seed, palette: 8, rendererVersion: 1 }).success, false);
assert.equal(beamAvatarSchema.safeParse({ seed: "email@example.test", palette: 0, rendererVersion: 1 }).success, false);
});
test("renders identical Beam SVG for identical persisted parameters", () => {
const avatar = { seed, palette: 0, rendererVersion: 1 } as const;
const first = renderToStaticMarkup(createElement(UserAvatar, { avatar, size: 40 }));
const second = renderToStaticMarkup(createElement(UserAvatar, { avatar, size: 40 }));
assert.equal(first, second);
assert.match(first, /<svg/);
assert.match(first, /width="40"/);
assert.match(first, /height="40"/);
});
test("accepts only bounded avatar actions and creates new seeds on the server", () => {
assert.equal(beamAvatarPatchSchema.safeParse({ action: "set_palette", palette: 3 }).success, true);
assert.equal(beamAvatarPatchSchema.safeParse({ action: "randomize" }).success, true);
assert.equal(beamAvatarPatchSchema.safeParse({ action: "randomize", seed }).success, false);
assert.equal(beamAvatarPatchSchema.safeParse({ action: "upload" }).success, false);
assert.deepEqual(
createBeamAvatarProfilePatch({ action: "set_palette", palette: 3 }, () => seed),
{ avatar_palette: 3, avatar_renderer_version: 1 },
);
assert.deepEqual(
createBeamAvatarProfilePatch({ action: "randomize" }, () => seed),
{ avatar_renderer_version: 1, avatar_seed: seed },
);
});
test("persists random defaults and constraints only for self-hosted profiles", () => {
assert.match(migration, /to_regclass\('identity\.users'\) is not null/);
assert.match(migration, /avatar_seed uuid not null default gen_random_uuid\(\)/);
assert.match(migration, /avatar_palette smallint not null[\s\S]*floor\(random\(\) \* 8\)/);
assert.match(migration, /check \(avatar_palette between 0 and 7\)/);
assert.match(migration, /check \(avatar_renderer_version = 1\)/);
assert.match(migration, /grant update \([\s\S]*avatar_seed[\s\S]*avatar_palette[\s\S]*avatar_renderer_version[\s\S]*\) on table public\.profiles to authenticated/);
assert.doesNotMatch(migration, /bytea|storage|bucket|upload/i);
});
test("account APIs expose and update Beam parameters through authenticated staging data", () => {
assert.match(accountRoute, /AUTH_PROVIDER\?\.trim\(\) === "self-hosted"/);
assert.match(accountRoute, /select\("avatar_seed,avatar_palette,avatar_renderer_version"\)/);
assert.match(accountRoute, /avatar,/);
assert.match(avatarRoute, /AUTH_PROVIDER\?\.trim\(\) === "self-hosted"/);
assert.match(avatarRoute, /createServerSupabaseClient\(\)/);
assert.match(avatarRoute, /auth\.getUser\(\)/);
assert.match(avatarRoute, /beamAvatarPatchSchema\.safeParse/);
assert.match(avatarRoute, /createBeamAvatarProfilePatch\(parsed\.data\)/);
assert.match(avatarRoute, /\.eq\("id", user\.id\)/);
assert.doesNotMatch(avatarRoute, /createAdminSupabaseClient|SUPABASE_SERVICE_ROLE_KEY|upload|bytea/i);
});
test("renders the persisted Beam avatar in the sidebar, account menu, and profile editor", () => {
assert.match(avatarComponent, /from "boring-avatars"/);
assert.match(avatarComponent, /variant="beam"/);
assert.match(avatarComponent, /name=\{avatar\.seed\}/);
assert.match(sidebar, /<UserAvatar avatar=\{account\.avatar\} size=\{32\}/);
assert.match(sidebar, /<UserAvatar avatar=\{account\.avatar\} size=\{40\}/);
assert.match(page, /Beam 形象由随机种子生成/);
assert.match(page, /role="radiogroup" aria-label="头像配色"/);
assert.match(page, /persistAvatar\(\{ action: "set_palette", palette: index \}\)/);
assert.match(page, /persistAvatar\(\{ action: "randomize" \}\)/);
assert.doesNotMatch(page, /avatar[^\n]{0,80}(?:file|upload|上传图片)/i);
});