修改页面,完成单页模式新增首页返回上一页按钮
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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} 仍在当前窗口跳转`
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user