修改功能
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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>/);
|
||||
});
|
||||
Reference in New Issue
Block a user