返回 oh-my-ppt
style-view-rendering.test.ts
根目录 / tests / unit / session / style-view-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 { StyleView } from '../../../src/renderer/src/components/session-detail/style/StyleView'
8 import { useGenerateStore } from '../../../src/renderer/src/store/generateStore'
9 import { useSessionStore } from '../../../src/renderer/src/store/sessionStore'
10
11 const ipcMocks = vi.hoisted(() => ({
12 listStyles: vi.fn(),
13 switchSessionStyle: vi.fn(),
14 onHtmlThumbnailChanged: vi.fn(() => () => undefined)
15 }))
16 const translate = vi.hoisted(() => vi.fn((key: string) => key))
17
18 type ObserverEntry = Pick<IntersectionObserverEntry, 'target' | 'isIntersecting'>
19
20 class MockIntersectionObserver {
21 static instances: MockIntersectionObserver[] = []
22 readonly observed = new Set<Element>()
23
24 constructor(private readonly callback: IntersectionObserverCallback) {
25 MockIntersectionObserver.instances.push(this)
26 }
27
28 observe = (element: Element): void => {
29 this.observed.add(element)
30 }
31
32 unobserve = (element: Element): void => {
33 this.observed.delete(element)
34 }
35
36 disconnect = (): void => {
37 this.observed.clear()
38 }
39
40 emit(entries: ObserverEntry[]): void {
41 this.callback(entries as IntersectionObserverEntry[], this as unknown as IntersectionObserver)
42 }
43 }
44
45 function getObservedCard(observer: MockIntersectionObserver, styleId: string): Element {
46 const card = Array.from(observer.observed).find(
47 (element) => (element as HTMLElement).dataset.styleCardId === styleId
48 )
49 if (!card) throw new Error(`Expected observed style card ${styleId}`)
50 return card
51 }
52
53 vi.mock('@renderer/lib/ipc', () => ({ ipc: ipcMocks }))
54 vi.mock('@renderer/i18n', () => ({ useT: () => translate }))
55 vi.mock('@renderer/hooks/useModelAction', () => ({
56 useModelAction: () => ({ selectedModelConfigId: 'model-1', ensureModelActive: vi.fn() })
57 }))
58
59 describe('StyleView preview rendering', () => {
60 beforeEach(() => {
61 vi.spyOn(console, 'error').mockImplementation(() => undefined)
62 MockIntersectionObserver.instances = []
63 vi.stubGlobal('IntersectionObserver', MockIntersectionObserver)
64 ipcMocks.listStyles.mockResolvedValue({
65 items: Array.from({ length: 10 }, (_, index) => ({
66 id: `style-${index + 1}`,
67 label: `Style ${index + 1}`,
68 description: `Description ${index + 1}`,
69 category: 'test',
70 previewPath: `/styles/style-${index + 1}/preview.html`,
71 thumbnailPath: index === 0 ? '/thumbnails/style-1.png' : null,
72 updatedAt: 10 - index
73 }))
74 })
75 useGenerateStore.getState().reset()
76 useSessionStore.getState().setCurrentSession({
77 id: 'session-1',
78 title: 'Session',
79 topic: null,
80 styleId: 'style-1',
81 page_count: null,
82 status: 'completed',
83 provider: '',
84 model: '',
85 created_at: 0,
86 updated_at: 0,
87 metadata: null
88 })
89 })
90
91 afterEach(() => {
92 vi.unstubAllGlobals()
93 vi.restoreAllMocks()
94 useSessionStore.getState().resetRuntimeState()
95 document.body.innerHTML = ''
96 })
97
98 it('prefers PNG thumbnails and caps visible iframe placeholders at eight', async () => {
99 const container = document.createElement('div')
100 document.body.appendChild(container)
101 const root = createRoot(container)
102 await act(async () => {
103 root.render(React.createElement(StyleView, { sessionId: 'session-1' }))
104 await Promise.resolve()
105 })
106
107 try {
108 expect(ipcMocks.listStyles).toHaveBeenCalledWith({ sessionId: 'session-1' })
109 expect(container.querySelectorAll('img')).toHaveLength(1)
110 expect(container.querySelectorAll('iframe')).toHaveLength(0)
111
112 const observer = MockIntersectionObserver.instances[0]
113 await act(async () => {
114 observer.emit(
115 Array.from({ length: 9 }, (_, index) => ({
116 target: getObservedCard(observer, `style-${index + 2}`),
117 isIntersecting: true
118 }))
119 )
120 })
121 expect(container.querySelectorAll('[data-testid="style-preview-iframe"]')).toHaveLength(8)
122 for (const iframe of container.querySelectorAll('[data-testid="style-preview-iframe"]')) {
123 expect(iframe.getAttribute('sandbox')).toBe('')
124 }
125 expect(
126 container.querySelector('[data-style-card-id="style-2"] iframe')
127 ).toBeNull()
128 const checkedBox = container.querySelector(
129 '[data-testid="style-selection-checkbox"][data-state="checked"]'
130 )
131 expect(
132 (checkedBox?.closest('[data-style-card-id]') as HTMLElement | null)?.dataset.styleCardId
133 ).toBe('style-1')
134 } finally {
135 await act(async () => root.unmount())
136 container.remove()
137 }
138 })
139 })
140
140 lines TYPESCRIPT