Files
MailUI4Agents/client/electron/test/theme.test.mjs
JianFeeeee a8ac2fc28b test(criteria): 闭环——判据自报条数 + 每文件期望条数(只增不减的棘轮)
pi 指出的残余缺口:我上一轮加的自检 4 是**文本证据**(文件里有 `test(` / `check(` /
`process.exit(1)`),只能证明"**有能红的路径**",不能证明"**它跑过**"。反例很短:

```js
const check = () => {};     // 实现被换空(现实形态:合并冲突改坏实现)
check('a', false);          // 存在、也执行了,但什么都不会红
console.log('主题:通过');   // 有输出
```

## 落地(pi 给的闭环形状)

1. 自定义 `check()` 的判据结尾打一行机器可读汇总 `RESULT pass=<条数> fail=<失败数>`
   (`node:test` 的判据不用改,已有 `# pass N`);
2. `run-all.mjs` **只解析这个固定 marker**(不猜口语汇总——「窄屏布局:全部通过」里没有数字,
   按数字猜会误报,这一点我上轮已经实测过);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。

关键细节:**计数写在 `check()` 内部**(theme/background 原本就在内部 ++;
narrow-layout 只有 failed 计数,补了 passed;markdown-xss 按 payload 条数算)。
写在调用点或靠扫源码的话,"实现被换空"就看不见了。

棘轮"只增不减":加判据**不用**改那个数,只有"条数掉了"才红。期望值按**实测**回填
(9/52/8/30/42/15/28/5/23/5/3/2)。

附带的可见性收益:这几轮我一直用"13→14""19→28""34→42"当信号,现在它成了判据 ——
某次改动顺手删掉两条判据、或某条被跳过,会立刻红。

## 变异

- pi 那个反例(`check` 换成空函数)→ 红(`自报 0 条 < 登记的 30 条`);
- 删掉 5 条 `check(` 调用 → 红(`自报 47 条 < 登记的 52 条`)。

## 规范

§6.5 新增"涉及运行时行为的结论必须实测过才能写进规范/判据"——同一个错这轮犯了两次
(我从"报告 0 个测试、退出 0"推断"退出码被吞",实测是照传;pi 拿我这个结论又建了一个洞)。
规则:**一次观察只支撑你看到的那一层**。
§6.6 记闭环形状与代价(故意删判据要同步改数字,属于一次可复核的显式编辑)。

⚠️ 并且如实记下一次**我自己违反规范**的事:写 §3 那条"变异后别用 `git checkout` 还原"的人
(就是我)在这次变异验证里又用了 `git checkout -- <文件>`,把刚加、尚未提交的 marker 抹掉了。
规矩写下来不等于会遵守 —— 已把这条实例写进规范,让人知道它是活人踩的坑。

## 验证

`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258);`run-all` 单独跑也 exit 0。
2026-09-14 15:06:14 +08:00

433 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 深色主题的结构性检查。
*
* 判据全部是「源码里存在/不存在某种形态」,不需要浏览器 ——
* 真正的视觉验收靠手工脚本test/manual/theme-verify.mjs
*
* 这些检查存在的理由:深色模式的 bug 形态是**白底白字**
* 它不报错、不影响构建、只有肉眼能发现,而且往往只出现在某个不常开的页面。
*/
import { readFileSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const read = p => readFileSync(join(here, p), 'utf8');
let pass = 0;
let fail = 0;
const check = (name, ok, detail = '') => {
if (ok) {
pass++;
console.log(` 通过 ${name}`);
} else {
fail++;
console.log(` 失败 ${name}${detail ? ' — ' + detail : ''}`);
}
};
const css = read('../src/index.css');
const cfg = read('../tailwind.config.js');
const html = read('../index.html');
/**
* 剥掉注释后的 CSS。
*
* 下面几处按 `indexOf(':root')` / `indexOf('.dark')` 切颜色块,而**注释里出现
* 选择器字面量会把块提前截断**:在 :root 的注释里写一句「深色主题里换值」
* (原文用了带点的选择器写法)就足以让浅色块在 color-scheme 之前被切开,
* 于是第 8 条真假失败。更危险的是反向情形:块被截短后变量集合变小,
* 「覆盖齐全」这类断言可能**真空通过**。
*
* 所以所有块切分都基于剥注释后的文本。字符串字面量里的 /* 在本文件里
* 不存在,直接用非贪婪匹配去掉块注释即可。
*/
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
// 1) tailwind 必须走 class 策略。
// media 策略下主题无法被人显式选择 —— 白天想开深色就做不到。
check('darkMode 为 class 策略', /darkMode:\s*['"]class['"]/.test(cfg));
// 2) 颜色必须经 CSS 变量。写死十六进制的话深色模式无从切换。
check(
'调色板指向 CSS 变量',
cfg.includes('rgb(var(') && cfg.includes('<alpha-value>'),
'缺少 rgb(var(--x) / <alpha-value>) 形态'
);
// 3) 变量值必须是 RGB 三元组而不是 #hex。
// 代码里有 bg-blue-50/70 这类透明度修饰符,#hex 会生成无效 CSS
// 那些半透明高亮静默失效(不报错,只是不透明)。
const varLines = css.match(/--c-[a-z]+-?\d*:\s*[^;]+;/g) || [];
const hexVars = varLines.filter(l => l.includes('#'));
check(
'色板变量存 RGB 三元组而非 #hex',
varLines.length >= 100 && hexVars.length === 0,
hexVars.length ? `${hexVars.length} 个变量是 hex${hexVars[0]}` : `只找到 ${varLines.length} 个变量`
);
// 4) 必须有 .dark 覆盖块,且覆盖了同样多的变量。
// 漏掉的那些会在深色下保持浅色值 —— 那正是白底白字的来源。
const lightBlock = cssNoComments.slice(
cssNoComments.indexOf(':root'),
cssNoComments.indexOf('.dark')
);
const darkBlock = cssNoComments.slice(cssNoComments.indexOf('.dark {'));
const lightVars = new Set((lightBlock.match(/--c-[\w-]+(?=:)/g) || []));
const darkVars = new Set((darkBlock.match(/--c-[\w-]+(?=:)/g) || []));
const missing = [...lightVars].filter(v => !darkVars.has(v));
check(
'.dark 覆盖了全部色板变量',
lightVars.size >= 15 && missing.length === 0,
missing.length ? `深色缺 ${missing.length} 个:${missing.slice(0, 5).join(', ')}` : `浅色只有 ${lightVars.size}`
);
// 5) 灰阶必须真的反转:深色的 white 要比 gray-900 暗。
// 不反转的话组件里的 `bg-white text-gray-900` 在深色下依然是白底黑字。
const lum = (block, name) => {
const m = block.match(new RegExp(`--c-${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)`));
if (!m) return null;
return (Number(m[1]) + Number(m[2]) + Number(m[3])) / 3;
};
const darkWhite = lum(darkBlock, 'white');
const darkG900 = lum(darkBlock, 'gray-900');
check(
'深色下灰阶已反转white 比 gray-900 暗)',
darkWhite !== null && darkG900 !== null && darkWhite < darkG900,
`white=${darkWhite} gray-900=${darkG900}`
);
// 6) 深色的页面底gray-50必须比卡片white更暗。
// 浅色下页面底比卡片浅,深色下要反过来 —— 否则卡片陷进背景失去边界。
const darkG50 = lum(darkBlock, 'gray-50');
check(
'深色下页面底比卡片更暗',
darkG50 !== null && darkWhite !== null && darkG50 < darkWhite,
`gray-50=${darkG50} white=${darkWhite}`
);
// 7) 次要文字gray-400/500在深色下必须提亮。
// 照搬浅色值只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 ——
// 实际效果是「看得见但读不动」。
const lightG400 = lum(lightBlock, 'gray-400');
const darkG400 = lum(darkBlock, 'gray-400');
check(
'深色下次要文字未沿用浅色值',
darkG400 !== null && lightG400 !== null && Math.abs(darkG400 - lightG400) > 5,
`light=${lightG400} dark=${darkG400}`
);
// 8) color-scheme 两处都要设。
// 不设的话深色页面上会出现浅色滚动条与白底的 autofill 输入框。
check(
':root 与 .dark 都声明 color-scheme',
/color-scheme:\s*light/.test(lightBlock) && /color-scheme:\s*dark/.test(darkBlock)
);
// 9) index.html 必须有同步内联脚本消除首帧闪屏。
// bundle 有几百 KB从 HTML 解析完到 React 挂载之间页面是 body 默认色 ——
// 深色用户每次刷新都被闪一下白屏。外链或 defer 都晚于首次绘制。
check(
'index.html 内联防闪屏脚本',
html.includes('agentmail.theme') &&
html.includes('prefers-color-scheme') &&
html.includes("classList.add('dark')") &&
!/<script[^>]+(src=|defer)[^>]*>[\s\S]*?agentmail\.theme/.test(html),
'缺少同步内联脚本'
);
// 10) 内联脚本与 themeStore 必须用同一个 localStorage 键。
// 不一致的后果是首帧按 A 键渲染、React 挂载后按 B 键重渲染 —— 闪一下再变回去
const store = read('../src/stores/themeStore.ts');
const keyInStore = store.match(/STORAGE_KEY\s*=\s*'([^']+)'/);
check(
'内联脚本与 themeStore 共用同一 storage 键',
keyInStore !== null && html.includes(`'${keyInStore[1]}'`),
keyInStore ? `store 用 ${keyInStore[1]}` : '未找到 STORAGE_KEY'
);
// 11) body 必须有显式底色:移动端橡皮筋回弹露出的是 body 背景,
// 不设的话深色下滑到边界会闪出白边。
check(
'body 有显式主题底色',
/body\s*\{[^}]*background-color:\s*rgb\(var\(--c-/.test(css)
);
// 12) 组件里不该残留写死的十六进制颜色。
// 它们不经变量,深色模式下不会变 —— 而这类遗漏只有肉眼能发现。
const compDir = join(here, '../src/components');
const offenders = [];
for (const f of readdirSync(compDir).filter(x => x.endsWith('.tsx'))) {
const src = readFileSync(join(compDir, f), 'utf8');
// 只看 className 与 style 里的颜色SVG 的 currentColor 不算
const hits = (src.match(/#[0-9a-fA-F]{3,6}\b/g) || []);
if (hits.length) offenders.push(`${f}(${hits.join(',')})`);
}
check(
'组件里没有写死的十六进制颜色',
offenders.length === 0,
offenders.join(' ')
);
// 13) 主题三态system 不能被当成 light 的别名。
// 只给开关的话,白天设浅色之后晚上系统切深色应用不会跟着变
check(
'ThemePref 是三态且含 system',
/'light'\s*\|\s*'dark'\s*\|\s*'system'/.test(store) && store.includes("=== 'system'")
);
// 14) 只有 pref 为 system 时才跟随系统变化。
// 显式选了 light/dark 的人不该因为日落被切换主题
check(
'仅 system 偏好跟随系统变化',
/if\s*\(pref === 'system'\)/.test(store)
);
// 15) text-white 必须与 bg-white 用不同的变量。
// `white` 服务两种冲突用途:卡片表面(深色下变暗)与彩色按钮上的文字
// (深色下必须保持浅色)。共用一个变量时后者跟着变暗 —— 实测激活导航项的
// 「收件」在 bg-chrome-700 上只剩 1.34:1几乎看不见。
check(
'textColor.white 指向独立变量(不跟 bg-white 一起反转)',
/textColor:\s*\{[^}]*white:\s*withAlpha\('--c-on-accent'\)/.test(cfg) &&
/--c-on-accent:/.test(lightBlock) &&
/--c-on-accent:/.test(darkBlock)
);
// 16) --c-on-accent 在深色下必须仍然是浅色。
// 它变暗就是上一条描述的那个 bug。
const onAccentDark = lum(darkBlock, 'on-accent');
check(
'深色下 on-accent 仍是浅色',
onAccentDark !== null && onAccentDark > 200,
`on-accent 亮度 ${onAccentDark}`
);
// 17) 每个被 Tailwind 实际使用的 CSS 变量都必须在 index.css 有定义。
//
// 这一条守的是本项目最贵的一次视觉 bugtailwind.config.js 里 `colors`
// 同时写了固定 hex 与 accent() 两份 red/green/amber/orange/yellow ——
// JS 对象字面量重复键**后者胜出**(不报错、不警告),而 index.css 里当时
// 没有对应的变量。`rgb(var(--c-red-600) / 1)` 里变量未定义会让整条
// background-color 声明失效,于是 bg-red-600 退回透明、text-white 的白字
// 落在白卡片上:**按钮看不见但点得动**。32 个变量全部缺失,
// 波及所有 red/green/amber/orange/yellow 的地方。
//
// 判据必须走 resolveConfig 而不是正则扫配置文本:真正出问题的那批变量名
// 是 accent('red') 这类**模板拼出来的**,配置源码里根本没有 `--c-red-600`
// 这个字面量 —— 扫文本会漏掉正是要防的那一类。
const resolveConfig = (await import('tailwindcss/resolveConfig.js')).default;
const resolved = resolveConfig((await import('../tailwind.config.js')).default);
const declared = new Set([...css.matchAll(/--[cs]-[a-z0-9-]+(?=\s*:)/g)].map(m => m[0]));
const usedVars = new Set();
const walk = (v) => {
if (typeof v === 'string') {
for (const m of v.matchAll(/var\((--[a-z0-9-]+)/g)) usedVars.add(m[1]);
} else if (v && typeof v === 'object') {
for (const k of Object.keys(v)) walk(v[k]);
}
};
// 只看颜色相关的 theme 段其余spacing/fontSize/...)不走变量
for (const key of ['colors', 'textColor', 'backgroundColor', 'borderColor', 'ringColor', 'divideColor']) {
walk(resolved.theme[key]);
}
const undef = [...usedVars].filter(v => !declared.has(v));
check(
'Tailwind 实际使用的每个变量都在 index.css 有定义',
usedVars.size >= 100 && undef.length === 0,
undef.length
? `${undef.length} 个未定义:${undef.slice(0, 6).join(', ')}`
: `只解析出 ${usedVars.size} 个变量引用`
);
// 18) colors 里每个颜色名只能定义一次。
// 重复键静默生效,是上一条那个 bug 的**成因**:读代码的人看到固定 hex
// 以为在用它,实际生效的是下面那份 accent()。
const colorsBlock = cfg.slice(cfg.indexOf('colors: {'));
const dupNames = [];
for (const name of ['blue', 'red', 'green', 'amber', 'orange', 'yellow', 'gray', 'chrome']) {
const n = (colorsBlock.match(new RegExp(`^\\s{8}${name}:`, 'gm')) || []).length;
if (n > 1) dupNames.push(`${name}×${n}`);
}
check('colors 里没有重复的颜色名', dupNames.length === 0, dupNames.join(' '));
// 19) 强调色的**表面段**50300深色下必须变暗。
// 照搬浅色值的话 red-50 (#fef2f2) 在深色页面上是一块近白亮斑 ——
// 那是错误提示条的底,结果比正文还抢眼,上面的红字反而读不动。
const surfaceOffenders = [];
for (const name of ['blue', 'red', 'green', 'amber', 'orange', 'yellow']) {
for (const shade of [50, 100, 200, 300]) {
const l = lum(lightBlock, `${name}-${shade}`);
const d = lum(darkBlock, `${name}-${shade}`);
if (l === null || d === null) { surfaceOffenders.push(`${name}-${shade}:缺失`); continue; }
if (d >= l) surfaceOffenders.push(`${name}-${shade}(${d}${l})`);
}
}
check(
'强调色表面段在深色下变暗',
surfaceOffenders.length === 0,
surfaceOffenders.slice(0, 6).join(' ')
);
// 20) 强调色的**前景段**400900在深色卡片上必须达到 WCAG AA 4.5:1。
// 照搬浅色值时 red-700 只有 2.67:1、amber-900 只有 1.90:1 ——
// 「看得见但读不动」跟看不见是同一类 bug。
const rgbOf = (block, name) => {
const m = block.match(new RegExp(`--c-${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)`));
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
};
const srgb = c => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
const relLum = ([r, g, b]) => 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
const contrast = (a, b) => {
const l1 = relLum(a), l2 = relLum(b);
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
return (hi + 0.05) / (lo + 0.05);
};
const darkCard = rgbOf(darkBlock, 'white');
const lowContrast = [];
for (const name of ['blue', 'red', 'green', 'amber', 'orange', 'yellow']) {
for (const shade of [400, 500, 600, 700, 800, 900]) {
const fg = rgbOf(darkBlock, `${name}-${shade}`);
if (!fg) { lowContrast.push(`${name}-${shade}:缺失`); continue; }
const r = contrast(fg, darkCard);
if (r < 4.5) lowContrast.push(`${name}-${shade}:${r.toFixed(2)}`);
}
}
check(
'强调色前景段在深色卡片上达到 4.5:1',
darkCard !== null && lowContrast.length === 0,
lowContrast.slice(0, 6).join(' ')
);
// 21) 实心按钮底走独立的 --s-* 且**两种模式同值**。
//
// accent 的 400900 在深色下被提亮(为了 text-red-600 读得动),
// 而 `bg-red-600 text-white` 的白字落在那个浅红上只有 1.6:1。
// 一个名字服务两种语义必然坏掉一头 —— 与 text-white/bg-white 那次同理。
check(
'实心按钮底走 --s-* 并只覆盖 backgroundColor',
/const solid = \(name\) =>/.test(cfg) &&
/backgroundColor:\s*\{[^}]*red:\s*solid\('red'\)/s.test(cfg) &&
/--s-red-600:/.test(css)
);
// 22) --s-* 不能出现在 .dark 里:它必须两种模式同值。
// 在 .dark 覆盖等于把「危险操作是红的」这件事也一起反转了。
check(
'--s-* 未被 .dark 覆盖(实心底不随主题变)',
!/--s-[a-z]+-\d+:/.test(darkBlock)
);
// 23) 白字落在实心按钮底上必须达到 4.5:1。
//
// **两种模式都要算**。之前只拿浅色的 on-accent纯白 255去算而深色用的是
// 「近白」244 246 250为了不刺眼—— 同一个实心底,白字换暗一点点就从
// 4.83 掉到 4.46,路过了 AA 线。盲区在真实渲染里才被量出来(导航未读徒标
// 「12」
const solidBlock = cssNoComments.slice(
cssNoComments.indexOf(':root'),
cssNoComments.indexOf('.dark')
);
const sRgb = name => {
const m = solidBlock.match(new RegExp(`--s-${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)`));
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
};
// 代码里真正出现过的「实心底 + 白字」组合
const solidPairs = [
['blue-600'], ['blue-700'],
['red-600'], ['red-700'],
['green-700'], ['orange-700']
];
const onAccentValues = [
['浅色', rgbOf(lightBlock, 'on-accent')],
['深色', rgbOf(darkBlock, 'on-accent')]
];
const weakButtons = [];
for (const [mode, onAccent] of onAccentValues) {
if (!onAccent) { weakButtons.push(`${mode}:on-accent 缺失`); continue; }
for (const [name] of solidPairs) {
const bg = sRgb(name);
if (!bg) { weakButtons.push(`${name}:缺失`); continue; }
// 这些按钮文字只有 914px按普通文字执行 WCAG AA 4.5:1。
const r = contrast(onAccent, bg);
if (r < 4.5) weakButtons.push(`${mode}/${name}:${r.toFixed(2)}`);
}
}
check(
'白字在实心按钮底上达到 4.5:1两种模式',
weakButtons.length === 0,
weakButtons.join(' ')
);
// 24) 应用框架(侧栏 / 底部导航)必须有独立色阶。
// 它在浅色模式下本来就是深色的 —— 并入反转的 gray 之后深色模式下会变成
// 近白色,比内容区还亮,整个层次翻过来(实测 rgb(243,245,248))。
check(
'框架有独立的 chrome 色阶',
/chrome:\s*\{/.test(cfg) &&
/--c-chrome-900:/.test(lightBlock) &&
/--c-chrome-900:/.test(darkBlock)
);
// 25) 深色下框架必须比内容区更沉(保持浅色下就有的层次关系)。
const chromeDark = lum(darkBlock, 'chrome-900');
check(
'深色下框架比内容区更暗',
chromeDark !== null && darkG50 !== null && chromeDark < darkG50,
`chrome-900=${chromeDark} gray-50=${darkG50}`
);
// 26) 侧栏与底部导航里不该残留 slate-*(那条色阶指向反转的 gray
const chromeFiles = ['Sidebar', 'NarrowNav'];
const slateLeft = [];
for (const f of chromeFiles) {
const src = readFileSync(join(here, `../src/components/${f}.tsx`), 'utf8');
const hits = src.match(/(?:bg|text|border|hover:bg|hover:text|active:bg|ring)-slate-\d+/g) || [];
if (hits.length) slateLeft.push(`${f}: ${hits.join(',')}`);
}
check('框架组件已全部改用 chrome 色阶', slateLeft.length === 0, slateLeft.join(' | '));
// 27) 可逆灰阶不能作为白字实心按钮底。gray-700/800/900 在深色模式下会
// 被反转成近白色,`bg-gray-900 text-white` 因而只剩约 1:1。
const componentSources = readdirSync(compDir)
.filter(x => x.endsWith('.tsx'))
.map(f => [f, readFileSync(join(compDir, f), 'utf8')]);
const graySolid = componentSources.flatMap(([f, src]) =>
(src.match(/(?:bg-gray-(?:700|800|900)[^'"\n]*text-white|text-white[^'"\n]*bg-gray-(?:700|800|900))/g) || [])
.map(hit => `${f}:${hit}`)
);
check('白字实心控件不使用可逆 gray 底色', graySolid.length === 0, graySolid.slice(0, 4).join(' | '));
// 28) 未映射色族会绕过 CSS 变量主题,浅色值会原样落进深色页面。
const unmapped = componentSources.flatMap(([f, src]) =>
(src.match(/(?:bg|text|border)-(?:emerald|purple)-\d+/g) || []).map(hit => `${f}:${hit}`)
);
check('组件未使用未映射的 emerald/purple 色族', unmapped.length === 0, unmapped.slice(0, 6).join(' | '));
// 29) 本地 markdown 层替代未安装 typography 插件时无效的 prose 类。
const inertProse = componentSources.flatMap(([f, src]) =>
(src.match(/\bprose(?:-sm)?\b/g) || []).map(hit => `${f}:${hit}`)
);
check('Markdown 内容使用本地 .markdown 排版层', inertProse.length === 0, inertProse.slice(0, 4).join(' | '));
// 30) gray-400 承担 912px 元数据与 placeholder白卡片上也必须达到 4.5:1。
const lightCard = rgbOf(lightBlock, 'white');
const lightSecondary = rgbOf(lightBlock, 'gray-400');
const secondaryContrast = lightCard && lightSecondary ? contrast(lightCard, lightSecondary) : 0;
check(
'浅色 gray-400 在白卡片上达到 4.5:1',
secondaryContrast >= 4.5,
secondaryContrast.toFixed(2)
);
console.log(`\n主题:${pass} 通过${fail ? `${fail} 失败` : ''}`);
/*
* 机器可读的汇总(契约):`run-all.mjs` 只认这一行来判"这条判据到底跑了几条"。
* 为什么需要它:光看"文件里存在 check("只能证明**有能红的路径**,不能证明**它跑过** ——
* 把 `check` 的实现换成空函数(合并冲突改坏实现的现实事故)时,文本证据照样成立。
* 计数在 check() **内部**自增,所以"实现被换空"会直接体现为 pass=0。
*/
console.log(`RESULT pass=${pass} fail=${fail}`);
process.exit(fail ? 1 : 0);