返回 oh-my-ppt
master-service.test.ts
根目录 / tests / unit / master / master-service.test.ts
1 import { afterEach, describe, expect, it } from 'vitest'
2 import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3 import os from 'node:os'
4 import path from 'node:path'
5 import {
6 createSessionMasterIfMissing,
7 getSessionMasterHtmlPath,
8 getSessionMasterLayoutsPath,
9 getSessionMasterPath,
10 readSessionLayoutLibrary,
11 readSessionMaster,
12 writeSessionLayoutLibrary,
13 writeSessionMaster
14 } from '../../../src/main/session/master-service'
15 import { createDefaultMasterGradient } from '../../../src/shared/master'
16
17 const roots: string[] = []
18
19 afterEach(async () => {
20 await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
21 })
22
23 describe('session master file service', () => {
24 it('reads a missing master in memory without creating a file', async () => {
25 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
26 roots.push(projectDir)
27
28 const result = await readSessionMaster(projectDir)
29
30 expect(result.exists).toBe(false)
31 expect(result.config.backgroundColor).toBe('#ffffff')
32 await expect(readFile(getSessionMasterPath(projectDir), 'utf8')).rejects.toMatchObject({
33 code: 'ENOENT'
34 })
35 })
36
37 it('reads a missing layout library in memory without creating a file', async () => {
38 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
39 roots.push(projectDir)
40
41 const result = await readSessionLayoutLibrary(projectDir)
42
43 expect(result.exists).toBe(false)
44 expect(result.library.mappings.cover).toBe('cover-statement')
45 await expect(readFile(getSessionMasterLayoutsPath(projectDir), 'utf8')).rejects.toMatchObject({
46 code: 'ENOENT'
47 })
48 })
49
50 it('ignores an unrelated root-level master.css without modifying it', async () => {
51 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
52 roots.push(projectDir)
53 const rootMasterPath = path.join(projectDir, 'master.css')
54 const rootMasterCss = ':root { --ppt-page-bg: #112233; }'
55 await writeFile(rootMasterPath, rootMasterCss, 'utf8')
56
57 const result = await readSessionMaster(projectDir)
58
59 expect(result.exists).toBe(false)
60 expect(result.config.backgroundColor).toBe('#ffffff')
61 expect(await readFile(rootMasterPath, 'utf8')).toBe(rootMasterCss)
62
63 await writeSessionMaster(projectDir, result.config)
64 expect(await readFile(rootMasterPath, 'utf8')).toBe(rootMasterCss)
65 })
66
67 it('creates once and atomically replaces only structured CSS', async () => {
68 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
69 roots.push(projectDir)
70
71 const created = await createSessionMasterIfMissing(projectDir)
72 await writeFile(
73 getSessionMasterPath(projectDir),
74 `${created.css}/* preserved only until save */`,
75 'utf8'
76 )
77 const saved = await writeSessionMaster(projectDir, {
78 backgroundColor: '#112233',
79 backgroundMode: 'override',
80 backgroundStyle: 'solid',
81 backgroundGradient: createDefaultMasterGradient(),
82 backgroundImage: null,
83 titleFontPreset: 'sans',
84 bodyFontPreset: 'inherit',
85 titleFontFamily: null,
86 bodyFontFamily: null,
87 titleFontSize: null,
88 bodyFontSize: null,
89 elements: {
90 logoImage: null,
91 footerText: 'Confidential',
92 watermarkText: '',
93 showPageNumber: true
94 }
95 })
96
97 expect(saved).toMatchObject({
98 exists: true,
99 config: {
100 backgroundColor: '#112233',
101 backgroundMode: 'override',
102 backgroundStyle: 'solid',
103 backgroundGradient: createDefaultMasterGradient(),
104 backgroundImage: null,
105 titleFontPreset: 'sans',
106 bodyFontPreset: 'inherit',
107 titleFontFamily: null,
108 bodyFontFamily: null,
109 titleFontSize: null,
110 bodyFontSize: null,
111 elements: {
112 logoImage: null,
113 footerText: 'Confidential',
114 watermarkText: '',
115 showPageNumber: true
116 }
117 }
118 })
119 const onDisk = await readFile(getSessionMasterPath(projectDir), 'utf8')
120 expect(onDisk).toBe(saved.css)
121 expect(onDisk).not.toContain('preserved only until save')
122 expect(await readFile(getSessionMasterHtmlPath(projectDir), 'utf8')).toContain(
123 'data-ppt-master-elements="1"'
124 )
125 expect(
126 JSON.parse(await readFile(getSessionMasterLayoutsPath(projectDir), 'utf8'))
127 ).toMatchObject({
128 version: 1,
129 mappings: { cover: 'cover-statement' }
130 })
131 })
132
133 it('persists only normalized, compatible layout mappings', async () => {
134 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
135 roots.push(projectDir)
136
137 const saved = await writeSessionLayoutLibrary(projectDir, {
138 version: 1,
139 mappings: { cover: 'cover-split', comparison: 'image-spotlight' }
140 })
141
142 expect(saved).toMatchObject({
143 exists: true,
144 library: { mappings: { cover: 'cover-split', comparison: 'comparison-versus' } }
145 })
146 expect(await readSessionLayoutLibrary(projectDir)).toMatchObject({
147 exists: true,
148 library: { mappings: { cover: 'cover-split', comparison: 'comparison-versus' } }
149 })
150 })
151
152 it('keeps logo image paths relative to the page document and restores them for the UI model', async () => {
153 const projectDir = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-master-'))
154 roots.push(projectDir)
155
156 await writeSessionMaster(projectDir, {
157 backgroundColor: '#ffffff',
158 backgroundMode: 'inherit',
159 backgroundStyle: 'solid',
160 backgroundGradient: createDefaultMasterGradient(),
161 backgroundImage: null,
162 titleFontPreset: 'inherit',
163 bodyFontPreset: 'inherit',
164 titleFontFamily: null,
165 bodyFontFamily: null,
166 titleFontSize: null,
167 bodyFontSize: null,
168 elements: {
169 logoImage: './images/brand.png',
170 footerText: 'Acme',
171 watermarkText: '',
172 showPageNumber: true
173 }
174 })
175
176 expect(await readFile(getSessionMasterHtmlPath(projectDir), 'utf8')).toContain(
177 'src="./images/brand.png"'
178 )
179 expect((await readSessionMaster(projectDir)).config.elements).toMatchObject({
180 logoImage: './images/brand.png'
181 })
182 })
183 })
184
184 lines TYPESCRIPT