修改页面,完成单页模式新增首页返回上一页按钮

This commit is contained in:
2026-08-31 14:48:32 +08:00
parent aaed0584b1
commit 15b301b358
363 changed files with 10895 additions and 213 deletions
+115 -2
View File
@@ -5,7 +5,11 @@ import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/
import { tmpdir } from 'node:os';
import { extname, join, resolve, sep } from 'node:path';
const projectRoot = resolve(import.meta.dirname, '..');
const sourceProjectRoot = resolve(import.meta.dirname, '..');
const projectRootArg = process.argv.find((arg) => arg.startsWith('--project-root='));
const projectRoot = projectRootArg
? resolve(sourceProjectRoot, projectRootArg.slice('--project-root='.length))
: sourceProjectRoot;
const chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
const genealogyId = '2062179707935264701';
const personId = '2062179707935264702';
@@ -27,6 +31,9 @@ const shouldCaptureNavigationAudit = process.argv.includes('--capture-navigation
process.env.JIAPU_CAPTURE_NAVIGATION_AUDIT === '1';
const shouldRunFullProjectAudit = process.argv.includes('--full-project-audit');
const shouldRunProfileUxAudit = process.argv.includes('--profile-ux-audit');
const shouldRunNavigationStructureSmoke = process.argv.includes('--navigation-structure-smoke');
const shouldRunProfileSpacingSmoke = process.argv.includes('--profile-spacing-smoke');
const shouldRunSiteSpacingSmoke = process.argv.includes('--site-spacing-smoke');
const profileOperationAuditRoundArg = process.argv.find((arg) => arg.startsWith('--profile-operation-audit-round='));
const profileOperationAuditRound = profileOperationAuditRoundArg
? Number(profileOperationAuditRoundArg.split('=')[1])
@@ -398,6 +405,14 @@ try {
localStorage.removeItem('genealogy_auth_token');
} else if (!['/login.html', '/register.html', '/forgot-password.html'].includes(location.pathname)) {
localStorage.setItem('genealogy_auth_token', 'browser-smoke-token');
if (browserSmokeParams.has('no-user-cache')) {
localStorage.removeItem('genealogy_public_user_v1');
} else {
localStorage.setItem('genealogy_public_user_v1', JSON.stringify({
nickName: '叶用户',
avatarUrl: 'http://127.0.0.1:${port}/public/images/logo-mark.png'
}));
}
}
sessionStorage.setItem('genealogy_current_context_v1', JSON.stringify({ genealogyId: '${genealogyId}', genealogyName: '叶氏家谱' }));
window.confirm = () => false;
@@ -874,6 +889,103 @@ try {
console.log(`BROWSER: profile operation audit ${roundName} captured ${records.length} operations and ${records.length * 2} screenshots`);
}
if (shouldRunProfileSpacingSmoke || shouldRunSiteSpacingSmoke) {
const spacingPages = [];
const spacingPagePattern = shouldRunSiteSpacingSmoke ? /\.html$/ : /^profile-.*\.html$/;
for (const file of (await readdir(projectRoot)).filter((name) => spacingPagePattern.test(name))) {
const source = await readFile(resolve(projectRoot, file), 'utf8');
if (/class="[^"]*(?:actions|toolbar|cta|auth-links)/.test(source)) spacingPages.push(file);
}
const spacingViolations = [];
for (const viewport of [{ width: 1440, height: 900, mobile: false }, { width: 390, height: 844, mobile: true }]) {
await client.send('Emulation.setDeviceMetricsOverride', { ...viewport, deviceScaleFactor: 1 });
for (const page of spacingPages) {
await navigate(fullAuditPageUrl(page));
await settlePageForOperationAudit();
const pageViolations = await evaluate(`(() => Array.from(document.querySelectorAll('.row-actions, .bottom-actions, .profile-form-actions, .toolbar, .cta, .auth-links, [class$="-actions"], [class*="-actions "]')).flatMap((actions) => {
if (!actions.getClientRects().length) return [];
let previous = actions.previousElementSibling;
while (previous && !previous.getClientRects().length) previous = previous.previousElementSibling;
if (!previous) return [];
const previousRect = previous.getBoundingClientRect();
const actionsRect = actions.getBoundingClientRect();
const gap = Math.round(actionsRect.top - previousRect.bottom);
const parentStyle = getComputedStyle(actions.parentElement);
if (actions.parentElement.matches('.editor-field') || parentStyle.display !== 'block' || gap < -1 || gap >= 12) return [];
return [{
className: actions.className,
parentClass: actions.parentElement.className,
parentDisplay: parentStyle.display,
parentGap: parentStyle.rowGap,
previousClass: previous.className,
previousTag: previous.tagName.toLowerCase(),
gap
}];
}))()`);
pageViolations.forEach((violation) => spacingViolations.push({ viewport: viewport.width, page, ...violation }));
}
}
assert.deepEqual(spacingViolations, [], `页面操作区间距不足:${JSON.stringify(spacingViolations)}`);
console.log(`BROWSER: ${spacingPages.length} ${shouldRunSiteSpacingSmoke ? 'site' : 'profile'} pages action spacing passed at desktop and mobile widths`);
} else if (shouldRunNavigationStructureSmoke) {
await navigate('login.html');
const authNavigation = await evaluate(`(() => ({
brandHref: document.querySelector('.brand')?.getAttribute('href'),
hasRedundantControls: Boolean(document.querySelector('[data-page-navigation-controls], [data-navigation-back]'))
}))()`);
assert.deepEqual(authNavigation, {
brandHref: 'index.html',
hasRedundantControls: false
});
await client.send('Emulation.setDeviceMetricsOverride', { width: 1200, height: 800, deviceScaleFactor: 1, mobile: false });
await navigate('index.html?no-user-cache=1');
await waitFor(async () => evaluate(`document.querySelector('.site-header .nav-user-name')?.textContent.trim() === '叶用户'`));
const publicNavigation = await evaluate(`(() => ({
brandHref: document.querySelector('.site-header .brand')?.getAttribute('href'),
hasHome: Array.from(document.querySelectorAll('.site-header .nav-links a')).some((link) => link.textContent.trim() === '首页'),
userName: document.querySelector('.site-header .nav-user-name')?.textContent.trim(),
userHref: document.querySelector('.site-header .nav-user-entry')?.getAttribute('href'),
hasAvatar: Boolean(document.querySelector('.site-header .nav-user-avatar img')),
hasLegacyAccountLinks: Array.from(document.querySelectorAll('.site-header .nav-actions a')).some((link) => ['个人中心', '账户资料'].includes(link.textContent.trim())),
hasRedundantControls: Boolean(document.querySelector('[data-page-navigation-controls], [data-navigation-back]')),
hasHorizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth
}))()`);
assert.deepEqual(publicNavigation, {
brandHref: 'index.html',
hasHome: true,
userName: '叶用户',
userHref: 'profile.html',
hasAvatar: true,
hasLegacyAccountLinks: false,
hasRedundantControls: false,
hasHorizontalOverflow: false
});
await client.send('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false });
await navigate('profile.html');
const profileNavigation = await evaluate(`(() => ({
brandHref: document.querySelector('.site-header .brand')?.getAttribute('href'),
hasHome: Array.from(document.querySelectorAll('.site-header .nav-links a')).some((link) => link.textContent.trim() === '首页'),
userName: document.querySelector('.site-header .nav-user-name')?.textContent.trim(),
userHref: document.querySelector('.site-header .nav-user-entry')?.getAttribute('href'),
hasAvatar: Boolean(document.querySelector('.site-header .nav-user-avatar img')),
hasLegacyAccountLinks: Array.from(document.querySelectorAll('.site-header .nav-actions a')).some((link) => ['个人中心', '账户资料'].includes(link.textContent.trim())),
hasRedundantControls: Boolean(document.querySelector('[data-page-navigation-controls], [data-navigation-back]')),
hasHorizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth
}))()`);
assert.deepEqual(profileNavigation, {
brandHref: 'index.html',
hasHome: true,
userName: '叶用户',
userHref: 'profile.html',
hasAvatar: true,
hasLegacyAccountLinks: false,
hasRedundantControls: false,
hasHorizontalOverflow: false
});
console.log('BROWSER: home logo and primary navigation structure passed');
} else {
let writesBefore;
await assertEmptyAuthFormBlocked({ page: 'login.html', form: '#login-password-form' });
@@ -1405,8 +1517,9 @@ try {
await runProfileOperationAudit();
}
assert.deepEqual(consoleErrors, []);
console.log(`BROWSER CLICK SMOKE: ${28 + contentDetailJourneys.length + inlineDetailJourneys.length + mobileRowActionPages.length + emptyEditorFlows.length + emptyManagementFlows.length + familyWorkspacePages.length + accountWorkspacePages.length + familyNavigationTargets.length + accountNavigationTargets.length} flows passed`);
}
assert.deepEqual(consoleErrors, []);
} finally {
if (client) client.close();
const chromeExited = chrome.exitCode === null
+113
View File
@@ -0,0 +1,113 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const createConfig = require('../config.js');
const projectRoot = path.resolve(__dirname, '..');
function read(relativePath) {
return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8');
}
test('NavigationUtil 在隔离的新窗口中打开有效地址', () => {
const openedWindow = { opener: 'source-window' };
const calls = [];
const root = {
location: { hostname: 'localhost', port: '5500' },
open(...args) {
calls.push(args);
return openedWindow;
}
};
createConfig(root);
const result = root.NavigationUtil.open('profile.html');
assert.equal(result, openedWindow);
assert.deepEqual(calls, [['profile.html', '_blank', 'noopener']]);
assert.equal(openedWindow.opener, null);
});
test('NavigationUtil 拒绝可执行协议', () => {
let openCount = 0;
const root = {
location: { hostname: 'localhost', port: '5500' },
open() {
openCount += 1;
}
};
createConfig(root);
assert.equal(root.NavigationUtil.open('javascript:alert(1)'), null);
assert.equal(root.NavigationUtil.open('data:text/html,unsafe'), null);
assert.equal(openCount, 0);
});
test('所有页面在公共交互脚本前加载全站跳转配置', () => {
const htmlFiles = fs.readdirSync(projectRoot).filter((file) => file.endsWith('.html'));
htmlFiles.forEach((file) => {
const source = read(file);
const configIndex = source.indexOf('<script src="config.js"></script>');
const effectsIndex = source.indexOf('<script src="public/js/page-effects.js"></script>');
assert.ok(configIndex >= 0, `${file} 缺少 config.js`);
assert.ok(effectsIndex > configIndex, `${file} 必须先加载 config.js`);
});
});
test('全站链接覆盖初始和动态生成的有效链接', () => {
const source = read('public/js/page-effects.js');
assert.match(source, /link\.target = "_blank"/);
assert.match(source, /link\.setAttribute\("rel", \[\.\.\.relValues, "noopener"\]/);
assert.match(source, /new MutationObserver/);
assert.match(source, /attributeFilter: \["href", "target", "download"\]/);
});
test('Logo 和首页提供稳定入口,主导航不注入重复返回操作', () => {
const htmlFiles = fs.readdirSync(projectRoot).filter((file) => file.endsWith('.html'));
const effects = read('public/js/page-effects.js');
const profileCommon = read('public/js/profile-common.js');
htmlFiles.forEach((file) => {
const source = read(file);
const brandLinks = [...source.matchAll(/<a\s+class="[^"]*\bbrand\b[^"]*"\s+href="([^"]+)"/g)];
brandLinks.forEach((match) => {
assert.equal(match[1], 'index.html', `${file} 的 Logo 没有指向首页`);
});
});
assert.doesNotMatch(effects, /data-page-navigation-controls|data-navigation-back/);
assert.match(profileCommon, /\{ label: '首页', href: 'index\.html' \}/);
assert.match(profileCommon, /\.attr\('href', 'index\.html'\)/);
assert.doesNotMatch(profileCommon, /data-navigation-back/);
});
test('登录态顶栏以头像和昵称作为唯一账户入口', () => {
const effects = read('public/js/page-effects.js');
const authPages = read('public/js/auth-pages.js');
const profilePages = read('public/js/profile-pages.js');
assert.match(effects, /className = "nav-user-entry"/);
assert.match(effects, /className = "nav-user-avatar"/);
assert.match(effects, /name\.textContent = user\.nickName/);
assert.doesNotMatch(effects, /loginLink\.textContent = "个人中心"/);
assert.doesNotMatch(effects, /registerLink\.textContent = "账户资料"/);
assert.match(authPages, /storeUser\(await api\.currentProfile\(\)\)/);
assert.match(profilePages, /PublicNavigation\.storeUser\(profile\)/);
});
test('业务脚本不再直接覆盖当前窗口地址', () => {
const scriptsDirectory = path.join(projectRoot, 'public', 'js');
const scriptFiles = fs.readdirSync(scriptsDirectory).filter((file) => file.endsWith('.js'));
scriptFiles.forEach((file) => {
assert.doesNotMatch(
read(path.join('public', 'js', file)),
/(?:root|window)\.location\.(?:href\s*=|replace\()/,
`${file} 仍在当前窗口跳转`
);
});
});