修改功,现在补齐页面
This commit is contained in:
@@ -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();
|
||||
@@ -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]
|
||||
]);
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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><b>可以编辑成员资料</b></p></details>'
|
||||
);
|
||||
|
||||
console.log('help-pages tests passed');
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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`);
|
||||
});
|
||||
});
|
||||
@@ -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>'), /<script>/);
|
||||
});
|
||||
|
||||
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} 不应标记为待开发`);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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"/);
|
||||
});
|
||||
@@ -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} 字符的压缩式长行`);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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 ID:2060001');
|
||||
|
||||
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;
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user