等待后端更新接口
This commit is contained in:
@@ -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 AdminPermissionPages = require('../public/js/admin-permission-pages.js');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('管理员权限页只从当前家谱上下文读取安全编号', () => {
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=2062179707935264769'), '2062179707935264769');
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=0'), '');
|
||||
assert.equal(AdminPermissionPages.getCurrentGenealogyId('?genealogyId=unsafe'), '');
|
||||
});
|
||||
|
||||
test('管理员权限概览只接受当前家谱的角色与严格布尔能力字段', () => {
|
||||
assert.deepEqual(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false,
|
||||
ownerUserId: '2062179707935264701'
|
||||
}, '2062179707935264769'), {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
});
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264768',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
}, '2062179707935264769'), null);
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: 'true',
|
||||
canEditContent: false
|
||||
}, '2062179707935264769').canManage, false);
|
||||
assert.equal(AdminPermissionPages.normalizePermissionOverview({
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'legacy-role',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
}, '2062179707935264769'), null);
|
||||
});
|
||||
|
||||
test('权限概览依据当前能力展示可管理、审核与内容维护状态', () => {
|
||||
const admin = {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'admin',
|
||||
canManage: true,
|
||||
canEditContent: false
|
||||
};
|
||||
const member = {
|
||||
genealogyId: '2062179707935264769',
|
||||
roleType: 'member',
|
||||
canManage: false,
|
||||
canEditContent: false
|
||||
};
|
||||
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /当前角色[\s\S]*管理员/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /成员角色维护[\s\S]*可管理/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(admin), /内容维护[\s\S]*可维护/);
|
||||
assert.match(AdminPermissionPages.renderCurrentPermissions(member), /入谱审核[\s\S]*不可审核/);
|
||||
assert.equal(AdminPermissionPages.canMaintainContent({ canManage: 'true', canEditContent: false }), false);
|
||||
});
|
||||
|
||||
test('管理员权限页使用既有家谱详情接口和统一状态,不提供 App 粒度授权表单', () => {
|
||||
const page = read('profile-admin-permissions.html');
|
||||
const script = read('public/js/admin-permission-pages.js');
|
||||
|
||||
assert.match(page, /data-admin-permission-current[\s\S]*data-api-state="loading"/);
|
||||
assert.match(page, /src="public\/js\/admin-permission-pages\.js"/);
|
||||
assert.match(script, /api\.genealogyDetail\(genealogyId\)/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /当前账号无权查看该家谱权限。/);
|
||||
assert.doesNotMatch(page, /<input|<select|<form/);
|
||||
assert.doesNotMatch(script, /addManageUser|getManagePower|auth_str/);
|
||||
});
|
||||
@@ -184,6 +184,9 @@ test('相册页面拆分列表、编辑和照片详情且不允许手填 OSS ID
|
||||
});
|
||||
assert.match(listPage, /data-album-list/);
|
||||
assert.match(editPage, /name="albumName"/);
|
||||
assert.match(editPage, /data-album-form[^>]+novalidate/);
|
||||
assert.match(editPage, /class="field-required"/);
|
||||
assert.match(editPage, /<div class="api-status" data-album-form-status/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-album-detail/);
|
||||
@@ -191,5 +194,42 @@ test('相册页面拆分列表、编辑和照片详情且不允许手填 OSS ID
|
||||
assert.match(detailPage, /name="ossId" type="hidden"/);
|
||||
assert.match(detailPage, /name="shootTime" type="datetime-local"/);
|
||||
assert.match(detailPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-album-photo-form novalidate/);
|
||||
assert.match(detailPage, /<div class="api-status" data-album-photo-status/);
|
||||
assert.doesNotMatch(editPage + detailPage, /type="text"[^>]+(?:coverOssId|ossId)|name="albumId"|name="photoId"/);
|
||||
});
|
||||
|
||||
test('相册列表、详情和表单将加载、空数据、403 与请求失败写入统一状态组件', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-album.html'), 'utf8');
|
||||
const detailPage = fs.readFileSync(path.join(projectRoot, 'profile-album-detail.html'), 'utf8');
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.match(listPage, /data-album-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-album-detail[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-album-photo-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setStatus\('\[data-album-list\]', '正在加载相册…', 'loading'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-list\]', '暂无相册', 'empty'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-list\]', '正在加载照片…', 'loading'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-list\]', '暂无照片', 'empty'\)/);
|
||||
assert.match(source, /当前账号无权查看该相册照片。/);
|
||||
assert.match(source, /正在读取家谱权限…', 'loading'/);
|
||||
assert.match(source, /setStatus\('\[data-album-form-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /setStatus\('\[data-album-photo-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /getApiStateType\(error\)/);
|
||||
});
|
||||
|
||||
test('相册与照片只接受后端布尔能力字段,并在所有写路径上门控', () => {
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
const canEditAlbums = requireFunction('canEditAlbums');
|
||||
|
||||
assert.equal(canEditAlbums({ canEditContent: true }), true);
|
||||
assert.equal(canEditAlbums({ canManage: true }), true);
|
||||
assert.equal(canEditAlbums({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.match(source, /canEditContent = canEditAlbums\(detail\)/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !albumId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !normalizeId\(albumId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !albumId \|\| !normalizeId\(photoId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
});
|
||||
|
||||
@@ -737,7 +737,7 @@ test('feedback methods use the PC collection path and AppFeedbackBody whitelist'
|
||||
await client.submitFeedback({
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173',
|
||||
contactInfo: '测试联系方式',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop'
|
||||
});
|
||||
@@ -747,7 +747,7 @@ test('feedback methods use the PC collection path and AppFeedbackBody whitelist'
|
||||
['post', '/genealogy/pc/feedback', {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
contactInfo: '测试联系方式'
|
||||
}],
|
||||
['get', '/genealogy/pc/feedback', undefined]
|
||||
]);
|
||||
|
||||
@@ -119,17 +119,23 @@ test('推广区域只把 401 视为登录失效', () => {
|
||||
assert.equal(AppPromotionPages.isUnauthorized(new Error('网络失败')), false);
|
||||
});
|
||||
|
||||
test('推广区域提供明确的未登录和空列表状态', () => {
|
||||
assert.match(
|
||||
AppPromotionPages.renderPromotionLoginRequired(),
|
||||
/<a href="login\.html">立即登录<\/a>/
|
||||
);
|
||||
assert.match(AppPromotionPages.renderPromotionList([]), /当前暂无应用推广/);
|
||||
test('推广区域将空、无权限与请求失败交给统一状态组件呈现', () => {
|
||||
const source = fs.readFileSync(path.resolve(__dirname, '../public/js/app-promotion-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(AppPromotionPages.renderPromotionList([]), '');
|
||||
assert.equal(AppPromotionPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(AppPromotionPages.isForbidden({ status: 401 }), false);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setPromotionState\(list, 'loading', '正在读取应用推广…'\)/);
|
||||
assert.match(source, /setPromotionState\(list, 'empty', '当前暂无应用推广。'\)/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
});
|
||||
|
||||
test('应用下载页只通过页面标记和新推广模块接入,不暴露业务 ID 输入', () => {
|
||||
assert.match(appPage, /<body[^>]*data-promotion-page/);
|
||||
assert.match(appPage, /data-promotion-list/);
|
||||
assert.match(appPage, /data-api-state="loading"/);
|
||||
assert.match(appPage, /src="public\/js\/profile-common\.js"/);
|
||||
assert.match(appPage, /src="public\/js\/app-promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /src="public\/js\/promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /name="(?:promotionId|coverOssId)"/);
|
||||
|
||||
@@ -166,7 +166,7 @@ test('谱文写后重读必须返回同一条稳定记录', () => {
|
||||
assert.equal(matchesSavedArticle({}, '2060000000000000001'), false);
|
||||
});
|
||||
|
||||
test('谱文页面开放 PC CRUD 且不提供手填分类、业务 ID 或停用状态', () => {
|
||||
test('谱文页面按家谱能力开放 PC CRUD,且不提供手填分类、业务 ID 或停用状态', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-article.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(projectRoot, 'profile-article-edit.html'), 'utf8');
|
||||
@@ -178,11 +178,34 @@ test('谱文页面开放 PC CRUD 且不提供手填分类、业务 ID 或停用
|
||||
assert.match(listPage, /public\/js\/article-pages\.js/);
|
||||
assert.doesNotMatch(editPage, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(editPage, /name="articleTitle"/);
|
||||
assert.match(editPage, /data-article-form[^>]*novalidate/);
|
||||
assert.match(editPage, /for="articleTitle">谱文标题<span class="field-required"/);
|
||||
assert.match(editPage, /name="articleSummary"/);
|
||||
assert.match(editPage, /name="articleContent"/);
|
||||
assert.match(editPage, /for="articleContent">正文<span class="field-required"/);
|
||||
assert.match(editPage, /name="authorName"/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /data-article-form-status role="status" aria-live="polite"/);
|
||||
assert.match(editPage, /public\/js\/article-pages\.js/);
|
||||
assert.doesNotMatch(editPage, /name="articleId"|name="categoryId"|type="text"[^>]+coverOssId/);
|
||||
});
|
||||
|
||||
test('谱文只接受后端布尔能力字段,并在提交层保持写权限门控', () => {
|
||||
const script = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.equal(requireFunction('canEditArticles')({ canEditContent: true }), true);
|
||||
assert.equal(requireFunction('canEditArticles')({ canManage: true }), true);
|
||||
assert.equal(requireFunction('canEditArticles')({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
||||
assert.match(script, /canEditContent = canEditArticles\(detail\)/);
|
||||
});
|
||||
|
||||
test('谱文编辑表单使用统一状态与提交中反馈', () => {
|
||||
const script = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存谱文…', 'loading'\)/);
|
||||
});
|
||||
|
||||
@@ -100,6 +100,26 @@ test('registration page exposes the documented SMS and confirmation inputs', ()
|
||||
assert.match(source, /name="confirmPassword"/);
|
||||
});
|
||||
|
||||
test('认证表单提供可见标签、统一状态容器和提交中反馈,且不暗示邀请码流程', () => {
|
||||
const login = fs.readFileSync(path.join(__dirname, '..', 'login.html'), 'utf8');
|
||||
const register = fs.readFileSync(path.join(__dirname, '..', 'register.html'), 'utf8');
|
||||
const reset = fs.readFileSync(path.join(__dirname, '..', 'forgot-password.html'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(__dirname, '..', 'public/js/auth-pages.js'), 'utf8');
|
||||
|
||||
[login, register, reset].forEach((source) => {
|
||||
assert.match(source, /class="auth-field"/);
|
||||
assert.match(source, /role="status" aria-live="polite" data-auth-form-status/);
|
||||
assert.match(source, /public\/js\/profile-common\.js/);
|
||||
assert.match(source, /novalidate/);
|
||||
});
|
||||
assert.doesNotMatch(register, /邀请码/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setSubmitPending\(/);
|
||||
assert.match(script, /正在登录…/);
|
||||
assert.match(script, /正在注册…/);
|
||||
assert.match(script, /正在重设…/);
|
||||
});
|
||||
|
||||
test('password login page retains a verification token field for the PC captcha strategy', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'login.html'), 'utf8');
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
|
||||
function loadProfileUI() {
|
||||
const source = fs.readFileSync(path.join(projectRoot, 'public/js/profile-common.js'), 'utf8');
|
||||
const window = {
|
||||
jQuery(value) {
|
||||
if (typeof value === 'function') return undefined;
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
window.window = window;
|
||||
vm.runInNewContext(source, { window, URLSearchParams });
|
||||
return window.ProfileUI;
|
||||
}
|
||||
|
||||
function loadProfileStateWithoutJquery() {
|
||||
const source = fs.readFileSync(path.join(projectRoot, 'public/js/profile-common.js'), 'utf8');
|
||||
const window = {};
|
||||
|
||||
window.window = window;
|
||||
vm.runInNewContext(source, { window, URLSearchParams });
|
||||
return window.ProfileUI;
|
||||
}
|
||||
|
||||
function loadProfileUIWithLinks(links, search) {
|
||||
const source = fs.readFileSync(path.join(projectRoot, 'public/js/profile-common.js'), 'utf8');
|
||||
const window = { location: { search } };
|
||||
|
||||
function wrapLink(link) {
|
||||
return {
|
||||
is(selector) {
|
||||
return selector === '[data-genealogy-context-link]' && link.contextLink === true;
|
||||
},
|
||||
attr(name, value) {
|
||||
if (value === undefined) return link[name];
|
||||
link[name] = value;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function wrapLinks(items) {
|
||||
return {
|
||||
filter(callback) {
|
||||
return wrapLinks(items.filter((link) => callback.call(link)));
|
||||
},
|
||||
each(callback) {
|
||||
items.forEach((link) => callback.call(link));
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.jQuery = function (value) {
|
||||
if (typeof value === 'function') return undefined;
|
||||
if (typeof value === 'string') {
|
||||
assert.equal(value, '[data-genealogy-context-link], a[href]');
|
||||
return wrapLinks(links);
|
||||
}
|
||||
return wrapLink(value);
|
||||
};
|
||||
window.window = window;
|
||||
vm.runInNewContext(source, { window, URLSearchParams });
|
||||
return window.ProfileUI;
|
||||
}
|
||||
|
||||
test('公共业务状态统一输出加载、空数据、失败和无权限语义', () => {
|
||||
const ui = loadProfileUI();
|
||||
|
||||
assert.match(ui.renderApiState('loading', '正在加载'), /data-api-state="loading"[^>]+role="status"[^>]+aria-busy="true"/);
|
||||
assert.match(ui.renderApiState('empty', '暂无内容'), /data-api-state="empty"[^>]+role="status"/);
|
||||
assert.match(ui.renderApiState('error', '读取失败'), /data-api-state="error"[^>]+role="alert"/);
|
||||
assert.match(ui.renderApiState('forbidden', '没有权限'), /data-api-state="forbidden"[^>]+role="alert"/);
|
||||
assert.throws(() => ui.renderApiState('unknown', '未知状态'), /不支持的业务状态/);
|
||||
});
|
||||
|
||||
test('公共业务状态转义接口消息并由单一方法写入容器', () => {
|
||||
const ui = loadProfileUI();
|
||||
const container = { innerHTML: '' };
|
||||
|
||||
ui.setApiState(container, 'error', '<img src=x onerror=alert(1)>');
|
||||
assert.doesNotMatch(container.innerHTML, /<img/);
|
||||
assert.match(container.innerHTML, /<img/);
|
||||
});
|
||||
|
||||
test('独立表单页不加载 jQuery 时仍可复用公共业务状态 owner', () => {
|
||||
const ui = loadProfileStateWithoutJquery();
|
||||
|
||||
assert.equal(typeof ui.renderApiState, 'function');
|
||||
assert.equal(typeof ui.setApiState, 'function');
|
||||
});
|
||||
|
||||
test('业务模块在初始化前先加载统一的家谱上下文与状态 owner', () => {
|
||||
const pages = {
|
||||
'profile.html': ['profile-pages.js'],
|
||||
'profile-data.html': ['upload-pages.js', 'region-pages.js', 'profile-pages.js'],
|
||||
'profile-families.html': ['genealogy-entry-pages.js'],
|
||||
'profile-feedback.html': ['feedback-pages.js'],
|
||||
'profile-security.html': ['captcha-pages.js', 'security-pages.js'],
|
||||
'profile-services.html': ['vip-pages.js']
|
||||
};
|
||||
|
||||
Object.entries(pages).forEach(([page, modules]) => {
|
||||
const source = fs.readFileSync(path.join(projectRoot, page), 'utf8');
|
||||
const commonIndex = source.indexOf('src="public/js/profile-common.js');
|
||||
|
||||
assert.ok(commonIndex >= 0, `${page} 未加载 ProfileUI`);
|
||||
modules.forEach((module) => {
|
||||
assert.ok(commonIndex < source.indexOf('src="public/js/' + module + '"'), `${page} 应先加载 ProfileUI 再加载 ${module}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('实际会刷新内容的表单状态容器统一具备可访问性语义', () => {
|
||||
const pages = {
|
||||
'profile-data.html': ['profileAvatarStatus'],
|
||||
'profile-album-edit.html': ['albumCoverStatus', 'data-album-form-status'],
|
||||
'profile-album-detail.html': ['albumPhotoUploadStatus', 'data-album-photo-status'],
|
||||
'profile-article-edit.html': ['articleCoverStatus'],
|
||||
'profile-ceremony-detail.html': ['data-ceremony-gift-status', 'data-ceremony-invitee-status'],
|
||||
'profile-families.html': ['data-genealogy-entry-status'],
|
||||
'profile-family-home.html': ['data-family-home-status'],
|
||||
'profile-feed-edit.html': ['feedMediaStatus', 'data-feed-form-status'],
|
||||
'profile-gift-edit.html': ['giftCoverStatus', 'data-ceremony-form-status'],
|
||||
'profile-gift.html': ['data-my-invitation-status'],
|
||||
'profile-growth-edit.html': ['growthMediaStatus'],
|
||||
'profile-memo-edit.html': ['memoMediaStatus'],
|
||||
'profile-relative-edit.html': ['relativeMediaStatus'],
|
||||
'profile-tree.html': ['data-lineage-page-status', 'data-lineage-binding-status', 'lineage-avatar-status'],
|
||||
'profile-video-edit.html': ['video-file-status', 'video-cover-status', 'data-video-form-status']
|
||||
};
|
||||
|
||||
Object.entries(pages).forEach(([page, markers]) => {
|
||||
const source = fs.readFileSync(path.join(projectRoot, page), 'utf8');
|
||||
|
||||
markers.forEach((marker) => {
|
||||
const attribute = marker.indexOf('data-') === 0 ? marker : 'id="' + marker + '"';
|
||||
const tag = source.match(new RegExp('<[^>]*' + attribute + '[^>]*>'));
|
||||
|
||||
assert.ok(tag, `${page} 缺少 ${marker} 状态容器`);
|
||||
assert.match(tag[0], /role="status"/, `${page} 的 ${marker} 缺少状态角色`);
|
||||
assert.match(tag[0], /aria-live="polite"/, `${page} 的 ${marker} 缺少实时反馈`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('家谱业务页链接由公共上下文 owner 统一识别', () => {
|
||||
const ui = loadProfileUI();
|
||||
const source = fs.readFileSync(path.join(projectRoot, 'public/js/profile-common.js'), 'utf8');
|
||||
|
||||
assert.equal(ui.isGenealogyContextHref('profile-article.html'), true);
|
||||
assert.equal(ui.isGenealogyContextHref('profile-video-edit.html?videoId=1'), true);
|
||||
assert.equal(ui.isGenealogyContextHref('profile-family-admin.html#members'), true);
|
||||
assert.equal(ui.isGenealogyContextHref('profile-data.html'), false);
|
||||
assert.equal(ui.isGenealogyContextHref('https://example.com/profile-article.html'), false);
|
||||
assert.match(source, /\[data-genealogy-context-link\], a\[href\]/);
|
||||
});
|
||||
|
||||
test('家谱业务页跳转自动透传上下文,未选家谱时回到家谱入口', () => {
|
||||
const links = [
|
||||
{ href: 'profile-article.html?articleId=1' },
|
||||
{ href: 'profile-data.html' },
|
||||
{ href: 'profile-data.html', contextLink: true }
|
||||
];
|
||||
const ui = loadProfileUIWithLinks(links, '?genealogyId=99');
|
||||
|
||||
ui.syncGenealogyContextLinks();
|
||||
assert.equal(links[0].href, 'profile-article.html?articleId=1&genealogyId=99');
|
||||
assert.equal(links[1].href, 'profile-data.html');
|
||||
assert.equal(links[2].href, 'profile-data.html?genealogyId=99');
|
||||
|
||||
const noContextLinks = [{ href: 'profile-video.html?videoId=1' }];
|
||||
const noContextUi = loadProfileUIWithLinks(noContextLinks, '');
|
||||
|
||||
noContextUi.syncGenealogyContextLinks();
|
||||
assert.equal(noContextLinks[0].href, 'profile-families.html?next=' + encodeURIComponent('profile-video.html?videoId=1'));
|
||||
assert.equal(
|
||||
noContextUi.getGenealogyEntryUrl('profile-video-edit.html?videoId=1#cover'),
|
||||
'profile-families.html?next=' + encodeURIComponent('profile-video-edit.html?videoId=1#cover')
|
||||
);
|
||||
});
|
||||
|
||||
test('谱文列表、详情和编辑页接入公共业务状态 owner', () => {
|
||||
const script = fs.readFileSync(path.join(projectRoot, 'public/js/article-pages.js'), 'utf8');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-article.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(projectRoot, 'profile-article-edit.html'), 'utf8');
|
||||
const css = fs.readFileSync(path.join(projectRoot, 'public/css/public.css'), 'utf8');
|
||||
|
||||
assert.match(script, /root\.ProfileUI\.renderApiState/);
|
||||
assert.match(script, /root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /'loading'[\s\S]+?'empty'[\s\S]+?'forbidden'[\s\S]+?'error'/);
|
||||
assert.match(listPage, /data-api-state="loading"/);
|
||||
assert.match(listPage, /data-api-state="empty"/);
|
||||
assert.match(editPage, /data-api-state="loading"/);
|
||||
assert.match(css, /\.api-state--error,[\s\S]+\.api-state--forbidden/);
|
||||
});
|
||||
|
||||
test('内容工作台的初始加载与待选详情占位使用统一状态标记', () => {
|
||||
const listPages = [
|
||||
'profile-feed.html',
|
||||
'profile-generation.html',
|
||||
'profile-growth.html',
|
||||
'profile-relative.html',
|
||||
'profile-merit.html',
|
||||
'profile-memo.html'
|
||||
];
|
||||
const detailPages = [
|
||||
'profile-growth.html',
|
||||
'profile-relative.html',
|
||||
'profile-merit.html',
|
||||
'profile-memo.html'
|
||||
];
|
||||
|
||||
listPages.forEach((page) => {
|
||||
const source = fs.readFileSync(path.join(projectRoot, page), 'utf8');
|
||||
|
||||
assert.match(source, /api-state--loading[^>]*data-api-state="loading"/);
|
||||
assert.doesNotMatch(source, /api-empty/);
|
||||
});
|
||||
detailPages.forEach((page) => {
|
||||
const source = fs.readFileSync(path.join(projectRoot, page), 'utf8');
|
||||
|
||||
assert.match(source, /api-state--empty[^>]*data-api-state="empty"/);
|
||||
});
|
||||
});
|
||||
|
||||
test('内容记录详情在未选中记录时也使用统一空状态组件', () => {
|
||||
const detailRenderers = [
|
||||
[require('../public/js/growth-pages.js').renderGrowthDetail, '成长记录'],
|
||||
[require('../public/js/relative-pages.js').renderRelativeDetail, '亲友记录'],
|
||||
[require('../public/js/memo-pages.js').renderMemoDetail, '备忘录'],
|
||||
[require('../public/js/merit-pages.js').renderMeritDetail, '功德记录']
|
||||
];
|
||||
|
||||
detailRenderers.forEach(([renderDetail, label]) => {
|
||||
const html = renderDetail(null);
|
||||
|
||||
assert.match(html, /api-state--empty/, `${label} 未使用统一空状态`);
|
||||
assert.doesNotMatch(html, /api-empty/, `${label} 仍保留旧空状态`);
|
||||
});
|
||||
});
|
||||
@@ -105,7 +105,7 @@ test('活动、祭品和邀请响应使用完整 PC VO 并拒绝不安全 ID', (
|
||||
genealogyId: '2060000000000000002',
|
||||
sponsorUserId: '2060000000000000003',
|
||||
sponsorNickName: '叶子',
|
||||
sponsorPhone: '19181970173',
|
||||
sponsorPhone: '测试联系方式',
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '清明祭祖',
|
||||
ceremonyDesc: '缅怀先祖',
|
||||
@@ -205,7 +205,7 @@ test('活动和祭品展示转义内容且不暴露手机号、OSS ID 或内部
|
||||
genealogyId: '2060000000000000002',
|
||||
sponsorUserId: '2060000000000000003',
|
||||
sponsorNickName: '<img src=x>叶子',
|
||||
sponsorPhone: '19181970173',
|
||||
sponsorPhone: '测试联系方式',
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '<script>alert(1)</script>祭祖',
|
||||
coverOssId: '2060000000000000004',
|
||||
@@ -226,7 +226,7 @@ test('活动和祭品展示转义内容且不暴露手机号、OSS ID 或内部
|
||||
})]);
|
||||
|
||||
assert.doesNotMatch(html, /<script|<img/);
|
||||
assert.doesNotMatch(html, /19181970173|19100000000|206000000000000000[1-6]/);
|
||||
assert.doesNotMatch(html, /测试联系方式|19100000000|206000000000000000[1-6]/);
|
||||
assert.match(html, /祭祖|缅怀|66\.66/);
|
||||
});
|
||||
|
||||
@@ -242,10 +242,58 @@ test('祭祀功能拆分列表、编辑和详情页面且不允许手填 ID', ()
|
||||
});
|
||||
assert.match(listPage, /data-ceremony-list/);
|
||||
assert.match(editPage, /name="ceremonyType"/);
|
||||
assert.match(editPage, /data-ceremony-form data-ceremony-editor hidden novalidate/);
|
||||
assert.match(editPage, /class="field-required"/);
|
||||
assert.match(editPage, /<div class="upload-status" data-ceremony-form-status/);
|
||||
assert.match(editPage, /Ceremony Editor/);
|
||||
assert.doesNotMatch(editPage, /创建贺礼|贺礼标题|贺礼内容/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-ceremony-gift-list/);
|
||||
assert.match(detailPage, /data-ceremony-invitee-options/);
|
||||
assert.match(detailPage, /name="giftAmount"/);
|
||||
assert.match(detailPage, /data-ceremony-gift-form data-ceremony-admin-editor hidden novalidate/);
|
||||
assert.match(detailPage, /data-ceremony-invitee-form data-ceremony-admin-editor hidden novalidate/);
|
||||
assert.match(detailPage, /<div class="upload-status" data-ceremony-gift-status/);
|
||||
assert.match(detailPage, /<div class="upload-status" data-ceremony-invitee-status/);
|
||||
assert.doesNotMatch(editPage + detailPage, /name="(?:genealogyId|ceremonyId|giftId|inviteeUserIds)"/);
|
||||
});
|
||||
|
||||
test('活动、祭品和受邀名单复用统一加载、空数据、403 与失败状态', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony.html'), 'utf8');
|
||||
const detailPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony-detail.html'), 'utf8');
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.match(listPage, /data-ceremony-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-ceremony-detail[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-ceremony-gift-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-ceremony-invitation-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-ceremony-invitee-options[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-list\]', '正在加载祭祀活动…', 'loading'\)/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-list\]', '暂无祭祀活动', 'empty'\)/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-gift-list\]', '暂无祭品记录', 'empty'\)/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-invitation-list\]', '暂无邀请记录', 'empty'\)/);
|
||||
assert.match(source, /当前账号无权查看或维护受邀名单。/);
|
||||
assert.match(source, /正在读取家谱权限…', 'loading'/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-form-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /setStatus\('\[data-ceremony-gift-status\]', validation, 'error'\)/);
|
||||
assert.match(source, /正在更新受邀人…', 'loading'/);
|
||||
assert.match(source, /getApiStateType\(error\)/);
|
||||
});
|
||||
|
||||
test('祭祀管理只接受后端布尔能力字段,并在全部写路径上门控', () => {
|
||||
const source = fs.readFileSync(modulePath, 'utf8');
|
||||
const canEditCeremonies = requireFunction('canEditCeremonies');
|
||||
|
||||
assert.equal(canEditCeremonies({ canEditContent: true }), true);
|
||||
assert.equal(canEditCeremonies({ canManage: true }), true);
|
||||
assert.equal(canEditCeremonies({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.match(source, /canEditContent = canEditCeremonies\(detail\)/);
|
||||
assert.match(source, /root\.AppRichEditor && root\.AppRichEditor\.syncAll/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !ceremonyId \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !normalizeId\(ceremonyId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !ceremonyId \|\| !normalizeId\(giftId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
||||
});
|
||||
|
||||
@@ -136,3 +136,21 @@ test('贺礼邀请页面只接入我的邀请闭环,不提供活动或献礼
|
||||
assert.doesNotMatch(page, /data-ceremony-gift-form/);
|
||||
assert.doesNotMatch(page, /href="profile-gift-edit\.html"/);
|
||||
});
|
||||
|
||||
test('我的邀请将列表、详情和响应结果写入统一状态组件', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const page = fs.readFileSync(path.join(root, 'profile-gift.html'), 'utf8');
|
||||
const source = fs.readFileSync(path.join(root, 'public', 'js', 'ceremony-pages.js'), 'utf8');
|
||||
|
||||
assert.match(page, /data-my-invitation-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(page, /data-my-invitation-detail[^>]*>[\s\S]*data-api-state="empty"/);
|
||||
assert.match(page, /data-my-invitation-status/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setInvitationState\('\[data-my-invitation-list\]', 'loading', '正在加载我的邀请…'\)/);
|
||||
assert.match(source, /setInvitationState\('\[data-my-invitation-list\]', 'empty', '暂无贺礼邀请'\)/);
|
||||
assert.match(source, /当前账号无权查看邀请。/);
|
||||
assert.match(source, /正在更新邀请状态…/);
|
||||
assert.match(source, /当前账号无权响应该邀请。/);
|
||||
assert.equal(CeremonyPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(CeremonyPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
@@ -119,6 +119,8 @@ test('家谱主页只把缺失登录态和 401 视为登录失效', () => {
|
||||
assert.equal(FamilyHomePages.shouldRedirectToLogin({
|
||||
getToken() { return ''; }
|
||||
}), true);
|
||||
assert.equal(FamilyHomePages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(FamilyHomePages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('家谱主页只读取当前家谱概览和世系树', async () => {
|
||||
@@ -152,30 +154,57 @@ test('家谱主页只读取当前家谱概览和世系树', async () => {
|
||||
assert.deepEqual(result.tree, tree);
|
||||
});
|
||||
|
||||
test('家谱主页开放真实只读概览且不制造邀请或聚合统计入口', () => {
|
||||
test('家谱主页以 PC 工作台分组开放已接入功能且不制造旧 APP 伪功能', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'profile-family-home.html'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'family-home-pages.js'), 'utf8');
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.doesNotMatch(source, /四川武胜汤氏族/);
|
||||
assert.doesNotMatch(source, /href="profile-invite\.html"/);
|
||||
assert.doesNotMatch(source, /href="profile-document/);
|
||||
assert.match(source, /PC 暂未开放邀请/);
|
||||
assert.match(source, /class="module-card is-unavailable" aria-disabled="true">\s*<span class="icon">邀<\/span>/);
|
||||
assert.match(source, /重要证件/);
|
||||
assert.match(source, /当前 PC 接口未提供家谱证件的列表、上传或维护能力/);
|
||||
assert.match(source, /<h3>删除家谱<\/h3>/);
|
||||
assert.match(source, /当前 PC 家谱契约未提供删除接口,暂不能查看或执行删除操作。/);
|
||||
assert.doesNotMatch(source, /data-genealogy-delete|deleteGenealogy/);
|
||||
assert.match(source, /亲友往来(贺礼簿)/);
|
||||
assert.match(source, /备忘录(家族恩人)/);
|
||||
assert.match(source, /当前 PC 不区分独立分类/);
|
||||
assert.match(source, /data-family-home-title/);
|
||||
assert.match(source, /data-family-home-overview/);
|
||||
assert.match(source, /data-family-home-member-count/);
|
||||
assert.match(source, /data-family-home-person-count/);
|
||||
assert.match(source, /data-family-home-management/);
|
||||
assert.match(source, /data-lineage-home-tree/);
|
||||
assert.match(source, /data-family-home-overview[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /data-lineage-home-tree[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /src="public\/js\/lineage-pages\.js"/);
|
||||
assert.match(source, /src="public\/js\/family-home-pages\.js"/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setHomeState\('\[data-family-home-overview\]', 'loading', '正在加载家谱概览…'\)/);
|
||||
assert.match(script, /setHomeState\('\[data-lineage-home-tree\]', 'loading', '正在加载世系树…'\)/);
|
||||
assert.match(script, /setHomeState\('\[data-lineage-home-tree\]', 'empty', '暂无世系树'\)/);
|
||||
assert.match(script, /treeHtml = Array\.isArray\(result\) \? root\.LineagePages\.renderLineageTreeHtml\(result\) : '';/);
|
||||
assert.match(script, /if \(!treeHtml\) \{/);
|
||||
assert.match(script, /当前账号无权查看该家谱世系树。/);
|
||||
|
||||
[
|
||||
'profile-feed.html',
|
||||
'profile-tree.html',
|
||||
'profile-generation.html',
|
||||
'profile-article.html',
|
||||
'profile-album.html',
|
||||
'profile-video.html',
|
||||
'profile-merit.html',
|
||||
'profile-ceremony.html'
|
||||
'profile-ceremony.html',
|
||||
'profile-gift.html',
|
||||
'profile-growth.html',
|
||||
'profile-relative.html',
|
||||
'profile-memo.html',
|
||||
'profile-family-admin.html',
|
||||
'profile-services.html'
|
||||
].forEach((href) => {
|
||||
assert.match(
|
||||
source,
|
||||
@@ -183,4 +212,6 @@ test('家谱主页开放真实只读概览且不制造邀请或聚合统计入
|
||||
`${href} 缺少家谱上下文`
|
||||
);
|
||||
});
|
||||
assert.match(source, /<h3>会员与订单<\/h3>/);
|
||||
assert.match(source, /关联当前家谱创建会员订单,并查看已有订单。/);
|
||||
});
|
||||
|
||||
@@ -242,6 +242,13 @@ test('家谱能力和当前用户决定编辑、管理与本人删除上下文',
|
||||
);
|
||||
});
|
||||
|
||||
test('家族圈编辑能力只接受真实布尔权限,并允许管理者维护内容', () => {
|
||||
assert.equal(FeedPages.canEditFeeds({ canEditContent: true }), true);
|
||||
assert.equal(FeedPages.canEditFeeds({ canManage: true }), true);
|
||||
assert.equal(FeedPages.canEditFeeds({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.equal(FeedPages.canEditFeeds({ canEditContent: false, canManage: 'true' }), false);
|
||||
});
|
||||
|
||||
test('家族圈页面提供真实详情、分页和上传控件,不暴露 OSS ID 或草稿伪语义', () => {
|
||||
const listPage = read('profile-feed.html');
|
||||
const editPage = read('profile-feed-edit.html');
|
||||
@@ -250,10 +257,13 @@ test('家族圈页面提供真实详情、分页和上传控件,不暴露 OSS
|
||||
|
||||
assert.match(listPage, /data-feed-page-prev/);
|
||||
assert.match(listPage, /data-feed-page-next/);
|
||||
assert.match(listPage, /data-feed-create-link[^>]+data-genealogy-context-link|data-genealogy-context-link[^>]+data-feed-create-link/);
|
||||
assert.match(listPage, /data-feed-create-link[^>]+data-genealogy-context-link[^>]+hidden|data-feed-create-link[^>]+hidden[^>]+data-genealogy-context-link/);
|
||||
assert.match(detailPage, /data-feed-detail-page/);
|
||||
assert.match(detailPage, /data-feed-detail/);
|
||||
assert.match(detailPage, /data-feed-detail-comments/);
|
||||
assert.match(detailPage, /data-feed-comment-form novalidate/);
|
||||
assert.match(detailPage, /name="commentContent"[^>]+required/);
|
||||
assert.match(detailPage, /data-feed-comment-form-status/);
|
||||
assert.match(detailPage, /data-feed-list-link[^>]+data-genealogy-context-link|data-genealogy-context-link[^>]+data-feed-list-link/);
|
||||
assert.doesNotMatch(editPage, /type="text"[^>]+name="mediaOssIds"|name="mediaOssIds"[^>]+type="text"/);
|
||||
assert.match(editPage, /name="mediaOssIds"[^>]+type="hidden"|type="hidden"[^>]+name="mediaOssIds"/);
|
||||
@@ -267,3 +277,45 @@ test('家族圈页面提供真实详情、分页和上传控件,不暴露 OSS
|
||||
assert.match(script, /genealogyDetail/);
|
||||
assert.match(script, /field\.disabled/);
|
||||
});
|
||||
|
||||
test('评论和回复使用详情页表单与统一状态,不再使用浏览器提示框', () => {
|
||||
const detailPage = read('profile-feed-detail.html');
|
||||
const script = read('public/js/feed-pages.js');
|
||||
|
||||
assert.match(detailPage, /data-feed-comment-form-target/);
|
||||
assert.match(detailPage, /data-feed-comment-cancel/);
|
||||
assert.match(detailPage, /data-feed-detail[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(detailPage, /data-feed-detail-comments[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(script, /function openFeedCommentForm\(feedId, parentCommentId\)/);
|
||||
assert.match(script, /function submitComment\(form\)/);
|
||||
assert.match(script, /function renderFeedState\(type, message\)/);
|
||||
assert.match(script, /function setReplyState\(parentCommentId, type, message\)/);
|
||||
assert.match(script, /setFeedCommentFormStatus\('正在发布评论…', 'loading'\)/);
|
||||
assert.match(script, /setFeedCommentFormEnabled\(false\)/);
|
||||
assert.match(script, /setFeedContainerState\('\[data-feed-detail-comments\]', 'loading', '正在加载评论…'\)/);
|
||||
assert.match(script, /setReplyState\(commentId, 'loading', '正在加载回复…'\)/);
|
||||
assert.match(script, /当前账号无权查看回复。/);
|
||||
assert.doesNotMatch(script, /api-empty/);
|
||||
assert.doesNotMatch(script, /root\.prompt/);
|
||||
});
|
||||
|
||||
test('动态发布表单明确必填项,并以真实家谱能力控制加载、无权限和保存失败反馈', () => {
|
||||
const editPage = read('profile-feed-edit.html');
|
||||
const script = read('public/js/feed-pages.js');
|
||||
|
||||
assert.match(editPage, /data-feed-form novalidate/);
|
||||
assert.match(editPage, /class="field-required"/);
|
||||
assert.match(editPage, /data-feed-form-status/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setFeedContainerState\('\[data-feed-list\]', 'loading'/);
|
||||
assert.match(script, /setFeedContainerState\('\[data-feed-detail\]', 'loading'/);
|
||||
assert.match(script, /'暂无家族动态'/);
|
||||
assert.match(script, /!viewerContext\.canEditContent/);
|
||||
assert.match(script, /setFeedFormEnabled\(false\)/);
|
||||
assert.match(script, /setFeedSubmitPending\(true\)/);
|
||||
assert.match(script, /正在保存…/);
|
||||
assert.match(script, /function canEditFeeds\(genealogy\)/);
|
||||
assert.match(script, /setFeedContentEditingEnabled\(viewerContext\.canEditContent\)/);
|
||||
assert.match(script, /if \(!viewerContext\.canEditContent\) \{\s*setFeedFormStatus\('当前账号无权发布或编辑该家谱的动态。', 'forbidden'\)/);
|
||||
assert.match(editPage, /data-feed-editor-action hidden disabled/);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ function feedbackFixture(overrides) {
|
||||
appUserPhone: '19100000000',
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173',
|
||||
contactInfo: '测试联系方式',
|
||||
handleStatus: '2',
|
||||
handleResult: '已经修复',
|
||||
handlerId: '2062179707935264702',
|
||||
@@ -33,14 +33,14 @@ test('反馈请求只构造 AppFeedbackBody 并裁剪文本', () => {
|
||||
assert.deepEqual(FeedbackPages.buildFeedbackBody({
|
||||
feedbackType: ' bug ',
|
||||
feedbackContent: ' 页面按钮无响应 ',
|
||||
contactInfo: ' 19181970173 ',
|
||||
contactInfo: ' 测试联系方式 ',
|
||||
feedbackTitle: 'must-drop',
|
||||
appUserId: 'must-drop',
|
||||
handlerId: 'must-drop'
|
||||
}), {
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173'
|
||||
contactInfo: '测试联系方式'
|
||||
});
|
||||
assert.deepEqual(FeedbackPages.buildFeedbackBody({
|
||||
feedbackType: '',
|
||||
@@ -69,7 +69,7 @@ test('FeedbackVo 只保留用户可见字段并保持长 ID 字符串', () => {
|
||||
feedbackId,
|
||||
feedbackType: 'bug',
|
||||
feedbackContent: '页面按钮无响应',
|
||||
contactInfo: '19181970173',
|
||||
contactInfo: '测试联系方式',
|
||||
handleStatus: '2',
|
||||
handleResult: '已经修复',
|
||||
handleTime: '2026-07-29 13:00:00',
|
||||
@@ -137,10 +137,12 @@ test('两个提交页只提供 AppFeedbackBody 字段并加载真实脚本', ()
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(source, /data-feedback-form/);
|
||||
assert.match(source, /data-feedback-form novalidate/);
|
||||
assert.match(source, /name="feedbackType"/);
|
||||
assert.match(source, /name="feedbackContent"/);
|
||||
assert.match(source, /name="contactInfo"/);
|
||||
assert.match(source, /public\/js\/feedback-pages\.js/);
|
||||
assert.match(source, /role="status"[^>]*data-feedback-status/);
|
||||
assert.doesNotMatch(source, /name="(?:feedbackTitle|feedbackId|appUserId|handlerId)"/);
|
||||
});
|
||||
});
|
||||
@@ -154,8 +156,10 @@ test('我的工单和详情页读取同一反馈集合且不制造处理动作',
|
||||
assert.match(source, /public\/js\/feedback-pages\.js/);
|
||||
});
|
||||
assert.match(listPage, /data-feedback-list/);
|
||||
assert.match(listPage, /data-api-state="loading"/);
|
||||
assert.match(listPage, /data-feedback-refresh/);
|
||||
assert.match(detailPage, /data-feedback-detail/);
|
||||
assert.match(detailPage, /data-api-state="loading"/);
|
||||
assert.doesNotMatch(detailPage, /回复|追问|删除|关闭工单|name="feedbackId"/);
|
||||
});
|
||||
|
||||
@@ -167,3 +171,24 @@ test('反馈运行时提交后重读同一 ID,详情不存在时不回退', ()
|
||||
assert.match(source, /提交后无法读取同一反馈记录/);
|
||||
assert.match(source, /反馈记录不存在或无权查看/);
|
||||
});
|
||||
|
||||
test('反馈请求的 403 显式归类为无权限,而不是登录失效', () => {
|
||||
assert.equal(FeedbackPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(FeedbackPages.isForbidden({ code: '403' }), true);
|
||||
assert.equal(FeedbackPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('反馈表单与列表使用可见标签和统一加载、空数据、失败状态', () => {
|
||||
const ticket = read('submit-ticket.html');
|
||||
const script = read('public/js/feedback-pages.js');
|
||||
|
||||
assert.match(ticket, /<label for="ticketContactInfo">联系方式(可选)<\/label>/);
|
||||
assert.match(ticket, /<label for="ticketFeedbackType">反馈类型<\/label>/);
|
||||
assert.match(ticket, /<label for="ticketFeedbackContent">问题描述/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setApiState\(list, 'loading', '正在读取反馈记录…'\)/);
|
||||
assert.match(script, /setApiState\(list, 'empty', '当前没有反馈记录。'\)/);
|
||||
assert.match(script, /setApiState\(detail, isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(script, /setPendingButton\(button, true, '正在提交…'\)/);
|
||||
assert.match(script, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
|
||||
test('all visible form controls have a label or accessible name', () => {
|
||||
const pages = fs.readdirSync(projectRoot).filter((name) => name.endsWith('.html'));
|
||||
|
||||
pages.forEach((page) => {
|
||||
const source = fs.readFileSync(path.join(projectRoot, page), 'utf8');
|
||||
|
||||
Array.from(source.matchAll(/<form\b[\s\S]*?<\/form>/gi)).forEach((form) => {
|
||||
const formHtml = form[0];
|
||||
|
||||
Array.from(formHtml.matchAll(/<(input|select|textarea)\b[^>]*>/gi)).forEach((control) => {
|
||||
const tag = control[0];
|
||||
const type = (tag.match(/\btype=["']([^"']+)/i) || [, ''])[1].toLowerCase();
|
||||
const id = (tag.match(/\bid=["']([^"']+)/i) || [, ''])[1];
|
||||
const name = (tag.match(/\bname=["']([^"']+)/i) || [, ''])[1] || 'unnamed control';
|
||||
const before = formHtml.slice(0, control.index);
|
||||
const hasExplicitLabel = Boolean(id) && (
|
||||
formHtml.includes('for="' + id + '"') || formHtml.includes("for='" + id + "'")
|
||||
);
|
||||
const hasImplicitLabel = before.lastIndexOf('<label') > before.lastIndexOf('</label');
|
||||
const hasAccessibleName = /\baria-label(?:ledby)?=["'][^"']+/i.test(tag);
|
||||
|
||||
if (['hidden', 'submit', 'button', 'reset'].includes(type) || /\bhidden\b/i.test(tag)) return;
|
||||
assert.ok(
|
||||
hasExplicitLabel || hasImplicitLabel || hasAccessibleName,
|
||||
page + ' control ' + name + ' has no label or accessible name'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,10 @@ function read(relativePath) {
|
||||
|
||||
test('家谱入口只接受本地家谱业务页作为返回目标', () => {
|
||||
assert.equal(GenealogyEntryPages.getTargetPage('?next=profile-feed.html'), 'profile-feed.html');
|
||||
assert.equal(
|
||||
GenealogyEntryPages.getTargetPage('?next=' + encodeURIComponent('profile-feed-detail.html?feedId=12#comments')),
|
||||
'profile-feed-detail.html?feedId=12#comments'
|
||||
);
|
||||
assert.equal(GenealogyEntryPages.getTargetPage('?next=https%3A%2F%2Fevil.example'), 'profile-family-home.html');
|
||||
});
|
||||
|
||||
@@ -39,6 +43,10 @@ test('家谱入口只使用 AppGenealogyVo 的真实字段并保持大整数 ID
|
||||
GenealogyEntryPages.buildEntryUrl('2062179707935264769', 'profile-feed.html'),
|
||||
'profile-feed.html?genealogyId=2062179707935264769'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.buildEntryUrl('2062179707935264769', 'profile-feed-detail.html?feedId=12&genealogyId=old#comments'),
|
||||
'profile-feed-detail.html?feedId=12&genealogyId=2062179707935264769#comments'
|
||||
);
|
||||
});
|
||||
|
||||
test('家谱创建只构造 AppGenealogyCreateBody 并省略空可选字段', () => {
|
||||
@@ -165,11 +173,30 @@ test('负数额度不向用户展示为可用数量', () => {
|
||||
|
||||
test('我的家谱页是可用的真实列表入口', () => {
|
||||
const source = read('profile-families.html');
|
||||
const script = read('public/js/genealogy-entry-pages.js');
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"/);
|
||||
assert.doesNotMatch(source, /pending-pages\.js/);
|
||||
assert.match(source, /data-genealogy-list="mine"/);
|
||||
assert.match(source, /data-genealogy-list="mine"[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(source, /genealogy-entry-pages\.js/);
|
||||
assert.match(script, /function setGenealogyListState\(container, type, message\)/);
|
||||
assert.match(script, /setGenealogyListState\(container, 'loading', '正在读取我的家谱…'\)/);
|
||||
assert.match(script, /setGenealogyListState\(container, 'empty', '当前账号还没有可进入的家谱'\)/);
|
||||
assert.match(script, /当前账号无权查看我的家谱。/);
|
||||
assert.match(script, /result = await api\.genealogiesMine\(\);/);
|
||||
assert.match(script, /result = await api\.genealogyQuota\(\);/);
|
||||
});
|
||||
|
||||
test('家谱排序在未声明 PC 读取和保存契约时保持明确不可操作状态', () => {
|
||||
const source = read('profile-families.html');
|
||||
const apiClient = read('utils/ApiClient.js');
|
||||
const verification = read('docs/验证20%记录.md');
|
||||
|
||||
assert.match(source, /class="module-card is-unavailable" aria-disabled="true">\s*<span class="icon">序<\/span>\s*<h3>家谱排序<\/h3>/);
|
||||
assert.match(source, /当前 PC 家谱契约未提供自定义排序的读取或保存接口/);
|
||||
assert.doesNotMatch(apiClient, /sortGenealogy|genealogySort/);
|
||||
assert.match(verification, /家谱邀请、家谱删除、家谱排序、分享奖励/);
|
||||
});
|
||||
|
||||
test('创建家谱页开放真实 PC 表单且不允许手填业务 ID', () => {
|
||||
@@ -178,8 +205,17 @@ test('创建家谱页开放真实 PC 表单且不允许手填业务 ID', () => {
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"/);
|
||||
assert.doesNotMatch(source, /pending-pages\.js/);
|
||||
assert.match(source, /data-genealogy-create-page/);
|
||||
assert.match(source, /data-genealogy-create-quota/);
|
||||
assert.match(source, /data-genealogy-create-quota role="status" aria-live="polite"/);
|
||||
assert.match(source, /data-genealogy-create-status/);
|
||||
assert.match(source, /data-genealogy-create-form novalidate/);
|
||||
assert.match(source, /class="field-required"/);
|
||||
assert.match(source, /role="group" aria-labelledby="family-region-label" aria-describedby="family-create-status"/);
|
||||
assert.match(source, /<label for="familyProvinceCode">省/);
|
||||
assert.match(source, /<label for="familyCityCode">市/);
|
||||
assert.match(source, /<label for="familyDistrictCode">区县/);
|
||||
assert.match(source, /data-region-picker-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /<div id="family-create-status" class="upload-status" data-genealogy-create-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /<p id="familyCoverStatus" class="upload-status" role="status" aria-live="polite">/);
|
||||
assert.match(source, /name="coverOssId"\s+type="hidden"/);
|
||||
assert.match(source, /data-upload-target="#familyCoverOssId"/);
|
||||
assert.match(source, /public\/js\/region-pages\.js/);
|
||||
@@ -190,6 +226,50 @@ test('创建家谱页开放真实 PC 表单且不允许手填业务 ID', () => {
|
||||
assert.doesNotMatch(source, /name="coverOssId"[^>]*type="text"/);
|
||||
});
|
||||
|
||||
test('公共创建入口复用真实 PC 创建表单,不保留无效的静态提交', () => {
|
||||
const source = read('create-genealogy.html');
|
||||
|
||||
assert.match(source, /data-genealogy-create-page/);
|
||||
assert.match(source, /data-genealogy-create-form novalidate aria-describedby="public-create-status"/);
|
||||
assert.match(source, /data-genealogy-create-quota role="status" aria-live="polite"/);
|
||||
assert.match(source, /data-genealogy-create-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /<label class="field" for="publicFamilyName">谱名/);
|
||||
assert.match(source, /<label class="field" for="publicFamilySurname">主要姓氏/);
|
||||
assert.match(source, /name="(?:ancestralHall|originPlace|addressDetail|intro|visibility|joinMode)"/);
|
||||
assert.match(source, /data-region-picker-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /id="publicJoinMode"[^>]*aria-describedby="public-invite-mode-note"/);
|
||||
assert.match(source, /当前 PC 未提供邀请码生成、发送或管理接口/);
|
||||
assert.match(source, /data-genealogy-create-submit/);
|
||||
assert.match(source, /public\/js\/profile-common\.js/);
|
||||
assert.match(source, /public\/js\/region-pages\.js/);
|
||||
assert.match(source, /public\/js\/genealogy-entry-pages\.js/);
|
||||
assert.doesNotMatch(source, /data-genealogy-form="create"|data-success-url/);
|
||||
assert.doesNotMatch(source, /name="(?:genealogyId|ownerUserId|coverOssId)"/);
|
||||
});
|
||||
|
||||
test('创建页保留合法邀请模式,但不把缺失的邀请码管理伪装成已开放能力', () => {
|
||||
const profileCreate = read('profile-create-family.html');
|
||||
const publicCreate = read('create-genealogy.html');
|
||||
|
||||
[profileCreate, publicCreate].forEach((source) => {
|
||||
assert.match(source, /<option value="2">邀请码加入<\/option>/);
|
||||
assert.match(source, /当前 PC 未提供邀请码生成、发送或管理接口/);
|
||||
});
|
||||
assert.match(profileCreate, /id="joinMode"[^>]*aria-describedby="family-invite-mode-note"/);
|
||||
assert.match(publicCreate, /id="publicJoinMode"[^>]*aria-describedby="public-invite-mode-note"/);
|
||||
});
|
||||
|
||||
test('创建家谱把加载、额度不足、校验和请求失败写入统一状态容器', () => {
|
||||
const source = read('public/js/genealogy-entry-pages.js');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /正在读取创建额度…', 'loading'/);
|
||||
assert.match(source, /当前没有可用的家谱创建额度', 'empty'/);
|
||||
assert.match(source, /setCreateStatus\(validation, 'error'\)/);
|
||||
assert.match(source, /getApiStateType\(error\)/);
|
||||
assert.match(source, /正在创建…/);
|
||||
});
|
||||
|
||||
test('加入家谱页面不允许用户手工填写 genealogyId', () => {
|
||||
const source = read('join-genealogy.html');
|
||||
|
||||
@@ -244,7 +324,7 @@ test('当前家谱名称从真实家谱响应写入会话上下文,并在页
|
||||
assert.match(common, /href="profile-families\.html"/);
|
||||
assert.match(entry, /data-genealogy-name=/);
|
||||
assert.match(entry, /root\.ProfileUI\.setCurrentGenealogyContext\(verified\.genealogyId, verified\.genealogyName\)/);
|
||||
assert.match(familyHome, /root\.ProfileUI\.setCurrentGenealogyContext\(result\.overview\.genealogyId, result\.overview\.genealogyName\)/);
|
||||
assert.match(familyHome, /root\.ProfileUI\.setCurrentGenealogyContext\(result\.genealogyId, result\.genealogyName\)/);
|
||||
});
|
||||
|
||||
test('小屏导航提供统一入口,并且所有家谱内链接继续透传当前上下文', () => {
|
||||
|
||||
@@ -122,6 +122,12 @@ test('家谱内容权限决定读取正常列表还是包含停用项的管理
|
||||
canEditContent: true,
|
||||
listMethod: 'generationPoemsManagement'
|
||||
});
|
||||
assert.deepEqual(GenerationPages.normalizeGenerationAccess({ canManage: true }), {
|
||||
canEditContent: true,
|
||||
listMethod: 'generationPoemsManagement'
|
||||
});
|
||||
assert.equal(GenerationPages.canEditGenerations({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.equal(GenerationPages.canEditGenerations({ canEditContent: false, canManage: 'true' }), false);
|
||||
});
|
||||
|
||||
test('批量预览逐项校验动作、状态、ID 与汇总计数', () => {
|
||||
@@ -195,15 +201,26 @@ test('停用缺失世代的批量保存确认文案明确不会删除历史', ()
|
||||
assert.equal(GenerationPages.buildBatchConfirmMessage(false), '确认按当前预览保存字辈吗?');
|
||||
});
|
||||
|
||||
test('字辈批量示例明确使用 Apifox 支持的分隔符', () => {
|
||||
test('字辈管理提供页内完整表单、统一状态和严格权限,而非浏览器提示框', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-generation.html'), 'utf8');
|
||||
const styles = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'profile-module.css'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'generation-pages.js'), 'utf8');
|
||||
|
||||
assert.match(page, /placeholder="例如:德 承 家 亦(用空格或标点分隔)"/);
|
||||
assert.doesNotMatch(page, /placeholder="例如:德承家亦"/);
|
||||
assert.match(page, /data-generation-management[^>]+hidden/);
|
||||
assert.match(page, /data-generation-add[^>]+disabled|disabled[^>]+data-generation-add/);
|
||||
assert.match(styles, /\[data-generation-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(page, /data-generation-form novalidate/);
|
||||
assert.match(page, /name="generationNo"[^>]+required/);
|
||||
assert.match(page, /name="generationText"[^>]+required/);
|
||||
assert.match(page, /data-generation-form-status/);
|
||||
assert.match(page, /data-generation-editor-submit/);
|
||||
assert.match(script, /function openPoemEditor\(poemId\)/);
|
||||
assert.match(script, /function submitPoem\(form\)/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /if \(writePending \|\| !managementAccess \|\| redirectUnauthorized\(api\) \|\| !form\) return;/);
|
||||
assert.doesNotMatch(script, /root\.prompt/);
|
||||
});
|
||||
|
||||
test('字辈管理接口的 403 不是登录失效', () => {
|
||||
|
||||
@@ -180,7 +180,7 @@ test('世系人物选择器只使用稳定人物编号和名称', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('成长记录列表和编辑页开放 PC CRUD 且不允许手填人物或 OSS ID', () => {
|
||||
test('成长记录列表和编辑页按家谱能力开放 PC CRUD,且不允许手填人物或 OSS ID', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-growth.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(root, 'profile-growth-edit.html'), 'utf8');
|
||||
@@ -188,13 +188,43 @@ test('成长记录列表和编辑页开放 PC CRUD 且不允许手填人物或 O
|
||||
|
||||
assert.match(listPage, /data-growth-list/);
|
||||
assert.match(listPage, /data-growth-detail/);
|
||||
assert.match(listPage, /data-growth-create-link hidden/);
|
||||
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
|
||||
assert.match(editPage, /name="lineagePersonId"[^>]*data-growth-lineage-person/);
|
||||
assert.match(editPage, /data-growth-form[^>]*novalidate/);
|
||||
assert.match(editPage, /for="growthTitle">标题<span class="field-required"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
|
||||
assert.match(editPage, /data-growth-form-status role="status" aria-live="polite"/);
|
||||
assert.match(editPage, /data-growth-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
|
||||
assert.doesNotMatch(editPage, /name="(?:lineagePersonId|mediaOssIds)"[^>]*type="text"/);
|
||||
assert.match(editPage, /public\/js\/md5\.js/);
|
||||
assert.match(editPage, /public\/js\/upload-pages\.js/);
|
||||
assert.match(editPage, /public\/js\/growth-pages\.js/);
|
||||
assert.match(styles, /\[data-growth-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('成长记录只向内容维护者或管理员开放写操作', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'growth-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(GrowthPages.canEditRecords({ canEditContent: true }), true);
|
||||
assert.equal(GrowthPages.canEditRecords({ canManage: true }), true);
|
||||
assert.equal(GrowthPages.canEditRecords({ canEditContent: false, canManage: false }), false);
|
||||
assert.match(script, /await loadCapability\(api, genealogyId\);/);
|
||||
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
||||
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-growth-delete-id/);
|
||||
});
|
||||
|
||||
test('成长记录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'growth-pages.js'), 'utf8');
|
||||
|
||||
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(script, /setState\(query\('\[data-growth-list\]'\), 'loading', '正在加载成长记录…'\)/);
|
||||
assert.match(script, /setState\(query\('\[data-growth-detail\]'\), 'loading', '正在加载成长记录详情…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存成长记录…', 'loading'\)/);
|
||||
});
|
||||
|
||||
@@ -103,6 +103,8 @@ test('帮助中心只把缺失登录态和 401 视为登录失效', () => {
|
||||
assert.equal(HelpPages.shouldRedirectToLogin({ getToken() { return ''; } }), true);
|
||||
assert.equal(HelpPages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 401 }), true);
|
||||
assert.equal(HelpPages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false);
|
||||
assert.equal(HelpPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(HelpPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('帮助中心使用真实 PC 列表和详情,不再展示硬编码问答', () => {
|
||||
@@ -110,7 +112,21 @@ test('帮助中心使用真实 PC 列表和详情,不再展示硬编码问答'
|
||||
|
||||
assert.match(page, /data-help-list/);
|
||||
assert.match(page, /data-help-status/);
|
||||
assert.match(page, /data-help-status[^>]*role="status"/);
|
||||
assert.match(page, /data-api-state="loading"/);
|
||||
assert.match(page, /public\/js\/help-pages\.js/);
|
||||
assert.match(page, /public\/js\/profile-common\.js/);
|
||||
assert.doesNotMatch(page, /如何创建第一本家谱|如何邀请亲人一起维护/);
|
||||
assert.doesNotMatch(page, /data-feature-status="pending"|pending-pages\.js/);
|
||||
});
|
||||
|
||||
test('帮助中心将加载、空数据、失败和无权限写入统一状态组件', () => {
|
||||
const source = read('public/js/help-pages.js');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setApiState\(list, 'loading', '正在读取帮助文章…'\)/);
|
||||
assert.match(source, /setApiState\(list, 'empty', '当前暂无帮助文章。'\)/);
|
||||
assert.match(source, /setApiState\(panel, 'loading', '正在读取完整解答…'\)/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(source, /setPendingButton\(refresh, true, '正在刷新…'\)/);
|
||||
});
|
||||
|
||||
@@ -15,14 +15,14 @@ function read(relativePath) {
|
||||
test('加入申请只构造 AppGenealogyJoinApplyBody 并裁剪可选文本', () => {
|
||||
assert.deepEqual(JoinPages.buildJoinApplyBody({
|
||||
applicantName: ' 叶子 ',
|
||||
phone: ' 19181970173 ',
|
||||
phone: ' 测试联系方式 ',
|
||||
relationDesc: ' 族亲 ',
|
||||
applyReason: ' 申请加入 ',
|
||||
inviterUserId: 'must-drop',
|
||||
genealogyId: 'must-drop'
|
||||
}), {
|
||||
applicantName: '叶子',
|
||||
phone: '19181970173',
|
||||
phone: '测试联系方式',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入'
|
||||
});
|
||||
@@ -156,17 +156,43 @@ test('申请加入页由后端家谱选项驱动且不允许手填业务 ID 或
|
||||
|
||||
assert.match(source, /data-join-apply-page/);
|
||||
assert.match(source, /data-join-genealogy-search/);
|
||||
assert.match(source, /data-join-genealogy-search novalidate aria-describedby="join-options-status"/);
|
||||
assert.match(source, /data-join-genealogy-options/);
|
||||
assert.match(source, /data-join-options-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /data-join-selected-genealogy role="status" aria-live="polite"/);
|
||||
assert.match(source, /data-join-apply-form/);
|
||||
assert.match(source, /data-join-apply-form novalidate aria-describedby="join-apply-status"/);
|
||||
assert.match(source, /name="applicantName"/);
|
||||
assert.match(source, /name="phone"/);
|
||||
assert.match(source, /name="relationDesc"/);
|
||||
assert.match(source, /name="applyReason"/);
|
||||
assert.match(source, /class="join-page-grid"/);
|
||||
assert.match(source, /class="join-search-panel"/);
|
||||
assert.match(source, /class="[^"]*join-apply-form/);
|
||||
assert.match(source, /<div id="join-apply-status" class="form-note" data-join-apply-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /public\/js\/profile-common\.js/);
|
||||
assert.match(source, /public\/js\/join-pages\.js/);
|
||||
assert.doesNotMatch(source, /name="(?:genealogyId|applyId|inviterUserId|inviteCode)"/);
|
||||
assert.doesNotMatch(source, /邀请码|分享码|手工.*ID|输入.*ID/);
|
||||
});
|
||||
|
||||
test('加入申请及申请列表复用统一状态,并在提交期间替换按钮反馈', () => {
|
||||
const source = read('public/js/join-pages.js');
|
||||
const profilePage = read('profile-join-family.html');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.renderApiState/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /function setOptionsState\(options, type, message\)/);
|
||||
assert.match(source, /function clearSelectedGenealogy\(\)/);
|
||||
assert.match(source, /clearSelectedGenealogy\(\);\s+setText\('\[data-join-options-status\]'/);
|
||||
assert.match(source, /setOptionsState\(options, 'loading', '正在读取可申请家谱…'\)/);
|
||||
assert.match(source, /\[data-join-options-status\]/);
|
||||
assert.match(source, /getApiStateType\(error\)/);
|
||||
assert.match(source, /setSubmitPending\(/);
|
||||
assert.match(source, /正在提交…/);
|
||||
assert.match(profilePage, /<div class="form-note" role="status" aria-live="polite" data-my-join-applies-status/);
|
||||
});
|
||||
|
||||
test('我的加入申请页加载真实列表和刷新入口', () => {
|
||||
const source = read('profile-join-family.html');
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ test('待审核列表隐藏内部 ID 和账号手机号,只有管理者可见
|
||||
genealogyId,
|
||||
genealogyName: '叶氏家谱',
|
||||
applicantName: '<叶子>',
|
||||
phone: '19181970173',
|
||||
phone: '测试联系方式',
|
||||
relationDesc: '族亲',
|
||||
applyReason: '申请加入',
|
||||
status: '0',
|
||||
@@ -56,7 +56,7 @@ test('待审核列表隐藏内部 ID 和账号手机号,只有管理者可见
|
||||
const memberHtml = JoinReviewPages.renderPendingJoinApplies(data, false);
|
||||
|
||||
assert.match(managerHtml, /<叶子>/);
|
||||
assert.match(managerHtml, /19181970173/);
|
||||
assert.match(managerHtml, /测试联系方式/);
|
||||
assert.match(managerHtml, new RegExp('data-join-audit-id="' + applyId + '"'));
|
||||
assert.match(managerHtml, /data-join-audit-status="1"/);
|
||||
assert.match(managerHtml, /data-join-audit-status="2"/);
|
||||
@@ -71,12 +71,25 @@ test('审核页开放真实 PC 管理行为且不允许手填业务 ID', () => {
|
||||
assert.doesNotMatch(page, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(page, /data-join-review-page/);
|
||||
assert.match(page, /data-join-apply-list="pending"/);
|
||||
assert.match(page, /data-join-apply-list="pending"[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(page, /data-join-review-refresh/);
|
||||
assert.match(page, /public\/js\/join-review-pages\.js/);
|
||||
assert.doesNotMatch(page, /name="(?:genealogyId|applyId|appUserId|inviterUserId|auditUserId)"/);
|
||||
assert.match(page, /input name="applyId" type="hidden"/);
|
||||
assert.doesNotMatch(page, /(?:input|textarea|select)[^>]+name="applyId"[^>]+type="(?:text|number)"/);
|
||||
assert.doesNotMatch(page, /name="(?:genealogyId|appUserId|inviterUserId|auditUserId)"/);
|
||||
assert.match(page, /data-join-review-form novalidate/);
|
||||
assert.match(page, /name="auditRemark"[^>]+maxlength="500"/);
|
||||
assert.match(page, /data-join-review-form-status/);
|
||||
assert.match(script, /await api\.genealogyDetail\(/);
|
||||
assert.match(script, /detail\.canManage === true/);
|
||||
assert.match(script, /await api\.auditGenealogyJoinApply\(/);
|
||||
assert.match(script, /await api\.pendingGenealogyJoinApplies\(/);
|
||||
assert.match(script, /审核后申请仍在待审核列表/);
|
||||
assert.match(script, /function openReviewForm\(applyId, status\)/);
|
||||
assert.match(script, /setReviewFormStatus\('正在提交审核结果…', 'loading'\)/);
|
||||
assert.match(script, /function setPendingListState\(message, type\)/);
|
||||
assert.match(script, /setPendingListState\('当前没有待审核申请', 'empty'\)/);
|
||||
assert.match(script, /当前账号没有审核该家谱申请的权限。/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.doesNotMatch(script, /root\.prompt/);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ test('世系管理页和家谱主页复用同一棵安全树的 HTML', () => {
|
||||
assert.match(html, /<始祖>/);
|
||||
assert.match(html, /data-lineage-person="2062179707935264770"/);
|
||||
assert.doesNotMatch(html, /2062179707935264769/);
|
||||
assert.match(LineagePages.renderLineageTreeHtml([]), /暂无世系树/);
|
||||
assert.equal(LineagePages.renderLineageTreeHtml([]), '');
|
||||
});
|
||||
|
||||
test('世系人物表单只构造 Apifox LineagePersonBody 字段', () => {
|
||||
@@ -301,6 +301,7 @@ test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => {
|
||||
pageSize: 20
|
||||
});
|
||||
assert.match(page, /placeholder="输入姓名、别名或人物编号搜索"/);
|
||||
assert.match(page, /data-lineage-keyword[^>]+aria-label="按姓名、别名或人物编号搜索成员"/);
|
||||
assert.match(page, /data-lineage-generation-filter/);
|
||||
assert.match(page, /data-lineage-status-filter/);
|
||||
assert.match(page, /data-lineage-pagination/);
|
||||
@@ -311,6 +312,7 @@ test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => {
|
||||
test('世系表单为全部 LineagePersonBody 字段提供真实来源且不允许手填业务 ID', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
|
||||
const styles = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'profile-module.css'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'lineage-pages.js'), 'utf8');
|
||||
|
||||
[
|
||||
'bindingMode', 'appUserId', 'avatarOssId', 'personNo', 'name', 'aliasName', 'sex',
|
||||
@@ -319,20 +321,33 @@ test('世系表单为全部 LineagePersonBody 字段提供真实来源且不允
|
||||
'biography', 'sortOrder', 'remark', 'relationName'
|
||||
].forEach((name) => assert.match(page, new RegExp('name="' + name + '"')));
|
||||
assert.match(page, /value="SPECIFIED"(?![^>]+disabled)/);
|
||||
assert.match(page, /data-lineage-specified-member hidden/);
|
||||
assert.match(page, /<label for="lineage-specified-member">指定账号<\/label>/);
|
||||
assert.match(page, /select[^>]+name="appUserId"[^>]+data-lineage-member-options|select[^>]+data-lineage-member-options[^>]+name="appUserId"/);
|
||||
assert.doesNotMatch(page, /name="appUserId"[^>]+type="(?:text|number)"|type="(?:text|number)"[^>]+name="appUserId"/);
|
||||
assert.doesNotMatch(page, /name="appUserId"[^>]+type="hidden"|type="hidden"[^>]+name="appUserId"/);
|
||||
assert.match(page, /name="avatarOssId"[^>]+type="hidden"|type="hidden"[^>]+name="avatarOssId"/);
|
||||
assert.match(page, /data-upload-target="#lineage-avatar-oss-id"/);
|
||||
assert.match(script, /specifiedMember\.hidden = !specified/);
|
||||
assert.match(page, /name="generation"[^>]*data-lineage-generation/);
|
||||
assert.match(page, /name="generationName"[^>]+readonly|readonly[^>]+name="generationName"/);
|
||||
assert.match(page, /name="fatherId"[^>]*data-lineage-parent-option/);
|
||||
assert.match(page, /name="motherId"[^>]*data-lineage-parent-option/);
|
||||
assert.match(page, /data-lineage-management[^>]+hidden/);
|
||||
assert.match(page, /data-lineage-form novalidate/);
|
||||
assert.match(page, /data-lineage-submit/);
|
||||
assert.match(page, /data-lineage-form-status/);
|
||||
assert.match(page, /data-lineage-action-status/);
|
||||
assert.match(page, /public\/js\/upload-pages\.js/);
|
||||
assert.match(styles, /\[data-lineage-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(styles, /\[data-lineage-spouse-field\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(styles, /\[data-lineage-pagination\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
assert.match(script, /function setLineageState\(selector, type, message\)/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setLineageState\('\[data-lineage-list\]', 'loading', '正在加载世系人物…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存成员…', 'loading'\)/);
|
||||
assert.match(script, /setActionStatus\('正在停用成员…', 'loading'\)/);
|
||||
assert.match(script, /function setLineageSubmitLabel\(label\)/);
|
||||
});
|
||||
|
||||
test('世系管理的 403 不会被当作登录失效', () => {
|
||||
|
||||
@@ -70,6 +70,7 @@ test('成员修改只构造后端可更新字段并收紧角色枚举', () => {
|
||||
roleType: 'editor',
|
||||
lineagePersonId: '2060000000000000004'
|
||||
});
|
||||
assert.deepEqual(buildMemberUpdateBody({ relationName: ' ' }), {});
|
||||
assert.equal(validateMemberUpdateBody(buildMemberUpdateBody({ memberName: '名'.repeat(51) })), '成员名称不能超过 50 个字符');
|
||||
assert.equal(validateMemberUpdateBody(buildMemberUpdateBody({ relationName: '关系'.repeat(51) })), '关系名称不能超过 100 个字符');
|
||||
assert.throws(
|
||||
@@ -96,8 +97,10 @@ test('成员管理动作遵守谱主和管理员的后端权限边界', () => {
|
||||
assert.equal(canRemoveMember('admin', 'admin'), false);
|
||||
assert.equal(canLeaveGenealogy('owner'), false);
|
||||
assert.equal(canLeaveGenealogy('member'), true);
|
||||
assert.equal(canTransferOwner('owner'), true);
|
||||
assert.equal(canTransferOwner('admin'), false);
|
||||
assert.equal(canTransferOwner('owner', true), true);
|
||||
assert.equal(canTransferOwner('owner', false), false);
|
||||
assert.equal(canTransferOwner('owner', 'true'), false);
|
||||
assert.equal(canTransferOwner('admin', true), false);
|
||||
});
|
||||
|
||||
test('谱主转移只使用成员列表中的真实目标 memberId', () => {
|
||||
@@ -160,17 +163,32 @@ test('成员列表展示转义内容并隐藏手机号与内部 ID', () => {
|
||||
|
||||
test('家族管理页开放成员维护、退出和谱主转移且不允许手填 ID', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-family-admin.html'), 'utf8');
|
||||
const script = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.doesNotMatch(page, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(page, /public\/js\/member-admin-pages\.js/);
|
||||
assert.match(page, /data-member-list/);
|
||||
assert.match(page, /data-member-form/);
|
||||
assert.match(page, /data-member-form novalidate/);
|
||||
assert.match(page, /name="memberName"/);
|
||||
assert.match(page, /name="relationName"/);
|
||||
assert.match(page, /name="roleType"/);
|
||||
assert.match(page, /select[^>]+name="lineagePersonId"|name="lineagePersonId"[^>]+select/);
|
||||
assert.match(page, /data-member-leave/);
|
||||
assert.match(page, /data-member-action-status/);
|
||||
assert.match(page, /select[^>]+name="targetMemberId"|name="targetMemberId"[^>]+select/);
|
||||
assert.match(page, /data-owner-transfer-form novalidate/);
|
||||
assert.match(page, /name="targetMemberId"[^>]+required/);
|
||||
assert.match(page, /data-member-form-status/);
|
||||
assert.match(page, /空值清除语义/);
|
||||
assert.match(page, /data-owner-transfer-status/);
|
||||
assert.doesNotMatch(page, /name="(?:memberId|appUserId)"|type="(?:text|number)"[^>]+name="(?:lineagePersonId|targetMemberId)"/);
|
||||
assert.doesNotMatch(page, /href="profile-data\.html"[^>]*>\s*添加成员/);
|
||||
assert.match(script, /function setMemberListState\(type, message\)/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /setMemberListState\('loading', '正在加载成员…'\)/);
|
||||
assert.match(script, /setStatus\('\[data-member-form-status\]', '正在保存成员…', 'loading'\)/);
|
||||
assert.match(script, /setStatus\('\[data-owner-transfer-status\]', '正在转让谱主…', 'loading'\)/);
|
||||
assert.match(script, /setStatus\('\[data-member-action-status\]', '正在移出成员…', 'loading'\)/);
|
||||
assert.match(script, /canTransferOwner\(actorRole, managementAccess\)/);
|
||||
});
|
||||
|
||||
@@ -160,7 +160,7 @@ test('备忘录编辑器拒绝不可重读的停用记录', () => {
|
||||
assert.equal(MemoPages.isEditableMemo(null), false);
|
||||
});
|
||||
|
||||
test('备忘录列表和编辑页开放 PC CRUD 且不允许手填 OSS ID 或业务状态', () => {
|
||||
test('备忘录列表和编辑页按家谱能力开放 PC CRUD,且不允许手填 OSS ID 或业务状态', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-memo.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(root, 'profile-memo-edit.html'), 'utf8');
|
||||
@@ -168,17 +168,47 @@ test('备忘录列表和编辑页开放 PC CRUD 且不允许手填 OSS ID 或业
|
||||
|
||||
assert.match(listPage, /data-memo-list/);
|
||||
assert.match(listPage, /data-memo-detail/);
|
||||
assert.match(listPage, /data-memo-create-link hidden/);
|
||||
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
|
||||
assert.match(editPage, /name="remindTime" type="datetime-local"/);
|
||||
assert.match(editPage, /data-memo-form[^>]*novalidate/);
|
||||
assert.match(editPage, /for="memoTitle">备忘标题<span class="field-required"/);
|
||||
assert.match(editPage, /name="completed"/);
|
||||
assert.match(editPage, /value="0"/);
|
||||
assert.match(editPage, /value="1"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
|
||||
assert.match(editPage, /data-memo-clear-media/);
|
||||
assert.match(editPage, /data-memo-form-status role="status" aria-live="polite"/);
|
||||
assert.match(editPage, /data-memo-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
|
||||
assert.doesNotMatch(editPage, /name="(?:mediaOssIds|status)"[^>]*type="text"/);
|
||||
assert.match(editPage, /public\/js\/md5\.js/);
|
||||
assert.match(editPage, /public\/js\/upload-pages\.js/);
|
||||
assert.match(editPage, /public\/js\/memo-pages\.js/);
|
||||
assert.match(styles, /\[data-memo-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('备忘录只向内容维护者或管理员开放写操作', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'memo-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(MemoPages.canEditRecords({ canEditContent: true }), true);
|
||||
assert.equal(MemoPages.canEditRecords({ canManage: true }), true);
|
||||
assert.equal(MemoPages.canEditRecords({ canEditContent: false, canManage: false }), false);
|
||||
assert.match(script, /await loadCapability\(api, genealogyId\);/);
|
||||
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
||||
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-memo-delete-id/);
|
||||
});
|
||||
|
||||
test('备忘录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'memo-pages.js'), 'utf8');
|
||||
|
||||
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(script, /setState\(query\('\[data-memo-list\]'\), 'loading', '正在加载备忘录…'\)/);
|
||||
assert.match(script, /setState\(query\('\[data-memo-detail\]'\), 'loading', '正在加载备忘录详情…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存备忘录…', 'loading'\)/);
|
||||
});
|
||||
|
||||
@@ -190,7 +190,7 @@ test('功德记录编辑器拒绝不可重读的停用记录', () => {
|
||||
assert.equal(isEditableMerit(null), false);
|
||||
});
|
||||
|
||||
test('功德录页面开放 PC CRUD 且不制造附件和契约外类型', () => {
|
||||
test('功德录页面按家谱能力开放 PC CRUD,且不制造附件和契约外类型', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-merit.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(projectRoot, 'profile-merit-edit.html'), 'utf8');
|
||||
@@ -199,6 +199,7 @@ test('功德录页面开放 PC CRUD 且不制造附件和契约外类型', () =>
|
||||
assert.doesNotMatch(listPage, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(listPage, /data-merit-list/);
|
||||
assert.match(listPage, /data-merit-detail/);
|
||||
assert.match(listPage, /data-merit-create-link hidden/);
|
||||
assert.doesNotMatch(editPage, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(editPage, /name="meritTime" type="datetime-local"/);
|
||||
assert.match(editPage, /value="donation"/);
|
||||
@@ -208,9 +209,38 @@ test('功德录页面开放 PC CRUD 且不制造附件和契约外类型', () =>
|
||||
assert.doesNotMatch(editPage, /value="service"/);
|
||||
assert.match(editPage, /name="amount" type="number" step="any"/);
|
||||
assert.match(editPage, /name="sortOrder"/);
|
||||
assert.match(editPage, /data-merit-form[^>]*novalidate/);
|
||||
assert.match(editPage, /for="meritName">功德人姓名<span class="field-required"/);
|
||||
assert.match(editPage, /for="meritTitle">功德标题<span class="field-required"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /data-merit-form-status role="status" aria-live="polite"/);
|
||||
assert.match(editPage, /data-merit-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
|
||||
assert.doesNotMatch(editPage, /mediaOssIds|type="file"|upload-pages\.js/);
|
||||
assert.match(editPage, /public\/js\/rich-editor\.js/);
|
||||
assert.match(editPage, /public\/js\/merit-pages\.js/);
|
||||
assert.match(styles, /\[data-merit-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('功德记录只向内容维护者或管理员开放写操作,并在提交前同步富文本', () => {
|
||||
const script = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.equal(requireFunction('canEditRecords')({ canEditContent: true }), true);
|
||||
assert.equal(requireFunction('canEditRecords')({ canManage: true }), true);
|
||||
assert.equal(requireFunction('canEditRecords')({ canEditContent: false, canManage: false }), false);
|
||||
assert.match(script, /await loadCapability\(api, genealogyId\);/);
|
||||
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
||||
assert.match(script, /root\.AppRichEditor && root\.AppRichEditor\.syncAll/);
|
||||
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-merit-delete-id/);
|
||||
});
|
||||
|
||||
test('功德记录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
|
||||
const script = fs.readFileSync(modulePath, 'utf8');
|
||||
|
||||
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(script, /setState\(query\('\[data-merit-list\]'\), 'loading', '正在加载功德记录…'\)/);
|
||||
assert.match(script, /setState\(query\('\[data-merit-detail\]'\), 'loading', '正在加载功德记录详情…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存功德记录…', 'loading'\)/);
|
||||
});
|
||||
|
||||
@@ -99,6 +99,12 @@ test('只有未读且正常的通知提供单条已读动作', () => {
|
||||
assert.equal(NotificationPages.canMarkRead({ readStatus: '0', status: '1' }), false);
|
||||
});
|
||||
|
||||
test('通知请求的 403 显式归类为无权限,而不是登录失效', () => {
|
||||
assert.equal(NotificationPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(NotificationPages.isForbidden({ code: '403' }), true);
|
||||
assert.equal(NotificationPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('消息页提供筛选、业务详情和单条/全部已读,不显示原始 JSON', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-messages.html'), 'utf8');
|
||||
|
||||
@@ -106,8 +112,20 @@ test('消息页提供筛选、业务详情和单条/全部已读,不显示原
|
||||
assert.match(source, /data-notification-filter/);
|
||||
assert.match(source, /data-notification-list/);
|
||||
assert.match(source, /data-notification-detail/);
|
||||
assert.match(source, /data-notification-status[^>]*role="status"/);
|
||||
assert.match(source, /data-api-state="loading"/);
|
||||
assert.match(source, /data-notification-unread-count/);
|
||||
assert.match(source, /data-notification-read-all/);
|
||||
assert.match(source, /src="public\/js\/notification-pages\.js"/);
|
||||
assert.doesNotMatch(source, /src="public\/js\/pending-pages\.js"/);
|
||||
});
|
||||
|
||||
test('消息中心将列表、详情与已读操作写入统一状态组件', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'public/js/notification-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setNotificationState\(container, 'empty', '当前暂无通知。'\)/);
|
||||
assert.match(source, /setNotificationState\(query\('\[data-notification-detail\]'\), 'loading', '正在读取通知详情…'\)/);
|
||||
assert.match(source, /updateStatus\('loading', '正在标记通知为已读…'\)/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,21 @@ function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('项目文档不记录测试手机号', () => {
|
||||
const docsRoot = path.join(root, 'docs');
|
||||
const phonePattern = /(?:^|[^0-9])1[3-9][0-9]{9}(?![0-9])/;
|
||||
|
||||
fs.readdirSync(docsRoot, { recursive: true })
|
||||
.filter((entry) => entry.endsWith('.md'))
|
||||
.forEach((entry) => {
|
||||
assert.doesNotMatch(
|
||||
fs.readFileSync(path.join(docsRoot, entry), 'utf8'),
|
||||
phonePattern,
|
||||
'文档不应包含手机号:' + entry
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('配置始终使用后端提供的 PC 接口地址', () => {
|
||||
const createConfig = require('../config.js');
|
||||
const development = createConfig({ location: { hostname: 'localhost', port: '5500' } });
|
||||
@@ -29,9 +44,12 @@ test('配置始终使用后端提供的 PC 接口地址', () => {
|
||||
|
||||
test('PC 对接规划记录本轮由用户指定的 YAML 冻结契约', () => {
|
||||
const plan = read('docs/PC接口对接规划.md');
|
||||
const handover = read('docs/交接文档.md');
|
||||
|
||||
assert.match(plan, /genealogy-pc-openapi\.yaml` 对接,因此该 YAML 是本轮冻结契约/);
|
||||
assert.match(plan, /旧 OpenAPI 文件不再作为新增功能依据/);
|
||||
assert.match(handover, /本文第二至第七节均为当日历史快照/);
|
||||
assert.match(handover, /不得据此新增、关闭或阻断任何功能/);
|
||||
});
|
||||
|
||||
test('pages do not load scripts for unavailable business APIs', () => {
|
||||
@@ -73,3 +91,42 @@ test('pages do not load scripts for unavailable business APIs', () => {
|
||||
assert.equal(read('profile-memo.html').includes('src="public/js/memo-pages.js"'), true, 'profile-memo.html must load memo-pages.js');
|
||||
assert.equal(read('profile-services.html').includes('src="public/js/vip-pages.js"'), true, 'profile-services.html must load vip-pages.js');
|
||||
});
|
||||
|
||||
test('个人中心将参考账户与家谱能力映射到 PC 工作台,未声明项不伪造操作', () => {
|
||||
const profile = read('profile.html');
|
||||
const services = read('profile-services.html');
|
||||
const share = read('profile-share.html');
|
||||
const familyHome = read('profile-family-home.html');
|
||||
const content = read('profile-content.html');
|
||||
const apiClient = read('utils/ApiClient.js');
|
||||
const verification = read('docs/验证20%记录.md');
|
||||
const mapping = read('docs/个人中心能力映射.md');
|
||||
|
||||
['profile-data.html', 'profile-security.html', 'profile-feedback.html', 'profile-services.html'].forEach((page) => {
|
||||
assert.match(profile, new RegExp('href="' + page.replace('.', '\\.') + '"'));
|
||||
});
|
||||
assert.match(profile, /data-logout-open/);
|
||||
assert.match(services, /href="help\.html"/);
|
||||
assert.match(services, /href="about\.html"/);
|
||||
assert.match(services, /应用分享与收益[\s\S]+?当前不提供不可用的跳转入口/);
|
||||
assert.match(services, /个性化推荐[\s\S]+?暂不能设置推荐开关/);
|
||||
assert.doesNotMatch(services, /data-(?:share|withdraw|recommendation)-(?:form|toggle)/);
|
||||
assert.doesNotMatch(share, /data-feature-status="pending"|pending-pages\.js|data-(?:share|withdraw)-(?:form|toggle)/);
|
||||
assert.match(share, /当前 PC 未声明应用分享、奖励、收益或提现的正式接口/);
|
||||
assert.match(familyHome, /class="module-card is-unavailable" aria-disabled="true">\s*<span class="icon">邀<\/span>\s*<h3>邀请家人<\/h3>/);
|
||||
assert.match(familyHome, /重要证件[\s\S]+?暂不能查看或保存/);
|
||||
assert.match(familyHome, /删除家谱[\s\S]+?暂不能查看或执行删除操作/);
|
||||
assert.doesNotMatch(familyHome, /data-genealogy-delete|deleteGenealogy/);
|
||||
assert.match(content, /亲友往来(贺礼簿)/);
|
||||
assert.match(content, /备忘录(家族恩人)/);
|
||||
assert.doesNotMatch(apiClient, /\/pages\/(?:mine|index)\//);
|
||||
assert.doesNotMatch(apiClient, /deleteGenealogy/);
|
||||
assert.doesNotMatch(apiClient, /(?:withdrawal|shareReward|recommendationPreference|documentRecords)/);
|
||||
assert.match(verification, /资料提醒、家谱邀请、家谱删除、家谱排序、分享奖励、推广收益与提现/);
|
||||
assert.match(mapping, /`Jiapu-App` 只作为产品信息架构参考/);
|
||||
['profile-data.html', 'profile-security.html', 'profile-family-home.html', 'profile-tree.html', 'profile-feed.html', 'profile-services.html'].forEach((page) => {
|
||||
assert.match(mapping, new RegExp('`' + page.replace('.', '\\.') + '`'));
|
||||
assert.equal(fs.existsSync(path.join(root, page)), true, `映射页面不存在:${page}`);
|
||||
});
|
||||
assert.doesNotMatch(mapping, /\/pages\//);
|
||||
});
|
||||
|
||||
@@ -11,15 +11,10 @@ const familyPendingPages = [
|
||||
];
|
||||
|
||||
const contentPendingPages = [
|
||||
'profile-admin-permissions.html',
|
||||
'profile-data-reminders.html'
|
||||
];
|
||||
|
||||
const residualPendingPages = [
|
||||
'profile-share.html'
|
||||
];
|
||||
|
||||
const pendingPages = familyPendingPages.concat(contentPendingPages, residualPendingPages);
|
||||
const pendingPages = familyPendingPages.concat(contentPendingPages);
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
@@ -47,8 +42,27 @@ test('待开发页面显式加载状态脚本', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('缺少 PC 接口契约的页面不展示静态成员、邀请或奖励数据', () => {
|
||||
const reminders = read('profile-data-reminders.html');
|
||||
const invite = read('profile-invite.html');
|
||||
const share = read('profile-share.html');
|
||||
|
||||
assert.match(reminders, /当前 PC 接口未提供资料缺项提醒列表/);
|
||||
assert.doesNotMatch(reminders, /鹿野 Loyel|张三|缺少出生日期/);
|
||||
assert.match(invite, /当前 PC 接口未提供邀请码/);
|
||||
assert.match(invite, /当前 PC 接口未提供邀请链接/);
|
||||
assert.doesNotMatch(invite, /data-invite-code|data-invite-link|加载中/);
|
||||
assert.match(share, /当前 PC 接口未提供分享链接或二维码/);
|
||||
assert.match(share, /当前 PC 接口未提供分享记录/);
|
||||
assert.match(share, /当前 PC 接口未提供收益账户或余额/);
|
||||
assert.match(share, /当前 PC 接口未提供提现方式、申请或记录/);
|
||||
assert.doesNotMatch(share, /data-member-overview|data-invite-link|data-withdraw(?:-form|-record)|3 人访问|1 人完成注册|加载中/);
|
||||
assert.doesNotMatch(share, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(share, /class="module-card is-unavailable" aria-disabled="true"/);
|
||||
});
|
||||
|
||||
test('已接入的家谱业务页面保持真实功能状态', () => {
|
||||
['profile-families.html', 'profile-create-family.html', 'profile-join-family.html', 'profile-join-review.html', 'profile-family-home.html', 'profile-family-admin.html', 'profile-feedback.html', 'profile-services.html', 'profile-content.html', 'profile-feed.html', 'profile-feed-edit.html', 'profile-feed-detail.html', 'profile-generation.html', 'profile-tree.html', 'profile-article.html', 'profile-article-edit.html', 'profile-album.html', 'profile-album-edit.html', 'profile-album-detail.html', 'profile-video.html', 'profile-video-edit.html', 'profile-gift.html', 'profile-gift-edit.html', 'profile-ceremony.html', 'profile-ceremony-detail.html', 'profile-growth.html', 'profile-growth-edit.html', 'profile-relative.html', 'profile-relative-edit.html', 'profile-memo.html', 'profile-memo-edit.html', 'profile-merit.html', 'profile-merit-edit.html', 'profile-messages.html'].forEach((page) => {
|
||||
['profile-families.html', 'profile-create-family.html', 'profile-join-family.html', 'profile-join-review.html', 'profile-family-home.html', 'profile-family-admin.html', 'profile-admin-permissions.html', 'profile-feedback.html', 'profile-services.html', 'profile-content.html', 'profile-feed.html', 'profile-feed-edit.html', 'profile-feed-detail.html', 'profile-generation.html', 'profile-tree.html', 'profile-article.html', 'profile-article-edit.html', 'profile-album.html', 'profile-album-edit.html', 'profile-album-detail.html', 'profile-video.html', 'profile-video-edit.html', 'profile-gift.html', 'profile-gift-edit.html', 'profile-ceremony.html', 'profile-ceremony-detail.html', 'profile-growth.html', 'profile-growth-edit.html', 'profile-relative.html', 'profile-relative-edit.html', 'profile-memo.html', 'profile-memo-edit.html', 'profile-merit.html', 'profile-merit-edit.html', 'profile-messages.html'].forEach((page) => {
|
||||
assert.doesNotMatch(read(page), /data-feature-status="pending"/, `${page} 不应标记为待开发`);
|
||||
});
|
||||
assert.match(read('profile-generation.html'), /src="public\/js\/generation-pages\.js"/);
|
||||
@@ -61,6 +75,9 @@ test('已接入的家谱业务页面保持真实功能状态', () => {
|
||||
assert.match(read('profile-family-home.html'), /src="public\/js\/family-home-pages\.js"/);
|
||||
assert.match(read('profile-tree.html'), /src="public\/js\/lineage-pages\.js"/);
|
||||
assert.match(read('profile-family-admin.html'), /src="public\/js\/member-admin-pages\.js"/);
|
||||
assert.doesNotMatch(read('profile-admin-permissions.html'), /src="public\/js\/pending-pages\.js"/);
|
||||
assert.match(read('profile-admin-permissions.html'), /href="profile-family-admin\.html" data-genealogy-context-link/);
|
||||
assert.match(read('profile-join-review.html'), /href="profile-admin-permissions\.html" data-genealogy-context-link/);
|
||||
assert.match(read('profile-article.html'), /src="public\/js\/article-pages\.js"/);
|
||||
assert.match(read('profile-article-edit.html'), /src="public\/js\/article-pages\.js"/);
|
||||
assert.match(read('profile-album.html'), /src="public\/js\/album-pages\.js"/);
|
||||
@@ -93,11 +110,21 @@ test('待开发的家谱管理页明确放行已接入的字辈入口并透传
|
||||
});
|
||||
|
||||
test('待开发页面仅放行显式标记的可用功能入口', () => {
|
||||
assert.equal(PageAvailability.shouldBlockLink(createLink({ href: 'profile-share.html' })), true);
|
||||
assert.equal(PageAvailability.shouldBlockLink(createLink({ href: 'profile-invite.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('待开发邀请页允许返回已接入的成员管理', () => {
|
||||
const invite = read('profile-invite.html');
|
||||
|
||||
assert.match(invite, /href="profile-family-admin\.html"[^>]*data-feature-link="available"[^>]*data-genealogy-context-link/);
|
||||
assert.equal(PageAvailability.shouldBlockLink(createLink({
|
||||
href: 'profile-family-admin.html',
|
||||
'data-feature-link': 'available'
|
||||
})), false);
|
||||
});
|
||||
|
||||
test('内容入口明确保留家族圈动态跳转', () => {
|
||||
assert.match(read('profile-content.html'), /href="profile-feed\.html" data-feature-link="available"/);
|
||||
assert.match(read('profile-content.html'), /href="profile-growth\.html" data-feature-link="available" data-genealogy-context-link/);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const ArticlePages = require('../public/js/article-pages.js');
|
||||
const AlbumPages = require('../public/js/album-pages.js');
|
||||
const CeremonyPages = require('../public/js/ceremony-admin-pages.js');
|
||||
const FeedPages = require('../public/js/feed-pages.js');
|
||||
const GenerationPages = require('../public/js/generation-pages.js');
|
||||
const GrowthPages = require('../public/js/growth-pages.js');
|
||||
const LineagePages = require('../public/js/lineage-pages.js');
|
||||
const MemoPages = require('../public/js/memo-pages.js');
|
||||
const MeritPages = require('../public/js/merit-pages.js');
|
||||
const RelativePages = require('../public/js/relative-pages.js');
|
||||
const VideoPages = require('../public/js/video-pages.js');
|
||||
|
||||
const contentCapabilities = [
|
||||
['谱文', ArticlePages.canEditArticles],
|
||||
['相册', AlbumPages.canEditAlbums],
|
||||
['仪式', CeremonyPages.canEditCeremonies],
|
||||
['动态', FeedPages.canEditFeeds],
|
||||
['字辈', GenerationPages.canEditGenerations],
|
||||
['成长记录', GrowthPages.canEditRecords],
|
||||
['备忘录', MemoPages.canEditRecords],
|
||||
['功德簿', MeritPages.canEditRecords],
|
||||
['亲属记录', RelativePages.canEditRecords],
|
||||
['视频', VideoPages.canEditVideos]
|
||||
];
|
||||
|
||||
test('内容模块只信任家谱详情返回的严格编辑能力', () => {
|
||||
contentCapabilities.forEach(function (entry) {
|
||||
const label = entry[0];
|
||||
const canEdit = entry[1];
|
||||
|
||||
assert.equal(typeof canEdit, 'function', label + '缺少编辑能力判断');
|
||||
assert.equal(canEdit({ canEditContent: true }), true, label + '应允许内容编辑者写入');
|
||||
assert.equal(canEdit({ canManage: true }), true, label + '应允许家谱管理者写入');
|
||||
assert.equal(canEdit({ canEditContent: 'true' }), false, label + '不能把字符串权限当作授权');
|
||||
assert.equal(canEdit({ canManage: 1 }), false, label + '不能把数值权限当作授权');
|
||||
assert.equal(canEdit({ canEditContent: false, canManage: false }), false, label + '不能向无权限成员开放写入');
|
||||
});
|
||||
});
|
||||
|
||||
test('世系编辑与成员、入谱审核分别收紧到正式能力字段', () => {
|
||||
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: true, canManage: false }), {
|
||||
canEditContent: true
|
||||
});
|
||||
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: false, canManage: true }), {
|
||||
canEditContent: false
|
||||
});
|
||||
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: 'true' }), {
|
||||
canEditContent: false
|
||||
});
|
||||
|
||||
const memberScript = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'member-admin-pages.js'), 'utf8');
|
||||
const reviewScript = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'join-review-pages.js'), 'utf8');
|
||||
|
||||
assert.match(memberScript, /managementAccess = detail\.canManage === true;/);
|
||||
assert.match(memberScript, /function canTransferOwner\(role, canManage\)[\s\S]+?role === 'owner' && canManage === true/);
|
||||
assert.match(reviewScript, /canManage = detail && detail\.canManage === true;/);
|
||||
});
|
||||
@@ -10,18 +10,21 @@ const UploadPages = require('../public/js/upload-pages.js');
|
||||
test('profile view reads only the documented profile fields', () => {
|
||||
assert.deepEqual(ProfilePages.buildProfileView({
|
||||
nickName: '小林',
|
||||
realName: '林某',
|
||||
phone: '13800000000',
|
||||
birthday: '1990-01-01',
|
||||
provinceCode: '11',
|
||||
userName: '不应读取',
|
||||
mobile: '不应读取'
|
||||
}), {
|
||||
displayName: '小林',
|
||||
realName: '林某',
|
||||
avatarText: '小',
|
||||
avatarStatus: '未设置',
|
||||
phone: '13800000000',
|
||||
sex: '待填写',
|
||||
birthday: '1990-01-01',
|
||||
regionText: '加载中...'
|
||||
email: '待填写',
|
||||
regionText: '当前资料接口未提供'
|
||||
});
|
||||
|
||||
assert.deepEqual(ProfilePages.buildProfileView({
|
||||
@@ -30,20 +33,27 @@ test('profile view reads only the documented profile fields', () => {
|
||||
mobile: '不应读取'
|
||||
}), {
|
||||
displayName: '未设置昵称',
|
||||
realName: '待填写',
|
||||
avatarText: '家',
|
||||
avatarStatus: '未设置',
|
||||
phone: '未绑定',
|
||||
sex: '待填写',
|
||||
birthday: '待填写',
|
||||
regionText: '待填写'
|
||||
email: '待填写',
|
||||
regionText: '当前资料接口未提供'
|
||||
});
|
||||
});
|
||||
|
||||
test('profile loading failure has an explicit non-demo view', () => {
|
||||
assert.deepEqual(ProfilePages.buildUnavailableProfileView(), {
|
||||
displayName: '账户资料暂时无法读取',
|
||||
realName: '暂无数据',
|
||||
avatarText: '!',
|
||||
avatarStatus: '暂无数据',
|
||||
phone: '暂无数据',
|
||||
birthday: '暂无数据',
|
||||
email: '暂无数据',
|
||||
sex: '暂无数据',
|
||||
regionText: '暂无数据'
|
||||
});
|
||||
});
|
||||
@@ -70,9 +80,23 @@ test('profile update emits only the six fields defined by ProfileUpdateBody', ()
|
||||
ProfilePages.buildProfileUpdateBody({ avatar: '2060000000000000001' }),
|
||||
{ avatar: '2060000000000000001' }
|
||||
);
|
||||
assert.throws(() => ProfilePages.buildProfileUpdateBody({ avatar: 'not-an-id' }), /头像 OSS ID 必须是整数/);
|
||||
assert.equal(UploadPages.normalizeUploadResult({ ossId: '2060000000000000000' }).ossId, '2060000000000000000');
|
||||
});
|
||||
|
||||
test('个人中心概览不读取资料接口未声明的地区字段', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'profile-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /regionText: '当前资料接口未提供'/);
|
||||
assert.doesNotMatch(source, /provinceCode|cityCode|districtCode|regionPath|applyProfileRegionSelection/);
|
||||
});
|
||||
|
||||
test('profile update rejects an empty payload and an invalid email before requesting the PC API', () => {
|
||||
assert.equal(ProfilePages.validateProfileUpdateBody({}), '请至少填写一项资料');
|
||||
assert.equal(ProfilePages.validateProfileUpdateBody({ email: 'invalid-email' }), '请输入正确的邮箱地址');
|
||||
assert.equal(ProfilePages.validateProfileUpdateBody({ nickName: '小林' }), '');
|
||||
});
|
||||
|
||||
test('region selector accepts only documented region fields and levels', () => {
|
||||
assert.deepEqual(RegionPages.normalizeList({ data: [{ regionCode: '11' }] }), []);
|
||||
assert.equal(RegionPages.getRegionCode({ value: '11' }), '');
|
||||
@@ -93,7 +117,10 @@ test('region selector accepts only documented region fields and levels', () => {
|
||||
test('profile data page exposes every editable ProfileUpdateBody field without a manual OSS ID input', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
|
||||
|
||||
assert.match(source, /data-profile-form novalidate/);
|
||||
|
||||
assert.match(source, /data-profile-form/);
|
||||
assert.match(source, /data-profile-text="realName"/);
|
||||
assert.match(source, /name="nickName"/);
|
||||
assert.match(source, /name="realName"/);
|
||||
assert.match(source, /name="avatar"/);
|
||||
@@ -104,7 +131,72 @@ test('profile data page exposes every editable ProfileUpdateBody field without a
|
||||
assert.match(source, /name="avatar"[\s\S]*type="hidden"|type="hidden"[\s\S]*name="avatar"/);
|
||||
assert.doesNotMatch(source, /data-region-profile-form/);
|
||||
assert.doesNotMatch(source, /data-region-profile-status/);
|
||||
assert.match(source, /data-region-search-form/);
|
||||
assert.match(source, /role="group" aria-labelledby="profile-region-heading" aria-describedby="profile-region-contract-note profile-region-picker-status"/);
|
||||
assert.match(source, /<label for="profileProvinceCode">省/);
|
||||
assert.match(source, /<label for="profileCityCode">市/);
|
||||
assert.match(source, /<label for="profileDistrictCode">区县/);
|
||||
assert.match(source, /id="profile-region-contract-note"/);
|
||||
assert.match(source, /id="profile-region-picker-status"[^>]*data-region-picker-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /data-region-search-form[^>]*novalidate/);
|
||||
assert.match(source, /id="profileRegionKeyword"[^>]*aria-describedby="profile-region-search-status"[^>]*required/);
|
||||
assert.match(source, /data-region-search-status role="status" aria-live="polite"/);
|
||||
assert.match(source, /src="public\/js\/upload-pages\.js"/);
|
||||
assert.match(source, /src="public\/js\/region-pages\.js"/);
|
||||
assert.match(source, /data-profile-load-status/);
|
||||
assert.match(source, /data-profile-status[^>]*role="status"/);
|
||||
assert.match(source, /data-genealogy-context-link/);
|
||||
assert.doesNotMatch(source, /第二代 顺字辈|微信 \/ QQ|学历 \/ 职业|父亲<\/span><b>某某/);
|
||||
});
|
||||
|
||||
test('地区搜索复用统一状态组件并区分空结果、失败和无权限', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'region-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setApiState\(results, 'loading', '正在搜索地区…'\)/);
|
||||
assert.match(source, /setApiState\(container, 'empty', '未找到匹配地区'\)/);
|
||||
assert.match(source, /当前账号无权搜索地区。/);
|
||||
assert.match(source, /当前账号无权定位地区。/);
|
||||
assert.match(source, /当前账号无权读取地区数据。/);
|
||||
assert.match(source, /setPickerState\(picker, 'loading', '正在加载省…'\)/);
|
||||
assert.match(source, /setPickerState\(picker, 'loading', '正在加载市…'\)/);
|
||||
assert.match(source, /setPickerState\(picker, 'loading', '正在加载区县…'\)/);
|
||||
assert.equal(RegionPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(RegionPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('个人资料页不伪造亲属数据,并明确指向当前家谱的成员管理', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
|
||||
|
||||
assert.match(source, /当前 PC 资料接口未提供个人亲属关系列表/);
|
||||
assert.match(source, /href="profile-family-admin\.html"[^>]*data-genealogy-context-link/);
|
||||
assert.match(source, /api-state--empty/);
|
||||
assert.doesNotMatch(source, /张三|父亲 · 已绑定成员资料|邀请激活/);
|
||||
});
|
||||
|
||||
test('个人中心以桌面服务面板承接 APP 的账户功能,并隔离家谱业务入口', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile.html'), 'utf8');
|
||||
const main = source.slice(0, source.indexOf('</main>'));
|
||||
|
||||
assert.match(main, /<h2>账户与服务<\/h2>/);
|
||||
[
|
||||
'profile-messages.html',
|
||||
'profile-data.html',
|
||||
'profile-security.html',
|
||||
'profile-services.html',
|
||||
'profile-feedback.html',
|
||||
'app.html'
|
||||
].forEach((href) => {
|
||||
assert.match(main, new RegExp('href="' + href.replace('.', '\\.') + '"'));
|
||||
});
|
||||
assert.doesNotMatch(main, /href="profile-(?:article|album|video|merit|gift|join-review)\.html"/);
|
||||
});
|
||||
|
||||
test('个人中心首页概览只展示已声明的账户资料字段', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile.html'), 'utf8');
|
||||
|
||||
['phone', 'displayName', 'realName', 'sex', 'birthday', 'email'].forEach((field) => {
|
||||
assert.match(source, new RegExp('data-profile-text="' + field + '"'));
|
||||
});
|
||||
assert.doesNotMatch(source, /data-profile-text="regionText"/);
|
||||
assert.match(source, /<div class="profile-load-status" data-profile-load-status role="status" aria-live="polite">/);
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ test('亲友记录编辑器拒绝不可重读的停用记录', () => {
|
||||
assert.equal(RelativePages.isEditableRecord(null), false);
|
||||
});
|
||||
|
||||
test('亲友记录列表和编辑页开放 PC CRUD 且不允许手填 OSS ID 或状态', () => {
|
||||
test('亲友记录列表和编辑页按家谱能力开放 PC CRUD,且不允许手填 OSS ID 或状态', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-relative.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(root, 'profile-relative-edit.html'), 'utf8');
|
||||
@@ -188,10 +188,15 @@ test('亲友记录列表和编辑页开放 PC CRUD 且不允许手填 OSS ID 或
|
||||
|
||||
assert.match(listPage, /data-relative-list/);
|
||||
assert.match(listPage, /data-relative-detail/);
|
||||
assert.match(listPage, /data-relative-create-link hidden/);
|
||||
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
|
||||
assert.match(editPage, /name="eventTime" type="datetime-local"/);
|
||||
assert.match(editPage, /data-relative-form[^>]*novalidate/);
|
||||
assert.match(editPage, /for="relativeName">亲友姓名<span class="field-required"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
|
||||
assert.match(editPage, /data-relative-form-status role="status" aria-live="polite"/);
|
||||
assert.match(editPage, /data-relative-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
|
||||
assert.match(editPage, /data-relative-clear-media/);
|
||||
assert.doesNotMatch(editPage, /name="(?:mediaOssIds|status)"[^>]*type="text"/);
|
||||
assert.match(editPage, /public\/js\/md5\.js/);
|
||||
@@ -199,3 +204,28 @@ test('亲友记录列表和编辑页开放 PC CRUD 且不允许手填 OSS ID 或
|
||||
assert.match(editPage, /public\/js\/relative-pages\.js/);
|
||||
assert.match(styles, /\[data-relative-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('亲友记录只向内容维护者或管理员开放写操作', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'relative-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(RelativePages.canEditRecords({ canEditContent: true }), true);
|
||||
assert.equal(RelativePages.canEditRecords({ canManage: true }), true);
|
||||
assert.equal(RelativePages.canEditRecords({ canEditContent: false, canManage: false }), false);
|
||||
assert.match(script, /await loadCapability\(api, genealogyId\);/);
|
||||
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
||||
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-relative-delete-id/);
|
||||
});
|
||||
|
||||
test('亲友记录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'relative-pages.js'), 'utf8');
|
||||
|
||||
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(script, /setState\(query\('\[data-relative-list\]'\), 'loading', '正在加载亲友记录…'\)/);
|
||||
assert.match(script, /setState\(query\('\[data-relative-detail\]'\), 'loading', '正在加载亲友记录详情…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /setFormStatus\('正在保存亲友记录…', 'loading'\)/);
|
||||
});
|
||||
|
||||
@@ -190,3 +190,30 @@ test('security page field names match phone-change and account-deactivation DTOs
|
||||
assert.match(source, /id="deactivateSmsCode"\s+name="smsCode"/);
|
||||
assert.match(source, /data-security-send-deactivate-code/);
|
||||
});
|
||||
|
||||
test('上传状态复用统一组件并区分无权限', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'upload-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setUploadStatus\(status, 'loading', files\[index\]\.name \+ ' 正在上传…'\)/);
|
||||
assert.match(source, /当前账号无权上传文件。/);
|
||||
assert.match(source, /status\.setAttribute\('role', 'status'\)/);
|
||||
assert.equal(UploadPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(UploadPages.isForbidden({ status: 401 }), false);
|
||||
});
|
||||
|
||||
test('security forms provide visible required labels and unified validation, loading and failure feedback', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-security.html'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'security-pages.js'), 'utf8');
|
||||
|
||||
assert.match(page, /data-security-form="password" novalidate/);
|
||||
assert.match(page, /data-security-form="phone" novalidate/);
|
||||
assert.match(page, /data-security-form="deactivate" novalidate/);
|
||||
assert.match(page, /field-required/);
|
||||
assert.match(page, /data-security-status[^>]*role="status"/);
|
||||
assert.match(page, /data-security-bound-phone[^>]*role="status"/);
|
||||
assert.doesNotMatch(page, /微信快捷登录|账号安全提醒/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /function setBoundPhoneStatus\(type, message\)/);
|
||||
assert.match(script, /setStatus\(form, 'error', '验证码发送失败,请稍后重试。'\)/);
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ test('站点资讯列表使用响应 ID 生成详情链接并隐藏内部字段'
|
||||
assert.match(html, /<script>/);
|
||||
assert.match(html, /<img src=x>/);
|
||||
assert.doesNotMatch(html, /<script|<img|secret-oss|secret-remark|coverOssId|sortOrder|status|remark/);
|
||||
assert.match(SiteNewsPages.renderArticleList([]), /当前暂无资讯/);
|
||||
assert.equal(SiteNewsPages.renderArticleList([]), '');
|
||||
});
|
||||
|
||||
test('站点资讯详情转义正文、保留换行并只开放安全外链', () => {
|
||||
@@ -159,19 +159,36 @@ test('详情 URL 只读取安全字符串文章 ID', () => {
|
||||
assert.equal(SiteNewsPages.readArticleId(''), '');
|
||||
});
|
||||
|
||||
test('资讯列表和详情将加载、空数据、失败及无权限写入统一状态组件', () => {
|
||||
const source = fs.readFileSync(path.resolve(__dirname, '../public/js/site-news-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(SiteNewsPages.isForbidden({ status: 403 }), true);
|
||||
assert.equal(SiteNewsPages.isForbidden({ status: 401 }), false);
|
||||
assert.equal(SiteNewsPages.isMissingArticleError(new Error('资讯不存在或已下线')), true);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setSiteState\(list, 'loading', '正在读取资讯…'\)/);
|
||||
assert.match(source, /setSiteState\(detail, 'loading', '正在读取资讯详情…'\)/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
|
||||
assert.match(source, /isForbidden\(error\) \? 'forbidden' : \(isMissingArticleError\(error\) \? 'empty' : 'error'\)/);
|
||||
});
|
||||
|
||||
test('官网资讯列表与详情页加载同一个公开内容 owner 且不暴露业务 ID', () => {
|
||||
assert.match(newsPage, /<body[^>]*data-site-news-page/);
|
||||
assert.match(newsPage, /data-site-news-list/);
|
||||
assert.match(newsPage, /data-site-news-status/);
|
||||
assert.match(newsPage, /data-site-news-status[^>]*role="status"/);
|
||||
assert.match(newsPage, /data-api-state="loading"/);
|
||||
assert.match(newsPage, /news\.html\?articleType=news/);
|
||||
assert.match(newsPage, /news\.html\?articleType=notice/);
|
||||
assert.match(newsPage, /news\.html\?articleType=download/);
|
||||
assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/);
|
||||
assert.match(newsPage, /src="public\/js\/profile-common\.js"/);
|
||||
|
||||
assert.match(detailPage, /<body[^>]*data-site-article-page/);
|
||||
assert.match(detailPage, /<title>资讯详情 - 代代相传<\/title>/);
|
||||
assert.match(detailPage, /data-site-article-detail/);
|
||||
assert.match(detailPage, /data-site-article-status/);
|
||||
assert.match(detailPage, /data-site-article-status[^>]*role="status"/);
|
||||
assert.match(detailPage, /data-api-state="loading"/);
|
||||
assert.match(detailPage, /src="public\/js\/profile-common\.js"/);
|
||||
assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/);
|
||||
assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/);
|
||||
assert.doesNotMatch(detailPage, /href="article-detail\.html"/);
|
||||
|
||||
@@ -18,6 +18,13 @@ test('内容发布页为全部家谱功能入口透传真实家谱上下文', ()
|
||||
});
|
||||
});
|
||||
|
||||
test('内容工作台在未选择家谱时使用统一的空上下文状态', () => {
|
||||
const page = read('profile-content.html');
|
||||
|
||||
assert.match(page, /api-state--empty[^>]*data-api-state="empty"/);
|
||||
assert.doesNotMatch(page, /api-empty/);
|
||||
});
|
||||
|
||||
test('家谱主页公开谱文、相册、视频、功德和祭祀活动入口并透传上下文', () => {
|
||||
const page = read('profile-family-home.html');
|
||||
|
||||
@@ -66,6 +73,6 @@ test('个人中心、我的家谱和当前家谱主页使用明确且可返回
|
||||
assert.match(account, /<a href="profile-families\.html">我的家谱<\/a>/);
|
||||
assert.match(families, /<nav class="profile-path" aria-label="当前位置">[\s\S]*个人中心[\s\S]*我的家谱/);
|
||||
assert.match(familyHome, /<nav class="profile-path" aria-label="当前位置">[\s\S]*profile-families\.html[\s\S]*data-family-home-context/);
|
||||
assert.match(familyHomeRuntime, /setText\('\[data-family-home-context\]', result\.overview\.genealogyName\)/);
|
||||
assert.match(familyHomeRuntime, /setText\('\[data-family-home-context\]', result\.genealogyName\)/);
|
||||
assert.doesNotMatch(coreHeaders, /返回中心|数字家谱/);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,15 @@ test('视频文件元数据时长按秒取整且拒绝无效值', () => {
|
||||
assert.equal(VideoPages.normalizeDurationSeconds(Infinity), undefined);
|
||||
});
|
||||
|
||||
test('视频维护资格只接受服务端返回的内容或管理布尔能力', () => {
|
||||
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'video-pages.js'), 'utf8');
|
||||
|
||||
assert.equal(VideoPages.canEditVideos({ canEditContent: true }), true);
|
||||
assert.equal(VideoPages.canEditVideos({ canManage: true }), true);
|
||||
assert.equal(VideoPages.canEditVideos({ canEditContent: 'true', canManage: false }), false);
|
||||
assert.match(script, /setManagementAccess\(canEditVideos\(result\[0\]\)\)/);
|
||||
});
|
||||
|
||||
test('视频列表和编辑页开放真实 PC 功能且不允许手填 OSS ID', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-video.html'), 'utf8');
|
||||
@@ -141,6 +150,9 @@ test('视频列表和编辑页开放真实 PC 功能且不允许手填 OSS ID',
|
||||
assert.match(listPage, /data-video-management hidden/);
|
||||
assert.match(listPage, /src="public\/js\/video-pages\.js"/);
|
||||
assert.match(editPage, /data-video-form data-video-management hidden/);
|
||||
assert.match(editPage, /data-video-form data-video-management hidden novalidate/);
|
||||
assert.match(editPage, /class="field-required"/);
|
||||
assert.match(editPage, /<div class="form-status" data-video-form-status/);
|
||||
assert.match(editPage, /name="videoTitle"/);
|
||||
assert.match(editPage, /name="videoDesc"/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
@@ -152,3 +164,26 @@ test('视频列表和编辑页开放真实 PC 功能且不允许手填 OSS ID',
|
||||
assert.match(editPage, /public\/js\/video-pages\.js/);
|
||||
assert.match(styles, /\[data-video-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
test('视频编辑将权限读取、校验、保存中和失败状态接入公共状态组件', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-video.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(root, 'profile-video-edit.html'), 'utf8');
|
||||
const script = fs.readFileSync(path.join(root, 'public', 'js', 'video-pages.js'), 'utf8');
|
||||
|
||||
assert.match(listPage, /data-video-list[^>]*>[\s\S]*data-api-state="loading"/);
|
||||
assert.match(listPage, /data-video-detail[^>]*>[\s\S]*data-api-state="empty"/);
|
||||
assert.match(editPage, /data-api-state="loading"[^>]*data-video-permission-placeholder/);
|
||||
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(script, /function setPermissionPlaceholder\(type, message\)/);
|
||||
assert.match(script, /setVideoListState\('loading', '正在加载视频…'\)/);
|
||||
assert.match(script, /setVideoDetailState\('loading', '正在加载视频详情…'\)/);
|
||||
assert.match(script, /setVideoListState\('empty', '暂无家族视频'\)/);
|
||||
assert.match(script, /setVideoListState\('forbidden', '当前账号无权查看该家谱视频。'\)/);
|
||||
assert.match(script, /setVideoDetailState\(\n\s*getApiStateType\(error\)/);
|
||||
assert.match(script, /正在读取家谱权限…', 'loading'/);
|
||||
assert.match(script, /setPermissionPlaceholder\('forbidden', '当前账号没有视频维护权限。'\)/);
|
||||
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
||||
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
||||
assert.match(script, /getApiStateType\(error\)/);
|
||||
});
|
||||
|
||||
@@ -112,6 +112,16 @@ test('家谱选择项只接受响应中的稳定 ID 和名称', () => {
|
||||
genealogyId,
|
||||
genealogyName: ''
|
||||
}), null);
|
||||
assert.deepEqual(VipPages.normalizeGenealogyOptions([
|
||||
{ genealogyId, genealogyName: '叶氏家谱', surname: '叶' },
|
||||
{ genealogyId: 'unsafe', genealogyName: '无效家谱' }
|
||||
]), [{ genealogyId, genealogyName: '叶氏家谱', surname: '叶' }]);
|
||||
assert.equal(VipPages.resolveContextGenealogyId([
|
||||
{ genealogyId, genealogyName: '叶氏家谱', surname: '叶' }
|
||||
], genealogyId), genealogyId);
|
||||
assert.equal(VipPages.resolveContextGenealogyId([
|
||||
{ genealogyId, genealogyName: '叶氏家谱', surname: '叶' }
|
||||
], 'unsafe'), '');
|
||||
});
|
||||
|
||||
test('订单请求只构造 AppVipOrderBody 且页面默认省略支付方式', () => {
|
||||
@@ -216,9 +226,27 @@ test('会员服务页只从接口结果选择套餐和家谱,不暴露业务 I
|
||||
assert.match(source, /<select[^>]+data-vip-genealogy-options/);
|
||||
assert.match(source, /data-vip-order-refresh/);
|
||||
assert.match(source, /data-vip-order-list/);
|
||||
assert.match(source, /data-vip-order-form novalidate/);
|
||||
assert.match(source, /data-vip-order-status[^>]*role="status"/);
|
||||
assert.match(source, /data-api-state="loading"/);
|
||||
assert.doesNotMatch(source, /<input[^>]+name="(?:packageId|genealogyId|orderId)"/);
|
||||
assert.doesNotMatch(source, /name="payType"/);
|
||||
assert.doesNotMatch(source, /立即支付|模拟支付|取消订单|退款/);
|
||||
assert.ok(source.indexOf('src="public/js/profile-common.js"') < source.indexOf('src="public/js/vip-pages.js"'));
|
||||
});
|
||||
|
||||
test('会员服务页补齐桌面端设置与协议入口,不伪造账户接口', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'profile-services.html'), 'utf8');
|
||||
|
||||
assert.match(source, /平台设置与协议/);
|
||||
assert.match(source, /href="about\.html"/);
|
||||
assert.match(source, /href="privacy\.html"/);
|
||||
assert.match(source, /href="children-privacy\.html"/);
|
||||
assert.match(source, /href="terms\.html"/);
|
||||
assert.match(source, /个性化推荐/);
|
||||
assert.match(source, /当前 PC 接口未提供推荐偏好的读取或保存/);
|
||||
assert.doesNotMatch(source, /data-setting-form|data-withdraw-form|data-share-reward/);
|
||||
assert.doesNotMatch(source, /data-recommendation-toggle|name="recommendation"/);
|
||||
});
|
||||
|
||||
test('会员服务运行时并行读取套餐、我的家谱和订单,并在创建后重新读取核验', () => {
|
||||
@@ -229,3 +257,16 @@ test('会员服务运行时并行读取套餐、我的家谱和订单,并在
|
||||
assert.match(source, /refreshed = normalizeVipOrders\(await api\.vipOrders\(\)\)/);
|
||||
assert.match(source, /item\.orderId === created\.orderId/);
|
||||
});
|
||||
|
||||
test('会员服务将加载、空数据、提交校验与请求失败写入统一状态组件', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'public/js/vip-pages.js'), 'utf8');
|
||||
|
||||
assert.match(source, /function setApiState\(target, type, message\)/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
||||
assert.match(source, /setListState\(packageList, 'loading', '正在读取会员套餐…'\)/);
|
||||
assert.match(source, /setListState\(orderList, 'empty', '当前没有会员订单。'\)/);
|
||||
assert.match(source, /setStatus\('error', validation\)/);
|
||||
assert.match(source, /setPendingButton\(submit, true, '正在创建订单…'\)/);
|
||||
assert.match(source, /root\.ProfileUI && typeof root\.ProfileUI\.getGenealogyId === 'function'/);
|
||||
assert.match(source, /已关联当前家谱,请选择会员套餐/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user