返回 oh-my-ppt
sessions-page-rendering.test.ts
根目录 / tests / unit / session / sessions-page-rendering.test.ts
1 /**
2 * @vitest-environment happy-dom
3 */
4 import React, { act } from 'react'
5 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6 import { createRoot } from 'react-dom/client'
7 import { MemoryRouter } from 'react-router-dom'
8 import { SessionsPage } from '../../../src/renderer/src/pages/sessions'
9
10 const state = vi.hoisted(() => ({
11 fetchSessions: vi.fn(async () => undefined),
12 deleteSession: vi.fn(async () => undefined),
13 updateSessionTitle: vi.fn(async () => undefined),
14 importSessionFile: vi.fn(async () => ({ cancelled: true })),
15 createTemplateFromSession: vi.fn(async () => 'template-1'),
16 listActiveGenerateRuns: vi.fn(async () => []),
17 listActivePageEditRuns: vi.fn(async () => []),
18 listActiveDeckEditRuns: vi.fn(async () => []),
19 onGenerateChunk: vi.fn(() => () => undefined),
20 onHtmlThumbnailChanged: vi.fn(() => () => undefined)
21 }))
22
23 vi.mock('../../../src/renderer/src/store', () => ({
24 useSessionStore: () => ({
25 sessions: [
26 {
27 id: 'session-1',
28 title: 'Quarterly Review',
29 topic: 'Review',
30 styleId: 'style-1',
31 page_count: 6,
32 status: 'completed',
33 provider: 'openai',
34 model: 'model',
35 created_at: 1,
36 updated_at: 2,
37 metadata: '{}',
38 generated_count: 6,
39 failed_count: 0,
40 slideSizeId: 'wide-16-9',
41 slideWidth: 1600,
42 slideHeight: 900,
43 thumbnailPath: '/cache/session-1.png'
44 },
45 {
46 id: 'session-2',
47 title: 'Draft Session',
48 topic: 'Draft',
49 styleId: 'style-1',
50 page_count: 0,
51 status: 'active',
52 provider: 'openai',
53 model: 'model',
54 created_at: 1,
55 updated_at: 1,
56 metadata: '{}',
57 generated_count: 0,
58 failed_count: 0,
59 slideSizeId: 'vertical-9-16',
60 slideWidth: 900,
61 slideHeight: 1600,
62 thumbnailPath: null
63 }
64 ],
65 fetchSessions: state.fetchSessions,
66 deleteSession: state.deleteSession,
67 updateSessionTitle: state.updateSessionTitle,
68 importSessionFile: state.importSessionFile
69 }),
70 useTemplateStore: () => ({ createTemplateFromSession: state.createTemplateFromSession }),
71 useToastStore: () => ({
72 success: vi.fn(),
73 error: vi.fn()
74 })
75 }))
76 vi.mock('@renderer/lib/ipc', () => ({
77 ipc: {
78 listActiveGenerateRuns: state.listActiveGenerateRuns,
79 listActivePageEditRuns: state.listActivePageEditRuns,
80 listActiveDeckEditRuns: state.listActiveDeckEditRuns,
81 onGenerateChunk: state.onGenerateChunk,
82 onHtmlThumbnailChanged: state.onHtmlThumbnailChanged
83 }
84 }))
85 vi.mock('@renderer/i18n', () => ({ useT: () => (key: string) => key }))
86 vi.mock('../../../src/renderer/src/components/templates/SaveTemplateDialog', () => ({
87 SaveTemplateDialog: () => null
88 }))
89
90 const setInputValue = (input: HTMLInputElement, value: string): void => {
91 const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
92 setter?.call(input, value)
93 input.dispatchEvent(new Event('input', { bubbles: true }))
94 }
95
96 describe('SessionsPage rendering', () => {
97 beforeEach(() => vi.clearAllMocks())
98
99 afterEach(() => {
100 vi.restoreAllMocks()
101 document.body.innerHTML = ''
102 })
103
104 it('renders sessions in fixed-height cards with centered size-aware thumbnails', async () => {
105 const container = document.createElement('div')
106 document.body.appendChild(container)
107 const root = createRoot(container)
108 await act(async () => {
109 root.render(React.createElement(MemoryRouter, null, React.createElement(SessionsPage)))
110 await Promise.resolve()
111 })
112
113 const card = container.querySelector('[data-session-card-id="session-1"]')
114 expect(card).toBeTruthy()
115 expect(card?.parentElement?.className).toContain('grid-cols-2')
116 expect(card?.className).toContain('flex-col')
117 expect(card?.querySelector('[data-session-thumbnail-frame]')?.className).toContain('h-[230px]')
118 expect((card?.querySelector('img') as HTMLImageElement | null)?.style.aspectRatio).toBe(
119 '1600 / 900'
120 )
121 const portraitCard = container.querySelector('[data-session-card-id="session-2"]')
122 expect((portraitCard?.querySelector('img') as HTMLImageElement | null)?.style.aspectRatio).toBe(
123 '900 / 1600'
124 )
125 expect(card?.querySelectorAll('img')).toHaveLength(1)
126 expect(card?.querySelectorAll('iframe')).toHaveLength(0)
127 const placeholderImage = container.querySelector(
128 '[data-session-card-id="session-2"] img'
129 ) as HTMLImageElement | null
130 expect(placeholderImage?.src).toContain('space.webp')
131 expect(container.querySelectorAll('iframe')).toHaveLength(0)
132 expect(container.textContent).toContain('Quarterly Review')
133 expect(container.querySelector('button[aria-label="sessions.editTitleTooltip"]')).toBeTruthy()
134 expect(
135 container.querySelector('button[aria-label="sessions.saveTemplateTooltip"]')
136 ).toBeTruthy()
137 expect(container.querySelector('button[aria-label="common.delete"]')).toBeTruthy()
138
139 await act(async () => root.unmount())
140 })
141
142 it('filters sessions by title from the page search field', async () => {
143 const container = document.createElement('div')
144 document.body.appendChild(container)
145 const root = createRoot(container)
146 await act(async () => {
147 root.render(React.createElement(MemoryRouter, null, React.createElement(SessionsPage)))
148 await Promise.resolve()
149 })
150
151 const searchButton = container.querySelector(
152 'button[aria-label="sessions.searchButton"]'
153 ) as HTMLButtonElement | null
154 expect(searchButton).toBeTruthy()
155
156 await act(async () => {
157 searchButton!.click()
158 })
159
160 const searchInput = container.querySelector(
161 'input[placeholder="sessions.searchPlaceholder"]'
162 ) as HTMLInputElement | null
163 expect(searchInput).toBeTruthy()
164
165 await act(async () => {
166 setInputValue(searchInput!, 'quarter')
167 })
168
169 expect(container.textContent).toContain('Quarterly Review')
170 expect(container.querySelector('[data-session-card-id="session-2"]')).toBeNull()
171
172 await act(async () => {
173 setInputValue(searchInput!, 'missing')
174 })
175
176 expect(container.querySelector('[data-session-card-id="session-1"]')).toBeNull()
177 expect(container.querySelector('[data-session-card-id="session-2"]')).toBeNull()
178 expect(container.textContent).toContain('sessions.noSearchResultsTitle')
179
180 const clearButton = container.querySelector(
181 'button[aria-label="sessions.clearSearch"]'
182 ) as HTMLButtonElement | null
183 expect(clearButton).toBeTruthy()
184
185 await act(async () => {
186 clearButton!.click()
187 })
188
189 expect(container.querySelector('input[placeholder="sessions.searchPlaceholder"]')).toBeNull()
190 expect(container.querySelector('[data-session-card-id="session-1"]')).toBeTruthy()
191 expect(container.querySelector('[data-session-card-id="session-2"]')).toBeTruthy()
192 expect(container.textContent).toContain('Quarterly Review')
193 expect(container.textContent).toContain('Draft Session')
194
195 await act(async () => root.unmount())
196 })
197 })
198
198 lines TYPESCRIPT