mirror of
https://github.com/lutinglt/gitea-github-theme.git
synced 2026-10-28 09:44:55 +00:00
* 新增 arc-green 主题支持 * 将 assets 目录标记为 linguist-generated * 将 arc-green 放入 gitea 系列 * 调整 gitea 系列主题边框色和下划线颜色 * 整理代码 & 补充测试 * 调整 gitea 主题下划线导航颜色并修正二级菜单图标间距与下划线位置 * 更新快照 --------- Co-authored-by: lutinglt <lutinglt@users.noreply.github.com>
260 lines
9.2 KiB
TypeScript
260 lines
9.2 KiB
TypeScript
/*!
|
|
* Copyright (c) https://github.com/lutinglt
|
|
*
|
|
* See the NOTICE file distributed with this work for additional
|
|
* information regarding copyright ownership.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { execSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT_DIR = path.resolve(__dirname);
|
|
const DIST_DIR = path.join(ROOT_DIR, "dist");
|
|
const PREFIX = "theme-github-";
|
|
|
|
// ============================================================================
|
|
// #4: 构建产物基本验证
|
|
// ============================================================================
|
|
|
|
/** 从 theme.config.ts 推导的预期主题文件名 */
|
|
const EXPECTED_THEME_FILES = [
|
|
// default
|
|
`${PREFIX}dark.css`,
|
|
`${PREFIX}light.css`,
|
|
`${PREFIX}soft-dark.css`,
|
|
`${PREFIX}auto.css`,
|
|
// colorblind
|
|
`${PREFIX}colorblind-dark.css`,
|
|
`${PREFIX}colorblind-light.css`,
|
|
`${PREFIX}colorblind-auto.css`,
|
|
// tritanopia
|
|
`${PREFIX}tritanopia-dark.css`,
|
|
`${PREFIX}tritanopia-light.css`,
|
|
`${PREFIX}tritanopia-auto.css`,
|
|
// high contrast
|
|
`${PREFIX}high-contrast-dark.css`,
|
|
`${PREFIX}high-contrast-light.css`,
|
|
`${PREFIX}high-contrast-soft-dark.css`,
|
|
`${PREFIX}high-contrast-auto.css`,
|
|
// high contrast colorblind
|
|
`${PREFIX}high-contrast-colorblind-dark.css`,
|
|
`${PREFIX}high-contrast-colorblind-light.css`,
|
|
`${PREFIX}high-contrast-colorblind-auto.css`,
|
|
// high contrast tritanopia
|
|
`${PREFIX}high-contrast-tritanopia-dark.css`,
|
|
`${PREFIX}high-contrast-tritanopia-light.css`,
|
|
`${PREFIX}high-contrast-tritanopia-auto.css`,
|
|
// pink
|
|
`${PREFIX}pink-dark.css`,
|
|
`${PREFIX}pink-light.css`,
|
|
`${PREFIX}pink-soft-dark.css`,
|
|
`${PREFIX}pink-auto.css`,
|
|
// gitea
|
|
`${PREFIX}gitea-dark.css`,
|
|
`${PREFIX}gitea-light.css`,
|
|
`${PREFIX}gitea-arc-green.css`,
|
|
`${PREFIX}gitea-auto.css`,
|
|
// catppuccin
|
|
`${PREFIX}catppuccin-mocha.css`,
|
|
`${PREFIX}catppuccin-latte.css`,
|
|
`${PREFIX}catppuccin-frappe.css`,
|
|
`${PREFIX}catppuccin-macchiato.css`,
|
|
`${PREFIX}catppuccin-auto.css`,
|
|
];
|
|
|
|
/** auto 主题文件名列表 */
|
|
const AUTO_THEME_FILES = EXPECTED_THEME_FILES.filter(f => f.includes("-auto-") || f.endsWith("-auto.css"));
|
|
|
|
/** 非 auto 主题文件名列表 */
|
|
const SOLID_THEME_FILES = EXPECTED_THEME_FILES.filter(f => !AUTO_THEME_FILES.includes(f));
|
|
|
|
describe("构建产物基本验证", () => {
|
|
// 确保有构建产物
|
|
try {
|
|
const hasCss = fs.existsSync(DIST_DIR) && fs.readdirSync(DIST_DIR).some(f => f.endsWith(".css"));
|
|
if (!hasCss) {
|
|
execSync("bun run bundle", { cwd: ROOT_DIR, stdio: "pipe" });
|
|
}
|
|
} catch {
|
|
execSync("bun run bundle", { cwd: ROOT_DIR, stdio: "pipe" });
|
|
}
|
|
|
|
it("dist 目录存在且包含主题文件", () => {
|
|
expect(fs.existsSync(DIST_DIR)).toBe(true);
|
|
const files = fs.readdirSync(DIST_DIR).filter(f => f.endsWith(".css"));
|
|
expect(files.length, "dist 目录中应有 CSS 主题文件").toBeGreaterThan(0);
|
|
});
|
|
|
|
it("所有预期主题文件都存在", () => {
|
|
const actualFiles = new Set(fs.readdirSync(DIST_DIR).filter(f => f.endsWith(".css")));
|
|
for (const expected of EXPECTED_THEME_FILES) {
|
|
expect(actualFiles.has(expected), `缺少预期主题文件: ${expected}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("无多余主题文件", () => {
|
|
const actualFiles = fs.readdirSync(DIST_DIR).filter(f => f.endsWith(".css"));
|
|
const expectedSet = new Set(EXPECTED_THEME_FILES);
|
|
const extra = actualFiles.filter(f => !expectedSet.has(f));
|
|
expect(extra, `存在多余主题文件: ${extra.join(", ")}`).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("非 auto 主题 CSS 内容验证", () => {
|
|
for (const fileName of SOLID_THEME_FILES) {
|
|
describe(fileName, () => {
|
|
let css: string;
|
|
|
|
beforeAll(() => {
|
|
css = fs.readFileSync(path.join(DIST_DIR, fileName), "utf-8");
|
|
});
|
|
|
|
it("文件非空", () => {
|
|
expect(css.length, `${fileName} 不应为空文件`).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("包含 Gitea 主题元信息 (--is-dark-theme 或 color-scheme)", () => {
|
|
const hasMeta = css.includes("--is-dark-theme") || css.includes("color-scheme");
|
|
expect(hasMeta, `${fileName} 应包含主题元信息`).toBe(true);
|
|
});
|
|
|
|
it("包含 --color-primary 变量", () => {
|
|
expect(css, `${fileName} 应包含 --color-primary 变量`).toMatch(/--color-primary\b/);
|
|
});
|
|
|
|
it("包含 --color-body 变量", () => {
|
|
expect(css, `${fileName} 应包含 --color-body 变量`).toMatch(/--color-body\b/);
|
|
});
|
|
|
|
it("包含 --color-text 变量", () => {
|
|
expect(css, `${fileName} 应包含 --color-text 变量`).toMatch(/--color-text\b/);
|
|
});
|
|
|
|
it("不包含 vanilla-extract 调试标识", () => {
|
|
expect(css, `${fileName} 不应包含 _ve_ 调试标识`).not.toMatch(/_ve_/);
|
|
});
|
|
|
|
it("hex 颜色均为小写格式", () => {
|
|
const hexColors = css.match(/#[0-9a-fA-F]{3,8}\b/g) || [];
|
|
const upperHex = hexColors.filter(c => /[A-F]/.test(c));
|
|
expect(upperHex, `${fileName} 中存在大写 hex 颜色: ${upperHex.slice(0, 5).join(", ")}`).toEqual([]);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("auto 主题 CSS 内容验证", () => {
|
|
for (const fileName of AUTO_THEME_FILES) {
|
|
describe(fileName, () => {
|
|
let css: string;
|
|
|
|
beforeAll(() => {
|
|
css = fs.readFileSync(path.join(DIST_DIR, fileName), "utf-8");
|
|
});
|
|
|
|
it("文件非空", () => {
|
|
expect(css.length, `${fileName} 不应为空文件`).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("包含 @import 和 prefers-color-scheme 媒体查询", () => {
|
|
const hasImport = css.includes("@import");
|
|
const hasMediaQuery = css.includes("prefers-color-scheme");
|
|
expect(hasImport && hasMediaQuery, `${fileName} 应包含 @import 和 prefers-color-scheme`).toBe(true);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("暗/亮主题语义验证", () => {
|
|
/** 从文件名推断是否为暗色主题 */
|
|
function isDarkTheme(fileName: string): boolean {
|
|
if (fileName.includes("-light")) return false;
|
|
if (fileName.includes("-latte")) return false;
|
|
return true; // dark, soft-dark, mocha, frappe, macchiato 等默认为暗色
|
|
}
|
|
|
|
for (const fileName of SOLID_THEME_FILES) {
|
|
it(`${fileName}: color-scheme 与主题类型匹配`, () => {
|
|
const css = fs.readFileSync(path.join(DIST_DIR, fileName), "utf-8");
|
|
const dark = isDarkTheme(fileName);
|
|
|
|
if (dark) {
|
|
expect(css, `${fileName} 暗色主题应包含 color-scheme: dark`).toMatch(/color-scheme:\s*dark/);
|
|
} else {
|
|
expect(css, `${fileName} 亮色主题应包含 color-scheme: light`).toMatch(/color-scheme:\s*light/);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// CSS 文件大小增长限制 — 防止意外膨胀
|
|
// ============================================================================
|
|
|
|
/** 单文件大小上限 */
|
|
const SIZE_LIMITS = {
|
|
/** auto 主题仅有 @import + 元信息,应保持极小 */
|
|
AUTO_MAX: 1 * 1024, // 1 KB
|
|
/** 非 auto 主题包含完整主题变量 + 公共样式,当前约 108-117 KB */
|
|
SOLID_MAX: 200 * 1024, // 200 KB (给予 ~70% 增长空间)
|
|
/** 所有 CSS 文件总大小上限,当前约 2.6 MB */
|
|
TOTAL_MAX: 5 * 1024 * 1024, // 5 MB
|
|
} as const;
|
|
|
|
describe("CSS 文件大小限制", () => {
|
|
it("所有 auto 主题文件大小 < 1KB", () => {
|
|
const oversized: string[] = [];
|
|
for (const fileName of AUTO_THEME_FILES) {
|
|
const size = fs.statSync(path.join(DIST_DIR, fileName)).size;
|
|
if (size >= SIZE_LIMITS.AUTO_MAX) {
|
|
oversized.push(`${fileName}: ${(size / 1024).toFixed(1)}KB`);
|
|
}
|
|
}
|
|
expect(
|
|
oversized,
|
|
`以下 auto 主题文件体积超过 ${SIZE_LIMITS.AUTO_MAX / 1024}KB 上限:\n${oversized.join("\n")}`
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("所有非 auto 主题文件大小 < 200KB", () => {
|
|
const oversized: string[] = [];
|
|
for (const fileName of SOLID_THEME_FILES) {
|
|
const size = fs.statSync(path.join(DIST_DIR, fileName)).size;
|
|
if (size >= SIZE_LIMITS.SOLID_MAX) {
|
|
oversized.push(`${fileName}: ${(size / 1024).toFixed(1)}KB`);
|
|
}
|
|
}
|
|
expect(
|
|
oversized,
|
|
`以下非 auto 主题文件体积超过 ${SIZE_LIMITS.SOLID_MAX / 1024}KB 上限:\n${oversized.join("\n")}`
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("所有 CSS 文件总大小 < 5MB", () => {
|
|
let totalSize = 0;
|
|
for (const fileName of EXPECTED_THEME_FILES) {
|
|
totalSize += fs.statSync(path.join(DIST_DIR, fileName)).size;
|
|
}
|
|
expect(
|
|
totalSize,
|
|
`CSS 文件总大小 ${(totalSize / 1024 / 1024).toFixed(2)}MB 超过 ${SIZE_LIMITS.TOTAL_MAX / 1024 / 1024}MB 上限`
|
|
).toBeLessThan(SIZE_LIMITS.TOTAL_MAX);
|
|
});
|
|
});
|