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