返回 oh-my-ppt
use-generation-notifications.test.ts
根目录 / tests / unit / renderer / use-generation-notifications.test.ts
1 /**
2 * @vitest-environment happy-dom
3 */
4 import React, { act } from 'react'
5 import { createRoot } from 'react-dom/client'
6 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7 import type { GenerateChunkEvent } from '@shared/generation'
8
9 const { onGenerateChunkMock, navigateMock, getSessionMock, toastMock } = vi.hoisted(() => ({
10 onGenerateChunkMock: vi.fn(),
11 navigateMock: vi.fn(),
12 getSessionMock: vi.fn(),
13 toastMock: {
14 success: vi.fn(),
15 error: vi.fn(),
16 warning: vi.fn(),
17 info: vi.fn()
18 }
19 }))
20
21 vi.mock('react-router-dom', () => ({
22 useNavigate: () => navigateMock
23 }))
24
25 vi.mock('../../../src/renderer/src/lib/ipc', () => ({
26 ipc: {
27 onGenerateChunk: onGenerateChunkMock,
28 getSession: getSessionMock
29 }
30 }))
31
32 vi.mock('../../../src/renderer/src/store', () => ({
33 useToastStore: {
34 getState: () => toastMock
35 }
36 }))
37
38 vi.mock('../../../src/renderer/src/i18n', () => ({
39 useT: () => (key: string, params?: Record<string, string | number>) => {
40 if (key === 'generationNotifications.untitled') return '未命名'
41 if (key === 'generationNotifications.view') return '查看'
42 if (key === 'generationNotifications.completed') return `完成 ${params?.title ?? ''}`
43 if (key === 'generationNotifications.failed') return `失败 ${params?.title ?? ''}`
44 return key
45 }
46 }))
47
48 import { useGenerationNotifications } from '../../../src/renderer/src/hooks/useGenerationNotifications'
49
50 function Probe() {
51 useGenerationNotifications()
52 return null
53 }
54
55 const flushAsync = async (): Promise<void> => {
56 // The notify() path awaits readSessionTitle() then calls toast. Two microtask ticks cover
57 // the awaited ipc.getSession resolution and the subsequent state-mutating sync call.
58 await Promise.resolve()
59 await Promise.resolve()
60 }
61
62 describe('useGenerationNotifications', () => {
63 let container: HTMLDivElement
64 let captured: ((chunk: GenerateChunkEvent) => void) | null = null
65
66 beforeEach(() => {
67 container = document.createElement('div')
68 document.body.appendChild(container)
69 captured = null
70 onGenerateChunkMock.mockImplementation((cb: (chunk: GenerateChunkEvent) => void) => {
71 captured = cb
72 return () => {
73 captured = null
74 }
75 })
76 getSessionMock.mockResolvedValue({ session: { title: 'Deck' } })
77 Object.values(toastMock).forEach((mock) => mock.mockClear())
78 navigateMock.mockClear()
79 act(() => {
80 createRoot(container).render(React.createElement(Probe))
81 })
82 })
83
84 afterEach(() => {
85 container.remove()
86 })
87
88 it('fires the completion toast for a full-deck generation run', async () => {
89 expect(captured).toBeTruthy()
90 captured!({
91 type: 'run_completed',
92 payload: { runId: 'run-1', totalPages: 3, sessionId: 's1', activityKind: undefined }
93 })
94 await flushAsync()
95 expect(toastMock.success).toHaveBeenCalledTimes(1)
96 })
97
98 it('suppresses the toast for a page-beautify run (dedicated in-page toast handles it)', async () => {
99 captured!({
100 type: 'run_completed',
101 payload: { runId: 'run-2', totalPages: 1, sessionId: 's1', activityKind: 'page-beautify' }
102 })
103 await flushAsync()
104 expect(toastMock.success).not.toHaveBeenCalled()
105 })
106
107 it('suppresses the toast for page-edit, deck-edit, and style-switch runs', async () => {
108 const kinds = ['page-edit', 'deck-edit', 'style-switch'] as const
109 for (const activityKind of kinds) {
110 captured!({
111 type: 'run_completed',
112 payload: { runId: `run-${activityKind}`, totalPages: 1, sessionId: 's1', activityKind }
113 })
114 }
115 await flushAsync()
116 expect(toastMock.success).not.toHaveBeenCalled()
117 })
118
119 it('suppresses the error toast for a failed page-beautify run (session-detail owns the error path)', async () => {
120 captured!({
121 type: 'run_error',
122 payload: {
123 runId: 'run-fail',
124 sessionId: 's1',
125 activityKind: 'page-beautify',
126 message: 'boom'
127 }
128 })
129 await flushAsync()
130 expect(toastMock.error).not.toHaveBeenCalled()
131 })
132
133 it('suppresses the error toast for a failed single-page retry', async () => {
134 captured!({
135 type: 'run_error',
136 payload: {
137 runId: 'run-single-page-fail',
138 sessionId: 's1',
139 activityKind: 'single-page-retry',
140 message: 'boom'
141 }
142 })
143 await flushAsync()
144 expect(toastMock.error).not.toHaveBeenCalled()
145 })
146
147 it('suppresses the error toast for a failed generated-page addition', async () => {
148 captured!({
149 type: 'run_error',
150 payload: {
151 runId: 'run-add-page-fail',
152 sessionId: 's1',
153 activityKind: 'addPage',
154 message: 'boom'
155 }
156 })
157 await flushAsync()
158 expect(toastMock.error).not.toHaveBeenCalled()
159 })
160 })
161
161 lines TYPESCRIPT