feat(banner): 首页常驻滚动通知与管理配置及类型样式支持
This commit is contained in:
@@ -292,3 +292,77 @@ test("request logs display user credit billing independently from upstream cost"
|
||||
expect(apiSource).toContain("billingAmountMicrocredits: number");
|
||||
expect(apiSource).toContain("billingAvailable: boolean");
|
||||
});
|
||||
|
||||
test("banner announcement editor keeps title styles through edit, save and status toggle", async () => {
|
||||
const [panelSource, editorSource, sliderSource, apiSource, contentSource, emojiPickerSource, noticeSource] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/components/admin-banner-announcements-panel.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/banner-title-editor.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/components/layout/banner-announcements-slider.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/services/api/announcements.ts", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/components/layout/banner-announcement-content.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/banner-notice-emoji-picker.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/lib/announcements/banner-notice.ts", import.meta.url)).text(),
|
||||
]);
|
||||
const panel = compactSource(panelSource);
|
||||
const slider = compactSource(sliderSource);
|
||||
|
||||
// 状态开关与保存都必须带上样式分段与通知类型:后端更新走显式字段表,漏传会清空它们。
|
||||
expect(panel).toContain("titleRuns: banner.titleRuns");
|
||||
expect(panel).toContain("titleRuns: runs");
|
||||
expect(panel).toContain("noticeType: selectedNoticeType");
|
||||
expect(panel).toContain("noticeType: normalizeBannerNoticeType(banner.noticeType)");
|
||||
// 编辑入口不能被新建信号重置:父级 createOpen 消费后立即复位,编辑路径不再写回该标志。
|
||||
expect(panel).toContain("onCreateOpenChange(false)");
|
||||
expect(panel).toContain('setDialog({ mode: "edit", banner })');
|
||||
expect(panel).toContain("banner.titleRuns?.length");
|
||||
// AntD 6 用 destroyOnHidden,destroyOnClose 已废弃。
|
||||
expect(panel).toContain("destroyOnHidden");
|
||||
expect(panel).not.toContain("destroyOnClose");
|
||||
// Modal 渲染在 body portal 里,必须挂 admin-modal-root 才能拿到弹窗作用域的强边框 / 分层 token,
|
||||
// 否则编辑器等自绘控件的边框回落到 :root 的 8% 透明度,肉眼不可见。
|
||||
expect(panel).toContain('rootClassName="admin-modal-root"');
|
||||
// 非表单控件不能放进 Form.Item(会被注入 value/onChange/ref)。
|
||||
expect(panel).toContain("<BannerNoticePreview runs={titleRuns} hasLink={Boolean(linkValue?.trim())} noticeType={noticeType} />");
|
||||
// 通知类型在后台要有独立入口;emoji 图标不设独立字段,经编辑器「图标」按钮插入。
|
||||
expect(panel).toContain("<BannerNoticeTypeSelector");
|
||||
expect(panel).not.toContain("BannerNoticeIconPicker");
|
||||
expect(panel).not.toContain("normalizeBannerNoticeIcon");
|
||||
|
||||
expect(editorSource).toContain("applyBannerTitleStyle");
|
||||
expect(editorSource).toContain("clearBannerTitleStyle");
|
||||
expect(editorSource).toContain("lowContrastBannerTitleColors");
|
||||
// 标题是单行语义,Enter 不产生新段落。
|
||||
expect(editorSource).toContain('event.key === "Enter"');
|
||||
// emoji 素材插到光标处、作为普通文本保存(无独立字段、无默认图标)。
|
||||
expect(editorSource).toContain("<BannerNoticeEmojiPopover");
|
||||
expect(editorSource).toContain("insertContent(emoji)");
|
||||
expect(editorSource).not.toContain("BannerAnnouncementIcon");
|
||||
// 预览必须走前台同一份展示单元,否则「预览即前台」不成立。
|
||||
expect(editorSource).toContain("bannerAnnouncementBarStyle(noticeType)");
|
||||
|
||||
// 前台通知条必须按分段渲染,不能退回纯文本;底色来自当前通知的类型,标题前不再有固定图标。
|
||||
expect(sliderSource).toContain("BannerAnnouncementTitle");
|
||||
expect(sliderSource).toContain("currentBanner.titleRuns");
|
||||
expect(slider).toContain("bannerAnnouncementBarStyle(currentBanner.noticeType)");
|
||||
expect(slider).not.toContain("BannerAnnouncementIcon");
|
||||
expect(slider).not.toContain("notice-banner-surface");
|
||||
|
||||
// 展示单元是唯一来源:底色、富文本标题、详情入口都在这里定义,前台与预览共用;
|
||||
// 图标素材是标题文本的一部分,展示单元不再单独处理图标。
|
||||
expect(contentSource).toContain("export function bannerAnnouncementBarStyle");
|
||||
expect(contentSource).toContain("export function BannerAnnouncementLinkHint");
|
||||
expect(contentSource).toContain("export function BannerAnnouncementTitle");
|
||||
expect(contentSource).not.toContain("BannerAnnouncementIcon");
|
||||
|
||||
// emoji 面板:默认收起(Popover 点击触发),插入后不自动关闭,方便连续插入。
|
||||
expect(emojiPickerSource).toContain('trigger="click"');
|
||||
expect(emojiPickerSource).toContain('onPick(item.char)');
|
||||
expect(noticeSource).toContain("BANNER_NOTICE_EMOJI_GROUPS");
|
||||
expect(noticeSource).not.toContain("DEFAULT_ICON");
|
||||
|
||||
expect(apiSource).toContain("titleRuns?: BannerTitleRun[]");
|
||||
expect(apiSource).toContain("noticeType?: BannerNoticeType");
|
||||
expect(apiSource).not.toContain("icon?:");
|
||||
expect(apiSource).toContain("export type { BannerTitleRun }");
|
||||
});
|
||||
|
||||
|
||||
147
web/test/banner-notice.test.ts
Normal file
147
web/test/banner-notice.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
BANNER_NOTICE_DEFAULT_TYPE,
|
||||
BANNER_NOTICE_EMOJI_GROUPS,
|
||||
BANNER_NOTICE_EMOJIS,
|
||||
BANNER_NOTICE_TYPE_VALUES,
|
||||
BANNER_NOTICE_TYPES,
|
||||
bannerNoticeSurfaceCSS,
|
||||
bannerNoticeTypeLabel,
|
||||
normalizeBannerNoticeType,
|
||||
searchBannerNoticeEmojis,
|
||||
} from "@/lib/announcements/banner-notice";
|
||||
import { bannerTitleContrastRatio } from "@/lib/announcements/banner-title";
|
||||
|
||||
async function readSource(relativePath: string) {
|
||||
return Bun.file(new URL(relativePath, import.meta.url)).text();
|
||||
}
|
||||
|
||||
/** 从 Go 的 `var X = map[string]struct{}{...}` 字面量里取出键集合。 */
|
||||
function goMapKeys(source: string, variable: string) {
|
||||
const start = source.indexOf(`var ${variable} = map[string]struct{}{`);
|
||||
if (start < 0) throw new Error(`未找到 ${variable}`);
|
||||
const end = source.indexOf("\n}", start);
|
||||
if (end < 0) throw new Error(`${variable} 字面量未闭合`);
|
||||
const body = source.slice(start, end);
|
||||
return Array.from(body.matchAll(/"([a-z0-9-]+)"/g)).map((match) => match[1]);
|
||||
}
|
||||
|
||||
describe("通知类型", () => {
|
||||
test("未知或空值回落到默认类型", () => {
|
||||
expect(normalizeBannerNoticeType(undefined)).toBe(BANNER_NOTICE_DEFAULT_TYPE);
|
||||
expect(normalizeBannerNoticeType("")).toBe(BANNER_NOTICE_DEFAULT_TYPE);
|
||||
expect(normalizeBannerNoticeType("rainbow")).toBe(BANNER_NOTICE_DEFAULT_TYPE);
|
||||
expect(normalizeBannerNoticeType(" ACTIVITY ")).toBe("activity");
|
||||
});
|
||||
|
||||
test("每种类型都有中文名与独立的底色 token", () => {
|
||||
const vars = BANNER_NOTICE_TYPES.map((item) => item.surfaceVar);
|
||||
expect(new Set(vars).size).toBe(vars.length);
|
||||
for (const type of BANNER_NOTICE_TYPES) {
|
||||
expect(bannerNoticeTypeLabel(type.key)).toBe(type.label);
|
||||
expect(bannerNoticeSurfaceCSS(type.key)).toBe(`var(${type.surfaceVar})`);
|
||||
}
|
||||
// 未知类型也必须拿到可用底色,不能渲染成透明条。
|
||||
expect(bannerNoticeSurfaceCSS("rainbow")).toBe(bannerNoticeSurfaceCSS(BANNER_NOTICE_DEFAULT_TYPE));
|
||||
});
|
||||
});
|
||||
|
||||
describe("emoji 图标素材库", () => {
|
||||
test("字符唯一、非空,每个素材都有中文名", () => {
|
||||
const chars = BANNER_NOTICE_EMOJIS.map((item) => item.char);
|
||||
expect(new Set(chars).size).toBe(chars.length);
|
||||
expect(chars.length).toBeGreaterThanOrEqual(50);
|
||||
for (const item of BANNER_NOTICE_EMOJIS) {
|
||||
expect(item.char.trim().length).toBeGreaterThan(0);
|
||||
expect(item.label.trim().length).toBeGreaterThan(0);
|
||||
expect(item.groupLabel.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("用户点名的素材必须在库内", () => {
|
||||
for (const char of ["💰", "🚀", "🔥", "📢"]) {
|
||||
expect(BANNER_NOTICE_EMOJIS.some((item) => item.char === char)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("搜索同时匹配中文名、分组名与字符本身", () => {
|
||||
expect(searchBannerNoticeEmojis("火箭").map((item) => item.char)).toContain("🚀");
|
||||
expect(searchBannerNoticeEmojis("钱袋").map((item) => item.char)).toContain("💰");
|
||||
// 分组名搜索命中整组。
|
||||
const statusHits = searchBannerNoticeEmojis("状态");
|
||||
expect(statusHits.length).toBeGreaterThanOrEqual(BANNER_NOTICE_EMOJI_GROUPS.find((g) => g.key === "status")!.emoji.length);
|
||||
// 字符本身也能搜到(支持从别处粘贴检索)。
|
||||
expect(searchBannerNoticeEmojis("🔥").map((item) => item.char)).toContain("🔥");
|
||||
expect(searchBannerNoticeEmojis(" ").length).toBe(BANNER_NOTICE_EMOJIS.length);
|
||||
expect(searchBannerNoticeEmojis("不存在的素材")).toEqual([]);
|
||||
});
|
||||
|
||||
test("emoji 是普通文本:标题纯文本拼接函数必须原样保留它", async () => {
|
||||
// 防止未来有人给 run.text 做字符过滤,把 emoji 丢掉。
|
||||
const { bannerTitlePlainText } = await import("@/lib/announcements/banner-title");
|
||||
const runs = [
|
||||
{ text: "🔥", fontSize: 20 },
|
||||
{ text: "限时", color: "#FFE58F" },
|
||||
{ text: "大促" },
|
||||
];
|
||||
expect(bannerTitlePlainText(runs as never)).toBe("🔥限时大促");
|
||||
});
|
||||
});
|
||||
|
||||
describe("与后端白名单保持一致", () => {
|
||||
test("通知类型的后端白名单逐项等于前端注册表", async () => {
|
||||
const goSource = await readSource("../../backend/internal/app/announcement.go");
|
||||
expect(new Set(goMapKeys(goSource, "bannerNoticeTypes"))).toEqual(new Set(BANNER_NOTICE_TYPE_VALUES));
|
||||
});
|
||||
|
||||
test("后端默认值常量与前端一致", async () => {
|
||||
const goSource = await readSource("../../backend/internal/app/announcement.go");
|
||||
expect(goSource).toContain(`bannerNoticeDefaultType = "${BANNER_NOTICE_DEFAULT_TYPE}"`);
|
||||
});
|
||||
|
||||
test("后端不保存独立图标字段:emoji 必须内嵌在标题分段里", async () => {
|
||||
const [goModel, goApp, goRepo] = await Promise.all([
|
||||
readSource("../../backend/internal/model/models_project.go"),
|
||||
readSource("../../backend/internal/app/announcement.go"),
|
||||
readSource("../../backend/internal/repository/announcement.go"),
|
||||
]);
|
||||
// 防止 icon 字段被加回来却没人维护白名单:图标的存储形态就是 TitleRuns 的文本。
|
||||
expect(goModel).not.toMatch(/\bIcon\s+string/);
|
||||
expect(goApp).not.toContain("normalizeBannerIcon");
|
||||
expect(goRepo).not.toContain('"icon"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("通知条主题色", () => {
|
||||
test("四种类型在明暗两档都满足白字对比度要求", async () => {
|
||||
const css = await readSource("../src/styles/globals.css");
|
||||
const value = (token: string) => {
|
||||
const match = css.match(new RegExp(`--${token}:\\s*(#[0-9a-fA-F]{6})`));
|
||||
if (!match) throw new Error(`globals.css 缺少 --${token}`);
|
||||
return match[1];
|
||||
};
|
||||
for (const type of BANNER_NOTICE_TYPES) {
|
||||
// 浅色档是正文级底色,必须达到 WCAG AA 的 4.5:1;
|
||||
// 深色档统一压到 7:1 以上,避免暗色主题里底色过亮。
|
||||
const light = value(`palette-banner-${type.key}-500`);
|
||||
const dark = value(`palette-banner-${type.key}-700`);
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", light)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", dark)).toBeGreaterThanOrEqual(7);
|
||||
}
|
||||
});
|
||||
|
||||
test("前台与预览都引用按类型取值的语义 token", async () => {
|
||||
const [globals, adminUi] = await Promise.all([readSource("../src/styles/globals.css"), readSource("../src/styles/admin-ui.css")]);
|
||||
for (const type of BANNER_NOTICE_TYPES) {
|
||||
// surfaceVar 自带 `--` 前缀,拼接时不要再加一次。
|
||||
const name = type.surfaceVar.replace(/^--/, "");
|
||||
// 语义层明暗各定义一次::root 一次、.dark 一次。
|
||||
const semantic = new RegExp(`--${name}: var\\(--palette-banner-${type.key}-\\d00\\)`, "g");
|
||||
expect(globals.match(semantic)?.length).toBe(2);
|
||||
// 后台预览强制两套底色,不受页面当前主题影响。
|
||||
expect(adminUi).toContain(`--${name}: var(--palette-banner-${type.key}-500)`);
|
||||
expect(adminUi).toContain(`--${name}: var(--palette-banner-${type.key}-700)`);
|
||||
}
|
||||
});
|
||||
});
|
||||
72
web/test/banner-title-rich-text.test.ts
Normal file
72
web/test/banner-title-rich-text.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { bannerDocToRuns, bannerTitleRunsToDoc } from "@/lib/announcements/banner-title-rich-text";
|
||||
|
||||
test("样式分段与编辑文档互转保持幂等", () => {
|
||||
const runs = [
|
||||
{ text: "限时活动", fontSize: 16, fontWeight: 700, color: "#FF0000" },
|
||||
{ text: "已上线", fontFamily: "mono" as const },
|
||||
{ text: ",欢迎体验" },
|
||||
];
|
||||
const doc = bannerTitleRunsToDoc(runs);
|
||||
expect(doc.content?.[0]?.type).toBe("paragraph");
|
||||
expect(bannerDocToRuns(doc)).toEqual(runs);
|
||||
});
|
||||
|
||||
test("编辑器文档里的白名单外样式在转回分段时被丢弃", () => {
|
||||
const doc = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "标题",
|
||||
marks: [{ type: "textStyle", attrs: { fontSize: "40px", fontWeight: "900", color: "red", fontFamily: "Comic Sans" } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(bannerDocToRuns(doc)).toEqual([{ text: "标题" }]);
|
||||
});
|
||||
|
||||
test("同一节点上的多个 textStyle 标记按出现顺序合并取值", () => {
|
||||
const doc = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "标题",
|
||||
marks: [
|
||||
{ type: "textStyle", attrs: { fontSize: "12px" } },
|
||||
{ type: "textStyle", attrs: { fontSize: "18px", color: "#00ff00" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(bannerDocToRuns(doc)).toEqual([{ text: "标题", fontSize: 18, color: "#00FF00" }]);
|
||||
});
|
||||
|
||||
test("多段文档只取首个段落,粘贴内容不会变成多行标题", () => {
|
||||
const doc = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{ type: "paragraph", content: [{ type: "text", text: "第一行" }] },
|
||||
{ type: "paragraph", content: [{ type: "text", text: "第二行" }] },
|
||||
],
|
||||
};
|
||||
expect(bannerDocToRuns(doc)).toEqual([{ text: "第一行" }]);
|
||||
});
|
||||
|
||||
test("空文档与空分段互转不报错", () => {
|
||||
expect(bannerDocToRuns(undefined)).toEqual([]);
|
||||
expect(bannerDocToRuns(null)).toEqual([]);
|
||||
expect(bannerTitleRunsToDoc([])).toEqual({ type: "doc", content: [{ type: "paragraph", content: [] }] });
|
||||
});
|
||||
101
web/test/banner-title.test.ts
Normal file
101
web/test/banner-title.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
BANNER_TITLE_FONT_FAMILIES,
|
||||
bannerTitleCharCount,
|
||||
bannerTitleContrastRatio,
|
||||
bannerTitlePlainText,
|
||||
bannerTitleRunStyle,
|
||||
lowContrastBannerTitleColors,
|
||||
normalizeBannerTitleHex,
|
||||
normalizeBannerTitleRuns,
|
||||
parseBannerTitleColor,
|
||||
} from "@/lib/announcements/banner-title";
|
||||
|
||||
describe("通知标题样式分段清洗", () => {
|
||||
test("合并相邻同样式分段,跳过空文本", () => {
|
||||
const runs = normalizeBannerTitleRuns([
|
||||
{ text: "限时", fontSize: 16, color: "#FF0000" },
|
||||
{ text: "活动", fontSize: 16, color: "#ff0000" },
|
||||
{ text: "", fontSize: 16 },
|
||||
{ text: "上线", fontSize: 20 },
|
||||
]);
|
||||
expect(runs).toEqual([
|
||||
{ text: "限时活动", fontSize: 16, color: "#FF0000" },
|
||||
{ text: "上线", fontSize: 20 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("丢弃白名单外的字号、字重、字体与颜色", () => {
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontSize: 9 }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontSize: 21 }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontWeight: 6000 }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontFamily: "comic" as never }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", color: "red" }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", color: "#fff;background:url(x)" }])).toEqual([{ text: "标题" }]);
|
||||
});
|
||||
|
||||
test("sans 视为默认字体,不写入分段", () => {
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontFamily: "sans" }])).toEqual([{ text: "标题" }]);
|
||||
expect(normalizeBannerTitleRuns([{ text: "标题", fontFamily: "mono" }])).toEqual([{ text: "标题", fontFamily: "mono" }]);
|
||||
});
|
||||
|
||||
test("纯文本与字符数按码点统计", () => {
|
||||
const runs = [{ text: "限时" }, { text: "活动🎉" }];
|
||||
expect(bannerTitlePlainText(runs)).toBe("限时活动🎉");
|
||||
expect(bannerTitleCharCount(runs)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("通知标题样式输出", () => {
|
||||
test("只输出已设置的维度", () => {
|
||||
expect(bannerTitleRunStyle({ text: "标题" })).toEqual({});
|
||||
expect(bannerTitleRunStyle({ text: "标题", fontSize: 20, fontWeight: 700, color: "#FF0000" })).toEqual({
|
||||
fontSize: "20px",
|
||||
fontWeight: 700,
|
||||
color: "#FF0000",
|
||||
});
|
||||
});
|
||||
|
||||
test("字体档位输出对应字体栈", () => {
|
||||
const serif = BANNER_TITLE_FONT_FAMILIES.find((item) => item.key === "serif");
|
||||
expect(bannerTitleRunStyle({ text: "标题", fontFamily: "serif" }).fontFamily).toBe(serif?.css);
|
||||
});
|
||||
|
||||
test("十六进制颜色归一化", () => {
|
||||
expect(normalizeBannerTitleHex("#abc")).toBe("#AABBCC");
|
||||
expect(normalizeBannerTitleHex("5f6ae4")).toBe("#5F6AE4");
|
||||
expect(normalizeBannerTitleHex("#12345")).toBe("");
|
||||
expect(parseBannerTitleColor("rgb(95, 106, 228)")).toEqual([95, 106, 228]);
|
||||
expect(parseBannerTitleColor("not-a-color")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("通知标题对比度检查", () => {
|
||||
test("白字在通知条两种底色上的对比度", () => {
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", "#5C67E2")).toBeCloseTo(4.67, 2);
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", "#3F4590")).toBeCloseTo(8.52, 2);
|
||||
expect(bannerTitleContrastRatio("not-a-color", "#3F4590")).toBeNull();
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", "")).toBeNull();
|
||||
});
|
||||
|
||||
test("通知条底色 token 的白字对比度必须达到 AA(改 token 时同步更新此处)", () => {
|
||||
// globals.css 的 --palette-banner-notice-500 / -700(「公告」类型);默认标题为白色 12px 正文,需 >= 4.5:1。
|
||||
// 四种类型的完整覆盖见 test/banner-notice.test.ts。
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", "#5C67E2")).toBeGreaterThanOrEqual(4.5);
|
||||
expect(bannerTitleContrastRatio("#FFFFFF", "#3F4590")).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
|
||||
test("挑出对比度不足的颜色并去重,默认无色不参与检查", () => {
|
||||
const runs = [
|
||||
{ text: "低", color: "#808BF5" },
|
||||
{ text: "也低", color: "#808BF5" },
|
||||
{ text: "足够", color: "#FFFFFF" },
|
||||
{ text: "没设颜色" },
|
||||
];
|
||||
expect(lowContrastBannerTitleColors(runs, "#5C67E2")).toEqual(["#808BF5"]);
|
||||
expect(lowContrastBannerTitleColors(runs, "#3F4590")).toEqual(["#808BF5"]);
|
||||
expect(lowContrastBannerTitleColors([{ text: "足够", color: "#FFFFFF" }], "#5C67E2")).toEqual([]);
|
||||
expect(lowContrastBannerTitleColors(runs, "")).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user