feat: add Husky hooks and fix provider tutorial UI/logic

- Add Husky pre-commit and pre-push hooks for linting, type checking, and build validation
- Fix visual hierarchy bug in the provider onboarding tutorial
- Remove feedback modal
- Move MinIO bucket creation logic to before app startup
- Wire MiniMax audio through voice generation pipeline
- Fix scene insertion issues
- Fix portal tutorial modal and harden panel variant task flow
This commit is contained in:
saturn
2026-03-09 02:42:35 +08:00
parent 881ed44996
commit fba480ae6e
34 changed files with 721 additions and 247 deletions

View File

@@ -0,0 +1,72 @@
import * as React from 'react'
import { createElement } from 'react'
import type { ComponentProps, ReactElement } from 'react'
import { describe, expect, it } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { NextIntlClientProvider } from 'next-intl'
import type { AbstractIntlMessages } from 'next-intl'
import LLMStageStreamCard from '@/components/llm-console/LLMStageStreamCard'
const messages = {
progress: {
status: {
completed: '已完成',
failed: '失败',
processing: '进行中',
queued: '排队中',
pending: '未开始',
},
stageCard: {
stage: '阶段',
realtimeStream: '实时流',
currentStage: '当前阶段',
outputTitle: 'AI 实时输出 · {stage}',
waitingModelOutput: '等待模型输出...',
reasoningNotProvided: '该步骤未返回思考过程',
},
runtime: {
llm: {
processing: '模型处理中...',
},
},
},
} as const
const renderWithIntl = (node: ReactElement) => {
const providerProps: ComponentProps<typeof NextIntlClientProvider> = {
locale: 'zh',
messages: messages as unknown as AbstractIntlMessages,
timeZone: 'Asia/Shanghai',
children: node,
}
return renderToStaticMarkup(
createElement(NextIntlClientProvider, providerProps),
)
}
describe('LLMStageStreamCard error rendering', () => {
it('renders the error without any feedback action entry', () => {
Reflect.set(globalThis, 'React', React)
const html = renderWithIntl(
createElement(LLMStageStreamCard, {
title: '内容到剧本',
stages: [{
id: 'story_to_script',
title: '内容到剧本',
status: 'failed',
progress: 0,
}],
activeStageId: 'story_to_script',
outputText: '',
errorMessage: 'Failed to fetch',
}),
)
expect(html).toContain('Failed to fetch')
expect(html).not.toContain('复制错误详情')
expect(html).not.toContain('打开问题反馈表单')
expect(html).not.toContain('Copy error detail')
expect(html).not.toContain('Open feedback form')
})
})

View File

@@ -0,0 +1,115 @@
import * as React from 'react'
import { createElement } from 'react'
import type { ComponentProps, ReactElement } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { NextIntlClientProvider } from 'next-intl'
import type { AbstractIntlMessages } from 'next-intl'
import Navbar from '@/components/Navbar'
const useSessionMock = vi.fn()
vi.mock('next-auth/react', () => ({
useSession: () => useSessionMock(),
}))
vi.mock('next/image', () => ({
default: ({ alt, ...props }: { alt: string } & Record<string, unknown>) => createElement('img', { alt, ...props }),
}))
vi.mock('@/components/LanguageSwitcher', () => ({
default: () => createElement('div', null, 'LanguageSwitcher'),
}))
vi.mock('@/hooks/common/useGithubReleaseUpdate', () => ({
useGithubReleaseUpdate: () => ({
currentVersion: '0.3.0',
update: null,
shouldPulse: false,
showModal: false,
openModal: () => undefined,
dismissCurrentUpdate: () => undefined,
checkNow: async () => undefined,
}),
}))
vi.mock('@/i18n/navigation', () => ({
Link: ({
href,
children,
...props
}: {
href: string | { pathname: string }
children: React.ReactNode
} & Record<string, unknown>) => {
const resolvedHref = typeof href === 'string' ? href : href.pathname
return createElement('a', { href: resolvedHref, ...props }, children)
},
}))
const messages = {
nav: {
workspace: '工作区',
assetHub: '资产中心',
profile: '设置中心',
downloadLogs: '下载日志',
signin: '登录',
signup: '注册',
},
common: {
appName: 'waoowaoo',
betaVersion: 'Beta v{version}',
updateNotice: {
openDialog: '打开更新弹窗',
updateTag: '更新',
checkUpdate: '检查更新',
upToDate: '已是最新版本',
},
},
} as const
const renderWithIntl = (node: ReactElement) => {
const providerProps: ComponentProps<typeof NextIntlClientProvider> = {
locale: 'zh',
messages: messages as unknown as AbstractIntlMessages,
timeZone: 'Asia/Shanghai',
children: node,
}
return renderToStaticMarkup(
createElement(NextIntlClientProvider, providerProps),
)
}
describe('Navbar download logs entry', () => {
beforeEach(() => {
useSessionMock.mockReset()
})
it('renders the download logs entry on the far-right action group for signed-in users', () => {
Reflect.set(globalThis, 'React', React)
useSessionMock.mockReturnValue({
data: { user: { name: 'Earth' } },
status: 'authenticated',
})
const html = renderWithIntl(createElement(Navbar))
expect(html).toContain('下载日志')
expect(html).toContain('href="/api/admin/download-logs"')
expect(html).toContain('download=""')
})
it('does not render the download logs entry for signed-out users', () => {
Reflect.set(globalThis, 'React', React)
useSessionMock.mockReturnValue({
data: null,
status: 'unauthenticated',
})
const html = renderWithIntl(createElement(Navbar))
expect(html).not.toContain('下载日志')
expect(html).not.toContain('/api/admin/download-logs')
})
})