Files
jiapu/tests/navigation-ux-regressions.test.js
T
2026-08-29 19:06:07 +08:00

252 lines
11 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
function read(relativePath) {
return fs.readFileSync(path.resolve(__dirname, '..', relativePath), 'utf8');
}
test('内容发布页在已选择家谱时不再显示“请先选择家谱”的静态误导提示', () => {
const source = read('profile-content.html');
assert.doesNotMatch(source, /请先选择家谱,再进入对应模块/);
});
test('会员入口直接定位到会员服务区,不改变通用服务中心语义', () => {
const familyHome = read('profile-family-home.html');
const profile = read('profile.html');
const services = read('profile-services.html');
assert.match(familyHome, /<h3>会员与订单<\/h3>/);
assert.match(familyHome, /href="profile-services\.html#membership"[^>]*data-genealogy-context-link/);
assert.match(profile, /href="profile-services\.html#membership"><strong>会员服务<\/strong>/);
assert.match(services, /<title>帮助与服务 - 个人中心 - 代代相传<\/title>/);
assert.match(services, /<h1>帮助与服务<\/h1>/);
assert.match(services, /<section class="module-panel" id="membership" tabindex="-1">\s*<h2>会员服务<\/h2>/);
});
test('家谱工作台不混用英文模块眉题', () => {
assert.doesNotMatch(read('profile-tree.html'), />Tree Mode</);
assert.doesNotMatch(read('profile-family-admin.html'), />Family Admin</);
});
test('世系维护先选择成员再展示关系操作,且字辈入口名称与目标一致', () => {
const source = read('profile-tree.html');
const script = read('public/js/lineage-pages.js');
const selectorIndex = source.indexOf('data-lineage-person-options');
const actionIndex = source.indexOf('data-lineage-relation="parents"');
assert.ok(selectorIndex > 0 && selectorIndex < actionIndex, '成员选择器应位于关系操作之前');
assert.match(source, /href="profile-generation\.html"[^>]*data-genealogy-context-link[^>]*>\s*管理字辈\s*<\/a/);
assert.doesNotMatch(source, />\s*管理世代\s*<\/a/);
assert.match(script, /queryAll\('\[data-lineage-relation\]'\)[\s\S]*?control\.disabled = writeDisabled \|\| !selectedPersonId/);
});
test('个人工作台使用紧凑首屏和高对比度导航', () => {
const publicCss = read('public/css/public.css');
const moduleCss = read('public/css/profile-module.css');
assert.match(publicCss, /\.nav-links\s*\{[^}]*color:\s*#fffdf8;/);
assert.match(publicCss, /\.nav-actions\s*\{[^}]*color:\s*#fffdf8;/);
assert.match(publicCss, /\.nav-links a,\s*\.nav-actions a\s*\{[^}]*color:\s*inherit;/);
assert.match(moduleCss, /\.page-profile-module \.nav\s*\{\s*height:\s*72px;/);
assert.match(moduleCss, /\.page-profile-module \.section\s*\{\s*padding:\s*var\(--space-7\) 0;/);
assert.match(moduleCss, /\.module-hero\s*\{[^}]*height:\s*168px;[^}]*min-height:\s*168px;/);
});
test('手机菜单按 Escape 关闭后把焦点还给菜单按钮', () => {
const source = read('public/js/profile-common.js');
assert.match(source, /event\.key === 'Escape'[\s\S]*?setMobileMenuOpen\(false\);[\s\S]*?\$\('\[data-profile-mobile-menu-toggle\]'\)\.first\(\)\.trigger\('focus'\)/);
});
test('公共页面页脚的栏目链接都有真实跳转地址', () => {
[
'index.html',
'create-genealogy.html',
'about.html',
'app.html',
'help.html',
'genealogy.html',
'plaza.html',
'surname.html',
'culture.html',
'promo-album.html',
'promo-video.html'
].forEach((page) => {
const source = read(page);
const footerMatch = source.match(/<footer[\s\S]*?<\/footer>/);
assert.ok(footerMatch, `${page} 缺少页脚`);
assert.doesNotMatch(
footerMatch[0],
/<a(?:\s+class="[^"]*")?>\s*(?:数字家谱|家谱广场|应用下载|帮助中心)\s*<\/a>/,
`${page} 页脚存在无 href 的伪链接`
);
});
});
test('公共导航提供统一的登录态切换脚本', () => {
const script = read('public/js/page-effects.js');
assert.match(script, /genealogy_auth_token/);
assert.match(script, /profile\.html/);
assert.match(script, /个人中心/);
assert.match(script, /data-site-menu-toggle/);
assert.match(script, /data-site-mobile-menu/);
});
test('用户页面不暴露占位链接、假联系方式和开发阶段说明', () => {
const htmlFiles = fs.readdirSync(path.resolve(__dirname, '..'))
.filter((file) => file.endsWith('.html'));
htmlFiles.forEach((file) => {
const source = read(file);
const visibleSource = source.replace(/<!--[\s\S]*?-->/g, '');
assert.doesNotMatch(source, /https?:\/\/example\.com/i, `${file} 仍包含占位外链`);
assert.doesNotMatch(source, /400-000-0000|service@example\.com/i, `${file} 仍包含假联系方式`);
assert.doesNotMatch(source, /实际项目可替换|后续可以接|前台先展示/, `${file} 仍包含开发阶段说明`);
assert.doesNotMatch(source, /承接 APP|整合 APP|对应 PC|当前 PC|PC 的“|真实接口|接口为准/, `${file} 仍包含实现口吻`);
assert.doesNotMatch(visibleSource, /YAML|OpenAPI|DTO|OSS ID|PC 反馈接口|PC 契约|资料接口/, `${file} 仍向用户暴露开发术语`);
});
});
test('页面眉题统一使用中文产品语言', () => {
const htmlFiles = fs.readdirSync(path.resolve(__dirname, '..'))
.filter((file) => file.endsWith('.html'));
htmlFiles.forEach((file) => {
const source = read(file);
const kickers = [...source.matchAll(/<div class="[^"]*(?:eyebrow|kicker)[^"]*">\s*([^<]+?)\s*<\/div>/g)]
.map((match) => match[1].trim());
kickers.forEach((kicker) => {
assert.doesNotMatch(kicker, /^[A-Za-z][A-Za-z ]*$/, `${file} 的眉题仍是纯英文:${kicker}`);
});
});
});
test('所有静态站内链接都指向存在的页面和锚点', () => {
const projectRoot = path.resolve(__dirname, '..');
const htmlFiles = fs.readdirSync(projectRoot).filter((file) => file.endsWith('.html'));
htmlFiles.forEach((file) => {
const source = read(file);
const hrefs = [...source.matchAll(/\shref="([^"]+)"/g)].map((match) => match[1]);
hrefs.forEach((href) => {
if (/^(?:https?:|mailto:|tel:|javascript:)/i.test(href)) return;
assert.notEqual(href, '#', `${file} 存在不能导航的空锚点`);
const targetUrl = new URL(href, `http://jiapu.local/${file}`);
const targetName = decodeURIComponent(targetUrl.pathname.replace(/^\//, '')) || 'index.html';
const targetPath = path.resolve(projectRoot, targetName);
assert.ok(targetPath.startsWith(projectRoot + path.sep), `${file} 链接越出项目目录:${href}`);
assert.ok(fs.existsSync(targetPath), `${file} 链接到不存在的页面:${href}`);
if (targetUrl.hash) {
const targetSource = fs.readFileSync(targetPath, 'utf8');
const anchor = decodeURIComponent(targetUrl.hash.slice(1));
assert.ok(
targetSource.includes(`id="${anchor}"`) || targetSource.includes(`name="${anchor}"`),
`${file} 链接到不存在的锚点:${href}`
);
}
});
});
});
test('个人中心双列表单从顶部对齐,带辅助按钮的字段不会拉高相邻输入框', () => {
const source = read('public/css/profile-module.css');
assert.match(source, /\.editor-two\s*\{[^}]*align-items:\s*start;/);
});
test('安全设置只在页头保留退出登录,注销区只处理注销流程', () => {
const source = read('profile-security.html');
const logoutControls = source.match(/data-security-logout/g) || [];
assert.equal(logoutControls.length, 1);
assert.match(source, /class="module-panel security-danger"[\s\S]*?class="btn danger[^\"]*"[^>]*data-security-submit/);
});
test('推广收益使用项目统一上传控件样式', () => {
const source = read('profile-earnings.html');
assert.match(source, /id="payoutQrFile"[^>]*class="upload-input"/);
});
test('全量浏览器审计会把缺失 Mock 当成失败,而不是截图后继续通过', () => {
const source = read('tests/browser-click-smoke.mjs');
assert.match(source, /mockTransportErrors:\s*bodyText\.split\(String\.fromCharCode\(10\)\)[\s\S]*?includes\('browser mock missing:'\)/);
assert.match(source, /type:\s*'browser-mock-missing'/);
});
test('浏览器测试数据不再把广告图当作家谱内容图', () => {
const source = read('tests/browser-click-smoke.mjs');
assert.doesNotMatch(source, /localImage\('ad-/);
});
test('个人中心列表按钮去除浏览器默认描边,删除操作使用统一危险层级', () => {
const moduleCss = read('public/css/profile-module.css');
const detailScripts = [
'public/js/article-pages.js',
'public/js/album-pages.js',
'public/js/ceremony-admin-pages.js',
'public/js/growth-pages.js',
'public/js/merit-pages.js',
'public/js/memo-pages.js',
'public/js/relative-pages.js',
'public/js/video-pages.js'
];
assert.match(moduleCss, /\.row-actions button\.pill\s*\{[^}]*border:\s*0;[^}]*font:\s*inherit;[^}]*cursor:\s*pointer;/);
detailScripts.forEach((file) => {
assert.doesNotMatch(read(file), /class="btn ghost"[^>]*data-[a-z-]*delete/);
});
assert.doesNotMatch(read('public/js/feed-pages.js'), /class="pill"[^>]*data-feed-(?:comment-)?delete/);
});
test('手机端列表操作提供可靠触控尺寸', () => {
const source = read('public/css/profile-module.css');
assert.match(source, /@media \(max-width: 720px\)[\s\S]*?\.row-actions \.pill\s*\{[^}]*min-height:\s*44px;/);
});
test('同页记录详情会保留选中编号并把焦点移到详情标题', () => {
const flows = [
['profile-growth.html', 'public/js/growth-pages.js', 'data-growth-detail-heading', 'buildListUrl'],
['profile-memo.html', 'public/js/memo-pages.js', 'data-memo-detail-heading', 'buildListUrl'],
['profile-merit.html', 'public/js/merit-pages.js', 'data-merit-detail-heading', 'buildListUrl'],
['profile-relative.html', 'public/js/relative-pages.js', 'data-relative-detail-heading', 'buildListUrl']
];
assert.match(read('public/js/profile-common.js'), /function revealDetail\([^)]+\)[\s\S]*?scrollIntoView[\s\S]*?focus/);
flows.forEach(([page, script, heading, listUrlBuilder]) => {
assert.match(read(page), new RegExp(`tabindex="-1" ${heading}`));
assert.match(read(script), new RegExp(`history\\.replaceState\\(null, '', ${listUrlBuilder}`));
assert.match(read(script), new RegExp(`ProfileUI\\.revealDetail\\(query\\('\\[${heading}\\]'\\)\\)`));
});
});
test('动态编辑页按新建和编辑状态使用一致文案', () => {
const page = read('profile-feed-edit.html');
const script = read('public/js/feed-pages.js');
assert.match(page, /data-feed-editor-kicker>发布动态</);
assert.match(page, /data-feed-editor-title>发布动态</);
assert.match(script, /var editorTitle = isEditing \? '编辑动态' : '发布动态';/);
assert.match(script, /button\.textContent = isEditing \? '保存修改' : '发布动态';/);
});
test('贺礼邀请使用明确的关联活动入口', () => {
const source = read('profile-gift.html');
assert.match(source, /<h2>关联活动<\/h2>/);
assert.match(source, />管理关联活动<\/a>/);
});