test: restore project verification suite

This commit is contained in:
boos
2026-06-06 03:35:29 +08:00
parent e4630c4cb6
commit fae0f607aa
12 changed files with 55 additions and 46 deletions
+2 -2
View File
@@ -38,8 +38,8 @@
"workflow-ui:install": "node scripts/workflow-ui/install.mjs all", "workflow-ui:install": "node scripts/workflow-ui/install.mjs all",
"workflow-ui:install:external": "node scripts/workflow-ui/install.mjs external", "workflow-ui:install:external": "node scripts/workflow-ui/install.mjs external",
"workflow-ui:install:cloud": "node scripts/workflow-ui/install.mjs cloud", "workflow-ui:install:cloud": "node scripts/workflow-ui/install.mjs cloud",
"test": "vitest run --root .", "test": "vitest run --root src/web",
"test:watch": "vitest --root ." "test:watch": "vitest --root src/web"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
+2
View File
@@ -6,6 +6,7 @@ export const USER_PROFILE_STORAGE_KEY = 'user_profile';
export const ADMIN_PROFILE_STORAGE_KEY = 'admin_profile'; export const ADMIN_PROFILE_STORAGE_KEY = 'admin_profile';
export const LEGACY_USER_PROFILE_STORAGE_KEY = USER_PROFILE_STORAGE_KEY; export const LEGACY_USER_PROFILE_STORAGE_KEY = USER_PROFILE_STORAGE_KEY;
export const FIRST_USE_DOC_REMINDER_KEY = 'boosapi_first_use_docs_reminder_seen_v1'; export const FIRST_USE_DOC_REMINDER_KEY = 'boosapi_first_use_docs_reminder_seen_v1';
export const LEGACY_FIRST_USE_DOC_REMINDER_KEY = 'metapi_first_use_docs_reminder_seen_v1';
type StorageLike = { type StorageLike = {
getItem?: (key: string) => string | null; getItem?: (key: string) => string | null;
@@ -28,4 +29,5 @@ export function clearAppInstallationState(storage?: StorageLike | null): void {
target.removeItem(USER_PROFILE_STORAGE_KEY); target.removeItem(USER_PROFILE_STORAGE_KEY);
target.removeItem(ADMIN_PROFILE_STORAGE_KEY); target.removeItem(ADMIN_PROFILE_STORAGE_KEY);
target.removeItem(FIRST_USE_DOC_REMINDER_KEY); target.removeItem(FIRST_USE_DOC_REMINDER_KEY);
target.removeItem(LEGACY_FIRST_USE_DOC_REMINDER_KEY);
} }
+7 -6
View File
@@ -8,11 +8,12 @@ export function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
const targetWindow = window;
const query = breakpoint === MOBILE_BREAKPOINT const query = breakpoint === MOBILE_BREAKPOINT
? MOBILE_MEDIA_QUERY ? MOBILE_MEDIA_QUERY
: `(max-width: ${breakpoint}px)`; : `(max-width: ${breakpoint}px)`;
const media = typeof window.matchMedia === 'function' ? window.matchMedia(query) : null; const media = typeof targetWindow.matchMedia === 'function' ? targetWindow.matchMedia(query) : null;
const update = () => setIsMobile(media ? media.matches : window.innerWidth <= breakpoint); const update = () => setIsMobile(media ? media.matches : targetWindow.innerWidth <= breakpoint);
update(); update();
if (media && typeof media.addEventListener === 'function') { if (media && typeof media.addEventListener === 'function') {
@@ -24,14 +25,14 @@ export function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
}; };
} }
const addResizeListener = typeof window.addEventListener === 'function'; const addResizeListener = typeof targetWindow.addEventListener === 'function';
const removeResizeListener = typeof window.removeEventListener === 'function'; const removeResizeListener = typeof targetWindow.removeEventListener === 'function';
if (!addResizeListener) return; if (!addResizeListener) return;
window.addEventListener('resize', update); targetWindow.addEventListener('resize', update);
return () => { return () => {
if (removeResizeListener) { if (removeResizeListener) {
window.removeEventListener('resize', update); targetWindow.removeEventListener('resize', update);
} }
}; };
}, [breakpoint]); }, [breakpoint]);
+5
View File
@@ -13,6 +13,7 @@ const { apiMock } = vi.hoisted(() => ({
getAccounts: vi.fn(), getAccounts: vi.fn(),
getAccountsSnapshot: vi.fn(), getAccountsSnapshot: vi.fn(),
getAccountTokens: vi.fn(), getAccountTokens: vi.fn(),
getSites: vi.fn(),
getDownstreamApiKeyOverview: vi.fn(), getDownstreamApiKeyOverview: vi.fn(),
getDownstreamApiKeyTrend: vi.fn(), getDownstreamApiKeyTrend: vi.fn(),
createDownstreamApiKey: vi.fn(), createDownstreamApiKey: vi.fn(),
@@ -166,6 +167,10 @@ beforeEach(() => {
}, },
}, },
]); ]);
apiMock.getSites.mockResolvedValue([
{ id: 201, name: '站点A', status: 'active' },
{ id: 202, name: '站点B', status: 'active' },
]);
apiMock.getAccountTokens.mockResolvedValue([ apiMock.getAccountTokens.mockResolvedValue([
{ {
id: 301, id: 301,
+6 -6
View File
@@ -214,7 +214,7 @@ describe('ImportExport', () => {
success: true, success: true,
config: { config: {
enabled: true, enabled: true,
fileUrl: 'https://dav.example.com/backups/metapi.json', fileUrl: 'https://dav.example.com/backups/boosapi.json',
username: 'alice', username: 'alice',
exportType: 'all', exportType: 'all',
autoSyncEnabled: true, autoSyncEnabled: true,
@@ -231,7 +231,7 @@ describe('ImportExport', () => {
success: true, success: true,
config: { config: {
enabled: true, enabled: true,
fileUrl: 'https://dav.example.com/backups/metapi.json', fileUrl: 'https://dav.example.com/backups/boosapi.json',
username: 'alice', username: 'alice',
exportType: 'all', exportType: 'all',
autoSyncEnabled: true, autoSyncEnabled: true,
@@ -246,7 +246,7 @@ describe('ImportExport', () => {
}); });
apiMock.exportBackupToWebdav.mockResolvedValue({ apiMock.exportBackupToWebdav.mockResolvedValue({
success: true, success: true,
fileUrl: 'https://dav.example.com/backups/metapi.json', fileUrl: 'https://dav.example.com/backups/boosapi.json',
exportType: 'all', exportType: 'all',
}); });
apiMock.importBackupFromWebdav.mockResolvedValue({ apiMock.importBackupFromWebdav.mockResolvedValue({
@@ -459,7 +459,7 @@ describe('ImportExport', () => {
const fileUrlInput = root!.root.findAll((node) => ( const fileUrlInput = root!.root.findAll((node) => (
node.type === 'input' node.type === 'input'
&& node.props.placeholder === 'https://dav.example.com/backups/metapi.json' && node.props.placeholder === 'https://dav.example.com/backups/boosapi.json'
)).at(-1); )).at(-1);
const cronInput = root!.root.findAll((node) => ( const cronInput = root!.root.findAll((node) => (
node.type === 'input' node.type === 'input'
@@ -507,7 +507,7 @@ describe('ImportExport', () => {
const fileUrlInput = root!.root.findAll((node) => ( const fileUrlInput = root!.root.findAll((node) => (
node.type === 'input' node.type === 'input'
&& node.props.placeholder === 'https://dav.example.com/backups/metapi.json' && node.props.placeholder === 'https://dav.example.com/backups/boosapi.json'
)).at(-1); )).at(-1);
expect(fileUrlInput).toBeTruthy(); expect(fileUrlInput).toBeTruthy();
@@ -538,7 +538,7 @@ describe('ImportExport', () => {
success: true, success: true,
config: { config: {
enabled: true, enabled: true,
fileUrl: 'https://dav.example.com/backups/metapi.json', fileUrl: 'https://dav.example.com/backups/boosapi.json',
username: 'alice', username: 'alice',
exportType: 'all', exportType: 'all',
autoSyncEnabled: true, autoSyncEnabled: true,
@@ -789,7 +789,7 @@ describe('Models marketplace text', () => {
expect(String(latencyBadge.props.className || '')).toContain('badge-muted'); expect(String(latencyBadge.props.className || '')).toContain('badge-muted');
expect(collectText(latencyBadge)).toContain('延迟'); expect(collectText(latencyBadge)).toContain('延迟');
expect(collectText(latencyBadge)).toContain('—'); expect(collectText(latencyBadge)).toContain('暂无');
expect(collectText(root!.root)).not.toContain('680ms'); expect(collectText(root!.root)).not.toContain('680ms');
} finally { } finally {
root?.unmount(); root?.unmount();
@@ -520,7 +520,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
await flushMicrotasks(); await flushMicrotasks();
expect(globalThis.localStorage.setItem).toHaveBeenCalledWith('metapi.proxyLogs.debugTracePanelExpanded', 'false'); expect(globalThis.localStorage.setItem).toHaveBeenCalledWith('boosapi.proxyLogs.debugTracePanelExpanded', 'false');
await act(async () => { await act(async () => {
root.unmount(); root.unmount();
@@ -547,7 +547,7 @@ describe('ProxyLogs server-driven page', () => {
&& node.props['data-debug-trace-panel-body'] === true && node.props['data-debug-trace-panel-body'] === true
)); ));
expect(globalThis.localStorage.getItem).toHaveBeenCalledWith('metapi.proxyLogs.debugTracePanelExpanded'); expect(globalThis.localStorage.getItem).toHaveBeenCalledWith('boosapi.proxyLogs.debugTracePanelExpanded');
expect(restoredToggleButton.props['aria-expanded']).toBe(false); expect(restoredToggleButton.props['aria-expanded']).toBe(false);
expect(String(restoredPanelBody.props.className || '')).not.toContain('is-open'); expect(String(restoredPanelBody.props.className || '')).not.toContain('is-open');
} finally { } finally {
@@ -8,7 +8,7 @@ describe('Dashboard mobile layout', () => {
expect(source).toContain('import { useIsMobile } from "../components/useIsMobile.js";'); expect(source).toContain('import { useIsMobile } from "../components/useIsMobile.js";');
expect(source).toContain('const isMobile = useIsMobile()'); expect(source).toContain('const isMobile = useIsMobile()');
expect(source).toContain('gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr"'); expect(source).toContain('className="dashboard-chart-grid"');
expect(source).toContain('gridTemplateColumns: isMobile ? "1fr" : "1fr 300px"'); expect(source).toContain('gridTemplateColumns: isMobile ? "1fr" : "1fr 300px"');
}); });
}); });
@@ -90,7 +90,7 @@ describe('modelTesterSession', () => {
name: 'draft.pdf', name: 'draft.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
dataUrl: 'data:application/pdf;base64,JVBERi0x', dataUrl: 'data:application/pdf;base64,JVBERi0x',
fileId: 'file-metapi-draft', fileId: 'file-boosapi-draft',
status: 'uploaded', status: 'uploaded',
errorMessage: null, errorMessage: null,
}, },
@@ -243,7 +243,7 @@ describe('modelTesterSession', () => {
it('creates user conversation messages that preserve uploaded file references', () => { it('creates user conversation messages that preserve uploaded file references', () => {
const message = createConversationUserMessage('请总结附件', [ const message = createConversationUserMessage('请总结附件', [
{ {
fileId: 'file-metapi-123', fileId: 'file-boosapi-123',
filename: 'paper.pdf', filename: 'paper.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
}, },
@@ -254,7 +254,7 @@ describe('modelTesterSession', () => {
expect(message.parts).toEqual([ expect(message.parts).toEqual([
{ {
type: 'input_file', type: 'input_file',
fileId: 'file-metapi-123', fileId: 'file-boosapi-123',
filename: 'paper.pdf', filename: 'paper.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
}, },
@@ -295,7 +295,7 @@ describe('modelTesterSession', () => {
parts: [ parts: [
{ {
type: 'input_file', type: 'input_file',
fileId: 'file-metapi-456', fileId: 'file-boosapi-456',
filename: 'appendix.txt', filename: 'appendix.txt',
mimeType: 'text/plain', mimeType: 'text/plain',
}, },
@@ -304,7 +304,7 @@ describe('modelTesterSession', () => {
expect(files).toEqual([ expect(files).toEqual([
{ {
fileId: 'file-metapi-456', fileId: 'file-boosapi-456',
filename: 'appendix.txt', filename: 'appendix.txt',
mimeType: 'text/plain', mimeType: 'text/plain',
}, },
@@ -322,7 +322,7 @@ describe('modelTesterSession', () => {
const files = await resolveConversationReplayFiles?.( const files = await resolveConversationReplayFiles?.(
[ [
{ {
fileId: 'file-metapi-123', fileId: 'file-boosapi-123',
filename: 'brief.pdf', filename: 'brief.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
}, },
@@ -331,10 +331,10 @@ describe('modelTesterSession', () => {
loader, loader,
); );
expect(loader).toHaveBeenCalledWith('file-metapi-123'); expect(loader).toHaveBeenCalledWith('file-boosapi-123');
expect(files).toEqual([ expect(files).toEqual([
{ {
fileId: 'file-metapi-123', fileId: 'file-boosapi-123',
filename: 'brief.pdf', filename: 'brief.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
data: 'data:application/pdf;base64,JVBERi0x', data: 'data:application/pdf;base64,JVBERi0x',
@@ -564,7 +564,7 @@ describe('modelTesterSession', () => {
parts: [ parts: [
{ {
type: 'input_file' as const, type: 'input_file' as const,
fileId: 'file-metapi-123', fileId: 'file-boosapi-123',
filename: 'brief.pdf', filename: 'brief.pdf',
mimeType: 'application/pdf', mimeType: 'application/pdf',
data: 'data:application/pdf;base64,JVBERi0x', data: 'data:application/pdf;base64,JVBERi0x',
@@ -592,7 +592,7 @@ describe('modelTesterSession', () => {
{ {
type: 'file', type: 'file',
file: { file: {
file_id: 'file-metapi-123', file_id: 'file-boosapi-123',
filename: 'brief.pdf', filename: 'brief.pdf',
mime_type: 'application/pdf', mime_type: 'application/pdf',
}, },
@@ -625,7 +625,7 @@ describe('modelTesterSession', () => {
{ type: 'input_text', text: 'reuse native file id' }, { type: 'input_text', text: 'reuse native file id' },
{ {
type: 'input_file', type: 'input_file',
file_id: 'file-metapi-123', file_id: 'file-boosapi-123',
filename: 'brief.pdf', filename: 'brief.pdf',
}, },
], ],
@@ -105,7 +105,7 @@ describe('Settings model availability probe confirmation', () => {
const toggleLabel = root.root.find((node) => ( const toggleLabel = root.root.find((node) => (
node.type === 'label' node.type === 'label'
&& collectText(node).includes('允许 metapi 后台主动批量测活') && collectText(node).includes('允许 BoosAPI 后台主动批量测活')
)); ));
const toggle = toggleLabel.findByType('input'); const toggle = toggleLabel.findByType('input');
expect(toggle.props.checked).toBe(false); expect(toggle.props.checked).toBe(false);
@@ -109,7 +109,7 @@ describe('Settings proxy transport', () => {
const websocketToggleLabel = root.root.find((node) => ( const websocketToggleLabel = root.root.find((node) => (
node.type === 'label' node.type === 'label'
&& collectText(node).includes('允许 metapi 到 Codex 上游使用 WebSocket') && collectText(node).includes('允许 BoosAPI 到 Codex 上游使用 WebSocket')
)); ));
const websocketToggle = websocketToggleLabel.findByType('input'); const websocketToggle = websocketToggleLabel.findByType('input');
expect(websocketToggle.props.checked).toBe(false); expect(websocketToggle.props.checked).toBe(false);
+16 -15
View File
@@ -264,19 +264,19 @@ describe('Sites create redirect', () => {
const platformSelect = selects.at(-1); const platformSelect = selects.at(-1);
expect(platformSelect?.props.options).toEqual( expect(platformSelect?.props.options).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ label: '阿里云 CodingPlan / OpenAI' }), expect.objectContaining({ label: '阿里云 Coding Plan (OpenAI)' }),
expect.objectContaining({ label: '阿里云 CodingPlan / Claude' }), expect.objectContaining({ label: '阿里云 Coding Plan (Claude)' }),
expect.objectContaining({ label: '智谱 Coding Plan / OpenAI' }), expect.objectContaining({ label: '智谱清言 Coding Plan (OpenAI)' }),
expect.objectContaining({ label: '智谱 Coding Plan / Claude' }), expect.objectContaining({ label: '智谱清言 Coding Plan (Claude)' }),
expect.objectContaining({ label: 'DeepSeek / OpenAI' }), expect.objectContaining({ label: 'DeepSeek (OpenAI)' }),
expect.objectContaining({ label: 'DeepSeek / Claude' }), expect.objectContaining({ label: 'DeepSeek (Claude)' }),
expect.objectContaining({ label: 'Moonshot(Kimi) / OpenAI' }), expect.objectContaining({ label: 'Moonshot (OpenAI)' }),
expect.objectContaining({ label: 'Moonshot(Kimi) / Claude' }), expect.objectContaining({ label: 'Moonshot (Claude)' }),
expect.objectContaining({ label: 'MiniMax / OpenAI' }), expect.objectContaining({ label: 'MiniMax (OpenAI)' }),
expect.objectContaining({ label: 'MiniMax / Claude' }), expect.objectContaining({ label: 'MiniMax (Claude)' }),
expect.objectContaining({ label: 'ModelScope / OpenAI' }), expect.objectContaining({ label: 'ModelScope (OpenAI)' }),
expect.objectContaining({ label: 'ModelScope / Claude' }), expect.objectContaining({ label: 'ModelScope (Claude)' }),
expect.objectContaining({ label: '豆包 Coding Plan / OpenAI' }), expect.objectContaining({ label: '豆包 Coding Plan (OpenAI)' }),
]), ]),
); );
} finally { } finally {
@@ -366,11 +366,12 @@ describe('Sites create redirect', () => {
await flushMicrotasks(); await flushMicrotasks();
expect(urlInput.props.value).toBe('https://open.bigmodel.cn/api/coding/paas/v4'); expect(urlInput.props.value).toBe('https://open.bigmodel.cn/api/coding/paas/v4');
expect(JSON.stringify(root.toJSON())).toContain('智谱 Coding Plan / OpenAI'); expect(JSON.stringify(root.toJSON())).toContain('智谱清言 Coding Plan (OpenAI)');
const presetAlerts = root.root.findAll((node) => ( const presetAlerts = root.root.findAll((node) => (
typeof node.props.className === 'string' typeof node.props.className === 'string'
&& node.props.className.includes('alert alert-info') && node.props.className.includes('alert alert-info')
&& collectText(node).includes('已应用官方预设')
)); ));
expect(presetAlerts).toHaveLength(1); expect(presetAlerts).toHaveLength(1);
expect(collectText(presetAlerts[0]!)).toContain('已应用官方预设'); expect(collectText(presetAlerts[0]!)).toContain('已应用官方预设');
@@ -645,7 +646,7 @@ describe('Sites create redirect', () => {
)); ));
expect(platformSelect?.props.value).toBe('preset:deepseek-openai'); expect(platformSelect?.props.value).toBe('preset:deepseek-openai');
expect(collectText(presetAlert)).toContain('已应用官方预设'); expect(collectText(presetAlert)).toContain('已应用官方预设');
expect(collectText(presetAlert)).toContain('DeepSeek / OpenAI'); expect(collectText(presetAlert)).toContain('DeepSeek (OpenAI)');
} finally { } finally {
root?.unmount(); root?.unmount();
} }