158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
import { flushPromises } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createEmptyBook, createEmptyTeachingDesign, type TeachingBook } from '../domain/teachingDesign'
|
|
import * as booksApi from '../services/booksApi'
|
|
import { useTeachingBook } from './useTeachingBook'
|
|
|
|
vi.mock('../services/booksApi')
|
|
|
|
function mockGetBook(data: TeachingBook, id = 'b1'): void {
|
|
vi.mocked(booksApi.getBook).mockResolvedValue({ id, name: '示例整本', updatedAt: data.updatedAt, data })
|
|
}
|
|
|
|
describe('useTeachingBook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.useFakeTimers()
|
|
})
|
|
|
|
it('loads the book from the API', async () => {
|
|
const data = createEmptyBook()
|
|
data.cover.courseName = 'Web 前端开发'
|
|
mockGetBook(data)
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
expect(booksApi.getBook).toHaveBeenCalledWith('b1')
|
|
expect(store.loadStatus.value).toBe('loaded')
|
|
expect(store.book.value.cover.courseName).toBe('Web 前端开发')
|
|
})
|
|
|
|
it('sets loadStatus to error when loading fails', async () => {
|
|
vi.mocked(booksApi.getBook).mockRejectedValue(new Error('网络错误。'))
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
expect(store.loadStatus.value).toBe('error')
|
|
expect(store.loadError.value).toBe('网络错误。')
|
|
})
|
|
|
|
it('imports files in natural order and selects the first lesson', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
const files = [
|
|
new File(['# 第十课 教学设计'], '10.md', { type: 'text/markdown' }),
|
|
new File(['# 第二课 教学设计'], '2.md', { type: 'text/markdown' }),
|
|
]
|
|
|
|
await store.importFiles(files, 'keep')
|
|
|
|
expect(store.book.value.designs.map((design) => design.originalFilename)).toEqual(['2.md', '10.md'])
|
|
expect(store.book.value.selectedId).toBe(store.book.value.designs[0]?.id)
|
|
})
|
|
|
|
it('reorders lessons without changing their identities', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
await store.importFiles(
|
|
[new File(['# One 教学设计'], '1.md'), new File(['# Two 教学设计'], '2.md')],
|
|
'keep',
|
|
)
|
|
|
|
const ids = store.book.value.designs.map((design) => design.id)
|
|
store.moveDesign(0, 1)
|
|
|
|
expect(store.book.value.designs.map((design) => design.id)).toEqual(ids.reverse())
|
|
})
|
|
|
|
it('does not autosave immediately after the initial load', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
await vi.advanceTimersByTimeAsync(300)
|
|
|
|
expect(booksApi.updateBook).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('autosaves the book via the API after the debounce delay', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
vi.mocked(booksApi.updateBook).mockResolvedValue({ id: 'b1', name: '示例整本', updatedAt: 'later' })
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
store.updateCover({ courseName: '新课程名' })
|
|
await vi.advanceTimersByTimeAsync(300)
|
|
|
|
expect(booksApi.updateBook).toHaveBeenCalledWith('b1', store.book.value)
|
|
expect(store.saveStatus.value).toBe('saved')
|
|
})
|
|
|
|
it('sets saveStatus to error when autosave fails', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
vi.mocked(booksApi.updateBook).mockRejectedValue(new Error('保存失败。'))
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
store.updateCover({ courseName: '新课程名' })
|
|
await vi.advanceTimersByTimeAsync(300)
|
|
|
|
expect(store.saveStatus.value).toBe('error')
|
|
expect(store.lastError.value).toBe('保存失败。')
|
|
})
|
|
|
|
it('generateLesson appends a parsed design and selects it', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
vi.mocked(booksApi.generateLesson).mockResolvedValue({
|
|
filename: 'css-flex.md',
|
|
markdown: '# CSS 弹性布局 教学设计',
|
|
})
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
const result = await store.generateLesson('CSS 弹性布局')
|
|
|
|
expect(result).toEqual({ ok: true })
|
|
expect(store.book.value.designs).toHaveLength(1)
|
|
expect(store.book.value.selectedId).toBe(store.book.value.designs[0]?.id)
|
|
})
|
|
|
|
it('generateLesson returns an error when the API call fails', async () => {
|
|
mockGetBook(createEmptyBook())
|
|
vi.mocked(booksApi.generateLesson).mockRejectedValue(new Error('Deepseek 请求失败。'))
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
const result = await store.generateLesson('CSS 弹性布局')
|
|
|
|
expect(result).toEqual({ ok: false, message: 'Deepseek 请求失败。' })
|
|
expect(store.book.value.designs).toHaveLength(0)
|
|
})
|
|
|
|
it('clearBook empties designs but keeps the cover', async () => {
|
|
const data = createEmptyBook()
|
|
data.cover.courseName = 'Web 前端开发'
|
|
data.designs.push(createEmptyTeachingDesign('1.md'))
|
|
mockGetBook(data)
|
|
|
|
const store = useTeachingBook('b1')
|
|
await flushPromises()
|
|
|
|
store.clearBook()
|
|
|
|
expect(store.book.value.designs).toEqual([])
|
|
expect(store.book.value.cover.courseName).toBe('Web 前端开发')
|
|
expect(store.book.value.selectedId).toBe('cover')
|
|
})
|
|
})
|