更新前端样式

This commit is contained in:
焦龙言
2026-06-29 14:59:49 +08:00
parent d6f858b363
commit 960d2aa028
42 changed files with 4223 additions and 638 deletions
+36
View File
@@ -0,0 +1,36 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const routerPath = join(process.cwd(), "src/router/index.ts");
const routerSource = readFileSync(routerPath, "utf-8");
assertIncludes(routerSource, "await auth.loadSession();");
assertIncludes(routerSource, "catch");
assertIncludes(routerSource, "auth.logout();");
assertIncludes(routerSource, 'return { path: "/login", query: { redirect: to.fullPath } };');
assertBefore(routerSource, "try", "await auth.loadSession();");
assertBefore(routerSource, "await auth.loadSession();", "auth.logout();");
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertBefore(content: string, first: string, second: string): void {
const firstIndex = content.indexOf(first);
const secondIndex = content.indexOf(second);
if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
throw new Error(`期望 ${first} 出现在 ${second} 之前`);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ assertIncludes(summaryStripBlock, "border: var(--card-border);");
assertIncludes(summaryStripBlock, "background: var(--card);");
assertIncludes(summaryStripBlock, "box-shadow: var(--card-shadow);");
const innerCardBlock = cssBlock(".summary-metric,\\s*\\n\\.compact-stat,\\s*\\n\\.salary-profile-summary-item,\\s*\\n\\.organization-summary-strip div,\\s*\\n\\.organization-node-summary div,\\s*\\n\\.employee-inline-metric,\\s*\\n\\.workflow-steps span,\\s*\\n\\.recent-item,\\s*\\n\\.salary-profile-modal-section,\\s*\\n\\.report-total-item,\\s*\\n\\.report-inline-metrics span,\\s*\\n\\.report-attendance-card");
const innerCardBlock = cssBlock(".summary-metric,\\s*\\n\\.compact-stat,\\s*\\n\\.salary-profile-summary-item,\\s*\\n\\.organization-summary-strip div,\\s*\\n\\.organization-node-summary div,\\s*\\n\\.workflow-steps span,\\s*\\n\\.recent-item,\\s*\\n\\.salary-profile-modal-section,\\s*\\n\\.report-total-item,\\s*\\n\\.report-inline-metrics span,\\s*\\n\\.report-attendance-card");
assertIncludes(innerCardBlock, "border: var(--card-inner-border);");
assertIncludes(innerCardBlock, "background: var(--card-inner-bg);");
assertIncludes(innerCardBlock, "box-shadow: var(--card-inner-shadow);");
+134
View File
@@ -0,0 +1,134 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const view = readFileSync(join(root, "src/views/CommissionsView.vue"), "utf-8");
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
assertIncludes(view, "view-stack commissions-page");
assertIncludes(view, "summary-strip commission-summary-strip");
assertIncludes(view, "commission-list-panel");
assertIncludes(view, "commission-filter-bar");
assertIncludes(view, "commission-keyword-field");
assertIncludes(view, "commission-month-field");
assertIncludes(view, "commission-search-button");
assertIncludes(view, "row-action-group commission-row-actions");
assertIncludes(view, "table-action-icon");
assertIncludes(view, "title=\"编辑提成\"");
assertIncludes(view, "title=\"删除提成\"");
assertIncludes(view, "sourceTypeLabel");
assertIncludes(view, "manual: \"手工录入\"");
assertIncludes(view, "excel: \"Excel导入\"");
assertIncludes(view, "dingtalk: \"钉钉同步\"");
assertIncludes(view, "{{ sourceTypeLabel(record.source_type) }}");
assertNotIncludes(view, "{{ record.source_type }}");
assertIncludes(view, "openCreateModal");
assertIncludes(view, "showCommissionModal");
assertIncludes(view, "closeCommissionModal");
assertIncludes(view, "Teleport to=\"body\"");
assertIncludes(view, "modal-backdrop commission-modal-backdrop");
assertIncludes(view, "profile-modal commission-modal");
assertIncludes(view, "aria-label=\"提成维护\"");
assertIncludes(view, "commission-form");
assertIncludes(view, "commission-form-grid");
assertIncludes(view, "commission-business-field");
assertIncludes(view, "commission-remark-field");
assertIncludes(view, "rows=\"2\"");
assertIncludes(view, "commission-form-actions");
assertIncludes(view, "@click=\"openCreateModal\"");
assertIncludes(view, "@click=\"closeCommissionModal\"");
assertNotIncludes(view, "field span-row");
assertNotIncludes(view, "commission-entry-panel");
const pageBlock = cssBlock(".commissions-page");
assertIncludes(pageBlock, "gap: 14px;");
const summaryBlock = cssBlock(".commission-summary-strip");
assertIncludes(summaryBlock, "display: grid;");
assertIncludes(summaryBlock, "grid-template-columns: repeat(3, minmax(0, 1fr));");
assertIncludes(summaryBlock, "padding: 8px;");
const summaryMetricBlock = cssBlock(".commission-summary-strip .summary-metric");
assertIncludes(summaryMetricBlock, "min-height: 58px;");
assertIncludes(summaryMetricBlock, "padding: 8px 12px;");
const summaryStrongBlock = cssBlock(".commission-summary-strip .summary-copy strong");
assertIncludes(summaryStrongBlock, "font-size: 20px;");
const listPanelBlock = cssBlock(".commission-list-panel");
assertIncludes(listPanelBlock, "overflow: hidden;");
const listHeaderBlock = cssBlock(".commission-list-header");
assertIncludes(listHeaderBlock, "align-items: center;");
const filterBlock = cssBlock(".commission-filter-bar");
assertIncludes(filterBlock, "grid-template-columns: minmax(220px, 420px) 180px 104px minmax(0, 1fr);");
assertIncludes(filterBlock, "align-items: end;");
const searchButtonBlock = cssBlock(".commission-search-button");
assertIncludes(searchButtonBlock, "width: 104px;");
const rowActionsBlock = cssBlock(".commission-row-actions");
assertIncludes(rowActionsBlock, "display: inline-flex;");
assertIncludes(rowActionsBlock, "justify-content: flex-end;");
const actionIconBlock = cssBlock(".table-action-icon");
assertIncludes(actionIconBlock, "width: 34px;");
assertIncludes(actionIconBlock, "height: 34px;");
const modalBlock = cssBlock(".commission-modal");
assertIncludes(modalBlock, "width: min(900px, 100%);");
assertIncludes(modalBlock, "max-height: min(720px, calc(100vh - 48px));");
const formBlock = cssBlock(".commission-form");
assertIncludes(formBlock, "padding: 14px var(--panel-padding) var(--panel-padding);");
const formGridBlock = cssBlock(".commission-form-grid");
assertIncludes(formGridBlock, "grid-template-columns: minmax(220px, 1.2fr) minmax(160px, 0.75fr) minmax(190px, 1fr) minmax(150px, 0.7fr);");
assertIncludes(formGridBlock, "gap: 10px 14px;");
const remarkFieldBlock = cssBlock(".commission-remark-field textarea");
assertIncludes(remarkFieldBlock, "min-height: 44px;");
assertIncludes(remarkFieldBlock, "max-height: 76px;");
const formActionsBlock = cssBlock(".commission-form-actions");
assertIncludes(formActionsBlock, "align-self: end;");
assertIncludes(formActionsBlock, "justify-content: flex-end;");
assertIncludes(css, ".commission-business-field,\n .commission-remark-field,\n .commission-form-actions {\n grid-column: auto;");
assertIncludes(css, ".commission-summary-strip,\n .commission-form-grid,\n .commission-filter-bar {\n grid-template-columns: 1fr;");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);
if (!match) {
throw new Error(`缺少样式块 ${selector}`);
}
return match[1];
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string): void {
if (content.includes(unexpected)) {
throw new Error(`不应包含 ${unexpected}`);
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,72 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const view = readFileSync(join(root, "src/views/ConfigCenterView.vue"), "utf-8");
const template = extractTemplate(view);
assertIncludes(template, "薪资规则设置");
assertIncludes(template, "设置考勤、加班、迟到、缺卡和请假等工资核算规则。");
assertIncludes(template, "恢复默认规则");
assertIncludes(template, "规则清单");
assertIncludes(template, "全部规则");
assertIncludes(view, "attendance: \"考勤规则\"");
assertIncludes(view, "payroll: \"薪资规则\"");
for (const label of ["规则", "当前设置", "是否使用", "用途说明", "操作"]) {
assertIncludes(template, `<th>${label}</th>`, `规则表格应展示业务化表头:${label}`);
}
for (const technicalText of ["配置列表", "<th>配置</th>", "<th>分组</th>", "<th>类型</th>", "全部分组"]) {
assertNotIncludes(template, technicalText, `规则页不应展示技术化文案:${technicalText}`);
}
for (const technicalBinding of [
"{{ config.config_key }}",
"v-model=\"config.config_group\"",
"v-model=\"config.value_type\"",
"<option value=\"string\">string</option>",
"<option value=\"integer\">integer</option>",
"<option value=\"number\">number</option>",
"<option value=\"boolean\">boolean</option>",
"<option value=\"json\">json</option>",
]) {
assertNotIncludes(template, technicalBinding, `规则页不应暴露系统字段:${technicalBinding}`);
}
assertIncludes(view, "groupLabel(config.config_group)", "规则分组应转换成业务名称展示");
assertIncludes(view, "formatConfigValue(config)", "规则值应转换成用户能理解的展示方式");
assertIncludes(template, "weekday-toggle-group", "周末规则应使用星期选择,不应展示原始数字数组");
assertIncludes(view, "displayEditValue(config)", "复杂规则编辑值应转换成业务可读格式");
assertIncludes(view, "updateDisplayValue(config", "用户输入应再转换回系统保存值");
assertNotIncludes(template, "v-model=\"config.config_value\"", "不应直接把系统保存值暴露给用户编辑");
function extractTemplate(content: string): string {
const match = content.match(/<template>([\s\S]*)<\/template>/);
if (!match) {
throw new Error("缺少 template");
}
return match[1];
}
function assertIncludes(content: string, expected: string, message?: string): void {
if (!content.includes(expected)) {
throw new Error(message || `期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string, message?: string): void {
if (content.includes(unexpected)) {
throw new Error(message || `不应包含 ${unexpected}`);
}
}
@@ -0,0 +1,88 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const component = readFileSync(join(root, "src/components/DatePicker.vue"), "utf-8");
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
const datePages = [
"CommissionsView.vue",
"MonthlyPayrollView.vue",
"PayrollView.vue",
"RealtimeAttendanceView.vue",
"ReportsView.vue",
"SalaryProfilesView.vue",
];
for (const page of datePages) {
const content = readFileSync(join(root, "src/views", page), "utf-8");
assertIncludes(content, "DatePicker", `${page} 应使用统一 DatePicker 组件`);
assertNotIncludes(content, "type=\"month\"", `${page} 不应再使用浏览器原生月份选择器`);
assertNotIncludes(content, "type=\"date\"", `${page} 不应再使用浏览器原生日期选择器`);
}
assertIncludes(component, 'mode: "month" | "date"');
assertIncludes(component, "date-picker-panel");
assertIncludes(component, "<Teleport to=\"body\">", "日期面板应挂到 body,避免被父级面板裁剪");
assertIncludes(component, "panelRef", "日期面板应独立追踪自身节点,保证弹层内部点击不会被当成外部点击");
assertIncludes(component, "panelStyle", "日期面板应使用运行时坐标定位到触发按钮附近");
assertIncludes(component, "updatePanelPosition", "日期面板打开、滚动或窗口变化时应重新定位");
assertIncludes(component, "month-grid");
assertIncludes(component, "calendar-grid");
assertIncludes(component, "emit(\"change\"");
assertIncludes(component, "本月");
assertIncludes(component, "今天");
const pickerBlock = cssBlock(".date-picker");
assertIncludes(pickerBlock, "position: relative;");
const triggerBlock = cssBlock(".date-picker-trigger");
assertIncludes(triggerBlock, "min-height: var(--control-height);");
assertIncludes(triggerBlock, "border: 1px solid var(--line);");
const panelBlock = cssBlock(".date-picker-panel");
assertIncludes(panelBlock, "position: fixed;");
assertIncludes(panelBlock, "max-height: min(420px, calc(100vh - 24px));");
assertIncludes(panelBlock, "overflow: auto;");
assertIncludes(panelBlock, "box-shadow: var(--shadow);");
const monthGridBlock = cssBlock(".month-grid");
assertIncludes(monthGridBlock, "grid-template-columns: repeat(4, minmax(0, 1fr));");
const calendarGridBlock = cssBlock(".calendar-grid");
assertIncludes(calendarGridBlock, "grid-template-columns: repeat(7, minmax(0, 1fr));");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);
if (!match) {
throw new Error(`缺少样式块 ${selector}`);
}
return match[1];
}
function assertIncludes(content: string, expected: string, message?: string): void {
if (!content.includes(expected)) {
throw new Error(message || `期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string, message?: string): void {
if (content.includes(unexpected)) {
throw new Error(message || `不应包含 ${unexpected}`);
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+103
View File
@@ -0,0 +1,103 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const view = readFileSync(join(root, "src/views/EmployeesView.vue"), "utf-8");
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
assertIncludes(view, "view-stack employees-page");
assertIncludes(view, "employee-list-header");
assertIncludes(view, "employee-list-title");
assertIncludes(view, "employee-inline-metric");
assertNotIncludes(view, "employee-header-actions");
assertIncludes(view, "employee_info_keyword");
assertIncludes(view, "organization_keyword");
assertIncludes(view, "employee-filter-employee");
assertIncludes(view, "employee-filter-organization");
assertIncludes(view, "employee-filter-status");
assertIncludes(view, "employee-search-button");
assertIncludes(view, "employee-status-pill");
assertIncludes(view, "employee-status-dot");
assertNotIncludes(view, "placeholder=\"编号、姓名、钉钉ID、部门或岗位\"");
assertNotIncludes(view, "class=\"status-chip\"");
assertIncludes(view, "管理员工资料,工资核算会优先使用这里的信息。");
assertIncludes(view, "在职员工");
assertIncludes(view, "<em>人</em>");
assertIncludes(view, "<span>找员工</span>");
assertIncludes(view, "placeholder=\"输入姓名、编号或手机号\"");
assertIncludes(view, "<span>找部门/岗位</span>");
assertIncludes(view, "placeholder=\"输入部门或岗位名称\"");
assertIncludes(view, "<th>钉钉账号</th>");
assertIncludes(view, "employee.dingtalk_user_id || \"未绑定\"");
assertIncludes(view, "<span>钉钉账号</span>");
assertIncludes(view, "placeholder=\"用于匹配钉钉考勤,可不填\"");
assertNotIncludes(view, "钉钉用户ID");
assertNotIncludes(view, "<span>组织</span>");
assertNotIncludes(view, "参与薪资计算");
const pageBlock = cssBlock(".employees-page");
assertIncludes(pageBlock, "gap: 14px;");
const headerBlock = cssBlock(".employee-list-header");
assertIncludes(headerBlock, "display: grid;");
assertIncludes(headerBlock, "grid-template-columns: minmax(0, 1fr) auto;");
const titleBlock = cssBlock(".employee-list-title");
assertIncludes(titleBlock, "display: flex;");
assertIncludes(titleBlock, "align-items: center;");
const metricBlock = cssBlock(".employee-inline-metric");
assertIncludes(metricBlock, "min-height: 30px;");
assertIncludes(metricBlock, "box-shadow: none;");
const filterBlock = cssBlock(".employee-filter-bar");
assertIncludes(filterBlock, "grid-template-columns: minmax(180px, 260px) minmax(180px, 260px) minmax(130px, 160px) 104px minmax(0, 1fr);");
const scopedFilterBlock = cssBlock(".table-filter-bar.employee-filter-bar");
assertIncludes(scopedFilterBlock, "grid-template-columns: minmax(180px, 260px) minmax(180px, 260px) minmax(130px, 160px) 104px minmax(0, 1fr);");
const statusBlock = cssBlock(".employee-status-pill");
assertIncludes(statusBlock, "display: inline-flex;");
assertIncludes(statusBlock, "min-width: 0;");
assertIncludes(statusBlock, "background: transparent;");
const dotBlock = cssBlock(".employee-status-dot");
assertIncludes(dotBlock, "width: 7px;");
assertIncludes(dotBlock, "height: 7px;");
assertIncludes(css, ".table-filter-bar.employee-filter-bar {\n grid-template-columns: 1fr;");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);
if (!match) {
throw new Error(`缺少样式块 ${selector}`);
}
return match[1];
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string): void {
if (content.includes(unexpected)) {
throw new Error(`不应包含 ${unexpected}`);
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+15 -2
View File
@@ -15,6 +15,10 @@ const root = process.cwd();
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
const themeStore = readFileSync(join(root, "src/stores/theme.ts"), "utf-8");
const appRootBlock = cssBlock("html,\nbody,\n#app");
assertIncludes(appRootBlock, "max-width: 100%;");
assertIncludes(appRootBlock, "overflow-x: hidden;");
const rootBlock = cssBlock(":root");
assertIncludes(rootBlock, "--base-font-size: 14px;");
assertIncludes(rootBlock, "--page-padding: 20px;");
@@ -25,6 +29,9 @@ const pageHeaderBlock = cssBlock(".page-header");
assertIncludes(pageHeaderBlock, "min-height: 52px;");
assertIncludes(pageHeaderBlock, "padding-bottom: 2px;");
const bodyBlock = cssBlock("body");
assertIncludes(bodyBlock, "overflow-x: hidden;");
const pageTitleBlock = cssBlock(".page-header h1");
assertIncludes(pageTitleBlock, "font-size: 26px;");
@@ -47,6 +54,12 @@ const tableFilterBlock = cssBlock(".table-filter-bar");
assertIncludes(tableFilterBlock, "padding: 10px var(--panel-padding);");
assertIncludes(tableFilterBlock, "background: var(--surface);");
const tableWrapBlock = cssBlock(".table-wrap");
assertIncludes(tableWrapBlock, "max-width: 100%;");
assertIncludes(tableWrapBlock, "min-width: 0;");
assertIncludes(tableWrapBlock, "contain: inline-size;");
assertIncludes(tableWrapBlock, "overflow-x: auto;");
const tableHeaderBlock = cssBlock(".data-table th");
assertIncludes(tableHeaderBlock, "position: sticky;");
assertIncludes(tableHeaderBlock, "top: 0;");
@@ -55,8 +68,8 @@ const modalBackdropBlock = cssBlock(".modal-backdrop");
assertIncludes(modalBackdropBlock, "backdrop-filter: blur(8px);");
const employeeMetricBlock = cssBlock(".employee-inline-metric");
assertIncludes(employeeMetricBlock, "min-height: 42px;");
assertIncludes(employeeMetricBlock, "padding: 6px 10px;");
assertIncludes(employeeMetricBlock, "min-height: 30px;");
assertIncludes(employeeMetricBlock, "box-shadow: none;");
const userFormBlock = cssBlock(".user-form");
assertIncludes(userFormBlock, "grid-template-columns: repeat(4, minmax(0, 1fr));");
@@ -0,0 +1,34 @@
declare const process: { cwd(): string };
declare function require(name: string): {
existsSync?: (path: string) => boolean;
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { existsSync, readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!existsSync || !readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const loginView = readFileSync(join(root, "src/views/LoginView.vue"), "utf-8");
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
const backgroundPath = join(root, "src/assets/login-background.png");
assertIncludes(loginView, "login-background-layer");
assertIncludes(css, "url(\"./login-background.png\")");
assertIncludes(css, ".login-background-layer");
assertIncludes(css, ".login-visual::before");
assertIncludes(css, ".login-visual::after");
if (!existsSync(backgroundPath)) {
throw new Error("登录页背景图资产不存在");
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
@@ -0,0 +1,59 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const css = readFileSync(join(process.cwd(), "src/assets/styles.css"), "utf-8");
const loginScreen = cssBlock(".login-screen");
assertIncludes(loginScreen, "grid-template-columns: minmax(340px, 34vw) minmax(0, 1fr);");
const loginFormSide = cssBlock(".login-form-side");
assertIncludes(loginFormSide, "background: linear-gradient(180deg, #f8fafc 0%, #f3f6fa 100%);");
assertIncludes(loginFormSide, "box-shadow: inset -1px 0 0 rgba(var(--primary-rgb), 0.08);");
const loginCard = cssBlock(".login-card");
assertIncludes(loginCard, "width: min(360px, 100%);");
const loginVisual = cssBlock(".login-visual");
assertIncludes(loginVisual, "justify-items: start;");
assertNotIncludes(loginVisual, "border-left: 1px solid var(--line);");
const ledgerVisual = cssBlock(".ledger-visual");
assertIncludes(ledgerVisual, "width: min(360px, 100%);");
assertIncludes(ledgerVisual, "background: rgba(255, 255, 255, 0.68);");
assertIncludes(ledgerVisual, "backdrop-filter: blur(10px);");
assertIncludes(ledgerVisual, "padding: 22px;");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);
if (!match) {
throw new Error(`缺少样式块 ${selector}`);
}
return match[1];
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string): void {
if (content.includes(unexpected)) {
throw new Error(`不应包含 ${unexpected}`);
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+88
View File
@@ -0,0 +1,88 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const pages = [
"RealtimeAttendanceView.vue",
"ConfigCenterView.vue",
"ReportsView.vue",
"OperationLogsView.vue",
"UsersView.vue",
"CommissionsView.vue",
"MonthlyPayrollView.vue",
"EmployeesView.vue",
"SalaryProfilesView.vue",
"OrganizationView.vue",
];
for (const page of pages) {
const content = readFileSync(join(root, "src/views", page), "utf-8");
assertNotIncludes(content, "<span>刷新</span>", `${page} 不应展示无业务含义的刷新按钮`);
}
const realtimeView = readView("RealtimeAttendanceView.vue");
assertIncludes(realtimeView, "同步钉钉");
assertIncludes(realtimeView, "@change=\"loadTodayAttendance\"");
assertNotIncludes(realtimeView, "RefreshCw");
const reportsView = readView("ReportsView.vue");
assertIncludes(reportsView, "查询");
assertIncludes(reportsView, "@submit.prevent=\"loadReport\"");
assertNotIncludes(reportsView, "RefreshCw");
const configView = readView("ConfigCenterView.vue");
assertIncludes(configView, "恢复默认规则");
assertIncludes(configView, "保存");
assertNotIncludes(configView, "RefreshCw");
const commissionView = readView("CommissionsView.vue");
assertIncludes(commissionView, "导入Excel");
assertIncludes(commissionView, "新增提成");
assertNotIncludes(commissionView, "RefreshCw");
const monthlyView = readView("MonthlyPayrollView.vue");
assertIncludes(monthlyView, "同步钉钉核算");
assertIncludes(monthlyView, "导入 Excel 核算");
assertNotIncludes(monthlyView, "RefreshCw");
const employeesView = readView("EmployeesView.vue");
assertIncludes(employeesView, "新增员工");
assertIncludes(employeesView, "在职员工");
assertNotIncludes(employeesView, "RefreshCw");
const salaryProfilesView = readView("SalaryProfilesView.vue");
assertIncludes(salaryProfilesView, "薪资档案");
assertIncludes(salaryProfilesView, "编辑");
assertNotIncludes(salaryProfilesView, "RefreshCw");
const organizationView = readView("OrganizationView.vue");
assertIncludes(organizationView, "新增部门");
assertIncludes(organizationView, "新增岗位");
assertNotIncludes(organizationView, "RefreshCw");
function readView(filename: string): string {
return readFileSync(join(root, "src/views", filename), "utf-8");
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string, message?: string): void {
if (content.includes(unexpected)) {
throw new Error(message || `不应包含 ${unexpected}`);
}
}
+145
View File
@@ -17,9 +17,15 @@ const realtimeApiPath = join(root, "src/api/realtimeAttendance.ts");
const monthlyApiPath = join(root, "src/api/monthlyPayroll.ts");
const realtimeViewPath = join(root, "src/views/RealtimeAttendanceView.vue");
const monthlyViewPath = join(root, "src/views/MonthlyPayrollView.vue");
const resultsTablePath = join(root, "src/components/ResultsTable.vue");
const topBarPath = join(root, "src/components/TopBar.vue");
const dashboardViewPath = join(root, "src/views/DashboardView.vue");
const routerPath = join(root, "src/router/index.ts");
assertTruthy(existsSync(realtimeApiPath), "缺少实时考勤 API 客户端");
assertTruthy(existsSync(monthlyApiPath), "缺少月度核算 API 客户端");
assertTruthy(existsSync(resultsTablePath), "缺少工资明细表组件");
assertTruthy(existsSync(topBarPath), "缺少顶部导航组件");
const realtimeApi = readFileSync(realtimeApiPath, "utf-8");
assertIncludes(realtimeApi, "/api/attendance/realtime/today");
@@ -27,15 +33,146 @@ assertIncludes(realtimeApi, "/api/attendance/realtime/sync");
const monthlyApi = readFileSync(monthlyApiPath, "utf-8");
assertIncludes(monthlyApi, "/api/payroll/monthly/runs");
assertIncludes(monthlyApi, "/api/payroll/monthly/dingtalk");
assertIncludes(monthlyApi, "/api/payroll/monthly/excel");
const realtimeView = readFileSync(realtimeViewPath, "utf-8");
assertNotIncludes(realtimeView, "待接入菜单");
assertNotIncludes(realtimeView, "待接入钉钉同步");
assertIncludes(realtimeView, "预估金额,最终以月度核算锁定结果为准");
assertIncludes(realtimeView, "realtime-page-header");
assertIncludes(realtimeView, "realtime-header-actions");
assertIncludes(realtimeView, "realtime-date-field");
assertIncludes(realtimeView, "realtime-overview-panel");
assertIncludes(realtimeView, "realtime-overview-header");
assertIncludes(realtimeView, "realtime-overview-title");
assertIncludes(realtimeView, "realtime-overview-stats");
assertIncludes(realtimeView, "syncStatusText");
assertIncludes(realtimeView, "formatSyncTime");
assertIncludes(realtimeView, "realtime-sync-chip");
assertNotIncludes(realtimeView, 'response?.sync_job_id ? "已同步" : "未同步"');
assertIncludes(realtimeView, "employeeQuery");
assertIncludes(realtimeView, "departmentFilter");
assertIncludes(realtimeView, "departmentOptions");
assertIncludes(realtimeView, "filteredRows");
assertIncludes(realtimeView, "hasRealtimeFilters");
assertIncludes(realtimeView, "clearRealtimeFilters");
assertIncludes(realtimeView, "realtime-table-filter-bar");
assertIncludes(realtimeView, "部门筛选");
assertIncludes(realtimeView, "员工搜索");
assertIncludes(realtimeView, "realtime-clear-filter");
assertIncludes(realtimeView, ":disabled=\"!hasRealtimeFilters\"");
assertIncludes(realtimeView, "@click=\"clearRealtimeFilters\"");
assertIncludes(realtimeView, "清空");
assertIncludes(realtimeView, "显示 {{ filteredRows.length }} / {{ rows.length }} 人");
assertIncludes(realtimeView, "v-for=\"row in filteredRows\"");
const topBar = readFileSync(topBarPath, "utf-8");
assertIncludes(topBar, "useRoute");
assertIncludes(topBar, "showGlobalSearch");
assertIncludes(topBar, 'route.path !== "/attendance/realtime"');
assertIncludes(topBar, 'v-if="showGlobalSearch"');
const monthlyView = readFileSync(monthlyViewPath, "utf-8");
assertNotIncludes(monthlyView, "下一阶段接入");
assertNotIncludes(monthlyView, "核算流程");
assertNotIncludes(monthlyView, "计算编号仅用于系统排查");
assertIncludes(monthlyView, "本月核算");
assertNotIncludes(monthlyView, "工资核算工作台");
assertIncludes(monthlyView, "同步钉钉核算");
assertIncludes(monthlyView, "导入 Excel 核算");
assertNotIncludes(monthlyView, "payroll-workbench");
assertNotIncludes(monthlyView, "payroll-flow-strip");
assertIncludes(monthlyView, "payroll-compact-toolbar");
assertIncludes(monthlyView, "payroll-toolbar-month");
assertIncludes(monthlyView, "payroll-toolbar-summary");
assertIncludes(monthlyView, "payroll-toolbar-actions");
assertIncludes(monthlyView, "payroll-toolbar-export");
assertIncludes(monthlyView, "payroll-toolbar-range");
assertIncludes(monthlyView, "payroll-detail-panel");
assertIncludes(monthlyView, "payroll-detail-summary");
assertIncludes(monthlyView, "payroll-secondary-grid");
assertIncludes(monthlyView, ":exceptions=\"detail.exceptions\"");
assertNotIncludes(monthlyView, "payroll-command-copy");
assertNotIncludes(monthlyView, "payroll-command-card");
assertNotIncludes(monthlyView, "payroll-command-step");
assertNotIncludes(monthlyView, "payroll-step-index");
assertNotIncludes(monthlyView, "payroll-period-row");
assertNotIncludes(monthlyView, "payroll-source-row");
assertNotIncludes(monthlyView, "payroll-source-copy");
assertNotIncludes(monthlyView, "payroll-summary-band");
assertNotIncludes(monthlyView, "核算操作");
assertNotIncludes(monthlyView, "1 选择月份");
assertNotIncludes(monthlyView, "2 取数核算");
assertNotIncludes(monthlyView, "3 核对工资");
assertNotIncludes(monthlyView, "4 锁定导出");
assertNotIncludes(monthlyView, "payroll-action-card");
assertNotIncludes(monthlyView, "payroll-result-panel");
assertIncludes(monthlyView, "锁定后本月工资不能重新计算");
assertIncludes(monthlyView, "核算历史");
assertIncludes(monthlyView, "查看明细");
assertIncludes(monthlyView, "技术信息");
assertIncludes(monthlyView, "<summary>技术信息</summary>");
assertNotIncludes(monthlyView, "请输入任务编号");
assertIncludes(monthlyView, "calculateMonthlyFromDingTalk");
assertIncludes(monthlyView, "calculateMonthlyFromExcel");
assertIncludes(monthlyView, "monthly_payroll:calculate");
assertNotIncludes(monthlyView, "monthly-source-strip");
assertIncludes(monthlyView, "核算月份");
assertIncludes(monthlyView, "同步范围");
assertIncludes(monthlyView, "Excel 文件月份");
assertIncludes(monthlyView, "请选择核算月份后再同步钉钉");
assertIncludes(monthlyView, "已根据文件识别核算月份");
assertIncludes(monthlyView, "生成 Excel 结果文件");
assertNotIncludes(monthlyView, "优先同步钉钉,也可导入钉钉月度汇总 Excel。");
assertNotIncludes(monthlyView, "选择 Excel 后会自动识别文件月份。");
assertNotIncludes(monthlyView, ">01<");
assertNotIncludes(monthlyView, ">02<");
assertBefore(monthlyView, "核算月份", "同步钉钉核算");
assertBefore(monthlyView, "工资明细", "核算历史");
assertBefore(monthlyView, "工资明细", "异常清单");
assertBefore(monthlyView, "payroll-compact-toolbar", "payroll-detail-panel");
assertBefore(monthlyView, "payroll-detail-panel", "payroll-secondary-grid");
const resultsTable = readFileSync(resultsTablePath, "utf-8");
assertIncludes(resultsTable, "embedded");
assertIncludes(resultsTable, "salary-detail-table");
assertIncludes(resultsTable, "salary-summary-toolbar");
assertIncludes(resultsTable, "salary-summary-table");
assertIncludes(resultsTable, "salary-detail-drawer");
assertIncludes(resultsTable, "filteredResults");
assertIncludes(resultsTable, "anomalyLabel");
assertIncludes(resultsTable, "查看构成");
assertIncludes(resultsTable, "部门");
assertIncludes(resultsTable, "异常");
assertIncludes(resultsTable, "实发工资");
assertBefore(resultsTable, "salary-summary-table", "salary-detail-drawer");
const dashboardView = readFileSync(dashboardViewPath, "utf-8");
assertNotIncludes(dashboardView, "/payroll/jobs");
assertNotIncludes(dashboardView, 'to="/payroll"');
assertNotIncludes(dashboardView, "readRecentJobs");
assertNotIncludes(dashboardView, "RecentPayrollJob");
assertIncludes(dashboardView, "listMonthlyRuns");
assertIncludes(dashboardView, "exportMonthlyRun");
assertNotIncludes(dashboardView, "payroll:dingtalk:calculate");
assertIncludes(dashboardView, "monthly_payroll:calculate");
assertNotIncludes(dashboardView, "任务编号");
assertNotIncludes(dashboardView, "任务记录");
assertNotIncludes(dashboardView, "最近任务");
assertNotIncludes(dashboardView, "任务来源");
assertNotIncludes(dashboardView, "暂无计算记录");
assertIncludes(dashboardView, "核算历史");
assertIncludes(dashboardView, "最近核算人数");
assertIncludes(dashboardView, "工资月份");
assertIncludes(dashboardView, "实发合计");
const router = readFileSync(routerPath, "utf-8");
assertIncludes(router, 'path: "payroll"');
assertIncludes(router, 'redirect: "/payroll/monthly"');
assertNotIncludes(router, "../views/PayrollView.vue");
assertNotIncludes(router, "component: PayrollView");
assertNotIncludes(router, 'meta: { permission: "payroll:excel:calculate" }');
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
@@ -54,3 +191,11 @@ function assertTruthy(value: boolean, message: string): void {
throw new Error(message);
}
}
function assertBefore(content: string, first: string, second: string): void {
const firstIndex = content.indexOf(first);
const secondIndex = content.indexOf(second);
if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
throw new Error(`期望 ${first} 出现在 ${second} 之前`);
}
}
+24
View File
@@ -0,0 +1,24 @@
import {
extractMonthFromExcelFilename,
formatMonthRange,
isSalaryMonth,
} from "../src/utils/payrollMonth";
const detected = extractMonthFromExcelFilename("宁波兆安流体科技有限公司_月度汇总_20260501-20260531.xlsx");
assertEqual(detected?.month || "", "2026-05");
assertEqual(detected?.rangeLabel || "", "2026/05/01 至 2026/05/31");
const dashed = extractMonthFromExcelFilename("考勤汇总_2026-06.xlsx");
assertEqual(dashed?.month || "", "2026-06");
assertEqual(formatMonthRange("2026-05"), "2026/05/01 至 2026/05/31");
assertEqual(formatMonthRange(""), "请选择核算月份");
assertEqual(String(isSalaryMonth("2026-05")), "true");
assertEqual(String(isSalaryMonth("")), "false");
assertEqual(String(isSalaryMonth("2026-13")), "false");
function assertEqual(actual: string, expected: string): void {
if (actual !== expected) {
throw new Error(`期望 ${expected},实际 ${actual}`);
}
}
+16 -1
View File
@@ -26,10 +26,13 @@ assertEqual(
);
assertEqual(sectionCodes("核心工作"), "dashboard,attendance_realtime");
assertEqual(sectionCodes("月度核算"), "monthly_payroll,jobs,commissions");
assertEqual(sectionCodes("月度核算"), "monthly_payroll,commissions");
assertEqual(sectionNames("月度核算"), "工资核算,提成录入");
assertEqual(sectionCodes("员工薪资"), "employees,salary_profiles,organization");
assertEqual(sectionCodes("报表分析"), "reports");
assertEqual(sectionCodes("系统管理"), "configs,users,operation_logs");
assertEqual(hasMenu("payroll"), "false");
assertEqual(hasMenu("jobs"), "false");
function sectionCodes(title: string): string {
const section = sections.find((item) => item.title === title);
@@ -39,8 +42,20 @@ function sectionCodes(title: string): string {
return section.items.map((item) => item.code).join(",");
}
function sectionNames(title: string): string {
const section = sections.find((item) => item.title === title);
if (!section) {
throw new Error(`找不到菜单分组:${title}`);
}
return section.items.map((item) => item.name).join(",");
}
function assertEqual(actual: string, expected: string): void {
if (actual !== expected) {
throw new Error(`期望 ${expected},实际 ${actual}`);
}
}
function hasMenu(code: string): string {
return String(sections.some((section) => section.items.some((item) => item.code === code)));
}
+84
View File
@@ -0,0 +1,84 @@
declare const process: { cwd(): string };
declare function require(name: string): {
readFileSync?: (path: string, encoding: string) => string;
join?: (...parts: string[]) => string;
};
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
if (!readFileSync || !join) {
throw new Error("测试运行环境缺少文件读取能力");
}
const root = process.cwd();
const view = readFileSync(join(root, "src/views/UsersView.vue"), "utf-8");
const css = readFileSync(join(root, "src/assets/styles.css"), "utf-8");
assertIncludes(view, "view-stack users-page");
assertIncludes(view, "showUserModal");
assertIncludes(view, "openCreateModal");
assertIncludes(view, "closeUserModal");
assertIncludes(view, "user-list-panel");
assertIncludes(view, "user-list-header");
assertIncludes(view, "@click=\"openCreateModal\"");
assertIncludes(view, "Teleport to=\"body\"");
assertIncludes(view, "modal-backdrop user-modal-backdrop");
assertIncludes(view, "profile-modal user-modal");
assertIncludes(view, "aria-label=\"用户维护\"");
assertIncludes(view, "user-modal-form");
assertIncludes(view, "user-modal-fields");
assertIncludes(view, "user-modal-actions");
assertIncludes(view, "@click=\"closeUserModal\"");
assertNotIncludes(view, "<section class=\"panel\">\n <div class=\"panel-header\">\n <div>\n <h2>新增用户</h2>");
const pageBlock = cssBlock(".users-page");
assertIncludes(pageBlock, "gap: 14px;");
const listPanelBlock = cssBlock(".user-list-panel");
assertIncludes(listPanelBlock, "overflow: hidden;");
const listHeaderBlock = cssBlock(".user-list-header");
assertIncludes(listHeaderBlock, "align-items: center;");
const modalBlock = cssBlock(".user-modal");
assertIncludes(modalBlock, "width: min(920px, 100%);");
assertIncludes(modalBlock, "max-height: min(760px, calc(100vh - 48px));");
const modalFormBlock = cssBlock(".user-modal-form");
assertIncludes(modalFormBlock, "padding: 14px var(--panel-padding) var(--panel-padding);");
const modalFieldsBlock = cssBlock(".user-modal-fields");
assertIncludes(modalFieldsBlock, "grid-template-columns: repeat(4, minmax(0, 1fr));");
const modalActionsBlock = cssBlock(".user-modal-actions");
assertIncludes(modalActionsBlock, "grid-column: 1 / -1;");
assertIncludes(modalActionsBlock, "justify-content: flex-end;");
assertIncludes(css, ".user-modal-fields {\n grid-template-columns: 1fr;");
assertIncludes(css, ".user-modal-actions {\n justify-content: stretch;");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);
if (!match) {
throw new Error(`缺少样式块 ${selector}`);
}
return match[1];
}
function assertIncludes(content: string, expected: string): void {
if (!content.includes(expected)) {
throw new Error(`期望包含 ${expected}`);
}
}
function assertNotIncludes(content: string, unexpected: string): void {
if (content.includes(unexpected)) {
throw new Error(`不应包含 ${unexpected}`);
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+126 -1
View File
@@ -20,7 +20,7 @@ assertNotIncludes(loginVisual, "rgba(var(--primary-rgb), 0.94)");
const ledgerVisual = cssBlock(".ledger-visual");
assertIncludes(ledgerVisual, "background: var(--card);");
assertIncludes(ledgerVisual, "padding: 32px;");
assertIncludes(ledgerVisual, "padding: 22px;");
const appearancePanel = cssBlock(".appearance-panel");
assertIncludes(appearancePanel, "width: min(480px, calc(100vw - 20px));");
@@ -32,6 +32,131 @@ assertIncludes(primaryFocus, "box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16
const tableRows = cssBlock(".data-table tbody tr");
assertIncludes(tableRows, "transition: background var(--motion-duration-fast) ease;");
const realtimeHeaderActions = cssBlock(".realtime-header-actions");
assertIncludes(realtimeHeaderActions, "display: grid;");
assertIncludes(realtimeHeaderActions, "grid-template-columns: minmax(210px, 242px) 140px;");
assertIncludes(realtimeHeaderActions, "align-items: end;");
const realtimeDateField = cssBlock(".realtime-date-field");
assertIncludes(realtimeDateField, "min-width: 0;");
const realtimeOverviewPanel = cssBlock(".realtime-overview-panel");
assertIncludes(realtimeOverviewPanel, "overflow: hidden;");
const realtimeOverviewHeader = cssBlock(".realtime-overview-header");
assertIncludes(realtimeOverviewHeader, "align-items: center;");
assertIncludes(realtimeOverviewHeader, "padding: 12px var(--panel-padding) 10px;");
const realtimeOverviewTitle = cssBlock(".realtime-overview-title");
assertIncludes(realtimeOverviewTitle, "display: flex;");
assertIncludes(realtimeOverviewTitle, "align-items: baseline;");
assertIncludes(realtimeOverviewTitle, "gap: 10px;");
const realtimeOverviewTitleText = cssBlock(".realtime-overview-title h2,\n.realtime-overview-title p");
assertIncludes(realtimeOverviewTitleText, "margin: 0;");
const realtimeOverviewStats = cssBlock(".realtime-overview-stats");
assertIncludes(realtimeOverviewStats, "padding-top: 0;");
const realtimeOverviewNumbers = cssBlock(".realtime-overview-stats .compact-stat strong");
assertIncludes(realtimeOverviewNumbers, "font-size: 24px;");
const realtimeSyncChip = cssBlock(".realtime-sync-chip");
assertIncludes(realtimeSyncChip, "min-width: 96px;");
assertIncludes(realtimeSyncChip, "justify-content: center;");
const realtimeTableFilterBar = cssBlock(".realtime-table-filter-bar");
assertIncludes(realtimeTableFilterBar, "display: grid;");
assertIncludes(realtimeTableFilterBar, "grid-template-columns: minmax(180px, 220px) minmax(240px, 1fr) 78px auto;");
assertIncludes(realtimeTableFilterBar, "align-items: end;");
assertIncludes(realtimeTableFilterBar, "padding: 10px var(--panel-padding);");
const realtimeClearFilter = cssBlock(".realtime-clear-filter");
assertIncludes(realtimeClearFilter, "min-height: var(--control-height);");
assertIncludes(realtimeClearFilter, "width: 78px;");
assertIncludes(realtimeClearFilter, "justify-self: start;");
assertIncludes(realtimeClearFilter, "padding: 0 12px;");
assertIncludes(realtimeClearFilter, "white-space: nowrap;");
const realtimeFilterResult = cssBlock(".realtime-filter-result");
assertIncludes(realtimeFilterResult, "justify-self: end;");
const payrollCompactToolbar = cssBlock(".payroll-compact-toolbar");
assertIncludes(payrollCompactToolbar, "grid-template-columns: minmax(190px, 240px) minmax(0, 1fr) auto;");
assertIncludes(payrollCompactToolbar, "padding: 12px var(--panel-padding);");
const payrollToolbarSummary = cssBlock(".payroll-toolbar-summary");
assertIncludes(payrollToolbarSummary, "grid-template-columns: minmax(110px, 0.7fr) minmax(110px, 0.7fr) minmax(110px, 0.7fr) minmax(270px, 1.45fr);");
assertIncludes(payrollToolbarSummary, "border-inline: 1px solid var(--line-soft);");
const payrollToolbarRangeText = cssBlock(".payroll-toolbar-range strong,\n.payroll-toolbar-range small");
assertIncludes(payrollToolbarRangeText, "overflow: visible;");
assertIncludes(payrollToolbarRangeText, "white-space: normal;");
assertIncludes(payrollToolbarRangeText, "text-overflow: clip;");
const payrollToolbarActions = cssBlock(".payroll-toolbar-actions");
assertIncludes(payrollToolbarActions, "grid-template-columns: repeat(2, 138px);");
assertIncludes(payrollToolbarActions, "justify-content: end;");
const payrollToolbarActionButtons = cssBlock(".payroll-toolbar-actions .primary-button,\n.payroll-toolbar-actions .secondary-button");
assertIncludes(payrollToolbarActionButtons, "min-height: 34px;");
assertIncludes(payrollToolbarActionButtons, "padding: 0 10px;");
assertIncludes(payrollToolbarActionButtons, "font-size: 13px;");
const payrollDetailPanel = cssBlock(".payroll-detail-panel");
assertIncludes(payrollDetailPanel, "border-color: rgba(var(--accent-rgb), 0.2);");
assertIncludes(payrollDetailPanel, "overflow: hidden;");
const payrollDetailHeader = cssBlock(".payroll-detail-header");
assertIncludes(payrollDetailHeader, "grid-template-columns: minmax(0, 1fr) auto;");
const payrollDetailSummary = cssBlock(".payroll-detail-summary");
assertIncludes(payrollDetailSummary, "grid-template-columns: repeat(4, minmax(110px, auto));");
const salaryDetailTable = cssBlock(".salary-detail-table");
assertIncludes(salaryDetailTable, "font-size: 13px;");
assertIncludes(salaryDetailTable, "min-width: 1560px;");
const salarySummaryToolbar = cssBlock(".salary-summary-toolbar");
assertIncludes(salarySummaryToolbar, "grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(140px, 180px));");
const salarySummaryTable = cssBlock(".salary-summary-table");
assertIncludes(salarySummaryTable, "min-width: 860px;");
const salarySummaryTableCells = cssBlock(".salary-summary-table th,\n.salary-summary-table td");
assertIncludes(salarySummaryTableCells, "padding: 12px 14px;");
const salaryDetailDrawer = cssBlock(".salary-detail-drawer");
assertIncludes(salaryDetailDrawer, "grid-template-columns: repeat(4, minmax(0, 1fr));");
const salaryDetailDrawerHeader = cssBlock(".salary-detail-drawer-header");
assertIncludes(salaryDetailDrawerHeader, "grid-column: 1 / -1;");
const salaryDetailCells = cssBlock(".salary-detail-table th,\n.salary-detail-table td");
assertIncludes(salaryDetailCells, "padding: 10px 12px;");
const payrollSecondaryGrid = cssBlock(".payroll-secondary-grid");
assertIncludes(payrollSecondaryGrid, "grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);");
assertNotIncludes(css, ".payroll-workbench {");
assertNotIncludes(css, ".payroll-flow-strip");
assertNotIncludes(css, ".payroll-summary-band");
assertIncludes(css, "@media (max-width: 1320px)");
assertIncludes(css, ".payroll-compact-toolbar {\n grid-template-columns: 1fr;\n }");
assertIncludes(css, ".payroll-toolbar-summary {\n grid-template-columns: repeat(2, minmax(0, 1fr));");
assertIncludes(css, ".payroll-secondary-grid {\n grid-template-columns: 1fr;\n }");
assertIncludes(css, ".payroll-toolbar-summary,\n .payroll-toolbar-actions,\n .payroll-detail-summary {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }");
assertIncludes(css, ".realtime-header-actions {\n grid-template-columns: 1fr;\n }");
assertIncludes(css, ".realtime-overview-title {\n align-items: flex-start;\n flex-direction: column;");
assertIncludes(css, ".realtime-table-filter-bar {\n grid-template-columns: 1fr;");
assertNotIncludes(css, ".payroll-command-step");
assertNotIncludes(css, ".payroll-step-index");
assertNotIncludes(css, ".payroll-command-copy");
assertNotIncludes(css, ".payroll-period-row");
assertNotIncludes(css, ".payroll-source-row");
assertNotIncludes(css, ".payroll-source-copy");
function cssBlock(selector: string): string {
const pattern = new RegExp(`${escapeRegExp(selector)}\\s*\\{([\\s\\S]*?)\\n\\}`, "m");
const match = css.match(pattern);