Files
CyberStrikeAI/web/static/js/project-folder-preview.test.cjs
T
RuoJi6andGitHub eb6bab574f 优化项目对话、刷新续流、实时滚动与工具状态恢复 (#245)
* feat(chat): add project-based conversation sidebar

* feat(chat): refine Codex-style conversation UI

* feat(chat): add Codex-style conversation workflow

* feat(ui): 优化对话框与项目侧边栏交互

* fix(chat): 修复暗色输入框圆角填色

* fix(chat): 恢复输入区分层错位布局

* fix(hitl): isolate reviewer state per conversation

* feat(hitl): 增加双入口审批与倒计时进度

* fix(hitl): 汇总项目审批并隔离对话状态

* fix(ui): 修复审批状态与无项目新任务

* fix(ui): 优化审批状态与对话切换性能

* fix(ui): 修复中断任务审批仍计时

* fix(ui): 修复多对话并发切换卡顿

* fix(hitl): 主动同步审批并关闭中断状态

* fix(ui): 固定项目审批汇总为绿色

* feat(ui): 同步系统模型与推理强度

* feat(ui): 完善项目侧栏预览与新建入口

* fix(ui): 防止无项目文件夹误展开

* fix(ui): 防止长历史对话滚动误触审批

* fix(ui): 修复对话操作并补充项目置顶

* fix(ui): 移除对话分组并调整项目置顶排序

* feat(ui): 优化迭代导航与审批交互

* fix(hitl): 将 write_file 加入内置免审批工具

* fix(chat): 支持回车发送与 Shift 回车换行

* fix(chat): 优化对话刷新与 Codex 风格交互

* fix(ui): 显示对话具体更新时间

* fix(ui): 优化对话刷新与项目加载

* fix(ui): 修复 Agent 审查文字裁切

* fix(chat): 修复刷新续流与多标签页同步

* fix(chat): 修复滚动跟随与中断任务终态

* fix(ui): 修复流式滚动跳动与工具状态恢复

* fix(ui): 修复刷新后流式输出停止粘底
2026-08-13 21:28:37 +08:00

125 lines
7.5 KiB
JavaScript

const fs = require('node:fs');
const test = require('node:test');
const assert = require('node:assert/strict');
const projects = fs.readFileSync('web/static/js/projects.js', 'utf8');
const styles = fs.readFileSync('web/static/css/style.css', 'utf8');
const chat = fs.readFileSync('web/static/js/chat.js', 'utf8');
const html = fs.readFileSync('web/templates/index.html', 'utf8');
const rbac = fs.readFileSync('web/static/js/rbac-guards.js', 'utf8');
const zh = fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8');
const en = fs.readFileSync('web/static/i18n/en-US.json', 'utf8');
function functionSource(source, name, nextName) {
const start = source.indexOf(`function ${name}(`);
const end = source.indexOf(`function ${nextName}(`, start);
assert.notEqual(start, -1, `${name} should exist`);
assert.notEqual(end, -1, `${nextName} should follow ${name}`);
return source.slice(start, end);
}
test('无项目文件夹与普通项目共用悬浮和键盘聚焦预览', () => {
const source = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
assert.match(source, /row\.addEventListener\('mouseenter', \(\) => scheduleShowProjectFolderPreview/);
assert.match(source, /button\.addEventListener\('focus', \(\) => scheduleShowProjectFolderPreview/);
assert.doesNotMatch(
source,
/if \(!isUnassigned\) \{\s*row\.addEventListener\('mouseenter', \(\) => scheduleShowProjectFolderPreview/
);
});
test('无项目预览隐藏测试范围和编辑入口', () => {
const source = functionSource(projects, 'showProjectFolderPreview', 'scheduleShowProjectFolderPreview');
assert.match(source, /preview\.classList\.toggle\('is-unassigned', isUnassigned\)/);
assert.match(source, /scopeRow\.hidden = isUnassigned \|\| !scope/);
assert.match(source, /editButton\.hidden = isUnassigned/);
assert.match(styles, /\.project-folder-preview\.is-unassigned \.project-folder-preview-edit\s*\{\s*display: none !important;/);
assert.match(styles, /\.project-folder-preview\.is-unassigned \.project-folder-preview-details\s*\{\s*border-bottom: 0;/);
});
test('项目标题提供受权限保护的新建项目入口', () => {
const source = functionSource(projects, 'showNewProjectModalFromChatSidebar', 'saveProjectModal');
assert.match(html, /class="add-group-btn project-folders-add-btn"[\s\S]*?onclick="showNewProjectModalFromChatSidebar\(\)"/);
assert.match(chat, /projectHeader\.querySelector\('\.project-folders-add-btn'\)/);
assert.match(source, /window\._projectModalFromChat = false/);
assert.match(source, /window\._projectModalFromChatSidebar = true/);
assert.match(rbac, /showNewProjectModalFromChatSidebar: 'project:write'/);
});
test('对话项目归属尚未加载时不会误展开无项目', () => {
const resolver = functionSource(projects, 'resolveChatProjectFolderSelection', 'renderChatProjectFolders');
const render = functionSource(projects, 'renderChatProjectFolders', 'refreshChatProjectFolders');
assert.match(resolver, /if \(!chatProjectFolderContext\.ready\) return null/);
assert.match(resolver, /if \(!conversation\) return null/);
assert.match(resolver, /conversation\.projectId \|\| conversation\.project_id \|\| ''/);
assert.match(render, /const selectedId = resolveChatProjectFolderSelection\(\)/);
assert.match(render, /selectedId !== null && chatProjectFolderLastSelectionId !== selectedId/);
});
test('项目按展开状态切换 Codex 风格的打开和关闭文件夹', () => {
const icon = functionSource(projects, 'projectFolderIconMarkup', 'clampProjectPreviewText');
const folder = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
assert.match(icon, /const path = isExpanded/);
assert.match(icon, /M3\.5 18V6\.5/);
assert.match(icon, /M3\.5 7a2 2 0 0 1 2-2h4l2 2/);
assert.match(folder, /icon\.className = 'project-folder-icon';/);
assert.match(folder, /icon\.innerHTML = projectFolderIconMarkup\(isExpanded\);/);
});
test('项目名仅在界面按 12 个 Unicode 字符省略并保留完整悬浮信息', () => {
const formatterSource = functionSource(chat, 'formatProjectNameForDisplay', 'applyProjectNameDisplay');
const formatter = new Function(
'PROJECT_NAME_DISPLAY_MAX_CHARACTERS',
`${formatterSource}; return formatProjectNameForDisplay;`
)(12);
const folder = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
const picker = functionSource(projects, 'appendChatProjectPanelItem', 'appendChatProjectPanelMessage');
const button = functionSource(projects, 'updateChatProjectButtonLabel', 'renderChatProjectPanel');
assert.equal(formatter('十二字符以内'), '十二字符以内');
assert.equal(formatter('这是一个非常非常长的项目名称'), '这是一个非常非常长的项目…');
assert.equal(formatter('😀😀😀😀😀😀😀😀😀😀😀😀😀'), '😀😀😀😀😀😀😀😀😀😀😀😀…');
assert.match(chat, /const PROJECT_NAME_DISPLAY_MAX_CHARACTERS = 12/);
assert.match(folder, /applyProjectNameDisplay\(title, project\.name/);
assert.match(projects, /applyProjectNameDisplay\(titleEl, text\)/);
assert.match(picker, /title="\$\{escapeAttr\(fullName\)\}"/);
assert.match(picker, /setAttribute\('aria-label', fullName\)/);
assert.match(button, /applyProjectNameDisplay/);
assert.match(styles, /\.project-selector-wrapper \.role-selector-text\s*\{[\s\S]*?max-width: 13em/);
});
test('项目文件夹首批显示 6 个并通过加载更多按批追加', () => {
const loadMore = functionSource(projects, 'loadMoreChatProjectFolders', 'renderChatProjectFolders');
const render = functionSource(projects, 'renderChatProjectFolders', 'refreshChatProjectFolders');
const search = functionSource(projects, 'handleProjectFolderSearch', 'clearProjectFolderSearch');
assert.match(projects, /const CHAT_PROJECT_FOLDER_PAGE_SIZE = 6/);
assert.match(loadMore, /chatProjectFolderVisibleCount \+= CHAT_PROJECT_FOLDER_PAGE_SIZE/);
assert.match(render, /const visibleFolders = folders\.slice\(0, chatProjectFolderVisibleCount\)/);
assert.match(render, /appendChatProjectFoldersLoadMore\(list, folders\.length - visibleFolders\.length\)/);
assert.match(render, /chatProjectFolderVisibleCount = selectedIndex \+ 1/);
assert.match(search, /renderChatProjectFolders\(projectsCacheAll\)/);
assert.match(styles, /\.project-folders-load-more\s*\{/);
assert.match(zh, /"projectFoldersLoadMoreRemaining": "加载更多,剩余 \{\{count\}\} 个项目"/);
assert.match(en, /"projectFoldersLoadMoreRemaining": "Load more, \{\{count\}\} projects remaining"/);
});
test('对话悬浮预览显示本地年月日时分', () => {
const age = functionSource(projects, 'formatProjectConversationPreviewAge', 'getProjectConversationModeLabel');
assert.match(age, /date\.getFullYear\(\)/);
assert.match(age, /date\.getMonth\(\) \+ 1/);
assert.match(age, /date\.getDate\(\)/);
assert.match(age, /date\.getHours\(\)/);
assert.match(age, /date\.getMinutes\(\)/);
assert.match(age, /chat\.conversationPreviewDateTime/);
assert.doesNotMatch(age, /elapsedMs|conversationPreviewDays|conversationPreviewHours/);
assert.match(zh, /"conversationPreviewDateTime": "\{\{year\}\}年\{\{month\}\}月\{\{day\}\}日 \{\{hour\}\}:\{\{minute\}\}"/);
assert.match(en, /"conversationPreviewDateTime": "\{\{year\}\}-\{\{month\}\}-\{\{day\}\} \{\{hour\}\}:\{\{minute\}\}"/);
});