Files
Jyotisha/frontend/tests/beam-avatar.test.ts
T
jesse-uxandGrok (agent loop) 352d45b2ef
Independent Staging Quality Gate / validate (push) Failing after 9m12s
Independent Staging Quality Gate / publish (push) Skipped
外壳换成组件库:侧栏可拖宽,会话可排序(#105)。
桌面侧栏可在 200 到 360 像素之间拖宽,拖到最左收起,宽度记在本机。手机从左边滑出。会话在同一组里拖动排序,顺序写入可空的 sidebar_order,收藏仍置顶。账户菜单、设置、反馈和退出确认改用组件库。顶栏点数只显示图标和数字。启程清单还在。菜单、拖宽、拖动排序和翻页在首屏之后再载入。

自测:tsc 退出码 0;npm run lint 0 error、121 warning;改到的前端测试 205 项全过;npm run build 后首页仍是 static;首屏 gzip 630350,相对 8f7c0f7d 的 621226 为 +1.47%。test:db 92 项里 79 过、13 败,失败名与 Windows 已知清单一致,排序列在通过的 ok 37。没有跑全量 npm test,没有跑 Python 快速门。没有登录态,没有在浏览器里拖侧栏。

Co-Authored-By: Grok (agent loop)
2026-10-25 01:41:19 +08:00

106 lines
5.6 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/(app)/page.tsx");
const profilePanel = readProjectFile("src/components/profile-panel.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, /<ProfilePanel[\s\S]*persistAvatar=\{persistAvatar\}/);
// 原值:必须出现「Beam 形象会在刷新和换设备后保持一致」
// 新值:这句说明不再出现
// 原因:D8 去掉重复解释布局的小字。配色、随机和侧栏头像尺寸仍在
assert.doesNotMatch(profilePanel, /Beam 形象会在刷新和换设备后保持一致/);
assert.match(profilePanel, /role="radiogroup" aria-label="头像配色"/);
assert.match(profilePanel, /persistAvatar\(\{ action: "set_palette", palette: index \}\)/);
assert.match(profilePanel, /persistAvatar\(\{ action: "randomize" \}\)/);
assert.doesNotMatch(`${page}\n${profilePanel}`, /avatar[^\n]{0,80}(?:file|upload|上传图片)/i);
});