修改功,现在补齐页面

This commit is contained in:
2026-07-11 21:36:03 +08:00
parent db397a7917
commit a6038be376
143 changed files with 7218 additions and 11912 deletions
-53
View File
@@ -1,53 +0,0 @@
const assert = require('assert');
const AlbumPages = require('../public/js/album-pages.js');
function run() {
assert.deepStrictEqual(AlbumPages.normalizeList({ rows: [{ albumId: 1 }] }), [{ albumId: 1 }]);
assert.deepStrictEqual(AlbumPages.normalizeList({ records: [{ albumId: 2 }] }), [{ albumId: 2 }]);
assert.deepStrictEqual(AlbumPages.buildAlbumBody({
albumName: ' 老照片 ',
albumDesc: ' 家族旧影 ',
coverOssId: '',
sortOrder: '',
status: ''
}), {
albumName: '老照片',
albumDesc: '家族旧影',
coverOssId: undefined,
sortOrder: 1,
status: '0'
});
assert.deepStrictEqual(AlbumPages.buildPhotoBody({
ossId: ' 2060001 ',
photoTitle: ' 老宅 ',
photoDesc: ' 合影 ',
photographer: ' 汤明 ',
shootTime: '2026-07-09',
sortOrder: '',
status: ''
}), {
ossId: 2060001,
photoTitle: '老宅',
photoDesc: '合影',
photographer: '汤明',
shootTime: '2026-07-09',
sortOrder: 1,
status: '0'
});
assert.strictEqual(
AlbumPages.buildAlbumRow({
albumId: 301,
albumName: '老照片',
photoCount: 6,
albumDesc: '家族旧影'
}, '900001001'),
'<button class="module-row album-row" type="button" data-album-id="301"><div><h3>老照片</h3><p>6 张照片 · 家族旧影</p></div><span class="pill">管理</span></button>'
);
console.log('album-pages tests passed');
}
run();
+122
View File
@@ -0,0 +1,122 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const GenealogyApi = require('../utils/ApiClient.js');
function createClient() {
return GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return null; },
setItem() {},
removeItem() {}
},
axiosInstance: {
request() {
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
}
test('API client exposes latest document-defined PC operations', () => {
const client = createClient();
const allowed = [
'clientId', 'tenantId', 'tokenKey', 'getToken', 'setToken', 'clearToken', 'buildApiUrl',
'register', 'login', 'loginBySms', 'sendSmsCode',
'currentProfile', 'updateProfile', 'changePassword', 'resetPassword', 'changePhone',
'deactivateAccount', 'logout', 'uploadFile',
'captchaRequirement', 'captchaChallenge', 'captchaVerify',
'regionChildren', 'regionPath', 'regionSearch', 'regionDetail',
'feeds', 'feedsPage', 'feedDetail', 'createFeed', 'updateFeed', 'deleteFeed',
'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
'deleteFeedComment'
].sort();
assert.deepEqual(Object.keys(client).sort(), allowed);
});
test('password login stores only the returned token', async () => {
let stored = '';
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return stored; },
setItem(_, value) { stored = value; },
removeItem() { stored = ''; }
},
axiosInstance: {
request(config) {
assert.equal(config.method, 'post');
assert.equal(config.url, '/genealogy/pc/auth/login');
assert.equal(config.headers.Authorization, undefined);
return Promise.resolve({ data: { code: 200, data: { accessToken: 'access-token' } } });
}
}
});
await client.login({ phone: '13800000000', password: 'hash' });
assert.equal(client.getToken(), 'access-token');
});
test('sending an SMS code uses the latest documented request fields', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.url, '/genealogy/pc/auth/sms/code');
assert.deepEqual(config.data, {
grantType: 'sms',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
sceneCode: 'WEB_H5_LOGIN',
validToken: 'captcha-ticket'
});
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.sendSmsCode({
phone: '13800000000',
sceneCode: 'WEB_H5_LOGIN',
validToken: 'captcha-ticket'
});
});
test('family feed methods use the documented paths and request bodies', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
await client.createFeed(900001001, {
feedType: 'text',
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
status: '0'
});
await client.feedCommentsPage(900001001, 7001, { pageNum: 1, pageSize: 10 });
await client.deleteFeedComment(900001001, 7001, 8001);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
['post', '/genealogy/pc/genealogies/900001001/feeds', undefined, {
feedType: 'text',
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
status: '0'
}],
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/page', { pageNum: 1, pageSize: 10 }, undefined],
['delete', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/8001', undefined, undefined]
]);
});
-245
View File
@@ -1,245 +0,0 @@
const assert = require('assert');
const GenealogyApi = require('../utils/ApiClient.js');
function createStore(initial) {
const values = Object.assign({}, initial);
return {
getItem(key) {
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null;
},
setItem(key, value) {
values[key] = String(value);
},
removeItem(key) {
delete values[key];
},
values
};
}
async function run() {
const calls = [];
const store = createStore({
'token-key': 'abc-token'
});
// 接口基础地址只能由根目录 config.js 负责,公共客户端不能重复声明。
assert.strictEqual(GenealogyApi.DEFAULT_BASE_URL, undefined);
const client = GenealogyApi.createClient({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'client-x',
tenantId: 'tenant-x',
tokenKey: 'token-key',
tokenHeaderName: 'Authorization',
tokenStore: store,
axiosInstance: {
request: async (config) => {
calls.push(config);
return {
status: 200,
data: {
code: 200,
msg: '操作成功',
data: { token: 'server-token', url: config.url }
}
};
}
}
});
await client.login({ phone: '13800000000', password: 'md5' });
assert.strictEqual(calls[0].url, '/genealogy/pc/auth/login');
assert.strictEqual(calls[0].headers.clientid, 'client-x');
assert.strictEqual(calls[0].headers.Authorization, undefined);
assert.strictEqual(calls[0].data.tenantId, 'tenant-x');
assert.strictEqual(store.values['token-key'], 'server-token');
await client.getProfile();
assert.strictEqual(calls[1].url, '/genealogy/pc/auth/profile');
assert.strictEqual(calls[1].headers.Authorization, 'Bearer server-token');
await client.updateProfile({
nickName: '张三',
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
});
assert.strictEqual(calls[2].url, '/genealogy/pc/auth/profile');
assert.strictEqual(calls[2].method, 'put');
await client.uploadFile('file-content');
assert.strictEqual(calls[3].url, '/genealogy/pc/files/upload');
assert.strictEqual(calls[3].headers['Content-Type'], undefined);
await client.initResumableUpload({
fileName: 'cover.jpg',
fileSize: 9532,
fileMd5: 'md5',
chunkSize: 4194304,
totalChunks: 1
});
assert.strictEqual(calls[4].url, '/genealogy/pc/files/resumable/init');
await client.getRegionChildren('51');
assert.strictEqual(calls[5].url, '/genealogy/region/children');
assert.deepStrictEqual(calls[5].params, { parentCode: '51' });
await client.captchaRequire({
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000'
});
assert.strictEqual(
calls[6].url,
'/captcha/require'
);
assert.deepStrictEqual(calls[6].params, {
tenantId: 'tenant-x',
clientId: 'client-x',
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000'
});
assert.strictEqual(calls[6].headers.Authorization, undefined);
await client.loginBySms({
phone: '13800000000',
smsCode: '6666',
validToken: 'sms-ticket'
});
assert.strictEqual(calls[7].url, '/genealogy/pc/auth/login/sms');
assert.strictEqual(calls[7].headers.Authorization, undefined);
assert.strictEqual(calls[7].data.grantType, 'sms');
assert.strictEqual(calls[7].data.tenantId, 'tenant-x');
assert.strictEqual(calls[7].data.clientId, 'client-x');
assert.strictEqual(store.values['token-key'], 'server-token');
store.setItem('token-key', 'existing-token');
const registerResponse = await client.register({
phone: '13900000000',
password: 'register-md5',
nickName: 'new-user',
validToken: 'register-ticket'
});
assert.deepStrictEqual(registerResponse, {
token: 'server-token',
url: '/genealogy/pc/auth/register'
});
assert.strictEqual(calls[8].url, '/genealogy/pc/auth/register');
assert.strictEqual(calls[8].headers.Authorization, undefined);
assert.strictEqual(calls[8].data.grantType, 'password');
assert.strictEqual(calls[8].data.registerSource, 'PC');
assert.strictEqual(store.getItem('token-key'), null);
const failedRegisterStore = createStore({
'token-key': 'existing-token'
});
const failedRegisterClient = GenealogyApi.createClient({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'client-x',
tenantId: 'tenant-x',
tokenKey: 'token-key',
tokenStore: failedRegisterStore,
axiosInstance: {
request: async () => {
throw new Error('register failed');
}
}
});
await assert.rejects(
failedRegisterClient.register({
phone: '13900000001',
password: 'register-md5',
nickName: 'failed-user',
validToken: 'register-ticket'
}),
/register failed/
);
assert.strictEqual(failedRegisterStore.getItem('token-key'), 'existing-token');
const missingTokenStore = createStore();
const missingTokenClient = GenealogyApi.createClient({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'client-x',
tenantId: 'tenant-x',
tokenKey: 'token-key',
tokenStore: missingTokenStore,
axiosInstance: {
request: async () => ({
status: 200,
data: { code: 200, data: {} }
})
}
});
await assert.rejects(
missingTokenClient.login({ phone: '13800000000', password: 'md5' }),
(error) => error.message === '登录响应缺少 token'
);
assert.strictEqual(missingTokenStore.getItem('token-key'), null);
const snakeTokenStore = createStore();
const snakeTokenClient = GenealogyApi.createClient({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'client-x',
tenantId: 'tenant-x',
tokenKey: 'token-key',
tokenStore: snakeTokenStore,
axiosInstance: {
request: async () => ({
status: 200,
data: { code: 200, data: { access_token: 'snake-token' } }
})
}
});
await snakeTokenClient.login({ phone: '13800000000', password: 'md5' });
assert.strictEqual(snakeTokenStore.getItem('token-key'), 'snake-token');
store.setItem('token-key', 'logout-token');
await client.logout();
assert.strictEqual(calls[9].url, '/genealogy/pc/auth/logout');
assert.strictEqual(store.getItem('token-key'), null);
const failedLogoutStore = createStore({
'token-key': 'logout-token'
});
const failedLogoutClient = GenealogyApi.createClient({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'client-x',
tenantId: 'tenant-x',
tokenKey: 'token-key',
tokenStore: failedLogoutStore,
axiosInstance: {
request: async () => {
throw new Error('logout failed');
}
}
});
await assert.rejects(failedLogoutClient.logout(), /logout failed/);
assert.strictEqual(failedLogoutStore.getItem('token-key'), null);
await client.sendSmsCode({
sceneCode: 'WEB_H5_LOGIN',
phone: '13800000000',
validToken: 'captcha-ticket'
});
assert.strictEqual(calls[10].url, '/genealogy/pc/auth/sms/code');
assert.strictEqual(calls[10].headers.Authorization, undefined);
assert.strictEqual(calls[10].data.grantType, 'sms');
assert.strictEqual(calls[10].data.sceneCode, 'WEB_H5_LOGIN');
assert.strictEqual(calls[10].data.validToken, 'captcha-ticket');
assert.throws(
() => client.listGenealogies(),
(error) => error.code === 'PC_API_NOT_AVAILABLE'
);
assert.throws(
() => client.publicGenealogies(),
(error) => error.code === 'PC_API_NOT_AVAILABLE'
);
assert.strictEqual(calls.length, 11);
console.log('api-client tests passed');
}
run();
-45
View File
@@ -1,45 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const rootDirectory = path.join(__dirname, '..');
function scriptIndex(html, source) {
return html.indexOf(`src="${source}"`);
}
function run() {
const pages = fs.readdirSync(rootDirectory).filter((fileName) => {
if (!fileName.endsWith('.html')) return false;
const html = fs.readFileSync(path.join(rootDirectory, fileName), 'utf8');
return html.includes('public/js/api-client.js') || html.includes('utils/ApiClient.js');
});
assert.strictEqual(pages.length, 45, '接口客户端加载页面数量应保持为 45 页');
pages.forEach((fileName) => {
const html = fs.readFileSync(path.join(rootDirectory, fileName), 'utf8');
const configIndex = scriptIndex(html, 'config.js');
const storageIndex = scriptIndex(html, 'utils/StorageUtil.js');
const axiosIndex = scriptIndex(html, 'utils/axios.js');
const requestIndex = scriptIndex(html, 'utils/AxiosRequestUtil.js');
const apiIndex = scriptIndex(html, 'utils/ApiClient.js');
assert.notStrictEqual(configIndex, -1, `${fileName} 应加载环境配置`);
assert.notStrictEqual(storageIndex, -1, `${fileName} 应加载 token 存储工具`);
assert.notStrictEqual(axiosIndex, -1, `${fileName} 应加载本地 Axios`);
assert.notStrictEqual(requestIndex, -1, `${fileName} 应加载 AxiosRequestUtil`);
assert.notStrictEqual(apiIndex, -1, `${fileName} 应加载 ApiClient`);
assert.ok(configIndex < storageIndex, `${fileName} 应先加载环境配置再加载存储工具`);
assert.ok(storageIndex < axiosIndex, `${fileName} 应先加载存储工具再加载 Axios`);
assert.ok(axiosIndex < requestIndex, `${fileName} 应先加载 Axios 再加载请求封装`);
assert.ok(requestIndex < apiIndex, `${fileName} 应先加载请求封装再加载接口客户端`);
assert.strictEqual(html.includes('utils/RequestUtil.js'), false, `${fileName} 不应继续加载旧 RequestUtil`);
assert.strictEqual(html.includes('public/js/api-client.js'), false, `${fileName} 不应继续加载旧 ApiClient`);
});
console.log('api-script-order tests passed');
}
run();
-45
View File
@@ -1,45 +0,0 @@
const assert = require('assert');
const ArticlePages = require('../public/js/article-pages.js');
function run() {
assert.deepStrictEqual(ArticlePages.normalizeList({ rows: [{ articleId: 1 }] }), [{ articleId: 1 }]);
assert.deepStrictEqual(ArticlePages.normalizeList({ records: [{ articleId: 2 }] }), [{ articleId: 2 }]);
assert.deepStrictEqual(ArticlePages.buildArticleBody({
categoryId: ' 900040001 ',
articleTitle: ' 族谱源流 ',
articleSummary: ' 本支源流 ',
coverOssId: '',
articleContent: '<p>正文</p>',
authorName: ' 管理员 ',
sortOrder: '',
status: ''
}), {
categoryId: 900040001,
articleTitle: '族谱源流',
articleSummary: '本支源流',
coverOssId: undefined,
articleContent: '<p>正文</p>',
authorName: '管理员',
sortOrder: 1,
status: '0'
});
assert.strictEqual(
ArticlePages.buildArticleRow({
articleId: 801,
articleTitle: '族谱源流',
categoryName: '谱序',
status: '0',
updateTime: '2026-07-09'
}, '900001001'),
'<a class="module-row article-row" href="article-detail.html?id=801&genealogyId=900001001"><div><h3>族谱源流</h3><p>谱序 · 已发布 · 2026-07-09</p></div><span class="pill">查看</span></a>'
);
assert.strictEqual(ArticlePages.formatStatus('0'), '已发布');
assert.strictEqual(ArticlePages.formatStatus('1'), '草稿');
console.log('article-pages tests passed');
}
run();
-34
View File
@@ -1,34 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function readProjectFile(fileName) {
return fs.readFileSync(path.join(__dirname, '..', fileName), 'utf8');
}
function run() {
[
'public/js/auth-pages.js',
'public/js/captcha-pages.js'
].forEach((fileName) => {
const content = readProjectFile(fileName);
assert.strictEqual(
content.includes('root.alert'),
false,
`${fileName} 不应再使用浏览器原生 alert 作为提示`
);
});
const publicCss = readProjectFile('public/css/public.css');
assert.strictEqual(
publicCss.includes('.auth-layer-message'),
true,
'public.css 应提供认证页 layui 提示皮肤'
);
console.log('auth-message tests passed');
}
run();
-61
View File
@@ -1,61 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const pages = [
'login.html',
'register.html',
'forgot-password.html'
];
function readPage(fileName) {
return fs.readFileSync(path.join(__dirname, '..', fileName), 'utf8');
}
function run() {
const expectedFormIds = {
'login.html': ['login-password-form', 'login-sms-form'],
'register.html': ['register-form'],
'forgot-password.html': ['password-reset-form']
};
pages.forEach((fileName) => {
const html = readPage(fileName);
const formCaptchaBox = /<form[\s\S]*?data-captcha-box[\s\S]*?<\/form>/i.test(html);
const pageCaptchaBox = /data-captcha-box/.test(html);
const hasLayuiCss = html.includes('href="public/layui/css/layui.css"');
const hasTacCss = html.includes('href="public/tac/css/tac.css"');
const hasCaptchaModalCss = html.includes('href="public/css/captcha-modal.css"');
const layuiScriptIndex = html.indexOf('src="public/layui/layui.js"');
const authScriptIndex = html.indexOf('src="public/js/auth-pages.js"');
assert.strictEqual(formCaptchaBox, false, `${fileName} 不应把 TAC 挂载点放在表单内部`);
assert.strictEqual(pageCaptchaBox, false, `${fileName} 不应保留会将 TAC 渲染到页面底部的静态挂载点`);
assert.strictEqual(hasLayuiCss, true, `${fileName} 应加载 layui.css 作为 layer 提示样式依赖`);
assert.strictEqual(hasTacCss, true, `${fileName} 应加载 TAC 自带样式`);
assert.strictEqual(hasCaptchaModalCss, false, `${fileName} 不应加载自定义验证码样式`);
assert.notStrictEqual(layuiScriptIndex, -1, `${fileName} 应加载 layui.js 使用 layer.msg`);
assert.ok(layuiScriptIndex < authScriptIndex, `${fileName} 应在 auth-pages.js 之前加载 layui.js`);
assert.deepStrictEqual(
Array.from(html.matchAll(/<form[^>]*id="([^"]+)"/g)).map((match) => match[1]),
expectedFormIds[fileName],
`${fileName} 应使用表单 id 作为 JS 挂载点`
);
assert.strictEqual(/data-auth-form=|data-captcha-scene=|data-success-url=|data-scene-code=|data-login-mode/.test(html), false, `${fileName} 不应把认证业务配置写在 HTML data 属性上`);
assert.strictEqual(
(html.match(/<input type="hidden" name="validToken" \/>/g) || []).length,
expectedFormIds[fileName].length,
`${fileName} 每个认证表单都应携带 validToken 隐藏字段`
);
});
assert.strictEqual(
readPage('register.html').includes('id="register-form"'),
true,
'register.html 应使用 register-form 挂载注册逻辑'
);
console.log('auth-page-structure tests passed');
}
run();
-91
View File
@@ -1,91 +0,0 @@
const assert = require('assert');
const AuthPages = require('../public/js/auth-pages.js');
function fakeHash(value) {
return `hashed:${value}`;
}
function run() {
assert.deepStrictEqual(AuthPages.buildLoginBody({
phone: '13800000000',
password: '123456',
validToken: 'ticket'
}, fakeHash), {
phone: '13800000000',
password: 'hashed:123456',
validToken: 'ticket'
});
assert.deepStrictEqual(AuthPages.buildSmsLoginBody({
phone: '13800000000',
smsCode: '6666',
validToken: 'ticket'
}), {
phone: '13800000000',
smsCode: '6666',
validToken: 'ticket'
});
assert.deepStrictEqual(AuthPages.buildRegisterBody({
phone: '13800000000',
password: '123456',
nickName: '汤小明',
validToken: 'ticket'
}, fakeHash), {
phone: '13800000000',
password: 'hashed:123456',
nickName: '汤小明',
validToken: 'ticket'
});
assert.deepStrictEqual(AuthPages.buildPasswordResetBody({
phone: '13800000000',
smsCode: '8888',
newPassword: 'abcdef',
validToken: 'ticket'
}, fakeHash), {
phone: '13800000000',
smsCode: '8888',
newPassword: 'hashed:abcdef',
validToken: 'ticket'
});
assert.strictEqual(AuthPages.getCaptchaScene('login'), 'WEB_H5_LOGIN');
assert.strictEqual(AuthPages.getCaptchaScene('sms-login'), 'WEB_H5_LOGIN');
assert.strictEqual(AuthPages.getCaptchaScene('register'), 'WEB_H5_REGISTER');
assert.strictEqual(AuthPages.getCaptchaScene('password-reset'), 'WEB_H5_FORGOT_PASSWORD');
assert.strictEqual(AuthPages.getSuccessUrl('register'), 'login.html');
assert.strictEqual(AuthPages.getSuccessUrl('password-reset'), 'login.html');
assert.strictEqual(AuthPages.validateAuthValues('login', {
phone: '',
password: '123456'
}), '请填写手机号');
assert.strictEqual(AuthPages.validateAuthValues('login', {
phone: '123',
password: '123456'
}), '请输入正确的手机号');
assert.strictEqual(AuthPages.validateAuthValues('register', {
phone: '13800000000',
password: '12345'
}), '密码至少需要 6 位');
assert.strictEqual(AuthPages.validateAuthValues('sms-login', {
phone: '13800000000',
smsCode: '12'
}), '请输入正确的短信验证码');
assert.strictEqual(AuthPages.validateAuthValues('password-reset', {
phone: '13800000000',
smsCode: '123456',
newPassword: 'abcdef',
confirmPassword: 'abcdeg'
}), '两次输入的新密码不一致');
assert.strictEqual(AuthPages.validateAuthValues('password-reset', {
phone: '13800000000',
smsCode: '123456',
newPassword: 'abcdef',
confirmPassword: 'abcdef'
}), '');
console.log('auth-pages tests passed');
}
run();
-162
View File
@@ -1,162 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const CaptchaPages = require('../public/js/captcha-pages.js');
const fakeApi = {
clientId: 'client-x',
tenantId: 'tenant-x'
};
function run() {
const captchaSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'captcha-pages.js'), 'utf8');
assert.strictEqual(captchaSource.includes('auth-captcha-modal'), false, '验证码适配层不应控制自定义弹窗样式');
assert.strictEqual(captchaSource.includes('bgUrl:'), false, '验证码适配层不应覆盖 TAC 背景样式');
assert.strictEqual(captchaSource.includes('logoUrl:'), false, '验证码适配层不应覆盖 TAC Logo 样式');
assert.strictEqual(captchaSource.includes('root.layui.layer.open'), true, '验证码适配层应使用 Layui 默认弹层承载 TAC');
assert.strictEqual(captchaSource.includes('root.layui.layer.close'), true, '验证码结束时应关闭对应 Layui 弹层');
assert.deepStrictEqual(CaptchaPages.buildChallengeBody({
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000'
}, fakeApi), {
tenantId: 'tenant-x',
clientId: 'client-x',
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000'
});
const challenge = CaptchaPages.adaptChallengeResponse({
code: 200,
data: {
required: true,
providerCode: 'tianai',
captchaType: 'SLIDER',
challengeId: 'challenge-1',
payload: {
id: 'tianai-1',
backgroundImage: 'bg.png',
templateImage: 'tpl.png',
backgroundImageWidth: 340
}
}
});
assert.strictEqual(challenge.code, 200);
assert.strictEqual(challenge.data.type, 'SLIDER');
assert.strictEqual(challenge.data.id, 'tianai-1');
assert.strictEqual(challenge.data.challengeId, 'challenge-1');
assert.strictEqual(challenge.data.backgroundImage, 'bg.png');
assert.strictEqual(challenge.data.templateImage, 'tpl.png');
assert.deepStrictEqual(CaptchaPages.buildVerifyBody({
id: 'tianai-1',
data: {
bgImageWidth: 340,
bgImageHeight: 180,
templateImageWidth: 50,
templateImageHeight: 50,
startTime: 1720000000000,
stopTime: 1720000001500,
trackList: [
{ x: 0, y: 0, t: 0, type: 'down' },
{ x: 120, y: 0, t: 650, type: 'move' },
{ x: 120, y: 0, t: 700, type: 'up' }
]
}
}, {
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000',
providerCode: 'tianai',
captchaType: 'SLIDER',
challengeId: 'challenge-1'
}, fakeApi), {
tenantId: 'tenant-x',
clientId: 'client-x',
sceneCode: 'WEB_H5_LOGIN',
subject: '13800000000',
challengeId: 'challenge-1',
providerCode: 'tianai',
captchaType: 'SLIDER',
payload: {
track: {
bgImageWidth: 340,
bgImageHeight: 180,
templateImageWidth: 50,
templateImageHeight: 50,
startTime: 1720000000000,
stopTime: 1720000001500,
left: 120,
top: 0,
trackList: [
{ x: 0, y: 0, t: 0, type: 'down' },
{ x: 120, y: 0, t: 650, type: 'move' },
{ x: 120, y: 0, t: 700, type: 'up' }
]
}
}
});
assert.strictEqual(CaptchaPages.extractValidToken({
code: 200,
data: {
passed: true,
validToken: 'captcha-ticket'
}
}), 'captcha-ticket');
assert.strictEqual(CaptchaPages.shouldRequireCaptcha({
required: false
}), false);
assert.strictEqual(CaptchaPages.shouldRequireCaptcha({
required: true
}), true);
const originalDocument = global.document;
const originalLayui = global.layui;
const mountedBox = {
innerHTML: '<div class="tianai-captcha-parent"></div>'
};
let layerOptions;
let closedLayerIndex;
global.document = {
querySelector(selector) {
assert.strictEqual(selector, '[data-captcha-layer-box="captcha-layer-1"]');
return mountedBox;
}
};
global.layui = {
layer: {
open(options) {
layerOptions = options;
return 7;
},
close(index) {
closedLayerIndex = index;
}
}
};
const captchaLayer = CaptchaPages.createCaptchaLayer();
assert.strictEqual(captchaLayer.box, mountedBox);
assert.strictEqual(captchaLayer.layerIndex, 7);
assert.strictEqual(layerOptions.type, 1);
assert.strictEqual(layerOptions.title, false);
assert.strictEqual(layerOptions.closeBtn, 0);
assert.deepStrictEqual(layerOptions.area, ['318px', '318px']);
assert.strictEqual(layerOptions.offset, 'auto');
assert.strictEqual(typeof layerOptions.content, 'string');
assert.strictEqual(layerOptions.content.includes('data-captcha-layer-box="captcha-layer-1"'), true);
CaptchaPages.closeCaptchaLayer(captchaLayer);
assert.strictEqual(mountedBox.innerHTML, '');
assert.strictEqual(closedLayerIndex, 7);
global.document = originalDocument;
global.layui = originalLayui;
console.log('captcha-pages tests passed');
}
run();
-62
View File
@@ -1,62 +0,0 @@
const assert = require('assert');
const CeremonyPages = require('../public/js/ceremony-pages.js');
function run() {
assert.deepStrictEqual(CeremonyPages.normalizeList({ rows: [{ ceremonyId: 1 }] }), [{ ceremonyId: 1 }]);
assert.deepStrictEqual(CeremonyPages.normalizeList({ records: [{ ceremonyId: 2 }] }), [{ ceremonyId: 2 }]);
assert.deepStrictEqual(CeremonyPages.buildCeremonyBody({
ceremonyType: ' wedding ',
ceremonyTitle: ' 婚礼邀请 ',
ceremonyDesc: ' 邀请亲友 ',
ceremonyTime: '2026-07-09 10:00:00',
location: ' 祠堂 ',
coverOssId: '',
sortOrder: '',
status: ''
}), {
ceremonyType: 'wedding',
ceremonyTitle: '婚礼邀请',
ceremonyDesc: '邀请亲友',
ceremonyTime: '2026-07-09 10:00:00',
location: '祠堂',
coverOssId: undefined,
sortOrder: 1,
status: '0'
});
assert.deepStrictEqual(CeremonyPages.buildMeritBody({
donorName: ' 汤明 ',
meritType: '',
meritTitle: ' 修谱捐款 ',
meritContent: ' 支持修谱 ',
amount: ' 100 ',
meritTime: '',
sortOrder: '',
status: ''
}), {
donorName: '汤明',
meritType: 'donation',
meritTitle: '修谱捐款',
meritContent: '支持修谱',
amount: 100,
meritTime: undefined,
sortOrder: 1,
status: '0'
});
assert.strictEqual(
CeremonyPages.buildCeremonyRow({
ceremonyId: 601,
ceremonyTitle: '婚礼邀请',
ceremonyType: 'wedding',
ceremonyTime: '2026-07-09',
giftCount: 2
}, '900001001'),
'<button class="module-row ceremony-row" type="button" data-ceremony-id="601"><div><h3>婚礼邀请</h3><p>wedding · 2026-07-09 · 2 条献礼</p></div><span class="pill">管理</span></button>'
);
console.log('ceremony-pages tests passed');
}
run();
-30
View File
@@ -1,30 +0,0 @@
const assert = require('assert');
const configFactory = require('../config.js');
const developmentConfig = configFactory({
location: {
hostname: '127.0.0.1',
port: '5501'
}
});
assert.deepStrictEqual(developmentConfig.getConfig(), {
environment: 'development',
apiBaseUrl: 'http://182.61.18.23:8080'
});
assert.strictEqual(developmentConfig.getClientId, undefined);
assert.strictEqual(developmentConfig.getTenantId, undefined);
assert.strictEqual(developmentConfig.getTokenKey, undefined);
assert.strictEqual(developmentConfig.getTokenHeaderName, undefined);
const productionConfig = configFactory({
location: {
hostname: 'genealogy.example.test',
port: ''
}
});
assert.strictEqual(productionConfig.getEnvironment(), 'production');
assert.strictEqual(productionConfig.getApiBaseUrl(), 'http://182.61.18.23:8080');
console.log('config tests passed');
+29 -39
View File
@@ -1,45 +1,35 @@
const assert = require('assert');
const assert = require('node:assert/strict');
const test = require('node:test');
const FeedPages = require('../public/js/feed-pages.js');
function run() {
assert.deepStrictEqual(FeedPages.normalizeList({ rows: [{ feedId: 1 }] }), [{ feedId: 1 }]);
assert.deepStrictEqual(FeedPages.normalizeList({ records: [{ feedId: 2 }] }), [{ feedId: 2 }]);
assert.deepStrictEqual(FeedPages.buildFeedBody({
content: ' 家族聚会 ',
mediaOssIds: ' 1,2 ',
visibility: '',
status: ''
}), {
content: '家族聚会',
mediaOssIds: '1,2',
visibility: '2',
status: '0'
});
assert.deepStrictEqual(FeedPages.buildCommentBody({
parentCommentId: '',
replyUserId: ' 9001 ',
content: ' 写得好 '
}), {
parentCommentId: undefined,
replyUserId: 9001,
content: '写得好'
});
assert.strictEqual(
FeedPages.buildFeedRow({
feedId: 901,
content: '家族聚会',
authorName: '汤明',
likeCount: 3,
commentCount: 2,
createTime: '2026-07-09'
test('动态表单只构造最新文档定义的 FamilyFeedBody 字段', () => {
assert.deepEqual(
FeedPages.buildFeedBody({
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
status: '0',
visibility: '2'
}),
'<div class="module-row feed-row" data-feed-id="901"><div><h3>汤明</h3><p>家族聚会</p><small>2026-07-09 · 3 赞 · 2 评论</small></div><div class="row-actions"><button class="pill" type="button" data-feed-like="901">点赞</button><button class="pill" type="button" data-feed-comment="901">评论</button></div></div>'
{
feedType: 'text',
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
status: '0'
}
);
});
console.log('feed-pages tests passed');
}
test('动态页只从 URL 读取真实家谱和动态编号', () => {
assert.equal(FeedPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(FeedPages.getCurrentGenealogyId(''), '');
assert.equal(FeedPages.getCurrentFeedId('?feedId=7001'), '7001');
});
run();
test('动态列表要求服务端返回 feedId 和 feedContent', () => {
assert.deepEqual(FeedPages.normalizeFeed({ feedId: 7001, feedContent: '家谱动态' }), {
feedId: '7001',
feedContent: '家谱动态'
});
assert.equal(FeedPages.normalizeFeed({ feedId: 7001, content: '旧字段' }), null);
});
-33
View File
@@ -1,33 +0,0 @@
const assert = require('assert');
const FeedbackPages = require('../public/js/feedback-pages.js');
function run() {
assert.deepStrictEqual(FeedbackPages.buildFeedbackBody({
feedbackType: 'bug',
feedbackTitle: '登录异常',
feedbackContent: '页面打不开',
contactInfo: '13800000000'
}), {
feedbackType: 'bug',
feedbackContent: '【登录异常】页面打不开',
contactInfo: '13800000000'
});
assert.deepStrictEqual(FeedbackPages.normalizeList({
rows: [{ id: 1 }]
}), [{ id: 1 }]);
assert.strictEqual(
FeedbackPages.buildFeedbackRow({
feedbackType: 'bug',
feedbackContent: '页面打不开',
status: '已受理',
createTime: '2026-07-09'
}),
'<div class="module-row"><div><h3>bug</h3><p>已受理 · 2026-07-09 · 页面打不开</p></div><span class="pill">查看</span></div>'
);
console.log('feedback-pages tests passed');
}
run();
-70
View File
@@ -1,70 +0,0 @@
const assert = require('assert');
const GenealogyPages = require('../public/js/genealogy-pages.js');
function run() {
assert.deepStrictEqual(GenealogyPages.buildGenealogyBody({
genealogyName: '四川武胜汤氏族谱',
surname: '汤',
regionCode: '',
addressDetail: '四川武胜',
intro: '家族简介'
}), {
genealogyName: '四川武胜汤氏族谱',
surname: '汤',
regionCode: '',
addressDetail: '四川武胜',
intro: '家族简介',
visibility: '1',
joinMode: '1'
});
assert.strictEqual(GenealogyPages.buildGenealogyBody({
genealogyName: '成都陈氏族谱',
surname: '陈',
regionCode: '510104'
}).regionCode, '510104');
assert.strictEqual(GenealogyPages.pick({
genealogyName: '汤氏族谱',
name: '备用名称'
}, ['genealogyName', 'name'], '未命名'), '汤氏族谱');
assert.deepStrictEqual(GenealogyPages.normalizeList({ rows: [{ id: 1 }] }), [{ id: 1 }]);
assert.deepStrictEqual(GenealogyPages.normalizeList([{ id: 2 }]), [{ id: 2 }]);
assert.strictEqual(GenealogyPages.getQueryParam('?id=900001001&from=plaza', 'id'), '900001001');
assert.strictEqual(GenealogyPages.getQueryParam('?genealogyId=42', 'id'), '');
assert.deepStrictEqual(GenealogyPages.buildJoinApplyBody({
applicantName: ' 汤小明 ',
phone: ' 13800000000 ',
relationDesc: ' 本族成员 ',
applyReason: ''
}), {
applicantName: '汤小明',
phone: '13800000000',
relationDesc: '本族成员',
applyReason: '申请加入家谱',
inviterUserId: undefined
});
assert.strictEqual(
GenealogyPages.buildFamilyHeroTitle({
genealogyName: '汤氏族谱'
}),
'汤氏族谱'
);
assert.strictEqual(GenealogyPages.formatJoinMode('0'), '关闭加入');
assert.strictEqual(GenealogyPages.formatJoinMode('1'), '审核加入');
assert.strictEqual(GenealogyPages.formatJoinMode('2'), '邀请码加入');
assert.strictEqual(
GenealogyPages.buildMyGenealogyRow({
genealogyId: 9,
genealogyName: '汤氏族谱',
roleName: '创建者',
memberCount: 18
}),
'<a class="module-row" href="profile-family-home.html?id=9"><div><h3>汤氏族谱</h3><p>创建者 · 共修入 18 人</p></div><span class="pill">进入主页</span></a>'
);
console.log('genealogy-pages tests passed');
}
run();
-52
View File
@@ -1,52 +0,0 @@
const assert = require('assert');
const GenerationPages = require('../public/js/generation-pages.js');
function run() {
assert.deepStrictEqual(GenerationPages.normalizeList({ rows: [{ id: 1 }] }), [{ id: 1 }]);
assert.deepStrictEqual(GenerationPages.normalizeList({ records: [{ id: 2 }] }), [{ id: 2 }]);
assert.deepStrictEqual(GenerationPages.buildGenerationBody({
generationNo: ' 5 ',
generationText: ' 忠 ',
description: ' 第五代 ',
sortOrder: '',
status: ''
}), {
generationNo: 5,
generationText: '忠',
description: '第五代',
sortOrder: 5,
status: '0'
});
assert.strictEqual(
GenerationPages.buildGenerationRow({
poemId: 77,
generationNo: 5,
generationText: '忠',
memberCount: 8,
description: '第五代'
}, '900001001'),
'<div class="module-row generation-row" data-generation-id="77"><div><h3>第 5 代:忠</h3><p>8 人 · 第五代</p></div><button class="pill" type="button" data-generation-edit="77" data-generation-no="5" data-generation-text="忠" data-genealogy-id="900001001">编辑</button></div>'
);
assert.deepStrictEqual(GenerationPages.buildGenerationBatchBody({
content: ' 德承家亦 ',
stopMissingOldGeneration: 'on'
}), {
content: '德承家亦',
stopMissingOldGeneration: true
});
assert.deepStrictEqual(GenerationPages.buildGenerationBatchBody({
content: '',
stopMissingOldGeneration: ''
}), {
content: '',
stopMissingOldGeneration: false
});
console.log('generation-pages tests passed');
}
run();
-43
View File
@@ -1,43 +0,0 @@
const assert = require('assert');
const GrowthPages = require('../public/js/growth-pages.js');
function run() {
assert.deepStrictEqual(GrowthPages.normalizeList({ rows: [{ recordId: 1 }] }), [{ recordId: 1 }]);
assert.deepStrictEqual(GrowthPages.normalizeList({ records: [{ recordId: 2 }] }), [{ recordId: 2 }]);
assert.deepStrictEqual(GrowthPages.buildGrowthBody({
lineagePersonId: ' 900020001 ',
recordType: '',
recordTitle: ' 出生记录 ',
recordContent: ' 成长内容 ',
recordDate: '2026-07-09',
remindTime: '',
mediaOssIds: ' 2060001 ',
sortOrder: '',
status: ''
}), {
lineagePersonId: 900020001,
recordType: 'growth',
recordTitle: '出生记录',
recordContent: '成长内容',
recordDate: '2026-07-09',
remindTime: undefined,
mediaOssIds: '2060001',
sortOrder: 1,
status: '0'
});
assert.strictEqual(
GrowthPages.buildGrowthRow({
recordId: 701,
recordTitle: '出生记录',
recordType: 'birth',
recordDate: '2026-07-09'
}, '900001001'),
'<a class="module-row growth-row" href="profile-growth-edit.html?recordId=701&genealogyId=900001001"><div><h3>出生记录</h3><p>birth · 2026-07-09</p></div><span class="pill">编辑</span></a>'
);
console.log('growth-pages tests passed');
}
run();
-29
View File
@@ -1,29 +0,0 @@
const assert = require('assert');
const HelpPages = require('../public/js/help-pages.js');
function run() {
assert.deepStrictEqual(HelpPages.normalizeList({ rows: [{ id: 1 }] }), [{ id: 1 }]);
assert.deepStrictEqual(HelpPages.normalizeList({ records: [{ id: 2 }] }), [{ id: 2 }]);
assert.strictEqual(
HelpPages.buildHelpItem({
helpId: 88,
title: '如何创建家谱?',
summary: '填写名称、姓氏、地区即可创建。'
}, 0),
'<details open class="tilt-card" data-help-id="88" data-help-loaded="false"><summary>如何创建家谱?</summary><p>填写名称、姓氏、地区即可创建。</p></details>'
);
assert.strictEqual(
HelpPages.buildHelpItem({
id: 89,
articleTitle: '资料填错了怎么办?',
content: '<b>可以编辑成员资料</b>'
}, 1),
'<details class="tilt-card" data-help-id="89" data-help-loaded="true"><summary>资料填错了怎么办?</summary><p>&lt;b&gt;可以编辑成员资料&lt;/b&gt;</p></details>'
);
console.log('help-pages tests passed');
}
run();
-44
View File
@@ -1,44 +0,0 @@
const assert = require('assert');
const JoinApplyPages = require('../public/js/join-apply-pages.js');
function run() {
assert.deepStrictEqual(JoinApplyPages.normalizeList({ rows: [{ id: 1 }] }), [{ id: 1 }]);
assert.deepStrictEqual(JoinApplyPages.normalizeList({ records: [{ id: 2 }] }), [{ id: 2 }]);
assert.strictEqual(JoinApplyPages.formatApplyStatus('0'), '待审核');
assert.strictEqual(JoinApplyPages.formatApplyStatus('1'), '已通过');
assert.strictEqual(JoinApplyPages.formatApplyStatus('2'), '已拒绝');
assert.deepStrictEqual(JoinApplyPages.buildAuditBody('approve', ''), {
status: '1',
auditRemark: '信息核验通过'
});
assert.deepStrictEqual(JoinApplyPages.buildAuditBody('reject', '资料不完整'), {
status: '2',
auditRemark: '资料不完整'
});
assert.strictEqual(
JoinApplyPages.buildPendingRow({
applyId: 66,
applicantName: '汤小明',
relationDesc: '本族成员',
createTime: '2026-07-09'
}, '900001001'),
'<div class="module-row join-apply-row" data-join-apply-id="66"><div><h3>汤小明申请加入家谱</h3><p>2026-07-09 · 本族成员</p></div><div class="bottom-actions"><button class="btn primary" type="button" data-join-audit="approve" data-genealogy-id="900001001" data-apply-id="66">同意</button><button class="btn ghost" type="button" data-join-audit="reject" data-genealogy-id="900001001" data-apply-id="66">拒绝</button></div></div>'
);
assert.strictEqual(
JoinApplyPages.buildMineRow({
applyId: 67,
genealogyName: '汤氏族谱',
status: '0',
relationDesc: '本族成员'
}),
'<div class="module-row join-apply-row" data-join-apply-id="67"><div><h3>汤氏族谱</h3><p>待审核 · 本族成员</p></div><button class="btn ghost" type="button" data-join-cancel="67">撤销申请</button></div>'
);
console.log('join-apply-pages tests passed');
}
run();
-77
View File
@@ -1,77 +0,0 @@
const assert = require('assert');
const LineagePages = require('../public/js/lineage-pages.js');
function run() {
assert.deepStrictEqual(LineagePages.normalizeList({ rows: [{ personId: 1 }] }), [{ personId: 1 }]);
assert.deepStrictEqual(LineagePages.normalizeList({ records: [{ personId: 2 }] }), [{ personId: 2 }]);
assert.deepStrictEqual(LineagePages.buildLineagePersonBody({
appUserId: '',
personNo: ' P001 ',
personName: ' 汤明 ',
aliasName: ' 明公 ',
sex: '0',
generationNo: ' 3 ',
generationName: '忠',
birthDate: '1990-01-02',
deathDate: '',
introduction: ' 三世成员 '
}), {
appUserId: undefined,
personNo: 'P001',
personName: '汤明',
aliasName: '明公',
sex: '0',
generationNo: 3,
generationName: '忠',
avatarOssId: undefined,
birthDate: '1990-01-02',
deathDate: undefined,
introduction: '三世成员'
});
assert.strictEqual(LineagePages.formatSex('0'), '男');
assert.strictEqual(LineagePages.formatSex('1'), '女');
assert.strictEqual(LineagePages.formatSex('9'), '未知');
assert.strictEqual(
LineagePages.buildPersonRow({
personId: 7001,
personName: '汤明',
generationNo: 3,
generationName: '忠',
sex: '0',
boundAccount: true
}),
'<button class="module-row lineage-row" type="button" data-lineage-person="7001"><div><h3>汤明</h3><p>第 3 世 · 忠字辈 · 男 · 已绑定账号</p></div><span class="pill">查看资料</span></button>'
);
assert.strictEqual(
LineagePages.buildTreeNode({
personId: 7001,
personName: '汤明',
generationNo: 3,
children: [
{
personId: 7002,
personName: '汤小明',
generationNo: 4
}
]
}),
'<li><button type="button" class="lineage-node" data-lineage-person="7001"><strong>汤明</strong><span>第 3 世</span></button><ul><li><button type="button" class="lineage-node" data-lineage-person="7002"><strong>汤小明</strong><span>第 4 世</span></button></li></ul></li>'
);
assert.strictEqual(
LineagePages.buildHomeSummary({
genealogyNo: 'JP001',
hallName: '敦本堂',
memberCount: 18
}, 3),
'JP001 · 堂号 敦本堂 · 共 18 人 · 3 个世系根节点'
);
console.log('lineage-pages tests passed');
}
run();
-49
View File
@@ -1,49 +0,0 @@
const assert = require('assert');
const MemberAdminPages = require('../public/js/member-admin-pages.js');
function run() {
assert.deepStrictEqual(MemberAdminPages.normalizeList({ rows: [{ memberId: 1 }] }), [{ memberId: 1 }]);
assert.deepStrictEqual(MemberAdminPages.normalizeList({ records: [{ memberId: 2 }] }), [{ memberId: 2 }]);
assert.deepStrictEqual(MemberAdminPages.buildMemberUpdateBody({
memberName: ' 汤明 ',
relationName: ' 族亲 ',
roleType: ' admin ',
lineagePersonId: ' 90002 '
}), {
memberName: '汤明',
relationName: '族亲',
roleType: 'admin',
lineagePersonId: 90002
});
assert.strictEqual(
MemberAdminPages.buildOverviewSummary({
genealogyName: '汤氏族谱',
memberCount: 18,
articleCount: 3,
albumCount: 2
}),
'汤氏族谱 · 18 位成员 · 3 篇谱文 · 2 个相册'
);
assert.strictEqual(
MemberAdminPages.buildMemberRow({
memberId: 501,
memberName: '汤明',
relationName: '族亲',
roleType: 'admin',
lineagePersonName: '忠字辈'
}),
'<div class="module-row member-admin-row" data-member-id="501"><div><h3>汤明</h3><p>族亲 · 管理员 · 绑定:忠字辈</p></div><div class="row-actions"><button class="pill" type="button" data-member-edit="501" data-member-name="汤明" data-member-relation="族亲" data-member-role="admin">权限</button><button class="pill is-danger" type="button" data-member-remove="501">移除</button></div></div>'
);
assert.strictEqual(
MemberAdminPages.buildInviteLink('https://example.test/profile-invite.html?id=900001001', '900001001'),
'https://example.test/join-genealogy.html?id=900001001'
);
console.log('member-admin-pages tests passed');
}
run();
-39
View File
@@ -1,39 +0,0 @@
const assert = require('assert');
const MemoPages = require('../public/js/memo-pages.js');
function run() {
assert.deepStrictEqual(MemoPages.normalizeList({ rows: [{ memoId: 1 }] }), [{ memoId: 1 }]);
assert.deepStrictEqual(MemoPages.normalizeList({ records: [{ memoId: 2 }] }), [{ memoId: 2 }]);
assert.deepStrictEqual(MemoPages.buildMemoBody({
memoTitle: ' 修谱事项 ',
memoContent: ' 联系族亲 ',
remindTime: '',
completed: '',
mediaOssIds: ' 2060001 ',
sortOrder: '',
status: ''
}), {
memoTitle: '修谱事项',
memoContent: '联系族亲',
remindTime: undefined,
completed: '0',
mediaOssIds: '2060001',
sortOrder: 1,
status: '0'
});
assert.strictEqual(
MemoPages.buildMemoRow({
memoId: 801,
memoTitle: '修谱事项',
completed: '0',
remindTime: '2026-08-01'
}, '900001001'),
'<a class="module-row memo-row" href="profile-memo-edit.html?memoId=801&genealogyId=900001001"><div><h3>修谱事项</h3><p>未完成 · 2026-08-01</p></div><span class="pill">编辑</span></a>'
);
console.log('memo-pages tests passed');
}
run();
-32
View File
@@ -1,32 +0,0 @@
const assert = require('assert');
const NotificationPages = require('../public/js/notification-pages.js');
function run() {
assert.strictEqual(NotificationPages.formatNotificationStatus({
readStatus: '0'
}), '未读');
assert.strictEqual(NotificationPages.formatNotificationStatus({
readStatus: '1'
}), '已读');
assert.strictEqual(NotificationPages.formatNotificationStatus({
readFlag: true
}), '已读');
assert.strictEqual(
NotificationPages.buildNotificationRow({
notificationId: 77,
title: '族人申请加入汤氏族谱',
content: '需要管理员审核',
createTime: '2026-07-09 12:00:00',
readStatus: '0'
}),
'<div class="module-row notification-row is-unread" data-notification-id="77"><div><h3>族人申请加入汤氏族谱</h3><p>2026-07-09 12:00:00 · 需要管理员审核</p></div><button class="btn ghost" type="button" data-notification-read="77">标记已读</button></div>'
);
assert.deepStrictEqual(NotificationPages.normalizeList({ rows: [{ id: 1 }] }), [{ id: 1 }]);
assert.deepStrictEqual(NotificationPages.normalizeList({ records: [{ id: 2 }] }), [{ id: 2 }]);
console.log('notification-pages tests passed');
}
run();
-76
View File
@@ -1,76 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const appContractPath = path.join(root, 'APP.openapi.json');
const obsoleteRecordPaths = [
'docs/api-page-integration-2026-07-09.md',
'docs/api-page-integration-tracker-2026-07-09.md'
];
const forbiddenPatterns = [
/\/genealogy\/app/,
/\bAPP_(?:LOGIN|REGISTER|PHONE_CHANGE|FORGOT_PASSWORD|SMS_CODE|ACCOUNT_DEACTIVATE)\b/,
/APP\.openapi\.json/
];
const checkedFiles = [
'utils/ApiClient.js',
'public/js/auth-pages.js',
'public/js/captcha-pages.js',
'login.html',
'register.html',
'forgot-password.html',
'app.html',
'download.html',
'notice-detail.html',
'profile-album.html',
'profile-data.html',
'profile-gift.html',
'profile-security.html',
'profile-services.html',
'profile-tree.html',
'genealogy-pc-openapi.yaml',
'docs/pc-api-page-integration-tracker-2026-07-09.md',
'docs/superpowers/plans/2026-07-09-pc-api-replan.md',
'docs/superpowers/plans/2026-07-09-pc-auth-three-pages.md',
'docs/superpowers/plans/2026-07-09-tac-captcha-auth.md',
'docs/superpowers/plans/2026-07-10-pc-tracker-reverification.md',
'public/js/album-pages.js',
'public/js/article-pages.js',
'public/js/ceremony-pages.js',
'public/js/feed-pages.js',
'public/js/genealogy-pages.js',
'public/js/growth-pages.js',
'public/js/lineage-pages.js',
'public/js/member-admin-pages.js',
'public/js/memo-pages.js',
'public/js/vip-pages.js'
];
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function run() {
const apiClient = read('utils/ApiClient.js');
const authPages = read('public/js/auth-pages.js');
assert.strictEqual(fs.existsSync(appContractPath), false);
obsoleteRecordPaths.forEach((relativePath) => {
assert.strictEqual(fs.existsSync(path.join(root, relativePath)), false, `${relativePath} must be removed`);
});
assert.match(apiClient, /\/genealogy\/pc\/auth\/login\/sms/);
assert.match(authPages, /captchaScene: 'WEB_H5_LOGIN'/);
checkedFiles.forEach((relativePath) => {
const content = read(relativePath);
forbiddenPatterns.forEach((pattern) => {
assert.doesNotMatch(content, pattern, `${relativePath} must not retain APP API contracts`);
});
});
console.log('pc-api-contract tests passed');
}
run();
-82
View File
@@ -1,82 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const openApi = fs.readFileSync(path.join(root, 'genealogy-pc-openapi.yaml'), 'utf8');
const apiClient = fs.readFileSync(path.join(root, 'utils', 'ApiClient.js'), 'utf8');
const authPages = fs.readFileSync(path.join(root, 'public', 'js', 'auth-pages.js'), 'utf8');
const captchaPages = fs.readFileSync(path.join(root, 'public', 'js', 'captcha-pages.js'), 'utf8');
const profilePages = fs.readFileSync(path.join(root, 'public', 'js', 'profile-pages.js'), 'utf8');
const securityPages = fs.readFileSync(path.join(root, 'public', 'js', 'security-pages.js'), 'utf8');
const regionPages = fs.readFileSync(path.join(root, 'public', 'js', 'region-pages.js'), 'utf8');
const uploadPages = fs.readFileSync(path.join(root, 'public', 'js', 'upload-pages.js'), 'utf8');
const ENDPOINTS = {
'GET /captcha/require': { source: captchaPages, needle: 'api.captchaRequire', status: 'complete' },
'POST /captcha/challenge': { source: captchaPages, needle: "api.buildApiUrl('/captcha/challenge')", status: 'complete' },
'POST /captcha/verify': { source: captchaPages, needle: "api.buildApiUrl('/captcha/verify')", status: 'complete' },
'GET /auth/code': { status: 'blocked', reason: 'legacy captcha has no PC request field' },
'POST /genealogy/pc/auth/register': { source: authPages, needle: 'api.register', status: 'complete' },
'POST /genealogy/pc/auth/login': { source: authPages, needle: 'api.login', status: 'complete' },
'POST /genealogy/pc/auth/login/sms': { source: authPages, needle: 'api.loginBySms', status: 'complete' },
'POST /genealogy/pc/auth/sms/code': { source: authPages, needle: 'api.sendSmsCode', status: 'complete' },
'GET /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.currentProfile', status: 'complete' },
'PUT /genealogy/pc/auth/profile': { source: profilePages, needle: 'api.updateProfile', status: 'complete' },
'PUT /genealogy/pc/auth/password': { source: securityPages, needle: 'api.changePassword', status: 'complete' },
'PUT /genealogy/pc/auth/password/reset': { source: authPages, needle: 'api.resetPassword', status: 'complete' },
'PUT /genealogy/pc/auth/phone': { status: 'blocked', reason: 'PC/H5 phone SMS scene is not documented' },
'POST /genealogy/pc/auth/account/deactivate': { source: securityPages, needle: 'api.deactivateAccount', status: 'complete' },
'DELETE /genealogy/pc/auth/logout': { source: securityPages, needle: 'api.logout', status: 'complete' },
'POST /genealogy/pc/files/upload': { source: uploadPages, needle: 'api.uploadFile', status: 'complete' },
'POST /genealogy/pc/files/resumable/init': { source: uploadPages, needle: 'api.initResumableUpload', status: 'complete' },
'POST /genealogy/pc/files/resumable/chunk': { source: uploadPages, needle: 'api.uploadChunk', status: 'complete' },
'POST /genealogy/pc/files/resumable/complete': { source: uploadPages, needle: 'api.completeResumableUpload', status: 'complete' },
'POST /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
'DELETE /genealogy/pc/files/reference': { status: 'blocked', reason: 'PC business table and ID contract is not documented' },
'GET /genealogy/region/children': { source: regionPages, needle: 'api.regionChildren', status: 'complete' },
'GET /genealogy/region/path/{regionCode}': { source: regionPages, needle: 'api.regionPath', apiNeedle: '/genealogy/region/path/', status: 'complete' },
'GET /genealogy/region/search': { source: regionPages, needle: 'api.regionSearch', status: 'complete' },
'GET /genealogy/region/{regionCode}': { source: regionPages, needle: 'api.regionDetail', apiNeedle: '/genealogy/region/', status: 'complete' }
};
function getDocumentOperations(documentText) {
let currentPath = '';
const operations = [];
documentText.split(/\r?\n/).forEach((line) => {
const pathMatch = line.match(/^ (\/[^:]+):$/);
const methodMatch = line.match(/^ (get|post|put|delete):$/);
if (pathMatch) currentPath = pathMatch[1];
if (methodMatch && currentPath) operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`);
});
return operations;
}
function run() {
const operations = getDocumentOperations(openApi);
assert.strictEqual(operations.length, 25);
assert.deepStrictEqual(Object.keys(ENDPOINTS).sort(), operations.slice().sort());
operations.forEach((operation) => {
const descriptor = ENDPOINTS[operation];
const route = operation.slice(operation.indexOf(' ') + 1);
assert.match(apiClient, new RegExp((descriptor.apiNeedle || route).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.ok(descriptor, `${operation} must have an owner`);
if (descriptor.status === 'blocked') {
assert.ok(descriptor.reason, `${operation} must name its backend block`);
return;
}
assert.ok(descriptor.source.includes(descriptor.needle), `${operation} must be used by its page module`);
});
console.log('pc-endpoint-coverage tests passed');
}
run();
+56
View File
@@ -0,0 +1,56 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..');
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
test('配置始终使用后端提供的 PC 接口地址', () => {
const createConfig = require('../config.js');
const development = createConfig({ location: { hostname: 'localhost', port: '5500' } });
const production = createConfig({ location: { hostname: 'genealogy.example.test', port: '' } });
assert.equal(development.getEnvironment(), 'development');
assert.equal(production.getEnvironment(), 'production');
assert.equal(development.getApiBaseUrl(), 'http://182.61.18.23:8080');
assert.equal(production.getApiBaseUrl(), 'http://182.61.18.23:8080');
});
test('最新 OpenAPI 文档包含已交付的家族圈接口', () => {
const contract = JSON.parse(read('PC.openapi2.json'));
assert.ok(contract.paths['/genealogy/pc/auth/login']);
assert.ok(contract.paths['/genealogy/pc/files/upload']);
assert.ok(contract.paths['/genealogy/region/children']);
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds']);
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds/{feedId}/comments/{commentId}']);
});
test('旧 YAML 明确指向最新 JSON 接口源', () => {
assert.match(read('PC.openapi.yaml'), /正式接口源为 PC\.openapi2\.json/);
});
test('pages do not load scripts for unavailable business APIs', () => {
const removedScripts = [
'album-pages.js', 'article-pages.js', 'ceremony-pages.js',
'feedback-pages.js', 'genealogy-pages.js', 'generation-pages.js', 'growth-pages.js',
'help-pages.js', 'join-apply-pages.js', 'lineage-pages.js', 'member-admin-pages.js',
'memo-pages.js', 'notification-pages.js', 'promotion-pages.js', 'vip-pages.js'
];
const retainedPages = fs.readdirSync(root).filter((entry) => entry.endsWith('.html'));
retainedPages.forEach((page) => {
const source = read(page);
removedScripts.forEach((script) => {
assert.equal(source.includes('src="public/js/' + script + '"'), false, `${page} still loads ${script}`);
});
});
['profile-feed.html', 'profile-feed-edit.html'].forEach((page) => {
assert.equal(read(page).includes('src="public/js/feed-pages.js"'), true, `${page} must load feed-pages.js`);
});
});
+98
View File
@@ -0,0 +1,98 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const PageAvailability = require('../public/js/pending-pages.js');
const root = path.resolve(__dirname, '..');
const familyPendingPages = [
'profile-families.html',
'profile-create-family.html',
'profile-join-family.html',
'profile-join-review.html',
'profile-family-admin.html',
'profile-invite.html',
'profile-tree.html',
'profile-generation.html'
];
const contentPendingPages = [
'profile-content.html',
'profile-article.html',
'profile-article-edit.html',
'profile-album.html',
'profile-video.html',
'profile-gift.html',
'profile-gift-edit.html',
'profile-growth.html',
'profile-growth-edit.html',
'profile-memo.html',
'profile-memo-edit.html',
'profile-merit.html',
'profile-merit-edit.html',
'profile-messages.html',
'profile-feedback.html',
'profile-admin-permissions.html',
'profile-data-reminders.html'
];
const residualPendingPages = [
'profile-family-home.html',
'profile-share.html',
'profile-services.html'
];
const pendingPages = familyPendingPages.concat(contentPendingPages, residualPendingPages);
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function createLink(attributes) {
return {
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(attributes, name) ? attributes[name] : null;
}
};
}
test('待开发页面生成明确且转义后的状态提示', () => {
assert.match(PageAvailability.buildBanner('家谱基础服务正在开发中'), /家谱基础服务正在开发中/);
assert.match(PageAvailability.buildBanner('<script>'), /&lt;script&gt;/);
});
test('待开发页面显式加载状态脚本', () => {
pendingPages.forEach((page) => {
const source = read(page);
assert.match(source, /data-feature-status="pending"/, `${page} 缺少待开发状态标记`);
assert.match(source, /src="public\/js\/pending-pages\.js"/, `${page} 未加载状态脚本`);
});
});
test('家族圈动态页面保持真实功能状态', () => {
['profile-feed.html', 'profile-feed-edit.html'].forEach((page) => {
assert.doesNotMatch(read(page), /data-feature-status="pending"/, `${page} 不应标记为待开发`);
});
});
test('待开发页面仅放行显式标记的可用功能入口', () => {
assert.equal(PageAvailability.shouldBlockLink(createLink({ href: 'profile-article.html' })), true);
assert.equal(PageAvailability.shouldBlockLink(createLink({ href: 'profile-feed.html', 'data-feature-link': 'available' })), false);
assert.equal(PageAvailability.shouldBlockLink(createLink({ href: '#section' })), false);
});
test('内容入口明确保留家族圈动态跳转', () => {
assert.match(read('profile-content.html'), /href="profile-feed\.html" data-feature-link="available"/);
});
test('家谱主页明确保留家族圈动态跳转', () => {
assert.match(read('profile-family-home.html'), /href="profile-feed\.html" data-feature-link="available"/);
});
test('账号闭环页面保持真实功能状态', () => {
['profile.html', 'profile-data.html', 'profile-security.html'].forEach((page) => {
assert.doesNotMatch(read(page), /data-feature-status="pending"/, `${page} 不应标记为待开发`);
});
});
-15
View File
@@ -1,15 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const profileHtml = fs.readFileSync(path.join(__dirname, '..', 'profile.html'), 'utf8');
const profileCommon = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'profile-common.js'), 'utf8');
assert.match(profileHtml, /data-logout-confirm/);
assert.match(profileHtml, /<a class="btn primary magnetic" href="index\.html" data-logout-confirm>/);
assert.match(profileHtml, /<span>退出登录<\/span><b>返回首页<\/b>/);
assert.match(profileCommon, /function performLogout\(targetUrl\)/);
assert.match(profileCommon, /api\.logout\(\)/);
assert.match(profileCommon, /targetUrl \|\| 'index\.html'/);
console.log('profile logout structure tests passed');
-95
View File
@@ -1,95 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const ProfilePages = require('../public/js/profile-pages.js');
function run() {
assert.strictEqual(ProfilePages.pick({
nickName: '汤小明',
userName: '备用'
}, ['nickName', 'userName'], '未设置'), '汤小明');
assert.strictEqual(ProfilePages.getAvatarText({
nickName: '汤小明'
}), '汤');
assert.deepStrictEqual(ProfilePages.buildProfileView({
nickName: '汤小明',
phone: '13800000000',
sex: '1',
birthday: '2026-07-09',
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
}), {
displayName: '汤小明',
avatarText: '汤',
phone: '13800000000',
sex: '1',
birthday: '2026-07-09',
regionText: '510000 / 510100 / 510104'
});
assert.deepStrictEqual(ProfilePages.buildProfileUpdateBody({
nickName: ' 汤小明 ',
avatarOssId: '2060001',
sex: '1',
birthday: '2026-07-09',
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
}), {
nickName: '汤小明',
avatarOssId: 2060001,
sex: '1',
birthday: '2026-07-09',
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
});
assert.deepStrictEqual(ProfilePages.buildProfileUpdateBody({
nickName: ' ',
avatarOssId: '',
sex: '',
birthday: ''
}), {
nickName: undefined,
avatarOssId: undefined,
sex: undefined,
birthday: undefined,
provinceCode: undefined,
cityCode: undefined,
districtCode: undefined
});
assert.strictEqual(ProfilePages.shouldRedirectToHome({
getToken: () => ''
}), true);
assert.strictEqual(ProfilePages.shouldRedirectToHome({
getToken: () => 'token-x'
}), false);
assert.strictEqual(ProfilePages.shouldRedirectToHome({
getToken: () => 'token-x'
}, { status: 401 }), true);
assert.strictEqual(ProfilePages.shouldRedirectToHome({
getToken: () => 'token-x'
}, { code: 401 }), true);
assert.strictEqual(ProfilePages.shouldRedirectToHome({
getToken: () => 'token-x'
}, { status: 500 }), false);
const profileData = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
assert.match(profileData, /data-upload-target="#profileAvatarOssId"/);
assert.match(profileData, /data-upload-mode="auto"/);
assert.match(profileData, /data-upload-biz-type="avatar"/);
assert.match(profileData, /<script src="public\/js\/md5\.js"><\/script>/);
assert.match(profileData, /<script src="public\/js\/upload-pages\.js"><\/script>/);
assert.match(profileData, /data-region-search-form/);
assert.match(profileData, /data-region-search-results/);
assert.match(profileData, /data-region-search-path/);
console.log('profile-pages tests passed');
}
run();
-53
View File
@@ -1,53 +0,0 @@
const assert = require('assert');
const PromotionPages = require('../public/js/promotion-pages.js');
function run() {
const promotions = [
{
promotionId: 11,
promotionTitle: '平台公告',
promotionContent: '<p>公开展示规范</p>',
summary: '保护家族资料安全',
coverUrl: 'cover.jpg',
linkUrl: 'download.html',
publishTime: '2026-07-09'
}
];
assert.deepStrictEqual(PromotionPages.normalizeList({ rows: promotions }), promotions);
assert.deepStrictEqual(PromotionPages.normalizeList({ records: promotions }), promotions);
assert.strictEqual(
PromotionPages.buildPromotionCard(promotions[0]),
'<a class="promotion-card" href="download.html" data-promotion-id="11"><img src="cover.jpg" alt="平台公告" /><div><h3>平台公告</h3><p>保护家族资料安全</p><span>2026-07-09</span></div></a>'
);
assert.strictEqual(
PromotionPages.buildDownloadCard({
promotionId: 12,
title: 'Android 安装包',
description: '适用于安卓手机',
linkUrl: 'https://example.test/app.apk'
}),
'<a class="card soft download-card tilt-card" href="https://example.test/app.apk" data-promotion-id="12"><h3>Android 安装包</h3><p>适用于安卓手机</p></a>'
);
assert.deepStrictEqual(
PromotionPages.pickPromotion(promotions, '11'),
promotions[0]
);
assert.deepStrictEqual(
PromotionPages.buildNoticeDetail(promotions[0]),
{
title: '平台公告',
summary: '保护家族资料安全',
content: '<p>公开展示规范</p>',
time: '2026-07-09'
}
);
console.log('promotion-pages tests passed');
}
run();
+84
View File
@@ -0,0 +1,84 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..');
const staticPages = [
'news.html',
'privacy.html',
'children-privacy.html',
'terms.html',
'not-found.html',
'forbidden.html',
'maintenance.html',
'my-tickets.html',
'ticket-detail.html'
];
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function exists(relativePath) {
return fs.existsSync(path.join(root, relativePath));
}
function localHrefs(source) {
return Array.from(source.matchAll(/\shref="([^"]+)"/g))
.map((match) => match[1])
.filter((href) => href && !href.startsWith('#') && !/^(?:https?:|mailto:|tel:)/i.test(href))
.map((href) => href.split('#')[0].split('?')[0]);
}
test('官网静态信息页面均有独立入口', () => {
staticPages.forEach((page) => {
assert.equal(exists(page), true, `${page} 不存在`);
});
});
test('新闻页提供分类与现有详情跳转', () => {
if (!exists('news.html')) return;
const source = read('news.html');
assert.match(source, /href="#platform"/);
assert.match(source, /href="#culture"/);
assert.match(source, /href="notice-detail\.html"/);
assert.match(source, /href="article-detail\.html"/);
});
test('法律页面明确等待法务确认', () => {
['privacy.html', 'children-privacy.html', 'terms.html'].forEach((page) => {
if (!exists(page)) return;
assert.match(read(page), /待法务确认/);
});
});
test('工单功能入口显示待开发状态并保留帮助中心跳转', () => {
['submit-ticket.html', 'my-tickets.html', 'ticket-detail.html'].forEach((page) => {
if (!exists(page)) return;
const source = read(page);
assert.match(source, /data-feature-status="pending"/);
assert.match(source, /src="public\/js\/pending-pages\.js"/);
assert.match(source, /href="help\.html"\s+data-feature-link="available"/);
});
});
test('新增静态页面的本地跳转均有目标文件', () => {
staticPages.forEach((page) => {
if (!exists(page)) return;
localHrefs(read(page)).forEach((href) => {
assert.equal(exists(href), true, `${page} 指向不存在的 ${href}`);
});
});
});
test('首页提供新闻和法律文档入口', () => {
const source = read('index.html');
assert.match(source, /href="news\.html"/);
assert.match(source, /href="privacy\.html"/);
assert.match(source, /href="children-privacy\.html"/);
assert.match(source, /href="terms\.html"/);
});
+26
View File
@@ -0,0 +1,26 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..');
const sourcePages = [
'news.html',
'privacy.html',
'children-privacy.html',
'terms.html',
'not-found.html',
'forbidden.html',
'maintenance.html',
'my-tickets.html',
'ticket-detail.html'
];
test('新增官网页面不保留压缩式长行', () => {
sourcePages.forEach((page) => {
const source = fs.readFileSync(path.join(root, page), 'utf8');
const longestLine = Math.max(...source.split(/\r?\n/).map((line) => line.length));
assert.ok(longestLine < 180, `${page} 仍存在 ${longestLine} 字符的压缩式长行`);
});
});
-70
View File
@@ -1,70 +0,0 @@
const assert = require('assert');
const RegionPages = require('../public/js/region-pages.js');
function run() {
assert.deepStrictEqual(RegionPages.normalizeList({ rows: [{ regionCode: '51' }] }), [{ regionCode: '51' }]);
assert.deepStrictEqual(RegionPages.normalizeList({ records: [{ regionCode: '510100' }] }), [{ regionCode: '510100' }]);
assert.strictEqual(
RegionPages.buildRegionOption({
regionCode: '510000',
regionName: '四川省'
}),
'<option value="510000">四川省</option>'
);
assert.strictEqual(
RegionPages.buildPathText([
{ regionName: '四川省' },
{ regionName: '成都市' },
{ name: '锦江区' }
]),
'四川省 / 成都市 / 锦江区'
);
assert.strictEqual(RegionPages.getFinalRegionCode({
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
}), '510104');
assert.strictEqual(RegionPages.getFinalRegionCode({
provinceCode: '510000',
cityCode: '',
districtCode: ''
}), '510000');
assert.deepStrictEqual(RegionPages.buildProfileRegionBody({
provinceCode: '510000',
cityCode: '510100',
districtCode: ''
}), {
provinceCode: '510000',
cityCode: '510100',
districtCode: undefined
});
assert.deepStrictEqual(RegionPages.buildRegionSelection([
{ regionCode: '510000', regionName: '四川省', level: 1 },
{ regionCode: '510100', regionName: '成都市', level: 2 },
{ regionCode: '510104', regionName: '锦江区', level: 3 }
]), {
provinceCode: '510000',
cityCode: '510100',
districtCode: '510104'
});
assert.deepStrictEqual(RegionPages.buildRegionSelection([
{ regionCode: '110000' },
{ regionCode: '110100' },
{ regionCode: '110105' }
]), {
provinceCode: '110000',
cityCode: '110100',
districtCode: '110105'
});
console.log('region-pages tests passed');
}
run();
-146
View File
@@ -1,146 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const SecurityPages = require('../public/js/security-pages.js');
const securityPagePath = path.join(__dirname, '..', 'profile-security.html');
const securityPagesPath = path.join(__dirname, '..', 'public', 'js', 'security-pages.js');
function hash(value) {
return `hashed:${value}`;
}
function loadSecurityPages(root) {
const source = fs.readFileSync(securityPagesPath, 'utf8');
const sandbox = {
module: { exports: {} },
exports: {},
globalThis: root,
window: root
};
vm.runInNewContext(source, sandbox, { filename: securityPagesPath });
return sandbox.module.exports;
}
function createPageRoot(api) {
const handlers = {};
const document = {
addEventListener(type, handler) {
handlers[type] = handler;
},
querySelector(selector) {
return selector === '[data-security-page]' ? {} : null;
},
querySelectorAll() {
return [];
}
};
return {
document,
handlers,
GenealogyApi: { defaultClient: api },
confirm: () => true,
alert: () => {},
location: { href: '' }
};
}
function waitForAsyncAction() {
return new Promise((resolve) => setImmediate(resolve));
}
async function run() {
assert.deepStrictEqual(SecurityPages.buildPasswordChangeBody({
oldPassword: '123456',
newPassword: 'abcdef'
}, hash), {
oldPassword: 'hashed:123456',
newPassword: 'hashed:abcdef'
});
assert.deepStrictEqual(SecurityPages.buildPhoneChangeBody({
newPhone: ' 13900000000 ',
smsCode: ' 123456 ',
validToken: ' ticket '
}), {
newPhone: '13900000000',
smsCode: '123456',
validToken: 'ticket'
});
assert.strictEqual(SecurityPages.buildSmsLoginBody, undefined);
assert.doesNotMatch(fs.readFileSync(securityPagePath, 'utf8'), /data-security-form="sms-login"/);
assert.deepStrictEqual(SecurityPages.buildDeactivateBody({
password: '123456',
reason: ' 不再使用 '
}, hash), {
password: 'hashed:123456',
reason: '不再使用'
});
assert.strictEqual(SecurityPages.isDangerConfirmed('确认注销'), true);
assert.strictEqual(SecurityPages.isDangerConfirmed('注销'), false);
const logoutRoot = createPageRoot({
logout() {
return Promise.reject(new Error('network'));
}
});
const LogoutPages = loadSecurityPages(logoutRoot);
LogoutPages.init();
logoutRoot.handlers.click({
target: {
closest(selector) {
return selector === '[data-security-logout]' ? this : null;
}
},
preventDefault() {}
});
await waitForAsyncAction();
assert.strictEqual(logoutRoot.location.href, 'index.html');
const deactivateRoot = createPageRoot({
deactivateAccount() {
return Promise.resolve();
}
});
const DeactivatePages = loadSecurityPages(deactivateRoot);
const deactivateForm = {
getAttribute(name) {
return name === 'data-security-form' ? 'deactivate' : null;
},
querySelectorAll(selector) {
if (selector !== '[name]') return [];
return [
{ name: 'password', value: '123456' },
{ name: 'reason', value: '' },
{ name: 'confirmText', value: '确认注销' }
];
},
querySelector() {
return null;
}
};
DeactivatePages.init();
deactivateRoot.handlers.submit({
target: {
closest(selector) {
return selector === '[data-security-form]' ? deactivateForm : null;
}
},
preventDefault() {}
});
await waitForAsyncAction();
assert.strictEqual(deactivateRoot.location.href, 'index.html');
console.log('security-pages tests passed');
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+28
View File
@@ -0,0 +1,28 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const SecurityPages = require('../public/js/security-pages.js');
const UploadPages = require('../public/js/upload-pages.js');
test('security forms use dedicated captcha scenes and carry the returned ticket', () => {
assert.equal(SecurityPages.getCaptchaScene('password'), 'WEB_H5_CHANGE_PASSWORD');
assert.equal(SecurityPages.getCaptchaScene('phone'), 'WEB_H5_CHANGE_PHONE');
assert.deepEqual(
SecurityPages.buildPasswordChangeBody({
oldPassword: 'old',
newPassword: 'new',
validToken: 'captcha-ticket'
}, (value) => 'md5-' + value),
{
oldPassword: 'md5-old',
newPassword: 'md5-new',
validToken: 'captcha-ticket'
}
);
});
test('upload helper exposes only single-file upload support', () => {
assert.equal('uploadResumable' in UploadPages, false);
assert.equal('buildResumableInitBody' in UploadPages, false);
assert.equal(UploadPages.getUploadMode(5 * 1024 * 1024), 'single');
});
-145
View File
@@ -1,145 +0,0 @@
const assert = require('assert');
const UploadPages = require('../public/js/upload-pages.js');
async function run() {
assert.deepStrictEqual(UploadPages.normalizeUploadResult({
ossId: 2060001,
fileName: 'cover.jpg',
url: 'https://cdn.example.test/cover.jpg'
}), {
ossId: '2060001',
fileName: 'cover.jpg',
url: 'https://cdn.example.test/cover.jpg'
});
assert.deepStrictEqual(UploadPages.normalizeUploadResult({
id: 2060002,
originalName: 'old.png',
fileUrl: '/old.png'
}), {
ossId: '2060002',
fileName: 'old.png',
url: '/old.png'
});
assert.strictEqual(UploadPages.mergeOssIds('', '2060001', false), '2060001');
assert.strictEqual(UploadPages.mergeOssIds('2060001', '2060002', true), '2060001,2060002');
assert.strictEqual(UploadPages.mergeOssIds('2060001', '2060001', true), '2060001');
assert.strictEqual(UploadPages.buildUploadStatus('cover.jpg', '2060001'), 'cover.jpg 上传完成,OSS ID2060001');
assert.strictEqual(UploadPages.getUploadMode(4194304), 'single');
assert.strictEqual(UploadPages.getUploadMode(4194305), 'resumable');
assert.deepStrictEqual(UploadPages.buildResumableInitBody({
name: 'avatar.jpg',
size: 5242880,
type: 'image/jpeg'
}, 'file-md5', 4194304, {
bizType: 'avatar',
usageScene: 'profile_avatar'
}), {
fileName: 'avatar.jpg',
fileSize: 5242880,
fileMd5: 'file-md5',
contentType: 'image/jpeg',
chunkSize: 4194304,
totalChunks: 2,
bizType: 'avatar',
usageScene: 'profile_avatar'
});
const calls = [];
const file = {
name: 'avatar.jpg',
size: 8,
type: 'image/jpeg',
slice(start, end) {
return { start, end, size: end - start };
}
};
const api = {
initResumableUpload(body) {
calls.push({ type: 'init', body });
return Promise.resolve({ uploadId: 'upload-1' });
},
uploadChunk(body) {
calls.push({ type: 'chunk', body });
return Promise.resolve();
},
completeResumableUpload(body) {
calls.push({ type: 'complete', body });
return Promise.resolve({ ossId: 2060001 });
}
};
const result = await UploadPages.uploadResumable(api, file, {
chunkSize: 4,
bizType: 'avatar',
usageScene: 'profile_avatar',
hashBlob(blob) {
return Promise.resolve(blob === file ? 'file-md5' : `chunk-${blob.start}`);
}
});
assert.deepStrictEqual(result, { ossId: 2060001 });
assert.deepStrictEqual(calls, [
{
type: 'init',
body: {
fileName: 'avatar.jpg',
fileSize: 8,
fileMd5: 'file-md5',
contentType: 'image/jpeg',
chunkSize: 4,
totalChunks: 2,
bizType: 'avatar',
usageScene: 'profile_avatar'
}
},
{
type: 'chunk',
body: {
uploadId: 'upload-1',
chunkIndex: 0,
chunkMd5: 'chunk-0',
file: { start: 0, end: 4, size: 4 }
}
},
{
type: 'chunk',
body: {
uploadId: 'upload-1',
chunkIndex: 1,
chunkMd5: 'chunk-4',
file: { start: 4, end: 8, size: 4 }
}
},
{
type: 'complete',
body: {
uploadId: 'upload-1',
fileMd5: 'file-md5',
fileSize: 8
}
}
]);
let singleUploadCalled = false;
const singleResult = await UploadPages.uploadFileForPage({
uploadFile(value) {
singleUploadCalled = value.size === 4;
return Promise.resolve({ ossId: 2060002 });
}
}, { size: 4 }, {
mode: 'auto',
chunkSize: 4
});
assert.strictEqual(singleUploadCalled, true);
assert.deepStrictEqual(singleResult, { ossId: 2060002 });
console.log('upload-pages tests passed');
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
-57
View File
@@ -1,57 +0,0 @@
const assert = require('assert');
const StorageUtil = require('../utils/StorageUtil.js');
const FormUtil = require('../utils/FormUtil.js');
const AxiosRequestUtil = require('../utils/AxiosRequestUtil.js');
assert.strictEqual(FormUtil.trimOrUndefined(' 张三 '), '张三');
assert.strictEqual(FormUtil.trimOrUndefined(' '), undefined);
assert.strictEqual(FormUtil.toNumberOrUndefined('12'), 12);
assert.strictEqual(FormUtil.toNumberOrUndefined(''), undefined);
assert.strictEqual(FormUtil.toBoolean('on'), true);
assert.strictEqual(FormUtil.toBoolean('0'), false);
const store = {};
const storage = {
getItem: (key) => Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null,
setItem: (key, value) => {
store[key] = String(value);
},
removeItem: (key) => {
delete store[key];
}
};
StorageUtil.write(storage, 'token', 'abc');
assert.strictEqual(StorageUtil.read(storage, 'token'), 'abc');
StorageUtil.remove(storage, 'token');
assert.strictEqual(StorageUtil.read(storage, 'token'), null);
const calls = [];
const requester = AxiosRequestUtil.createRequester({
baseUrl: 'http://182.61.18.23:8080',
clientId: 'pc-client',
tokenHeaderName: 'Authorization',
getToken: () => 'token-x',
axiosInstance: {
request: async (config) => {
calls.push(config);
return {
status: 200,
data: { code: 200, data: { ok: true } }
};
}
}
});
requester('GET', '/genealogy/pc/auth/profile', {
query: { keyword: '汤 氏' }
}).then((data) => {
assert.deepStrictEqual(data, { ok: true });
assert.strictEqual(calls[0].method, 'get');
assert.strictEqual(calls[0].url, '/genealogy/pc/auth/profile');
assert.deepStrictEqual(calls[0].params, { keyword: '汤 氏' });
assert.strictEqual(calls[0].headers.clientid, 'pc-client');
assert.strictEqual(calls[0].headers.Authorization, 'Bearer token-x');
console.log('utils tests passed');
});
-52
View File
@@ -1,52 +0,0 @@
const assert = require('assert');
const VipPages = require('../public/js/vip-pages.js');
function run() {
assert.deepStrictEqual(VipPages.normalizeList({ rows: [{ packageId: 1 }] }), [{ packageId: 1 }]);
assert.deepStrictEqual(VipPages.normalizeList({ records: [{ packageId: 2 }] }), [{ packageId: 2 }]);
assert.deepStrictEqual(VipPages.buildVipOrderBody({
packageId: ' 900060001 ',
genealogyId: ' 900001001 ',
payType: 'wechat'
}), {
packageId: 900060001,
genealogyId: 900001001,
payType: 'wechat'
});
assert.deepStrictEqual(VipPages.buildVipOrderBody({
packageId: '900060002',
genealogyId: '',
payType: ''
}), {
packageId: 900060002,
genealogyId: undefined,
payType: undefined
});
assert.strictEqual(
VipPages.buildPackageCard({
packageId: 900060001,
packageName: '年度会员',
price: 199,
durationDays: 365,
description: '适合长期维护家谱'
}),
'<button class="vip-package-card" type="button" data-vip-package-id="900060001"><span class="vip-package-name">年度会员</span><strong>¥199</strong><small>365天</small><p>适合长期维护家谱</p></button>'
);
assert.strictEqual(
VipPages.buildOrderRow({
orderNo: 'VIP202607090001',
packageName: '年度会员',
payType: 'wechat',
orderStatus: 'created'
}),
'<div class="module-row vip-order-row"><div><h3>年度会员</h3><p>VIP202607090001 · 微信支付 · created</p></div><span class="pill">订单</span></div>'
);
console.log('vip-pages tests passed');
}
run();