等待后端更新接口
This commit is contained in:
@@ -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} 仍保留旧空状态`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user